// SIGTERM every child we still own when the server shuts down gracefully. // // WHY THIS IS ITS OWN MODULE, and not inline in editor/instrumentation.ts: // `register()` runs in every runtime, and Next statically analyses that file for // the EDGE bundle. Node APIs there (`process.once`, `process.kill`, // `process.listenerCount`) are flagged as unsupported and the file fails to // compile — the `NEXT_RUNTIME !== "nodejs"` early return happens at runtime, and // the bundler's static scan does not know about it. Keeping the Node-only code // behind a lazy `await import()` inside that guard is the idiom instrumentation.ts // already uses for the heartbeat, worker pool and digest sweep. // // WHAT IT FIXES: a spawned yt-dlp/whisper/parakeet child otherwise outlives the // server. Route handlers run in a Next-spawned worker, so when that worker dies // the child reparents and keeps burning CPU. The registry holds the only handle // that can reach it — runManagedFunction sets `record.abortController` but never // `record.child` (controller/../jobs/streamCommand.ts), so `cancel()`'s // `child.kill()` branch is a no-op for those jobs and `abort()` is the only kill // path. That handle dies with the process unless we use it first. // // HONEST LIMIT: graceful shutdown only. A SIGKILLed server still orphans its // children because nothing in-process gets to run — that case is covered by the // fixtures' own watchdog (editor/e2e/fixtures/bin/_watchdog.mjs) in tests, and is // simply unavoidable for a real `kill -9` in production. // Set once the handlers are armed so a second register() — or a second signal — // cannot re-enter. register() is documented as running once per server instance, // but this is cheap and keeps shutdown re-entrancy-free. let shutdownArmed = false; export function armShutdownCancel(): void { if (shutdownArmed) return; shutdownArmed = true; for (const signal of ["SIGTERM", "SIGINT"] as const) { process.once(signal, () => { try { // Read the global directly: getRegistry() CONSTRUCTS on read, and // building a registry during shutdown just to cancel nothing would be // worse than useless. No registry means nothing ever ran. const registry = globalThis.__yttJobRegistry__; // First: these cancels are the exit's, not an operator's, so a queued // job keeps its `queued` sidecar for the boot pass (registry.cancel). // Optional-called: under `next dev` the registry on globalThis can // predate this method (HMR swaps the class, not the instance), and a // throw here would skip every cancel below. registry?.beginShutdown?.(); for (const job of registry?.list() ?? []) { if (job.status !== "running" && job.status !== "queued") continue; try { registry?.cancel(job.id); } catch { /* one wedged job must not block the rest, or the exit */ } } } catch { /* shutdown must proceed even if the registry is in a bad state */ } // No await, no delay: abort() fires execa's cancelSignal inline, so the // SIGTERMs are already delivered by the time we get here. This runs on // every ordinary dev restart, so it must not add latency. // // Re-raise only if nobody else is listening. `once` has already removed // this listener, so a remaining count means another handler (Next's own) // owns the exit — re-raising then would run that handler a second time. if (process.listenerCount(signal) === 0) { process.kill(process.pid, signal); } }); } }