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:
2026-06-02 22:57:03 +00:00
parent ee1379d5be
commit ab23107300
10 changed files with 623 additions and 38 deletions

View File

@@ -162,6 +162,35 @@ Moonlight tears down the stream
---
## Headless capture backends: wlr vs X11/NVENC
The installer's headless mode assumes **wlr** (`capture = wlr`) on Hyprland —
that's the runtime flow diagrammed above. There is a second headless backend,
used on hosts that don't run Hyprland (e.g. Ubuntu) or that want a guaranteed
NVIDIA GL context for NVENC: **X11 capture of a headless Xorg**.
| | wlr path (installer default) | X11/NVENC path (manual) |
|---|---|---|
| Compositor | Hyprland (or sway-headless on a server) | headless Xorg on `:0` |
| `sunshine.conf` | `capture = wlr`, `output_name = HEADLESS-N` | `capture = x11`, `output_name = 0` |
| Display unit | the Hyprland/sway session | `xorg-headless.service` (user unit) |
| Per-client resize | yes — `global_prep_cmd` do/undo hooks | no — Xorg has a fixed `MetaModes` resolution |
| How the display is faked | wlroots headless output | NVIDIA `ConnectedMonitor` + `ModeValidation` (TwinView) |
| Service env drop-in | inherits Wayland env | pins `DISPLAY=:0`, `XDG_SESSION_TYPE=x11` |
The X11/NVENC path is the systemd-service form of the upstream "Remote SSH
Headless Setup" guide. It trades per-client resolution adaptation (the wlr
path's main feature) for a simpler, compositor-free capture that the NVIDIA
driver accelerates directly. Input injection is identical for both — `input`
group + `60-sunshine.rules` on `/dev/uinput`.
Both backends share the same boot caveat: on a headless host the Sunshine unit
must be wired into `default.target`, not `graphical-session.target`, or it
never auto-starts. See TROUBLESHOOTING.md §1213 for the drop-ins and the
alias-merge gotcha that lets a stale wlr drop-in poison the X11 environment.
---
## Cert pipeline
A separate one-time bootstrap creates the CA in 1Password. Every host then
@@ -265,6 +294,7 @@ verify.
omarchy-moonlight/
├── install.sh Orchestrator
├── uninstall.sh Reverse install (preserves user data by default)
├── status.sh Runtime health check (what's running + g2g verdict)
├── README.md User-facing install + usage
├── scripts/
│ └── cert-bootstrap.sh One-time CA generation + 1P upload

View File

@@ -220,6 +220,38 @@ that prefix. Substantial work; not justified without a real second user.
---
## P3 — Installer support for the X11/NVENC (non-Hyprland) headless path
**Symptom**: `install.sh` only knows the Arch + Hyprland + wlr world. At least
one real deployment is an **Ubuntu host running the X11/NVENC path** (headless
Xorg on `:0`, `capture = x11`, NVIDIA TwinView virtual display) — set up
entirely by hand. None of it is reproducible from the repo: not the
`xorg-headless.service` unit, not the X11 `sunshine.conf`, not the
`DISPLAY=:0` service drop-in, not the `default.target` boot wiring (see
TROUBLESHOOTING.md §1213).
**Current workaround**: configure those hosts manually using the recipes now
documented in ARCHITECTURE.md ("Headless capture backends") and
TROUBLESHOOTING.md §13.
**Fix sketch**:
- A `--backend x11|wlr` flag (or auto-detect: Hyprland reachable → wlr,
NVIDIA + no Wayland compositor → x11).
- `lib/config.sh`: emit the X11 conf variant when backend is x11.
- Ship an `xorg-headless.service` + `xorg-headless.conf` template (the
`ConnectedMonitor`/`ModeValidation` block is GPU-output-specific — needs
`xrandr` detection or a prompt).
- `lib/service.sh`: install the `default.target` boot drop-in for headless
hosts regardless of backend, and the `DISPLAY=:0` env drop-in for x11.
- Debian/Ubuntu package install path (`apt` + the `.deb`), since `yay`/AUR
don't exist there. This is a larger lift than the flag itself.
**Complexity**: medium-high. The capture-backend split is moderate; full
Debian packaging support is the bulk of the work.
---
## Not on the list (intentionally)
- **TLS for the stream itself.** Sunshine and Moonlight handle this with

View File

@@ -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)