mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
fix(cli): check never hangs inside an agent's macOS sandbox (#5836)
Inside Codex's seatbelt the GPU probe's Metal Chrome is always killed, and when it dies after puppeteer's setAutoAttach is answered, puppeteer waits forever for its initial targets. The probe now answers software in a named seatbelt without starting Chrome, and every CLI browser launch is bounded by its timeout, so a stuck launch rejects and a signal can end the command.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { LaunchOptions, PuppeteerNode } from "puppeteer-core";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const probe = vi.hoisted(() => ({ options: [] as LaunchOptions[] }));
|
||||
|
||||
@@ -36,3 +36,19 @@ describe("the GPU probe where nothing may listen", () => {
|
||||
expect(probe.options[1]!.args).toEqual(["--headless"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the GPU probe inside an agent's macOS seatbelt", () => {
|
||||
const platform = process.platform;
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
Object.defineProperty(process, "platform", { value: platform });
|
||||
});
|
||||
|
||||
it("answers software without starting a Chrome the seatbelt would crash", async () => {
|
||||
vi.stubEnv("CODEX_SANDBOX", "seatbelt");
|
||||
Object.defineProperty(process, "platform", { value: "darwin" });
|
||||
probe.options.length = 0;
|
||||
await expect(resolveManagedGpuMode("auto", "/chrome")).resolves.toBe("software");
|
||||
expect(probe.options).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ const closing = new WeakMap<Browser, Promise<void>>();
|
||||
const pending = new Map<Promise<Browser>, AbortController>();
|
||||
const sessionCleanups = new Set<() => Promise<void>>();
|
||||
const CLOSE_GRACE_MS = 1_000;
|
||||
const LAUNCH_TIMEOUT_MS = 30_000;
|
||||
const EXIT_POLL_MS = 50;
|
||||
let listening = false;
|
||||
let shutdown: Promise<void> | undefined;
|
||||
@@ -57,6 +58,7 @@ export async function resolveManagedGpuMode(
|
||||
} = {},
|
||||
): Promise<ResolvedBrowserGpuMode> {
|
||||
ownSignals();
|
||||
if (requestedMode === "auto" && inAgentSandbox()) return "software";
|
||||
return resolveCaptureBrowserGpuMode(requestedMode, chromePath, {
|
||||
launch: async (puppeteer, options) => {
|
||||
context.signal?.throwIfAborted();
|
||||
@@ -108,12 +110,18 @@ async function launchOwnedBrowser(
|
||||
ownSignals();
|
||||
const abort = new AbortController();
|
||||
const signal = options.signal ? AbortSignal.any([options.signal, abort.signal]) : abort.signal;
|
||||
const launch = puppeteer.launch({
|
||||
...options,
|
||||
const launch = untilLaunchOrAbort(
|
||||
puppeteer.launch({ ...options, signal, handleSIGINT: false, handleSIGTERM: false }),
|
||||
signal,
|
||||
handleSIGINT: false,
|
||||
handleSIGTERM: false,
|
||||
});
|
||||
);
|
||||
const limit = options.timeout ?? LAUNCH_TIMEOUT_MS;
|
||||
const timer =
|
||||
limit > 0
|
||||
? setTimeout(
|
||||
() => abort.abort(new Error(`The browser did not finish starting within ${limit} ms.`)),
|
||||
limit,
|
||||
)
|
||||
: undefined;
|
||||
pending.set(launch, abort);
|
||||
try {
|
||||
const browser = await launch;
|
||||
@@ -123,10 +131,22 @@ async function launchOwnedBrowser(
|
||||
child.once("exit", () => browsers.delete(browser));
|
||||
return browser;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
pending.delete(launch);
|
||||
}
|
||||
}
|
||||
|
||||
function untilLaunchOrAbort(launch: Promise<Browser>, signal: AbortSignal): Promise<Browser> {
|
||||
const aborted = new Promise<never>((_, reject) => {
|
||||
if (signal.aborted) reject(signal.reason);
|
||||
signal.addEventListener("abort", () => reject(signal.reason), { once: true });
|
||||
});
|
||||
void launch
|
||||
.then((late) => (signal.aborted ? closeManagedBrowser(late) : undefined))
|
||||
.catch(() => undefined);
|
||||
return Promise.race([launch, aborted]);
|
||||
}
|
||||
|
||||
/** Resolved launches keep their signal live; only pending launches are aborted during shutdown. */
|
||||
async function closeManagedBrowsers(): Promise<void> {
|
||||
for (const abort of pending.values()) abort.abort();
|
||||
|
||||
@@ -69,3 +69,12 @@ describe("a browser inside an agent's macOS seatbelt", () => {
|
||||
expect(launches[1]!.args).toEqual(["--headless", "--single-process"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a browser launch that never settles", () => {
|
||||
it("gives up at its timeout, as when Chrome dies while puppeteer waits for its first targets", async () => {
|
||||
const stuck = { launch: () => new Promise<Browser>(() => {}) } as unknown as PuppeteerNode;
|
||||
await expect(launchManagedBrowser(stuck, { timeout: 50 })).rejects.toThrow(
|
||||
"The browser did not finish starting within 50 ms.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user