fix(host-service): reap orphaned host-service before spawn (single-writer)
Problem
The coordinator only tracks host-services it spawned in this Electron process (its in-memory instances map). After a restart that map is empty, so a host-service left alive by a previous process is invisible — and spawn() never consults the manifest before starting a new one. It just binds a competitor on a fresh port, giving two processes connected to the same org host.db. That concurrent-writer contention is the root trigger behind the migration lock-up (see #4996, which hardens the migration side).
How an orphan arises:
- Electron died before
before-quitran (hard crash / SIGKILL / OOM) — child never SIGTERMed, parent watchdog hadn't fired yet; - a CLI-spawned host-service — no
HOST_PARENT_PID, so no watchdog at all; - a wedged child from a failed spawn that ignored SIGTERM.
(reset() already reaps via the manifest, but it's only reachable manually via tRPC — never on the normal start path.)
Fix
Reap based on the manifest (the only cross-process record) before spawning, in spawn():
- Identity-validated kill (PID-reuse safe): only terminate a pid we can positively confirm is our host-service — its endpoint answers
health.checkwith the manifest secret, or (best-effort, POSIX) its command line matches ourhost-service.js. A live-but-unrecognized pid is left alone and the stale manifest dropped, so a recycled pid can't cause friendly-fire. - Escalation: SIGTERM → await exit → SIGKILL → await.
Why safe to kill: host-service is stateless w.r.t. terminals — PTYs live in the detached pty-daemon, which a fresh host-service re-adopts. Reaping an orphan doesn't touch user sessions.
Structure
host-service-reap.ts— dependency-injected reap logic (electron-free, so it unit-tests underbun testwithout spawning real processes).host-service-coordinator.ts— wires real deps + the health/argv identity probe; calls reap at the top ofspawn().host-service-utils.ts— addshealthCheckOnce(single-shot identity probe;pollHealthChecknow reuses it).host-service-reap.test.ts— 6 cases: no-manifest, own-live-instance, dead-pid-cleared, unidentified-left-alone (PID reuse), SIGTERM-exit, SIGTERM→SIGKILL escalation.
Desktop typecheck + lint clean; reap tests 6/6.
Relationship to #4996
Independent and complementary. #4996 makes the DB migration survive contention (busy_timeout + fail-closed); this PR removes the source of the contention. Either can merge first.
Summary by cubic
Prevents two host-service processes from writing to the same org DB by reaping any orphaned host-service before spawn. Uses manifest-driven identity checks to safely terminate previous instances and ensure a single writer, resolving migration contention.
- Bug Fixes
- Reap any previous host-service for the org at the start of
spawn()using the manifest. - Identity-validated kill:
health.checkwith the manifest secret, with a POSIX argv fallback; unrecognized live PIDs are left alone and the stale manifest is removed. - Escalation flow:
SIGTERMwith grace, thenSIGKILLif needed; clears the manifest after successful reaping or when the PID is dead. - Added
healthCheckOnceand updatedpollHealthCheckto reuse it. - Introduced dependency-injected
host-service-reap.tswith unit tests covering no-manifest, own-live, dead-pid, PID-reuse, and SIGTERM→SIGKILL paths.
- Reap any previous host-service for the org at the start of
Summary by CodeRabbit
-
Improvements
- Enhanced host-service process lifecycle management with proactive cleanup of previous instances to prevent conflicts.
-
Tests
- Added comprehensive test coverage for host-service process cleanup logic, including edge cases for process termination and manifest handling.