feat: add --auto-connect flag to discover and connect to running Chrome (#432)

This commit is contained in:
Chris Tate
2026-02-12 18:37:37 -06:00
committed by GitHub
parent 9c20979bfe
commit 9a01e8b3b5
12 changed files with 283 additions and 5 deletions
+147 -3
View File
@@ -16,7 +16,7 @@ import {
} from 'playwright-core';
import path from 'node:path';
import os from 'node:os';
import { existsSync, mkdirSync, rmSync } from 'node:fs';
import { existsSync, mkdirSync, rmSync, readFileSync } from 'node:fs';
import type { LaunchCommand } from './types.js';
import { type RefMap, type EnhancedSnapshot, getEnhancedSnapshot, parseRef } from './snapshot.js';
@@ -1082,10 +1082,14 @@ export class BrowserManager {
if (this.isLaunched()) {
const needsRelaunch =
(!cdpEndpoint && this.cdpEndpoint !== null) ||
(!!cdpEndpoint && this.needsCdpReconnect(cdpEndpoint));
(!cdpEndpoint && !options.autoConnect && this.cdpEndpoint !== null) ||
(!!cdpEndpoint && this.needsCdpReconnect(cdpEndpoint)) ||
(!!options.autoConnect && !this.isCdpConnectionAlive());
if (needsRelaunch) {
await this.close();
} else if (options.autoConnect && this.isCdpConnectionAlive()) {
// Already connected via auto-connect, no need to reconnect
return;
} else {
return;
}
@@ -1096,6 +1100,11 @@ export class BrowserManager {
return;
}
if (options.autoConnect) {
await this.autoConnectViaCDP();
return;
}
// Cloud browser providers require explicit opt-in via -p flag or AGENT_BROWSER_PROVIDER env var
// -p flag takes precedence over env var
const provider = options.provider ?? process.env.AGENT_BROWSER_PROVIDER;
@@ -1283,6 +1292,141 @@ export class BrowserManager {
}
}
/**
* Get Chrome's default user data directory paths for the current platform.
* Returns an array of candidate paths to check (stable, then beta/canary).
*/
private getChromeUserDataDirs(): string[] {
const home = os.homedir();
const platform = os.platform();
if (platform === 'darwin') {
return [
path.join(home, 'Library', 'Application Support', 'Google', 'Chrome'),
path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Canary'),
path.join(home, 'Library', 'Application Support', 'Chromium'),
];
} else if (platform === 'win32') {
const localAppData = process.env.LOCALAPPDATA ?? path.join(home, 'AppData', 'Local');
return [
path.join(localAppData, 'Google', 'Chrome', 'User Data'),
path.join(localAppData, 'Google', 'Chrome SxS', 'User Data'),
path.join(localAppData, 'Chromium', 'User Data'),
];
} else {
// Linux
return [
path.join(home, '.config', 'google-chrome'),
path.join(home, '.config', 'google-chrome-unstable'),
path.join(home, '.config', 'chromium'),
];
}
}
/**
* Try to read the DevToolsActivePort file from a Chrome user data directory.
* Returns { port, wsPath } if found, or null if not available.
*/
private readDevToolsActivePort(userDataDir: string): { port: number; wsPath: string } | null {
const filePath = path.join(userDataDir, 'DevToolsActivePort');
try {
if (!existsSync(filePath)) return null;
const content = readFileSync(filePath, 'utf-8').trim();
const lines = content.split('\n');
if (lines.length < 2) return null;
const port = parseInt(lines[0].trim(), 10);
const wsPath = lines[1].trim();
if (isNaN(port) || port <= 0 || port > 65535) return null;
if (!wsPath) return null;
return { port, wsPath };
} catch {
return null;
}
}
/**
* Try to discover a Chrome CDP endpoint by querying an HTTP debug port.
* Returns the WebSocket debugger URL if available.
*/
private async probeDebugPort(port: number): Promise<string | null> {
try {
const response = await fetch(`http://127.0.0.1:${port}/json/version`, {
signal: AbortSignal.timeout(2000),
});
if (!response.ok) return null;
const data = (await response.json()) as { webSocketDebuggerUrl?: string };
return data.webSocketDebuggerUrl ?? null;
} catch {
return null;
}
}
/**
* Auto-discover and connect to a running Chrome/Chromium instance.
*
* Discovery strategy:
* 1. Read DevToolsActivePort from Chrome's default user data directories
* 2. If found, connect using the port and WebSocket path from that file
* 3. If not found, probe common debugging ports (9222, 9229)
* 4. If a port responds, connect via CDP
*/
private async autoConnectViaCDP(): Promise<void> {
// Strategy 1: Check DevToolsActivePort files
const userDataDirs = this.getChromeUserDataDirs();
for (const dir of userDataDirs) {
const activePort = this.readDevToolsActivePort(dir);
if (activePort) {
// Verify the port is actually responding
const wsUrl = await this.probeDebugPort(activePort.port);
if (wsUrl) {
// Connect using the discovered WebSocket URL
await this.connectViaCDP(wsUrl);
return;
}
// Port from file exists but not responding; try HTTP endpoint directly
const httpUrl = `http://127.0.0.1:${activePort.port}`;
try {
await this.connectViaCDP(httpUrl);
return;
} catch {
// Port listed but not connectable, try next directory
}
}
}
// Strategy 2: Probe common debugging ports
const commonPorts = [9222, 9229];
for (const port of commonPorts) {
const wsUrl = await this.probeDebugPort(port);
if (wsUrl) {
await this.connectViaCDP(wsUrl);
return;
}
}
// Nothing found
const platform = os.platform();
let hint: string;
if (platform === 'darwin') {
hint =
'Start Chrome with: /Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=9222\n' +
'Or enable remote debugging in Chrome 144+ at chrome://inspect/#remote-debugging';
} else if (platform === 'win32') {
hint =
'Start Chrome with: chrome.exe --remote-debugging-port=9222\n' +
'Or enable remote debugging in Chrome 144+ at chrome://inspect/#remote-debugging';
} else {
hint =
'Start Chrome with: google-chrome --remote-debugging-port=9222\n' +
'Or enable remote debugging in Chrome 144+ at chrome://inspect/#remote-debugging';
}
throw new Error(`No running Chrome instance with remote debugging found.\n${hint}`);
}
/**
* Set up console, error, and close tracking for a page
*/
+1
View File
@@ -31,6 +31,7 @@ const launchSchema = baseCommandSchema.extend({
{ message: 'CDP URL must start with ws://, wss://, http://, or https://' }
)
.optional(),
autoConnect: z.boolean().optional(),
executablePath: z.string().optional(),
extensions: z.array(z.string()).optional(),
headers: z.record(z.string()).optional(),
+1
View File
@@ -16,6 +16,7 @@ export interface LaunchCommand extends BaseCommand {
executablePath?: string;
cdpPort?: number;
cdpUrl?: string;
autoConnect?: boolean; // Auto-discover and connect to running Chrome via DevToolsActivePort
extensions?: string[];
profile?: string; // Path to persistent browser profile directory
storageState?: string; // Path to storage state JSON file