import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdtempSync, readFileSync, rmSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { killChildTreesNow, killsChildTrees, runChildIntoLog, setKillChildTrees } from "./runChild"; // Run with: // pnpm --filter yt-dlp-transcript-common test // // Release 18: a publish stage child turns on tree-kill, so a cancel takes the // child's whole process group — `next build`'s workers, wrangler — not just // the direct child. A shell that starts a background grandchild stands in. function alive(pid: number): boolean { try { process.kill(pid, 0); return true; } catch { return false; } } async function waitFor(cond: () => boolean, ms = 5_000): Promise { const end = Date.now() + ms; while (Date.now() < end) { if (cond()) return true; await new Promise((r) => setTimeout(r, 25)); } return cond(); } async function runWithGrandchild(tree: boolean): Promise<{ grandchild: number; code: number }> { const dir = mkdtempSync(path.join(os.tmpdir(), "run-child-")); const pidFile = path.join(dir, "gc.pid"); setKillChildTrees(tree); try { const ac = new AbortController(); const running = runChildIntoLog(() => {}, ac.signal, { command: "sh", args: ["-c", `sleep 30 & echo $! > '${pidFile}'; wait`], cwd: dir, }); let grandchild = 0; await waitFor(() => { try { grandchild = Number(readFileSync(pidFile, "utf8").trim()); return grandchild > 0; } catch { return false; } }); ac.abort(); const code = await running; return { grandchild, code }; } finally { setKillChildTrees(false); rmSync(dir, { recursive: true, force: true }); } } test("tree-kill is off by default", () => { assert.equal(killsChildTrees(), false); }); test("with tree-kill on, a cancel takes the grandchildren too", { skip: process.platform === "win32" }, async () => { const { grandchild, code } = await runWithGrandchild(true); assert.notEqual(code, 0); assert.ok(await waitFor(() => !alive(grandchild)), `grandchild ${grandchild} survived`); }); test("killChildTreesNow (a second Ctrl-C) takes every live group down at once, no cancel needed", { skip: process.platform === "win32" }, async () => { const dir = mkdtempSync(path.join(os.tmpdir(), "run-child-now-")); const pidFile = path.join(dir, "gc.pid"); setKillChildTrees(true); try { const running = runChildIntoLog(() => {}, new AbortController().signal, { command: "sh", args: ["-c", `sleep 30 & echo $! > '${pidFile}'; wait`], cwd: dir, }); let grandchild = 0; await waitFor(() => { try { grandchild = Number(readFileSync(pidFile, "utf8").trim()); return grandchild > 0; } catch { return false; } }); killChildTreesNow("SIGKILL"); assert.notEqual(await running, 0); assert.ok(await waitFor(() => !alive(grandchild)), `grandchild ${grandchild} survived`); } finally { setKillChildTrees(false); rmSync(dir, { recursive: true, force: true }); } });