What
If a shutdown request arrives while Vibepollo is still starting up, main() takes one of its early returns (most often the one right after startup_probe()). Those returns skip the ordered Windows teardown at the end of main(). By then, proc::refresh() has already started the virtual display lease thread (g_watchdog_thread in virtual_display_sunshine.cpp), which lives in static storage. It is still joinable when the CRT on-exit handlers destroy it, so std::thread's destructor terminates the process (0xc0000409 in ucrtbase.dll). The normal path already has comments warning about exactly this.
This happens on every Windows power-off or restart where the service relaunches sunshine.exe during shutdown and then stops it mid-startup. Each time, it leaves a crash dump, and the Web UI's Maintenance page then shows a "crash detected" banner for an instance the user never actually used.
Change
A fail_guard declared right after platf::init() runs the same steps on early returns that the normal path runs, with the shutdown deadline armed and the shutdown phase recorded:
- stop and join the deferred virtual-output worker and the virtual display recovery monitors
display_helper_integration::stop_watchdog(true)VDISPLAY::cleanup_retained_ensure_display()andVDISPLAY::closeVDisplayDevice()(joins the lease thread)task_pool.stop()/join()
The normal path still performs these steps itself and disables the guard after task_pool.join(). Every step it calls is idempotent and safe when nothing was started. Being declared after platf::init() means it runs after the later guards (proc, input, host_stats, session_history, orphan recovery) and before platform deinit, tray and session monitor shutdown, which matches the order of the normal path.
Testing
Windows 11 26300 host, NVIDIA GPU, Sunshine virtual display driver. A script started ApolloService, waited for the startup encoder probe to log Creating encoder, then stopped the service and counted Application Error (1000) events for Sunshine.exe:
| build | runs | crashes |
|---|---|---|
| without this change | 3 | 3 (0xc0000409, log ends after nvprefs: Restored ...) |
| with this change | 3 | 0 |
The host's event log had six such crashes over three days, each a few seconds after a Windows power-off or restart (System event 1074). The service starts and streams normally with the change.
Built and tested on a local stack on top of canary a8ef56b0. main.cpp and the headers it uses are unchanged between that and the current canary tip this branch is based on.
Not covered
Two of the six crashes were 0x80000003 from the shutdown deadline's debug_trap(). The stopping instance was still stuck inside the startup probe 10 seconds after the stop request (the reported phase, early-exit-cleanup, is just the tracker's default, since startup never sets a phase). That is a separate problem and is not addressed here.
🤖 Generated with Claude Code