Fix stat widgets freezing permanently after a transient sensor error#1036
Open
axectly wants to merge 3 commits into
Open
Fix stat widgets freezing permanently after a transient sensor error#1036axectly wants to merge 3 commits into
axectly wants to merge 3 commits into
Conversation
Every stat is scheduled in its own thread with its own sched.scheduler. An exception raised by the job propagates out of scheduler.run(), so that thread dies and its widget freezes on screen until the program is restarted. Nothing is logged when this happens: the thread exception goes to sys.stderr, which is None in the packaged windowed executable. Catch exceptions around the job instead. The next run is already scheduled at that point, so the stat simply skips one cycle and recovers. Failures are now logged, rate-limited to one message per second per job because some jobs (e.g. QueueHandler) run every millisecond. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ping3 returns None on timeout (and False for an unknown host), so Ping.stats raised TypeError on int(delay) as soon as one ping timed out, and the None was also stored in the line graph history. DisplayLineGraph calls math.isnan() on every stored value, which raises TypeError on None, so subsequent redraws of that graph failed too, for as long as the value stayed in the history buffer. Combined with a scheduled job exception killing its thread, a single ping timeout was enough to freeze the ping widget permanently. Normalize a missing ping delay to math.nan, which is what this code already uses as its "no data" marker (see last_values_list), and skip the widgets that need a number for that cycle. Guard save_last_value too, so any sensor returning None cannot poison a graph history. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sensors already use math.nan to report "no value" (LibreHardwareMonitor does it
for CPU load and fan speed when it cannot read them), but the display helpers
convert the value with int(), which raises ValueError on nan and kills the
thread of that stat, freezing its widgets until the program is restarted:
File "library/stats.py", line 208, in display_themed_percent_radial_bar
ValueError: cannot convert float NaN to integer
For the radial bar and the text value the conversion also happens before the
SHOW check inside the called function, so a hidden widget crashes the stat too.
Skip drawing when there is no value, as reported in mathoudebine#718.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Problem
A stat widget can freeze permanently while the rest of the program keeps running fine. It happens at random, after minutes or hours, and nothing is written to
log.log.Three things combine:
1. A scheduled job that raises kills its thread for good. Each stat is scheduled in its own thread with its own
sched.scheduler(library/scheduler.py). An exception raised by the job propagates out ofscheduler.run(), the thread exits and that widget is never updated again. The traceback is invisible in the packaged Windows executable, becausethreading.excepthookwrites tosys.stderr, which isNonethere.2.
ping3returnsNoneinstead of a number. On a ping timeout,Ping.stats()raises atint(delay). TheNoneis also stored in the line graph history, andDisplayLineGraph()callsmath.isnan()on every stored value, which raises onNonefor as long as the value stays in the buffer.3.
int()is called on values that aremath.nan. Sensors already usemath.nanto report "no value" — LibreHardwareMonitor does it for CPU load and fan speed when it cannot read them — but the display helpers convert withint(), which raisesValueError. Fordisplay_themed_percent_radial_bar()anddisplay_themed_percent_value()the conversion happens before theSHOWcheck inside the called function, so the stat crashes even when the widget is hidden. This looks like the cause of #718.Observed on Windows 11 / Python 3.13,
HW_SENSORS: AUTO(LHM), Wi-Fi connection,PING: 8.8.8.8.Cause 2, during a short network outage:
and then, on the following cycles, once the network was back:
Cause 3, twice in one day, with
RADIALnot even shown in the theme:(the
[ERROR] Scheduled job ...lines come from this PR; without it the thread dies silently at the first traceback)Changes
library/scheduler.py: wrap the scheduled call intry/except, log the failure and keep going — the next run is already scheduled at that point, so the stat just skips one cycle. Logging is rate-limited to one message per second per job, becauseQueueHandlerruns every millisecond and a permanently failing job would otherwise floodlog.log(which rotates at 1 MB).library/stats.py: normalize a missing ping delay tomath.nanand skip, for that cycle, the widgets that need a number.save_last_value()mapsNonetomath.nanas well, so no sensor returningNonecan poison a graph history —math.nanis already this code's "no data" marker, seelast_values_list().library/stats.py: skip drawing in the display helpers when the value isNoneornan, instead of lettingint()raise.The changes are complementary: the last two remove the two known ways a stat can raise, the first one makes sure that any remaining one only costs a cycle instead of the whole thread, and becomes visible in the log.
SystemExitandKeyboardInterruptderive fromBaseExceptionand are not caught, so the clean shutdown path inmain.pyis unaffected.Tested on a Turing 5" (rev C) with Windows 11 and Python 3.13.
🤖 Generated with Claude Code