Uninstall: detach /nix lazily instead of waiting for its users

Programs started from the Nix store (the Steam session itself, the
desktop portal) keep /nix busy until logout, so nix-installer's
`systemctl stop nix.mount` failed and the wait for them could never end.
A runtime drop-in (LazyUnmount=yes on nix.mount, in /run, removed again
afterwards) makes that stop detach /nix; the programs keep their open
files until they exit. They are listed for information only, without a
prompt. The re-exec of a bash from the Nix store is no longer needed.
This commit is contained in:
Pierre Kisters committed 2026-10-01 23:14:35 +02:00
1 parent b852a1eb07
commit 269321ea92
2 files changed
+90 -78

No files matched your search

+53 -60
View File
@@ -28,6 +28,8 @@ USER_NAME="$(id -un)"
USER_ID="$(id -u)"
OUTER_RUNTIME_DIR="${STEAM_FRAME_NIX_RUNTIME_DIR:-/run/user/$USER_ID}"
SFN_PROC="${STEAM_FRAME_NIX_PROC:-/proc}" # tests: a fake /proc
# Runtime drop-in for the uninstall (tests: another directory).
NIX_MOUNT_DROPIN="${STEAM_FRAME_NIX_SYSTEM_RUNTIME:-/run/systemd/system}/nix.mount.d/50-steam-frame-nix-lazy-unmount.conf"
ASSUME_YES=0
RO_RELOCK=0
@@ -70,8 +72,8 @@ Commands:
uninstall [--yes] [--keep-nix]
Stop Home Manager's user services, run 'cleanup --all', uninstall Home
Manager, uninstall Nix (unless --keep-nix) and remove per-user Nix
leftovers. Before removing Nix it lists the programs started from the
Nix store that still use /nix and waits until they are closed. Your
leftovers. /nix is detached lazily: programs started from the Nix
store (listed) keep running until you log out or reboot. Your
configuration directory (also a --clone) is never deleted.
cleanup [--dry-run] [--quiet] (--all | --orphans [--keep <artifact>]...)
@@ -233,20 +235,45 @@ install_nix() {
ro_relock
}
nix_mounted() { mountpoint -q /nix 2>/dev/null; }
uninstall_nix() {
local rc=0 bin=
local rc=0 bin='' lazy=0 users=()
need_sudo
ro_unlock
step "Uninstalling Nix"
# From a root-owned copy: the uninstaller running from /nix would keep
# /nix busy.
mapfile -t users < <(nix_users)
if (( ${#users[@]} )); then
info "Started from the Nix store, these keep running until you log out or reboot:"
printf ' - %s\n' "${users[@]}"
fi
# From a root-owned copy outside /nix.
if ! bin="$(sudo mktemp)" || ! sudo cp "$NIX_INSTALLER_BIN" "$bin" \
|| ! sudo chmod 700 "$bin" || ! sudo "$bin" --version >/dev/null 2>&1; then
[[ -n $bin ]] && sudo rm -f "$bin"
bin=$NIX_INSTALLER_BIN
fi
# Programs started from the Nix store keep /nix busy, so the uninstaller's
# `systemctl stop nix.mount` would fail. With LazyUnmount= that stop
# detaches /nix; the programs keep their open files until they exit.
if nix_mounted; then
if sudo mkdir -p "${NIX_MOUNT_DROPIN%/*}" \
&& printf '%s\n' '# steam-frame-nix: removing Nix detaches /nix while programs use it.' \
'[Mount]' 'LazyUnmount=yes' | sudo tee "$NIX_MOUNT_DROPIN" >/dev/null \
&& sudo systemctl daemon-reload; then
lazy=1
else
warn "could not add $NIX_MOUNT_DROPIN; removing Nix fails if a program still uses /nix"
fi
fi
sudo "$bin" uninstall --no-confirm || rc=$?
[[ $bin == "$NIX_INSTALLER_BIN" ]] || sudo rm -f "$bin"
if (( lazy )); then
sudo rm -f "$NIX_MOUNT_DROPIN"
sudo rmdir "${NIX_MOUNT_DROPIN%/*}" 2>/dev/null || true
sudo systemctl daemon-reload || true
fi
hash -r # commands found in /nix are gone
ro_relock
return "$rc"
}
@@ -1147,13 +1174,12 @@ remove_hm() {
fi
}
# --- /nix in use ---
# --- programs from the Nix store ---
#
# nix-installer can't unmount /nix while a process uses it: its program, a
# library, an open file or its working directory in /nix. Only this user's
# processes are readable (the uninstaller stops the Nix daemon itself). This
# script, its subshells and its curl | bash pipeline are skipped; the
# shells and apps it was started from are not.
# Listed before Nix goes, for information (uninstall_nix detaches /nix
# lazily): their program, a library, an open file or their working
# directory is in /nix. Only this user's processes are readable. This
# script, its subshells and its curl | bash pipeline are skipped.
proc_ppid() { # pid
local k v
@@ -1171,9 +1197,7 @@ proc_pgid() { # pid
# "<name> (PID <pid>)" per process using /nix.
nix_users() {
local p pid name argv0 self=$$ pgid ancestors=" "
p=$self
while [[ $p =~ ^[0-9]+$ ]] && (( p > 1 )); do ancestors+="$p "; p="$(proc_ppid "$p")"; done
local p pid name argv0 self=$$ pgid
pgid="$(proc_pgid "$self")"
{ find "$SFN_PROC"/[0-9]*/{exe,cwd,root} "$SFN_PROC"/[0-9]*/fd -maxdepth 1 -lname '/nix/*' 2>/dev/null || true
grep -ls '[[:space:]]/nix/' "$SFN_PROC"/[0-9]*/maps || true
@@ -1184,10 +1208,6 @@ nix_users() {
# argv[0]'s name says more than comm (often a thread name)
argv0=''; { IFS= read -r -d '' argv0 <"$SFN_PROC/$pid/cmdline"; } 2>/dev/null || true
[[ ${argv0##*/} == '' || ${argv0##*/} == exe ]] || name=${argv0##*/}
if [[ $ancestors == *" $pid "* ]]; then
echo "$name (PID $pid, started this uninstall: close it and run uninstall from another terminal)"
continue
fi
[[ -n $pgid && $(proc_pgid "$pid") == "$pgid" ]] && continue # our pipeline
p=$pid
while [[ $p =~ ^[0-9]+$ && $p != "$self" ]] && (( p > 1 )); do p="$(proc_ppid "$p")"; done
@@ -1196,22 +1216,6 @@ nix_users() {
done
}
# Waits until no process uses /nix; fails if the user gives up (or can't be
# asked: --yes, no terminal). Nothing is stopped automatically.
nix_idle() {
local users=() reply
while :; do
mapfile -t users < <(nix_users)
(( ${#users[@]} )) || return 0
warn "programs started from the Nix store still use /nix, so Nix can't be removed:"
printf ' - %s\n' "${users[@]}" >&2
(( ! ASSUME_YES )) && have_tty || return 1
printf 'Close them, then press Enter to check again (a: abort) ' >/dev/tty
read -r reply </dev/tty || return 1
[[ $reply != [aA]* ]] || return 1
done
}
remove_path() {
if [[ -e $1 || -L $1 ]]; then rm -rf -- "$1"; info "removed $1"; fi
}
@@ -1245,7 +1249,7 @@ remove_leftovers() { # keep_nix
}
cmd_uninstall() {
local keep_nix=0 orig_args=("$@")
local keep_nix=0
while (( $# )); do
case $1 in
-y|--yes) ASSUME_YES=1; shift ;;
@@ -1256,14 +1260,6 @@ cmd_uninstall() {
done
need_not_root
# A bash from Nix (e.g. via /usr/bin/env) would keep /nix busy.
if (( ! keep_nix )) && [[ $(readlink "$SFN_PROC/$$/exe") == /nix/store/* ]]; then
if [[ -f ${BASH_SOURCE[0]:-} && -x /usr/bin/bash ]]; then
exec /usr/bin/bash "${BASH_SOURCE[0]}" uninstall "${orig_args[@]}"
fi
die "this shell's bash is from Nix; run: curl -fsSL https://steam-frame-nix.lhns.de | /usr/bin/bash -s -- uninstall"
fi
if (( keep_nix )); then
step "This uninstalls Home Manager (its files and services); Nix stays"
else
@@ -1275,18 +1271,17 @@ cmd_uninstall() {
stop_hm_services
cmd_cleanup --all
remove_hm
hash -r # commands found in Home Manager's profile are gone
# Nix still in use or a failed Nix uninstall must not skip the rest: Home
# Manager is gone by now, so its config link goes regardless; the per-user
# Nix files stay for the Nix that is still there.
local nix_failed=''
# A failed Nix uninstall must not skip the rest: Home Manager is gone by
# now, so its config link goes regardless; the per-user Nix files stay
# for the Nix that is still there.
local nix_failed=0
if (( ! keep_nix )); then
if [[ -x $NIX_INSTALLER_BIN ]]; then
if ! nix_idle; then
nix_failed=busy keep_nix=1
elif ! uninstall_nix; then
if ! uninstall_nix; then
warn "the Nix uninstall failed (see above)"
nix_failed=failed keep_nix=1
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"
@@ -1305,19 +1300,17 @@ Intentionally left in place:
- your configuration (e.g. $DEFAULT_CONFIG_DIR, also when cloned)
- files Home Manager renamed to *.hm-backup-<time>
- app data, e.g. ~/.local/share/docker, Firefox profiles, and Flatpak apps
Log out or reboot so running sessions drop the removed tweaks.
Log out or reboot: running sessions drop the removed tweaks, and programs
that were started from Nix restart without it.
EOF
if (( ${#CLEAN_DEFERRED[@]} )) || [[ -n $nix_failed ]]; then
if (( ${#CLEAN_DEFERRED[@]} || nix_failed )); then
printf '\nNot done yet:\n'
(( ${#CLEAN_DEFERRED[@]} )) && printf ' - %s\n' "${CLEAN_DEFERRED[@]}"
case $nix_failed in
busy) printf '%s\n' " - Nix: still in use by the programs listed above. Close these or reboot," ;;
failed) printf '%s\n' " - Nix: its uninstaller failed. Reboot (so nothing uses /nix)," ;;
esac
[[ -n $nix_failed ]] && printf '%s\n' \
" then run uninstall again; it also removes ~/.nix-profile and ~/.local/state/nix."
(( nix_failed )) && printf '%s\n' \
" - Nix: its uninstaller failed. Reboot, then run uninstall again;" \
" it also removes ~/.nix-profile and ~/.local/state/nix."
fi
[[ -z $nix_failed ]]
(( ! nix_failed ))
}
# --- status -----------------------------------------------------------------
+37 -18
View File
@@ -5,8 +5,8 @@
# Also `install.sh restart-check` (what waits for a session/SteamVR restart).
# Also `install.sh install --clone` (argument parsing and the clone step),
# against local bare repositories through a logging git, without network.
# Also `install.sh uninstall` stopping before Nix while programs from the
# Nix store run (a fake /proc).
# Also `install.sh uninstall` removing Nix while programs from the Nix store
# run (a fake /proc, a logging nix-installer).
{ pkgs }:
let
cleanup = pkgs.callPackage ./package.nix { };
@@ -388,7 +388,7 @@ pkgs.runCommand "cleanup-check" { nativeBuildInputs = [ cleanup pkgs.jq pkgs.git
has "$res" "gh auth login"
echo "G ok"
# --- H: uninstall: /nix in use ---
# --- H: uninstall: Nix goes while programs from the store still run ---
fresh h
export -f fail has hasnt gone
export root INSTALL
@@ -413,23 +413,42 @@ pkgs.runCommand "cleanup-check" { nativeBuildInputs = [ cleanup pkgs.jq pkgs.git
fake 41 cwd 1 41 /usr/bin/cwd; ln -sfn /nix/store/e $P/41/cwd
fake 42 fd 1 42 /usr/bin/fd; ln -s /nix/store/f/file $P/42/fd/3
fake 43 clean 1 43 /usr/bin/clean
printf '#!/bin/sh\n' > $root/nix-installer; chmod +x $root/nix-installer
mkdir -p $HOME/.config; ln -s $HOME/cfg $HOME/.config/home-manager
uninst() {
(STEAM_FRAME_NIX_PROC=$P; . $INSTALL; ASSUME_YES=1 NIX_INSTALLER_BIN=$root/nix-installer
uninstall_nix() { echo "nix-installer uninstall"; }; cmd_uninstall) 2>&1
# nix-installer: logs its arguments and the lazy-unmount drop-in it sees
D=$root/run-system/nix.mount.d/50-steam-frame-nix-lazy-unmount.conf
export D LOG=$root/log RC=0
printf '#!%s\n[ "$1" = --version ] && exit 0\necho "nix-installer $*" >> $LOG\ncat $D >> $LOG 2>/dev/null || echo "no drop-in" >> $LOG\nexit $RC\n' \
"$(command -v bash)" > $root/nix-installer
chmod +x $root/nix-installer
uninst() { # mounted: 1|0
local m=$1
: > $LOG; rm -rf $HOME/.config; mkdir -p $HOME/.config; ln -s $HOME/cfg $HOME/.config/home-manager
(STEAM_FRAME_NIX_PROC=$P STEAM_FRAME_NIX_SYSTEM_RUNTIME=$root/run-system; . $INSTALL
ASSUME_YES=1 NIX_INSTALLER_BIN=$root/nix-installer
# sudo without root: systemctl only logged
sudo() {
case $1 in -v) return 0 ;; -n) shift ;; esac
echo "sudo $*" >> $LOG
[ "$1" = systemctl ] || "$@"
}
nix_mounted() { [ "$m" = 1 ]; }
cmd_uninstall </dev/null) 2>&1
}
res=$(uninst) && fail "uninstall went on with /nix in use: $res"
res=$(uninst 1) || fail "uninstall failed: $res"
echo "$res"
hasnt "$res" "nix-installer uninstall"
has "$res" "zsh (PID 30, started this uninstall"
for p in "app (PID 40)" "cwd (PID 41)" "fd (PID 42)" "Close these or reboot,"; do has "$res" "$p"; done
for p in "PID 31" "PID 32" "PID 43" "PID $$"; do hasnt "$res" "$p"; done
gone $HOME/.config/home-manager
# closed: Nix is removed
rm -r $P/30 $P/40 $P/41 $P/42; fake $$ bash 1 $$ /usr/bin/bash
res=$(uninst) || fail "uninstall failed: $res"
has "$res" "nix-installer uninstall"
for p in "zsh (PID 30)" "app (PID 40)" "cwd (PID 41)" "fd (PID 42)" "keep running until you log out or reboot"; do has "$res" "$p"; done
for p in "PID 31" "PID 32" "PID 43" "PID $$" "Not done yet"; do hasnt "$res" "$p"; done
log=$(cat $LOG)
has "$log" "nix-installer uninstall --no-confirm"
has "$log" "LazyUnmount=yes" # the drop-in was there while it ran
[ "$(grep -c '^sudo systemctl daemon-reload$' $LOG)" = 2 ] || fail "daemon-reloads: $log"
gone $D ''${D%/*} $HOME/.config/home-manager
# /nix not a mount point: no drop-in
res=$(uninst 0) || fail "uninstall failed: $res"
has "$(cat $LOG)" "no drop-in"
# the uninstaller fails: Nix stays, the drop-in goes
res=$(RC=1 uninst 1) && fail "uninstall succeeded: $res"
has "$res" "its uninstaller failed. Reboot, then run uninstall again"
gone $D $HOME/.config/home-manager
SH
echo "H ok"
touch $out