mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 08:03:33 +00:00
* fix(daemon): retire macOS daemons whose login session died (#7936) A daemon that survives a full macOS logout is unsalvageable: its PAM context can no longer host login(1) spawns (every new PTY becomes a 'Login incorrect' prompt zombie) and its Mach bootstrap namespace has lost the system DNS resolver, so terminals it hosts have no egress. Today it also keeps the #9301 preflight's cached 'accepted' verdict, so it keeps wrapping spawns in login(1) forever; only a manual daemon restart recovers. GUI-spawned daemons now watch for login-session death from the inside: a fresh cache-bypassing PAM probe (triggered by PTY-exit bursts, fresh client hellos, and a slow periodic timer) must conclusively reject three consecutive times AND the in-process system resolver must be degraded; then the daemon exits crash-style so session meta stays unclean and the replacement daemon cold-restores scrollback. A conclusive rejection also flips the spawn-wrapper cache off immediately. Headless serve/SSH daemons never get the watch (they must survive their spawning session ending), and a session that never conclusively accepted login(1) never arms it — a PAM anomaly alone can't kill a healthy daemon (fast user switching keeps accepting, so switched-away sessions are preserved). * test(daemon): e2e seam to drive login-session death oracles from a verdict file A dead macOS login session cannot be fabricated without root (PAM owns audit-session teardown), so live lifecycle QA drives the death watch's probe and resolver oracles from ORCA_E2E_LOGIN_SESSION_PROBE_FILE: 'alive' → accepted/healthy, 'dead' → rejected/unhealthy, anything else inconclusive — with compressed watch timing. Mirrors the existing ORCA_E2E_DAEMON_INIT_DELAY_MS seam; inert unless the env var is set. * fix(daemon): close the hang-shaped gap in login-session death detection The conclusive-PAM-verdict trigger had one blind failure shape: login(1) hanging at the prompt past the probe bound (killed → inconclusive forever → the watch never fires). Three changes close it: - The death-watch probe gets its own 4s bound (the 500ms preflight bound exists for spawn-path latency, which doesn't apply off-path), so a slow-but-answering PAM stack isn't misread as a hang. - An inconclusive pipe probe escalates to a PTY-hosted probe via script(1) — a dead session's PAM stack may only misbehave under a real tty (the pipe-vs-PTY fidelity limit the preflight documents). - A streak of timeout-killed probes (which a live session never produces) is a second retirement trigger, at a higher threshold (5) and still gated on the degraded resolver, logged with a distinct cause so field logs discriminate the two paths. Every dead-session behavior — fast reject, prompt-then-EOF, or hang — now fires retirement; all inconclusive states still fail toward preserving the daemon. * fix(daemon): keep login-session retirement conclusive * fix(daemon): stop login watch before clean shutdown * fix(daemon): make login-session PTY probe reliable * fix(daemon): close login-session watch races * fix(daemon): ignore health probes for login watch activity
52 lines
1.9 KiB
TypeScript
52 lines
1.9 KiB
TypeScript
import { DaemonServer, type DaemonServerOptions } from './daemon-server'
|
|
import type { DaemonFileLog } from './daemon-file-log'
|
|
|
|
export type DaemonStartOptions = {
|
|
socketPath: string
|
|
tokenPath: string
|
|
pidPath?: string
|
|
launchNonce?: string
|
|
startedAtMs?: number
|
|
/** Direct-construction seam for versioned protocol fixtures; never CLI/env configured. */
|
|
protocolVersion?: number
|
|
spawnSubprocess: DaemonServerOptions['spawnSubprocess']
|
|
preparePtySpawn?: DaemonServerOptions['preparePtySpawn']
|
|
onPtySessionExit?: DaemonServerOptions['onPtySessionExit']
|
|
onAuthenticatedClientPair?: DaemonServerOptions['onAuthenticatedClientPair']
|
|
log?: DaemonFileLog
|
|
onIdleShutdown?: () => void
|
|
initialAdoptionTestConfig?: DaemonServerOptions['initialAdoptionTestConfig']
|
|
}
|
|
|
|
export type DaemonHandle = {
|
|
shutdown(): Promise<void>
|
|
}
|
|
|
|
export async function startDaemon(opts: DaemonStartOptions): Promise<DaemonHandle> {
|
|
const server = new DaemonServer({
|
|
socketPath: opts.socketPath,
|
|
tokenPath: opts.tokenPath,
|
|
...(opts.pidPath ? { pidPath: opts.pidPath } : {}),
|
|
...(opts.launchNonce ? { launchNonce: opts.launchNonce } : {}),
|
|
...(opts.startedAtMs ? { startedAtMs: opts.startedAtMs } : {}),
|
|
...(opts.protocolVersion !== undefined ? { protocolVersion: opts.protocolVersion } : {}),
|
|
spawnSubprocess: opts.spawnSubprocess,
|
|
...(opts.preparePtySpawn ? { preparePtySpawn: opts.preparePtySpawn } : {}),
|
|
...(opts.onPtySessionExit ? { onPtySessionExit: opts.onPtySessionExit } : {}),
|
|
...(opts.onAuthenticatedClientPair
|
|
? { onAuthenticatedClientPair: opts.onAuthenticatedClientPair }
|
|
: {}),
|
|
...(opts.log ? { log: opts.log } : {}),
|
|
...(opts.onIdleShutdown ? { onIdleShutdown: opts.onIdleShutdown } : {}),
|
|
...(opts.initialAdoptionTestConfig
|
|
? { initialAdoptionTestConfig: opts.initialAdoptionTestConfig }
|
|
: {})
|
|
})
|
|
|
|
await server.start()
|
|
|
|
return {
|
|
shutdown: () => server.shutdown()
|
|
}
|
|
}
|