Fix first-boot service never starting on Ubuntu 26.04
Reported: installed from the ISO, files present, but the kiosk setup never appeared on first boot. Cause: kiosk-firstboot.service had After=cloud-final.service, and on 26.04 cloud-final.service is After=multi-user.target - while multi-user.target, which wants kiosk-firstboot, is implicitly After= it. An ordering cycle; systemd breaks it at boot by deleting a job, and kiosk-firstboot's was the one to go. systemd-analyze verify against the 26.04.1 ISO's own units reproduces it: Found ordering cycle: kiosk-firstboot.service/start after cloud-final.service/start after multi-user.target/start - after kiosk-firstboot.service - Drop After=cloud-final.service. The script's `cloud-init status --wait` goes too: cloud-final waits for multi-user.target, which waits for this service - it would deadlock. DPkg::Lock::Timeout already covers what it was guarding against (dpkg lock contention). - Drop Conflicts=getty@tty1.service: getty.target pulls getty@tty1 into the same boot transaction, and a conflict between two wanted jobs is also resolved by dropping one. Before=getty@tty1 alone keeps tty1's login prompt waiting until this oneshot finishes. Verified: systemd-analyze verify of kiosk-firstboot + multi-user.target + cloud-final + cloud-init.target + getty@tty1 + graphical.target in the 26.04.1 server filesystem - old unit reports the cycle, fixed unit is clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
This commit is contained in:
@@ -61,8 +61,9 @@ if [[ "${1:-}" == "--cleanup" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Hand tty1 back to a normal login prompt (the unit Conflicts= with
|
||||
# getty@tty1 so it can own the console; systemd won't restart it alone).
|
||||
# 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
|
||||
}
|
||||
@@ -167,12 +168,10 @@ echo "Mode : $([[ "$KIOSK_UNATTENDED" == "1" ]] && echo "unattended (defau
|
||||
echo "Log : $LOG"
|
||||
echo
|
||||
|
||||
# cloud-init and the first apt/unattended-upgrades run on a fresh system
|
||||
# both want the dpkg lock; let them finish rather than racing them.
|
||||
if command -v cloud-init &>/dev/null; then
|
||||
echo "Waiting for cloud-init to finish..."
|
||||
cloud-init status --wait >/dev/null 2>&1 || true
|
||||
fi
|
||||
# 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"
|
||||
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
# Installed by the custom ISO (iso/build-iso.sh) and enabled from its
|
||||
# autoinstall late-commands. Runs install.sh once, on tty1, the first
|
||||
# time the freshly installed system boots - takes tty1 over from getty
|
||||
# so an interactive install has a keyboard and screen, then hands tty1
|
||||
# back. /var/lib/kiosk-firstboot/pending is removed once the install
|
||||
# time the freshly installed system boots - ordered before getty@tty1,
|
||||
# so the login prompt only appears on tty1 once this has finished and
|
||||
# an interactive install has the keyboard and screen to itself. /var/lib/kiosk-firstboot/pending is removed once the install
|
||||
# finishes, so this is a no-op on every boot after that; an interrupted
|
||||
# or failed install leaves it in place and simply runs again next boot.
|
||||
#
|
||||
# Ordering, learned the hard way on 26.04:
|
||||
# - NOT After=cloud-final.service: cloud-final is After=multi-user.target,
|
||||
# and multi-user.target (which wants this unit) is After= it - an
|
||||
# ordering cycle, which systemd "fixes" at boot by dropping a job: this
|
||||
# one, so the install silently never ran. (The script doesn't wait for
|
||||
# cloud-init either, for the same reason - it would deadlock.)
|
||||
# - NOT Conflicts=getty@tty1: getty.target pulls getty@tty1 into the same
|
||||
# boot transaction, and systemd resolves a conflict between two wanted
|
||||
# jobs by dropping one. Before= alone is enough - getty@tty1 waits for
|
||||
# this oneshot to finish.
|
||||
[Unit]
|
||||
Description=Ubuntu Based Kiosk first-boot install
|
||||
After=network-online.target systemd-user-sessions.service cloud-final.service plymouth-quit-wait.service
|
||||
After=network-online.target systemd-user-sessions.service plymouth-quit-wait.service
|
||||
Wants=network-online.target
|
||||
Before=getty@tty1.service
|
||||
Conflicts=getty@tty1.service
|
||||
ConditionPathExists=/var/lib/kiosk-firstboot/pending
|
||||
|
||||
[Service]
|
||||
|
||||
Reference in New Issue
Block a user