diff --git a/packages/cli/src/browser/preflight.test.ts b/packages/cli/src/browser/preflight.test.ts index ad1e9a4324..02e0d6b7ff 100644 --- a/packages/cli/src/browser/preflight.test.ts +++ b/packages/cli/src/browser/preflight.test.ts @@ -125,6 +125,41 @@ describe("runEnvironmentChecks", () => { } }); + it.each([ + { failure: { status: 133, signal: "SIGTRAP" }, detail: "SIGTRAP" }, + { failure: { code: "EACCES" }, detail: "EACCES" }, + { failure: { code: "ETIMEDOUT", signal: "SIGKILL" }, detail: "ETIMEDOUT" }, + ])("rejects an existing browser that fails --version: $detail", async ({ failure, detail }) => { + execFileSync.mockImplementation((_path, args) => { + if (args[0] === "--version") throw Object.assign(new Error("cannot execute"), failure); + return "ffmpeg version 7.1.1\n"; + }); + const findBrowser = vi.spyOn(manager, "findBrowser").mockResolvedValue({ + executablePath: process.execPath, + source: "cache", + }); + try { + for (const browserPath of [undefined, process.execPath]) { + const result = await runEnvironmentChecks({ includeBrowser: true, browserPath }); + const chrome = result.outcomes.find((outcome) => outcome.name === "Chrome"); + expect(chrome).toMatchObject({ ok: false, level: "error", title: "Chrome cannot start" }); + expect(chrome?.detail).toContain(detail); + expect(result.browser).toBeUndefined(); + } + expect(execFileSync).toHaveBeenCalledWith( + process.execPath, + ["--version"], + expect.objectContaining({ + timeout: 5000, + killSignal: "SIGKILL", + windowsHide: true, + }), + ); + } finally { + findBrowser.mockRestore(); + } + }); + it("reports an explicit missing browser path before render starts", async () => { const result = await runEnvironmentChecks({ includeBrowser: true, diff --git a/packages/cli/src/browser/preflight.ts b/packages/cli/src/browser/preflight.ts index dc8c9dd1f3..fabcd97ca2 100644 --- a/packages/cli/src/browser/preflight.ts +++ b/packages/cli/src/browser/preflight.ts @@ -180,10 +180,56 @@ function chromeSharedLibOutcome( }; } +function chromeLaunchFailureDetails(error: unknown): string { + if (typeof error !== "object" || error === null) return ""; + const status = "status" in error ? error.status : undefined; + const signal = "signal" in error ? error.signal : undefined; + const code = "code" in error ? error.code : undefined; + return [ + typeof status === "number" ? `exit code ${status}` : "", + typeof signal === "string" ? `signal ${signal}` : "", + typeof code === "string" ? code : "", + ] + .filter(Boolean) + .join(", "); +} + +function chromeLaunchOutcome( + executablePath: string, + found: EnvironmentCheckOutcome, +): EnvironmentCheckOutcome { + const libraries = chromeSharedLibOutcome(executablePath, found); + if (!libraries.ok) return libraries; + try { + execFileSync(executablePath, ["--version"], { + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + timeout: 5000, + killSignal: "SIGKILL", + maxBuffer: 64 * 1024, + windowsHide: true, + }); + return found; + } catch (error) { + const details = chromeLaunchFailureDetails(error); + return { + name: "Chrome", + ok: false, + level: "error", + title: "Chrome cannot start", + detail: `Failed to run "${executablePath}" --version${details ? ` (${details})` : ""}.`, + hint: + "Select a working Chrome/Chromium binary for this OS and architecture with " + + "HYPERFRAMES_BROWSER_PATH, or reinstall with: npx hyperframes browser ensure --force", + path: executablePath, + }; + } +} + async function checkChrome(browserPath?: string): Promise { if (browserPath) { if (existsSync(browserPath)) { - return chromeSharedLibOutcome(browserPath, { + return chromeLaunchOutcome(browserPath, { name: "Chrome", ok: true, level: "ok", @@ -213,7 +259,7 @@ async function checkChrome(browserPath?: string): Promise