From ddddbc12bc5c8ee0573ce7e0a97271f8cf3e98e1 Mon Sep 17 00:00:00 2001 From: brecht stamper Date: Tue, 30 Apr 2024 15:01:20 +0200 Subject: [PATCH] feat: chrome remote debugger over websocket --- agent/main/env.ts | 1 + agent/main/lib/Browser.ts | 17 ++++- agent/main/lib/BrowserProcess.ts | 25 ++++++- agent/main/lib/WebsocketTransport.ts | 75 +++++++++++++++++++ agent/main/package.json | 3 +- specification/agent/browser/IBrowserEngine.ts | 1 + .../agent/browser/IBrowserUserConfig.ts | 1 + yarn.lock | 40 ++++++++-- 8 files changed, 150 insertions(+), 13 deletions(-) create mode 100644 agent/main/lib/WebsocketTransport.ts diff --git a/agent/main/env.ts b/agent/main/env.ts index 9d2d9cd4c..1759760df 100644 --- a/agent/main/env.ts +++ b/agent/main/env.ts @@ -8,6 +8,7 @@ export default { disableMitm: parseEnvBool(env.ULX_DISABLE_MITM), showChrome: parseEnvBool(env.ULX_SHOW_CHROME), noChromeSandbox: parseEnvBool(env.ULX_NO_CHROME_SANDBOX), + useRemoteDebuggingPort: parseEnvBool(env.ULX_USE_REMOTE_DEBUGGING_PORT), disableGpu: parseEnvBool(env.ULX_DISABLE_GPU), enableHeadlessNewMode: parseEnvBool(env.ULX_ENABLE_HEADLESS_NEW), defaultChromeId: diff --git a/agent/main/lib/Browser.ts b/agent/main/lib/Browser.ts index ac59c8b32..502639849 100755 --- a/agent/main/lib/Browser.ts +++ b/agent/main/lib/Browser.ts @@ -88,6 +88,7 @@ export default class Browser extends TypedEventEmitter implement browserUserConfig ??= {}; browserUserConfig.disableGpu ??= env.disableGpu; browserUserConfig.noChromeSandbox ??= env.noChromeSandbox; + browserUserConfig.useRemoteDebuggingPort ??= env.useRemoteDebuggingPort; browserUserConfig.showChrome ??= env.showChrome; this.applyDefaultLaunchArgs(browserUserConfig); @@ -142,12 +143,18 @@ export default class Browser extends TypedEventEmitter implement try { this.setUserDataDir(); this.process = new BrowserProcess(this.engine); + this.connection = new Connection(this.process.transport); this.devtoolsSession = this.connection.rootSession; this.bindDevtoolsEvents(); - await Promise.all([this.testConnection(), this.process.isProcessFunctionalPromise]); + // Pipe transport needs data send to detect if it is connected/functional + this.process.transport.send(''); + await this.process.isProcessFunctionalPromise; + // Needs to be after isProcessFunctionalPromise to make sure our transport is ready + await this.testConnection(); + this.process.once('close', () => this.emit('close')); this.launchPromise.resolve(); @@ -367,8 +374,14 @@ export default class Browser extends TypedEventEmitter implement launchArgs.push('--no-sandbox'); } } + if (options.useRemoteDebuggingPort) { + this.engine.useRemoteDebuggingPort = true; + launchArgs.push('--remote-debugging-port=0'); + } else { + launchArgs.push('--remote-debugging-pipe'); + } - launchArgs.push('--remote-debugging-pipe', '--ignore-certificate-errors'); + launchArgs.push('--ignore-certificate-errors'); this.engine.isHeaded = options.showChrome === true; if (!this.engine.isHeaded) { diff --git a/agent/main/lib/BrowserProcess.ts b/agent/main/lib/BrowserProcess.ts index 648d36703..f0b044c64 100755 --- a/agent/main/lib/BrowserProcess.ts +++ b/agent/main/lib/BrowserProcess.ts @@ -11,25 +11,36 @@ import { arch } from 'os'; import ShutdownHandler from '@ulixee/commons/lib/ShutdownHandler'; import { PipeTransport } from './PipeTransport'; import env from '../env'; +import { WebsocketTransport } from './WebsocketTransport'; const { log } = Log(module); export default class BrowserProcess extends TypedEventEmitter<{ close: void }> { - public readonly transport: PipeTransport; + public readonly transport: PipeTransport | WebsocketTransport; public isProcessFunctionalPromise = new Resolvable(); public launchStderr: string[] = []; private processKilled = false; private readonly launchedProcess: ChildProcess; + private remoteDebuggingUrl?: Resolvable; - constructor(private browserEngine: IBrowserEngine, private processEnv?: NodeJS.ProcessEnv) { + constructor( + private browserEngine: IBrowserEngine, + private processEnv?: NodeJS.ProcessEnv, + ) { super(); bindFunctions(this); this.launchedProcess = this.launch(); this.bindProcessEvents(); - this.transport = new PipeTransport(this.launchedProcess); + if (browserEngine.useRemoteDebuggingPort) { + this.remoteDebuggingUrl = new Resolvable(); + this.transport = new WebsocketTransport(this.remoteDebuggingUrl.promise); + } else { + this.transport = new PipeTransport(this.launchedProcess); + } + this.transport.connectedPromise .then(() => this.isProcessFunctionalPromise.resolve(true)) .catch(err => setTimeout(() => this.isProcessFunctionalPromise.reject(err), 1.1e3)); @@ -88,6 +99,14 @@ export default class BrowserProcess extends TypedEventEmitter<{ close: void }> { }); readline.createInterface({ input: stderr }).on('line', line => { if (!line) return; + + if (this.remoteDebuggingUrl?.isResolved === false) { + const match = line.match(/DevTools listening on (.*)/); + if (match) { + this.remoteDebuggingUrl.resolve(match[1].trim()); + } + } + this.launchStderr.push(line); // don't grow in perpetuity! if (this.launchStderr.length > 100) { diff --git a/agent/main/lib/WebsocketTransport.ts b/agent/main/lib/WebsocketTransport.ts new file mode 100644 index 000000000..90a3aecf1 --- /dev/null +++ b/agent/main/lib/WebsocketTransport.ts @@ -0,0 +1,75 @@ +import Log from '@ulixee/commons/lib/Logger'; +import Resolvable from '@ulixee/commons/lib/Resolvable'; +import * as WebSocket from 'ws'; +import EventSubscriber from '@ulixee/commons/lib/EventSubscriber'; +import IConnectionTransport from '../interfaces/IConnectionTransport'; + +const { log } = Log(module); + +export class WebsocketTransport implements IConnectionTransport { + public get url(): string { + return this.webSocket.url; + } + + public onMessageFn: (message: string) => void; + public readonly onCloseFns: (() => void)[] = []; + public connectedPromise = new Resolvable(); + public isClosed = false; + + private events = new EventSubscriber(); + private webSocket?: WebSocket; + + constructor(urlPromise: Promise) { + urlPromise + .then(url => this.connect(url)) + .catch(error => { + if (!this.connectedPromise.isResolved) this.connectedPromise.reject(error); + }); + } + + send(message: string): boolean { + if (this.webSocket?.readyState === WebSocket.OPEN) { + this.webSocket.send(message); + return true; + } + return false; + } + + close(): void { + this.isClosed = true; + this.events.close(); + try { + this.webSocket?.close(); + } catch {} + } + + private onClosed(): void { + log.stats('WebSocketTransport.Closed'); + for (const close of this.onCloseFns) close(); + } + + private onMessage(event: string): void { + this.onMessageFn?.(event); + } + + private connect(url: string): void { + url = url.replace('localhost', '127.0.0.1'); + this.webSocket = new WebSocket(url, [], { + perMessageDeflate: false, + followRedirects: true, + }); + this.webSocket.once('open', this.connectedPromise.resolve); + this.webSocket.once('error', err => this.connectedPromise.reject(err, true)); + this.events.on(this.webSocket, 'message', this.onMessage.bind(this)); + this.events.once(this.webSocket, 'close', this.onClosed.bind(this)); + this.events.once(this.webSocket, 'error', error => { + if (!this.connectedPromise.isResolved) this.connectedPromise.reject(error, true); + if (this.isClosed) return; + if (error.code !== 'EPIPE') { + log.error('WebsocketTransport.error', { error, sessionId: null }); + } + }); + } +} + + diff --git a/agent/main/package.json b/agent/main/package.json index d7473f4e9..6f289cc64 100644 --- a/agent/main/package.json +++ b/agent/main/package.json @@ -12,7 +12,8 @@ "@ulixee/unblocked-specification": "2.0.0-alpha.28", "devtools-protocol": "^0.0.1137505", "nanoid": "^3.3.6", - "tough-cookie": "^4.1.3" + "tough-cookie": "^4.1.3", + "ws": "^8.17.0" }, "devDependencies": { "@ulixee/unblocked-agent-testing": "2.0.0-alpha.28", diff --git a/specification/agent/browser/IBrowserEngine.ts b/specification/agent/browser/IBrowserEngine.ts index 29cf748ff..b52c534fb 100644 --- a/specification/agent/browser/IBrowserEngine.ts +++ b/specification/agent/browser/IBrowserEngine.ts @@ -10,5 +10,6 @@ export default interface IBrowserEngine { isHeaded?: boolean; isHeadlessNew?: boolean; + useRemoteDebuggingPort?: boolean; verifyLaunchable?(): Promise; } diff --git a/specification/agent/browser/IBrowserUserConfig.ts b/specification/agent/browser/IBrowserUserConfig.ts index 85b564ccd..915958987 100644 --- a/specification/agent/browser/IBrowserUserConfig.ts +++ b/specification/agent/browser/IBrowserUserConfig.ts @@ -6,4 +6,5 @@ export default interface IBrowserUserConfig { disableIncognito?: boolean; disableMitm?: boolean; noChromeSandbox?: boolean; + useRemoteDebuggingPort?: boolean; } diff --git a/yarn.lock b/yarn.lock index 910463936..60201bf17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1762,10 +1762,10 @@ dependencies: "@babel/types" "^7.20.7" -"@types/better-sqlite3@^7.6.9": - version "7.6.9" - resolved "https://registry.yarnpkg.com/@types/better-sqlite3/-/better-sqlite3-7.6.9.tgz#4bff3eb7c5eaaae26f8099606c69279146561c50" - integrity sha512-FvktcujPDj9XKMJQWFcl2vVl7OdRIqsSRX9b0acWwTmwLK9CF2eqo/FRcmMLNpugKoX/avA6pb7TorDLmpgTnQ== +"@types/better-sqlite3@^7.6.3": + version "7.6.10" + resolved "https://registry.yarnpkg.com/@types/better-sqlite3/-/better-sqlite3-7.6.10.tgz#1818e56490953404acfd44cdde0464f201be6105" + integrity sha512-TZBjD+yOsyrUJGmcUj6OS3JADk3+UZcNv3NOBqGkM09bZdi28fNZw8ODqbMOLfKCu7RYCO62/ldq1iHbzxqoPw== dependencies: "@types/node" "*" @@ -2236,6 +2236,17 @@ progress "^2.0.3" tar "^6.1.11" +"@ulixee/commons@2.0.0-alpha.28": + version "2.0.0-alpha.28" + resolved "https://registry.yarnpkg.com/@ulixee/commons/-/commons-2.0.0-alpha.28.tgz#52c3401fbbb25563ccb93d1e02980de83780f44d" + integrity sha512-/WXy+4yRr7GMCZYuYiqpiv4qEhfOrgDL8jZ85H4I+uyPIxgSTk8TWujHp5XkieE/4AbIwb8I4Q/lE+0TKKPGUQ== + dependencies: + "@jridgewell/trace-mapping" "^0.3.18" + bech32 "^2.0.0" + devtools-protocol "^0.0.1137505" + https-proxy-agent "^5.0.0" + semver "^7.3.7" + "@ulixee/repo-tools@^1.0.29": version "1.0.29" resolved "https://registry.yarnpkg.com/@ulixee/repo-tools/-/repo-tools-1.0.29.tgz#aa90ea63b8bbfa7a84ac081988ae3d6acee00fe1" @@ -3166,7 +3177,7 @@ commander@2.11.x: resolved "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz" integrity sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ== -commander@^9.5.0: +commander@^9.3.0, commander@^9.5.0: version "9.5.0" resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== @@ -3677,6 +3688,11 @@ devtools-protocol@^0.0.1137505: resolved "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1137505.tgz" integrity sha512-etlSdcQy8DiTCw5oV/AaQiEqEDMCHTGRcMpsqzlKUQQdC/AKadVNbN7GTVAwFOKtMo4i907DczhNkXebiZe85g== +devtools-protocol@^0.0.981744: + version "0.0.981744" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.981744.tgz#9960da0370284577d46c28979a0b32651022bacf" + integrity sha512-0cuGS8+jhR67Fy7qG3i3Pc7Aw494sb9yG9QgpG97SFVWwolgYjlhJg7n+UaHxOQT30d1TYu/EYe9k01ivLErIg== + dezalgo@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" @@ -8163,7 +8179,7 @@ sort-keys@^4.0.0: dependencies: is-plain-obj "^2.0.0" -source-map-js@^1.0.2: +source-map-js@^1.0.1, source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== @@ -8876,6 +8892,11 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== +typescript@^4.7.4: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== + typescript@^5.3.3, typescript@~5.3.3: version "5.3.3" resolved "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz" @@ -9274,11 +9295,16 @@ ws@>=8.7.0: resolved "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz" integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== -ws@^7.2.0, ws@^7.5.9: +ws@^7.2.0, ws@^7.4.6, ws@^7.5.9: version "7.5.9" resolved "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +ws@^8.17.0: + version "8.17.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea" + integrity sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow== + xregexp@4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz"