From 45104692dbc1b2b1903dc9faa5fe3dc16226da14 Mon Sep 17 00:00:00 2001 From: Priya Raman Date: Wed, 26 Aug 2026 19:08:39 +0000 Subject: [PATCH] fix(adapter-utils): guard SSH env-lab fixture kill calls against ESRCH process.kill can throw ESRCH when the fixture process exits in the gap between the identity check and the signal. Treat that as a confirmed exit instead of letting it surface as a false failure. Co-authored-by: Paperclip --- packages/adapter-utils/src/ssh.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/adapter-utils/src/ssh.ts b/packages/adapter-utils/src/ssh.ts index db317950c8..60db25e266 100644 --- a/packages/adapter-utils/src/ssh.ts +++ b/packages/adapter-utils/src/ssh.ts @@ -1729,6 +1729,25 @@ async function waitUntilFixtureProcessExits( } } +// Sends a signal to a pid and treats an already-dead process as success. +// The identity check that runs before this call is not free: it spawns +// `ps`, which opens a real gap between the check and the signal. If the +// process exits inside that gap, `process.kill` throws ESRCH even though +// the outcome the caller wants (the process is gone) already holds. Any +// other error, such as EPERM for a pid that belongs to another user, must +// still propagate. +function signalFixtureProcess(pid: number, signal: NodeJS.Signals): boolean { + try { + process.kill(pid, signal); + return true; + } catch (error) { + if (error instanceof Error && "code" in error && error.code === "ESRCH") { + return false; + } + throw error; + } +} + // Bounded shutdown escalation shared by every caller that must stop a // fixture process: send SIGTERM, wait, re-check process identity (the pid // can be reused in the gap between two signals), then SIGKILL, then wait @@ -1738,11 +1757,11 @@ async function escalateSshEnvLabFixtureShutdown( ): Promise { if (!(await isSshEnvLabFixtureProcess(state))) return true; - process.kill(state.pid, "SIGTERM"); + if (!signalFixtureProcess(state.pid, "SIGTERM")) return true; if (await waitUntilFixtureProcessExits(state, 5_000)) return true; if (!(await isSshEnvLabFixtureProcess(state))) return true; - process.kill(state.pid, "SIGKILL"); + if (!signalFixtureProcess(state.pid, "SIGKILL")) return true; if (await waitUntilFixtureProcessExits(state, 2_000)) return true; return !(await isSshEnvLabFixtureProcess(state));