From b80f6623cfa09802d88a8c75ff51285edc4e67bd Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 05:43:20 +0200 Subject: [PATCH] test(server): reproduce a reused PID read as a running daemon (#5863) The supervisor's lock survives an unclean exit, and later in the same boot the OS hands its PID to an unrelated process. isLocked() reports that lock as held. This test fails on main and has no fix. --- packages/server/src/server/pid-lock.test.ts | 40 ++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/pid-lock.test.ts b/packages/server/src/server/pid-lock.test.ts index 70e2eddae3..bdfe0d2c94 100644 --- a/packages/server/src/server/pid-lock.test.ts +++ b/packages/server/src/server/pid-lock.test.ts @@ -1,4 +1,5 @@ import { mkdtemp, open, readFile, rm, stat, utimes, writeFile } from "node:fs/promises"; +import { spawn } from "node:child_process"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, test } from "vitest"; @@ -84,7 +85,9 @@ describe("pid-lock ownership", () => { const staleTime = new Date(Date.now() - 10 * 60_000); await utimes(pidPath, staleTime, staleTime); - await expect(isLocked(paseoHome)).resolves.toMatchObject({ locked: true }); + await expect(isLocked(paseoHome)).resolves.toMatchObject({ + locked: true, + }); await expect( acquirePidLock(paseoHome, null, { ownerPid: replacementOwnerPid }), ).rejects.toThrow("Another Paseo daemon is already running"); @@ -257,6 +260,41 @@ describe("pid-lock ownership", () => { } }); + // #5863: the supervisor died without removing its lock, and later in the same boot the OS + // handed its PID to an unrelated process. A process that started after the lock was + // written cannot be the supervisor that wrote it. + test("treats a lock whose PID now belongs to a process started after it as abandoned", async () => { + const paseoHome = await mkdtemp(join(tmpdir(), "paseo-pid-lock-reused-pid-")); + const lockWrittenAt = new Date(Date.now() - 60_000); + const unrelated = spawn(process.execPath, ["-e", "setTimeout(() => {}, 60_000)"], { + stdio: "ignore", + }); + + try { + const pidPath = join(paseoHome, "paseo.pid"); + await writeFile( + pidPath, + JSON.stringify({ + pid: unrelated.pid, + startedAt: lockWrittenAt.toISOString(), + hostname: "current-host", + uid: process.getuid?.() ?? 0, + listen: "127.0.0.1:6767", + desktopManaged: true, + heartbeat: true, + }), + ); + await utimes(pidPath, lockWrittenAt, lockWrittenAt); + + await expect(isLocked(paseoHome)).resolves.toMatchObject({ + locked: false, + }); + } finally { + unrelated.kill(); + 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;