mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 01:00:13 +02:00
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:
1 parent
b852a1eb07
commit
269321ea92
2 files changed
+90
-78
No files matched your search
+53
-60
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user