diff --git a/packages/server/src/server/pid-lock.test.ts b/packages/server/src/server/pid-lock.test.ts index e54093f177..70e2eddae3 100644 --- a/packages/server/src/server/pid-lock.test.ts +++ b/packages/server/src/server/pid-lock.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, open, rm, stat, utimes, writeFile } from "node:fs/promises"; +import { mkdtemp, open, readFile, rm, stat, utimes, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, test } from "vitest"; @@ -256,4 +256,38 @@ describe("pid-lock ownership", () => { await rm(paseoHome, { recursive: true, force: true }); } }); + + test("starts over an empty lock file left by a supervisor killed before writing it", async () => { + const paseoHome = await mkdtemp(join(tmpdir(), "paseo-pid-lock-empty-")); + const ownerPid = process.pid + 10_000; + + try { + await writeFile(join(paseoHome, "paseo.pid"), ""); + + await expect(getPidLockInfo(paseoHome)).resolves.toBeNull(); + await acquirePidLock(paseoHome, null, { ownerPid }); + + const lock = await getPidLockInfo(paseoHome); + expect(lock?.pid).toBe(ownerPid); + } finally { + await rm(paseoHome, { recursive: true, force: true }); + } + }); + + test("keeps a lock file whose contents cannot be read as a lock", async () => { + const paseoHome = await mkdtemp(join(tmpdir(), "paseo-pid-lock-unparseable-")); + const pidPath = join(paseoHome, "paseo.pid"); + + try { + await writeFile(pidPath, JSON.stringify({ pid: "unknown" })); + + await expect( + acquirePidLock(paseoHome, null, { ownerPid: process.pid + 10_000 }), + ).rejects.toThrow("Cannot read daemon state"); + + await expect(readFile(pidPath, "utf-8")).resolves.toBe(JSON.stringify({ pid: "unknown" })); + } finally { + await rm(paseoHome, { recursive: true, force: true }); + } + }); }); diff --git a/packages/server/src/server/pid-lock.ts b/packages/server/src/server/pid-lock.ts index bffab63063..3005dce9d0 100644 --- a/packages/server/src/server/pid-lock.ts +++ b/packages/server/src/server/pid-lock.ts @@ -1,4 +1,4 @@ -import { open, readFile, unlink, utimes } from "node:fs/promises"; +import { open, readFile, stat, unlink, utimes } from "node:fs/promises"; import type { FileHandle } from "node:fs/promises"; import { ensurePrivateDirectory } from "./private-files.js"; import { join } from "node:path"; @@ -84,18 +84,26 @@ async function touchPidLockFile(pidPath: string): Promise { async function readPidLock(pidPath: string): Promise { let lastError: unknown; + let empty = false; for (let attempt = 0; attempt < PID_LOCK_READ_RETRY_ATTEMPTS; attempt++) { try { const content = await readFile(pidPath, "utf-8"); - const lock = parsePidLockInfo(JSON.parse(content)); - if (lock) return lock; - lastError = new Error("Invalid lock shape"); + empty = content === ""; + if (!empty) { + const lock = parsePidLockInfo(JSON.parse(content)); + if (lock) return lock; + lastError = new Error("Invalid lock shape"); + } } catch (error) { if (isErrnoException(error) && error.code === "ENOENT") return null; + empty = false; lastError = error; } await new Promise((resolve) => setTimeout(resolve, PID_LOCK_READ_RETRY_DELAY_MS)); } + // A supervisor writes its lock right after creating the file, so a file still empty + // after the retries was abandoned in between and names no owner. + if (empty) return null; throw Object.assign( new PidLockError(`Cannot read daemon state at ${pidPath}: ${String(lastError)}`), { code: "DAEMON_STATE_READ_FAILED" }, @@ -149,6 +157,14 @@ async function clearExistingPidLock( return "cleared"; } +async function removeEmptyPidLock(pidPath: string): Promise { + try { + if ((await stat(pidPath)).size === 0) await unlink(pidPath); + } catch (error) { + if (!isErrnoException(error) || error.code !== "ENOENT") throw error; + } +} + async function writeNewPidLock(pidPath: string, lockInfo: PidLockInfo): Promise { let fd; try { @@ -191,6 +207,8 @@ export async function acquirePidLock( if (result === "already_owned") { return; } + } else { + await removeEmptyPidLock(pidPath); } // Create new lock with exclusive flag