From 6b12d5329fd573a2dafa63831fb9087d7a7b6d95 Mon Sep 17 00:00:00 2001 From: Pierre Kisters <1524059+lhns@users.noreply.github.com> Date: Thu, 1 Oct 2026 18:01:33 +0200 Subject: [PATCH] Install: name what waits for a restart; uninstall survives a failed Nix uninstall A fresh install into a running session left the keyboard layout and the SteamVR dashboard patches (VR pet, the VR keyboard's suggestion strip, ...) silently off until a reboot: gamescope reads XKB_DEFAULT_* only at its start, and SteamVR opens its DevTools port (VRWebHelper.DebuggerEnabled) only at its start. Neither can be applied at runtime. - install.sh restart-check (run on every switch by session.nix, and by install at its end): compares the keyboard-layout drop-in with the running gamescope session's environment and checks whether SteamVR runs without port 8087; install ends with "Restart once" listing them. - uninstall: a failing nix-installer uninstall (e.g. /nix busy) no longer aborts the script before the ~/.config/home-manager link and Home Manager state are removed; it says to reboot and run uninstall again. - cleanup check: restart-check on fake cgroup/proc dirs. --- README.md | 13 ++++- docs/development.md | 4 +- docs/session.md | 9 ++- docs/steamvr-debugger.md | 7 ++- install.sh | 116 ++++++++++++++++++++++++++++++++++++-- modules/cleanup/check.nix | 41 +++++++++++++- modules/session.nix | 15 ++++- 7 files changed, 191 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index d3d4b63..66e63e7 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,13 @@ The installer installs Nix (skipped if Nix already works), uses `~/.config/home-manager` or `--flake ` (if there is none, it creates `~/nix-config` from the [template](template) with your user name) and runs `home-manager switch`; what it sets up is listed under -[Set up by install.sh](#set-up-by-installsh). Re-running it just switches +[Set up by install.sh](#set-up-by-installsh). Everything works in the running +session right away, except two settings read only at a process start: the +[keyboard layout](docs/session.md#keyboard-layout) (next Steam session start) +and the [SteamVR dashboard patches](docs/steamvr-debugger.md) (VR pet, the VR +keyboard's suggestion strip, window curvature and the other patches of +SteamVR's dashboard; next SteamVR start). If you use them, the installer ends +with "Restart once": reboot once. Re-running it just switches again (`--yes` answers every question). Afterwards edit `~/nix-config/home.nix` and switch (see [Usage](#usage)). @@ -482,7 +488,10 @@ stops Home Manager's user services (reverting the UI patches), runs `cleanup --all`, runs `home-manager uninstall`, then removes Nix and the per-user Nix state (see [Set up by install.sh](#set-up-by-installsh)). If SteamVR is running, its key is restored when SteamVR stops (the closing -message says so). Your configuration, `*.hm-backup-*` files, app data and +message says so). If Nix's own uninstaller fails (e.g. something still uses +`/nix`), the rest still runs (the `~/.config/home-manager` link goes) except +the per-user Nix state, and the closing message says to reboot and run +`uninstall` again. Your configuration, `*.hm-backup-*` files, app data and Flatpaks stay. To drop steam-frame-nix from a Home Manager configuration you keep, first diff --git a/docs/development.md b/docs/development.md index a9e3a21..d10ad77 100644 --- a/docs/development.md +++ b/docs/development.md @@ -47,8 +47,8 @@ scripts/ modules/ session.nix switch: outer bus/runtime dir, user services on switch cleanup.nix switch: `cleanup --orphans`; steam-frame-nix-cleanup on PATH - cleanup/package.nix build: install.sh as a command (cleanup, steamvr-debugger-arm) - cleanup/check.nix test: install.sh cleanup on fake home/runtime dirs + cleanup/package.nix build: install.sh as a command (cleanup, steamvr-debugger-arm, restart-check) + cleanup/check.nix test: install.sh cleanup and restart-check on fake home/runtime dirs portal.nix Steam session portal config (session.portalFix) portal/check.nix test: KDE FileChooser on by default, absent when disabled applications-menu.nix applications.menu link for KDE apps (session.applicationsMenu) diff --git a/docs/session.md b/docs/session.md index 1ee9672..a873ebb 100644 --- a/docs/session.md +++ b/docs/session.md @@ -23,6 +23,9 @@ running") and skips `reloadSystemd`. - After every switch this module reloads the Steam session's user manager and applies `session.services.start` / `stop` / `restart`, which other modules fill (you can add your own units). +- Then it names what the running session can't pick up until a restart: + the [keyboard layout](#keyboard-layout) and the + [SteamVR debugger](steamvr-debugger.md) of the dashboard patches. **Configuration:** apps that keep secrets in the wallet get a [launcher](launchers.md) with `keyring.enable`. Another app that must reach @@ -104,7 +107,11 @@ the link dangles, which is the same as no menu. `~/.config/environment.d` isn't read on the Frame. **What you get:** the layout in the Steam session, from the next Steam -session start. The layout also picks the +session start (e.g. reboot): gamescope reads it only when it starts, and +sends its own keymap to the X apps again whenever the VR keyboard types, so +it can't be set in a running session. While the running session has another +layout, the switch (and `install.sh install` at its end) says so +(`install.sh restart-check`). The layout also picks the [VR keyboard](keyboard.md#swipe-and-suggestions)'s default dictionary language. diff --git a/docs/steamvr-debugger.md b/docs/steamvr-debugger.md index beba6e4..fbec7f8 100644 --- a/docs/steamvr-debugger.md +++ b/docs/steamvr-debugger.md @@ -21,8 +21,11 @@ to its previous value when SteamVR stops, also after a rollback or uninstall (without Nix). A value you set to `true` yourself is never touched. **The first time, restart SteamVR once** (e.g. reboot); until then the -dashboard patches wait. Turned off, the setting is restored at the switch -(or when SteamVR stops, if it runs). +dashboard patches wait. SteamVR opens the port only when it starts, so no +switch or install can do it for a running SteamVR; the switch (and +`install.sh install` at its end) says so while SteamVR runs without the port +(`install.sh restart-check`). Turned off, the setting is restored at the +switch (or when SteamVR stops, if it runs). ## Security diff --git a/install.sh b/install.sh index df0b605..88f4199 100755 --- a/install.sh +++ b/install.sh @@ -90,6 +90,12 @@ Commands: Internal (run before each SteamVR start by steamFrame.steamvrDebugger): set VRWebHelper.DebuggerEnabled until SteamVR stops. + restart-check + Internal (run on every switch, and by install): name what of the + linked configuration only takes effect after a restart of the Steam + session or SteamVR (the keyboard layout, the SteamVR dashboard + patches). + Options: --yes, -y Don't ask; answer yes to every question. --help, -h Show this help. @@ -218,11 +224,13 @@ install_nix() { } uninstall_nix() { + local rc=0 need_sudo ro_unlock step "Uninstalling Nix" - sudo "$NIX_INSTALLER_BIN" uninstall --no-confirm + sudo "$NIX_INSTALLER_BIN" uninstall --no-confirm || rc=$? ro_relock + return "$rc" } ensure_nix() { @@ -357,6 +365,10 @@ cmd_install() { step "Activating Home Manager (conflicting files are renamed to *.$backup)" hm switch --flake "$ref" -b "$backup" + # Settings read only at a process start (the activation warned already). + local pending=() + mapfile -t pending < <(restart_pending) + local switch_cmd="home-manager switch --flake $ref" edit="your configuration" if [[ -n $dir ]]; then edit="$dir/home.nix" @@ -367,6 +379,11 @@ cmd_install() { fi step "Done" + if (( ${#pending[@]} )); then + printf '\n%sRestart once%s (reboot, or restart the Steam session) for:\n' "$Y" "$N" + printf ' - %s\n' "${pending[@]}" + printf '%s\n' " Everything else already works in the running session." + fi cat <=... in a unit file (empty if unset). +unit_env() { # file name + sed -n "s/^Environment=$2=//p" "$1" 2>/dev/null | tail -n1 +} + +# XKB_DEFAULT_ of the running gamescope session: from the first +# process of gamescope-session.service whose environment is readable +# (gamescope itself isn't: it has capabilities). Fails if unknown. +session_xkb() { # LAYOUT|VARIANT + local cg pid env found=1 + [[ $(c_systemctl show -p ActiveState --value gamescope-session.service 2>/dev/null) == active ]] || return 1 + cg="$(c_systemctl show -p ControlGroup --value gamescope-session.service 2>/dev/null)" && [[ -n $cg ]] || return 1 + [[ -r $SFN_CGROUP$cg/cgroup.procs ]] || return 1 + while read -r pid; do + env="$( { tr '\0' '\n' <"$SFN_PROC/$pid/environ"; } 2>/dev/null)" || continue + [[ -n $env ]] || continue + sed -n "s/^XKB_DEFAULT_$1=//p" <<<"$env" | tail -n1 + found=0 + break + done <"$SFN_CGROUP$cg/cgroup.procs" + return $found +} + +# Whether something listens on 127.0.0.1/::1/any : (/proc/net/tcp*). +port_listening() { # port + local hex + local hex f files=() + printf -v hex '%04X' "$1" + for f in "$SFN_PROC/net/tcp" "$SFN_PROC/net/tcp6"; do [[ -r $f ]] && files+=("$f"); done + (( ${#files[@]} )) || return 1 + awk -v p=":$hex" 'FNR > 1 && $4 == "0A" && substr($2, length($2) - 4) == p { f = 1 } END { exit !f }' "${files[@]}" +} + +# One line per setting that waits for a restart. +restart_pending() { + local want_l want_v run_l run_v + if [[ -e $LAYOUT_DROPIN ]]; then + want_l="$(unit_env "$LAYOUT_DROPIN" XKB_DEFAULT_LAYOUT)" + want_v="$(unit_env "$LAYOUT_DROPIN" XKB_DEFAULT_VARIANT)" + if run_l="$(session_xkb LAYOUT)" && run_v="$(session_xkb VARIANT)" \ + && [[ $run_l != "$want_l" || $run_v != "$want_v" ]]; then + echo "keyboard layout ${want_l:-us}${want_v:+ ($want_v)} (the running Steam session has ${run_l:-us}${run_v:+ ($run_v)}): from the next start of the Steam session" + fi + fi + if [[ -e $DEBUGGER_HM_DROPIN && $(steamvr_state) == running ]] && ! port_listening "$DEBUGGER_PORT"; then + echo "SteamVR dashboard patches (e.g. the VR pet, the VR keyboard's suggestion strip; DevTools port $DEBUGGER_PORT not open yet): from the next SteamVR start" + fi + return 0 +} + +cmd_restart_check() { + local pending=() + need_not_root + mapfile -t pending < <(restart_pending) + (( ${#pending[@]} )) || return 0 + warn "steam-frame-nix: some settings take effect only after a restart (reboot once, or restart the Steam session):" + printf ' - %s\n' "${pending[@]}" >&2 +} + # --- uninstall -------------------------------------------------------------- # User units installed by Home Manager (unit files resolving into /nix/store). @@ -1061,10 +1157,17 @@ cmd_uninstall() { cmd_cleanup --all remove_hm + # A failed Nix uninstall (e.g. /nix still busy) must not skip the rest: + # Home Manager is gone by now, so its config link goes regardless; the + # per-user Nix files stay for a Nix that may still be there. + local nix_failed=0 if (( ! keep_nix )); then if [[ -x $NIX_INSTALLER_BIN ]]; then stop_nix_processes - uninstall_nix + if ! uninstall_nix; then + warn "the Nix uninstall failed (see above)" + nix_failed=1 keep_nix=1 + fi elif command -v nix >/dev/null 2>&1; then warn "Nix wasn't installed by nix-installer ($NIX_INSTALLER_BIN missing); not removing it" keep_nix=1 @@ -1084,10 +1187,14 @@ Intentionally left in place: - app data, e.g. ~/.local/share/docker, Firefox profiles, and Flatpak apps Log out or reboot so running sessions drop the removed tweaks. EOF - if (( ${#CLEAN_DEFERRED[@]} )); then + if (( ${#CLEAN_DEFERRED[@]} || nix_failed )); then printf '\nNot done yet:\n' - printf ' - %s\n' "${CLEAN_DEFERRED[@]}" + (( ${#CLEAN_DEFERRED[@]} )) && printf ' - %s\n' "${CLEAN_DEFERRED[@]}" + (( nix_failed )) && printf '%s\n' \ + " - Nix: its uninstaller failed. Reboot (so nothing uses /nix), then run" \ + " uninstall again; it also removes ~/.nix-profile and ~/.local/state/nix." fi + (( ! nix_failed )) } # --- status ----------------------------------------------------------------- @@ -1161,6 +1268,7 @@ main() { status) cmd_status "$@" ;; cleanup) cmd_cleanup "$@" ;; steamvr-debugger-arm) cmd_debugger_arm "$@" ;; + restart-check) cmd_restart_check "$@" ;; -h|--help|help) usage ;; '') usage >&2; exit 2 ;; *) printf 'unknown command: %s\n\n' "$cmd" >&2; usage >&2; exit 2 ;; diff --git a/modules/cleanup/check.nix b/modules/cleanup/check.nix index 10bd097..feb396a 100644 --- a/modules/cleanup/check.nix +++ b/modules/cleanup/check.nix @@ -2,6 +2,7 @@ # artifact any version wrote (removed or restored) next to look-alikes that # aren't ours (left alone), --dry-run changes nothing, a second run changes # nothing, SteamVR running defers the debugger key to a runtime drop-in. +# Also `install.sh restart-check` (what waits for a session/SteamVR restart). { pkgs }: let cleanup = pkgs.callPackage ./package.nix { }; @@ -17,7 +18,11 @@ pkgs.runCommand "cleanup-check" { nativeBuildInputs = [ cleanup pkgs.jq ]; } '' cat > $STUB/systemctl <<'SH' #!/usr/bin/env bash echo "$*" >> "$STUB/log" - case "$*" in *"show -p ActiveState --value steamvr.service"*) cat "$STUB/state" ;; esac + case "$*" in + *"show -p ActiveState --value steamvr.service"*) cat "$STUB/state" ;; + *"show -p ActiveState --value gamescope-session.service"*) cat "$STUB/gs-state" ;; + *"show -p ControlGroup --value gamescope-session.service"*) echo /gs.service ;; + esac SH chmod +x $STUB/systemctl sed -i "1s|.*|#!${pkgs.bash}/bin/bash|" $STUB/systemctl @@ -27,7 +32,7 @@ pkgs.runCommand "cleanup-check" { nativeBuildInputs = [ cleanup pkgs.jq ]; } '' fresh() { root=$PWD/$1; rm -rf $root; mkdir -p $root/home $root/run HOME=$root/home XDG_RUNTIME_DIR=$root/run STEAM_FRAME_NIX_RUNTIME_DIR=$root/run - : > $STUB/log; echo inactive > $STUB/state + : > $STUB/log; echo inactive > $STUB/state; echo inactive > $STUB/gs-state S=$HOME/.local/state/steam-frame-nix V=$HOME/.config/openvr/config/steamvr.vrsettings I=$HOME/.local/share/icons/hicolor/scalable/apps @@ -263,5 +268,37 @@ pkgs.runCommand "cleanup-check" { nativeBuildInputs = [ cleanup pkgs.jq ]; } '' bash $root/run/steam-frame-nix/steamvr-debugger-restore gone $V $S/steamvr-debugger.armed echo "E ok" + + # --- F: restart-check (settings read only at a process start) --- + export STEAM_FRAME_NIX_PROC STEAM_FRAME_NIX_CGROUP + rc() { bash ${../../install.sh} restart-check 2>&1; } + fresh f + STEAM_FRAME_NIX_PROC=$root/proc STEAM_FRAME_NIX_CGROUP=$root/cg + U=$HOME/.config/systemd/user + mkdir -p $U/gamescope-session.service.d $U/steamvr.service.d $root/cg/gs.service $root/proc/net $root/proc/10 $root/proc/11 + printf '[Service]\nEnvironment=XKB_DEFAULT_LAYOUT=de\n' > $U/gamescope-session.service.d/keyboard.conf + printf '[Unit]\nWants=steamvr-webhelper-debugger.service\n' > $U/steamvr.service.d/webhelper-debugger.conf + printf '10\n11\n' > $root/cg/gs.service/cgroup.procs + : > $root/proc/10/environ; chmod 000 $root/proc/10/environ # gamescope: unreadable, skipped + printf 'HOME=/h\0XKB_DEFAULT_LAYOUT=us\0' > $root/proc/11/environ + tcp() { printf ' sl local_address rem_address st\n'; for l; do printf ' 0: %s 00000000:0000 0A\n' "$l"; done; } + tcp 0100007F:1F90 > $root/proc/net/tcp # only Steam's 8080 + # nothing runs: nothing waits + res=$(rc); [ -z "$res" ] || fail "restart-check while stopped: $res" + # fresh install into a running session: both wait + echo active > $STUB/gs-state; echo active > $STUB/state + res=$(rc); echo "$res" + has "$res" "keyboard layout de (the running Steam session has us): from the next start of the Steam session" + has "$res" "SteamVR dashboard patches" + # after the restarts: nothing waits + printf 'XKB_DEFAULT_LAYOUT=de\0' > $root/proc/11/environ + tcp 0100007F:1F90 0100007F:1F97 > $root/proc/net/tcp + res=$(rc); [ -z "$res" ] || fail "restart-check after restart: $res" + # a variant change waits; no layout configured: no check + printf '[Service]\nEnvironment=XKB_DEFAULT_LAYOUT=de\nEnvironment=XKB_DEFAULT_VARIANT=nodeadkeys\n' > $U/gamescope-session.service.d/keyboard.conf + res=$(rc); has "$res" "keyboard layout de (nodeadkeys) (the running Steam session has de)" + rm -r $U/gamescope-session.service.d $U/steamvr.service.d; : > $root/proc/net/tcp + res=$(rc); [ -z "$res" ] || fail "restart-check without the drop-ins: $res" + echo "F ok" touch $out '' diff --git a/modules/session.nix b/modules/session.nix index c59b6aa..3427629 100644 --- a/modules/session.nix +++ b/modules/session.nix @@ -6,10 +6,17 @@ # home-manager's reloadSystemd is skipped ("User systemd daemon not running"), # so `steamFrameUserServices` does it against the outer session: always # daemon-reload, then start/stop/restart the units in `session.services`. -{ config, lib, ... }: +# Then `steamFrameRestartCheck` (`install.sh restart-check`) names what the +# running session can't pick up (read only at a process start): the keyboard +# layout, the SteamVR dashboard patches' DevTools port. +{ config, lib, pkgs, ... }: let cfg = config.steamFrame.session; units = lib.escapeShellArgs; + restartCheck = pkgs.callPackage ./cleanup/package.nix { + name = "steam-frame-nix-restart-check"; + command = "restart-check"; + }; rename = from: to: lib.mkRenamedOptionModule ([ "steamFrame" ] ++ from) [ "steamFrame" "session" to ]; in { imports = [ @@ -91,6 +98,12 @@ in { # In a subshell: the outer session's environment must not leak into # activation steps that run later. + # Read-only; a warning, never a failed switch. + config.home.activation.steamFrameRestartCheck = lib.hm.dag.entryAfter [ "steamFrameUserServices" ] '' + STEAM_FRAME_NIX_RUNTIME_DIR=${lib.escapeShellArg cfg.runtimeDir} XDG_CONFIG_HOME=${lib.escapeShellArg config.xdg.configHome} \ + ${lib.getExe restartCheck} || true + ''; + config.home.activation.steamFrameUserServices = lib.hm.dag.entryAfter [ "reloadSystemd" ] ('' ( export XDG_RUNTIME_DIR=${lib.escapeShellArg cfg.runtimeDir} DBUS_SESSION_BUS_ADDRESS=${lib.escapeShellArg cfg.bus}