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
{{ message }}
Repository navigation
After clicking Quit or the --for expiry, a Ctrl+C/SIGTERM during a hung OnStop is still swallowed: only RequestShutdown marks the shutdown as under way #80
Only TrayApplication.RequestShutdown (TrayApp/Tray/TrayApplication.cs:265-274) sets shutdownRequested. Two other paths end the tray run by calling Shutdown() directly:
the Quit item: OnQuitRequested => Shutdown() (L252)
the --for expiry timer's Tick (L320-324)
Neither sets the flag. After either one, the first Ctrl+C or SIGTERM still finds shutdownRequested == 0. RequestShutdown returns true, ShutdownSignals sets Cancel = true (TrayApp/Tray/ShutdownSignals.cs:217), and a second Shutdown is posted to a UI thread that is already blocked.
The remarks on RequestShutdown (L258-263) and #63/#66 promise the opposite: a signal that arrives while a stop is already under way gets its default handling. The console runner keeps that promise, because stopping.Close() (ConsoleRunner.cs:83) marks the run as stopping whatever ended the wait, including --for.
The test that is supposed to cover this, ShutdownSignalsTests.ASignalAfterQuit_IsNotCancelled (TrayApp.Test/ShutdownSignalsTests.cs:133-145), fakes Quit by calling host.RequestShutdown(). The real Quit item never makes that call, so the test passes on a path the product doesn't take.
Failure scenario
The tool's OnStop hangs, for example an inhibitor release blocked on D-Bus.
Quit: the user clicks Quit. desktop.Shutdown() leads to OnExit, then Dispose, then OnStop, which hangs. The user presses Ctrl+C and nothing happens, because the signal is cancelled. Only a second Ctrl+C ends the process.
--for under systemd: the window expires and OnStop hangs. systemctl stop sends SIGTERM, which is cancelled, so the unit sits out TimeoutStopSec and is SIGKILLed. A --no-tray run in the same state exits on the first SIGTERM.
Suggested fix / acceptance criteria
Claim the flag on every path that ends the run. For example, do Interlocked.Exchange(ref shutdownRequested, 1) inside Shutdown() itself and keep RequestShutdown's check-then-post, or send Quit and the expiry through one helper.
After Quit, or after the expiry fires, the next termination signal leaves Cancel == false.
ASignalAfterQuit_IsNotCancelled drives the Quit item (OnQuitRequested), not RequestShutdown. Add an equivalent test for the expiry.
Category: Bug Priority: Medium. It only shows when OnStop hangs, but the consequences in that case are serious: the first Ctrl+C is ignored, and under systemd a --for run waits out TimeoutStopSec and gets SIGKILLed. The existing test passes because it calls RequestShutdown instead of driving the real Quit path, so CI cannot catch this.
Suggested area / assignment: Shutdown signalling in TrayApp/Tray/TrayApplication.cs (Shutdown, RequestShutdown L258-274, the Quit handler L252, the --for timer L320-324) and TrayApp.Test/ShutdownSignalsTests.cs.
Duplicates / in progress: Not a duplicate. It is the follow-up to #63/#66, which added the "a signal during a stop gets its default handling" promise that these two paths miss. Related to #30 (console mode signal handling around OnStart/SIGHUP), but that covers the console runner, not the tray. No open PR covers it.
Next step: Claim shutdownRequested inside Shutdown() itself (or route Quit and the expiry through one helper). Change ASignalAfterQuit_IsNotCancelled to drive OnQuitRequested, and add an equivalent test for the expiry.
Activity
matt-edmondson commentedon Nov 5, 2026
Triage
Category: Bug
Priority: Medium. It only shows when
OnStophangs, but the consequences in that case are serious: the first Ctrl+C is ignored, and under systemd a--forrun waits outTimeoutStopSecand gets SIGKILLed. The existing test passes because it callsRequestShutdowninstead of driving the real Quit path, so CI cannot catch this.Suggested area / assignment: Shutdown signalling in
TrayApp/Tray/TrayApplication.cs(Shutdown,RequestShutdownL258-274, the Quit handler L252, the--fortimer L320-324) andTrayApp.Test/ShutdownSignalsTests.cs.Duplicates / in progress: Not a duplicate. It is the follow-up to #63/#66, which added the "a signal during a stop gets its default handling" promise that these two paths miss. Related to #30 (console mode signal handling around OnStart/SIGHUP), but that covers the console runner, not the tray. No open PR covers it.
Next step: Claim
shutdownRequestedinsideShutdown()itself (or route Quit and the expiry through one helper). ChangeASignalAfterQuit_IsNotCancelledto driveOnQuitRequested, and add an equivalent test for the expiry.Generated by Claude Code