From 946771d4061828e108309f2f24aa7e0e76cf2f16 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 7 Nov 2026 10:56:35 -0800 Subject: [PATCH] Discover installed Chromium executable --- web/backend/src/ChromiumFallback.ts | 20 ++++++++++++++++- web/backend/test/ChromiumFallback.test.cjs | 26 ++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/web/backend/src/ChromiumFallback.ts b/web/backend/src/ChromiumFallback.ts index d97bf16..0566d69 100644 --- a/web/backend/src/ChromiumFallback.ts +++ b/web/backend/src/ChromiumFallback.ts @@ -1,4 +1,5 @@ import { ChildProcess, spawn } from "child_process"; +import { accessSync, constants } from "fs"; import { mkdtemp, rm } from "fs/promises"; import os from "os"; import path from "path"; @@ -15,9 +16,26 @@ export class ChromiumFallback { constructor( private readonly onExit: (url: string) => void = () => undefined, - private readonly executable = process.env.QUEUECUBE_CHROME_BIN || "chromium", + private readonly executable = process.env.QUEUECUBE_CHROME_BIN || ChromiumFallback.findExecutable(), ) {} + private static findExecutable(): string { + const directories = (process.env.PATH || "").split(path.delimiter); + for (const name of ["chromium", "chromium-browser", "google-chrome"]) { + for (const directory of directories) { + if (!directory) continue; + const candidate = path.join(directory, name); + try { + accessSync(candidate, constants.X_OK); + return candidate; + } catch { + // Keep checking the remaining supported executable names. + } + } + } + return "chromium"; + } + public get isRunning(): boolean { return this.active !== null; } diff --git a/web/backend/test/ChromiumFallback.test.cjs b/web/backend/test/ChromiumFallback.test.cjs index 82191ce..2949dd8 100644 --- a/web/backend/test/ChromiumFallback.test.cjs +++ b/web/backend/test/ChromiumFallback.test.cjs @@ -41,6 +41,32 @@ test("uses isolated profile and safe argv; stop kills only its process and clean } }); +test("discovers chromium-browser when that is the installed executable", async () => { + const fixture = await fs.mkdtemp(path.join(os.tmpdir(), "queuecube-browser-path-test-")); + const argsFile = path.join(fixture, "argv.json"); + const executable = path.join(fixture, "chromium-browser"); + const previousPath = process.env.PATH; + const previousOverride = process.env.QUEUECUBE_CHROME_BIN; + await fs.writeFile(executable, `#!/usr/bin/env node\nconst fs=require('fs'); fs.writeFileSync(${JSON.stringify(argsFile)}, JSON.stringify(process.argv.slice(2))); setInterval(()=>{},1000);\n`); + await fs.chmod(executable, 0o700); + await fs.symlink(process.execPath, path.join(fixture, "node")); + + try { + process.env.PATH = fixture; + delete process.env.QUEUECUBE_CHROME_BIN; + const browser = new ChromiumFallback(); + assert.equal(await browser.start("https://example.test/browser-discovery", () => true), true); + await waitFor(async () => { try { await fs.access(argsFile); return true; } catch { return false; } }); + assert.equal(JSON.parse(await fs.readFile(argsFile, "utf8")).at(-1), "https://example.test/browser-discovery"); + await browser.stop(); + } finally { + process.env.PATH = previousPath; + if (previousOverride === undefined) delete process.env.QUEUECUBE_CHROME_BIN; + else process.env.QUEUECUBE_CHROME_BIN = previousOverride; + await fs.rm(fixture, { recursive: true, force: true }); + } +}); + test("canceled startup cleans its profile without spawning", async () => { const browser = new ChromiumFallback(() => undefined, "/bin/false"); assert.equal(await browser.start("https://example.test/", () => false), false);