From 936cc6fe6712790e4582e5d72a7cb88e58088582 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Wed, 8 Jul 2026 16:44:41 -0400 Subject: [PATCH] fix(cli): actionable remediation for chrome-headless-shell SIGTRAP on arm64 macOS On arm64 macOS the bundled chrome-headless-shell can crash with SIGTRAP (EXC_BREAKPOINT) at launch, and the render fails with no guidance. Confirmed on an arm64 macOS 26.5.1 machine with 21 chrome-headless-shell trap crash reports in DiagnosticReports, including crashes against builds NEWER than the current pin, so bumping CHROME_VERSION is an unverified gamble with a repo-wide blast radius. Surface an actionable error instead. macosChromeCrashRemediation() (darwin+arm64-gated; reuses isTransientBrowserError from the engine, no duplicated regex) wired into handleRenderError after the Linux chromeLaunchRemediation check; surfaces the --docker / HYPERFRAMES_DOCKER_PLATFORM=linux/amd64 and HYPERFRAMES_BROWSER_PATH workarounds. CHROME_VERSION and the Linux path untouched. 7 tests. --- .../cli/src/browser/macosChromeCrash.test.ts | 37 +++++++++++++++++++ packages/cli/src/browser/macosChromeCrash.ts | 20 ++++++++++ packages/cli/src/commands/render.ts | 6 +++ 3 files changed, 63 insertions(+) create mode 100644 packages/cli/src/browser/macosChromeCrash.test.ts create mode 100644 packages/cli/src/browser/macosChromeCrash.ts diff --git a/packages/cli/src/browser/macosChromeCrash.test.ts b/packages/cli/src/browser/macosChromeCrash.test.ts new file mode 100644 index 000000000..49d4028af --- /dev/null +++ b/packages/cli/src/browser/macosChromeCrash.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import { macosChromeCrashRemediation } from "./macosChromeCrash.js"; + +describe("macosChromeCrashRemediation", () => { + it.each([ + ["darwin", "arm64"], + ["darwin", "x64"], + ["linux", "arm64"], + ] as const)("returns undefined for unrelated errors on %s/%s", (platform, arch) => { + expect(macosChromeCrashRemediation("Composition has zero duration", platform, arch)).toBe( + undefined, + ); + }); + + it("returns undefined on Intel macOS for browser crash messages", () => { + expect(macosChromeCrashRemediation("Page crashed!", "darwin", "x64")).toBe(undefined); + }); + + it("returns undefined on non-macOS platforms for browser crash messages", () => { + expect(macosChromeCrashRemediation("Target closed", "linux", "arm64")).toBe(undefined); + expect(macosChromeCrashRemediation("Target closed", "win32", "arm64")).toBe(undefined); + }); + + it.each(["Target closed", "Page crashed!"])( + "returns Docker guidance on Apple Silicon macOS for %s", + (message) => { + const remediation = macosChromeCrashRemediation(message, "darwin", "arm64"); + + expect(remediation).toBeDefined(); + expect(remediation).toContain("known chrome-headless-shell crash pattern"); + expect(remediation).toContain("HYPERFRAMES_DOCKER_PLATFORM=linux/amd64"); + expect(remediation).toContain("--docker"); + expect(remediation).toContain("native macOS browser"); + expect(remediation).toContain("HYPERFRAMES_BROWSER_PATH"); + }, + ); +}); diff --git a/packages/cli/src/browser/macosChromeCrash.ts b/packages/cli/src/browser/macosChromeCrash.ts new file mode 100644 index 000000000..bb9d76bb2 --- /dev/null +++ b/packages/cli/src/browser/macosChromeCrash.ts @@ -0,0 +1,20 @@ +import { isTransientBrowserError } from "@hyperframes/engine"; + +export function macosChromeCrashRemediation( + errorMessage: string, + platform: NodeJS.Platform = process.platform, + arch: NodeJS.Architecture = process.arch, +): string | undefined { + if (platform !== "darwin" || arch !== "arm64") return undefined; + if (!isTransientBrowserError(errorMessage)) return undefined; + + const lines: string[] = []; + lines.push("This matches a known chrome-headless-shell crash pattern on Apple Silicon macOS."); + lines.push( + "Re-run with Docker to render inside a Linux container instead of the native macOS browser:", + ); + lines.push(" HYPERFRAMES_DOCKER_PLATFORM=linux/amd64 npx hyperframes render --docker"); + lines.push("Or point HyperFrames at a different local Chrome or Chromium build:"); + lines.push(" HYPERFRAMES_BROWSER_PATH=/path/to/chrome npx hyperframes render"); + return lines.join("\n"); +} diff --git a/packages/cli/src/commands/render.ts b/packages/cli/src/commands/render.ts index 2b6519847..069770048 100644 --- a/packages/cli/src/commands/render.ts +++ b/packages/cli/src/commands/render.ts @@ -75,6 +75,7 @@ import { buildDockerRunArgs, resolveDockerPlatform } from "../utils/dockerRunArg import { normalizeErrorMessage } from "../utils/errorMessage.js"; import { runEnvironmentChecks } from "../browser/preflight.js"; import { chromeLaunchRemediation } from "../browser/linuxDeps.js"; +import { macosChromeCrashRemediation } from "../browser/macosChromeCrash.js"; import type { ProducerLogger, RenderJob } from "@hyperframes/producer"; import { MAX_VP9_CPU_USED, @@ -1636,6 +1637,11 @@ function handleRenderError( errorBox("Render failed — Chrome could not launch", message, remediation); process.exit(1); } + const macosRemediation = macosChromeCrashRemediation(message); + if (macosRemediation) { + errorBox("Render failed — Chrome could not launch", message, macosRemediation); + process.exit(1); + } errorBox("Render failed", message, hint); process.exit(1); }