Repository navigation
Do not let a refused preference restore kill the tray - #6
Merged
Merged
Conversation
Restoring a remembered toggle calls the tool's setter, and that setter can refuse for exactly the reason a click can: restoring "keep awake: on" asks the tool to take an inhibitor, and the machine may have none to give. The call sat bare in LoadPreferences, outside any handling, and it runs before there is a menu to fail into - so the exception escaped RunAsync and ended the process before the tray ever appeared. That is the one failure mode this library exists to prevent, in the library's own startup path, and it is a regression against the tools it is meant to replace: NoSleep catches SleepBlockException and PlatformNotSupportedException around its startup Enable() and still shows the tray. The refusal is now caught per toggle, written to standard error, and handed back for the menu's status line through TrayMenu.SeedError, which the next successful action clears like any other failure. The toggles after the one that refused are still restored, and the tray still comes up saying why. Verified: the refusal path and the seeded status line have tests; and with a real status-notifier host under xvfb, the demo's remembered toggle still round trips across a restart - click it off, and the next process starts with the entry unchecked and the status line already reading "idle". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qksp7NcKB2jMJjXuxGS3nP
github-code-quality flagged the new catch in LoadPreferences as a generic catch clause, and it is right: reporting an OutOfMemoryException in a menu and carrying on presents a tray that cannot work and buries the real cause under whatever fails next. Filtering only the flagged line would have been worse than leaving it, though. LoadPreferences and TrayMenu.Run invoke the same thing - the consuming tool's toggle setter - one on a restore and one on a click. Filtering one and not the other makes identical user code behave differently depending on how it was reached. So the rule is stated once, in FatalError, and applied at the three places the library runs the tool's own code: the preference restore, the menu's action path, and the console runner's start action. Fatal means the process is already lost: OutOfMemoryException and AccessViolationException. StackOverflowException is absent because managed code cannot catch it, and InsufficientMemoryException is explicitly not fatal - it means one allocation was refused before being attempted, not that memory is gone. Two catches stay total on purpose. RunTray's is the "fall back, never die" case: the windowing backends report failure as whatever type they felt like, a console fallback always beats exiting, and there is no tool code involved. The preference store's timer callback is async void, where an escaping exception crashes the process rather than reporting anything. Verified: 108 tests (107 passing, 1 inconclusive by platform), including that the menu still absorbs an ordinary refusal and now lets a fatal one through; and the tray still round trips a remembered toggle across a restart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qksp7NcKB2jMJjXuxGS3nP
|
This was referenced Sep 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Found while starting the NoSleep migration onto
ktsu.TrayApp1.1.0 — NoSleep is the first real tool through this path, and it hits the bug.The bug
Restoring a remembered toggle calls the tool's setter:
That setter can refuse for exactly the reason a click can. Restoring "keep awake: on" asks the tool to take an inhibitor, and the machine may have none to give — NoSleep's
KeepAwakeController.Enable()throwsSleepBlockExceptionorPlatformNotSupportedExceptionin that case.LoadPreferencesruns fromRunAsyncbefore the menu is built, so there is nowhere for the refusal to land, and nothing catches it. The exception escapesRunAsyncand ends the process before the tray ever appears.That is the one failure mode this library exists to prevent, in the library's own startup path — and it is a regression against the tool it is meant to replace: NoSleep today wraps its startup
Enable()incatch (Exception ex) when (ex is SleepBlockException or PlatformNotSupportedException), prints the message, and still shows the tray so the user can retry once whatever was missing is installed. A tool ported onto 1.1.0 as-is would lose that.The fix
Catch per toggle, report to standard error, and hand the first message back for the menu's status line via a new internal
TrayMenu.SeedError, which the next successful action clears like any other failure. Two consequences worth stating:RunTraytakes the message and seeds the menu before the host starts, so the status line is already correct on the first paint rather than after the first refresh.Verified
LoadPreferencesreturns the message instead of propagating, and the second toggle is still restored; a clean restore returnsnull; and a seeded error shows asDemo failed: …in the status line and clears on the next successful action.xvfb, across two processes:A fresh process comes up already unchecked with the status line reading
idle. The restore path was previously only unit-tested; this is it working through a realcom.canonical.dbusmenumenu.Tagged
[patch]. As before, this is left for you to merge — it publishes.🤖 Generated with Claude Code
https://claude.ai/code/session_01C5iSfFNhum18dnc794cVvy
Generated by Claude Code