Skip to content

Commit 7774ad9

Browse files
authored
chore(extension): support custom executablePath (microsoft#947)
Fixes microsoft#941
1 parent 1a64a51 commit 7774ad9

File tree

4 files changed

+47
-10
lines changed

4 files changed

+47
-10
lines changed

extension/tests/extension.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,32 @@ for (const [mode, startClientMethod] of [
276276
});
277277

278278
}
279+
280+
test(`custom executablePath`, async ({ startClient, server, useShortConnectionTimeout }) => {
281+
useShortConnectionTimeout(1000);
282+
283+
const executablePath = test.info().outputPath('echo.sh');
284+
await fs.promises.writeFile(executablePath, '#!/bin/bash\necho "Custom exec args: $@" > "$(dirname "$0")/output.txt"', { mode: 0o755 });
285+
286+
const { client } = await startClient({
287+
args: [`--extension`],
288+
config: {
289+
browser: {
290+
launchOptions: {
291+
executablePath,
292+
},
293+
}
294+
},
295+
});
296+
297+
const navigateResponse = await client.callTool({
298+
name: 'browser_navigate',
299+
arguments: { url: server.HELLO_WORLD },
300+
timeout: 1000,
301+
});
302+
expect(await navigateResponse).toHaveResponse({
303+
result: expect.stringContaining('Extension connection timeout.'),
304+
isError: true,
305+
});
306+
expect(await fs.promises.readFile(test.info().outputPath('output.txt'), 'utf8')).toContain('Custom exec args: chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html?');
307+
});

src/extension/cdpRelay.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export class CDPRelayServer {
6060
private _wsHost: string;
6161
private _browserChannel: string;
6262
private _userDataDir?: string;
63+
private _executablePath?: string;
6364
private _cdpPath: string;
6465
private _extensionPath: string;
6566
private _wss: WebSocketServer;
@@ -73,10 +74,11 @@ export class CDPRelayServer {
7374
private _nextSessionId: number = 1;
7475
private _extensionConnectionPromise!: ManualPromise<void>;
7576

76-
constructor(server: http.Server, browserChannel: string, userDataDir?: string) {
77+
constructor(server: http.Server, browserChannel: string, userDataDir?: string, executablePath?: string) {
7778
this._wsHost = httpAddressToString(server.address()).replace(/^http/, 'ws');
7879
this._browserChannel = browserChannel;
7980
this._userDataDir = userDataDir;
81+
this._executablePath = executablePath;
8082

8183
const uuid = crypto.randomUUID();
8284
this._cdpPath = `/cdp/${uuid}`;
@@ -125,12 +127,16 @@ export class CDPRelayServer {
125127
if (toolName)
126128
url.searchParams.set('newTab', String(toolName === 'browser_navigate'));
127129
const href = url.toString();
128-
const executableInfo = registry.findExecutable(this._browserChannel);
129-
if (!executableInfo)
130-
throw new Error(`Unsupported channel: "${this._browserChannel}"`);
131-
const executablePath = executableInfo.executablePath();
132-
if (!executablePath)
133-
throw new Error(`"${this._browserChannel}" executable not found. Make sure it is installed at a standard location.`);
130+
131+
let executablePath = this._executablePath;
132+
if (!executablePath) {
133+
const executableInfo = registry.findExecutable(this._browserChannel);
134+
if (!executableInfo)
135+
throw new Error(`Unsupported channel: "${this._browserChannel}"`);
136+
executablePath = executableInfo.executablePath();
137+
if (!executablePath)
138+
throw new Error(`"${this._browserChannel}" executable not found. Make sure it is installed at a standard location.`);
139+
}
134140

135141
const args: string[] = [];
136142
if (this._userDataDir)

src/extension/extensionContextFactory.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ const debugLogger = debug('pw:mcp:relay');
2626
export class ExtensionContextFactory implements BrowserContextFactory {
2727
private _browserChannel: string;
2828
private _userDataDir?: string;
29+
private _executablePath?: string;
2930

30-
constructor(browserChannel: string, userDataDir: string | undefined) {
31+
constructor(browserChannel: string, userDataDir: string | undefined, executablePath: string | undefined) {
3132
this._browserChannel = browserChannel;
3233
this._userDataDir = userDataDir;
34+
this._executablePath = executablePath;
3335
}
3436

3537
async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> {
@@ -55,7 +57,7 @@ export class ExtensionContextFactory implements BrowserContextFactory {
5557
httpServer.close();
5658
throw new Error(abortSignal.reason);
5759
}
58-
const cdpRelayServer = new CDPRelayServer(httpServer, this._browserChannel, this._userDataDir);
60+
const cdpRelayServer = new CDPRelayServer(httpServer, this._browserChannel, this._userDataDir, this._executablePath);
5961
abortSignal.addEventListener('abort', () => cdpRelayServer.stop());
6062
debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`);
6163
return cdpRelayServer;

src/program.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ program
7272

7373
const config = await resolveCLIConfig(options);
7474
const browserContextFactory = contextFactory(config);
75-
const extensionContextFactory = new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir);
75+
const extensionContextFactory = new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir, config.browser.launchOptions.executablePath);
7676

7777
if (options.extension) {
7878
const serverBackendFactory: mcpServer.ServerBackendFactory = {

0 commit comments

Comments
 (0)