mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-15 22:08:03 +00:00
fix(cli): guard process snapshot capture (#3974)
Co-authored-by: heygengenesis[bot] <262951085+heygengenesis[bot]@users.noreply.github.com> Co-authored-by: miguel.sierra <229591595+miguel-heygen@users.noreply.github.com>
This commit is contained in:
co-authored by
miguel.sierra
parent
ef48485b91
commit
08d48ab753
@@ -0,0 +1,150 @@
|
||||
import { execFileSync, spawn, type ChildProcess, type SpawnOptions } from "node:child_process";
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
|
||||
interface SpawnedProcess {
|
||||
once(event: "error", listener: (error: Error) => void): unknown;
|
||||
once(event: "close", listener: (status: number | null) => void): unknown;
|
||||
}
|
||||
|
||||
export type SpawnProcess = (
|
||||
command: string,
|
||||
args: readonly string[],
|
||||
options: SpawnOptions,
|
||||
) => SpawnedProcess;
|
||||
|
||||
export interface ProcessTreeTerminationOptions {
|
||||
child?: Pick<ChildProcess, "kill">;
|
||||
processGroup?: boolean;
|
||||
signal?: NodeJS.Signals;
|
||||
spawnProcess?: SpawnProcess;
|
||||
}
|
||||
|
||||
export function processIsAlive(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return (error as NodeJS.ErrnoException | undefined)?.code !== "ESRCH";
|
||||
}
|
||||
}
|
||||
|
||||
function processGroupIsAlive(pid: number): boolean {
|
||||
if (process.platform === "linux") {
|
||||
try {
|
||||
return readdirSync("/proc").some((entry) => {
|
||||
if (!/^\d+$/.test(entry)) return false;
|
||||
try {
|
||||
const stat = readFileSync(`/proc/${entry}/stat`, "utf8");
|
||||
const fields = stat.slice(stat.lastIndexOf(") ") + 2).split(/\s+/);
|
||||
return fields[2] === String(pid) && fields[0] !== "Z" && fields[0] !== "X";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
return processIsAlive(-pid);
|
||||
}
|
||||
|
||||
function waitForDelay(delayMs: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
}
|
||||
|
||||
async function waitForProcessGroupExit(pid: number): Promise<void> {
|
||||
while (processGroupIsAlive(pid)) await waitForDelay(20);
|
||||
}
|
||||
|
||||
export function windowsProcessTreeKillArgs(pid: number): string[] {
|
||||
return ["/PID", String(pid), "/T", "/F"];
|
||||
}
|
||||
|
||||
export function terminateWindowsProcessTree(
|
||||
pid: number,
|
||||
spawnProcess: SpawnProcess = (command, args, options) => spawn(command, [...args], options),
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const killer = spawnProcess("taskkill", windowsProcessTreeKillArgs(pid), {
|
||||
stdio: "ignore",
|
||||
windowsHide: true,
|
||||
});
|
||||
killer.once("error", reject);
|
||||
killer.once("close", (status) => {
|
||||
if (status === 0) resolve();
|
||||
else reject(new Error(`taskkill exited with status ${status ?? "unknown"}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function signalProcessGroup(pid: number, signal: NodeJS.Signals): void {
|
||||
try {
|
||||
process.kill(-pid, signal);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function getDescendants(pid: number): number[] {
|
||||
let children: number[];
|
||||
try {
|
||||
const raw = execFileSync("pgrep", ["-P", String(pid)], {
|
||||
encoding: "utf8",
|
||||
timeout: 2_000,
|
||||
}).trim();
|
||||
children = raw
|
||||
.split(/\r?\n/)
|
||||
.map(Number)
|
||||
.filter((childPid) => Number.isInteger(childPid) && childPid > 0);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return children.flatMap((childPid) => [childPid, ...getDescendants(childPid)]);
|
||||
}
|
||||
|
||||
function signalProcesses(pids: readonly number[], signal: NodeJS.Signals): void {
|
||||
for (const pid of pids) {
|
||||
try {
|
||||
process.kill(pid, signal);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
async function terminatePidTree(pid: number, signal: NodeJS.Signals): Promise<NodeJS.Signals> {
|
||||
const pids = [...getDescendants(pid).reverse(), pid];
|
||||
signalProcesses(pids, signal);
|
||||
if (signal === "SIGKILL") return "SIGKILL";
|
||||
await waitForDelay(500);
|
||||
const survivors = pids.filter(processIsAlive);
|
||||
signalProcesses(survivors, "SIGKILL");
|
||||
return survivors.length > 0 ? "SIGKILL" : "SIGTERM";
|
||||
}
|
||||
|
||||
async function terminateProcessGroup(pid: number, signal: NodeJS.Signals): Promise<NodeJS.Signals> {
|
||||
signalProcessGroup(pid, signal);
|
||||
if (signal === "SIGKILL") {
|
||||
await waitForProcessGroupExit(pid);
|
||||
return "SIGKILL";
|
||||
}
|
||||
await Promise.race([waitForProcessGroupExit(pid), waitForDelay(250)]);
|
||||
if (!processGroupIsAlive(pid)) return "SIGTERM";
|
||||
signalProcessGroup(pid, "SIGKILL");
|
||||
await waitForProcessGroupExit(pid);
|
||||
return "SIGKILL";
|
||||
}
|
||||
|
||||
export async function terminateProcessTree(
|
||||
pid: number,
|
||||
options: ProcessTreeTerminationOptions = {},
|
||||
): Promise<NodeJS.Signals> {
|
||||
if (process.platform === "win32") {
|
||||
try {
|
||||
await terminateWindowsProcessTree(pid, options.spawnProcess);
|
||||
} catch (error) {
|
||||
try {
|
||||
options.child?.kill("SIGKILL");
|
||||
} catch {}
|
||||
if (options.child) throw error;
|
||||
}
|
||||
return "SIGKILL";
|
||||
}
|
||||
|
||||
const signal = options.signal ?? "SIGTERM";
|
||||
return options.processGroup ? terminateProcessGroup(pid, signal) : terminatePidTree(pid, signal);
|
||||
}
|
||||
Reference in New Issue
Block a user