mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
fix: clean up orphaned Chrome and ffmpeg processes on preview exit
The preview command's shutdown handler only closed the HTTP server, leaving Chrome (browser pool) and ffmpeg processes alive. This caused silent resource leaks — orphaned processes consuming CPU and RAM with no parent. Root cause: preview.ts never called drainBrowserPool() or killed tracked ffmpeg processes. The thumbnail browser in studioServer.ts registered its own competing signal handlers that raced with preview's shutdown. Fix: - Add a central process tracker (processTracker.ts) that registers every spawned ffmpeg across engine and producer packages - Centralize thumbnail browser cleanup via exported closeThumbnailBrowser() instead of scattered signal handlers - Wire preview shutdown to call closeThumbnailBrowser(), drainBrowserPool(), and killTrackedProcesses() before closing the HTTP server (embedded mode) - Add killProcessTree() for dev/local modes where Chrome runs in a child process tree - Add startup orphan detection that finds and kills orphaned chrome-headless-shell/Puppeteer Chrome processes (PPID=1) from previously crashed sessions Closes #1038
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
/**
|
||||
* Find and kill orphaned Chrome processes from previous crashed sessions.
|
||||
* Targets both chrome-headless-shell (production/CI) and Google Chrome
|
||||
* launched by Puppeteer (dev mode). Puppeteer Chrome is identified by the
|
||||
* `puppeteer_dev_chrome_profile` marker in its user-data-dir argument.
|
||||
*
|
||||
* An orphan is a process whose PPID=1 (reparented to init/launchd after
|
||||
* its parent died). We kill the orphan's entire subtree so child helper
|
||||
* processes (GPU, renderer, network, etc.) are also cleaned up.
|
||||
*
|
||||
* Returns the count of killed process trees.
|
||||
*/
|
||||
export function killOrphanedProcesses(): number {
|
||||
if (process.platform === "win32") return 0;
|
||||
|
||||
let killed = 0;
|
||||
|
||||
// chrome-headless-shell: used in production/CI via the engine's browser manager.
|
||||
for (const name of ["chrome-headless-shell", "chrome_headless_shell"]) {
|
||||
killed += killOrphansByName(name);
|
||||
}
|
||||
|
||||
// Puppeteer-launched Chrome (dev mode): identified by the temp profile dir
|
||||
// that Puppeteer creates. This avoids killing the user's real Chrome.
|
||||
killed += killOrphansByName("puppeteer_dev_chrome_profile");
|
||||
|
||||
return killed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill an entire process tree rooted at `pid`. Walks descendants
|
||||
* depth-first so children are killed before parents, preventing
|
||||
* re-adoption races.
|
||||
*/
|
||||
export function killProcessTree(pid: number, signal: NodeJS.Signals = "SIGTERM"): void {
|
||||
if (process.platform === "win32") return;
|
||||
|
||||
const descendants = getDescendants(pid);
|
||||
for (const child of descendants.reverse()) {
|
||||
try {
|
||||
process.kill(child, signal);
|
||||
} catch {
|
||||
// Already exited.
|
||||
}
|
||||
}
|
||||
try {
|
||||
process.kill(pid, signal);
|
||||
} catch {
|
||||
// Already exited.
|
||||
}
|
||||
}
|
||||
|
||||
function getDescendants(pid: number): number[] {
|
||||
let children: number[];
|
||||
try {
|
||||
const raw = execSync(`pgrep -P ${pid}`, { encoding: "utf-8", timeout: 2000 }).trim();
|
||||
if (!raw) return [];
|
||||
children = raw
|
||||
.split("\n")
|
||||
.map((s) => parseInt(s, 10))
|
||||
.filter((n) => !isNaN(n) && n > 0);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const all: number[] = [];
|
||||
for (const child of children) {
|
||||
all.push(child);
|
||||
all.push(...getDescendants(child));
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
function killOrphansByName(processName: string): number {
|
||||
let pids: number[];
|
||||
try {
|
||||
const raw = execSync(`pgrep -f ${processName}`, {
|
||||
encoding: "utf-8",
|
||||
timeout: 3000,
|
||||
}).trim();
|
||||
if (!raw) return 0;
|
||||
pids = raw
|
||||
.split("\n")
|
||||
.map((s) => parseInt(s, 10))
|
||||
.filter((n) => !isNaN(n) && n > 0);
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let killed = 0;
|
||||
for (const pid of pids) {
|
||||
if (!isOrphan(pid)) continue;
|
||||
killProcessTree(pid);
|
||||
killed++;
|
||||
}
|
||||
return killed;
|
||||
}
|
||||
|
||||
function isOrphan(pid: number): boolean {
|
||||
try {
|
||||
const ppid = execSync(`ps -p ${pid} -o ppid=`, {
|
||||
encoding: "utf-8",
|
||||
timeout: 2000,
|
||||
}).trim();
|
||||
return ppid === "1";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user