diff --git a/src/lib/upgrade.ts b/src/lib/upgrade.ts index 0862f7db..13ad8a5d 100644 --- a/src/lib/upgrade.ts +++ b/src/lib/upgrade.ts @@ -117,12 +117,22 @@ export function cleanupOldBinary(): void { /** * Check if a process with the given PID is still running. + * + * On Unix, process.kill(pid, 0) throws: + * - ESRCH: Process does not exist (not running) + * - EPERM: Process exists but we lack permission to signal it (IS running) */ export function isProcessRunning(pid: number): boolean { try { process.kill(pid, 0); // Signal 0 just checks if process exists return true; - } catch { + } catch (error) { + // EPERM means process exists but we can't signal it (different user) + // This is still a running process, so return true + if ((error as NodeJS.ErrnoException).code === "EPERM") { + return true; + } + // ESRCH or other errors mean process is not running return false; } } diff --git a/test/lib/upgrade.test.ts b/test/lib/upgrade.test.ts index 9a4f5576..1da25de5 100644 --- a/test/lib/upgrade.test.ts +++ b/test/lib/upgrade.test.ts @@ -4,7 +4,7 @@ * Tests for upgrade detection and logic. */ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"; import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { unlink } from "node:fs/promises"; import { homedir, platform } from "node:os"; @@ -482,6 +482,36 @@ describe("isProcessRunning", () => { // PID 4194304 is above typical max PID on most systems expect(isProcessRunning(4_194_304)).toBe(false); }); + + test("returns true on EPERM (process exists but owned by different user)", () => { + // Mock process.kill to throw EPERM + const epermError = Object.assign(new Error("EPERM"), { code: "EPERM" }); + const spy = spyOn(process, "kill").mockImplementation(() => { + throw epermError; + }); + + try { + // EPERM means the process exists, we just can't signal it + expect(isProcessRunning(12_345)).toBe(true); + } finally { + spy.mockRestore(); + } + }); + + test("returns false on ESRCH (process does not exist)", () => { + // Mock process.kill to throw ESRCH + const esrchError = Object.assign(new Error("ESRCH"), { code: "ESRCH" }); + const spy = spyOn(process, "kill").mockImplementation(() => { + throw esrchError; + }); + + try { + // ESRCH means the process does not exist + expect(isProcessRunning(12_345)).toBe(false); + } finally { + spy.mockRestore(); + } + }); }); describe("acquireUpgradeLock", () => {