fix(scripts): kill boundary prep trees on windows

This commit is contained in:
Vincent Koc
2026-06-21 06:34:10 +02:00
parent 5af318b95d
commit b0f21f8af7
2 changed files with 51 additions and 6 deletions

View File

@@ -1,6 +1,6 @@
// Prepares declaration and entry-shim artifacts that prove plugin package
// boundary imports resolve through public package surfaces.
import { spawn } from "node:child_process";
import { spawn, spawnSync } from "node:child_process";
import fs from "node:fs";
import path, { resolve } from "node:path";
import { isLocalCheckEnabled } from "./lib/local-heavy-check-runtime.mjs";
@@ -399,8 +399,16 @@ function abortSiblingSteps(abortController) {
}
}
function signalNodeStep(child, signal) {
if (process.platform !== "win32" && typeof child.pid === "number") {
export function signalNodeStep(
child,
signal,
{
platform = process.platform,
runTaskkill = spawnSync,
useProcessGroup = platform !== "win32",
} = {},
) {
if (useProcessGroup && typeof child.pid === "number") {
try {
process.kill(-child.pid, signal);
return;
@@ -408,6 +416,16 @@ function signalNodeStep(child, signal) {
// The child process group can already be gone by the time cleanup runs.
}
}
if (platform === "win32" && typeof child.pid === "number") {
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);
}
@@ -464,9 +482,10 @@ export function runNodeStep(label, args, timeoutMs, params = {}) {
let killDeadlineAt = 0;
const stdoutWriter = createPrefixedOutputWriter(label, process.stdout);
const stderrWriter = createPrefixedOutputWriter(label, process.stderr);
const killNodeStep = (signal) => signalNodeStep(child, signal);
const useProcessGroup = process.platform !== "win32";
const killNodeStep = (signal) => signalNodeStep(child, signal, { useProcessGroup });
const processGroupAlive = () => {
if (process.platform === "win32" || !child.pid) {
if (!useProcessGroup || !child.pid) {
return false;
}
try {

View File

@@ -7,7 +7,7 @@ import os from "node:os";
import path from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { pathToFileURL } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
createPrefixedOutputWriter,
isArtifactSetFresh,
@@ -17,6 +17,7 @@ import {
runNodeStep,
runNodeSteps,
runNodeStepsInParallel,
signalNodeStep,
} from "../../scripts/prepare-extension-package-boundary-artifacts.mjs";
import { makeTempDir } from "../helpers/temp-dir.js";
@@ -123,6 +124,31 @@ describe("prepare-extension-package-boundary-artifacts", () => {
expect(Date.now() - startedAt).toBeLessThan(abortBudgetMs);
}, 45_000);
it("signals Windows node step process trees with taskkill", () => {
const child = {
kill: vi.fn(),
pid: 12345,
};
const runTaskkill = vi.fn(() => ({ error: undefined, status: 0 }));
signalNodeStep(child, "SIGTERM", {
platform: "win32",
runTaskkill,
});
expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12345", "/T"], {
stdio: "ignore",
});
signalNodeStep(child, "SIGKILL", {
platform: "win32",
runTaskkill,
});
expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "12345", "/T", "/F"], {
stdio: "ignore",
});
expect(child.kill).not.toHaveBeenCalled();
});
it.runIf(process.platform !== "win32")(
"force-kills aborted sibling step process groups",
async () => {