You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
enableCrashLog installs SIGTERM and SIGHUP listeners that write a crash log and then return without terminating. Registering any listener for those signals suppresses Node's default terminate action, so the process survives the signal entirely.
Since enableDevTools turns crash logging on by default, any app using devtools becomes immune to SIGTERM. Under node --watch this means every file save hangs on Waiting for graceful termination... until Node gives up and SIGKILLs, and each restart leaves two storm-crash-*.json files in the cwd.
Run it, then kill -TERM <pid>. The process writes two crash logs and keeps running. Same under node --watch child.tsx — touch the file and it hangs on restart.
For contrast, enableDevTools(app, { crashLog: false }) exits immediately, which isolates it to the crash-log handler.
Mechanism
Screen registers its signal handlers first (during render), enableCrashLog second. On SIGTERM:
Screen.onSignal runs — cleans up, removes its own listeners, then re-raises via process.kill(process.pid, sig).
The re-raised SIGTERM is delivered. Screen's listener is gone, but crash-log's is still installed, so the default action is still suppressed — log fix: update ci and package #2 is written, and again nothing exits.
The process runs forever.
That accounts for both symptoms: the duplicate logs (1 ms apart) and the hang.
Suggested fix
src/devtools/crash-log.ts — uninstall and re-raise so the default handler runs:
letsignalHandled=false;constonSignal=(sig)=>{if(!installed||signalHandled)return;// Screen.onSignal re-raises after its own cleanup, which would otherwise// land here a second time and write a duplicate log.signalHandled=true;constdata=buildCrashData(app,profiler,frames,includeTree,{signal: sig});constpath=writeCrashLog(dir,data);if(path){try{process.stderr.write(`[storm] Crash log written: ${path}\n`);}catch{}}// Registering ANY listener suppresses Node's default terminate action —// without this the process survives the signal.process.removeListener("SIGTERM",onSignal);process.removeListener("SIGHUP",onSignal);process.kill(process.pid,sig);};
Verified via pnpm patch against dist/: node --watch restarts cleanly, and SIGINT / SIGTERM / SIGHUP all terminate in 12–18 ms (previously SIGTERM and SIGHUP never terminated at all).
Two related things you may want to consider
1. Screen.onSignal's re-raise assumes it is the only listener. It removes its own handlers and re-raises, which only terminates if nothing else is listening. Any user code that registers a SIGTERM listener — a DB pool draining, a telemetry flush — reintroduces exactly this hang, and it'll look like a storm bug. Re-raising is the right pattern, but it might be worth documenting that storm-managed shutdown expects to own the signal, or having Screen explicitly process.exit() after a grace period rather than relying on the default action.
2. Should SIGTERM produce a crash log at all?SIGTERM is an ordinary "please shut down" — it's what node --watch, kill, systemd, Docker and CI all send on normal stop. Treating it as a crash means a routine restart writes a 60-frame profiler dump to the cwd every time. In a --watch loop that's a file per save. SIGSEGV-style genuine crashes and uncaughtException seem worth capturing; a clean SIGTERM maybe less so, or at least opt-in separately from enableDevTools' defaults.
Both are judgement calls rather than bugs, so I've left them alone in my patch — happy to follow whatever you prefer if you'd like a PR.
Environment
@orchetron/storm 0.2.0 (latest on npm)
react 19.2.8, react-reconciler 0.31.0
Node 24.15.0, Linux
Unrelated to #21 / #22 / #23 — this one reproduces on a clean unpatched install with no reconciler involvement.
Summary
enableCrashLoginstallsSIGTERMandSIGHUPlisteners that write a crash log and then return without terminating. Registering any listener for those signals suppresses Node's default terminate action, so the process survives the signal entirely.Since
enableDevToolsturns crash logging on by default, any app using devtools becomes immune toSIGTERM. Undernode --watchthis means every file save hangs onWaiting for graceful termination...until Node gives up andSIGKILLs, and each restart leaves twostorm-crash-*.jsonfiles in the cwd.Reproduction
Run it, then
kill -TERM <pid>. The process writes two crash logs and keeps running. Same undernode --watch child.tsx— touch the file and it hangs on restart.For contrast,
enableDevTools(app, { crashLog: false })exits immediately, which isolates it to the crash-log handler.Mechanism
Screenregisters its signal handlers first (duringrender),enableCrashLogsecond. OnSIGTERM:Screen.onSignalruns — cleans up, removes its own listeners, then re-raises viaprocess.kill(process.pid, sig).crash-log.onSignalruns (same dispatch) — writes log feat: storm reconsiler major optimization and upgrade #1, returns without exiting.SIGTERMis delivered.Screen's listener is gone, but crash-log's is still installed, so the default action is still suppressed — log fix: update ci and package #2 is written, and again nothing exits.That accounts for both symptoms: the duplicate logs (1 ms apart) and the hang.
Suggested fix
src/devtools/crash-log.ts— uninstall and re-raise so the default handler runs:Verified via
pnpm patchagainstdist/:node --watchrestarts cleanly, andSIGINT/SIGTERM/SIGHUPall terminate in 12–18 ms (previouslySIGTERMandSIGHUPnever terminated at all).Two related things you may want to consider
1.
Screen.onSignal's re-raise assumes it is the only listener. It removes its own handlers and re-raises, which only terminates if nothing else is listening. Any user code that registers aSIGTERMlistener — a DB pool draining, a telemetry flush — reintroduces exactly this hang, and it'll look like a storm bug. Re-raising is the right pattern, but it might be worth documenting that storm-managed shutdown expects to own the signal, or havingScreenexplicitlyprocess.exit()after a grace period rather than relying on the default action.2. Should
SIGTERMproduce a crash log at all?SIGTERMis an ordinary "please shut down" — it's whatnode --watch,kill, systemd, Docker and CI all send on normal stop. Treating it as a crash means a routine restart writes a 60-frame profiler dump to the cwd every time. In a--watchloop that's a file per save.SIGSEGV-style genuine crashes anduncaughtExceptionseem worth capturing; a cleanSIGTERMmaybe less so, or at least opt-in separately fromenableDevTools' defaults.Both are judgement calls rather than bugs, so I've left them alone in my patch — happy to follow whatever you prefer if you'd like a PR.
Environment
@orchetron/storm0.2.0 (latest on npm)react19.2.8,react-reconciler0.31.0Unrelated to #21 / #22 / #23 — this one reproduces on a clean unpatched install with no reconciler involvement.