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.
This commit is contained in:
Pierre Kisters committed 2026-10-01 18:06:50 +02:00
1 parent 92e9d17e09
commit 6b12d5329f
7 files changed
+191 -14

No files matched your search

+11 -2
View File
@@ -81,7 +81,13 @@ The installer installs Nix (skipped if Nix already works), uses
`~/.config/home-manager` or `--flake <dir-or-flakeref>` (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
+2 -2
View File
@@ -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)
+8 -1
View File
@@ -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.
+5 -2
View File
@@ -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
+112 -4
View File
@@ -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 <<EOF
Next steps:
@@ -908,6 +925,85 @@ cmd_cleanup() {
return 0
}
# --- what waits for a restart -------------------------------------------------
#
# Two settings are read only when their process starts, so a switch (or the
# first install) can't apply them to the running session:
# - the keyboard layout (keyboard-layout.nix: XKB_DEFAULT_* in a drop-in on
# gamescope-session.service), read by gamescope at its start;
# - SteamVR's DevTools port (steamvr-debugger.nix: a Wants= drop-in on
# steamvr.service sets VRWebHelper.DebuggerEnabled before SteamVR starts),
# which the dashboard patches (VR pet, the keyboard's suggestion strip, ...)
# need.
# Both are compared with the running session; neither can be applied at
# runtime (gamescope sends its own keymap to Xwayland again, e.g. whenever
# the VR keyboard types, and only a SteamVR start opens the port).
SFN_PROC="${STEAM_FRAME_NIX_PROC:-/proc}"
SFN_CGROUP="${STEAM_FRAME_NIX_CGROUP:-/sys/fs/cgroup}"
LAYOUT_DROPIN="$USER_UNIT_DIR/gamescope-session.service.d/keyboard.conf"
DEBUGGER_HM_DROPIN="$USER_UNIT_DIR/steamvr.service.d/webhelper-debugger.conf"
DEBUGGER_PORT=8087
# Value of Environment=<name>=... 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_<what> 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 :<port> (/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 ;;
+39 -2
View File
@@ -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
''
+14 -1
View File
@@ -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}