diff --git a/packages/cli/src/__tests__/kill-with-timeout.test.ts b/packages/cli/src/__tests__/kill-with-timeout.test.ts new file mode 100644 index 00000000..39824df9 --- /dev/null +++ b/packages/cli/src/__tests__/kill-with-timeout.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "bun:test"; +import { killWithTimeout } from "../shared/ssh"; + +describe("killWithTimeout", () => { + it("sends SIGKILL after grace period if process ignores SIGTERM", async () => { + const signals: (number | undefined)[] = []; + const proc = { + kill(signal?: number) { + signals.push(signal); + }, + }; + + killWithTimeout(proc, 100); + await new Promise((r) => setTimeout(r, 200)); + + expect(signals).toEqual([ + undefined, + 9, + ]); + }); + + it("does not throw if process is already dead when SIGKILL fires", async () => { + let callCount = 0; + const proc = { + kill(signal?: number) { + callCount++; + if (callCount > 1) { + throw new Error("No such process"); + } + }, + }; + + killWithTimeout(proc, 50); + await new Promise((r) => setTimeout(r, 150)); + + // Should not throw — the error is caught internally + expect(callCount).toBe(2); + }); + + it("does not send SIGKILL if initial SIGTERM throws", async () => { + const signals: (number | undefined)[] = []; + const proc = { + kill(_signal?: number) { + throw new Error("No such process"); + }, + }; + + killWithTimeout(proc, 50); + await new Promise((r) => setTimeout(r, 150)); + + // No signals recorded because the first kill() threw + expect(signals).toEqual([]); + }); +}); diff --git a/packages/cli/src/shared/ssh.ts b/packages/cli/src/shared/ssh.ts index bf1538fa..26e639e1 100644 --- a/packages/cli/src/shared/ssh.ts +++ b/packages/cli/src/shared/ssh.ts @@ -112,7 +112,6 @@ export function sleep(ms: number): Promise { export function killWithTimeout( proc: { kill(signal?: number): void; - readonly killed: boolean; }, gracePeriodMs = 5000, ): void { @@ -121,11 +120,7 @@ export function killWithTimeout( return; } const sigkillTimer = setTimeout(() => { - tryCatch(() => { - if (!proc.killed) { - proc.kill(9); - } - }); + tryCatch(() => proc.kill(9)); }, gracePeriodMs); // Don't let this timer keep the event loop alive — the process may already // be dead from SIGTERM, so there's no reason to block exit for 5 seconds.