chore(extension): find installed chrome (#728)

This commit is contained in:
Yury Semikhatsky 2025-07-21 17:57:38 -07:00 committed by GitHub
parent f1826b96b6
commit cfcca40b90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 31 additions and 15 deletions

View File

@ -23,13 +23,15 @@
*/ */
import http from 'http'; import http from 'http';
import { promisify } from 'util'; import { spawn } from 'child_process';
import { exec } from 'child_process';
import { WebSocket, WebSocketServer } from 'ws'; import { WebSocket, WebSocketServer } from 'ws';
import debug from 'debug'; import debug from 'debug';
import * as playwright from 'playwright'; import * as playwright from 'playwright';
import { httpAddressToString, startHttpServer } from '../transport.js'; import { httpAddressToString, startHttpServer } from '../transport.js';
import { BrowserContextFactory } from '../browserContextFactory.js'; import { BrowserContextFactory } from '../browserContextFactory.js';
// @ts-ignore
const { registry } = await import('playwright-core/lib/server/registry/index');
import type websocket from 'ws'; import type websocket from 'ws';
const debugLogger = debug('pw:mcp:relay'); const debugLogger = debug('pw:mcp:relay');
@ -52,6 +54,7 @@ type CDPResponse = {
export class CDPRelayServer { export class CDPRelayServer {
private _wsHost: string; private _wsHost: string;
private _browserChannel: string;
private _cdpPath: string; private _cdpPath: string;
private _extensionPath: string; private _extensionPath: string;
private _wss: WebSocketServer; private _wss: WebSocketServer;
@ -65,8 +68,9 @@ export class CDPRelayServer {
private _extensionConnectionPromise: Promise<void>; private _extensionConnectionPromise: Promise<void>;
private _extensionConnectionResolve: (() => void) | null = null; private _extensionConnectionResolve: (() => void) | null = null;
constructor(server: http.Server) { constructor(server: http.Server, browserChannel: string) {
this._wsHost = httpAddressToString(server.address()).replace(/^http/, 'ws'); this._wsHost = httpAddressToString(server.address()).replace(/^http/, 'ws');
this._browserChannel = browserChannel;
const uuid = crypto.randomUUID(); const uuid = crypto.randomUUID();
this._cdpPath = `/cdp/${uuid}`; this._cdpPath = `/cdp/${uuid}`;
@ -88,10 +92,13 @@ export class CDPRelayServer {
} }
async ensureExtensionConnectionForMCPContext(clientInfo: { name: string, version: string }) { async ensureExtensionConnectionForMCPContext(clientInfo: { name: string, version: string }) {
debugLogger('Ensuring extension connection for MCP context');
if (this._extensionConnection) if (this._extensionConnection)
return; return;
await this._connectBrowser(clientInfo); await this._connectBrowser(clientInfo);
debugLogger('Waiting for incoming extension connection');
await this._extensionConnectionPromise; await this._extensionConnectionPromise;
debugLogger('Extension connection established');
} }
private async _connectBrowser(clientInfo: { name: string, version: string }) { private async _connectBrowser(clientInfo: { name: string, version: string }) {
@ -101,12 +108,19 @@ export class CDPRelayServer {
url.searchParams.set('mcpRelayUrl', mcpRelayEndpoint); url.searchParams.set('mcpRelayUrl', mcpRelayEndpoint);
url.searchParams.set('client', JSON.stringify(clientInfo)); url.searchParams.set('client', JSON.stringify(clientInfo));
const href = url.toString(); const href = url.toString();
const command = `'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' '${href}'`; const executableInfo = registry.findExecutable(this._browserChannel);
try { if (!executableInfo)
await promisify(exec)(command); throw new Error(`Unsupported channel: "${this._browserChannel}"`);
} catch (err) { const executablePath = executableInfo.executablePath();
debugLogger('Failed to run command:', err); if (!executablePath)
} throw new Error(`"${this._browserChannel}" executable not found. Make sure it is installed at a standard location.`);
spawn(executablePath, [href], {
windowsHide: true,
detached: true,
shell: false,
stdio: 'ignore',
});
} }
stop(): void { stop(): void {
@ -307,9 +321,9 @@ class ExtensionContextFactory implements BrowserContextFactory {
} }
} }
export async function startCDPRelayServer(port: number) { export async function startCDPRelayServer(port: number, browserChannel: string) {
const httpServer = await startHttpServer({ port }); const httpServer = await startHttpServer({ port });
const cdpRelayServer = new CDPRelayServer(httpServer); const cdpRelayServer = new CDPRelayServer(httpServer, browserChannel);
process.on('exit', () => cdpRelayServer.stop()); process.on('exit', () => cdpRelayServer.stop());
debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`); debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`);
return new ExtensionContextFactory(cdpRelayServer); return new ExtensionContextFactory(cdpRelayServer);
@ -332,7 +346,7 @@ class ExtensionConnection {
async send(method: string, params?: any, sessionId?: string): Promise<any> { async send(method: string, params?: any, sessionId?: string): Promise<any> {
if (this._ws.readyState !== WebSocket.OPEN) if (this._ws.readyState !== WebSocket.OPEN)
throw new Error('WebSocket closed'); throw new Error(`Unexpected WebSocket state: ${this._ws.readyState}`);
const id = ++this._lastId; const id = ++this._lastId;
this._ws.send(JSON.stringify({ id, method, params, sessionId })); this._ws.send(JSON.stringify({ id, method, params, sessionId }));
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@ -19,9 +19,11 @@ import { startHttpServer, startHttpTransport, startStdioTransport } from '../tra
import { Server } from '../server.js'; import { Server } from '../server.js';
import { startCDPRelayServer } from './cdpRelay.js'; import { startCDPRelayServer } from './cdpRelay.js';
export async function runWithExtension(options: any) { import type { CLIOptions } from '../config.js';
const config = await resolveCLIConfig({ });
const contextFactory = await startCDPRelayServer(9225); export async function runWithExtension(options: CLIOptions) {
const config = await resolveCLIConfig(options);
const contextFactory = await startCDPRelayServer(9225, config.browser.launchOptions.channel || 'chrome');
const server = new Server(config, contextFactory); const server = new Server(config, contextFactory);
server.setupExitWatchdog(); server.setupExitWatchdog();