Skip to content
This repository has been archived by the owner on Sep 16, 2024. It is now read-only.

Commit

Permalink
feat: chrome remote debugger over websocket
Browse files Browse the repository at this point in the history
  • Loading branch information
soundofspace committed May 3, 2024
1 parent 6b59b42 commit ddddbc1
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 13 deletions.
1 change: 1 addition & 0 deletions agent/main/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
17 changes: 15 additions & 2 deletions agent/main/lib/Browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export default class Browser extends TypedEventEmitter<IBrowserEvents> implement
browserUserConfig ??= {};
browserUserConfig.disableGpu ??= env.disableGpu;
browserUserConfig.noChromeSandbox ??= env.noChromeSandbox;
browserUserConfig.useRemoteDebuggingPort ??= env.useRemoteDebuggingPort;
browserUserConfig.showChrome ??= env.showChrome;

this.applyDefaultLaunchArgs(browserUserConfig);
Expand Down Expand Up @@ -142,12 +143,18 @@ export default class Browser extends TypedEventEmitter<IBrowserEvents> 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();
Expand Down Expand Up @@ -367,8 +374,14 @@ export default class Browser extends TypedEventEmitter<IBrowserEvents> 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) {
Expand Down
25 changes: 22 additions & 3 deletions agent/main/lib/BrowserProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>();
public launchStderr: string[] = [];
private processKilled = false;
private readonly launchedProcess: ChildProcess;
private remoteDebuggingUrl?: Resolvable<string>;

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<string>();
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));
Expand Down Expand Up @@ -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) {
Expand Down
75 changes: 75 additions & 0 deletions agent/main/lib/WebsocketTransport.ts
Original file line number Diff line number Diff line change
@@ -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<void>();
public isClosed = false;

private events = new EventSubscriber();
private webSocket?: WebSocket;

constructor(urlPromise: Promise<string>) {
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 });
}
});
}
}


3 changes: 2 additions & 1 deletion agent/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions specification/agent/browser/IBrowserEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export default interface IBrowserEngine {

isHeaded?: boolean;
isHeadlessNew?: boolean;
useRemoteDebuggingPort?: boolean;
verifyLaunchable?(): Promise<any>;
}
1 change: 1 addition & 0 deletions specification/agent/browser/IBrowserUserConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export default interface IBrowserUserConfig {
disableIncognito?: boolean;
disableMitm?: boolean;
noChromeSandbox?: boolean;
useRemoteDebuggingPort?: boolean;
}
40 changes: 33 additions & 7 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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" "*"

Expand Down Expand Up @@ -2236,6 +2236,17 @@
progress "^2.0.3"
tar "^6.1.11"

"@ulixee/[email protected]":
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"
Expand Down Expand Up @@ -3166,7 +3177,7 @@ [email protected]:
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==
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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==
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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==

[email protected]:
version "4.0.0"
resolved "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz"
Expand Down

0 comments on commit ddddbc1

Please sign in to comment.