From 4ed41ce84b2a6eb925525879b56de7d0ab89f261 Mon Sep 17 00:00:00 2001 From: navigator Date: Sat, 3 Oct 2026 01:29:35 +0000 Subject: [PATCH] fix: postinst never restarts the console's getty on an upgrade When inithooks.service was not running, postinst restarted getty@tty1.service and container-getty@1.service "to make sure" a login prompt was there, and the restart ended the session on that tty: the maintainer ran apt upgrade from the container console (pct console, tty1) and was logged out mid-upgrade. A getty is now only ever started, only when it is inactive, only one the machine has (systemctl cat; a container has no getty@tty1), and only once the first boot is over, inithooks.service neither running nor queued, asked the way libinithooks/init_lock.py asks (is-active, then a start job in list-jobs). The branch for an inithooks that was running before the upgrade is unchanged. tests/test-packaging.bats runs the packaged postinst with systemctl answering from the environment. --- debian/changelog | 16 ++++++ debian/postinst | 29 +++++++++-- tests/test-packaging.bats | 102 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 3 deletions(-) diff --git a/debian/changelog b/debian/changelog index 07ebcd8..df1590b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,19 @@ +inithooks (2.3.6+keel20) trixie; urgency=medium + + * postinst no longer restarts the console's getty on an upgrade. When + inithooks.service was not running it restarted getty@tty1.service and + container-getty@1.service "to make sure" a login prompt was there, + and the restart ended the session on that tty: the maintainer ran + apt upgrade from the container console (pct console, tty1) and was + logged out mid-upgrade (2026-10-03). A getty is now only ever + started, only when it is inactive, only one the machine has (a + container has no getty@tty1), and only once the first boot is over, + inithooks.service neither running nor queued, asked the way + libinithooks/init_lock.py asks. The branch for an inithooks that was + running before the upgrade is unchanged. + + -- Marcos Mendez Sat, 03 Oct 2026 09:00:00 +0000 + inithooks (2.3.6+keel19) trixie; urgency=medium * The first boot asks the machine's fully qualified domain name, in a diff --git a/debian/postinst b/debian/postinst index a3b26a9..abf97e1 100755 --- a/debian/postinst +++ b/debian/postinst @@ -21,9 +21,32 @@ if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] \ systemctl restart turnkey-init-fence.service fi else - # getty1 should already be running if inithooks wasn't, but let's - # make sure - systemctl restart getty@tty1.service container-getty@1.service 2>/dev/null || true + # A login prompt should be on the console already. Make sure + # there is one without touching the one there is: a restart of + # the getty ends the session on it, and an operator running apt + # upgrade from the console (tty1, pct console) was logged out + # mid-upgrade (2026-10-03). So a getty is only ever started, + # only when it is inactive, only one the machine has (a + # container has no getty@tty1), and only once the first boot is + # over: inithooks.service neither running nor queued, as + # libinithooks/init_lock.py asks (is-active, then a start job + # in list-jobs), since the wizard draws on that tty. + state=$(systemctl is-active inithooks.service 2>/dev/null || true) + case "$state" in + active|activating|reloading|deactivating) state=running ;; + *) if [ -n "$(systemctl list-jobs --no-legend inithooks.service 2>/dev/null)" ]; then + state=queued + else + state=stopped + fi ;; + esac + if [ "$state" = stopped ]; then + for unit in getty@tty1.service container-getty@1.service; do + systemctl cat "$unit" >/dev/null 2>&1 || continue + systemctl is-active --quiet "$unit" \ + || systemctl start "$unit" 2>/dev/null || true + done + fi fi else # fresh install ($2 is empty) - enable services diff --git a/tests/test-packaging.bats b/tests/test-packaging.bats index 267600d..eda1bea 100644 --- a/tests/test-packaging.bats +++ b/tests/test-packaging.bats @@ -158,6 +158,108 @@ prepare_postinst() { [ -e "$CRON_HOURLY/other-job" ] } +# getty_answers: systemctl answers from the environment, for the upgrade +# branch of postinst: INITHOOKS_STATE is what is-active says of +# inithooks.service, INITHOOKS_JOB a queued job, GETTY_UNITS the getty units +# the machine has (systemctl cat), GETTY_ACTIVE the ones running. +getty_answers() { + stub systemctl 'case "$1" in + is-active) + unit=${2#--quiet}; unit=${unit:-$3} + case "$unit" in + inithooks.service) echo "${INITHOOKS_STATE:-inactive}" + [ "${INITHOOKS_STATE:-inactive}" = active ] ;; + *) [[ " ${GETTY_ACTIVE-} " == *" $unit "* ]] ;; + esac ;; + list-jobs) printf "%s" "${INITHOOKS_JOB-}" ;; + cat) [[ " ${GETTY_UNITS-} " == *" $2 "* ]] ;; + *) exit 0 ;; +esac' +} + +upgrade_postinst() { + run sh "$BATS_TEST_TMPDIR/postinst" configure 2.3.6+keel18 + [ "$status" -eq 0 ] +} + +# 2026-10-03: the maintainer ran apt upgrade from the container console and +# was logged out mid-upgrade; postinst restarted the getty of the console +# the upgrade was running on, "to make sure" it was running. +@test "an upgrade never restarts a getty" { + prepare_postinst + getty_answers + export GETTY_UNITS="getty@tty1.service container-getty@1.service" + export GETTY_ACTIVE="getty@tty1.service container-getty@1.service" + upgrade_postinst + run ! grep -qE "^(start|restart)" "$STUBS/systemctl.calls" +} + +@test "the packaged postinst has no getty restart left in it" { + packaged_scripts + run ! grep -E 'restart.*getty' "$DEBIAN/postinst" +} + +@test "an upgrade starts a getty that is inactive, among the units the machine has" { + prepare_postinst + getty_answers + # a container: container-getty@1 only, not running + export GETTY_UNITS="container-getty@1.service" + upgrade_postinst + [ "$(grep -E '^start' "$STUBS/systemctl.calls")" = "start container-getty@1.service" ] +} + +@test "an upgrade starts each getty the machine has and does not run" { + prepare_postinst + getty_answers + export GETTY_UNITS="getty@tty1.service container-getty@1.service" + export GETTY_ACTIVE="container-getty@1.service" + upgrade_postinst + [ "$(grep -E '^start' "$STUBS/systemctl.calls")" = "start getty@tty1.service" ] +} + +@test "an upgrade touches no getty while the first boot runs" { + prepare_postinst + getty_answers + export GETTY_UNITS="getty@tty1.service" + export INITHOOKS_STATE=activating + upgrade_postinst + run ! grep -qE "^(start|restart)" "$STUBS/systemctl.calls" +} + +@test "an upgrade touches no getty while the first boot is queued" { + prepare_postinst + getty_answers + export GETTY_UNITS="getty@tty1.service" + export INITHOOKS_JOB="12 inithooks.service start waiting" + upgrade_postinst + run ! grep -qE "^(start|restart)" "$STUBS/systemctl.calls" +} + +@test "an upgrade with inithooks running before it restarts inithooks, as before" { + prepare_postinst + getty_answers + export GETTY_UNITS="getty@tty1.service" + # the marker preinst leaves: the copy run here reads the real /run, so + # the path is pointed at a scratch one, the way the cron path is + sed -i "s|/run/inithooks-was-active|$BATS_TEST_TMPDIR/inithooks-was-active|g" \ + "$BATS_TEST_TMPDIR/postinst" + touch "$BATS_TEST_TMPDIR/inithooks-was-active" + upgrade_postinst + grep -qx "restart inithooks.service" "$STUBS/systemctl.calls" + [ ! -e "$BATS_TEST_TMPDIR/inithooks-was-active" ] + run ! grep -q getty "$STUBS/systemctl.calls" +} + +@test "a fresh install touches no getty" { + prepare_postinst + getty_answers + export GETTY_UNITS="getty@tty1.service" + run sh "$BATS_TEST_TMPDIR/postinst" configure + [ "$status" -eq 0 ] + run ! grep -q getty "$STUBS/systemctl.calls" + grep -qx "enable inithooks.service" "$STUBS/systemctl.calls" +} + @test "a system that never had the job installs cleanly" { prepare_postinst run sh "$BATS_TEST_TMPDIR/postinst" configure