From 068a42dd425bf6d85e7c0a3773698c343d41b3b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Mon, 13 Jul 2026 14:55:31 +0000 Subject: [PATCH] fix(render): recover orphaned browsers before retry --- packages/cli/src/commands/render.test.ts | 30 ++++++++++++++++++++++++ packages/cli/src/commands/render.ts | 10 ++++++++ 2 files changed, 40 insertions(+) diff --git a/packages/cli/src/commands/render.test.ts b/packages/cli/src/commands/render.test.ts index c1179c7545..b0665dcf72 100644 --- a/packages/cli/src/commands/render.test.ts +++ b/packages/cli/src/commands/render.test.ts @@ -72,6 +72,11 @@ const preflightState = vi.hoisted(() => ({ }, })); +const orphanCleanupState = vi.hoisted(() => ({ + calls: 0, + killed: 0, +})); + vi.mock("../utils/producer.js", () => ({ loadProducer: vi.fn(async () => ({ resolveConfig: vi.fn((overrides: Record) => { @@ -128,6 +133,13 @@ vi.mock("../browser/preflight.js", () => ({ runEnvironmentChecks: vi.fn(async () => preflightState.result), })); +vi.mock("../utils/orphanCleanup.js", () => ({ + killOrphanedProcesses: vi.fn(() => { + orphanCleanupState.calls += 1; + return orphanCleanupState.killed; + }), +})); + describe("renderLocal browser GPU config", () => { const savedEnv = new Map(); // Pre-resolve once. The first dynamic `import("./render.js")` in this file @@ -173,6 +185,8 @@ describe("renderLocal browser GPU config", () => { configState.writeConfigCalls = []; trackingState.shouldTrack = true; trackingState.renderObservations = []; + orphanCleanupState.calls = 0; + orphanCleanupState.killed = 0; resetTrialState(); savedEnv.clear(); savedEnv.set("HYPERFRAMES_FFMPEG_PATH", process.env.HYPERFRAMES_FFMPEG_PATH); @@ -185,6 +199,22 @@ describe("renderLocal browser GPU config", () => { delete process.env.HF_DE_PARALLEL_ROUTER; }); + it("cleans orphaned browser trees before starting a local render", async () => { + orphanCleanupState.killed = 1; + + await renderLocal("/tmp/project", "/tmp/out.mp4", { + fps: { num: 30, den: 1 }, + quality: "standard", + format: "mp4", + gpu: false, + browserGpuMode: "software", + hdrMode: "auto", + quiet: true, + }); + + expect(orphanCleanupState.calls).toBe(1); + }); + afterEach(() => { for (const [key, value] of savedEnv) { if (value === undefined) { diff --git a/packages/cli/src/commands/render.ts b/packages/cli/src/commands/render.ts index f4e3e5df61..75fd3c574f 100644 --- a/packages/cli/src/commands/render.ts +++ b/packages/cli/src/commands/render.ts @@ -78,6 +78,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 { killOrphanedProcesses } from "../utils/orphanCleanup.js"; import type { ProducerLogger, RenderJob } from "@hyperframes/producer"; import { MAX_VP9_CPU_USED, @@ -1425,6 +1426,15 @@ export async function renderLocal( outputPath: string, options: RenderOptions, ): Promise { + const recoveredOrphanTrees = killOrphanedProcesses(); + if (recoveredOrphanTrees > 0 && !options.quiet) { + console.warn( + c.warn( + ` Recovered ${recoveredOrphanTrees} orphaned browser process ${recoveredOrphanTrees === 1 ? "tree" : "trees"} from an interrupted render.`, + ), + ); + } + const preflight = await runEnvironmentChecks({ projectDir, browserPath: options.browserPath,