From 6e68300d050c37beadaf831f408209f956e18729 Mon Sep 17 00:00:00 2001 From: Florian Egger Date: Mon, 7 Sep 2026 12:30:26 +0200 Subject: [PATCH] build: add systemd autostart for the receiver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ship systemd/screencast-receiver.service, a template the install script substitutes (binary path, run user) and enables so the receiver starts at boot after the network and avahi-daemon, with restart-on-failure and a raised start limit for slow-booting boards. SIGTERM gives the graceful shutdown that withdraws the mDNS announcement — systemctl stop never triggers the stale-record trap that kill -9 does. Headless consoles render via KMSDRM (the script adds the run user to video/render/input); desktop autologin sessions uncomment two documented Environment lines. The install script gains SC_RECEIVER_SERVICE=0 to opt out. Validated: systemd-analyze verify on the substituted unit (only the expected missing /usr/local/bin path on this dev machine), and a live lifecycle test as a user unit: the service-managed receiver announced itself (--discover found it), systemctl stop produced a clean exit (Result=success, no restart) and the mDNS announcement was withdrawn immediately. --- .agents/MEMORY.md | 6 ++++ README.md | 4 ++- docs/RUNBOOK.md | 20 ++++++++++++ scripts/install-receiver.sh | 50 +++++++++++++++++++++++------ systemd/screencast-receiver.service | 46 ++++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 11 deletions(-) create mode 100644 systemd/screencast-receiver.service diff --git a/.agents/MEMORY.md b/.agents/MEMORY.md index bec252c..24284fc 100644 --- a/.agents/MEMORY.md +++ b/.agents/MEMORY.md @@ -31,6 +31,12 @@ loopback; current phase is Phase 7. building SDL3 from source when the distro lacks it. Requires GCC 13+ (``). Validated on x86_64: builds, tests 5/5, --discover works, --send refuses with a message. +- **systemd autostart**: `systemd/screencast-receiver.service` is a + template (`__SC_RECEIVER_BIN__`/`__SC_RECEIVER_USER__`); the install + script substitutes and enables it (opt out: `SC_RECEIVER_SERVICE=0`), + adding the run user to video/render/input for headless KMSDRM. + Lifecycle-validated with a user unit on this machine: SIGTERM stop → + clean exit (success), mDNS withdrawn, no restart. - **mDNS operational lesson (hit during validation)**: `kill -9` on a process holding an avahi registration leaves stale daemon records; the service then browses but never resolves (timeout for every peer) until diff --git a/README.md b/README.md index 9820a7c..e5504ff 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,9 @@ The script installs the dependencies, builds a receiver-only binary (`-Dsender=false`), runs the test suite, and installs to `/usr/local/bin` (override with `SC_RECEIVER_INSTALL_DIR`). It needs Raspberry Pi OS Trixie or newer (GCC 13+ for C++20 ``), builds SDL3 from source when the -distribution does not package it, and enables `avahi-daemon`. +distribution does not package it, and enables `avahi-daemon`. It also +installs and enables a systemd service so the receiver starts at boot — +`systemctl status screencast-receiver` to check on it. Performance note: decoding is software H.264; on very small boards expect smooth playback for modest resolutions and reduced frame rates at high diff --git a/docs/RUNBOOK.md b/docs/RUNBOOK.md index c6a37be..f7e9bef 100644 --- a/docs/RUNBOOK.md +++ b/docs/RUNBOOK.md @@ -70,6 +70,26 @@ records for that service name until they expire (~75 min), making resolution time out for every peer. If that happens, `systemctl restart avahi-daemon` clears it. Always stop screencast with Ctrl-C (SIGTERM). +## Receiver as a systemd service (autostart) + +`scripts/install-receiver.sh` installs and enables +`screencast-receiver.service` (see `systemd/screencast-receiver.service`). +It starts at boot after the network and avahi-daemon, and restarts on +failure. On a headless console SDL renders via KMSDRM; on a desktop +autologin, edit `/etc/systemd/system/screencast-receiver.service` and +uncomment the Environment lines for `XDG_RUNTIME_DIR`/`WAYLAND_DISPLAY`. + +```sh +systemctl status screencast-receiver # is it running? +journalctl -u screencast-receiver -f # follow logs +sudo systemctl stop screencast-receiver # graceful; withdraws mDNS +sudo systemctl disable --now screencast-receiver # turn autostart off +``` + +Verified behavior: `systemctl stop` results in a clean exit (success, +no restart) and the mDNS announcement is withdrawn — senders stop finding +the receiver immediately. + ## Sender / receiver loopback (Phase 5, manual) Two terminals on the same desktop session: diff --git a/scripts/install-receiver.sh b/scripts/install-receiver.sh index 22bf076..ce7ad6f 100755 --- a/scripts/install-receiver.sh +++ b/scripts/install-receiver.sh @@ -15,6 +15,7 @@ # Environment overrides: # SC_RECEIVER_INSTALL_DIR install target (default /usr/local/bin) # SC_RECEIVER_SKIP_TESTS set to 1 to skip the test suite (faster install) +# SC_RECEIVER_SERVICE set to 0 to skip the systemd service setup # # Requirements: # - Debian/Raspberry Pi OS (apt) with GCC 13 or newer (the code uses @@ -28,6 +29,7 @@ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" BUILD_DIR="${REPO_ROOT}/build-receiver" INSTALL_DIR="${SC_RECEIVER_INSTALL_DIR:-/usr/local/bin}" SKIP_TESTS="${SC_RECEIVER_SKIP_TESTS:-0}" +ENABLE_SERVICE="${SC_RECEIVER_SERVICE:-1}" log() { printf '\n=== %s\n' "$*"; } die() { printf 'install-receiver: error: %s\n' "$*" >&2; exit 1; } @@ -99,22 +101,50 @@ install -m 755 "${BUILD_DIR}/src/app/screencast" "${INSTALL_DIR}/screencast" systemctl enable --now avahi-daemon 2>/dev/null || true +# --- systemd autostart -------------------------------------------------------- +if [ "${ENABLE_SERVICE}" = "1" ] && command -v systemctl >/dev/null 2>&1; then + log "installing the systemd service (autostart on boot)" + RUN_USER="${SUDO_USER:-root}" + + # Headless KMSDRM rendering needs device access; skip groups that do + # not exist on this system. + if [ "${RUN_USER}" != "root" ] && id "${RUN_USER}" >/dev/null 2>&1; then + for group in video render input; do + if getent group "${group}" >/dev/null 2>&1; then + usermod -aG "${group}" "${RUN_USER}" || true + fi + done + fi + + sed -e "s|__SC_RECEIVER_BIN__|${INSTALL_DIR}/screencast|g" \ + -e "s|__SC_RECEIVER_USER__|${RUN_USER}|g" \ + "${REPO_ROOT}/systemd/screencast-receiver.service" \ + > /etc/systemd/system/screencast-receiver.service + chmod 644 /etc/systemd/system/screencast-receiver.service + systemctl daemon-reload + systemctl enable --now screencast-receiver.service +else + log "skipping the systemd service (disabled or systemd not available)" +fi + log "done." cat <<'EOF' -The receiver is installed. On this machine run: +The receiver is installed and starts automatically at boot. - screencast --receive - -and on the sending machine: - - screencast --send # discovers this receiver on the LAN + systemctl status screencast-receiver # is it running? + journalctl -u screencast-receiver -f # follow its logs + sudo systemctl stop screencast-receiver # stop gracefully (Ctrl-C like) + sudo systemctl disable --now screencast-receiver # turn autostart off Notes for small boards (e.g. Pi Zero 2 W): - Decoding is software H.264; expect smooth playback for small streams and a lower frame rate at high resolutions. - - Stop the receiver with Ctrl-C; never kill -9 (it would leave stale - mDNS records that break discovery for ~75 minutes). - - The receiver needs avahi-daemon (enabled) and a graphical session or - KMS console to open its window. + - Stop the receiver via systemctl (never kill -9; stale mDNS records + would break discovery for ~75 minutes). + - The service runs on the headless console via KMSDRM. On a desktop + session instead, edit /etc/systemd/system/screencast-receiver.service: + uncomment the Environment lines and point XDG_RUNTIME_DIR at the + session owner's /run/user/ (see the comments in that file). + - avahi-daemon must stay enabled; it is what senders discover. EOF \ No newline at end of file diff --git a/systemd/screencast-receiver.service b/systemd/screencast-receiver.service new file mode 100644 index 0000000..d055c40 --- /dev/null +++ b/systemd/screencast-receiver.service @@ -0,0 +1,46 @@ +# screencast-receiver.service — autostart for the receiver. +# +# This file is a template: scripts/install-receiver.sh substitutes +# __SC_RECEIVER_BIN__ and __SC_RECEIVER_USER__ before installing it to +# /etc/systemd/system/screencast-receiver.service. +# +# Display environments: +# +# 1. Headless console (SDL renders via KMSDRM): works as installed. The +# configured User needs access to /dev/dri and the input devices; the +# install script adds it to the video/render/input groups. +# +# 2. Graphical session (Wayland or X11 desktop with autologin): uncomment +# the Environment lines in [Service] and adjust them to the session +# owner (XDG_RUNTIME_DIR usually lives in /run/user/1000). + +[Unit] +Description=Screencast receiver (H.264 over RTP/UDP) +Wants=network-online.target +After=network-online.target avahi-daemon.service +# Give slow-booting boards (and desktop sessions that start late) a chance; +# systemd's default limit would give up after 5 fast failures. +StartLimitIntervalSec=30 +StartLimitBurst=10 + +[Service] +Type=simple +User=__SC_RECEIVER_USER__ +ExecStart=__SC_RECEIVER_BIN__ --receive +# SIGTERM (systemctl stop) shuts the receiver down gracefully, which +# withdraws its mDNS announcement. Never SIGKILL it; the systemd default +# KillMode escalates only after the graceful timeout. +KillSignal=SIGTERM +TimeoutStopSec=10 +Restart=on-failure +RestartSec=3 +NoNewPrivileges=true +PrivateTmp=true + +# Graphical session: uncomment and adjust to the session owner. +#Environment=XDG_RUNTIME_DIR=/run/user/1000 +#Environment=WAYLAND_DISPLAY=wayland-0 +#Environment=DISPLAY=:0 + +[Install] +WantedBy=multi-user.target \ No newline at end of file