fix(scripts): kill managed child trees on windows

This commit is contained in:
Vincent Koc
2026-06-20 13:39:16 +02:00
parent 66f84a9bf1
commit 85f71f4c8f
2 changed files with 46 additions and 4 deletions

View File

@@ -1,5 +1,5 @@
// Runs child commands with process-group signal forwarding and Windows shell normalization.
import { spawn } from "node:child_process";
import { spawn, spawnSync } from "node:child_process";
import { constants as osConstants } from "node:os";
import { buildCmdExeCommandLine } from "../windows-cmd-helpers.mjs";
@@ -22,14 +22,19 @@ export function signalExitCode(signal) {
/**
* @param {import("node:child_process").ChildProcess} child
* @param {NodeJS.Signals} [signal]
* @param {{ platform?: NodeJS.Platform; runTaskkill?: typeof spawnSync }} [options]
*/
function terminateManagedChild(child, signal = "SIGTERM") {
export function terminateManagedChild(
child,
signal = "SIGTERM",
{ platform = process.platform, runTaskkill = spawnSync } = {},
) {
if (!child.pid) {
return;
}
try {
if (process.platform !== "win32") {
if (platform !== "win32") {
process.kill(-child.pid, signal);
return;
}
@@ -44,6 +49,17 @@ function terminateManagedChild(child, signal = "SIGTERM") {
return;
}
if (platform === "win32") {
const args = ["/PID", String(child.pid), "/T"];
if (signal === "SIGKILL") {
args.push("/F");
}
const result = runTaskkill("taskkill", args, { stdio: "ignore" });
if (!result?.error && result?.status === 0) {
return;
}
}
child.kill(signal);
}

View File

@@ -4,11 +4,12 @@ import fs from "node:fs";
import path from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { pathToFileURL } from "node:url";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import {
createManagedCommandSpawnSpec,
runManagedCommand,
signalExitCode,
terminateManagedChild,
} from "../../scripts/lib/managed-child-process.mjs";
import { createScriptTestHarness } from "./test-helpers.js";
@@ -110,6 +111,31 @@ describe("managed-child-process", () => {
).toThrow("unsafe Windows cmd.exe argument detected");
});
it("signals Windows managed process trees with taskkill", () => {
const child = {
kill: vi.fn(),
pid: 12345,
};
const runTaskkill = vi.fn(() => ({ error: undefined, status: 0 }));
terminateManagedChild(child, "SIGTERM", {
platform: "win32",
runTaskkill,
});
expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12345", "/T"], {
stdio: "ignore",
});
terminateManagedChild(child, "SIGKILL", {
platform: "win32",
runTaskkill,
});
expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "12345", "/T", "/F"], {
stdio: "ignore",
});
expect(child.kill).not.toHaveBeenCalled();
});
it("shares process signal listeners across parallel managed commands", async () => {
const signals = ["SIGHUP", "SIGINT", "SIGTERM"] as const;
const baseline = new Map(signals.map((signal) => [signal, process.listenerCount(signal)]));