A Moonlight client connecting to the x11-backend host got a black screen even though pairing, NVENC, and input injection all worked: the headless Xorg on :0 had no window manager rendering on it, so capture=x11 grabbed an empty black root window. (The wlr/kms backends don't hit this — their capture source renders for itself.) This was a hand-built path with nothing in the repo to reproduce the desktop piece. Now: - files/headless-desktop.service: Openbox session on :0, bound to xorg-headless.service, enabled via default.target for lingering boots, with a best-effort xsetroot so the desktop is visibly non-black. - lib/headless.sh: capture_backend_is_x11 + install_headless_desktop (idempotent; pulls openbox/xsetroot via the distro dispatch). - install.sh: installs the desktop unit when capture=x11 is detected. - status.sh: x11 branch now FAILs if no window manager is on :0 instead of only checking the X server answers — the gap that hid this failure. - docs: TROUBLESHOOTING §13 black-screen lesson; FOLLOWUPS P3 updated. Part of the P3 x11-backend work; --backend flag, config.sh x11 variant, and xorg-headless templates remain outstanding. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
274 lines
11 KiB
Markdown
274 lines
11 KiB
Markdown
# Outstanding follow-ups
|
||
|
||
Tracked work that isn't on `main` yet. Each item lists the symptom, current
|
||
workaround, the fix sketch, and rough complexity so it can be picked up
|
||
in any order.
|
||
|
||
---
|
||
|
||
## P1 — Inhibit screensaver during streaming
|
||
|
||
**Symptom**: Omarchy's hypridle fires `omarchy-screensaver` after the host's
|
||
idle timeout, even when a Moonlight client is actively streaming. Sunshine
|
||
input arrives via `/dev/uinput` virtual devices, which hypridle's input
|
||
watcher doesn't always treat as activity. From the client's perspective the
|
||
stream "freezes" on the screensaver screen until the user wiggles input
|
||
enough to break hypridle's threshold.
|
||
|
||
**Current workaround**: wiggle the mouse / tap a key in the stream window.
|
||
|
||
**Fix sketch**:
|
||
|
||
1. In `bin/sunshine-stream-do.sh`, on stream start, take a `systemd-inhibit`
|
||
lock or `systemctl --user stop hypridle.service`:
|
||
|
||
```bash
|
||
# at the top, after env recovery
|
||
if command -v systemctl >/dev/null && systemctl --user is-active --quiet hypridle.service; then
|
||
touch "$STATE_DIR/hypridle-was-active"
|
||
systemctl --user stop hypridle.service
|
||
fi
|
||
```
|
||
|
||
2. In `bin/sunshine-stream-undo.sh`, on disconnect, restore:
|
||
|
||
```bash
|
||
if [[ -f "$STATE_DIR/hypridle-was-active" ]]; then
|
||
systemctl --user start hypridle.service
|
||
rm -f "$STATE_DIR/hypridle-was-active"
|
||
fi
|
||
```
|
||
|
||
**Complexity**: low. ~10 lines split across the two hook scripts. Self-healing
|
||
(if a stream crashes and undo doesn't run, next connect will be a no-op on
|
||
the inhibit and undo will idempotently leave the marker file).
|
||
|
||
**Alternative**: pass `--inhibit` to systemd-inhibit and run sunshine wrapped
|
||
in it. Cleaner separation but requires changing the service unit, which
|
||
fights the AUR package every upgrade.
|
||
|
||
---
|
||
|
||
## P1 — Auto-switch to the busiest workspace on connect
|
||
|
||
**Symptom**: when a client connects, the prep-cmd migrates the
|
||
*active* workspace to `HEADLESS-N`. If active was empty (e.g. workspace 4)
|
||
and the user's apps are on workspace 1, the stream shows an empty desktop
|
||
until the user presses `Super+1`.
|
||
|
||
**Current workaround**: press `Cmd+1` (or whatever maps to the apps workspace)
|
||
in the stream.
|
||
|
||
**Fix sketch**: change the workspace-detection in `bin/sunshine-stream-do.sh`
|
||
from `activeworkspace` to "workspace with the most windows":
|
||
|
||
```bash
|
||
# replace this:
|
||
PREV_WS="$(hyprctl activeworkspace -j 2>/dev/null | jq -r '.id // 1' || echo 1)"
|
||
|
||
# with this:
|
||
PREV_WS="$(hyprctl workspaces -j 2>/dev/null \
|
||
| jq -r 'sort_by(-.windows) | .[0].id // 1')"
|
||
```
|
||
|
||
**Complexity**: trivial. 2-line patch.
|
||
|
||
**Edge case**: if every workspace is empty (no apps running), `sort_by` is
|
||
still deterministic — picks whichever workspace happens to be first. That's
|
||
fine; the user just lands on an empty workspace either way.
|
||
|
||
---
|
||
|
||
## P2 — 1Password streams as a black rectangle
|
||
|
||
**Symptom**: the 1Password Linux app deliberately masks its window from
|
||
screen-capture surfaces. Through Moonlight, the 1Password window outline is
|
||
visible but the interior is solid black. Untested whether this is recoverable.
|
||
|
||
**Current workaround**: use the 1Password browser extension during the stream
|
||
(it lives inside Chromium and captures normally), or unlock 1Password on the
|
||
host before walking away (the SSH-agent / browser extension / `op` CLI all
|
||
still work even though the desktop window is unviewable).
|
||
|
||
**Things to test, in order of "least invasive"**:
|
||
|
||
1. Launch 1Password with `--disable-gpu --no-sandbox`. Software rendering
|
||
path may bypass the anti-capture flag.
|
||
|
||
```bash
|
||
pkill -f 1password
|
||
1password --disable-gpu --no-sandbox &
|
||
```
|
||
|
||
2. Force XWayland: `1password --ozone-platform=x11`. XWayland surfaces are
|
||
captured through a different code path than native-Wayland Electron
|
||
surfaces.
|
||
|
||
3. Patch the launcher (`~/.local/share/applications/1password.desktop`) with
|
||
whichever flag works.
|
||
|
||
4. If neither works: this is by-design upstream behavior and the realistic
|
||
answer is the browser-extension workaround. Document in
|
||
`client/README.md` under "Limitations."
|
||
|
||
**Complexity**: low (test command-line flags) to medium (patching .desktop
|
||
file properly).
|
||
|
||
---
|
||
|
||
## P2 — Resolve `<host>.lan` from clients
|
||
|
||
**Symptom**: `getent hosts <host>.lan` returns nothing — there's no DNS or
|
||
mDNS entry for the friendly LAN name. Clients work via raw IP
|
||
(`192.168.x.y`), and the host cert SAN covers both the .lan name and the IP,
|
||
so cert trust works either way. The friendly name just isn't reachable.
|
||
|
||
**Current workaround**: use the LAN IP, or add `<IP> <host> <host>.lan` to
|
||
the Mac's `/etc/hosts`.
|
||
|
||
**Fix sketch**: this is a network-layer change, not a repo change.
|
||
|
||
- **Unifi**: Settings → Networks → (your network) → DHCP → static reservation
|
||
for the host machine + Settings → DNS or "Local domain name" → set to
|
||
`lan`. Then the controller publishes `A` records for every reserved
|
||
device.
|
||
- **pi-hole / dnsmasq**: add an `address=/<host>.lan/<IP>` entry.
|
||
- **avahi-daemon**: would publish via mDNS, but Macs and some Linux clients
|
||
don't always pick it up. Less reliable than proper DNS.
|
||
|
||
**Complexity**: out-of-repo (depends on the user's network stack). Document
|
||
the choice once made.
|
||
|
||
---
|
||
|
||
## P2 — 1Password SSH-agent timeout breaks git signing
|
||
|
||
**Symptom**: long sessions interspersed with idle time cause 1Password's
|
||
SSH agent to go to sleep. Subsequent `git commit` fails with:
|
||
|
||
```
|
||
error: 1Password: failed to fill whole buffer
|
||
fatal: failed to write commit object
|
||
```
|
||
|
||
**Current workaround**: touch the 1Password desktop app or run
|
||
`eval $(op signin)` to revive the agent.
|
||
|
||
**Fix sketch** (none are perfect):
|
||
|
||
1. **Lengthen the agent timeout** — 1Password app → Settings → Developer →
|
||
SSH agent → bump the lock-after timeout. Cap is configurable but capped.
|
||
2. **Watchdog script** — periodically `ssh-add -l` and warn the user if it
|
||
fails. Adds noise.
|
||
3. **Per-repo `signingkey` config** to a different key not held by 1P —
|
||
defeats the purpose.
|
||
|
||
**Complexity**: low. Setting (1) is the realistic choice; (2) is overkill.
|
||
|
||
---
|
||
|
||
## P3 — Cert renewal automation
|
||
|
||
**Symptom**: host certs are valid 365 days. The installer re-mints when
|
||
`<30 days remaining` on the next `install.sh` run, but if you don't re-run
|
||
the installer in 11 months the cert silently expires and the next run
|
||
(somewhere between day 365 and day 395) re-mints — leaving a gap.
|
||
|
||
**Current workaround**: re-run `install.sh` periodically.
|
||
|
||
**Fix sketch**: ship a systemd `--user` timer that runs
|
||
`install.sh --doctor && install.sh --force-certs` weekly. Tied to the same
|
||
op-signin requirement, so it won't run unattended if 1P isn't unlocked —
|
||
that's actually fine (failure is loud, not silent).
|
||
|
||
**Complexity**: low-medium. New `.timer` + `.service` units; tricky bit is
|
||
making the timer fire only when both Hyprland and `op` are reachable.
|
||
|
||
---
|
||
|
||
## P3 — Stale config keys produce harmless warnings
|
||
|
||
**Symptom**: Sunshine logs `Unrecognized configurable option [nvenc_rc]` (and
|
||
`nvenc_tune`, `nvenc_coder`) on every startup. Recent Sunshine versions
|
||
renamed these keys — the encoder still works because the rename is
|
||
backward-compatible for the most important ones (`nvenc_preset`), but the
|
||
specific tuning keys we set don't take effect.
|
||
|
||
**Current workaround**: ignore the warnings; encoder defaults still work.
|
||
|
||
**Fix sketch**: update `lib/config.sh` to emit the new key names. Need to
|
||
verify the current spelling against the Sunshine version installed (likely
|
||
`nv_preset`/`nv_tune`/`nv_rc`/`nv_coder` without the `enc`, but the user
|
||
should `sunshine --help` to confirm). Same situation likely on AMD's
|
||
`amd_*` keys — verify when first Framework install actually exercises that
|
||
code path.
|
||
|
||
**Complexity**: trivial once the right key names are confirmed.
|
||
|
||
---
|
||
|
||
## P3 — Single-user assumption in everything
|
||
|
||
The installer + scripts assume a single human-user / single-host model. Not
|
||
a problem now, but if someone wants to share a JARVIS install across
|
||
multiple Sunshine instances (one per logged-in user), the headless prestart
|
||
script and `output_name` would collide.
|
||
|
||
**Fix sketch**: introduce a username-scoped `output_name` (e.g.
|
||
`HEADLESS-${USER}-1`) and scope the prestart's dedupe to outputs matching
|
||
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.
|
||
|
||
**Done so far**: the *desktop* piece of the x11 backend is now reproducible.
|
||
`install.sh` detects `capture = x11` (via `capture_backend_is_x11`) and installs
|
||
+ enables `files/headless-desktop.service` (Openbox on `:0`) so the headless
|
||
Xorg has a window manager to render — without it, capture is a black screen.
|
||
`status.sh` gained a matching check (FAIL if no WM on `:0`). Still outstanding:
|
||
the `--backend x11|wlr` flag/auto-detect, the `config.sh` x11 conf variant, and
|
||
shipping the `xorg-headless.service` + `xorg-headless.conf` templates.
|
||
|
||
**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
|
||
their own pinned-cert protocol; we don't touch it.
|
||
- **AV1 encoder.** RTX 3070 Ti is HEVC-capable but not AV1; users with
|
||
Ada/40-series can opt in via the web UI without code changes.
|
||
- **Remote-access layer** (Tailscale / WireGuard / Cloudflare). LAN-only
|
||
for now; the design assumes RFC1918 only and would need real DNS + DNS-01
|
||
cert issuance for proper remote.
|
||
- **Windows host support.** Sunshine works on Windows but the installer is
|
||
Arch-only by design.
|