A useless process must not hold the lock
Explore this workspace.
Technical background
Exactly one process answers for the app, so once that process cannot show a window, every later launch is handed to it and silently swallowed — no window, no error, no crash, indefinitely. Six hours of it, measured. A process in which no window has ever reported itself ready has run nothing and holds nothing to lose, and that one rule is what makes quitting safe.
The problem
Exactly one process answers for the app. Once that process cannot show a window, every later launch is handed to it and silently swallowed — no window, no error, no crash, indefinitely. Six hours of it, measured. Nothing anywhere checked that the lock holder had ever produced a working window.
How it works
app:ready is the only line that matters. It is sent once the Angular root exists in a renderer, and everything that can open a tab or spawn a PTY lives at or after that point — so a process in which no window has ever emitted it has never run a session and holds nothing to lose. The first one disarms the watchdog permanently for the life of the process. That single rule is what makes code that can call app.exit() safe to ship.
Settings
TABBY_WATCHDOG:1Environment variable.
0disables it.TABBY_WATCHDOG_BOOT_MS:60000How long a window gets to reach
app:ready.TABBY_WATCHDOG_NO_WINDOW_MS:5000How long the process may have no window at all.
Details
- Two failure shapes, needing different tests. No window at all — a creation that threw, or a handoff that produced nothing, since the paths that call
newWindow()have no catch and the failure simply vanishes. And a window that never booted — the one that actually bit, and the half a zero-window check cannot see, because the window is pushed onto the list before its readiness is awaited, so from outside nothing looks wrong. - The boot budget is spent in ticks of a live event loop, not wall clock. A main process blocked for 19.7s during startup is measured on every cold launch here — it has not given the renderer that time, and burning the budget on it would quit a build that was only slow. Measured margin: a healthy dev build reaches
app:ready1.3–3.8s after the watchdog arms. app.exit(), neverapp.quit(). The window's own close handler asks the renderer to confirm; a renderer that never booted never answers, so quitting politely would hang in exactly the place we are escaping.- It writes to both logs before exiting.
diagnostics.logbatches behind a one-second timer andapp.exit()runs no timers, so the flush is synchronous — otherwise the one record explaining the exit is the one record guaranteed to be lost. - A
window-readyrecord is now written on every successful boot, with how long it took. "How long does this build take to start?" was previously unanswerable from outside the process.
What this does not claim
- The original reproduction no longer reproduces. A poisoned
NODE_PATHwas the fault this was written for, and the module-lookup fix landed first — a dev build launched with the installed app's plugin directories on its path now boots in under two seconds. The test blacklists a required builtin instead, which lands in the same state. Like the module-lookup test it also runs the fault with the watchdog disabled and asserts it still hangs, because a fixture that quietly stopped reproducing would turn the whole run green. - A wedged main process is beyond this by construction — the watchdog runs on that loop.