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

@@ -70,10 +70,10 @@ $encoder_block
min_threads = 4
# Audio sink is intentionally left unset so Sunshine auto-detects the default
# PulseAudio/PipeWire sink and creates its virtual `sink-sunshine-stereo`.
# PulseAudio/PipeWire sink and creates its virtual 'sink-sunshine-stereo'.
# Hard-coding audio_sink = pulse here breaks capture: Sunshine treats it as a
# literal sink name, can't resolve its monitor source, and pa_simple_new()
# fails with "Invalid argument" no audio in the stream.
# fails with "Invalid argument" -> no audio in the stream.
# Keyboard / mouse / gamepad pass-through via /dev/uinput.
# (Requires user to be in the 'input' group; install.sh handles this.)

View File

@@ -108,11 +108,11 @@ _ubuntu_sunshine_deb_url() {
}
_install_sunshine_debian() {
# Universal runtime deps. libva-utils gives `vainfo`; jq is used by hooks.
# Universal runtime deps. vainfo is what Ubuntu calls libva-utils on Arch.
# pipewire-pulse is the Ubuntu 24.04+ default audio path; on older releases
# `pulseaudio-utils` works too — we don't force the codename split since
# sunshine just needs *a* PulseAudio API endpoint.
pkg_install jq vulkan-tools libva-utils curl ca-certificates
pkg_install jq vulkan-tools vainfo curl ca-certificates
if pkg_installed sunshine; then
ok "sunshine already installed (dpkg)"
@@ -198,15 +198,36 @@ install_gpu_encoder_packages() {
yay_install intel-media-driver vulkan-intel
;;
debian:nvidia)
# On Ubuntu, the proprietary driver is usually already installed via
# `ubuntu-drivers autoinstall` or the Server install path. Don't force a
# specific nvidia-* version — they vary by release / driver branch.
# Pull only the userspace VAAPI bridge if available; harmless if missing.
pkg_install libnvidia-encode-no-dkms 2>/dev/null \
|| pkg_install libnvidia-encode-575 2>/dev/null \
|| pkg_install libnvidia-encode-565 2>/dev/null \
|| pkg_install libnvidia-encode-560 2>/dev/null \
|| info "NVENC userspace library not found via a known package name — relying on the existing driver install."
# Ubuntu's nvidia-driver-NNN(-server) metapackage pulls in the matching
# libnvidia-encode-NNN(-server) as a dependency, so NVENC is normally
# already present. Only intervene if it's missing — and derive the right
# package name from the loaded driver's major version instead of
# guessing.
if dpkg-query -W -f='${Status}\n' 'libnvidia-encode-*' 2>/dev/null \
| grep -q '^install ok installed$'; then
ok "NVENC runtime library already installed via the driver metapackage"
else
local drv_major drv_full
drv_full="$(nvidia-smi --query-gpu=driver_version --format=csv,noheader,nounits 2>/dev/null | head -n1)"
drv_major="${drv_full%%.*}"
if [[ -z "$drv_major" ]]; then
warn "Could not detect NVIDIA driver version; NVENC may be missing."
else
# Try -server first (cloud GPUs usually run server branches), then the
# consumer branch. Stop at the first one apt knows about.
local picked=""
for cand in "libnvidia-encode-${drv_major}-server" "libnvidia-encode-${drv_major}"; do
if apt-cache show "$cand" >/dev/null 2>&1; then
picked="$cand"; break
fi
done
if [[ -n "$picked" ]]; then
pkg_install "$picked"
else
warn "No libnvidia-encode-${drv_major}* package in apt — install it manually if NVENC fails."
fi
fi
fi
;;
debian:amd)
pkg_install mesa-va-drivers mesa-vulkan-drivers vainfo

View File

