#!/bin/bash ################################################################################ # kiosk-firstboot - runs ubuntu-based-kiosk's ./install.sh on the first # boot of a system installed from the custom ISO (iso/build-iso.sh). # # install.sh refuses to run as root and expects a regular sudo user, so # this runs it as the admin account the Ubuntu installer created (the # first regular user, uid 1000), from a copy of the repo in that user's # home - the same layout as a manual `git clone && ./install.sh`, so # re-running ./install.sh later and Advanced -> Upgrade both work as # usual afterwards. # # For the duration of the install only, that user gets passwordless sudo # (nobody has typed a password yet, and in unattended mode nobody will); # the drop-in is removed on every exit path, including the reboot at the # end of install.sh (see ExecStopPost in kiosk-firstboot.service). # # Settings come from /etc/kiosk-firstboot.conf, written at ISO build time: # KIOSK_UNATTENDED=1 take every prompt's default, skip the Core # Settings menu, reboot into the kiosk when done. # KIOSK_UNATTENDED=0 (default) normal interactive install.sh on tty1. # 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 CONF=/etc/kiosk-firstboot.conf STATE_DIR=/var/lib/kiosk-firstboot PENDING="$STATE_DIR/pending" DONE_MARKER="$STATE_DIR/done" SUDOERS_DROPIN=/etc/sudoers.d/90-kiosk-firstboot APT_DROPIN=/etc/apt/apt.conf.d/90-kiosk-firstboot REPO_SRC=/opt/ubuntu-based-kiosk LOG="$STATE_DIR/install.log" OFFLINE_BUNDLE=/opt/kiosk-offline OFFLINE_APT=/usr/local/sbin/kiosk-offline-apt 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" cleanup() { rm -f "$SUDOERS_DROPIN" "$APT_DROPIN" # Offline mode was only for this install - hand apt back to the real # archive so normal updates work once the kiosk is online. if [[ -f "$OFFLINE_FLAG" ]]; then "$OFFLINE_APT" off >/dev/null 2>&1 || true rm -f "$OFFLINE_FLAG" fi if [[ -f "$DONE_MARKER" ]]; then rm -f "$PENDING" fi } if [[ "${1:-}" == "--cleanup" ]]; then cleanup 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). release_tty() { systemctl --no-block start getty@tty1.service 2>/dev/null || true } trap cleanup EXIT trap 'exit 130' INT TERM HUP banner() { clear echo "════════════════════════════════════════════════════════════" echo " Ubuntu Based Kiosk - First Boot Setup" echo "════════════════════════════════════════════════════════════" echo } # Interactive-mode escape hatch when something needs a human: retry, # 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 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$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:" echo " sudo systemctl start kiosk-firstboot" return 1 ;; esac done } # Returns 0 once online, 2 to install from the offline bundle instead # (only when there is one), 1 to give up. wait_for_internet() { local waited=0 until curl -fsS --max-time 10 -o /dev/null https://deb.nodesource.com/ 2>/dev/null \ || curl -fsS --max-time 10 -o /dev/null http://archive.ubuntu.com/ubuntu/ 2>/dev/null; do if (( waited == 0 )); then echo "Waiting for an internet connection (needed for packages," echo "Node.js and Electron)..." if [[ -f "$OFFLINE_BUNDLE/apt/Packages" ]]; then echo "This disk has the offline bundle: installing from it instead" echo "if there's still no internet after ${KIOSK_OFFLINE_WAIT}s." fi fi sleep 10 waited=$((waited + 10)) if [[ -f "$OFFLINE_BUNDLE/apt/Packages" ]] && (( waited >= KIOSK_OFFLINE_WAIT )); then echo "No internet after ${waited}s - installing from the offline bundle on this disk." return 2 fi if (( waited % 120 == 0 )); then echo "Still no internet after ${waited}s. Network status:" networkctl list --no-pager 2>/dev/null || ip -brief addr if [[ "$KIOSK_UNATTENDED" != "1" ]]; then echo "WiFi or static IP? Use the Shell option and edit /etc/netplan/*.yaml, then 'sudo netplan apply'." offer_retry || return 1 fi fi done echo "Internet connection OK." } ################################################################################ # Keep kernel/systemd chatter off the screen the installer is drawing on. dmesg -n 1 2>/dev/null || true chvt 1 2>/dev/null || true export TERM=linux ADMIN_USER="$(getent passwd 1000 | cut -d: -f1)" banner if [[ -z "$ADMIN_USER" ]]; then echo "ERROR: no regular user (uid 1000) found - can't run install.sh." echo "Create one, then: sudo systemctl start kiosk-firstboot" release_tty exit 1 fi if [[ "$ADMIN_USER" == "kiosk" ]]; then echo "ERROR: the admin account is named 'kiosk', which install.sh reserves" echo "for the kiosk's own restricted user. Reinstall with a different username." release_tty exit 1 fi ADMIN_HOME="$(getent passwd "$ADMIN_USER" | cut -d: -f6)" REPO_DEST="$ADMIN_HOME/ubuntu-based-kiosk" if [[ ! -d "$REPO_DEST" ]]; then cp -a "$REPO_SRC" "$REPO_DEST" chmod -R u+w "$REPO_DEST" chown -R "$ADMIN_USER:$ADMIN_USER" "$REPO_DEST" fi echo "Admin user : $ADMIN_USER" echo "Installer : $REPO_DEST/install.sh" echo "Mode : $([[ "$KIOSK_UNATTENDED" == "1" ]] && echo "unattended (defaults, reboots when done)" || echo "interactive")" echo "Log : $LOG" echo # Anything else on a fresh system that wants the dpkg lock (e.g. the # first unattended-upgrades run): wait for it rather than failing. Not # `cloud-init status --wait` - cloud-init's final stage runs after # multi-user.target, which waits for this service: a deadlock. mkdir -p "$STATE_DIR" printf 'DPkg::Lock::Timeout "900";\n' > "$APT_DROPIN" wait_for_internet case $? in 0) ;; 2) # install.sh notices this by itself (lib/config.sh's # offline_mode_active) and unpacks prebuilt node_modules instead # of running npm; cleanup() switches it back off. touch "$OFFLINE_FLAG" if ! "$OFFLINE_APT" on; then echo "ERROR: couldn't switch apt to the offline bundle." release_tty exit 1 fi ;; *) release_tty; exit 1 ;; esac # install.sh's own preflight needs jq; Advanced -> Upgrade needs git. echo "Installing installer prerequisites (jq, git)..." DEBIAN_FRONTEND=noninteractive apt-get update -q >/dev/null DEBIAN_FRONTEND=noninteractive apt-get install -y -q jq git curl >/dev/null cat > "$SUDOERS_DROPIN".tmp </dev/null; then rm -f "$SUDOERS_DROPIN".tmp echo "ERROR: generated sudoers drop-in failed validation." release_tty exit 1 fi mv "$SUDOERS_DROPIN".tmp "$SUDOERS_DROPIN" run_install() { local -a envs=( HOME="$ADMIN_HOME" USER="$ADMIN_USER" LOGNAME="$ADMIN_USER" SHELL=/bin/bash TERM=linux LANG="${LANG:-C.UTF-8}" PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin # config.sh would otherwise derive this from SUDO_USER/whoami. BUILD_USER="$ADMIN_USER" KIOSK_UNATTENDED="$KIOSK_UNATTENDED" KIOSK_PROVISION_DONE_MARKER="$DONE_MARKER" ) # Even interactively: stdout goes through tee, which debconf dialogs # cannot draw on, and nothing install.sh installs needs a debconf answer. envs+=(DEBIAN_FRONTEND=noninteractive) # 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=() 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]}" } attempt=1 while true; do run_install rc=$? if [[ -f "$DONE_MARKER" ]]; then # Reached only when the final "Reboot now?" was declined. echo echo "Kiosk install complete. Reboot to start the kiosk: sudo reboot" cleanup release_tty exit 0 fi 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 exit 1 fi attempt=$((attempt + 1)) echo "Retrying in 60 seconds (attempt $attempt of 3)..." sleep 60 else offer_retry || { release_tty; exit 1; } fi done