mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
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:
co-authored by
Claude Opus 5.5
parent
19374955c7
commit
f180ffca64
@@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,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"]),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user