mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Compare commits
2 Commits
v0.2.1
...
issue-1950
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
215566bcb6 | ||
|
|
bc534ad09c |
@@ -1,4 +1,4 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
@@ -49,4 +49,31 @@ describe("pid-lock ownership", () => {
|
||||
await rm(paseoHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("reclaims a stale lock when the recorded pid belongs to a different process", async () => {
|
||||
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-pid-lock-reused-"));
|
||||
const replacementOwnerPid = process.pid + 10_000;
|
||||
|
||||
try {
|
||||
await writeFile(
|
||||
join(paseoHome, "paseo.pid"),
|
||||
JSON.stringify({
|
||||
pid: process.pid,
|
||||
startedAt: "2026-01-01T00:00:00.000Z",
|
||||
hostname: "old-host",
|
||||
uid: process.getuid?.() ?? 0,
|
||||
listen: "127.0.0.1:6767",
|
||||
desktopManaged: true,
|
||||
}),
|
||||
);
|
||||
|
||||
await acquirePidLock(paseoHome, null, { ownerPid: replacementOwnerPid });
|
||||
|
||||
const lock = await getPidLockInfo(paseoHome);
|
||||
expect(lock?.pid).toBe(replacementOwnerPid);
|
||||
expect(lock?.listen).toBeNull();
|
||||
} finally {
|
||||
await rm(paseoHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { open, readFile, unlink, mkdir } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { hostname } from "node:os";
|
||||
import { z } from "zod";
|
||||
@@ -34,6 +35,11 @@ export class PidLockError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
const PROCESS_START_SKEW_TOLERANCE_MS = 60_000;
|
||||
const PROCESS_LOCK_ACQUIRE_TOLERANCE_MS = 5 * 60_000;
|
||||
const POSIX_PROCESS_TIME_ENV = { ...process.env, LC_ALL: "C", LANG: "C" };
|
||||
let cachedClockTicksPerSecond: number | null = null;
|
||||
|
||||
function isPidRunning(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
@@ -43,6 +49,126 @@ function isPidRunning(pid: number): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function readClockTicksPerSecond(): number {
|
||||
if (cachedClockTicksPerSecond !== null) {
|
||||
return cachedClockTicksPerSecond;
|
||||
}
|
||||
|
||||
try {
|
||||
const output = execFileSync("getconf", ["CLK_TCK"], {
|
||||
encoding: "utf8",
|
||||
timeout: 1000,
|
||||
}).trim();
|
||||
const value = Number(output);
|
||||
cachedClockTicksPerSecond = Number.isFinite(value) && value > 0 ? value : 100;
|
||||
} catch {
|
||||
cachedClockTicksPerSecond = 100;
|
||||
}
|
||||
|
||||
return cachedClockTicksPerSecond;
|
||||
}
|
||||
|
||||
function readLinuxProcessStartedAtMs(pid: number): number | null {
|
||||
try {
|
||||
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
||||
const closeParen = stat.lastIndexOf(")");
|
||||
if (closeParen === -1) return null;
|
||||
|
||||
const fieldsAfterCommand = stat
|
||||
.slice(closeParen + 2)
|
||||
.trim()
|
||||
.split(/\s+/);
|
||||
const startTicks = Number(fieldsAfterCommand[19]);
|
||||
if (!Number.isFinite(startTicks)) return null;
|
||||
|
||||
const bootTimeLine = readFileSync("/proc/stat", "utf8")
|
||||
.split("\n")
|
||||
.find((line) => line.startsWith("btime "));
|
||||
const bootSeconds = Number(bootTimeLine?.trim().split(/\s+/)[1]);
|
||||
if (!Number.isFinite(bootSeconds)) return null;
|
||||
|
||||
return bootSeconds * 1000 + (startTicks / readClockTicksPerSecond()) * 1000;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readPsProcessStartedAtMs(pid: number): number | null {
|
||||
try {
|
||||
const output = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
|
||||
encoding: "utf8",
|
||||
env: POSIX_PROCESS_TIME_ENV,
|
||||
timeout: 1000,
|
||||
}).trim();
|
||||
const parsed = Date.parse(output);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readWindowsProcessStartedAtMs(pid: number): number | null {
|
||||
try {
|
||||
const command = [
|
||||
`$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"`,
|
||||
'if ($p) { $p.CreationDate.ToUniversalTime().ToString("o") }',
|
||||
].join("; ");
|
||||
const output = execFileSync(
|
||||
"powershell.exe",
|
||||
["-NoProfile", "-NonInteractive", "-Command", command],
|
||||
{
|
||||
encoding: "utf8",
|
||||
timeout: 3000,
|
||||
},
|
||||
).trim();
|
||||
const parsed = Date.parse(output);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readProcessStartedAtMs(pid: number): number | null {
|
||||
if (process.platform === "linux") {
|
||||
return readLinuxProcessStartedAtMs(pid) ?? readPsProcessStartedAtMs(pid);
|
||||
}
|
||||
if (process.platform === "win32") {
|
||||
return readWindowsProcessStartedAtMs(pid);
|
||||
}
|
||||
return readPsProcessStartedAtMs(pid);
|
||||
}
|
||||
|
||||
function lockMatchesLiveProcessIdentity(lock: PidLockInfo): boolean {
|
||||
const lockStartedAtMs = Date.parse(lock.startedAt);
|
||||
if (!Number.isFinite(lockStartedAtMs)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const processStartedAtMs = readProcessStartedAtMs(lock.pid);
|
||||
if (processStartedAtMs === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (processStartedAtMs - lockStartedAtMs > PROCESS_START_SKEW_TOLERANCE_MS) {
|
||||
return false;
|
||||
}
|
||||
if (lockStartedAtMs - processStartedAtMs > PROCESS_LOCK_ACQUIRE_TOLERANCE_MS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isPidLockOwnerRunning(lock: PidLockInfo): boolean {
|
||||
if (!isPidRunning(lock.pid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// PIDs can be reused after an unclean daemon exit. Treat a live PID as the
|
||||
// lock owner only when its process start time still lines up with the lock.
|
||||
return lockMatchesLiveProcessIdentity(lock);
|
||||
}
|
||||
|
||||
function getPidFilePath(paseoHome: string): string {
|
||||
return join(paseoHome, "paseo.pid");
|
||||
}
|
||||
@@ -78,7 +204,7 @@ export async function acquirePidLock(
|
||||
// Check if existing lock is stale
|
||||
const lockOwnerPid = resolveOwnerPid(options?.ownerPid);
|
||||
if (existingLock) {
|
||||
if (isPidRunning(existingLock.pid)) {
|
||||
if (isPidLockOwnerRunning(existingLock)) {
|
||||
if (existingLock.pid === lockOwnerPid) {
|
||||
return;
|
||||
}
|
||||
@@ -197,7 +323,7 @@ export async function isLocked(
|
||||
if (!info) {
|
||||
return { locked: false };
|
||||
}
|
||||
if (!isPidRunning(info.pid)) {
|
||||
if (!isPidLockOwnerRunning(info)) {
|
||||
return { locked: false, info };
|
||||
}
|
||||
return { locked: true, info };
|
||||
|
||||
Reference in New Issue
Block a user