refactor(engine): manage child process lifecycles (#2160)

* refactor(engine): manage child process lifecycles

* fix(engine): preserve child reaping after runtime errors

* fix(engine): untrack child processes on exit
This commit is contained in:
James Russo
2026-07-17 01:17:53 -04:00
committed by GitHub
parent 8e162921bc
commit 57d3bf4960
17 changed files with 697 additions and 537 deletions
+31 -57
View File
@@ -8,6 +8,8 @@
import { spawn } from "child_process";
import { getFfmpegBinary } from "./ffmpegBinaries.js";
import { ManagedChildProcess } from "./managedChildProcess.js";
import { trackChildProcess } from "./processTracker.js";
export type ConcreteGpuEncoder = "nvenc" | "videotoolbox" | "vaapi" | "qsv" | "amf";
export type GpuEncoder = ConcreteGpuEncoder | null;
@@ -60,25 +62,20 @@ export async function selectUsableGpuEncoder(
}
export async function detectGpuEncoder(): Promise<GpuEncoder> {
return new Promise((resolve) => {
const ffmpeg = spawn(getFfmpegBinary(), ["-encoders"], {
stdio: ["pipe", "pipe", "pipe"],
});
let stdout = "";
ffmpeg.stdout.on("data", (data) => {
stdout += data.toString();
});
ffmpeg.on("close", () => {
const candidates = getCompiledGpuEncoders(stdout);
void selectUsableGpuEncoder(candidates, canUseGpuEncoder)
.then(resolve)
.catch(() => resolve(null));
});
ffmpeg.on("error", () => resolve(null));
const ffmpeg = spawn(getFfmpegBinary(), ["-encoders"], {
stdio: ["pipe", "pipe", "pipe"],
});
trackChildProcess(ffmpeg);
let stdout = "";
ffmpeg.stdout.on("data", (data) => {
stdout += data.toString();
});
const outcome = await new ManagedChildProcess(ffmpeg, {
deadlineAtMs: Date.now() + 30_000,
}).wait();
if (outcome.reason !== "exit" || outcome.exitCode !== 0) return null;
const candidates = getCompiledGpuEncoders(stdout);
return selectUsableGpuEncoder(candidates, canUseGpuEncoder).catch(() => null);
}
let cachedGpuEncoder: GpuEncoder | undefined = undefined;
@@ -147,46 +144,23 @@ export function getProbeArgs(encoder: ConcreteGpuEncoder): string[] {
}
async function canUseGpuEncoder(encoder: ConcreteGpuEncoder): Promise<boolean> {
return new Promise((resolve) => {
let settled = false;
let timedOut = false;
let killTimer: ReturnType<typeof setTimeout> | undefined;
let stderr = "";
const finish = (usable: boolean) => {
if (settled) return;
settled = true;
clearTimeout(timer);
if (killTimer) clearTimeout(killTimer);
resolve(usable);
};
const ffmpeg = spawn(getFfmpegBinary(), getProbeArgs(encoder), {
stdio: ["ignore", "ignore", "pipe"],
});
ffmpeg.stderr?.on("data", (data) => {
stderr += data.toString();
});
const timer = setTimeout(() => {
timedOut = true;
ffmpeg.kill("SIGTERM");
killTimer = setTimeout(() => {
ffmpeg.kill("SIGKILL");
finish(false);
}, GPU_PROBE_KILL_GRACE_MS);
}, GPU_PROBE_TIMEOUT_MS);
ffmpeg.on("close", (code, signal) => {
const usable = code === 0;
logGpuProbeFailure(encoder, { code, signal, stderr, timedOut });
finish(usable);
});
ffmpeg.on("error", (error) => {
logGpuProbeFailure(encoder, { error, timedOut });
finish(false);
});
const ffmpeg = spawn(getFfmpegBinary(), getProbeArgs(encoder), {
stdio: ["ignore", "ignore", "pipe"],
});
trackChildProcess(ffmpeg);
const outcome = await new ManagedChildProcess(ffmpeg, {
deadlineAtMs: Date.now() + GPU_PROBE_TIMEOUT_MS,
terminationGraceMs: GPU_PROBE_KILL_GRACE_MS,
}).wait();
const usable = outcome.reason === "exit" && outcome.exitCode === 0;
logGpuProbeFailure(encoder, {
code: outcome.exitCode,
signal: outcome.signal,
stderr: outcome.stderr,
error: outcome.error,
timedOut: outcome.reason === "deadline",
});
return usable;
}
function logGpuProbeFailure(