Skip to content

Shut a tray run down through OnStop on SIGTERM and Ctrl+C [patch] - #52

Merged
matt-edmondson merged 2 commits into
mainfrom
fix/21-tray-signals
Oct 27, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
fix/21-tray-signals

Conversation

@matt-edmondson

@matt-edmondson matt-edmondson commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21

What was wrong

Only ConsoleRunner installed signal handlers, and Avalonia handles neither signal. So a tray run sent SIGTERM (kill, systemctl stop, logout) or SIGINT (Ctrl+C in the launching terminal) died to the runtime's default handler:

  • the tool's OnStop never ran;
  • a toggle clicked inside the 500 ms debounce window was never saved.

That contradicted the README and the OnStop XML docs.

Change

  • New internal ShutdownSignals, which RunTray holds for the length of the run:
    • It hooks Console.CancelKeyPress and registers PosixSignalRegistration for SIGTERM, mirroring ConsoleRunner.
    • Each handler cancels the default termination and asks the tray to shut down.
    • Before the tray host exists, the signal is left to end the process as it did before.
    • It is disposed before any fallback to the console, which installs its own handlers.
  • TrayApplication.RequestShutdown() posts the existing Shutdown to the UI thread. That is the path the Quit item and the --for timer already take, so the run ends through OnExit → Dispose → OnStop → the FinishAsync flush, and exits 0. Only the first request posts anything, so a second Ctrl+C, or a signal during a Quit, doesn't shut down twice.
  • New ShutdownSignalsTests call the handlers directly, with no display. They cover:
    • a signal before the tray exists is not taken;
    • a signal once the tray exists is taken and requests the shutdown;
    • repeated signals post only one shutdown.
  • New scripts/check-tray-signals.sh is the end-to-end regression check, documented in CLAUDE.md under Testing.
    • It runs the demo in the tray under Xvfb with the status-notifier watcher and clicks the remembered Running toggle.
    • It sends the signal about 100 ms later, inside the debounce window.
    • It fails on a non-zero exit or a toggle that was not saved.

Verification

Run on Linux under dbus-run-session + Xvfb:

exit toggle saved
SIGTERM, before 143 (Terminated) no ("Toggles": {})
SIGINT, before 130 no ("Toggles": {})
SIGTERM, after 0 yes ("running": false)
SIGINT, after 0 yes ("running": false)
  • scripts/check-tray-signals.sh fails on the base commit (SIGTERM: exited 143 instead of shutting down) and passes with this change.
  • Unit suite: 158 passed, 1 skipped, 0 failed.

Not covered here:

  • Windows and macOS: the handlers are the same calls ConsoleRunner already makes on those platforms, but the tray path was only exercised on Linux.
  • CI: the end-to-end script isn't wired in. The workflows call the shared pipeline, and adding an Xvfb + D-Bus job there is a separate decision.

🤖 Generated with Claude Code

https://claude.ai/code/session_017UuQscGFrjteUf9Ui3jWdd

Only the console path installed signal handlers, and Avalonia handles
neither signal, so a tray run sent SIGTERM (kill, systemctl stop, logout)
or SIGINT died to the runtime's default handler: the tool's OnStop never
ran, and a toggle clicked inside the 500 ms debounce window was never
saved.

RunTray now takes both, as ConsoleRunner does, and posts the same
lifetime shutdown the Quit item asks for, so the run ends through OnExit,
Dispose, OnStop and the preference flush and exits 0.

scripts/check-tray-signals.sh drives this under Xvfb with a
status-notifier watcher: it clicks the remembered toggle, signals inside
the debounce window, and fails on a non-zero exit or a lost toggle.

Fixes #21

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UuQscGFrjteUf9Ui3jWdd
…sted

RunTray only runs with a real tray, so the handlers written inline there
were covered by nothing but the Xvfb script. ShutdownSignals now holds the
CancelKeyPress hook and the SIGTERM registration, and decides from the
host whether to take the signal; TrayApplication.RequestShutdown reports
whether it posted the shutdown. Both are exercised directly in
ShutdownSignalsTests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UuQscGFrjteUf9Ui3jWdd
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

In tray mode, SIGTERM and Ctrl+C kill the process without running OnStop or flushing a pending preference save

2 participants