mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
refactor(plugins): the display publishes plugin runtime state; retire plugin_state.json (#690)
Stage 2 of the web plugin catalog, after #688. - The display publishes a plugin runtime snapshot (plugin_runtime.py) to the shared cache: per plugin loaded, lifecycle state, a short redacted error summary, the version it loaded and when, plus published_at / stale_after / running. Written on change (throttled to 10 s; the RUNNING/ENABLED flip of an ordinary update is not a change) and once a minute otherwise; cleanup() publishes running: false. - The web reads it back and restores loaded / state / error_info in /api/v3/plugins/installed (plus loaded_version, loaded_at and data.runtime). Only a live snapshot counts; stale, stopped or missing answers null and says which. - data/plugin_state.json is retired: every reader and writer moved to config + disk (desired) or the snapshot (observed). Nothing in it was non-derivable, so nothing is migrated and an existing file is left unread. The web-side PluginStateManager (state_manager.py) is removed; the display's plugin_state.PluginStateManager is the only state machine. - StateReconciliation compares config + disk with the snapshot, reporting enabled-but-not-loaded and older-version-loaded as no_action findings. - Backups list installed manifests with enabled from config.json. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,48 +1,16 @@
|
||||
"""plugin_state.json and the operation history file are replaced atomically.
|
||||
"""The operation history file is replaced atomically.
|
||||
|
||||
Both were written with a plain ``open(path, 'w')`` + ``json.dump`` outside
|
||||
their lock. The open truncates first, so a value json can't encode (or a
|
||||
crash, or a second Flask thread saving at the same moment) left a partial
|
||||
file, and the next load dropped every saved state. They now serialise first
|
||||
and go through a temp file + rename while holding the lock.
|
||||
It was written with a plain ``open(path, 'w')`` + ``json.dump`` outside its
|
||||
lock. The open truncates first, so a value json can't encode (or a crash, or
|
||||
a second Flask thread saving at the same moment) left a partial file, and the
|
||||
next load dropped every saved record. It now serialises first and goes
|
||||
through a temp file + rename while holding the lock. (plugin_state.json had
|
||||
the same fix; it is retired now -- nothing writes it.)
|
||||
"""
|
||||
|
||||
import json
|
||||
import threading
|
||||
|
||||
from src.plugin_system.operation_history import OperationHistory
|
||||
from src.plugin_system.state_manager import PluginStateManager
|
||||
|
||||
|
||||
def test_state_file_survives_a_failed_save(tmp_path):
|
||||
state_file = tmp_path / "plugin_state.json"
|
||||
mgr = PluginStateManager(state_file=str(state_file))
|
||||
mgr.set_plugin_enabled("clock", True)
|
||||
before = json.loads(state_file.read_text())
|
||||
|
||||
# Not JSON-serialisable: the save fails (and is logged, not raised).
|
||||
mgr.update_plugin_state("clock", {"metadata": {"bad": object()}})
|
||||
|
||||
assert json.loads(state_file.read_text()) == before
|
||||
assert [p.name for p in tmp_path.iterdir()] == ["plugin_state.json"]
|
||||
|
||||
|
||||
def test_state_file_is_valid_after_concurrent_saves(tmp_path):
|
||||
state_file = tmp_path / "plugin_state.json"
|
||||
mgr = PluginStateManager(state_file=str(state_file))
|
||||
|
||||
def worker(n):
|
||||
for i in range(15):
|
||||
mgr.set_plugin_enabled(f"plugin-{n}", i % 2 == 0)
|
||||
|
||||
threads = [threading.Thread(target=worker, args=(n,)) for n in range(6)]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
data = json.loads(state_file.read_text())
|
||||
assert set(data["states"]) == {f"plugin-{n}" for n in range(6)}
|
||||
|
||||
|
||||
def test_history_file_survives_a_failed_save(tmp_path):
|
||||
|
||||
Reference in New Issue
Block a user