diff options
Diffstat (limited to 'installer')
| -rwxr-xr-x | installer/archangel | 103 | ||||
| -rw-r--r-- | installer/lib/btrfs.sh | 11 |
2 files changed, 101 insertions, 13 deletions
diff --git a/installer/archangel b/installer/archangel index 065772c..56290d9 100755 --- a/installer/archangel +++ b/installer/archangel @@ -1374,16 +1374,87 @@ cleanup() { info "Cleanup complete." } -# Trap target for ERR / INT / TERM during install_zfs and +# The trap set that routes a mid-install failure to install_failure_cleanup. +# EXIT is the one that does the work. Every install step is a function, and +# bash never delivers an ERR trap to a command failing inside a called +# function, so errexit used to end the script with the cleanup unrun. The +# `|| error "..."` shape leaves through a plain `exit 1`, which no ERR trap +# sees either. Both left /mnt mounted and the pool imported for the next +# attempt to trip over (2026-08-06, 2026-09-12). ERR still fires for a +# failure directly in install_zfs/install_btrfs, and INT and TERM cover +# Ctrl-C and a kill. The success path disarms all four before its own +# cleanup, whose non-zero exits are expected. +# +# I deliberately don't turn on errtrace (set -E) to widen ERR. It hands the +# trap to command substitutions too, so a masked failing `$(...)` ran the +# whole cleanup inside that subshell and unmounted /mnt while the install +# carried on in the parent. +arm_failure_trap() { + trap 'install_failure_cleanup' ERR INT TERM EXIT +} + +# How many one-second tries the failure cleanup gives a busy pool export or +# LUKS close. When a signal interrupts pacstrap, pacman's children in the +# chroot can still be exiting as the cleanup runs, so the first export fails +# busy and a manual one seconds later succeeds (VM, 2026-09-13). +CLEANUP_BUSY_ATTEMPTS="${CLEANUP_BUSY_ATTEMPTS:-15}" + +# Run a command until it succeeds, up to <attempts> times, sleeping one +# second between tries. Returns 1 if the last try still fails. +# +# Usage: retry_busy <attempts> <command> [args...] +retry_busy() { + local attempts="$1" i + shift + for ((i = 1; i <= attempts; i++)); do + "$@" && return 0 + ((i < attempts)) && sleep 1 + done + return 1 +} + +# Close this install's LUKS mappings and succeed only once none is left. +# close_luks_container swallows cryptsetup's errors, so a busy close has to +# be detected by looking for the mapping afterwards. +close_luks_and_confirm() { + btrfs_close_encryption 2>/dev/null + ! luks_mappings_open +} + +disarm_failure_trap() { + trap - ERR INT TERM EXIT +} + +# Trap target for ERR / INT / TERM / EXIT during install_zfs and # install_btrfs. Captures the failing exit code first, disarms the -# trap to prevent recursion, clears sensitive variables, and -# dispatches to the right per-filesystem cleanup before exiting via -# error(). All cleanup steps swallow their own errors — partial -# state is expected when this fires mid-install, so individual tool -# failures are not fatal. +# trap to prevent recursion (its own error() exit would re-fire EXIT), +# clears sensitive variables, and dispatches to the right +# per-filesystem cleanup before exiting via error(). All cleanup +# steps swallow their own errors — partial state is expected when +# this fires mid-install, so individual tool failures are not fatal. install_failure_cleanup() { local exit_code=$? - trap - ERR INT TERM + disarm_failure_trap + + # A SIGTERM sent to the whole process group (kill -- -PGID, timeout, a + # service stop) also kills the tee init_logging put on stdout. With the + # tee gone, the first warn below raised SIGPIPE and killed the shell + # before anything was unmounted. Ctrl-C doesn't hit this, because bash + # starts a process substitution with SIGINT ignored. Ignore SIGPIPE, and + # turn errexit off so a failed write to the dead pipe can't end the + # cleanup either. `local -` scopes that to this function: the cleanup + # normally leaves through error(), but a caller that gets control back + # (the unit tests) would otherwise keep running with errexit off. + local - + trap '' PIPE + set +e + + # disarm_failure_trap put INT and TERM back to their defaults, so a + # second Ctrl-C or SIGTERM during the busy retries below (several silent + # seconds, easy to read as a hang) killed the cleanup halfway and left + # the pool imported. Ignore both until the cleanup exits. It's bounded + # by CLEANUP_BUSY_ATTEMPTS, and SIGKILL still stops it. + trap '' INT TERM ROOT_PASSWORD="" ZFS_PASSPHRASE="" @@ -1402,7 +1473,7 @@ install_failure_cleanup() { umount "$EFI_DIR" 2>/dev/null || umount -l "$EFI_DIR" 2>/dev/null || true umount -R "$MNTPOINT" 2>/dev/null || umount -R -l "$MNTPOINT" 2>/dev/null || true if zpool list "$POOL_NAME" >/dev/null 2>&1; then - zpool export "$POOL_NAME" 2>/dev/null \ + retry_busy "$CLEANUP_BUSY_ATTEMPTS" zpool export "$POOL_NAME" 2>/dev/null \ || zpool export -f "$POOL_NAME" 2>/dev/null \ || true fi @@ -1410,7 +1481,13 @@ install_failure_cleanup() { btrfs) umount "$EFI_DIR" 2>/dev/null || umount -l "$EFI_DIR" 2>/dev/null || true btrfs_cleanup 2>/dev/null || true - btrfs_close_encryption 2>/dev/null || true + # btrfs_cleanup only unmounts the subvolumes it mounted. A + # pacstrap interrupted mid-transaction can leave its /proc, /sys + # and /dev bind mounts under the root, which keep it busy, so the + # LUKS mapping can't close. Same recursive-then-lazy fallback as + # the ZFS branch, before closing encryption. + umount -R "$MNTPOINT" 2>/dev/null || umount -R -l "$MNTPOINT" 2>/dev/null || true + retry_busy "$CLEANUP_BUSY_ATTEMPTS" close_luks_and_confirm || true ;; esac @@ -1531,7 +1608,7 @@ install_zfs() { # cleanup (unmount /mnt + export pool) so the live ISO is left in a # state where the user can re-run the installer without manual # intervention. - trap 'install_failure_cleanup' ERR INT TERM + arm_failure_trap partition_disks create_zfs_pool @@ -1553,7 +1630,7 @@ install_zfs() { # Disarm the failure trap before the success-path cleanup. The # success cleanup may emit non-zero exit codes that we don't want # to interpret as "installation failed". - trap - ERR INT TERM + disarm_failure_trap cleanup print_summary } @@ -1565,7 +1642,7 @@ install_zfs() { install_btrfs() { # Arm the failure trap before any destructive operation. See the # matching block in install_zfs() for the rationale. - trap 'install_failure_cleanup' ERR INT TERM + arm_failure_trap local btrfs_devices=() @@ -1605,7 +1682,7 @@ install_btrfs() { # Disarm the failure trap before the success-path cleanup. See # the matching block in install_zfs() for the rationale. - trap - ERR INT TERM + disarm_failure_trap # Cleanup btrfs_cleanup diff --git a/installer/lib/btrfs.sh b/installer/lib/btrfs.sh index 67c96a0..96ad29e 100644 --- a/installer/lib/btrfs.sh +++ b/installer/lib/btrfs.sh @@ -153,6 +153,17 @@ close_luks_containers() { done } +# Succeed when any of this install's LUKS mappings is still open. Names +# follow get_luks_devices: the bare LUKS_MAPPER_NAME for the first disk, +# then LUKS_MAPPER_NAME1, 2, ... MAPPER_DIR is overridable for tests. +luks_mappings_open() { + local dir="${MAPPER_DIR:-/dev/mapper}" m + for m in "$dir/$LUKS_MAPPER_NAME" "$dir/$LUKS_MAPPER_NAME"[0-9]*; do + [[ -e "$m" ]] && return 0 + done + return 1 +} + # Get list of opened LUKS mapper devices get_luks_devices() { local count="$1" |
