Repository navigation
feat(ipc): display control socket, stage 1 - on-demand with acks - #706
Conversation
The display process now serves a Unix socket, /run/ledmatrix/control.sock, carrying versioned newline-delimited JSON commands that are acknowledged. Stage 1 moves on-demand start/stop (plus status, hello and ping) onto it; the cache-file mailbox stays as the fallback for one release. - src/ipc/contract.py: typed request/response envelopes, command args, error codes, NDJSON framing with a 64 KiB limit, socket path rules. - src/ipc/server.py: threaded server owned by the display. Handlers only queue onto a bounded queue and ack with the request id; the render thread drains it where it reads the mailbox. Bounded clients, timeouts, garbage/oversize/disconnect handling; 0660 socket in the cache dir's group plus SO_PEERCRED checks; skips cleanly on Windows or when off. - src/ipc/client.py: one short-timeout request; any failure raises ControlError(reason). - api_v3/display.py: on-demand start/stop try the socket, fall back to the mailbox exactly as before, and report transport/socket_error. - display_controller.py: start/close the server; the mailbox handler body is extracted into _handle_on_demand_request and shared by both paths. - docs/IPC_CONTROL_SOCKET.md: protocol, security model, stage plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 17 minutes. View limit detailsLimit details: You’ve used the included review currently available. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (18)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 291 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
The on-demand routes returned ControlError.reason verbatim. Every reason the client raises is a fixed code, but the display's error code arrives over the socket, so map the value onto the known set (transport reasons plus ErrorCode) and report anything else as "other". Resolves CodeQL py/stack-trace-exposure on the on-demand stop/start responses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hardware check on ledpi (Pi 4, 192x48, 2026-10-01)Branch 1. Permissions: PASS. 2. Access: PASS, with one gap. As
Not run: an actual uid outside the group against the live socket. 3. Latency: no gain on this rig. Times run from the POST to
On Vegas the socket is no faster. The journal shows 4. Display restart with the web up: PASS.
5. Frame cost: PASS.
That is 9 late frames out of 14,441 on the branch and 5 out of 14,440 on main. Every run is under the 0.1% limit, and the spread between the two main runs (0 to 5) is as large as the gap between builds. There were no new journal warnings or errors. 🤖 Generated with Claude Code |
# Conflicts: # CHANGELOG.md # docs/README.md
#703, #705, #706 and #693 merged after this branch was cut; their CHANGELOG entries now sit under 3.8.0. The summary and New modules list name them (sports consolidation stage 4's four modules, src/ipc, field_model), stage 4's section says to floor on 3.8.0, and src/common/README.md marks its four modules 3.8.0 instead of Unreleased. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(deprecation): remove the 35 APIs deprecated for 3.8.0 The usage scan (docs/DEPRECATIONS_3.8.md, regenerated 2026-10-01 and committed here) finds no call or override of any of them in the 46 monorepo plugins or the 8 third-party plugins plugins.json lists; the only core callers were other deprecated methods removed alongside. - CacheManager: 13 methods, plus the private helpers only has_data_changed used (_has_*_changed, _is_market_open). - DisplayManager: 7 methods, plus WEATHER_COLORS and the private _draw_sun/_cloud/_rain/_snow/_storm helpers only the icon methods used. - FontManager: 14 methods, plus size_tokens, _save_overrides and _clear_plugin_font_cache. font_overrides and _load_overrides stay: resolve_font() still applies config/font_overrides.json. performance_stats stays: get_font() keeps it and tests read it. - PluginManager.get_enabled_plugins. test_deprecation.py pins only the two 3.9.0 markers now; the scanner tests run against a stand-in core instead of the real markers. The memory-tier tests read stats through log_memory_cache_stats() and the component, and the test of the removed _clear_plugin_font_cache goes. Docs drop the removed methods' reference entries; the Deprecated APIs table becomes "Removed in 3.8.0". CHANGELOG gains a Removed section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(deprecation): drop the test harness's copies of the removed icon methods VisualTestDisplayManager still drew weather icons that DisplayManager no longer has, so a plugin's visual tests could pass on calls that raise AttributeError on the real display. Its draw_sun/draw_cloud/draw_rain/ draw_snow/draw_weather_icon/draw_text_with_icons, WEATHER_COLORS and the private helpers go, with the tests that exercised them. The CHANGELOG's Deprecations entries no longer say nothing is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: prepare the 3.8.0 release Bumps src.__version__ to 3.8.0 and turns Unreleased into ## 3.8.0, with a summary and a New modules list (vegas_elements, testing.vegas, sports_vegas; display_watchdog, plugin_catalog, plugin_runtime) for plugins flooring on 3.8.0. Adds the CHANGELOG line #701's second commit lacked (blocks laid out off the render thread). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: 3.8.0 also ships what landed on main since the prep #703, #705, #706 and #693 merged after this branch was cut; their CHANGELOG entries now sit under 3.8.0. The summary and New modules list name them (sports consolidation stage 4's four modules, src/ipc, field_model), stage 4's section says to floor on 3.8.0, and src/common/README.md marks its four modules 3.8.0 instead of Unreleased. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Stage 1 of roadmap theme T2: the display process serves a control socket, and the web interface uses it for on-demand start and stop. Commands get an acknowledgement. The file mailbox stays as the fallback for one release.
Full write-up:
docs/IPC_CONTROL_SOCKET.md(protocol, security model, stage plan).What changes
src/ipc/contract.py: the typed, versioned messages. Standard library only; both processes import it.{v, id, cmd, args}, response{v, id, ok, result | error{code, message}}.hello(version negotiation),ping,on_demand.start(plugin_id/mode/duration/pinned),on_demand.stop,on_demand.status.src/ipc/server.py(display side): a threaded server on/run/ledmatrix/control.sock.{accepted, request_id}.on_demand.statusanswers from a read-only snapshot.AF_UNIX, or withLEDMATRIX_CONTROL_SOCKET=off. A non-root dev run uses a private per-user path.src/ipc/client.py(web side): one request with a 1 s total budget. Any failure raisesControlError(reason).api_v3/display.py:on-demand/startand/stoptry the socket first. On any failure they write the mailbox exactly as before.transport: "socket" | "mailbox", plussocket_erroron a fallback.request_id, so a request that arrives both ways is processed once.display_controller.py: kept small, becauseclaude/run-loop-stage1is restructuringrun(). The changes:run()gets one line:_start_control_server().cleanup()closes the server._poll_on_demand_requestsis extracted, unchanged, into_handle_on_demand_request(request). The poll drains the socket queue into that same handler first, so there is no duplicated activation code._service_pending_changesskips its 0.25 s floor while a socket command is queued._on_demand_state()is factored out of_publish_on_demand_stateso the status command can reuse it./run/ledmatrixalready comes from feat(display): systemd watchdog and heartbeat for a frozen render loop #687'sRuntimeDirectory=. Under an older unit the display creates it as root, as it already does for the heartbeat.test/conftest.py: an autouse fixture setsLEDMATRIX_CONTROL_SOCKET=off. A suite run on a device therefore never serves or drives the live display's socket.src/ipcmodules; CHANGELOG entry; ARCHITECTURE, PERMISSIONS, REST_API_REFERENCE and docs index updated.Security model
/run/ledmatrix: root-owned,0755, tmpfs, removed when the display stops.root:<shared group>, mode0660, so the kernel refusesconnect()to anyone else.ledmatrixon an installed device, and it is the same rule DiskCache uses for every file the two services share.get_shared_group_gid()).0600.SO_PEERCREDis checked as well. Allowed: root, the display's own user, or a member of the group (primary gid, or a supplementary group from/proc/<pid>/status, falling back to the group database). Anyone else getsforbidden. This also covers a socket mode loosened by hand.Verification
Windows unit tests (this host): the new and adjacent tests give 256 passed and 31 skipped (the skips are the Linux-only socket tests).
WSL Ubuntu 24.04 (Python 3.12), real sockets:
test_ipc_contract,test_ipc_server,test_ipc_display_hookandtest_api_v3_on_demand_socketgive 172 passed as an ordinary user and 172 passed as root.nobody:connect()(EACCES);forbidden.close()unlinking any file;request_idhanded to the handler.run.py -eas root (bundledweb-ui-infoplugin), and the real Flask routes as the userchuck, who is a supplementary member of the stand-in shared groupusers(/var/cache/ledmatrixset toroot:users 2775).srw-rw---- root users /run/ledmatrix/control.sock, logged as "Control socket listening ... (mode 660, group 100)".chuck,hello,pingandstatusall answered.statusreportedcurrent_mode: web_ui_info.transport: "socket"in 1.6-1.8 ms of HTTP. Each start was active 0.41-0.44 s later and each stop idle 1.0 s later.nobodywas refused atconnect()(EACCES →refused).no_socket, and the route answeredtransport: "mailbox"withsocket_error: "no_socket", with the request in the mailbox.Full suite (Windows,
pytest -m "not hardware" test/ --ignore=test/plugins), branch against a baseline worktree oforigin/main: baselineorigin/main(f4bda50) gave 59 failed, 6934 passed, 143 skipped, 6 errors; this branch gave 59 failed, 7077 passed, 174 skipped, 6 errors. The sorted FAILED/ERROR test IDs are identical (71 lines each, 0 differences). The failures are this host's known Windows-only ones. The extra passes and skips are the new tests; the skips are the Linux-only socket tests.plugin-repos/was untouched after the run.mypy ratchet (
scripts/check_types.py, mypy 1.20.2): no issues in 82 modules, the 4 new ones included. flake8 (pre-commit selection) and bandit are clean on the new files.Checks needed on ledpi (not run; the rigs were not touched)
ls -l /run/ledmatrix/control.sock; it should showsrw-rw---- root ledmatrix. Checkid ledpi; it should includeledmatrix.journalctl -u ledmatrix | grep "Control socket"should showlistening ... (mode 660, group <ledmatrix gid>).sudo -u ledpi python3 -c 'from src.ipc import client; print(client.ping())'from the project directory, and the same as another account (sudo -u nobody ...), which should be refused. Then click on-demand in the UI; the response should show"transport": "socket".POST /api/v3/display/on-demand/start|stop, readingdisplay_on_demand_state. Do it three ways:LEDMATRIX_CONTROL_SOCKET=offin a drop-in forledmatrix-web.service, for the mailbox numbers.sudo systemctl restart ledmatrixand, during the restart, POST on-demand start. Expecttransport: "mailbox",socket_error: "no_socket", and the request picked up from the mailbox once the display is back. After it is up, expecttransport: "socket"again without restarting the web service.python3 scripts/frame_soak.py --previewagainst main. The server threads sleep inaccept(), and the render thread adds oneEvent.is_set()per on-demand check.🤖 Generated with Claude Code