(options: EnsureDaemonOptions = {})
| 289 | * detached process after this returns. |
| 290 | */ |
| 291 | export async function ensureDaemon(options: EnsureDaemonOptions = {}): Promise<EnsureDaemonResult> { |
| 292 | const host = options.host ?? DEFAULT_SERVER_HOST; |
| 293 | const preferred = options.port ?? DEFAULT_SERVER_PORT; |
| 294 | const logLevel = options.logLevel ?? DEFAULT_DAEMON_LOG_LEVEL; |
| 295 | |
| 296 | // 1. Reuse an already-live daemon if one holds the lock. |
| 297 | const existing = getLiveLock(); |
| 298 | if (existing) { |
| 299 | const origin = serverOrigin(lockConnectHost(existing), existing.port); |
| 300 | if (await waitForServerHealthy(origin, REUSE_HEALTH_TIMEOUT_MS)) { |
| 301 | return { |
| 302 | origin, |
| 303 | reused: true, |
| 304 | host: existing.host ?? DEFAULT_SERVER_HOST, |
| 305 | port: existing.port, |
| 306 | }; |
| 307 | } |
| 308 | // Live pid but not responding (wedged or mid-boot failure). Fall through |
| 309 | // and spawn: if it is truly wedged our child loses the lock race and we |
| 310 | // reconnect below; if it died, stale takeover lets our child claim it. |
| 311 | } |
| 312 | |
| 313 | // 2. No reusable daemon — pick a free port and spawn one detached. |
| 314 | const port = await resolveDaemonPort(host, preferred); |
| 315 | const child = spawnDaemonChild({ |
| 316 | host, |
| 317 | port, |
| 318 | logLevel, |
| 319 | debugEndpoints: options.debugEndpoints, |
| 320 | insecureNoTls: options.insecureNoTls, |
| 321 | allowRemoteShutdown: options.allowRemoteShutdown, |
| 322 | allowRemoteTerminals: options.allowRemoteTerminals, |
| 323 | allowedHosts: options.allowedHosts, |
| 324 | idleGraceMs: options.idleGraceMs, |
| 325 | }); |
| 326 | |
| 327 | // Watch for an early exit so a boot failure (e.g. the non-loopback TLS gate, |
| 328 | // a config error, or a lost lock race with no other daemon to fall back to) |
| 329 | // surfaces the real error immediately instead of waiting out the full spawn |
| 330 | // timeout. The exit code/signal plus a tail of the daemon log is what tells |
| 331 | // the operator *why* it failed. |
| 332 | let childExit: { code: number | null; signal: NodeJS.Signals | null } | undefined; |
| 333 | child.once('exit', (code, signal) => { |
| 334 | childExit = { code, signal }; |
| 335 | }); |
| 336 | child.once('error', () => { |
| 337 | // Spawn failure (ENOENT etc.) is already recorded in the log by |
| 338 | // spawnDaemonChild; treat it as an early exit here. |
| 339 | childExit = { code: -1, signal: null }; |
| 340 | }); |
| 341 | |
| 342 | // 3. Wait until some live daemon (ours, or a racer that won the lock) is up. |
| 343 | const deadline = Date.now() + SPAWN_TIMEOUT_MS; |
| 344 | while (Date.now() < deadline) { |
| 345 | const live = getLiveLock(); |
| 346 | if (live) { |
| 347 | const origin = serverOrigin(lockConnectHost(live), live.port); |
| 348 | if (await isServerHealthy(origin, 500)) { |
no test coverage detected