Files
orca/src/main/daemon/daemon-pid-file-parse.test.ts
T
Brennan BensonandBrennan Benson 7abdf037d6 Fix Windows daemon host pruning on unverifiable liveness (#16908)
* Fix Windows daemon host prune liveness contract

* Scope host prune evidence per version

* Drop redundant default cases from exhaustive liveness switches

All three switches consume ProcessLivenessVerdict/ProcessSignalEvidence values
constructed in-process by inspectProcessSignal/inspectProcessLiveness; the union
is never deserialized from a wire, RPC, or persisted record, so the defaults are
genuinely unreachable.

* Cover prune liveness gates and quarantine corrupt pid records

* Make the prune delete-gate fail safe and refuse truncated pid salvage

The prune switch shared #16900's delete-gate shape: an unhandled future
verdict status fell through into rmSync, protected only by the lint
exhaustiveness rule. Deletion is now opted into by a positively matched
'exited' via reclaimUnownedDaemonHostDir; a pinning test feeds an
out-of-contract verdict and asserts the host dir survives.

Pid salvage from corrupt records now requires the digit run to be
terminated by a following non-digit byte. A tear inside the digits leaves
a truncated prefix that is a different pid: probing it either quarantined
a record on an unrelated process's death or, when the prefix collided
with an immortal pid (Windows System pid 4), re-created the permanent
prune veto for that record. Unterminated digits mean the writer died
mid-write, so the record quarantines without consulting any probe.

* fix(daemon): stop an in-flight pid publish from being read as a dead version

publishDaemonPidFile creates the record before writing it (writeFileSync with
flag 'wx'), so a concurrent launch can read a live daemon's record as empty. A
two-process probe observed the empty window on 7 of 2273 reads.

An empty record was not treated as corrupt at all: the parser's legacy
bare-integer fallback coerces it to pid 0 (Number('') === 0) with appVersion
null, so the scan skipped it as a pre-relocation daemon, left its version
unpinned, and the prune reclaimed a running daemon's host image -- the exact
destructive outcome this change exists to prevent, reached without any
'unverifiable' verdict. A pid that is not a positive integer names no process
(process.kill(0, 0) probes the caller's own process group), so it is now a
veto rather than a skip.

Quarantine additionally refuses any record written in the last minute: an
in-flight publish is by definition fresh, while a record left corrupt by a
dead writer ages past the floor and is quarantined on a later launch. Fixed
locally rather than in parseDaemonPidFile, whose null result also drives an
unlink in daemon-stale-kill.

Each gate is pinned by a test that fails individually when it is reverted.

---------

Co-authored-by: Brennan Benson <brennanb2025@users.noreply.github.com>
2026-08-28 15:54:21 -07:00

23 lines
1.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { salvagePidFromCorruptDaemonRecord } from './daemon-pid-file-parse'
describe('salvagePidFromCorruptDaemonRecord', () => {
it('salvages a pid whose digit run is terminated by a following byte', () => {
expect(salvagePidFromCorruptDaemonRecord('{"pid":4242,"startedAtMs":17')).toBe(4242)
expect(salvagePidFromCorruptDaemonRecord('{"pid": 4242, "startedAtMs"')).toBe(4242)
expect(salvagePidFromCorruptDaemonRecord('{"pid":4242}')).toBe(4242)
})
it('refuses digits at end-of-bytes: a tear inside the digits leaves a different pid', () => {
// Pid 42420 torn mid-digits — the surviving prefix 4242 must not be mistaken for a pid.
expect(salvagePidFromCorruptDaemonRecord('{"pid":4242')).toBe(null)
expect(salvagePidFromCorruptDaemonRecord('{"pid":4')).toBe(null)
})
it('refuses records with no usable pid field', () => {
expect(salvagePidFromCorruptDaemonRecord('not a daemon record')).toBe(null)
expect(salvagePidFromCorruptDaemonRecord('{"pid":0,"startedAtMs":17')).toBe(null)
expect(salvagePidFromCorruptDaemonRecord('{"pid":-42,')).toBe(null)
})
})