Add runtime status checker + headless/X11 docs; distro-support refinements
- status.sh: runtime health check (service state, boot wiring, display backend auto-detect, encoder, ports, web UI, /dev/uinput, pairing) ending in a g2g verdict or concrete TODO list
- docs: TROUBLESHOOTING §12 (headless graphical-session.target boot trap) + §13 (X11/NVENC path & stale wlr drop-in env conflict); ARCHITECTURE capture-backend comparison; FOLLOWUPS P3 (installer X11/Ubuntu support); README diagnostics pointer
- lib/{config,packages,permissions,service}.sh, files/sway-headless.service: in-progress Debian/Ubuntu + headless support refinements
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -364,6 +364,120 @@ IP.2 = 127.0.0.1
|
||||
And the idempotency check in `fetch_and_install_certs` requires those SANs
|
||||
— existing hosts re-mint on next install.
|
||||
|
||||
### 12. Headless host: service never auto-starts after boot (`graphical-session.target` never activates)
|
||||
|
||||
**Symptom**
|
||||
|
||||
Sunshine works right after install, and works again if you `systemctl --user
|
||||
start` it by hand, but never comes up on its own after a reboot. `systemctl
|
||||
--user status` shows the unit `enabled` yet `inactive (dead)` — *not* failed,
|
||||
just never started. Ports 47984/47989/47990 aren't listening and nothing is
|
||||
in the journal because the unit was never invoked.
|
||||
|
||||
**Cause**
|
||||
|
||||
The packaged unit (`sunshine.service`, or `app-dev.lizardbyte.app.Sunshine.service`)
|
||||
is wired `WantedBy=graphical-session.target` and `After=graphical-session.target`.
|
||||
On a desktop, logging in activates `graphical-session.target`, which pulls
|
||||
Sunshine up. A **headless host has no graphical login session**, so
|
||||
`graphical-session.target` never activates — even with `loginctl enable-linger`
|
||||
on and a headless display server (Xorg/sway) already running. The installer's
|
||||
one-time `systemctl --user start` is why it appears to work at install time;
|
||||
the wiring just never fires again at boot.
|
||||
|
||||
This bites any headless deployment (KVM box, server with a dummy/virtual
|
||||
display, the X11/NVENC path in §13). It does *not* bite a laptop/desktop that
|
||||
actually logs into Hyprland.
|
||||
|
||||
**Fix**
|
||||
|
||||
Add a drop-in that wires the unit into a target the lingering user-manager
|
||||
actually reaches (`default.target`) and orders it after whatever provides the
|
||||
display:
|
||||
|
||||
```bash
|
||||
# adjust the unit name to whichever one is installed (see issue #3)
|
||||
UNIT=app-dev.lizardbyte.app.Sunshine.service
|
||||
mkdir -p ~/.config/systemd/user/$UNIT.d
|
||||
cat > ~/.config/systemd/user/$UNIT.d/headless-boot.conf <<'EOF'
|
||||
[Unit]
|
||||
# Order after the headless display unit (xorg-headless.service for the X11
|
||||
# path, sway-headless.service for the wlr path).
|
||||
Requires=xorg-headless.service
|
||||
After=xorg-headless.service
|
||||
|
||||
[Install]
|
||||
# graphical-session.target never activates on a headless host; default.target
|
||||
# is reached by the lingering user manager, so this is what makes boot work.
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user reenable $UNIT # recreates default.target.wants symlink
|
||||
systemctl --user restart $UNIT
|
||||
```
|
||||
|
||||
Confirm the load-bearing symlink exists:
|
||||
`ls ~/.config/systemd/user/default.target.wants/ | grep -i sunshine`. The real
|
||||
proof is a reboot — `enabled` alone isn't enough on a headless box.
|
||||
|
||||
> Note: dependency keys (`Requires=`, `After=`) belong in the `[Unit]` section.
|
||||
> A drop-in that puts them in `[Service]` is silently ignored — systemd logs
|
||||
> `Unknown key name 'Requires' in section 'Service', ignoring` in
|
||||
> `systemctl status`, and the ordering never takes effect.
|
||||
|
||||
### 13. X11/NVENC headless path (Ubuntu / non-Hyprland hosts)
|
||||
|
||||
**Symptom / context**
|
||||
|
||||
This repo's headless mode assumes Hyprland + `capture = wlr`. On a host that
|
||||
isn't running Hyprland (e.g. an Ubuntu box, or one where you want NVIDIA NVENC
|
||||
with a guaranteed GL context), the wlr path has nothing to capture and every
|
||||
encoder probe fails at startup (same red banner as issue #4).
|
||||
|
||||
**The X11/NVENC alternative (as deployed on at least one host)**
|
||||
|
||||
Instead of a wlroots compositor, run a **headless Xorg on `:0`** and have
|
||||
Sunshine grab the X root window. This is the systemd-service equivalent of the
|
||||
upstream "Remote SSH Headless Setup" guide's TwinView trick — NVIDIA Xorg is
|
||||
told a monitor is connected so it produces a hardware-accelerated virtual
|
||||
display.
|
||||
|
||||
- A `xorg-headless.service` (user unit) runs
|
||||
`Xorg :0 -config xorg-headless.conf …` with an NVIDIA `ConnectedMonitor` +
|
||||
`ModeValidation` block (see the upstream guide). It's the headless display
|
||||
the Sunshine unit orders against in issue #12.
|
||||
- `~/.config/sunshine/sunshine.conf` uses `capture = x11`, `output_name = 0`,
|
||||
`encoder = nvenc`. (Hand-edited — delete the `# managed-by:` marker line so
|
||||
the Arch installer leaves it alone.)
|
||||
- A service drop-in pins the X11 environment so Sunshine doesn't auto-probe
|
||||
Wayland:
|
||||
```ini
|
||||
[Service]
|
||||
Environment=
|
||||
Environment=DISPLAY=:0
|
||||
Environment=XDG_SESSION_TYPE=x11
|
||||
UnsetEnvironment=WAYLAND_DISPLAY XDG_SESSION_TYPE
|
||||
```
|
||||
- Input injection works the normal way — user in the `input` group +
|
||||
`60-sunshine.rules` udev rule on `/dev/uinput`. The upstream guide's
|
||||
`chown`-via-passwordless-sudo `/dev/uinput` workaround is **not needed**;
|
||||
it only applies when you SSH in without group membership.
|
||||
|
||||
**Gotcha: leftover wlr drop-ins poison the X11 env.** If a host was first set
|
||||
up for the wlr path and later migrated to X11, a stale
|
||||
`sunshine.service.d/sway-headless.conf` drop-in can linger. Because
|
||||
`sunshine.service` is an *alias* of `app-dev.lizardbyte.app.Sunshine.service`,
|
||||
systemd merges drop-ins from **both** name directories, so that leftover keeps
|
||||
injecting `XDG_SESSION_TYPE=wayland`, `WAYLAND_DISPLAY=wayland-1`, and a hard
|
||||
`Requires=sway-headless.service` (a dead unit). It "works" only because
|
||||
`capture = x11` is explicit in the conf, but at boot it tries to pull in the
|
||||
dead sway unit. Delete the stale drop-in and verify the effective env:
|
||||
|
||||
```bash
|
||||
systemctl --user show $UNIT -p Environment -p Requires -p After
|
||||
# should show DISPLAY=:0 + XDG_SESSION_TYPE=x11, xorg-headless.service, no wayland/sway
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom keybinding to escape Moonlight (Mac)
|
||||
|
||||
Reference in New Issue
Block a user