Skip to content

Commit 1b0fb95

Browse files
Bug 2040849 - fix: update logic to locate geckodriver binary on windows
1 parent 4c55ffc commit 1b0fb95

2 files changed

Lines changed: 79 additions & 57 deletions

File tree

src/firefox/core.ts

Lines changed: 76 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,66 +4,84 @@
44

55
import { Builder, Browser, Capabilities, WebDriver } from 'selenium-webdriver';
66
import firefox from 'selenium-webdriver/firefox.js';
7-
import { mkdirSync, openSync, closeSync } from 'node:fs';
7+
import { mkdirSync, openSync, closeSync, existsSync, readdirSync, statSync } from 'node:fs';
88
import { homedir } from 'node:os';
9-
import { join } from 'node:path';
9+
import { join, delimiter } from 'node:path';
1010
import type { FirefoxLaunchOptions } from './types.js';
1111
import { log, logDebug } from '../utils/logger.js';
1212
import { resolveProfilePath } from './profile.js';
1313

1414
// ---------------------------------------------------------------------------
15-
// Geckodriver binary finder — used only for --connect-existing mode
15+
// Geckodriver binary finder
1616
// ---------------------------------------------------------------------------
1717

18-
/**
19-
* Finds the geckodriver binary path via selenium-manager.
20-
* Uses --driver (not --browser) to avoid downloading Firefox, which is
21-
* already running in connect-existing mode.
22-
*/
23-
async function findGeckodriver(): Promise<string> {
24-
const path = await import('node:path');
25-
const { execFileSync } = await import('node:child_process');
18+
function findGeckodriverInPath(binaryName: string): string | null {
19+
for (const dir of (process.env.PATH ?? '').split(delimiter)) {
20+
if (!dir) {
21+
continue;
22+
}
23+
const candidate = join(dir, binaryName);
24+
if (existsSync(candidate)) {
25+
return candidate;
26+
}
27+
}
28+
return null;
29+
}
2630

31+
function findGeckodriverInSeleniumCache(binaryName: string): string | null {
32+
const cacheBase = join(homedir(), '.cache/selenium/geckodriver');
2733
try {
28-
const { createRequire } = await import('node:module');
29-
const require = createRequire(import.meta.url);
30-
const swPkg = require.resolve('selenium-webdriver/package.json');
31-
const swDir = path.dirname(swPkg);
32-
const platform =
33-
process.platform === 'win32' ? 'windows' : process.platform === 'darwin' ? 'macos' : 'linux';
34-
const ext = process.platform === 'win32' ? '.exe' : '';
35-
const smBin = path.join(swDir, 'bin', platform, `selenium-manager${ext}`);
36-
const result = JSON.parse(
37-
execFileSync(smBin, ['--driver', 'geckodriver', '--output', 'json'], { encoding: 'utf-8' })
38-
);
39-
return result.result.driver_path as string;
40-
} catch {
41-
// Fallback: walk the selenium cache directory to find any geckodriver binary
42-
const os = await import('node:os');
43-
const fs = await import('node:fs');
44-
const cacheBase = path.join(os.homedir(), '.cache/selenium/geckodriver');
45-
const ext = process.platform === 'win32' ? '.exe' : '';
46-
const binaryName = `geckodriver${ext}`;
47-
try {
48-
if (fs.existsSync(cacheBase)) {
49-
for (const platformDir of fs.readdirSync(cacheBase)) {
50-
const platformPath = path.join(cacheBase, platformDir);
51-
if (!fs.statSync(platformPath).isDirectory()) {
52-
continue;
53-
}
54-
for (const versionDir of fs.readdirSync(platformPath).sort().reverse()) {
55-
const candidate = path.join(platformPath, versionDir, binaryName);
56-
if (fs.existsSync(candidate)) {
57-
return candidate;
58-
}
59-
}
34+
if (!existsSync(cacheBase)) {
35+
return null;
36+
}
37+
for (const platformDir of readdirSync(cacheBase)) {
38+
const platformPath = join(cacheBase, platformDir);
39+
if (!statSync(platformPath).isDirectory()) {
40+
continue;
41+
}
42+
for (const versionDir of readdirSync(platformPath).sort().reverse()) {
43+
const candidate = join(platformPath, versionDir, binaryName);
44+
if (existsSync(candidate)) {
45+
return candidate;
6046
}
6147
}
62-
} catch {
63-
// ignore permission errors
6448
}
65-
throw new Error('Cannot find geckodriver binary. Ensure selenium-webdriver is installed.');
49+
} catch {
50+
// ignore permission errors
6651
}
52+
return null;
53+
}
54+
55+
async function findGeckodriverInNpmPackage(): Promise<string | null> {
56+
try {
57+
const { download } = await import('geckodriver');
58+
log('geckodriver not found in PATH or selenium cache, downloading via npm package...');
59+
return await download();
60+
} catch {
61+
return null;
62+
}
63+
}
64+
65+
/**
66+
* Finds the geckodriver binary using three strategies in order:
67+
* 1. Search PATH directly (fast, no subprocess)
68+
* 2. Walk the selenium cache directory
69+
* 3. Download via the bundled geckodriver npm package
70+
* Throws if none succeeds.
71+
*/
72+
async function findGeckodriver(): Promise<string> {
73+
const ext = process.platform === 'win32' ? '.exe' : '';
74+
const binaryName = `geckodriver${ext}`;
75+
76+
const found =
77+
findGeckodriverInPath(binaryName) ??
78+
findGeckodriverInSeleniumCache(binaryName) ??
79+
(await findGeckodriverInNpmPackage());
80+
81+
if (!found) {
82+
throw new Error('Cannot find geckodriver binary. Ensure geckodriver is in PATH.');
83+
}
84+
return found;
6785
}
6886

6987
export class FirefoxCore {
@@ -119,7 +137,6 @@ export class FirefoxCore {
119137
} else if (this.options.connectExisting) {
120138
const port = this.options.marionettePort ?? 2828;
121139

122-
// Find geckodriver binary (--driver avoids downloading Firefox via selenium-manager)
123140
const geckodriverPath = await findGeckodriver();
124141
logDebug(`Using geckodriver: ${geckodriverPath}`);
125142

@@ -206,19 +223,23 @@ export class FirefoxCore {
206223
}
207224
}
208225

209-
// Configure geckodriver service to capture output
210-
const serviceBuilder = new firefox.ServiceBuilder();
226+
let serviceBuilder;
227+
if (process.platform === 'win32') {
228+
// On windows, firefox.ServiceBuilder() invoked from the MCP will hang.
229+
// geckodriver has to be in the PATH. See Bug 2040849.
230+
const geckodriverPath = await findGeckodriver();
231+
logDebug(`Using geckodriver: ${geckodriverPath}`);
232+
serviceBuilder = new firefox.ServiceBuilder(geckodriverPath);
233+
} else {
234+
// On other platforms, the default ServiceBuilder should locate and
235+
// start geckodriver successfully.
236+
serviceBuilder = new firefox.ServiceBuilder();
237+
}
211238

212-
// If we have a log file, open it and redirect geckodriver output there
213-
// This captures both geckodriver logs and Firefox stderr (including MOZ_LOG)
214239
if (this.logFilePath) {
215240
// Open file for appending, create if doesn't exist
216241
this.logFileFd = openSync(this.logFilePath, 'a');
217-
218-
// Configure stdio: stdin=ignore, stdout=logfile, stderr=logfile
219-
// This redirects all output from geckodriver and Firefox to the log file
220242
serviceBuilder.setStdio(['ignore', this.logFileFd, this.logFileFd]);
221-
222243
log(`Capturing Firefox output to: ${this.logFilePath}`);
223244
}
224245

tests/firefox/core.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,10 @@ describe('FirefoxCore connect() profile handling', () => {
119119
Browser: { FIREFOX: 'firefox' },
120120
}));
121121

122-
// Mock node:fs so profile.ts doesn't touch the real filesystem
122+
// Mock node:fs so profile.ts doesn't touch the real filesystem.
123+
// existsSync returns true for geckodriver paths so findGeckodriver() succeeds.
123124
vi.doMock('node:fs', () => ({
124-
existsSync: vi.fn().mockReturnValue(false),
125+
existsSync: vi.fn((p: unknown) => String(p).includes('geckodriver')),
125126
mkdirSync: vi.fn(),
126127
copyFileSync: vi.fn(),
127128
openSync: vi.fn().mockReturnValue(3),

0 commit comments

Comments
 (0)