From d30f7c7260c20e1d68090c410ee25353fece5195 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 15:33:54 +0000 Subject: [PATCH 1/2] First boot: don't hang forever on a stalled online install; fall back to the offline bundle Unattended installs on an --offline ISO cap the online attempt (default 40 min) and finish from the bundle on timeout or failure. Interactive retry menu gains [O] to install from the bundle. NodeSource curl gets timeouts. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01A97KUggSE3rBL8veDM5Mr4 --- iso/firstboot/kiosk-firstboot | 44 ++++++++++++++++++++++++++++++++--- lib/provision.sh | 2 +- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/iso/firstboot/kiosk-firstboot b/iso/firstboot/kiosk-firstboot index 66fa5f7..90f20d6 100755 --- a/iso/firstboot/kiosk-firstboot +++ b/iso/firstboot/kiosk-firstboot @@ -22,6 +22,10 @@ # KIOSK_OFFLINE_WAIT=N on an ISO built with --offline: seconds to wait # for internet before installing from the bundled # packages instead (default 90). +# KIOSK_ONLINE_TIMEOUT=N on an --offline ISO, unattended: if the online +# install hasn't finished after N seconds (a stalled +# download on a flaky link), stop it and finish from +# the bundled packages instead (default 2400). ################################################################################ set -uo pipefail @@ -40,6 +44,7 @@ OFFLINE_FLAG="$STATE_DIR/offline-mode" KIOSK_UNATTENDED=0 KIOSK_OFFLINE_WAIT=90 +KIOSK_ONLINE_TIMEOUT=2400 # shellcheck source=/dev/null [[ -f "$CONF" ]] && source "$CONF" @@ -61,6 +66,21 @@ if [[ "${1:-}" == "--cleanup" ]]; then exit 0 fi +have_bundle() { [[ -f "$OFFLINE_BUNDLE/apt/Packages" ]]; } + +# Finish this install from the bundled packages instead of the internet +# (cleanup() turns it back off). install.sh notices via lib/config.sh's +# offline_mode_active. +switch_to_offline() { + touch "$OFFLINE_FLAG" + if ! "$OFFLINE_APT" on; then + echo "ERROR: couldn't switch apt to the offline bundle." + rm -f "$OFFLINE_FLAG" + return 1 + fi + return 0 +} + # Make sure tty1 gets its login prompt back. getty@tty1 is ordered after # this unit and normally starts by itself once we exit; this just covers # a getty that was stopped or never queued (e.g. started by hand later). @@ -83,13 +103,15 @@ banner() { # drop to a shell as the admin user to fix it, or give up until next boot. # Returns 0 to retry, 1 to give up. offer_retry() { - local choice + local choice offline_opt="" + have_bundle && [[ ! -f "$OFFLINE_FLAG" ]] && offline_opt=" [O] Install from the offline bundle on this disk" while true; do echo - echo " [R] Retry [S] Shell (as $ADMIN_USER, exit to come back) [Q] Quit to login prompt" + echo " [R] Retry [S] Shell (as $ADMIN_USER, exit to come back) [Q] Quit to login prompt$offline_opt" read -r -p "Choice [R]: " choice case "${choice,,}" in ""|r) return 0 ;; + o) [[ -n "$offline_opt" ]] && switch_to_offline && return 0 ;; s) runuser -l "$ADMIN_USER" ;; q) echo "Setup will run again on next boot, or run it now with:" @@ -227,7 +249,14 @@ run_install() { # runuser rather than sudo -u: sudo would set SUDO_USER=root, which # config.sh reads as "the admin running this tool". - runuser -u "$ADMIN_USER" -- env -i "${envs[@]}" \ + # A stalled download on a flaky connection would otherwise hang an + # unattended install forever: with the offline bundle as a safety net, + # cap the online attempt (timeout signals the whole process group). + local -a cap=() + if [[ "$KIOSK_UNATTENDED" == "1" ]] && have_bundle && [[ ! -f "$OFFLINE_FLAG" ]]; then + cap=(timeout --kill-after=30 "$KIOSK_ONLINE_TIMEOUT") + fi + "${cap[@]}" runuser -u "$ADMIN_USER" -- env -i "${envs[@]}" \ bash -c 'cd "$1" && exec ./install.sh' _ "$REPO_DEST" \ 2>&1 | tee -a "$LOG" return "${PIPESTATUS[0]}" @@ -249,6 +278,15 @@ while true; do echo echo "install.sh did not finish (exit code $rc). Full output: $LOG" if [[ "$KIOSK_UNATTENDED" == "1" ]]; then + # Online attempt failed or stalled: the bundle on this disk is + # always going to work, so use it rather than retry the same thing. + if have_bundle && [[ ! -f "$OFFLINE_FLAG" ]]; then + echo "Online install didn't finish - continuing from the offline bundle on this disk." + if switch_to_offline; then + attempt=$((attempt + 1)) + continue + fi + fi if (( attempt >= 3 )); then echo "Giving up after $attempt attempts; will try again next boot." release_tty diff --git a/lib/provision.sh b/lib/provision.sh index 307a613..cf33cb8 100644 --- a/lib/provision.sh +++ b/lib/provision.sh @@ -127,7 +127,7 @@ provision_install_nodejs() { # The bundle carries NodeSource's own nodejs package. sudo apt install -y nodejs else - curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - + curl -fsSL --connect-timeout 20 --max-time 300 https://deb.nodesource.com/setup_22.x | sudo -E bash - sudo apt install -y nodejs fi fi From 763162fc571ab6ea7e9d5be8ea0688030859eae7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 15:37:34 +0000 Subject: [PATCH 2/2] First boot: detect a stalled online install by silence (10 min), not a 40 min cap Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01A97KUggSE3rBL8veDM5Mr4 --- iso/firstboot/kiosk-firstboot | 46 +++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/iso/firstboot/kiosk-firstboot b/iso/firstboot/kiosk-firstboot index 90f20d6..dca388d 100755 --- a/iso/firstboot/kiosk-firstboot +++ b/iso/firstboot/kiosk-firstboot @@ -22,10 +22,10 @@ # KIOSK_OFFLINE_WAIT=N on an ISO built with --offline: seconds to wait # for internet before installing from the bundled # packages instead (default 90). -# KIOSK_ONLINE_TIMEOUT=N on an --offline ISO, unattended: if the online -# install hasn't finished after N seconds (a stalled -# download on a flaky link), stop it and finish from -# the bundled packages instead (default 2400). +# KIOSK_STALL_TIMEOUT=N on an --offline ISO, unattended: if the install +# prints nothing for N seconds (a stalled download on +# a flaky link), stop it and finish from the bundled +# packages instead (default 600). ################################################################################ set -uo pipefail @@ -44,7 +44,7 @@ OFFLINE_FLAG="$STATE_DIR/offline-mode" KIOSK_UNATTENDED=0 KIOSK_OFFLINE_WAIT=90 -KIOSK_ONLINE_TIMEOUT=2400 +KIOSK_STALL_TIMEOUT=600 # shellcheck source=/dev/null [[ -f "$CONF" ]] && source "$CONF" @@ -250,13 +250,39 @@ run_install() { # runuser rather than sudo -u: sudo would set SUDO_USER=root, which # config.sh reads as "the admin running this tool". # A stalled download on a flaky connection would otherwise hang an - # unattended install forever: with the offline bundle as a safety net, - # cap the online attempt (timeout signals the whole process group). - local -a cap=() + # unattended install forever. With the offline bundle as a safety net, + # watch the log: no new output for KIOSK_STALL_TIMEOUT seconds means + # stuck, so the whole process group is stopped (the caller then + # finishes from the bundle). if [[ "$KIOSK_UNATTENDED" == "1" ]] && have_bundle && [[ ! -f "$OFFLINE_FLAG" ]]; then - cap=(timeout --kill-after=30 "$KIOSK_ONLINE_TIMEOUT") + touch "$LOG" + setsid bash -c ' + runuser -u "$1" -- env -i "${@:4}" bash -c "cd \"\$1\" && exec ./install.sh" _ "$2" 2>&1 | tee -a "$3" + exit "${PIPESTATUS[0]}" + ' _ "$ADMIN_USER" "$REPO_DEST" "$LOG" "${envs[@]}" & + local pid=$! + ( + while kill -0 "$pid" 2>/dev/null; do + sleep 20 + age=$(( $(date +%s) - $(stat -c %Y "$LOG") )) + if (( age > KIOSK_STALL_TIMEOUT )); then + echo + echo "No output for ${age}s - the online install looks stuck; stopping it." + kill -TERM -- "-$pid" 2>/dev/null + sleep 30 + kill -KILL -- "-$pid" 2>/dev/null + break + fi + done + ) & + local watcher=$! + wait "$pid" + local rc=$? + kill "$watcher" 2>/dev/null + wait "$watcher" 2>/dev/null + return "$rc" fi - "${cap[@]}" runuser -u "$ADMIN_USER" -- env -i "${envs[@]}" \ + runuser -u "$ADMIN_USER" -- env -i "${envs[@]}" \ bash -c 'cd "$1" && exec ./install.sh' _ "$REPO_DEST" \ 2>&1 | tee -a "$LOG" return "${PIPESTATUS[0]}"