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:
@@ -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 §12–13 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
|
||||
|
||||
@@ -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 §12–13).
|
||||
|
||||
**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
|
||||
|
||||
@@ -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