fix: harden windows gateway stop cleanup

This commit is contained in:
Peter Steinberger 2026-03-13 18:47:28 +00:00
parent 51fe0bf663
commit 6cb8729952
No known key found for this signature in database
2 changed files with 80 additions and 2 deletions

View File

@ -121,6 +121,52 @@ describe("Scheduled Task stop/restart cleanup", () => {
});
});
it("force-kills remaining busy port listeners when the first stop pass does not free the port", async () => {
await withWindowsEnv(async ({ env }) => {
await writeGatewayScript(env);
schtasksResponses.push(
{ code: 0, stdout: "", stderr: "" },
{ code: 0, stdout: "", stderr: "" },
{ code: 0, stdout: "", stderr: "" },
);
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([4242]);
inspectPortUsage.mockResolvedValueOnce({
port: 18789,
status: "busy",
listeners: [{ pid: 4242, command: "node.exe" }],
hints: [],
});
for (let i = 0; i < 20; i += 1) {
inspectPortUsage.mockResolvedValueOnce({
port: 18789,
status: "busy",
listeners: [{ pid: 4242, command: "node.exe" }],
hints: [],
});
}
inspectPortUsage
.mockResolvedValueOnce({
port: 18789,
status: "busy",
listeners: [{ pid: 5252, command: "node.exe" }],
hints: [],
})
.mockResolvedValueOnce({
port: 18789,
status: "free",
listeners: [],
hints: [],
});
const stdout = new PassThrough();
await stopScheduledTask({ env, stdout });
expect(killProcessTree).toHaveBeenNthCalledWith(1, 4242, { graceMs: 300 });
expect(killProcessTree).toHaveBeenNthCalledWith(2, expect.any(Number), { graceMs: 300 });
expect(inspectPortUsage.mock.calls.length).toBeGreaterThanOrEqual(22);
});
});
it("falls back to inspected gateway listeners when sync verification misses on Windows", async () => {
await withWindowsEnv(async ({ env }) => {
await writeGatewayScript(env);

View File

@ -463,6 +463,24 @@ async function waitForGatewayPortRelease(port: number, timeoutMs = 5_000): Promi
return false;
}
async function terminateBusyPortListeners(port: number): Promise<number[]> {
const diagnostics = await inspectPortUsage(port).catch(() => null);
if (diagnostics?.status !== "busy") {
return [];
}
const pids = Array.from(
new Set(
diagnostics.listeners
.map((listener) => listener.pid)
.filter((pid): pid is number => Number.isFinite(pid) && pid > 0),
),
);
for (const pid of pids) {
await terminateGatewayProcessTree(pid, 300);
}
return pids;
}
async function resolveFallbackRuntime(env: GatewayServiceEnv): Promise<GatewayServiceRuntime> {
const port = resolveConfiguredGatewayPort(env);
if (!port) {
@ -655,7 +673,14 @@ export async function stopScheduledTask({ stdout, env }: GatewayServiceControlAr
await terminateScheduledTaskGatewayListeners(effectiveEnv);
await terminateInstalledStartupRuntime(effectiveEnv);
if (stopPort) {
await waitForGatewayPortRelease(stopPort);
const released = await waitForGatewayPortRelease(stopPort);
if (!released) {
await terminateBusyPortListeners(stopPort);
const releasedAfterForce = await waitForGatewayPortRelease(stopPort, 2_000);
if (!releasedAfterForce) {
throw new Error(`gateway port ${stopPort} is still busy after stop`);
}
}
}
stdout.write(`${formatLine("Stopped Scheduled Task", taskName)}\n`);
}
@ -684,7 +709,14 @@ export async function restartScheduledTask({
await terminateScheduledTaskGatewayListeners(effectiveEnv);
await terminateInstalledStartupRuntime(effectiveEnv);
if (restartPort) {
await waitForGatewayPortRelease(restartPort);
const released = await waitForGatewayPortRelease(restartPort);
if (!released) {
await terminateBusyPortListeners(restartPort);
const releasedAfterForce = await waitForGatewayPortRelease(restartPort, 2_000);
if (!releasedAfterForce) {
throw new Error(`gateway port ${restartPort} is still busy before restart`);
}
}
}
const res = await execSchtasks(["/Run", "/TN", taskName]);
if (res.code !== 0) {