mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-24 11:58:15 +00:00
fix(display): retry a plugin that is enabled but failed to load
A plugin whose validate_config() returns False is treated as a hard load failure. The API then reports enabled=true, loaded=false, error=null: the plugin is simply absent, with nothing saying why. hockey-scoreboard sat in that state on a live rig for four days. The recovery path existed but could not be reached. _reconcile_enabled_plugins computes to_add = desired - current, and a plugin that failed to load is never in current, so it stays in to_add and would be retried. But the reconcile is queued by _enabled_set_changed(), which compares only top-level `enabled` flags -- and the edit that actually fixes such a plugin (enabling a league, filling in an API key) is nested inside the plugin's own config section. No top-level flag changes, so no reconcile is queued, and the save that should have fixed it does nothing. Only toggling some unrelated plugin -- which does change a top-level flag -- queues the global reconcile that recovers it. Add a second gate: queue a reconcile when a discovered plugin is enabled in config but absent from the running set. It is deliberately narrow rather than "reconcile on any config change". Reconcile calls discover_plugins(), a ~39-manifest filesystem scan, and it runs on the render thread; doing that on every config save would trade this bug for a frame hitch. Gating on plugin_manifests also keeps non-plugin sections that carry their own `enabled` flag (schedule, display) from queueing a reconcile they can never satisfy. In the steady state -- every enabled plugin loaded -- the new check is False and costs nothing. The same valid-but-unconfigured => hard-fail shape still exists in text-display, youtube-stats, birdnet-go, ledmatrix-flights and mqtt-notifications; this makes all of them recoverable without a restart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW
This commit is contained in:
co-authored by
Claude Opus 5
parent
6b74506695
commit
4143aa958c
@@ -463,7 +463,8 @@ class DisplayController:
|
||||
self._refresh_config_cache(new_config)
|
||||
# If a plugin was enabled/disabled, flag a reconcile for the main
|
||||
# loop to apply (loading/unloading off the watcher thread is unsafe).
|
||||
if self._enabled_set_changed(old_config, new_config):
|
||||
if (self._enabled_set_changed(old_config, new_config)
|
||||
or self._enabled_plugin_not_running(new_config)):
|
||||
self._pending_plugin_reconcile = True
|
||||
|
||||
self.config_service.subscribe(_controller_config_change)
|
||||
@@ -2892,6 +2893,41 @@ class DisplayController:
|
||||
}
|
||||
return enabled_map(old_config) != enabled_map(new_config)
|
||||
|
||||
def _enabled_plugin_not_running(self, new_config: Dict[str, Any]) -> bool:
|
||||
"""True when a discovered plugin is enabled in config but not running.
|
||||
|
||||
``_enabled_set_changed`` compares only top-level ``enabled`` flags, which
|
||||
misses the case that strands a plugin: one whose ``validate_config()``
|
||||
returned False is absent from the running set, and the edit that fixes it
|
||||
(enabling a league, filling in an API key) lives *nested* inside that
|
||||
plugin's own section. No top-level flag changes, so no reconcile is
|
||||
queued, and the save that should have fixed it appears to do nothing --
|
||||
only toggling some unrelated plugin recovers it. hockey-scoreboard sat
|
||||
enabled-but-absent on a live rig for four days this way.
|
||||
|
||||
Deliberately narrow: it fires only for ids the plugin manager has
|
||||
actually discovered, so non-plugin sections that carry their own
|
||||
``enabled`` flag (``schedule``, ``display``, ...) don't queue a reconcile
|
||||
on every save. In the steady state -- everything enabled is loaded --
|
||||
this is False and costs nothing. That matters because reconcile runs
|
||||
``discover_plugins()`` on the render thread, where a needless
|
||||
filesystem scan per config save would show up as a frame hitch.
|
||||
"""
|
||||
if self.plugin_manager is None:
|
||||
return False
|
||||
try:
|
||||
known = set(self.plugin_manager.plugin_manifests)
|
||||
running = set(self.plugin_display_modes)
|
||||
except (RuntimeError, AttributeError):
|
||||
# Mid-mutation on the render thread, or a manager without the
|
||||
# attribute. Let reconcile decide -- it no-ops when nothing differs.
|
||||
return True
|
||||
for key, value in new_config.items():
|
||||
if (key in known and isinstance(value, dict)
|
||||
and value.get('enabled', False) and key not in running):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _reconcile_enabled_plugins(self) -> bool:
|
||||
"""Load/unload plugins so the running set matches the enabled set in
|
||||
config. Runs on the main display thread (never the config-watcher
|
||||
|
||||
Reference in New Issue
Block a user