Files
Omarchy-Stream/docs/FOLLOWUPS.md
Levi Woodard 6fc34a2bd2 Add reproducible headless desktop for the X11/NVENC capture path
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>
2026-06-02 23:43:00 +00:00

274 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 §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.
**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.