fix(display): Vegas resumes after live priority, and six smaller runtime fixes (#644)

- Vegas: a live-priority pause was only lifted from inside run_frame(),
  which returns before that check while paused, so the ticker never came
  back until a restart. run_iteration() now resumes it (the controller
  only calls it when nothing preempts Vegas); start()/stop() clear the
  pause state. Iteration length is timed with the monotonic clock.
- Dim schedule: a per-day disabled day now updates the minute-gate cache,
  so brightness no longer flips back to dim within each minute.
- On-demand: a second request no longer overwrites the rotation resume
  index with the first request's mode.
- Render pipeline: reset() drops the prepared group and deferred queue,
  and a prefetch in flight across a reset discards its result.
- Sync: stop() removes the status file (and the controller's cleanup now
  calls it), standalone removes a stale one at startup, and writes use a
  unique mkstemp temp file.
- render_gate.swap_releases_gil() delegates to frame_timing.
- Stale docstrings/comments corrected.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-28 08:25:26 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent bcef1957a9
commit 6bc13a8934
14 changed files with 401 additions and 32 deletions
+42 -1
View File
@@ -875,9 +875,50 @@ class TestWriteStatusFile:
def test_write_failure_is_swallowed(self, monkeypatch):
mgr = make_manager(role=SyncRole.STANDALONE)
monkeypatch.setattr("builtins.open", MagicMock(side_effect=OSError("disk full")))
monkeypatch.setattr(sync_manager.json, "dump",
MagicMock(side_effect=OSError("disk full")))
mgr.write_status_file() # must not raise
assert mgr.logger.debug.called
# ...and the half-written temp file is not left behind.
assert list(Path(sync_manager.STATUS_FILE).parent.iterdir()) == []
def test_concurrent_writers_each_use_their_own_temp_file(self):
"""The receive loop, watchdog and hello handler all write. With one
fixed ".tmp" name, one writer's os.replace() could move the other's
half-written file into place, or find it already gone."""
mgr = make_manager(role=SyncRole.LEADER)
errors = []
mgr.logger.debug.side_effect = lambda *a: errors.append(a)
def hammer():
for _ in range(50):
mgr.write_status_file()
threads = [threading.Thread(target=hammer) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
assert errors == []
assert [p.name for p in Path(sync_manager.STATUS_FILE).parent.iterdir()] == [
Path(sync_manager.STATUS_FILE).name]
assert json.loads(Path(sync_manager.STATUS_FILE).read_text())["role"] == "leader"
def test_stop_withdraws_the_status_file(self):
"""Left behind, the web UI kept reporting a connected peer after the
display service had stopped."""
mgr = make_manager(role=SyncRole.LEADER)
mgr.write_status_file()
assert Path(sync_manager.STATUS_FILE).exists()
mgr.stop()
assert not Path(sync_manager.STATUS_FILE).exists()
mgr.write_status_file() # a late write from a thread still winding down
assert not Path(sync_manager.STATUS_FILE).exists()
def test_standalone_removes_a_stale_status_file(self):
Path(sync_manager.STATUS_FILE).write_text('{"role": "leader", "state": "connected"}')
DisplaySyncManager("standalone", {}, {}, MagicMock())
assert not Path(sync_manager.STATUS_FILE).exists()
def test_web_status_endpoint_reads_the_file_that_was_written(self, api_v3_client):
"""GET /sync/status reads STATUS_FILE, which lives under