From 7cf6bd5e4b69db5d0edd0ac8f3226c43fefdf982 Mon Sep 17 00:00:00 2001 From: Po-Han Shih <79733497+stantheman0128@users.noreply.github.com> Date: Thu, 23 Jul 2026 04:56:09 +0800 Subject: [PATCH] fix: treat EPERM as alive in isPidAlive (#110235) * fix(pid): treat EPERM as alive in isPidAlive Match isPidDefinitelyDead: process.kill(pid, 0) throwing EPERM means the PID exists but cannot be signaled, so it should not look dead. Co-authored-by: Cursor * fix(pid): check Linux zombies after EPERM existence probe EPERM means the PID exists but cannot be signaled; still run the zombie check so Linux zombies are not reported as alive. Co-authored-by: Cursor * test(pid): isolate EPERM liveness probe Co-authored-by: stantheman0128 --------- Co-authored-by: Cursor Co-authored-by: Peter Steinberger --- src/shared/pid-alive.test.ts | 39 ++++++++++++++++++++++++++++++++++++ src/shared/pid-alive.ts | 9 +++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/shared/pid-alive.test.ts b/src/shared/pid-alive.test.ts index 9f744f71f5a2..c3f1f15d1c3c 100644 --- a/src/shared/pid-alive.test.ts +++ b/src/shared/pid-alive.test.ts @@ -42,6 +42,45 @@ describe("isPidAlive", () => { expect(isPidAlive(Number.POSITIVE_INFINITY)).toBe(false); }); + it("returns true when process probing reports EPERM", () => { + const error = Object.assign(new Error("permission denied"), { code: "EPERM" }); + vi.spyOn(process, "kill").mockImplementation(() => { + throw error; + }); + mockProcReads({ + "/proc/42/status": "Name:\tnode\nState:\tS (sleeping)\nPid:\t42\n", + }); + + expect(isPidAlive(42)).toBe(true); + expect(process["kill"]).toHaveBeenCalledWith(42, 0); + }); + + it("returns false for Linux zombies even when probing reports EPERM", async () => { + const error = Object.assign(new Error("permission denied"), { code: "EPERM" }); + vi.spyOn(process, "kill").mockImplementation(() => { + throw error; + }); + mockProcReads({ + "/proc/42/status": "Name:\tnode\nUmask:\t0022\nState:\tZ (zombie)\nTgid:\t42\nPid:\t42\n", + }); + + await withMockedPlatform("linux", async () => { + expect(isPidAlive(42)).toBe(false); + }); + + expect(process["kill"]).toHaveBeenCalledWith(42, 0); + }); + + it("returns false when process probing reports ESRCH", () => { + const error = Object.assign(new Error("missing process"), { code: "ESRCH" }); + vi.spyOn(process, "kill").mockImplementation(() => { + throw error; + }); + + expect(isPidAlive(42)).toBe(false); + expect(process["kill"]).toHaveBeenCalledWith(42, 0); + }); + it("returns false for zombie processes on Linux", async () => { const zombiePid = process.pid; diff --git a/src/shared/pid-alive.ts b/src/shared/pid-alive.ts index b36955e25dfc..68ab91086f6b 100644 --- a/src/shared/pid-alive.ts +++ b/src/shared/pid-alive.ts @@ -32,8 +32,13 @@ export function isPidAlive(pid: number): boolean { } try { process.kill(pid, 0); - } catch { - return false; + } catch (err) { + // EPERM means the PID exists but we cannot signal it. Treat that as a + // successful existence probe, then still apply the Linux zombie check. + // Keep parity with isPidDefinitelyDead (EPERM is not "definitely dead"). + if ((err as NodeJS.ErrnoException).code !== "EPERM") { + return false; + } } if (isZombieProcess(pid)) { return false;