fix(systemd): give the receiver service its own VT instead of tty1

The receiver service never started on the Pi: systemd blocks before
exec in acquire_terminal(), waiting for a controlling terminal that
the console session or getty on tty1 already owns. Symptoms: the
status shows the main PID as "(screencast)" with Tasks:1 and ~40ms
CPU, a silent journal, and no mDNS announcement — the binary never
ran. Reproduced locally with a transient unit (the desktop's Wayland
session owns tty1) and diagnosed by gdb-attaching the stuck process.

The service now owns a dedicated free VT (tty7) so acquisition is
immediate, with a tolerant ExecStartPre chvt to make the screencast
the on-screen console at boot. tty1 keeps the console login;
Ctrl+Alt+F1/Ctrl+Alt+F7 switch between them. The previous advice to
enable Console Autologin actively caused the hang and is removed
from the unit, RUNBOOK, and install script notes.

Validated locally: the same properties via systemd-run give a
running receiver with all ~15 threads alive that announces itself
and is found by --discover; verified unit passes systemd-analyze
verify; graceful stop withdraws mDNS.
This commit is contained in:
2026-09-07 13:45:47 +02:00
parent 414c2c39ee
commit 343c6b45e6
4 changed files with 33 additions and 12 deletions
+12 -3
View File
@@ -34,9 +34,18 @@ loopback; current phase is Phase 7.
- **systemd autostart**: `systemd/screencast-receiver.service` is a - **systemd autostart**: `systemd/screencast-receiver.service` is a
template (`__SC_RECEIVER_BIN__`/`__SC_RECEIVER_USER__`); the install template (`__SC_RECEIVER_BIN__`/`__SC_RECEIVER_USER__`); the install
script substitutes and enables it (opt out: `SC_RECEIVER_SERVICE=0`), script substitutes and enables it (opt out: `SC_RECEIVER_SERVICE=0`),
adding the run user to video/render/input for headless KMSDRM. The unit adding the run user to video/render/input for headless KMSDRM.
gets a controlling tty (StandardInput=tty, tty1) because SDL's KMSDRM - **systemd tty trap (hit on the real Pi, reproduced locally)**: with
backend needs one for VT handling. StandardInput=tty + TTYPath=tty1, the service NEVER started — systemd
blocks PRE-EXEC in acquire_terminal() waiting for a tty that the console
session/getty already owns. Symptoms: status shows the main PID as
"(screencast)" with Tasks:1 and ~40ms CPU, silent journal, no mDNS.
Diagnosed by gdb-attaching the stuck process (acquire_terminal backtrace)
after reproducing with a local transient unit. Fix: the service owns a
dedicated free VT (tty7) + tolerant `ExecStartPre=-/usr/bin/chvt 7`;
tty1 keeps the console. Also: `systemctl status` Tasks counts THREADS
(a healthy receiver shows ~15), and "Console Autologin" advice was
WRONG (it made the hang deterministic) — removed from all docs.
- **Fullscreen headless rendering**: under the KMSDRM video driver (no - **Fullscreen headless rendering**: under the KMSDRM video driver (no
window manager) the renderer goes fullscreen automatically; --fullscreen window manager) the renderer goes fullscreen automatically; --fullscreen
forces it on desktops. Aspect is preserved via forces it on desktops. Aspect is preserved via
+6 -2
View File
@@ -96,9 +96,13 @@ The receiver does not need X11, Wayland, or any window manager: SDL3's
KMSDRM backend renders straight to the kernel display pipeline. This is KMSDRM backend renders straight to the kernel display pipeline. This is
the recommended setup for small boards (Raspberry Pi OS **Lite**): the recommended setup for small boards (Raspberry Pi OS **Lite**):
1. `sudo raspi-config` → System Options → Boot → **Console Autologin**. 1. No desktop may be enabled — a running compositor holds the DRM master
2. No desktop may be enabled — a running compositor holds the DRM master
and the receiver cannot take it (the two modes are mutually exclusive). and the receiver cannot take it (the two modes are mutually exclusive).
No console autologin is needed either.
2. The service renders on its **own VT (tty7)** and switches to it at
boot: `Ctrl+Alt+F1` returns to the console login, `Ctrl+Alt+F7` back
to the screencast. (A service must not target tty1: acquiring a tty
that the console session or getty owns blocks the start forever.)
3. Disable console blanking: append `consoleblank=0` to 3. Disable console blanking: append `consoleblank=0` to
`/boot/firmware/cmdline.txt` and reboot. `/boot/firmware/cmdline.txt` and reboot.
+4 -2
View File
@@ -184,8 +184,10 @@ The receiver is installed and starts automatically at boot.
Notes for small boards (e.g. Pi Zero 2 W): Notes for small boards (e.g. Pi Zero 2 W):
- No window manager is required: the receiver renders directly to the - No window manager is required: the receiver renders directly to the
kernel display pipeline (SDL KMSDRM). Use Raspberry Pi OS Lite with kernel display pipeline (SDL KMSDRM). Use Raspberry Pi OS Lite with
Console Autologin (raspi-config) and no desktop enabled — a running NO desktop enabled — a running compositor would own the display and
compositor would own the display and the receiver could not start. the receiver could not start. No console autologin is needed either.
- The service owns tty7 and switches to it at boot (Ctrl+Alt+F1 returns
to the console login, Ctrl+Alt+F7 to the screencast).
- Disable console blanking so the picture never goes dark: add - Disable console blanking so the picture never goes dark: add
consoleblank=0 to /boot/firmware/cmdline.txt and reboot. consoleblank=0 to /boot/firmware/cmdline.txt and reboot.
- Decoding is software H.264; expect smooth playback for small streams - Decoding is software H.264; expect smooth playback for small streams
+11 -5
View File
@@ -26,12 +26,16 @@ StartLimitBurst=10
[Service] [Service]
Type=simple Type=simple
User=__SC_RECEIVER_USER__ User=__SC_RECEIVER_USER__
# SDL's KMSDRM backend wants a tty. The service gets its OWN virtual
# terminal: systemd must acquire the controlling terminal before exec, and
# acquiring one that another session owns (tty1: getty, console autologin,
# or a desktop) blocks the start FOREVER in a pre-exec placeholder process
# (systemctl status shows the main PID as "(screencast)"). tty7 is free on
# stock systems; tty1 keeps the console login.
ExecStartPre=-/usr/bin/chvt 7
ExecStart=__SC_RECEIVER_BIN__ --receive ExecStart=__SC_RECEIVER_BIN__ --receive
# SDL's KMSDRM backend (headless console) expects a controlling terminal
# for its VT handling; system services have none by default. Harmless in
# desktop mode, where the process never touches the tty.
StandardInput=tty StandardInput=tty
TTYPath=/dev/tty1 TTYPath=/dev/tty7
# SIGTERM (systemctl stop) shuts the receiver down gracefully, which # SIGTERM (systemctl stop) shuts the receiver down gracefully, which
# withdraws its mDNS announcement. Never SIGKILL it; the systemd default # withdraws its mDNS announcement. Never SIGKILL it; the systemd default
# KillMode escalates only after the graceful timeout. # KillMode escalates only after the graceful timeout.
@@ -54,6 +58,8 @@ WantedBy=multi-user.target
# to the kernel via KMSDRM): # to the kernel via KMSDRM):
# * Raspberry Pi OS Lite, no desktop enabled (a running compositor would # * Raspberry Pi OS Lite, no desktop enabled (a running compositor would
# hold the DRM master and the receiver could not take it) # hold the DRM master and the receiver could not take it)
# * raspi-config → System Options → Boot → Console Autologin # * no console autologin needed: the service owns tty7 and switches to it
# at boot; Ctrl+Alt+F1 returns to the console login, Ctrl+Alt+F7 to the
# screencast
# * disable console blanking: add consoleblank=0 to # * disable console blanking: add consoleblank=0 to
# /boot/firmware/cmdline.txt # /boot/firmware/cmdline.txt