mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2025-07-27 00:52:27 +08:00
chore(extension): exit gracefully when waiting for extension connection (#754)
This commit is contained in:
parent
e0fb748ccc
commit
e153ac3b7c
@ -17,6 +17,7 @@
|
|||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import * as playwright from 'playwright';
|
import * as playwright from 'playwright';
|
||||||
|
|
||||||
|
import { logUnhandledError } from './log.js';
|
||||||
import { Tab } from './tab.js';
|
import { Tab } from './tab.js';
|
||||||
|
|
||||||
import type { Tool } from './tools/tool.js';
|
import type { Tool } from './tools/tool.js';
|
||||||
@ -140,7 +141,7 @@ export class Context {
|
|||||||
|
|
||||||
async closeBrowserContext() {
|
async closeBrowserContext() {
|
||||||
if (!this._closeBrowserContextPromise)
|
if (!this._closeBrowserContextPromise)
|
||||||
this._closeBrowserContextPromise = this._closeBrowserContextImpl();
|
this._closeBrowserContextPromise = this._closeBrowserContextImpl().catch(logUnhandledError);
|
||||||
await this._closeBrowserContextPromise;
|
await this._closeBrowserContextPromise;
|
||||||
this._closeBrowserContextPromise = undefined;
|
this._closeBrowserContextPromise = undefined;
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,8 @@ import * as playwright from 'playwright';
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const { registry } = await import('playwright-core/lib/server/registry/index');
|
const { registry } = await import('playwright-core/lib/server/registry/index');
|
||||||
import { httpAddressToString, startHttpServer } from '../httpServer.js';
|
import { httpAddressToString, startHttpServer } from '../httpServer.js';
|
||||||
|
import { logUnhandledError } from '../log.js';
|
||||||
|
import { ManualPromise } from '../manualPromise.js';
|
||||||
import type { BrowserContextFactory } from '../browserContextFactory.js';
|
import type { BrowserContextFactory } from '../browserContextFactory.js';
|
||||||
import type websocket from 'ws';
|
import type websocket from 'ws';
|
||||||
|
|
||||||
@ -65,8 +67,7 @@ export class CDPRelayServer {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
} | undefined;
|
} | undefined;
|
||||||
private _nextSessionId: number = 1;
|
private _nextSessionId: number = 1;
|
||||||
private _extensionConnectionPromise: Promise<void>;
|
private _extensionConnectionPromise!: ManualPromise<void>;
|
||||||
private _extensionConnectionResolve: (() => void) | null = null;
|
|
||||||
|
|
||||||
constructor(server: http.Server, browserChannel: string) {
|
constructor(server: http.Server, browserChannel: string) {
|
||||||
this._wsHost = httpAddressToString(server.address()).replace(/^http/, 'ws');
|
this._wsHost = httpAddressToString(server.address()).replace(/^http/, 'ws');
|
||||||
@ -76,9 +77,7 @@ export class CDPRelayServer {
|
|||||||
this._cdpPath = `/cdp/${uuid}`;
|
this._cdpPath = `/cdp/${uuid}`;
|
||||||
this._extensionPath = `/extension/${uuid}`;
|
this._extensionPath = `/extension/${uuid}`;
|
||||||
|
|
||||||
this._extensionConnectionPromise = new Promise(resolve => {
|
this._resetExtensionConnection();
|
||||||
this._extensionConnectionResolve = resolve;
|
|
||||||
});
|
|
||||||
this._wss = new WebSocketServer({ server });
|
this._wss = new WebSocketServer({ server });
|
||||||
this._wss.on('connection', this._onConnection.bind(this));
|
this._wss.on('connection', this._onConnection.bind(this));
|
||||||
}
|
}
|
||||||
@ -166,15 +165,15 @@ export class CDPRelayServer {
|
|||||||
|
|
||||||
private _closeExtensionConnection(reason: string) {
|
private _closeExtensionConnection(reason: string) {
|
||||||
this._extensionConnection?.close(reason);
|
this._extensionConnection?.close(reason);
|
||||||
|
this._extensionConnectionPromise.reject(new Error(reason));
|
||||||
this._resetExtensionConnection();
|
this._resetExtensionConnection();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _resetExtensionConnection() {
|
private _resetExtensionConnection() {
|
||||||
this._connectedTabInfo = undefined;
|
this._connectedTabInfo = undefined;
|
||||||
this._extensionConnection = null;
|
this._extensionConnection = null;
|
||||||
this._extensionConnectionPromise = new Promise(resolve => {
|
this._extensionConnectionPromise = new ManualPromise();
|
||||||
this._extensionConnectionResolve = resolve;
|
void this._extensionConnectionPromise.catch(logUnhandledError);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _closePlaywrightConnection(reason: string) {
|
private _closePlaywrightConnection(reason: string) {
|
||||||
@ -197,7 +196,7 @@ export class CDPRelayServer {
|
|||||||
this._closePlaywrightConnection(`Extension disconnected: ${reason}`);
|
this._closePlaywrightConnection(`Extension disconnected: ${reason}`);
|
||||||
};
|
};
|
||||||
this._extensionConnection.onmessage = this._handleExtensionMessage.bind(this);
|
this._extensionConnection.onmessage = this._handleExtensionMessage.bind(this);
|
||||||
this._extensionConnectionResolve?.();
|
this._extensionConnectionPromise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleExtensionMessage(method: string, params: any) {
|
private _handleExtensionMessage(method: string, params: any) {
|
||||||
@ -323,10 +322,10 @@ class ExtensionContextFactory implements BrowserContextFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startCDPRelayServer(browserChannel: string) {
|
export async function startCDPRelayServer(browserChannel: string, abortController: AbortController) {
|
||||||
const httpServer = await startHttpServer({});
|
const httpServer = await startHttpServer({});
|
||||||
const cdpRelayServer = new CDPRelayServer(httpServer, browserChannel);
|
const cdpRelayServer = new CDPRelayServer(httpServer, browserChannel);
|
||||||
process.on('exit', () => cdpRelayServer.stop());
|
abortController.signal.addEventListener('abort', () => 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);
|
||||||
}
|
}
|
||||||
|
@ -20,8 +20,8 @@ import * as mcpTransport from '../mcp/transport.js';
|
|||||||
|
|
||||||
import type { FullConfig } from '../config.js';
|
import type { FullConfig } from '../config.js';
|
||||||
|
|
||||||
export async function runWithExtension(config: FullConfig) {
|
export async function runWithExtension(config: FullConfig, abortController: AbortController) {
|
||||||
const contextFactory = await startCDPRelayServer(config.browser.launchOptions.channel || 'chrome');
|
const contextFactory = await startCDPRelayServer(config.browser.launchOptions.channel || 'chrome', abortController);
|
||||||
const serverBackendFactory = () => new BrowserServerBackend(config, contextFactory);
|
const serverBackendFactory = () => new BrowserServerBackend(config, contextFactory);
|
||||||
await mcpTransport.start(serverBackendFactory, config.server);
|
await mcpTransport.start(serverBackendFactory, config.server);
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ program
|
|||||||
.addOption(new Option('--extension', 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.').hideHelp())
|
.addOption(new Option('--extension', 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.').hideHelp())
|
||||||
.addOption(new Option('--vision', 'Legacy option, use --caps=vision instead').hideHelp())
|
.addOption(new Option('--vision', 'Legacy option, use --caps=vision instead').hideHelp())
|
||||||
.action(async options => {
|
.action(async options => {
|
||||||
setupExitWatchdog();
|
const abortController = setupExitWatchdog();
|
||||||
|
|
||||||
if (options.vision) {
|
if (options.vision) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
@ -67,7 +67,7 @@ program
|
|||||||
const config = await resolveCLIConfig(options);
|
const config = await resolveCLIConfig(options);
|
||||||
|
|
||||||
if (options.extension) {
|
if (options.extension) {
|
||||||
await runWithExtension(config);
|
await runWithExtension(config, abortController);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,12 +85,15 @@ program
|
|||||||
});
|
});
|
||||||
|
|
||||||
function setupExitWatchdog() {
|
function setupExitWatchdog() {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
let isExiting = false;
|
let isExiting = false;
|
||||||
const handleExit = async () => {
|
const handleExit = async () => {
|
||||||
if (isExiting)
|
if (isExiting)
|
||||||
return;
|
return;
|
||||||
isExiting = true;
|
isExiting = true;
|
||||||
setTimeout(() => process.exit(0), 15000);
|
setTimeout(() => process.exit(0), 15000);
|
||||||
|
abortController.abort('Process exiting');
|
||||||
await Context.disposeAll();
|
await Context.disposeAll();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
@ -98,6 +101,8 @@ function setupExitWatchdog() {
|
|||||||
process.stdin.on('close', handleExit);
|
process.stdin.on('close', handleExit);
|
||||||
process.on('SIGINT', handleExit);
|
process.on('SIGINT', handleExit);
|
||||||
process.on('SIGTERM', handleExit);
|
process.on('SIGTERM', handleExit);
|
||||||
|
|
||||||
|
return abortController;
|
||||||
}
|
}
|
||||||
|
|
||||||
void program.parseAsync(process.argv);
|
void program.parseAsync(process.argv);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user