diff --git a/src/server/apps.ts b/src/server/apps.ts index 8d05f19..bb618f9 100644 --- a/src/server/apps.ts +++ b/src/server/apps.ts @@ -583,30 +583,19 @@ async function runApp(dir: string, port: number) { app.proc = proc - // Check if process is alive using ps(1) - more reliable than Bun's API - const isProcessAlive = async (pid: number): Promise => { - try { - const ps = Bun.spawn(['ps', '-p', String(pid)], { stdout: 'pipe', stderr: 'pipe' }) - const code = await ps.exited - return code === 0 - } catch { - return false - } - } - // Poll to verify app started - tries /ok for HTTP apps, falls back to survival check const pollStartup = async () => { const pollInterval = 500 const survivalThreshold = 5000 // Consider non-HTTP apps running after 5s const startTime = Date.now() - const pid = proc.pid + + // Use proc.exited as the authoritative death signal instead of ps(1) + let processExited = false + proc.exited.then(() => { processExited = true }) while (app.state === 'starting' && app.proc === proc) { - // First check if process is still alive - const alive = await isProcessAlive(pid) - if (!alive) { + if (processExited) { info(app, 'Process died during startup') - // proc.exited handler will clean up return } @@ -636,6 +625,13 @@ async function runApp(dir: string, port: number) { // If process survived long enough, consider it running (non-HTTP app) if (Date.now() - startTime >= survivalThreshold) { + // One final check — process could have died between loop iterations + // Yield to let proc.exited handler run if pending + await new Promise(resolve => setTimeout(resolve, 0)) + if (processExited) { + info(app, 'Process died during startup') + return + } info(app, 'No /ok endpoint, marking as running (process survived 5s)') markAsRunning(app, port, false) return @@ -787,35 +783,10 @@ function startHealthChecks(app: App, port: number) { } function startProcessHealthChecks(app: App) { - // For non-HTTP apps, just verify process is still alive using ps(1) - app.healthCheckTimer = setInterval(async () => { - if (app.state !== 'running') { - if (app.healthCheckTimer) { - clearInterval(app.healthCheckTimer) - app.healthCheckTimer = undefined - } - return - } - - const pid = app.proc?.pid - if (!pid) { - handleHealthCheckFailure(app) - return - } - - try { - const ps = Bun.spawn(['ps', '-p', String(pid)], { stdout: 'pipe', stderr: 'pipe' }) - const code = await ps.exited - if (code === 0) { - // Process is alive - app.consecutiveHealthFailures = 0 - } else { - handleHealthCheckFailure(app) - } - } catch { - handleHealthCheckFailure(app) - } - }, HEALTH_CHECK_INTERVAL) + // For non-HTTP apps, the proc.exited handler is the authoritative death signal. + // No need to poll — when the process dies, proc.exited fires and cleans up. + // This is a no-op; health checks only matter for HTTP apps where the process + // can be alive but the server unresponsive. } function startShutdownTimeout(app: App) {