feat(ipc): display control socket, stage 1: on-demand with acks

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>
This commit is contained in:
Chuck
2026-09-30 21:28:31 -04:00
co-authored by Claude Opus 5.5
parent f4bda50710
commit af8dc3940a
18 changed files with 3129 additions and 42 deletions
+15 -2
View File
@@ -447,13 +447,23 @@ Request a specific plugin to display on-demand.
"mode": "nfl_live",
"duration": 45,
"pinned": true,
"service": { "active": true, "returncode": 0, "stdout": "", "stderr": "" }
"service": { "active": true, "returncode": 0, "stdout": "", "stderr": "" },
"transport": "socket"
}
}
```
`service` is `null` when `start_service` is false.
`transport` says how the request reached the display: `"socket"` means the
display's control socket acknowledged it (it is queued for the render thread;
see [IPC_CONTROL_SOCKET.md](IPC_CONTROL_SOCKET.md)), `"mailbox"` means it was
written to the cache mailbox the display polls, as before the socket existed.
With `"mailbox"`, `socket_error` gives the reason the socket was not used
(`no_socket` when the display is stopped or predates the socket, `timeout`,
`refused`, `busy`, ...). Either way the request is applied the same way;
`request_id` is the same id in both.
### Stop On-Demand Display
**POST** `/api/v3/display/on-demand/stop`
@@ -476,11 +486,14 @@ Stop the current on-demand display.
"status": "success",
"data": {
"request_id": "uuid-here",
"service": null
"service": null,
"transport": "socket"
}
}
```
`transport` and `socket_error` are as for start.
---
## Plugins