fix(cli): reap only orphaned browsers HyperFrames launched (#6251)

* fix(cli): reap only orphaned browsers HyperFrames launched

Orphan cleanup on preview and render killed every PPID-1 process of the
current user whose args named chrome-headless-shell or a Puppeteer temp
profile, which included detached browsers started by other tools.

Every HyperFrames Chrome launch now passes an inert
--hyperframes-owned-browser switch, and the reaper only kills PPID-1
processes that carry it as a whole argument.

Fixes #6203

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(core): serve the browser marker from its own subpath

The GCP image's BeginFrame contract step imports the engine's
browserManager straight from source before core's generated runtime
exists, so pulling the marker from the core root entry broke it.
A dependency-free @hyperframes/core/browser-marker subpath keeps that
import chain as light as it was.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(cli): expect the browser marker in the capture fallback launches

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(cli): say orphan cleanup runs before preview and render

The cleanup only stops browsers HyperFrames launched, so the opt-out is
no longer needed to protect other tools' browsers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
James Russo
2026-10-24 23:09:57 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 19374955c7
commit f180ffca64
19 changed files with 260 additions and 50 deletions
+4 -3
View File
@@ -50,9 +50,10 @@ Google Cloud Run. Most commands take `--json` when you need to read the result f
Start with the simple command, then open the [full reference](/packages/cli) for the exact options needed by automation or infrastructure.
When embedding preview in another tool or sharing a machine with other browser
automation, set `HYPERFRAMES_NO_ORPHAN_CLEANUP=1` in the calling environment
to skip startup cleanup of detached headless Chrome processes:
Before preview and render, HyperFrames stops headless Chrome processes left
behind by crashed HyperFrames sessions. It only stops browsers that HyperFrames
launched, so detached browsers from other tools keep running. To skip this
cleanup, set `HYPERFRAMES_NO_ORPHAN_CLEANUP=1` in the calling environment:
```bash
HYPERFRAMES_NO_ORPHAN_CLEANUP=1 npx hyperframes preview ./my-video
@@ -1,4 +1,5 @@
import { EventEmitter } from "node:events";
import { HYPERFRAMES_BROWSER_MARKER_ARG } from "@hyperframes/core/browser-marker";
import type { LaunchOptions, PuppeteerNode } from "puppeteer-core";
import { afterEach, describe, expect, it, vi } from "vitest";
@@ -30,10 +31,15 @@ describe("the GPU probe where nothing may listen", () => {
await resolveManagedGpuMode("auto", "/chrome");
expect(probe.options[0]).toMatchObject({
pipe: true,
args: ["--headless", "--single-process", "--disable-gpu-compositing"],
args: [
"--headless",
"--single-process",
"--disable-gpu-compositing",
HYPERFRAMES_BROWSER_MARKER_ARG,
],
});
expect(probe.options[1]!.pipe).toBe(true);
expect(probe.options[1]!.args).toEqual(["--headless"]);
expect(probe.options[1]!.args).toEqual(["--headless", HYPERFRAMES_BROWSER_MARKER_ARG]);
});
});
+9 -1
View File
@@ -1,4 +1,5 @@
import type { Browser, LaunchOptions, PuppeteerNode } from "puppeteer-core";
import { withHyperframesBrowserMarker } from "@hyperframes/core/browser-marker";
import {
resolveCaptureBrowserGpuMode,
type BrowserGpuMode,
@@ -117,7 +118,14 @@ async function launchOwnedBrowser(
const signal = options.signal ? AbortSignal.any([options.signal, abort.signal]) : abort.signal;
const launchedAt = Date.now();
const launch = untilLaunchOrAbort(
puppeteer.launch({ ...options, pipe: true, signal, handleSIGINT: false, handleSIGTERM: false }),
puppeteer.launch({
...options,
args: withHyperframesBrowserMarker(options.args),
pipe: true,
signal,
handleSIGINT: false,
handleSIGTERM: false,
}),
signal,
);
const limit = options.timeout ?? LAUNCH_TIMEOUT_MS;
+16 -4
View File
@@ -6,6 +6,7 @@ import {
type PuppeteerNode,
} from "puppeteer-core";
import { TargetCloseError } from "puppeteer-core/internal/common/Errors.js";
import { HYPERFRAMES_BROWSER_MARKER_ARG } from "@hyperframes/core/browser-marker";
import { afterEach, describe, expect, it, vi } from "vitest";
import { launchManagedBrowser } from "./launch.js";
@@ -66,7 +67,12 @@ describe("a browser inside an agent's macOS seatbelt", () => {
expect(launches).toHaveLength(1);
expect(launches[0]).toMatchObject({
pipe: true,
args: ["--headless", "--single-process", "--disable-gpu-compositing"],
args: [
"--headless",
"--single-process",
"--disable-gpu-compositing",
HYPERFRAMES_BROWSER_MARKER_ARG,
],
});
});
@@ -75,11 +81,12 @@ describe("a browser inside an agent's macOS seatbelt", () => {
const { puppeteer, launches } = recordingPuppeteer([SEATBELT_LAUNCH_FAILURE]);
await expect(launchManagedBrowser(puppeteer, { args: ["--headless"] })).resolves.toBeDefined();
expect(launches.map((options) => options.pipe ?? false)).toEqual([true, true]);
expect(launches[0]!.args).toEqual(["--headless"]);
expect(launches[0]!.args).toEqual(["--headless", HYPERFRAMES_BROWSER_MARKER_ARG]);
expect(launches[1]!.args).toEqual([
"--headless",
"--single-process",
"--disable-gpu-compositing",
HYPERFRAMES_BROWSER_MARKER_ARG,
]);
});
it("relaunches as one process when an unnamed sandbox closes a pipe launch before its first targets", async () => {
@@ -90,8 +97,13 @@ describe("a browser inside an agent's macOS seatbelt", () => {
]);
await expect(launchManagedBrowser(puppeteer, { args: ["--headless"] })).resolves.toBeDefined();
expect(launches.map((options) => options.args)).toEqual([
["--headless"],
["--headless", "--single-process", "--disable-gpu-compositing"],
["--headless", HYPERFRAMES_BROWSER_MARKER_ARG],
[
"--headless",
"--single-process",
"--disable-gpu-compositing",
HYPERFRAMES_BROWSER_MARKER_ARG,
],
]);
});
@@ -1,4 +1,5 @@
import { EventEmitter } from "node:events";
import { HYPERFRAMES_BROWSER_MARKER_ARG } from "@hyperframes/core/browser-marker";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
@@ -232,6 +233,7 @@ describe("openSettledCompositionPage when nothing may listen", () => {
"--headless",
"--single-process",
"--disable-gpu-compositing",
HYPERFRAMES_BROWSER_MARKER_ARG,
]);
expect(opened.page.setRequestInterception).toHaveBeenCalledWith(true);
const onRequest = opened.page.on.mock.calls.find(([event]) => event === "request")![1];
@@ -263,7 +265,7 @@ describe("openSettledCompositionPage when nothing may listen", () => {
mocks.launch.mockResolvedValue(opened.browser);
await openSettledCompositionPage(HTML, "http://127.0.0.1:3000", OPTIONS);
expect(launched().pipe).toBe(true);
expect(launched().args).toEqual(["--headless"]);
expect(launched().args).toEqual(["--headless", HYPERFRAMES_BROWSER_MARKER_ARG]);
expect(opened.page.setRequestInterception).not.toHaveBeenCalled();
});
});
@@ -1,3 +1,4 @@
import { HYPERFRAMES_BROWSER_MARKER_ARG } from "@hyperframes/core/browser-marker";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const child = vi.hoisted(() => ({ execFileSync: vi.fn(), execSync: vi.fn() }));
@@ -28,14 +29,17 @@ describe.skipIf(process.platform === "win32")("killOrphanedProcesses scan", () =
expect(tree.terminateProcessTree).not.toHaveBeenCalled();
});
it("finds orphaned browsers from one process-table read", () => {
it("kills only orphaned browsers HyperFrames marked, from one process-table read", () => {
child.execFileSync.mockReturnValue(
[
" 101 1 /opt/chrome-headless-shell --headless",
` 101 1 /opt/chrome-headless-shell --headless ${HYPERFRAMES_BROWSER_MARKER_ARG}`,
" 102 101 /opt/chrome-headless-shell --type=renderer",
" 103 1 chrome --user-data-dir=/tmp/puppeteer_dev_chrome_profile-x",
" 104 900 chrome --user-data-dir=/tmp/puppeteer_dev_chrome_profile-y",
` 103 1 chrome --user-data-dir=/tmp/puppeteer_dev_chrome_profile-x ${HYPERFRAMES_BROWSER_MARKER_ARG} about:blank`,
` 104 900 chrome --user-data-dir=/tmp/puppeteer_dev_chrome_profile-y ${HYPERFRAMES_BROWSER_MARKER_ARG}`,
" 105 1 /usr/bin/node server.js",
" 106 1 /opt/chrome-headless-shell --remote-debugging-port=0 about:blank",
" 107 1 chrome --user-data-dir=/tmp/puppeteer_dev_chrome_profile-z --headless",
` 108 1 chrome ${HYPERFRAMES_BROWSER_MARKER_ARG}-lookalike`,
].join("\n"),
);
@@ -44,4 +48,13 @@ describe.skipIf(process.platform === "win32")("killOrphanedProcesses scan", () =
expect(child.execSync).not.toHaveBeenCalled();
expect(tree.terminateProcessTree.mock.calls.map(([pid]) => pid)).toEqual([101, 103]);
});
it("leaves another tool's detached headless shell running", () => {
child.execFileSync.mockReturnValue(
" 201 1 /opt/chrome-headless-shell --remote-debugging-port=0 about:blank",
);
expect(killOrphanedProcesses()).toBe(0);
expect(tree.terminateProcessTree).not.toHaveBeenCalled();
});
});
+6 -10
View File
@@ -1,24 +1,20 @@
// fallow-ignore-file code-duplication
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { HYPERFRAMES_BROWSER_MARKER_ARG } from "@hyperframes/core/browser-marker";
import { runRenderSetupWorker } from "./cancellableProcess.js";
import { terminateProcessTree, windowsProcessTreeKillArgs } from "./processTree.js";
export { windowsProcessTreeKillArgs };
/**
* Kill orphaned Chrome processes (PPID 1: their parent died) left by crashed sessions, headless shell or
* Puppeteer's Chrome (`puppeteer_dev_chrome_profile`), with their helper subtrees. Only the current user's
* processes are read. Returns the count of killed process trees.
* Kill the current user's orphaned (PPID 1) Chrome trees that carry the HyperFrames launch marker, so detached
* browsers other tools started survive. Returns the count of killed process trees.
*/
export function killOrphanedProcesses(): number {
if (process.env.HYPERFRAMES_NO_ORPHAN_CLEANUP === "1" || process.platform === "win32") return 0;
return killOrphansMatching([
"chrome-headless-shell",
"chrome_headless_shell",
"puppeteer_dev_chrome_profile",
]);
return killOrphansMatching(HYPERFRAMES_BROWSER_MARKER_ARG);
}
export async function killOrphanedProcessesForRender(signal: AbortSignal): Promise<number> {
@@ -230,7 +226,7 @@ export function isProcessDescendant(
}
/** One process-table read: a spawn per candidate PID cost half a second before `preview` listened. */
function killOrphansMatching(markers: readonly string[]): number {
function killOrphansMatching(marker: string): number {
const uid = process.getuid?.();
const owner = uid === undefined ? ["-A"] : ["-U", String(uid)];
let table: string;
@@ -247,7 +243,7 @@ function killOrphansMatching(markers: readonly string[]): number {
for (const line of table.split("\n")) {
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);
if (!match || match[2] !== "1") continue;
if (!markers.some((marker) => match[3]!.includes(marker))) continue;
if (!match[3]!.split(/\s+/).includes(marker)) continue;
killProcessTree(Number(match[1]));
killed++;
}
+6
View File
@@ -56,6 +56,12 @@
"types": "./dist/safePath.d.ts",
"environments": ["bun", "node"]
},
"./browser-marker": {
"source": "./src/browserMarker.ts",
"runtime": "./dist/browserMarker.js",
"types": "./dist/browserMarker.d.ts",
"environments": ["bun", "node"]
},
"./atomic-file": {
"source": "./src/atomicFile.ts",
"runtime": "./dist/atomicFile.js",
+10
View File
@@ -68,6 +68,12 @@
"import": "./src/safePath.ts",
"types": "./src/safePath.ts"
},
"./browser-marker": {
"bun": "./src/browserMarker.ts",
"node": "./dist/browserMarker.js",
"import": "./src/browserMarker.ts",
"types": "./src/browserMarker.ts"
},
"./atomic-file": {
"bun": "./src/atomicFile.ts",
"node": "./dist/atomicFile.js",
@@ -594,6 +600,10 @@
"import": "./dist/safePath.js",
"types": "./dist/safePath.d.ts"
},
"./browser-marker": {
"import": "./dist/browserMarker.js",
"types": "./dist/browserMarker.d.ts"
},
"./atomic-file": {
"import": "./dist/atomicFile.js",
"types": "./dist/atomicFile.d.ts"
+19
View File
@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { HYPERFRAMES_BROWSER_MARKER_ARG, withHyperframesBrowserMarker } from "./browserMarker.js";
describe("withHyperframesBrowserMarker", () => {
it("appends the marker to a copy of the args", () => {
const args = ["--no-sandbox"];
expect(withHyperframesBrowserMarker(args)).toEqual([
"--no-sandbox",
HYPERFRAMES_BROWSER_MARKER_ARG,
]);
expect(args).toEqual(["--no-sandbox"]);
});
it("adds the marker once when a wrapper marks already-marked args", () => {
expect(withHyperframesBrowserMarker(withHyperframesBrowserMarker())).toEqual([
HYPERFRAMES_BROWSER_MARKER_ARG,
]);
});
});
+8
View File
@@ -0,0 +1,8 @@
/** An inert Chrome switch on every browser HyperFrames launches, so orphan cleanup can tell its own from other tools'. */
export const HYPERFRAMES_BROWSER_MARKER_ARG = "--hyperframes-owned-browser";
export function withHyperframesBrowserMarker(args: readonly string[] = []): string[] {
return args.includes(HYPERFRAMES_BROWSER_MARKER_ARG)
? [...args]
: [...args, HYPERFRAMES_BROWSER_MARKER_ARG];
}
@@ -0,0 +1,74 @@
import { HYPERFRAMES_BROWSER_MARKER_ARG } from "@hyperframes/core/browser-marker";
import type { LaunchOptions, PuppeteerNode } from "puppeteer-core";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
_resetAutoBrowserGpuModeCacheForTests,
_resetBrowserPoolForTests,
_setPuppeteerForTests,
acquireBrowser,
drainBrowserPool,
resolveBrowserGpuMode,
} from "./browserManager.js";
function fakeChrome(newPage = vi.fn().mockResolvedValue({ evaluate: vi.fn() })) {
const launch = vi.fn(async (_options: LaunchOptions) => ({
connected: true,
newPage,
version: vi.fn().mockResolvedValue("HeadlessChrome/152.0.7977.30"),
close: vi.fn().mockResolvedValue(undefined),
disconnect: vi.fn(),
process: () => ({ kill: vi.fn(), killed: false }),
}));
_setPuppeteerForTests({ launch } as unknown as PuppeteerNode);
return launch;
}
const launchedArgs = (launch: ReturnType<typeof fakeChrome>) =>
launch.mock.calls.map(([options]) => options.args ?? []);
describe("every engine Chrome carries the HyperFrames marker", () => {
beforeEach(() => {
vi.stubEnv("PRODUCER_HEADLESS_SHELL_PATH", process.execPath);
vi.spyOn(console, "log").mockImplementation(() => {});
vi.spyOn(console, "warn").mockImplementation(() => {});
vi.spyOn(console, "error").mockImplementation(() => {});
});
afterEach(async () => {
await drainBrowserPool();
_resetBrowserPoolForTests();
_resetAutoBrowserGpuModeCacheForTests();
_setPuppeteerForTests(undefined);
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
it("marks the GPU probe browser", async () => {
const launch = fakeChrome();
await resolveBrowserGpuMode("auto", { platform: "linux" });
expect(launchedArgs(launch)[0]).toContain(HYPERFRAMES_BROWSER_MARKER_ARG);
});
it("marks the capture browser without changing the caller's args", async () => {
const launch = fakeChrome();
const args = ["--no-sandbox"];
const lease = await acquireBrowser(args, { forceScreenshot: true });
await lease.release();
expect(launchedArgs(launch)).toEqual([["--no-sandbox", HYPERFRAMES_BROWSER_MARKER_ARG]]);
expect(args).toEqual(["--no-sandbox"]);
});
it.runIf(process.platform === "linux")(
"marks the screenshot relaunch after a failed beginFrame probe",
async () => {
const launch = fakeChrome(vi.fn().mockRejectedValue(new Error("no beginFrame")));
const lease = await acquireBrowser(["--enable-begin-frame-control"], {
forceScreenshot: false,
});
await lease.release();
const args = launchedArgs(launch);
expect(args).toHaveLength(2);
for (const launched of args) expect(launched).toContain(HYPERFRAMES_BROWSER_MARKER_ARG);
},
);
});
@@ -6,6 +6,7 @@
* launch args, pooled browser acquisition/release.
*/
import { withHyperframesBrowserMarker } from "@hyperframes/core/browser-marker";
import { scanHtmlOpeningTags } from "@hyperframes/parsers/html-attribute-spans";
import type { Browser, LaunchOptions, Page, PuppeteerNode } from "puppeteer-core";
import { execSync } from "child_process";
@@ -111,7 +112,7 @@ async function probeHardwareWebGlInfo(
const launchOptions: LaunchOptions = {
...sigintLaunchOptions(),
headless: true,
args: options.args,
args: withHyperframesBrowserMarker(options.args),
defaultViewport: { width: 64, height: 64 },
executablePath: options.executablePath,
env: options.env,
@@ -854,7 +855,7 @@ async function launchBrowser(
browser = await ppt.launch({
...sigintLaunchOptions(),
headless: true,
args: [...fingerprint.args],
args: withHyperframesBrowserMarker(fingerprint.args),
defaultViewport: null,
executablePath: fingerprint.executablePath,
env,
@@ -887,7 +888,7 @@ async function launchBrowser(
browser = await ppt.launch({
...sigintLaunchOptions(),
headless: true,
args: stripBeginFrameFlags([...fingerprint.args]),
args: withHyperframesBrowserMarker(stripBeginFrameFlags([...fingerprint.args])),
defaultViewport: null,
executablePath: fingerprint.executablePath,
env,
@@ -0,0 +1,23 @@
import { HYPERFRAMES_BROWSER_MARKER_ARG } from "@hyperframes/core/browser-marker";
import type { LaunchOptions } from "puppeteer-core";
import { expect, it, vi } from "vitest";
const launch = vi.hoisted(() =>
vi.fn(async (_options: LaunchOptions) => ({
newPage: async () => ({ setViewport: async () => undefined }),
})),
);
vi.mock("puppeteer", () => ({ default: { launch } }));
vi.mock("puppeteer-core", () => ({ default: { launch } }));
vi.mock("./browserManager.js", async (importOriginal) => ({
...(await importOriginal<typeof import("./browserManager.js")>()),
resolveFullChromePath: () => "/opt/chrome/chrome",
}));
const { launchHdrBrowser } = await import("./hdrCapture.js");
it("marks the HDR browser", async () => {
await launchHdrBrowser(64, 64);
expect(launch).toHaveBeenCalledTimes(1);
expect(launch.mock.calls[0]![0].args).toContain(HYPERFRAMES_BROWSER_MARKER_ARG);
});
+2 -1
View File
@@ -19,6 +19,7 @@
* Performance: ~6 fps at 1080x1920 via base64 transfer.
*/
import { withHyperframesBrowserMarker } from "@hyperframes/core/browser-marker";
import type { Page, Browser, PuppeteerNode } from "puppeteer-core";
import { resolveFullChromePath, sigintLaunchOptions } from "./browserManager.js";
@@ -249,7 +250,7 @@ export async function launchHdrBrowser(
...sigintLaunchOptions(),
headless: false,
executablePath: chromePath,
args: buildHdrChromeArgs(width, height),
args: withHyperframesBrowserMarker(buildHdrChromeArgs(width, height)),
});
const page = await browser.newPage();
+3 -2
View File
@@ -1,4 +1,5 @@
import type { TimelineRequest } from "../../core/src/runtime/timelineInspection";
import { withHyperframesBrowserMarker } from "../../core/src/browserMarker";
import {
inspectComponents,
inspectElementMap,
@@ -176,12 +177,12 @@ export class Inspector {
this.browser = await puppeteer.launch({
executablePath: this.options.executablePath ?? chromePath(),
headless: true,
args: [
args: withHyperframesBrowserMarker([
"--disable-dev-shm-usage",
"--enable-webgl",
"--use-gl=angle",
"--use-angle=swiftshader",
],
]),
});
this.stats.browserStarts++;
}
@@ -2,26 +2,32 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, it, vi } from "vitest";
import { HYPERFRAMES_BROWSER_MARKER_ARG } from "../../core/src/browserMarker";
import { Inspector } from "../src/service";
const launches = vi.hoisted(() => [] as { args?: string[] }[]);
vi.mock("puppeteer-core", () => ({
default: {
launch: async () => ({
close: async () => {},
newPage: async () => {
let html = "";
return {
setViewport: async () => {},
goto: async (url: string) => {
html = await (await fetch(url)).text();
},
waitForFunction: async () => {},
evaluate: async () => {},
content: async () => html,
close: async () => {},
};
},
}),
launch: async (options: { args?: string[] }) => {
launches.push(options);
return {
close: async () => {},
newPage: async () => {
let html = "";
return {
setViewport: async () => {},
goto: async (url: string) => {
html = await (await fetch(url)).text();
},
waitForFunction: async () => {},
evaluate: async () => {},
content: async () => html,
close: async () => {},
};
},
};
},
},
}));
@@ -50,3 +56,24 @@ it("reloads the standalone page when an HF ID is stamped onto disk", async () =>
rmSync(project, { recursive: true, force: true });
}
});
it("launches its browser with the HyperFrames marker", async () => {
const project = mkdtempSync(join(tmpdir(), "inspect-marker-"));
writeFileSync(
join(project, "index.html"),
'<!doctype html><html><body><main data-composition-id="root" data-duration="1" data-width="64" data-height="64"></main></body></html>',
);
const inspector = new Inspector(project, {
executablePath: "browser-stand-in",
instrument: false,
});
try {
launches.length = 0;
await inspector.withPage((page) => page.content());
expect(launches).toHaveLength(1);
expect(launches[0]!.args).toContain(HYPERFRAMES_BROWSER_MARKER_ARG);
} finally {
await inspector.dispose();
rmSync(project, { recursive: true, force: true });
}
});
+3 -2
View File
@@ -4,6 +4,7 @@ import { dirname, join, resolve } from "node:path";
import process from "node:process";
import { spawnSync } from "node:child_process";
import puppeteer, { type Browser, type Page } from "puppeteer-core";
import { withHyperframesBrowserMarker } from "@hyperframes/core/browser-marker";
import { MEDIA_VISUAL_STYLE_PROPERTIES, quantizeTimeToFrame } from "./utils/parityContract.js";
type ParityHarnessOptions = {
@@ -319,7 +320,7 @@ async function run(): Promise<void> {
height: options.height,
deviceScaleFactor: 1,
},
args: [
args: withHyperframesBrowserMarker([
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
@@ -331,7 +332,7 @@ async function run(): Promise<void> {
"--font-render-hinting=none",
"--force-color-profile=srgb",
`--window-size=${options.width},${options.height}`,
],
]),
});
try {
+2 -1
View File
@@ -2,6 +2,7 @@
// error, catching a script-fetched asset a static markup scan can't see.
import puppeteer, { type Browser } from "puppeteer";
import { withHyperframesBrowserMarker } from "@hyperframes/core/browser-marker";
import { injectScriptsAtHeadStart } from "@hyperframes/core/compiler";
import { getVerifiedHyperframeRuntimeSource } from "./services/hyperframeRuntimeLoader.js";
@@ -13,7 +14,7 @@ export async function launchVerifyBrowser(): Promise<Browser> {
return puppeteer.launch({
headless: true,
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
args: withHyperframesBrowserMarker(["--no-sandbox", "--disable-setuid-sandbox"]),
});
}