Files

47 lines
1.5 KiB
JavaScript

import { spawn } from "node:child_process";
import { setTimeout as delay } from "node:timers/promises";
// Unix children must be spawned detached so their process group belongs to us.
export async function stopProcessTree(child) {
if (!child?.pid) return;
if (process.platform === "win32") {
if (child.exitCode !== null || child.signalCode !== null) return;
let onExit;
const exited = new Promise((resolve) => {
onExit = resolve;
child.once("exit", onExit);
});
try {
await new Promise((resolve, reject) => {
const killer = spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
killer.once("error", reject);
killer.once("close", (code) => {
if (code === 0 || child.exitCode !== null || child.signalCode !== null) resolve();
else reject(new Error(`taskkill failed with exit code ${code}`));
});
});
await exited;
} finally {
child.removeListener("exit", onExit);
}
return;
}
const signal = (value) => {
try {
process.kill(-child.pid, value);
return true;
} catch (error) {
// macOS can return EPERM for a group containing only reparented zombies.
if (error.code === "ESRCH" || (value === 0 && error.code === "EPERM")) return false;
throw error;
}
};
if (!signal("SIGTERM")) return;
// A leader exiting does not imply that its descendants have exited.
for (let i = 0; i < 40; i++) {
await delay(50);
if (!signal(0)) return;
}
signal("SIGKILL");
}