@@ -8,13 +8,33 @@ UINPUT_RULE_PATH="/etc/udev/rules.d/60-uinput.rules"
UINPUT_RULE_CONTENT='KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess", OWNER="root", GROUP="input", MODE="0660"'
ensure_input_group() {
if id -nG "$USER" | tr ' ' '\n' | grep -qx input; then
ok "User '$USER' already in 'input' group"
_ensure_user_in_group input
# On Debian/Ubuntu, /dev/dri/renderD* nodes are mode 0660 owned by
# root:render, and /dev/dri/card* are root:video. Sway's wlroots renderer
# needs the render node (Vulkan/GLES FD); KMS capture needs the card node.
# Arch typically grants both via udev tag=uaccess for the logged-in seat,
# so we only add explicit memberships on Debian.
if [[ "${DISTRO:-}" == "debian" ]]; then
_ensure_user_in_group render
_ensure_user_in_group video
fi
}
# Internal: add $USER to a group if it exists and they're not already in it.
_ensure_user_in_group() {
local g="$1"
if ! getent group "$g" >/dev/null 2>&1; then
info "Group '$g' does not exist on this system — skipping."
return 0
fi
info "Adding '$USER' to 'input' group"
as_root usermod -aG input "$USER"
warn "You must log out and back in (or run 'newgrp input') for this to take effect."
if id -nG "$USER" | tr ' ' '\n' | grep -qx "$g"; then
ok "User '$USER' already in '$g' group"
return 0
fi
info "Adding '$USER' to '$g' group"
as_root usermod -aG "$g" "$USER"
warn "Group '$g' change takes effect on next login (or 'newgrp $g'). For systemd-user"
warn "services, you must fully log out and back in so the user manager restarts."
}
ensure_uinput_udev_rule() {

View File

@@ -4,23 +4,46 @@
# Sway-headless capture path, also installs + enables sway-headless.service
# and wires sunshine.service to depend on it.
# Resolves the actual unit file to operate on. Exports:
# SUNSHINE_ENABLE_NAME - the name to pass to `systemctl --user enable`. May
# be sunshine.service or app-dev.lizardbyte....service
# depending on how the package shipped the unit.
# SUNSHINE_SERVICE - the short name we use everywhere else (start, restart,
# status, drop-ins). Always sunshine.service — systemd
# resolves it via Alias= or our installed unit.
ensure_sunshine_unit_present() {
# Case 1: a sunshine.service unit already exists in any path systemd-user
# scans. The LizardByte .deb on Ubuntu drops it at /lib/systemd/user/.
# sunshine-bin on Arch drops it at /usr/lib/systemd/user/.
SUNSHINE_SERVICE="sunshine.service"
# Clean up a broken symlink from older runs that pointed sunshine.service
# at the FQDN unit. The upstream .deb already declares Alias=sunshine.service
# in [Install], so the symlink we used to create conflicts with systemd's
# enable path ("Refusing to operate on alias name").
local user_unit="$HOME/.config/systemd/user/sunshine.service"
if [[ -L "$user_unit" ]] && readlink "$user_unit" 2>/dev/null \
| grep -q 'app-dev.lizardbyte.app.Sunshine.service$'; then
info "Removing stale alias symlink at $user_unit (upstream unit declares Alias=)"
rm -f "$user_unit"
fi
# Case 1: a real sunshine.service unit ships in a system path (sunshine-bin
# on Arch drops it at /usr/lib/systemd/user/).
for p in \
/lib/systemd/user/sunshine.service \
/usr/lib/systemd/user/sunshine.service \
/etc/systemd/user/sunshine.service \
"$HOME/.config/systemd/user/sunshine.service" \
"$HOME/.local/share/systemd/user/sunshine.service"
do
[[ -e "$p" ]] && return 0
if [[ -f "$p" && ! -L "$p" ]]; then
SUNSHINE_ENABLE_NAME="sunshine.service"
export SUNSHINE_ENABLE_NAME SUNSHINE_SERVICE
return 0
fi
done
# Case 2: the AUR source 'sunshine' package ships the unit under a
# Flatpak-style reverse-DNS name. Symlink it as sunshine.service so the rest
# of our tooling can keep using the short name.
# Case 2: the LizardByte .deb (Ubuntu) and the AUR source package ship the
# unit under a reverse-DNS FQDN with Alias=sunshine.service in [Install].
# Do NOT symlink — `systemctl --user enable` on the FQDN name creates the
# alias symlink itself in ~/.config/systemd/user/.
local fqdn_unit=""
for p in \
/usr/lib/systemd/user/app-dev.lizardbyte.app.Sunshine.service \
@@ -30,10 +53,9 @@ ensure_sunshine_unit_present() {
[[ -f "$p" ]] && { fqdn_unit="$p"; break; }
done
if [[ -n "$fqdn_unit" ]]; then
info "Found packaged unit at $fqdn_unit"
info "Aliasing it as sunshine.service in $HOME/.config/systemd/user/"
mkdir -p "$HOME/.config/systemd/user"
ln -sf "$fqdn_unit" "$HOME/.config/systemd/user/sunshine.service"
info "Found packaged unit at $fqdn_unit (enables via alias)"
SUNSHINE_ENABLE_NAME="app-dev.lizardbyte.app.Sunshine.service"
export SUNSHINE_ENABLE_NAME SUNSHINE_SERVICE
return 0
fi
@@ -47,6 +69,8 @@ ensure_sunshine_unit_present() {
mkdir -p "$HOME/.config/systemd/user"
install -m 0644 "$fallback" "$HOME/.config/systemd/user/sunshine.service"
ok "Installed $HOME/.config/systemd/user/sunshine.service"
SUNSHINE_ENABLE_NAME="sunshine.service"
export SUNSHINE_ENABLE_NAME SUNSHINE_SERVICE
}
# Install and enable the headless sway compositor unit + config (Debian/Ubuntu
@@ -114,8 +138,8 @@ enable_sunshine_service() {
install_headless_prestart_dropin
fi
if ! systemctl --user list-unit-files sunshine.service >/dev/null 2>&1; then
err "sunshine.service still not found after fallback. Inspect: find /usr /lib ~/.config -name sunshine.service"
if ! systemctl --user list-unit-files "$SUNSHINE_ENABLE_NAME" >/dev/null 2>&1; then
err "$SUNSHINE_ENABLE_NAME not found after fallback. Inspect: find /usr /lib ~/.config -name '*[Ss]unshine*'"
return 1
fi
@@ -126,8 +150,8 @@ enable_sunshine_service() {
ok "User lingering already enabled"
fi
info "Enabling sunshine.service (user)"
systemctl --user enable sunshine.service >/dev/null
info "Enabling ${SUNSHINE_ENABLE_NAME} (user)"
systemctl --user enable "$SUNSHINE_ENABLE_NAME" >/dev/null
# Clear any prior start-limit state from a failed run so this attempt isn't
# immediately rejected with "Start request repeated too quickly."