From f03bf06742a2ed4535d326e3220c685bca5aa0ed Mon Sep 17 00:00:00 2001 From: Emir Beganovic Date: Fri, 9 Oct 2026 23:26:20 +0200 Subject: [PATCH] Stop the boot from waiting on a black screen for the console to answer About 1 boot in 10 of an installed system stops on a black screen at "Starting Switch Root" until a key is pressed. systemd 257 and later asks the console for its size with ANSI escape sequences when PID 1 starts and waits for the reply; with Plymouth holding the VT the reply sometimes never comes. Measured in QEMU on repeated boots of one install: 3 hangs in 30 and 1 in 40 on the prebuilt UKI, 7 in 40 on the stock initramfs, every one of them ended by pressing Enter; none in 40 with a serial console, none in 40 with plymouth.enable=0, and none in 80 with systemd.tty.term.console=dumb, which makes systemd skip the query. systemd fixed two variants of this in v261; Arch's 261.2 still shows it. The parameter goes where the last Plymouth boot fix went (3f725531, initramfs_async=0): into the packaged omarchy-defaults.conf for fresh installs, and a migration rebuilds the boot image on existing systems, once per machine, only where the booted command line lacks it. Nothing visible changes under "quiet splash". Refs #2931 --- etc/limine-entry-tool.d/omarchy-defaults.conf | 9 +++ migrations/1791581004.sh | 31 +++++++++ .../console-no-ansi-query-migration-test.sh | 66 +++++++++++++++++++ test/shell.d/limine-defaults-test.sh | 4 ++ 4 files changed, 110 insertions(+) create mode 100644 migrations/1791581004.sh create mode 100644 test/shell.d/console-no-ansi-query-migration-test.sh diff --git a/etc/limine-entry-tool.d/omarchy-defaults.conf b/etc/limine-entry-tool.d/omarchy-defaults.conf index ec0fa96f..1e421365 100644 --- a/etc/limine-entry-tool.d/omarchy-defaults.conf +++ b/etc/limine-entry-tool.d/omarchy-defaults.conf @@ -9,6 +9,15 @@ KERNEL_CMDLINE[default]+=" quiet splash loglevel=0 systemd.show_status=false rd. # is fixed upstream. KERNEL_CMDLINE[default]+=" initramfs_async=0" +# systemd 257 and later asks the console for its size with ANSI escape +# sequences when PID 1 starts, and waits for the reply. With Plymouth holding +# the VT the reply sometimes never comes, and the boot sits on a black screen +# at "Starting Switch Root" until a key is pressed: about 1 boot in 10. A dumb +# $TERM for the console makes systemd skip the query. Nothing visible changes +# under "quiet splash". Drop it once a systemd release boots clean in a +# repeated boot test. +KERNEL_CMDLINE[default]+=" systemd.tty.term.console=dumb" + CUSTOM_UKI_NAME="omarchy" ENABLE_LIMINE_FALLBACK=yes diff --git a/migrations/1791581004.sh b/migrations/1791581004.sh new file mode 100644 index 00000000..c95a74ff --- /dev/null +++ b/migrations/1791581004.sh @@ -0,0 +1,31 @@ +echo "Stop the boot from waiting on a black screen for the console to answer" + +# systemd asks the console for its size when PID 1 starts and waits for the +# reply. With Plymouth holding the screen the reply sometimes never comes, and +# about 1 boot in 10 sits on a black screen until a key is pressed. +# omarchy-defaults.conf now carries systemd.tty.term.console=dumb, which makes +# systemd skip the query. The package has already written that file by the +# time this runs; the boot image still holds the old command line until it is +# rebuilt. + +defaults_conf="${OMARCHY_LIMINE_DEFAULTS_CONF:-/etc/limine-entry-tool.d/omarchy-defaults.conf}" +running_cmdline="${OMARCHY_RUNNING_CMDLINE:-/proc/cmdline}" +rebuild_marker="${OMARCHY_LIMINE_REBUILD_MARKER:-/var/lib/omarchy/migrations/1791581004}" +param="systemd.tty.term.console=dumb" + +omarchy-cmd-present limine-mkinitcpio || exit 0 +[[ -f $defaults_conf && -r $running_cmdline ]] || exit 0 + +# The running kernel keeps its old command line until reboot, so a marker +# records the machine-wide rebuild: another user's migration must not repeat +# it before then, while a missing marker still retries an interrupted rebuild. +[[ ! -e $rebuild_marker ]] || exit 0 + +# A machine that boots with it already, or whose defaults were edited to leave +# it out, has nothing to rebuild. +[[ " $(<"$running_cmdline") " != *" $param "* ]] || exit 0 +grep -Eq "^KERNEL_CMDLINE\[default\]\+=\".*${param//./\\.}.*\"" "$defaults_conf" || exit 0 + +echo "The booted kernel is missing $param; rebuilding the boot image" +sudo limine-mkinitcpio +sudo install -Dm644 /dev/null "$rebuild_marker" diff --git a/test/shell.d/console-no-ansi-query-migration-test.sh b/test/shell.d/console-no-ansi-query-migration-test.sh new file mode 100644 index 00000000..d0c2b1d0 --- /dev/null +++ b/test/shell.d/console-no-ansi-query-migration-test.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# +# The migration that puts systemd.tty.term.console=dumb into effect on an +# existing install: the package has already written the parameter into +# omarchy-defaults.conf, so what is left is rebuilding the boot image, once per +# machine, and only where it is needed. + +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh" + +migration="$ROOT/migrations/1791581004.sh" +test_dir=$(mktemp -d) +trap 'rm -rf "$test_dir"' EXIT +stub_bin="$test_dir/bin" +mkdir -p "$stub_bin" + +# sudo just drops the prefix; limine-mkinitcpio records that it ran. +printf '#!/bin/bash\nexec "$@"\n' >"$stub_bin/sudo" +printf '#!/bin/bash\necho rebuilt >>"${REBUILDS:?}"\n' >"$stub_bin/limine-mkinitcpio" +chmod +x "$stub_bin"/* + +old_cmdline="root=PARTUUID=1 rw quiet splash initramfs_async=0" +new_cmdline="$old_cmdline systemd.tty.term.console=dumb" + +run() { # booted cmdline, defaults conf + : >"$test_dir/rebuilds" + printf '%s\n' "$1" >"$test_dir/cmdline" + PATH="$stub_bin:$ROOT/bin:$PATH" REBUILDS="$test_dir/rebuilds" \ + OMARCHY_RUNNING_CMDLINE="$test_dir/cmdline" OMARCHY_LIMINE_DEFAULTS_CONF="$2" \ + OMARCHY_LIMINE_REBUILD_MARKER="$test_dir/marker" bash "$migration" >/dev/null +} +rebuilds() { wc -l <"$test_dir/rebuilds"; } + +run "$old_cmdline" "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf" +[[ $(rebuilds) == 1 && -e $test_dir/marker ]] || + fail "a machine booted without the parameter did not get its boot image rebuilt" +pass "a machine booted without the parameter gets its boot image rebuilt, with the packaged defaults" + +run "$old_cmdline" "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf" +[[ $(rebuilds) == 0 ]] || fail "the rebuild ran a second time before a reboot" +pass "the rebuild runs once per machine, not once per user" + +rm -f "$test_dir/marker" +run "$new_cmdline" "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf" +[[ $(rebuilds) == 0 && ! -e $test_dir/marker ]] || fail "a machine that already boots with the parameter was rebuilt" +pass "a machine that already boots with the parameter is left alone" + +grep -v "tty.term.console" "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf" >"$test_dir/edited.conf" +run "$old_cmdline" "$test_dir/edited.conf" +[[ $(rebuilds) == 0 ]] || fail "a machine whose defaults leave the parameter out was rebuilt anyway" +pass "a machine whose defaults were edited to leave the parameter out is left alone" + +# No Limine: the stand-in goes, and the PATH holds only what the migration +# needs, so a limine-mkinitcpio installed on the machine running this test is +# not found either. +rm "$stub_bin/limine-mkinitcpio" +mkdir -p "$test_dir/tools" +for tool in bash grep install cat; do ln -s "$(command -v "$tool")" "$test_dir/tools/$tool"; done +: >"$test_dir/rebuilds" +printf '%s\n' "$old_cmdline" >"$test_dir/cmdline" +PATH="$stub_bin:$ROOT/bin:$test_dir/tools" REBUILDS="$test_dir/rebuilds" \ + OMARCHY_RUNNING_CMDLINE="$test_dir/cmdline" OMARCHY_LIMINE_DEFAULTS_CONF="$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf" \ + OMARCHY_LIMINE_REBUILD_MARKER="$test_dir/marker" "$test_dir/tools/bash" "$migration" >/dev/null +[[ ! -e $test_dir/marker ]] || fail "a machine without Limine was marked as rebuilt" +pass "a machine without Limine is left alone" diff --git a/test/shell.d/limine-defaults-test.sh b/test/shell.d/limine-defaults-test.sh index 21adc94f..f69e9535 100644 --- a/test/shell.d/limine-defaults-test.sh +++ b/test/shell.d/limine-defaults-test.sh @@ -10,6 +10,10 @@ grep -Fq 'KERNEL_CMDLINE[default]+=" initramfs_async=0"' "$packaged_defaults" || fail "the packaged Limine defaults still unpack the initramfs synchronously" pass "packaged Limine defaults keep Plymouth alive at the LUKS prompt" +grep -Fxq 'KERNEL_CMDLINE[default]+=" systemd.tty.term.console=dumb"' "$packaged_defaults" || + fail "the packaged Limine defaults let PID 1 wait for the console to answer a size query" +pass "packaged Limine defaults keep PID 1 from waiting on the console" + grep -Fxq 'BOOT_ORDER="linux-t2, linux-omarchy, linux-omarchy-*, *, *fallback, Snapshots"' "$packaged_defaults" || fail "packaged Limine defaults protect T2 Macs and prefer the exact Omarchy kernel elsewhere" pass "packaged Limine defaults protect T2 Macs and prefer the exact Omarchy kernel elsewhere"