Skip to content

Commit 8d2e6d7

Browse files
committed
fix(scripts): kill run-with-env trees on windows
1 parent b039e94 commit 8d2e6d7

2 files changed

Lines changed: 65 additions & 16 deletions

File tree

scripts/run-with-env.mjs

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Runs a command with inline KEY=value assignments while preserving signal behavior.
2-
import { spawn } from "node:child_process";
2+
import { spawn, spawnSync } from "node:child_process";
33

44
const ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/u;
55
const USAGE = "Usage: node scripts/run-with-env.mjs KEY=value [KEY=value ...] -- command [args...]";
@@ -80,6 +80,42 @@ export function resolveForceKillDelayMs(env = process.env) {
8080
return parsed;
8181
}
8282

83+
/**
84+
* Signals the wrapped command tree when this small parent wrapper is stopped.
85+
*/
86+
export function signalRunWithEnvChild(
87+
child,
88+
signal,
89+
{
90+
platform = process.platform,
91+
runTaskkill = spawnSync,
92+
useChildProcessGroup = platform !== "win32",
93+
} = {},
94+
) {
95+
if (useChildProcessGroup && typeof child.pid === "number") {
96+
try {
97+
process.kill(-child.pid, signal);
98+
return;
99+
} catch (error) {
100+
if (error?.code !== "ESRCH") {
101+
child.kill(signal);
102+
return;
103+
}
104+
}
105+
}
106+
if (platform === "win32" && typeof child.pid === "number") {
107+
const args = ["/PID", String(child.pid), "/T"];
108+
if (signal === "SIGKILL") {
109+
args.push("/F");
110+
}
111+
const result = runTaskkill("taskkill", args, { stdio: "ignore" });
112+
if (!result?.error && result?.status === 0) {
113+
return;
114+
}
115+
}
116+
child.kill(signal);
117+
}
118+
83119
function main(argv = process.argv.slice(2)) {
84120
if (isRunWithEnvHelpRequest(argv)) {
85121
console.log(USAGE);
@@ -120,20 +156,7 @@ function main(argv = process.argv.slice(2)) {
120156
const forwardedSignals = useChildProcessGroup
121157
? ["SIGTERM", "SIGHUP", "SIGINT"]
122158
: ["SIGTERM", "SIGHUP"];
123-
const signalChild = (signal) => {
124-
if (useChildProcessGroup && typeof child.pid === "number") {
125-
try {
126-
process.kill(-child.pid, signal);
127-
return;
128-
} catch (error) {
129-
if (error?.code !== "ESRCH") {
130-
child.kill(signal);
131-
return;
132-
}
133-
}
134-
}
135-
child.kill(signal);
136-
};
159+
const signalChild = (signal) => signalRunWithEnvChild(child, signal, { useChildProcessGroup });
137160
const childProcessGroupAlive = () => {
138161
if (!useChildProcessGroup || typeof child.pid !== "number") {
139162
return false;

test/scripts/run-with-env.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import { spawn, spawnSync, type ChildProcess } from "node:child_process";
33
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
44
import { tmpdir } from "node:os";
55
import path from "node:path";
6-
import { describe, expect, it } from "vitest";
6+
import { describe, expect, it, vi } from "vitest";
77
import {
88
isRunWithEnvHelpRequest,
99
parseRunWithEnvArgs,
1010
resolveForceKillDelayMs,
1111
resolveSpawnCommand,
12+
signalRunWithEnvChild,
1213
} from "../../scripts/run-with-env.mjs";
1314

1415
async function waitFor(predicate: () => boolean, label: string, timeoutMs = 3_000): Promise<void> {
@@ -158,6 +159,31 @@ describe("run-with-env", () => {
158159
);
159160
});
160161

162+
it("signals Windows wrapped command trees with taskkill", () => {
163+
const child = {
164+
kill: vi.fn(),
165+
pid: 12345,
166+
};
167+
const runTaskkill = vi.fn(() => ({ error: undefined, status: 0 }));
168+
169+
signalRunWithEnvChild(child, "SIGTERM", {
170+
platform: "win32",
171+
runTaskkill,
172+
});
173+
expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12345", "/T"], {
174+
stdio: "ignore",
175+
});
176+
177+
signalRunWithEnvChild(child, "SIGKILL", {
178+
platform: "win32",
179+
runTaskkill,
180+
});
181+
expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "12345", "/T", "/F"], {
182+
stdio: "ignore",
183+
});
184+
expect(child.kill).not.toHaveBeenCalled();
185+
});
186+
161187
it.runIf(process.platform !== "win32").each(["SIGTERM", "SIGHUP", "SIGINT"] as const)(
162188
"forwards parent %s to the wrapped command",
163189
async (signal) => {

0 commit comments

Comments
 (0)