From 4553fcccd69ee51788d43b2bdb45cbd1429e0b3f Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 11 Apr 2026 07:01:25 +0000 Subject: [PATCH] fix(server): acquire pid lock for unsupervised daemon workers The direct worker path (non-supervisor) was not writing paseo.pid, so `paseo daemon status` could not detect the running daemon. This caused test 26-daemon-restart-unsupervised to time out in CI waiting for the status to become "running". Acquire the pid lock before daemon creation, update it with the listen address after start, and release on shutdown/error. Supervision detection now requires both PASEO_SUPERVISED=1 and an active IPC channel to avoid misclassification when the env var is inherited. Co-Authored-By: Claude Opus 4.6 --- packages/server/src/server/index.ts | 32 ++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/index.ts b/packages/server/src/server/index.ts index 87f104963..00658901e 100644 --- a/packages/server/src/server/index.ts +++ b/packages/server/src/server/index.ts @@ -3,7 +3,7 @@ import { loadConfig } from "./config.js"; import { resolvePaseoHome } from "./paseo-home.js"; import { createRootLogger } from "./logger.js"; import { loadPersistedConfig } from "./persisted-config.js"; -import { PidLockError } from "./pid-lock.js"; +import { acquirePidLock, PidLockError, releasePidLock, updatePidLock } from "./pid-lock.js"; import type { DaemonLifecycleIntent } from "./bootstrap.js"; type SupervisorLifecycleMessage = @@ -22,6 +22,8 @@ async function main() { let daemon: Awaited> | null = null; let shutdownPromise: Promise | null = null; let exitHookInstalled = false; + const supervised = process.env.PASEO_SUPERVISED === "1" && typeof process.send === "function"; + let pidLockAcquired = false; try { paseoHome = resolvePaseoHome(); @@ -73,6 +75,10 @@ async function main() { return 1; } await daemon.stop(); + if (pidLockAcquired) { + await releasePidLock(paseoHome); + pidLockAcquired = false; + } clearTimeout(forceExit); logger.info("Server closed"); return options?.successExitCode ?? 0; @@ -131,6 +137,11 @@ async function main() { }; try { + if (!supervised) { + await acquirePidLock(paseoHome, null); + pidLockAcquired = true; + } + daemon = await createPaseoDaemon( { ...config, @@ -139,6 +150,10 @@ async function main() { logger, ); } catch (err) { + if (pidLockAcquired) { + await releasePidLock(paseoHome); + pidLockAcquired = false; + } if (err instanceof PidLockError) { logger.error({ pid: err.existingLock?.pid }, err.message); process.exit(1); @@ -149,7 +164,22 @@ async function main() { try { await daemon.start(); + if (!supervised) { + const listenTarget = daemon.getListenTarget(); + const listen = + listenTarget?.type === "tcp" + ? `${listenTarget.host}:${listenTarget.port}` + : listenTarget?.path; + if (!listen) { + throw new Error("Daemon did not expose a listen target after startup"); + } + await updatePidLock(paseoHome, { listen }); + } } catch (err) { + if (pidLockAcquired) { + await releasePidLock(paseoHome); + pidLockAcquired = false; + } if (err instanceof PidLockError) { logger.error({ pid: err.existingLock?.pid }, err.message); process.exit(1);