mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-05 14:55:08 +00:00
feat(web): a start that waits for the display answers 202 and is delivered in the background
The start route held a request open for up to 45 s while a cold-started display loaded its plugins; the MQTT bridge (15 s timeout) and browsers reported a failure for a request that was then delivered. Now, when no display is listening, the route starts the service if asked and answers 202 with status "starting" at once. A single worker in the web process (web_interface/on_demand_dispatch.py) sends the request until the display acknowledges it or the wait runs out (45 s cold start, 10 s for a running service without a socket yet). A newer start supersedes the pending one; a stop cancels it (and succeeds, with cancelled_request_id, even with no display listening). The outcome is reported by /display/on-demand/status (source "web": starting, or error with start-timeout or the socket's reason, until the display publishes something newer) and by /display/current-status as on_demand_pending. Callers: the web UI's on-demand modal and "Preview on display" treat "starting" as taken (an info toast); the MQTT bridge already treats any non-error 2xx as success (now pinned by a test). Tests: the dispatcher (ack, retry then ack, start-timeout, other failures, superseded, an in-flight ack for a superseded start, stop while pending, a per-start wait, outcome lifetime); the routes (202, status routes while pending and after a timeout, a later display state replacing the failure, stop while pending, a new start superseding); a JS suite for app.js. Mutation check: 20 mutants on the worker, the routes and app.js, 20 killed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,8 @@ This file previously pinned that restart path (it guarded a broken
|
||||
``import _pkg.time`` inside it). The path is gone; these tests pin its
|
||||
replacement: a running service is left alone, a stopped one is started (only
|
||||
when start_service is set), and the request goes over the control socket
|
||||
either way -- to a stopped display once it has started and its socket is up.
|
||||
either way -- to a stopped display once it has started and its socket is up,
|
||||
sent by the web process's dispatcher after the route has answered 202.
|
||||
Nothing is ever written to the cache: the file mailbox is gone (stage 5).
|
||||
|
||||
The service helpers are patched where they run. display.py binds
|
||||
@@ -26,6 +27,7 @@ _run_systemctl_command is the one place a systemctl command is issued.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -157,10 +159,18 @@ class TestStartWhileTheServiceIsStopped:
|
||||
def test_start_service_starts_it_once_and_never_stops_it(self, api_v3_client, service):
|
||||
service["state"]["active"] = False
|
||||
response = api_v3_client.post(START_URL, json={"plugin_id": "weather"})
|
||||
assert response.status_code == 200, response.get_json()
|
||||
# Answered at once: the web process sends the request in the
|
||||
# background once the started display listens.
|
||||
assert response.status_code == 202, response.get_json()
|
||||
assert response.get_json()["status"] == "starting"
|
||||
assert _systemctl_verbs(service["systemctl"]) == ["start"]
|
||||
service["stop_service"].assert_not_called()
|
||||
# Sent once the started display's socket answered.
|
||||
from web_interface import on_demand_dispatch
|
||||
dispatcher = on_demand_dispatch.current()
|
||||
deadline = time.monotonic() + 5
|
||||
while dispatcher.pending() and time.monotonic() < deadline:
|
||||
time.sleep(0.01)
|
||||
assert dispatcher.status()["status"] == "delivered"
|
||||
assert [s[0] for s in service["sent"]] == ["start"]
|
||||
assert _mailbox_writes(service["cache"]) == []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user