diff --git a/.gitea/workflows/installer-smoke.yml b/.gitea/workflows/installer-smoke.yml index 354cf212..451da8b4 100644 --- a/.gitea/workflows/installer-smoke.yml +++ b/.gitea/workflows/installer-smoke.yml @@ -18,14 +18,25 @@ on: paths: - scripts/install.sh - data/platforms.json + - scripts/ci/check-install-defaults.sh - .gitea/workflows/installer-smoke.yml pull_request: paths: - scripts/install.sh - data/platforms.json + - scripts/ci/check-install-defaults.sh - .gitea/workflows/installer-smoke.yml jobs: + defaults: + name: derived defaults + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Assert per-family installer defaults + run: sh scripts/ci/check-install-defaults.sh + smoke: name: smoke (${{ matrix.family }}) runs-on: ubuntu-24.04 diff --git a/docs-site/content/docs/bazzite.mdx b/docs-site/content/docs/bazzite.mdx index 74652c20..ffa36b0b 100644 --- a/docs-site/content/docs/bazzite.mdx +++ b/docs-site/content/docs/bazzite.mdx @@ -9,8 +9,10 @@ Steam **Gaming Mode** and the **KDE desktop**, and follows the box when you swit Nothing in the config picks a mode. **One command instead?** The [guided installer](/docs/install#guided-install-preview) (preview) does -everything on this page — it detects the distro, installs the host, joins the groups, opens the -firewall and starts it: +everything on this page — it detects the distro, installs the host, joins the `input` and +`punktfunk` groups, enables linger, opens the firewall and starts it. Those last two match the +hand steps below (Bazzite hosts are usually headless, and the virtual Steam Deck pad needs the +group): diff --git a/docs-site/content/docs/gamescope.md b/docs-site/content/docs/gamescope.md index a624f48a..5cd5dace 100644 --- a/docs-site/content/docs/gamescope.md +++ b/docs-site/content/docs/gamescope.md @@ -54,9 +54,13 @@ Versions before this one stopped the display manager for the stream's duration root helper, the `punktfunk` group, and lingering, and left the box with nothing able to start a desktop session, so Steam's own "Switch to Desktop" hung until a reboot. -> **Join the `punktfunk` group on any box you stream Game Mode from.** The takeover itself no -> longer needs it — the group now gates the usbip nodes the virtual Steam Deck pad attaches -> through, so without it the pad arrives as an ordinary Xbox 360 controller: +> **Join the `punktfunk` group on any box you stream Game Mode from.** The guided installer +> defaults to yes on Bazzite and Nobara; on any other distro it asks, and `--punktfunk-group` +> answers it up front. (Silverblue, Kinoite, Bluefin and Aurora are desktop images, so they get +> the ordinary "ask, default no" — being Fedora Atomic is not on its own a Game Mode box.) The +> takeover +> itself no longer needs the group — it now gates the usbip nodes the virtual Steam Deck pad +> attaches through, so without it the pad arrives as an ordinary Xbox 360 controller: > > ```sh > sudo usermod -aG punktfunk "$USER" # then log out and back in diff --git a/docs-site/content/docs/install.mdx b/docs-site/content/docs/install.mdx index 763c79eb..3040f598 100644 --- a/docs-site/content/docs/install.mdx +++ b/docs-site/content/docs/install.mdx @@ -35,15 +35,17 @@ Prefer to read what runs first (it's plain `sh`, ~450 lines): -It asks before anything optional (Moonlight compat, the shared clipboard, the `punktfunk` group, -starting at boot) and every answer has a default, so `sh install.sh --yes` — or piping it with no -terminal — runs unattended; `--channel stable|canary`, `--mgmt-port`, `--no-start`, `--uninstall` and the environment -twins (`PUNKTFUNK_INSTALL_YES`, `PUNKTFUNK_INSTALL_CHANNEL`, `PUNKTFUNK_INSTALL_GAMESTREAM`, -`PUNKTFUNK_INSTALL_CLIPBOARD`, `PUNKTFUNK_INSTALL_PUNKTFUNK_GROUP`, `PUNKTFUNK_INSTALL_LINGER`, -`PUNKTFUNK_INSTALL_MGMT_PORT`) are listed by `--help`. It covers Ubuntu/Debian, Fedora, Arch-family -and Bazzite/Fedora Atomic; NixOS, SteamOS and Windows it points at their pages. **Preview:** the -per-system pages above remain the documented path, and the script runs exactly the install commands -they show (CI fails if the two drift apart). Re-running is safe. +It asks about intent, not internals (full controller, third-party Moonlight clients, clipboard, +start at boot) and every answer has a default, so `sh install.sh --yes` — or piping it with no +terminal — runs unattended. The default follows the box: Bazzite and Nobara join the `punktfunk` +group and linger; an active Sunshine/Apollo host opts into Moonlight compat; a seatless SSH +session lingers. Clipboard stays off. `--channel stable|canary`, `--mgmt-port`, `--no-start`, +`--uninstall` and the environment twins (`PUNKTFUNK_INSTALL_YES`, `PUNKTFUNK_INSTALL_CHANNEL`, +`PUNKTFUNK_INSTALL_GAMESTREAM`, `PUNKTFUNK_INSTALL_CLIPBOARD`, `PUNKTFUNK_INSTALL_PUNKTFUNK_GROUP`, +`PUNKTFUNK_INSTALL_LINGER`, `PUNKTFUNK_INSTALL_MGMT_PORT`) are listed by `--help`. It covers +Ubuntu/Debian, Fedora, Arch-family and Bazzite/Fedora Atomic; NixOS, SteamOS and Windows it points +at their pages. **Preview:** the per-system pages above remain the documented path, and the script +runs exactly the install commands they show (CI fails if the two drift apart). Re-running is safe. `--channel` also **switches** a host that is already installed — see [Release Channels](/docs/channels#switch-an-installed-box-between-channels). Without the flag the diff --git a/scripts/ci/check-docs-drift.sh b/scripts/ci/check-docs-drift.sh index 0852a373..0c305d6b 100755 --- a/scripts/ci/check-docs-drift.sh +++ b/scripts/ci/check-docs-drift.sh @@ -31,6 +31,7 @@ # 7. The installer under --dry-run against faked os-release files detects every family it claims # to (and --uninstall prints each family's removal) — the committed half of the manual # 16-file matrix PR #345 was verified with. Needs curl on PATH (the script's own prerequisite). +# Per-family *defaults* (group / linger / GameStream) live in check-install-defaults.sh. # 8. scripts/web-init.sh — the web console's host-readiness gate — actually waits for the host's # first-run files and actually stops waiting when they land, against a faked config dir. Not # docs drift, but the same shape: a packaging script no build exercises, whose failure mode @@ -165,6 +166,7 @@ installer_case fedora-rm 'ID=fedora\nVERSION_ID=44\n' 'yum.rep installer_case arch-rm 'ID=arch\n' '/etc/pacman.conf' --uninstall installer_case omarchy-rm 'ID=omarchy\nID_LIKE=arch\n' 'punktfunk-omarchy remove' --uninstall installer_case bazzite-rm 'ID=bazzite\nID_LIKE="fedora"\nVERSION_ID=43\n' 'punktfunk-sysext remove' --uninstall +sh scripts/ci/check-install-defaults.sh || fail=1 # ---------------------------------------------------------------- gate 8: channel switching # A box that already has all three binaries, on a repo config naming one channel, told --channel diff --git a/scripts/ci/check-install-defaults.sh b/scripts/ci/check-install-defaults.sh new file mode 100755 index 00000000..48e6f7c8 --- /dev/null +++ b/scripts/ci/check-install-defaults.sh @@ -0,0 +1,171 @@ +#!/bin/sh +# Derived defaults for the guided installer (scripts/install.sh, issue #431). +# Faked os-release + --dry-run --yes --no-start: each family must print the summary that +# --yes would actually take, and that summary must appear before the first sudo. +# DISPLAY / WAYLAND_DISPLAY / XDG_SESSION_TYPE are pinned per case so a graphical seat on +# the machine running the gate cannot leak into a "headless" assertion. +set -u +cd "$(dirname "$0")/../.." || exit 2 + +fail=0 +osr=$(mktemp -d) +gsbin=$(mktemp -d) +ujbin=$(mktemp -d) +trap 'rm -rf "$osr" "$gsbin" "$ujbin"' EXIT + +# name os-release-body seat(desktop|headless) extra-PATH-dir expected-substring [install.sh args...] +defaults_case() { + name=$1 + printf '%b' "$2" > "$osr/$name" + seat=$3 + extra=$4 + want=$5 + shift 5 + case "$seat" in + desktop) _env="DISPLAY=:0 WAYLAND_DISPLAY= XDG_SESSION_TYPE=x11" ;; + headless) _env="DISPLAY= WAYLAND_DISPLAY= XDG_SESSION_TYPE=" ;; + *) echo "::error::defaults_case $name: seat must be desktop or headless, got '$seat'"; fail=1; return ;; + esac + # $_env is expanded on purpose: the seat pins must override a graphical session on the + # machine running the gate. Quoted "$@" are extra install.sh flags (e.g. --no-punktfunk-group). + out=$(env $_env PATH="${extra:+$extra:}$PATH" \ + PUNKTFUNK_INSTALL_OS_RELEASE="$osr/$name" \ + sh scripts/install.sh --dry-run --yes --no-start "$@" 2>&1) || { + echo "::error::scripts/install.sh --dry-run --yes --no-start on fake $name ($seat) exited $?" + printf '%s\n' "$out" | sed 's/^/ /' + fail=1 + return + } + case "$out" in + *"$want"*) ;; + *) + echo "::error::scripts/install.sh --dry-run on fake $name ($seat) did not print '$want':" + printf '%s\n' "$out" | sed 's/^/ /' + fail=1 + return ;; + esac + # Summary is the audit trail for --yes; it has to land before any privileged command. + # A re-run on a box that already has the host may print no sudo at all — Choices must + # still appear; when sudo does print, it must not come first. + if ! printf '%s\n' "$out" | awk ' + /Choices \(nothing below has run yet\)/ { c=NR } + index($0, " + sudo") && !s { s=NR } + END { if (!c) exit 1; if (s && c > s) exit 1 } + '; then + echo "::error::scripts/install.sh --dry-run on fake $name ($seat): Choices must print before the first '+ sudo':" + printf '%s\n' "$out" | sed 's/^/ /' + fail=1 + fi +} + +DEB='ID=debian\nVERSION_ID=13\nPRETTY_NAME="Debian GNU/Linux 13"\n' +FED='ID=fedora\nVERSION_ID=44\nPRETTY_NAME="Fedora Linux 44"\n' +ARCH='ID=arch\nPRETTY_NAME="Arch Linux"\n' +OMA='ID=omarchy\nID_LIKE=arch\nVERSION_ID=4.0.1\nPRETTY_NAME="Omarchy"\n' +BAZ='ID=bazzite\nID_LIKE="fedora"\nVERSION_ID=43\nPRETTY_NAME="Bazzite"\n' +NOB='ID=nobara\nID_LIKE="fedora"\nVERSION_ID=43\nPRETTY_NAME="Nobara Linux"\n' +SIL='ID=fedora\nVARIANT_ID=silverblue\nVERSION_ID=44\nPRETTY_NAME="Fedora Linux 44 (Silverblue)"\n' + +# Desktop seat: only the couch/HTPC distros flip group + linger on. +defaults_case debian-desk "$DEB" desktop '' 'Full controller (joins the punktfunk group): no' +defaults_case debian-desk2 "$DEB" desktop '' 'Third-party clients (Moonlight, Artemis): no' +defaults_case debian-desk3 "$DEB" desktop '' 'Shared clipboard: no' +defaults_case debian-desk4 "$DEB" desktop '' 'Start at boot with nobody logged in: no' +defaults_case fedora-desk "$FED" desktop '' 'Full controller (joins the punktfunk group): no' +defaults_case fedora-desk2 "$FED" desktop '' 'Start at boot with nobody logged in: no' +defaults_case arch-desk "$ARCH" desktop '' 'Full controller (joins the punktfunk group): no' +defaults_case arch-desk2 "$ARCH" desktop '' 'Start at boot with nobody logged in: no' +# Omarchy is a sit-at Arch flavour, not a couch/HTPC default — linger only if the session is seatless. +defaults_case omarchy-desk "$OMA" desktop '' 'Full controller (joins the punktfunk group): no' +defaults_case omarchy-desk2 "$OMA" desktop '' 'Start at boot with nobody logged in: no' +defaults_case omarchy-ssh "$OMA" headless '' 'Start at boot with nobody logged in: yes (no graphical session)' + +defaults_case bazzite-desk "$BAZ" desktop '' 'Full controller (joins the punktfunk group): yes (Bazzite — virtual Steam Deck pad)' +defaults_case bazzite-desk2 "$BAZ" desktop '' 'Start at boot with nobody logged in: yes (Bazzite hosts are usually headless)' +defaults_case bazzite-desk3 "$BAZ" desktop '' 'Third-party clients (Moonlight, Artemis): no' +defaults_case bazzite-desk4 "$BAZ" desktop '' 'Shared clipboard: no' + +defaults_case nobara-desk "$NOB" desktop '' 'Full controller (joins the punktfunk group): yes (Nobara — virtual Steam Deck pad)' +defaults_case nobara-desk2 "$NOB" desktop '' 'Start at boot with nobody logged in: yes (Nobara hosts are usually headless)' +# Flags/env still win over the distro default. +defaults_case bazzite-flag "$BAZ" desktop '' 'Full controller (joins the punktfunk group): no' --no-punktfunk-group + +# No graphical seat (SSH, CI, a pipe) → linger even on a generic distro. +defaults_case debian-ssh "$DEB" headless '' 'Start at boot with nobody logged in: yes (no graphical session)' +defaults_case debian-ssh2 "$DEB" headless '' 'Full controller (joins the punktfunk group): no' + +# Active Sunshine-family host (detect-conflicts exit 1) → Moonlight compat defaults on. +# A dormant leftover is not enough — that is the same split detect-conflicts uses. +cat > "$gsbin/punktfunk-host" <<'EOF' +#!/bin/sh +if [ "${1:-}" = detect-conflicts ]; then + echo "Sunshine is running" + exit 1 +fi +echo 0.0.0-test +EOF +chmod +x "$gsbin/punktfunk-host" +defaults_case debian-gs "$DEB" desktop "$gsbin" 'Third-party clients (Moonlight, Artemis): yes (Sunshine/Apollo already on this box)' +defaults_case debian-gs2 "$DEB" desktop "$gsbin" 'Full controller (joins the punktfunk group): no' + +# `ujust` and `rpm-ostree` are NOT couch-box tells. Bluefin and Aurora ship ujust; Silverblue +# and Kinoite are rpm-ostree (so the installer calls them FAMILY=sysext, same as Bazzite). All +# four are desktop workstations, and joining their users to the punktfunk group or enabling +# linger under --yes without asking is not what they installed. Only ID/ID_LIKE decides. +printf '#!/bin/sh\nexit 0\n' > "$ujbin/ujust" +chmod +x "$ujbin/ujust" +printf '#!/bin/sh\nexit 0\n' > "$ujbin/rpm-ostree" +chmod +x "$ujbin/rpm-ostree" +defaults_case debian-ujust "$DEB" desktop "$ujbin" 'Full controller (joins the punktfunk group): no' +defaults_case debian-ujust2 "$DEB" desktop "$ujbin" 'Start at boot with nobody logged in: no' +defaults_case silverblue "$SIL" desktop "$ujbin" 'Full controller (joins the punktfunk group): no' +defaults_case silverblue2 "$SIL" desktop "$ujbin" 'Start at boot with nobody logged in: no' +# …and the narrowing must not cost Bazzite its own defaults, sysext family and all. +defaults_case bazzite-sysext "$BAZ" desktop "$ujbin" 'Full controller (joins the punktfunk group): yes (Bazzite — virtual Steam Deck pad)' + +# --no-start is "don't enable the services", not "don't configure": linger was promised in the +# summary, so it has to actually run. Every other case here passes --no-start, so without this +# one the matrix would happily certify a summary the run drops on the floor. +defaults_case bazzite-linger "$BAZ" desktop '' '+ sudo loginctl enable-linger' + +# ------------------------------------------------ run(): the command must survive to the shell +# --dry-run returns before run() executes anything, so the matrix above is structurally blind to +# what run() does to a command. That blindness cost a red smoke job: a `sudo` → `sudo -n` rewrite +# under --yes met install.sh's own root-container shim, which is `exec "$@"`, and `exec -n …` is +# not a command. So the stub here IS that shim, byte for byte, and the assertion is simply that +# the command still runs. Any flag injected into a sudo line fails this in every mode. +# TTY=/dev/null stands in for a terminal: run() only tests whether the variable is set, and a CI +# runner has no controlling terminal to open. +sudobin=$(mktemp -d) +trap 'rm -rf "$osr" "$gsbin" "$ujbin" "$sudobin"' EXIT +printf '#!/bin/sh\nexec "$@"\n' > "$sudobin/sudo" +chmod +x "$sudobin/sudo" + +# name TTY-value YES +# The probe is `sudo expr 40 + 2`, and the assertion is on its OUTPUT (42), which appears in the +# command's *text* nowhere. run() echoes every command before running it, so a marker word would +# match that echo and pass even when nothing executed. +run_case() { + { + echo 'die() { echo DIED; exit 1; }' + awk '/^run\(\) \{/,/^\}/' scripts/install.sh + printf "DRY=0; TTY=%s; YES=%s; DOCS_PAGE=x\nrun 'sudo expr 40 + 2'\n" "$2" "$3" + } > "$sudobin/harness.sh" + got=$(PATH="$sudobin:$PATH" sh "$sudobin/harness.sh" 2>&1) + case "$got" in + *42*) ;; + *) + echo "::error::run() with $1: the sudo line never reached the shell:" + printf '%s\n' "$got" | sed 's/^/ /' + fail=1 ;; + esac +} +run_case 'a terminal and --yes' /dev/null 1 +run_case 'a terminal, prompting' /dev/null 0 +run_case 'no terminal (the smoke job, root + shim)' '' 1 + +if [ "$fail" -ne 0 ]; then + echo "installer default matrix failed" + exit 1 +fi +echo "installer default matrix ok" diff --git a/scripts/install.sh b/scripts/install.sh index e65244c3..dd4255f1 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -4,10 +4,11 @@ # curl -fsSL https://punktfunk.unom.io/install.sh | sh # curl -fsSLO https://punktfunk.unom.io/install.sh && sh install.sh --help # read it first # -# Zero-to-streaming for a Linux host: detect the distro → add the package repo → install → deal -# with a Sunshine/Apollo/Vibeshine already on the box (move one port) → groups → options → -# firewall → start the services → verify → print how to pair. Plain POSIX sh, `read` prompts, -# no TUI. Every prompt has a default so `--yes` (or no terminal) runs unattended. +# Zero-to-streaming for a Linux host: detect the distro → ask intent (not internals) → print +# the choices → add the package repo → install → deal with a Sunshine/Apollo/Vibeshine already +# on the box (move one port) → groups → options → firewall → start the services → verify → +# print how to pair. Plain POSIX sh, `read` prompts, no TUI. Every prompt has a default so +# `--yes` (or no terminal) runs unattended; the default follows the box, not a global no. # # This is WP4 of the docs-and-onboarding overhaul and is labelled PREVIEW on purpose: the per-distro # docs pages (https://docs.punktfunk.unom.io/docs/install) remain the documented default until it @@ -25,10 +26,10 @@ USER=${USER:-$(id -un)}; export USER YES=${PUNKTFUNK_INSTALL_YES:-0} CHANNEL=${PUNKTFUNK_INSTALL_CHANNEL:-stable} CHANNEL_SET=0; [ -n "${PUNKTFUNK_INSTALL_CHANNEL:-}" ] && CHANNEL_SET=1 # asked for, vs. defaulted -GAMESTREAM=${PUNKTFUNK_INSTALL_GAMESTREAM:-} # 1/0, empty = ask (default no) -CLIPBOARD=${PUNKTFUNK_INSTALL_CLIPBOARD:-} # 1/0, empty = ask (default no) -PF_GROUP=${PUNKTFUNK_INSTALL_PUNKTFUNK_GROUP:-} # 1/0, empty = ask (default no) -LINGER=${PUNKTFUNK_INSTALL_LINGER:-} # 1/0, empty = ask (default no) +GAMESTREAM=${PUNKTFUNK_INSTALL_GAMESTREAM:-} # 1/0, empty = ask +CLIPBOARD=${PUNKTFUNK_INSTALL_CLIPBOARD:-} # 1/0, empty = ask +PF_GROUP=${PUNKTFUNK_INSTALL_PUNKTFUNK_GROUP:-} # 1/0, empty = ask +LINGER=${PUNKTFUNK_INSTALL_LINGER:-} # 1/0, empty = ask MGMT_PORT=${PUNKTFUNK_INSTALL_MGMT_PORT:-47991} # where the management API moves to on a conflict START=1 DRY=${PUNKTFUNK_INSTALL_DRY_RUN:-0} @@ -42,10 +43,10 @@ usage: sh install.sh [options] -y, --yes no prompts: take every default (also the behaviour without a terminal) --channel stable|canary package channel (default stable; canary = latest main build). On a box that already has the host this SWITCHES channel, either direction. - --gamestream | --no-gamestream also serve stock Moonlight clients (default no — trusted LANs only) - --clipboard | --no-clipboard allow the shared clipboard on this host (default no) - --punktfunk-group | --no-punktfunk-group join the punktfunk group (virtual Steam Deck pad; default no) - --linger | --no-linger start the host at boot with nobody logged in (default no) + --gamestream | --no-gamestream Moonlight/Artemis/third-party clients (default depends on the box) + --clipboard | --no-clipboard shared clipboard (default no) + --punktfunk-group | --no-punktfunk-group full controller / virtual Steam Deck pad (default depends on the box) + --linger | --no-linger start at boot with nobody logged in (default depends on the box) --mgmt-port N port to move the management API to if Sunshine/Apollo holds 47990 (default $MGMT_PORT) --no-start install and configure, but don't enable the services --uninstall stop the services and remove the packages + repo (config stays: $DOCS/uninstall) @@ -120,6 +121,12 @@ run() { fi printf ' + %s\n' "$cmd" [ "$DRY" = 1 ] && return 0 + # Deliberately no `sudo -n` rewrite. It breaks the root-container shim below — that shim is + # `exec "$@"`, and `exec -n install …` is not a command — which is a path installer-smoke + # covers. It also buys nothing it claims to: with no terminal, sudo already exits at once + # with "no tty present and no askpass program specified" instead of hanging, and on a box + # where an askpass helper IS configured, -n would break the one unattended path that works. + # Stdin is the terminal when there is one so a package manager's own prompt still reaches it. if [ -n "$TTY" ]; then sh -ec "$cmd" < "$TTY"; else sh -ec "$cmd" < /dev/null; fi \ || die "that step failed — fix it and re-run (the script is safe to repeat), or follow the page by hand: $DOCS_PAGE" } @@ -296,6 +303,122 @@ if [ "$FAMILY" = dnf ]; then esac fi +# ---------------------------------------------------------------------------- choices +# Empty flag/env still means "ask". The default behind Enter (and --yes) follows the box: +# couch/HTPC distros want the Deck pad and linger; an active Sunshine-family host wants +# Moonlight compat; a seatless session wants linger. Clipboard stays off. Flags/env win. +has_graphical_seat() { + [ -n "${DISPLAY:-}" ] && return 0 + [ -n "${WAYLAND_DISPLAY:-}" ] && return 0 + case "${XDG_SESSION_TYPE:-}" in x11|wayland) return 0 ;; esac + return 1 +} +# Only the Game Mode / HTPC images, which is exactly what the docs promise. `rpm-ostree`, +# `bootc` and `ujust` are NOT tells: Silverblue, Kinoite, Bluefin and Aurora ship all three and +# are desktop workstations, so keying off FAMILY=sysext or a `ujust` on PATH would join their +# users to the punktfunk group and enable linger under --yes without ever asking. +couch_box() { like bazzite || like nobara; } +# Same split as `punktfunk-host detect-conflicts`: exit 1 only when a Sunshine-family host +# runs or autostarts. A dormant leftover is not a reason to open Moonlight ports. +sunshine_active() { + if command -v punktfunk-host >/dev/null 2>&1; then + punktfunk-host detect-conflicts >/dev/null 2>&1 + # Only 1 is an answer. Any other code is a host too old to know the subcommand, a + # half-installed one, or a crash — fall through to the unit probe rather than opening + # the plain-HTTP GameStream surface on a guess. + case $? in + 0) return 1 ;; + 1) return 0 ;; + esac + fi + for u in sunshine.service apollo.service vibeshine.service; do + systemctl is-active --quiet "$u" 2>/dev/null && return 0 + systemctl is-enabled --quiet "$u" 2>/dev/null && return 0 + systemctl --user is-active --quiet "$u" 2>/dev/null && return 0 + systemctl --user is-enabled --quiet "$u" 2>/dev/null && return 0 + done + return 1 +} + +GROUP_WHY=; GS_WHY=; LINGER_WHY= +DEF_GROUP=n; DEF_GS=n; DEF_LINGER=n + +if couch_box; then + DEF_GROUP=y + DEF_LINGER=y + case "$ID" in + bazzite) + GROUP_WHY="Bazzite — virtual Steam Deck pad" + LINGER_WHY="Bazzite hosts are usually headless" ;; + nobara) + GROUP_WHY="Nobara — virtual Steam Deck pad" + LINGER_WHY="Nobara hosts are usually headless" ;; + *) + GROUP_WHY="Game Mode / HTPC box — virtual Steam Deck pad" + LINGER_WHY="Game Mode / HTPC box" ;; + esac +fi +if ! has_graphical_seat; then + DEF_LINGER=y + [ -z "$LINGER_WHY" ] && LINGER_WHY="no graphical session" +fi +if sunshine_active; then + DEF_GS=y + GS_WHY="Sunshine/Apollo already on this box" +fi + +if [ -z "$PF_GROUP" ]; then + if [ "$DEF_GROUP" = y ]; then + case "$ID" in + bazzite) q="Bazzite detected" ;; + nobara) q="Nobara detected" ;; + *) q="Game Mode / HTPC box detected" ;; + esac + # Ask about intent, but never hide what the answer grants: the group gates usbip attach. + ask "$q — join the punktfunk group for the full controller (paddles, trackpads, gyro)? It grants usbip attach, so only on a machine you trust ($DOCS/gamescope#nobara-and-other-autologin-display-managers)" y && PF_GROUP=1 || PF_GROUP=0 + else + ask "Do you want the full controller — paddles, trackpads, gyro? It joins the punktfunk group, which grants usbip attach — only on a machine you trust ($DOCS/gamescope#nobara-and-other-autologin-display-managers). Skip it and the pad arrives as a plain Xbox 360 controller" n && PF_GROUP=1 || PF_GROUP=0 + GROUP_WHY= + fi + [ "$PF_GROUP" = 0 ] && GROUP_WHY= +fi +if [ -z "$GAMESTREAM" ]; then + if [ "$DEF_GS" = y ]; then + ask "Sunshine or Apollo is already on this box — also serve Moonlight, Artemis, or another third-party client? (Punktfunk's own apps don't need this)" y && GAMESTREAM=1 || GAMESTREAM=0 + else + ask "Will you connect with Moonlight, Artemis, or another third-party client? (Punktfunk's own apps don't need this)" n && GAMESTREAM=1 || GAMESTREAM=0 + GS_WHY= + fi + [ "$GAMESTREAM" = 0 ] && GS_WHY= +fi +if [ -z "$CLIPBOARD" ]; then + ask "Share the clipboard between this host and your clients? (each client still opts in per host)" n && CLIPBOARD=1 || CLIPBOARD=0 +fi +if [ -z "$LINGER" ]; then + if [ "$DEF_LINGER" = y ]; then + ask "$LINGER_WHY — start the host at boot with nobody logged in?" y && LINGER=1 || LINGER=0 + else + ask "Is this a box you stream from and rarely log into? (starts the host at boot with nobody logged in)" n && LINGER=1 || LINGER=0 + LINGER_WHY= + fi + [ "$LINGER" = 0 ] && LINGER_WHY= +fi + +yn() { [ "$1" = 1 ] && printf yes || printf no; } +choice_line() { + if [ -n "$3" ] && [ "$2" = 1 ]; then + printf ' %s: %s (%s)\n' "$1" "$(yn "$2")" "$3" + else + printf ' %s: %s\n' "$1" "$(yn "$2")" + fi +} +# Under --yes this summary is the ONLY place the group grant is stated, so it names the group. +say "Choices (nothing below has run yet)" +choice_line "Full controller (joins the punktfunk group)" "$PF_GROUP" "$GROUP_WHY" +choice_line "Third-party clients (Moonlight, Artemis)" "$GAMESTREAM" "$GS_WHY" +choice_line "Shared clipboard" "$CLIPBOARD" "" +choice_line "Start at boot with nobody logged in" "$LINGER" "$LINGER_WHY" + # ---------------------------------------------------------------------------- 1. install # The snippets below are data/platforms.json's install lines, verbatim (stable channel); canary # and the Fedora group are edited in. check-docs-drift.sh gate 6 keeps them identical. @@ -547,9 +670,6 @@ elif ! getent group input >/dev/null 2>&1; then else run 'sudo usermod -aG input "$USER"'; RELOGIN=1 fi -if [ -z "$PF_GROUP" ]; then - ask "Also join the punktfunk group? It enables the virtual Steam Deck controller (paddles, trackpads, gyro) by granting usbip attach — only on a machine you trust ($DOCS/gamescope#nobara-and-other-autologin-display-managers)" n && PF_GROUP=1 || PF_GROUP=0 -fi if [ "$PF_GROUP" = 1 ]; then if id -nG "$USER" 2>/dev/null | tr ' ' '\n' | grep -qx punktfunk; then ok "already in the punktfunk group" else run 'sudo usermod -aG punktfunk "$USER"'; RELOGIN=1; fi @@ -557,16 +677,10 @@ fi # ---------------------------------------------------------------------------- 4. options say "Options (host.env — everything here is off by default and reversible)" -if [ -z "$GAMESTREAM" ]; then - ask "Also serve stock Moonlight clients (GameStream compat)? Its pairing runs over plain HTTP — trusted LANs only; Punktfunk's own apps don't need it" n && GAMESTREAM=1 || GAMESTREAM=0 -fi if [ "$GAMESTREAM" = 1 ]; then [ "$CONFLICT" = 1 ] && warn "with another GameStream host running, only one can bind the Moonlight ports — stop the other first or skip this" set_env PUNKTFUNK_GAMESTREAM 1 fi -if [ -z "$CLIPBOARD" ]; then - ask "Allow the shared clipboard on this host (each client still opts in per host)?" n && CLIPBOARD=1 || CLIPBOARD=0 -fi [ "$CLIPBOARD" = 1 ] && set_env PUNKTFUNK_CLIPBOARD on # ---------------------------------------------------------------------------- 5. firewall @@ -589,6 +703,22 @@ else fi # ---------------------------------------------------------------------------- 6. start +# Linger is configuration, not starting, so --no-start still honours it — the summary above +# promised it either way, and a promise the run silently drops is worse than not offering it. +# It is also what creates the user manager on a seatless box, so it has to land before the +# `systemctl --user` probe below, or SSH/headless installs print the enable command and stop. +if [ "$LINGER" = 1 ]; then + say "Starting at boot with nobody logged in" + # A container (installer-smoke, a chroot, docker) has no logind: enable-linger there fails + # with "System has not been booted with systemd as init system", and linger would mean + # nothing anyway. Say so and carry on — the rest of the install worked. --dry-run still + # prints the command, because it reports what a real box would do. + if [ "$DRY" = 1 ] || [ -d /run/systemd/system ]; then + run 'sudo loginctl enable-linger "$USER"' + else + warn "no systemd as PID 1 here (a container?) — skipping linger, nothing would honour it" + fi +fi if [ "$START" = 1 ]; then say "Starting the host and the web console" if ! systemctl --user show-environment >/dev/null 2>&1; then @@ -606,10 +736,6 @@ if [ "$START" = 1 ]; then # The plugin runner fills the game library; apt/dnf/sysext start it themselves, Arch doesn't. if systemctl --user list-unit-files punktfunk-scripting.service 2>/dev/null | grep -q disabled; then units="$units punktfunk-scripting"; fi run "systemctl --user enable --now $units" - if [ -z "$LINGER" ]; then - ask "Start the host at boot even with nobody logged in (headless box)?" n && LINGER=1 || LINGER=0 - fi - [ "$LINGER" = 1 ] && run 'sudo loginctl enable-linger "$USER"' fi fi