feat(installer): Offer repair chroot on post-pacstrap failure
Tests / tests (push) Successful in 3m5s
Tests / tests (push) Successful in 3m5s
- offer_chroot(): optional arch-chroot shell into the still-mounted target, from install_cleanup() on success and from the EXIT trap when a step fails after pacstrap (TARGET_INSTALLED flag); temp NOPASSWD sudoers drop-ins are always removed before the shell is opened. - INT/TERM handler re-exits with 128+N: without it bash runs the EXIT trap with $? = 0 for a signal-killed shell and the rc != 0 test would skip the offer. A signal during teardown only aborts the prompt (first) so the unmount still runs; a second forces the exit. - cleanup(): report a failed umount -R / cryptsetup close instead of hiding stderr, so a repair shell leaving busy mounts is visible before reboot. - Document the flow in AGENTS.md.
This commit is contained in:
@@ -74,7 +74,7 @@ The airootfs overlay ships it as `/usr/local/bin/alvin-install` (mode `0755` for
|
||||
locale, login manager, sshd, summary) then a fixed order of `install_*` steps
|
||||
(partition → LUKS → format/mount → pacstrap → fstab → repos → timezone → locale →
|
||||
hostname → network → sshd → user → git/SSH → end-4 dots → mkinitcpio → bootloader →
|
||||
TPM/unl0kr → login manager (none if unl0kr succeeded; with unl0kr selected the login screen only asks for a fallback) → autologin → update (reflector + keyrings + `pacman -Syu`, warn-only) → cleanup). Screens and install steps are
|
||||
TPM/unl0kr → login manager (none if unl0kr succeeded; with unl0kr selected the login screen only asks for a fallback) → autologin → update (reflector + keyrings + `pacman -Syu`, warn-only) → cleanup, which offers an optional `arch-chroot` shell into the target before unmounting; the `EXIT` trap offers the same shell when a step after pacstrap fails or the install is interrupted (INT/TERM re-exit with 128+N so signals count as failure). Screens and install steps are
|
||||
independent — edit one without
|
||||
touching the other. An `EXIT` trap unmounts `$TARGET` and closes `cryptroot` on any exit.
|
||||
- **Package list**: `install_pacstrap` builds the *installed* system's list from the medium's
|
||||
|
||||
@@ -43,20 +43,74 @@ SSHD_APPLIED="false"
|
||||
TPM_DISCLAIMER=""
|
||||
TARGET_DB_SYNCED="false"
|
||||
UPDATE_STATUS="not run"
|
||||
# Flipped once pacstrap succeeds: from then on a failed step still leaves a
|
||||
# chroot-able system, so the EXIT trap offers a repair shell into it.
|
||||
TARGET_INSTALLED="false"
|
||||
# Teardown state: a signal arriving while cleanup() runs must not skip the
|
||||
# unmount - the first one only aborts what is on screen, a second forces exit.
|
||||
CLEANUP_RUNNING="false"
|
||||
SIGNALS_SEEN="0"
|
||||
|
||||
log() { echo "[$(date '+%H:%M:%S')] $*" >>"$LOG"; }
|
||||
|
||||
cleanup() {
|
||||
set +e
|
||||
# Temporary NOPASSWD drop-ins some install steps create; never leave them
|
||||
# behind (EXIT) or expose them to the user's post-install chroot shell.
|
||||
remove_temp_sudoers() {
|
||||
rm -f "${TARGET}/etc/sudoers.d/99-end4-install" \
|
||||
"${TARGET}/etc/sudoers.d/99-ssh-import-id" \
|
||||
"${TARGET}/etc/sudoers.d/99-unl0kr-install" 2>/dev/null
|
||||
umount -R "$TARGET" 2>/dev/null
|
||||
cryptsetup close cryptroot 2>/dev/null
|
||||
"${TARGET}/etc/sudoers.d/99-unl0kr-install" 2>/dev/null || true
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
local rc=$?
|
||||
CLEANUP_RUNNING="true"
|
||||
set +e
|
||||
trap - ERR
|
||||
# A later step failed but the base system is on disk: let the user fix
|
||||
# it from inside before everything is unmounted.
|
||||
if ((rc != 0)) && [[ "$TARGET_INSTALLED" == "true" && -t 0 ]]; then
|
||||
echo ""
|
||||
warn "Installation failed after the base system was installed."
|
||||
offer_chroot "The installed system is still mounted. Chroot into it to fix things?"
|
||||
fi
|
||||
remove_temp_sudoers
|
||||
# A repair shell can leave mounts behind, and the LUKS mapping may still
|
||||
# be open - report a failure instead of hiding it, so the user knows the
|
||||
# target was not cleanly torn down before rebooting.
|
||||
if mountpoint -q "$TARGET" 2>/dev/null; then
|
||||
if ! umount -R "$TARGET"; then
|
||||
log "Cleanup: umount -R $TARGET failed"
|
||||
warn "Some mounts under ${TARGET} are busy - unmounting failed (released at reboot)."
|
||||
fi
|
||||
fi
|
||||
if [[ -e /dev/mapper/cryptroot ]] && ! cryptsetup close cryptroot 2>/dev/null; then
|
||||
log "Cleanup: cryptsetup close cryptroot failed"
|
||||
warn "cryptroot is still open - it is released at reboot."
|
||||
fi
|
||||
true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Signal handler. Without one, bash runs the EXIT trap with $? = 0 when the
|
||||
# shell itself is killed (SIGINT between commands, SIGTERM from a shutdown),
|
||||
# and cleanup()'s rc != 0 test would read that as success and skip the repair
|
||||
# shell. Re-exit with the conventional 128+N status so an interrupted install
|
||||
# takes the same path as a failed step. Note: a SIGINT delivered while a
|
||||
# foreground command is running is consumed together with that command (bash
|
||||
# then suppresses both errexit and this handler) - that case is unchanged.
|
||||
on_signal() {
|
||||
local status="$1"
|
||||
if [[ "$CLEANUP_RUNNING" == "true" && "$SIGNALS_SEEN" == "0" ]]; then
|
||||
SIGNALS_SEEN="1"
|
||||
return 0
|
||||
fi
|
||||
echo "" >&2
|
||||
echo "Interrupted." >&2
|
||||
exit "$status"
|
||||
}
|
||||
trap 'on_signal 130' INT
|
||||
trap 'on_signal 143' TERM
|
||||
|
||||
# ── error surfacing ───────────────────────────────────────────────────────────
|
||||
# Step output normally ends up in $LOG only. These helpers make sure a failure
|
||||
# is ALSO shown on screen, paged through `gum pager` so long outputs stay
|
||||
@@ -811,6 +865,7 @@ install_pacstrap() {
|
||||
>>"$LOG" 2>&1 || warn "cert-sync failed - mono apps may reject TLS certificates."
|
||||
fi
|
||||
|
||||
TARGET_INSTALLED="true"
|
||||
ok "Packages installed."
|
||||
}
|
||||
|
||||
@@ -2337,11 +2392,33 @@ install_cleanup() {
|
||||
gum pager <"$LOG" || true
|
||||
fi
|
||||
echo ""
|
||||
offer_chroot
|
||||
# Last line on screen after the optional log view - the instruction to
|
||||
# reboot must be the final thing the user reads.
|
||||
echo "Run 'reboot' to boot into your new system."
|
||||
}
|
||||
|
||||
# Optional shell inside the installed system. $TARGET (and cryptroot) are
|
||||
# still mounted here - cleanup() only tears them down afterwards.
|
||||
# Also called from the EXIT trap when a step after pacstrap failed ($1 then
|
||||
# carries the failure prompt).
|
||||
offer_chroot() {
|
||||
gum confirm --affirmative "Open chroot" --negative "No thanks" \
|
||||
"${1:-Chroot into the installed system to make changes before rebooting?}" ||
|
||||
return 0
|
||||
remove_temp_sudoers
|
||||
# Copy first so the log is readable from inside (/var/log/alvin-install.log).
|
||||
cp "$LOG" "${TARGET}/var/log/alvin-install.log" 2>/dev/null || true
|
||||
info "Entering ${TARGET} - type 'exit' to leave and finish."
|
||||
log "Chroot: interactive shell opened"
|
||||
# The user's last command may fail; that must not trip errexit/ERR.
|
||||
arch-chroot "$TARGET" /bin/bash || true
|
||||
log "Chroot: shell closed"
|
||||
# Keep the copy in the target current with the lines logged above.
|
||||
cp "$LOG" "${TARGET}/var/log/alvin-install.log" 2>/dev/null || true
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
main() {
|
||||
|
||||
Reference in New Issue
Block a user