Skip to content

Commit 83785a6

Browse files
committed
fix(build): kill tsdown trees on windows
1 parent 195890f commit 83785a6

2 files changed

Lines changed: 93 additions & 10 deletions

File tree

scripts/tsdown-build.mjs

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,47 @@ export function resolveTsdownBuildInvocation(params = {}) {
593593
};
594594
}
595595

596+
function signalWindowsProcessTree(pid, signal, runTaskkill = spawnSync) {
597+
const args = ["/PID", String(pid), "/T"];
598+
if (signal === "SIGKILL") {
599+
args.push("/F");
600+
}
601+
const result = runTaskkill("taskkill", args, { stdio: "ignore" });
602+
return !result?.error && result?.status === 0;
603+
}
604+
605+
function signalWindowsProcessTreeOrForce(pid, signal, runTaskkill = spawnSync) {
606+
if (signalWindowsProcessTree(pid, signal, runTaskkill)) {
607+
return true;
608+
}
609+
return signal !== "SIGKILL" && signalWindowsProcessTree(pid, "SIGKILL", runTaskkill);
610+
}
611+
612+
export function signalTsdownBuildProcessTree(
613+
child,
614+
signal,
615+
{
616+
platform = process.platform,
617+
runTaskkill = spawnSync,
618+
useProcessGroup = platform !== "win32",
619+
} = {},
620+
) {
621+
if (useProcessGroup && child.pid) {
622+
try {
623+
process.kill(-child.pid, signal);
624+
return;
625+
} catch {
626+
// The group may already be gone; fall back to the direct child handle.
627+
}
628+
}
629+
if (platform === "win32" && child.pid) {
630+
if (signalWindowsProcessTreeOrForce(child.pid, signal, runTaskkill)) {
631+
return;
632+
}
633+
}
634+
child.kill(signal);
635+
}
636+
596637
export async function runTsdownBuildInvocation(invocation, params = {}) {
597638
const stdout = params.stdout ?? process.stdout;
598639
const stderr = params.stderr ?? process.stderr;
@@ -610,7 +651,9 @@ export async function runTsdownBuildInvocation(invocation, params = {}) {
610651
let lastOutputAt = Date.now();
611652
let forceKillAt = null;
612653

613-
const useProcessGroup = process.platform !== "win32";
654+
const platform = params.platform ?? process.platform;
655+
const runTaskkill = params.runTaskkill ?? spawnSync;
656+
const useProcessGroup = platform !== "win32";
614657
const child = spawn(invocation.command, invocation.args, {
615658
...invocation.options,
616659
detached: useProcessGroup,
@@ -622,15 +665,11 @@ export async function runTsdownBuildInvocation(invocation, params = {}) {
622665
}
623666

624667
function signalChild(signal) {
625-
if (useProcessGroup && child.pid) {
626-
try {
627-
process.kill(-child.pid, signal);
628-
return;
629-
} catch {
630-
// The group may already be gone; fall back to the direct child handle.
631-
}
632-
}
633-
child.kill(signal);
668+
signalTsdownBuildProcessTree(child, signal, {
669+
platform,
670+
runTaskkill,
671+
useProcessGroup,
672+
});
634673
}
635674

636675
const parentSignalHandlers = [];

test/scripts/tsdown-build.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
pruneUntrackedGeneratedSourceDeclarations,
1616
resolveTsdownBuildInvocation,
1717
runTsdownBuildInvocation,
18+
signalTsdownBuildProcessTree,
1819
} from "../../scripts/tsdown-build.mjs";
1920
import { createScriptTestHarness } from "./test-helpers.js";
2021

@@ -662,6 +663,49 @@ describe("runTsdownBuildInvocation", () => {
662663
expect(output.chunks.join("")).toContain("timeout after 50ms");
663664
});
664665

666+
it("signals Windows tsdown process trees with taskkill", () => {
667+
const childKill = vi.fn(() => true);
668+
const runTaskkill = vi.fn(() => ({ error: undefined, status: 0 }));
669+
670+
signalTsdownBuildProcessTree({ pid: 123, kill: childKill }, "SIGTERM", {
671+
platform: "win32",
672+
runTaskkill,
673+
});
674+
expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "123", "/T"], {
675+
stdio: "ignore",
676+
});
677+
678+
signalTsdownBuildProcessTree({ pid: 123, kill: childKill }, "SIGKILL", {
679+
platform: "win32",
680+
runTaskkill,
681+
});
682+
expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "123", "/T", "/F"], {
683+
stdio: "ignore",
684+
});
685+
expect(childKill).not.toHaveBeenCalled();
686+
});
687+
688+
it("force-kills Windows tsdown process trees when graceful taskkill fails", () => {
689+
const childKill = vi.fn(() => true);
690+
const runTaskkill = vi
691+
.fn()
692+
.mockReturnValueOnce({ error: undefined, status: 1 })
693+
.mockReturnValueOnce({ error: undefined, status: 0 });
694+
695+
signalTsdownBuildProcessTree({ pid: 123, kill: childKill }, "SIGTERM", {
696+
platform: "win32",
697+
runTaskkill,
698+
});
699+
700+
expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "123", "/T"], {
701+
stdio: "ignore",
702+
});
703+
expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "123", "/T", "/F"], {
704+
stdio: "ignore",
705+
});
706+
expect(childKill).not.toHaveBeenCalled();
707+
});
708+
665709
it.skipIf(process.platform === "win32")(
666710
"kills timed-out tsdown process groups when the wrapper exits first",
667711
async () => {

0 commit comments

Comments
 (0)