Skip to content

Commit 604aa30

Browse files
committed
fix(qa): taskkill lifecycle probe trees on windows
1 parent 5dd30c3 commit 604aa30

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

test/e2e/qa-lab/plugins/plugin-lifecycle-probe-runtime.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Plugin Lifecycle Probe tests cover QA Lab plugin lifecycle evidence.
2-
import { spawn } from "node:child_process";
2+
import { spawn, spawnSync } from "node:child_process";
33
import { randomBytes } from "node:crypto";
44
import fs from "node:fs";
55
import os from "node:os";
@@ -17,6 +17,7 @@ interface CommandOptions {
1717
env?: NodeJS.ProcessEnv;
1818
outputFile?: string;
1919
spawnImpl?: typeof spawn;
20+
taskkillImpl?: typeof spawnSync;
2021
timeoutKillGraceMs?: number;
2122
timeoutMs?: number;
2223
}
@@ -266,6 +267,26 @@ async function runCommand(command: string, args: readonly string[], options: Com
266267
// The process group may already be gone; fall back to the direct child.
267268
}
268269
}
270+
if (!useProcessGroup && child.pid) {
271+
const runTaskkill = options.taskkillImpl ?? spawnSync;
272+
const args = ["/PID", String(child.pid), "/T"];
273+
if (signal === "SIGKILL") {
274+
args.push("/F");
275+
}
276+
const result = runTaskkill("taskkill", args, { stdio: "ignore", windowsHide: true });
277+
if (!result.error && result.status === 0) {
278+
return;
279+
}
280+
if (signal !== "SIGKILL") {
281+
const forceResult = runTaskkill("taskkill", [...args, "/F"], {
282+
stdio: "ignore",
283+
windowsHide: true,
284+
});
285+
if (!forceResult.error && forceResult.status === 0) {
286+
return;
287+
}
288+
}
289+
}
269290
child.kill(signal);
270291
};
271292
const isProcessGroupRunning = () => {

test/e2e/qa-lab/plugins/plugin-lifecycle-probe.e2e.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,50 @@ describe("plugin lifecycle matrix probe", () => {
136136
}
137137
});
138138

139+
it("force-kills timed Windows commands with taskkill when graceful taskkill fails", async () => {
140+
vi.useFakeTimers();
141+
const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
142+
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
143+
try {
144+
const child = Object.assign(new FakeCommandChild(), { pid: 12345 });
145+
const taskkillImpl = vi
146+
.fn()
147+
.mockReturnValueOnce({ status: 1 })
148+
.mockImplementationOnce(() => {
149+
queueMicrotask(() => child.emit("exit", null, "SIGTERM"));
150+
return { status: 0 };
151+
});
152+
const runPromise = probeTesting.runCommand("fake-command", ["install"], {
153+
spawnImpl: (() => child) as unknown as typeof import("node:child_process").spawn,
154+
taskkillImpl,
155+
timeoutKillGraceMs: 100,
156+
timeoutMs: 10,
157+
});
158+
const runError = runPromise.catch((error: unknown) => error);
159+
160+
await vi.advanceTimersByTimeAsync(10);
161+
162+
expect(taskkillImpl).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12345", "/T"], {
163+
stdio: "ignore",
164+
windowsHide: true,
165+
});
166+
expect(taskkillImpl).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "12345", "/T", "/F"], {
167+
stdio: "ignore",
168+
windowsHide: true,
169+
});
170+
expect(child.signals).toEqual([]);
171+
172+
const error = await runError;
173+
expect(error).toBeInstanceOf(Error);
174+
expect((error as Error).message).toBe("fake-command install timed out after 10ms");
175+
} finally {
176+
if (platformDescriptor) {
177+
Object.defineProperty(process, "platform", platformDescriptor);
178+
}
179+
vi.useRealTimers();
180+
}
181+
});
182+
139183
it("keeps fallback SIGKILL armed for ignored-stdio descendants", async () => {
140184
if (process.platform === "win32") {
141185
return;

0 commit comments

Comments
 (0)