Compare commits

...

2 Commits

Author SHA1 Message Date
891b08ecd8 try to better detect failed process start 2026-02-09 21:32:48 -08:00
302ef63485 round duration to the second 2026-02-09 21:32:11 -08:00
2 changed files with 17 additions and 46 deletions

View File

@ -415,7 +415,7 @@ function statusLabel(job: CronJob): string {
function formatDuration(ms?: number): string { function formatDuration(ms?: number): string {
if (!ms) return '-' if (!ms) return '-'
if (ms < 1000) return `${ms}ms` if (ms < 1000) return `${ms}ms`
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s` if (ms < 60000) return `${Math.round(ms / 1000)}s`
return `${Math.round(ms / 60000)}m` return `${Math.round(ms / 60000)}m`
} }

View File

@ -583,30 +583,19 @@ async function runApp(dir: string, port: number) {
app.proc = proc app.proc = proc
// Check if process is alive using ps(1) - more reliable than Bun's API
const isProcessAlive = async (pid: number): Promise<boolean> => {
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 // Poll to verify app started - tries /ok for HTTP apps, falls back to survival check
const pollStartup = async () => { const pollStartup = async () => {
const pollInterval = 500 const pollInterval = 500
const survivalThreshold = 5000 // Consider non-HTTP apps running after 5s const survivalThreshold = 5000 // Consider non-HTTP apps running after 5s
const startTime = Date.now() 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) { while (app.state === 'starting' && app.proc === proc) {
// First check if process is still alive if (processExited) {
const alive = await isProcessAlive(pid)
if (!alive) {
info(app, 'Process died during startup') info(app, 'Process died during startup')
// proc.exited handler will clean up
return return
} }
@ -636,6 +625,13 @@ async function runApp(dir: string, port: number) {
// If process survived long enough, consider it running (non-HTTP app) // If process survived long enough, consider it running (non-HTTP app)
if (Date.now() - startTime >= survivalThreshold) { 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)') info(app, 'No /ok endpoint, marking as running (process survived 5s)')
markAsRunning(app, port, false) markAsRunning(app, port, false)
return return
@ -787,35 +783,10 @@ function startHealthChecks(app: App, port: number) {
} }
function startProcessHealthChecks(app: App) { function startProcessHealthChecks(app: App) {
// For non-HTTP apps, just verify process is still alive using ps(1) // For non-HTTP apps, the proc.exited handler is the authoritative death signal.
app.healthCheckTimer = setInterval(async () => { // No need to poll — when the process dies, proc.exited fires and cleans up.
if (app.state !== 'running') { // This is a no-op; health checks only matter for HTTP apps where the process
if (app.healthCheckTimer) { // can be alive but the server unresponsive.
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)
} }
function startShutdownTimeout(app: App) { function startShutdownTimeout(app: App) {