Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/lib/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
32 changes: 31 additions & 1 deletion test/lib/upgrade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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", () => {
Expand Down
Loading