From 0900855a2848ccd295573647a096de5aeebc4cc1 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 12 Aug 2026 16:42:37 +0200 Subject: [PATCH] Stop hot-reloading the shell when a package upgrade rewrites it (#6751) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quickshell watches the QML it loaded and reloads on change, so pacman replacing /usr/share/omarchy/shell mid-transaction makes the running shell reload against a half-written tree. That reload fails, and a failure that reaches the config load is not harmless: it raises the reload popup, which is a second engine generation. EngineGeneration::currentGeneration() returns null unless exactly one exists, so the IPC kill that omarchy-update sends moments later takes the QCoreApplication::exit(0) branch instead of the generation's own quit, and Quickshell tears the QML graph down after deleting the QGuiApplication. The first GUI resource touched on the way out aborts: FATAL: QPixmap: Must construct a QGuiApplication before a QPixmap The user gets the crash dialog after an update and a coredump per occurrence. Reported in #6748 with 3 crashes across 10 updates, always following a failed reload. Fixing this in omarchy-update — stopping the shell around the pacman step — would cover one caller and cost the polkit agent and the notification server for the length of the transaction, which the migrations that run next still notify through. It would also have to carry omarchy-restart-shell's refusal to restart a locked session, or reintroduce the hazard that refusal exists for. And omarchy-update is not the only thing that rewrites the tree. The pacman guard turns away a bare pacman -Syu, but nothing turns away a targeted pacman -S omarchy, a pacman -U of a locally built package, the documented OMARCHY_ALLOW_DIRECT_PACMAN bypass, omarchy-dev-pkg-test, or a checkout in a dev-linked tree. Turn the watcher off instead. Omarchy has never reloaded through it: omarchy-restart-shell is what picks up QML changes, and config and plugin changes go through the shell's own IPC. Third-party plugin hot reload is PluginRegistry's own inotifywait and FileView watches its own files, neither of which this touches — QuickshellSettings::watchFiles() gates the config scanner and nothing else. The popup goes off with it, because QML can still ask for a reload directly and leave the same extra generation behind. Environment reaches Quickshell only at launch, so the update that delivers this still runs under a watching shell. It takes effect from the next one. Verified against an isolated instance: breaking a config in place and then sending the IPC kill reproduces the FATAL, and it stops with either variable set. QS_DISABLE_FILE_WATCHER also keeps the failed reload from happening at all. Co-authored-by: Claude Opus 5 (1M context) --- bin/omarchy-launch-shell | 8 +++++++- test/shell.d/launch-shell-test.sh | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/bin/omarchy-launch-shell b/bin/omarchy-launch-shell index e3e13d05..c183eb0b 100755 --- a/bin/omarchy-launch-shell +++ b/bin/omarchy-launch-shell @@ -9,8 +9,14 @@ # # Backgrounded because bash defers a trap until a foreground command returns but # interrupts wait. systemd-cat execs, so the job is Quickshell itself. +# +# Quickshell's own reloading is off; Omarchy restarts the shell deliberately. +# A package upgrade rewriting $OMARCHY_PATH/shell would otherwise reload it +# against a half-written tree, and that failed reload leaves a second engine +# generation behind that turns the next restart's IPC kill into a crash. run_shell() { - systemd-cat -t omarchy-shell -- quickshell -n -p "$OMARCHY_PATH/shell" & + QS_DISABLE_FILE_WATCHER=1 QS_NO_RELOAD_POPUP=1 \ + systemd-cat -t omarchy-shell -- quickshell -n -p "$OMARCHY_PATH/shell" & shell_pid=$! local status diff --git a/test/shell.d/launch-shell-test.sh b/test/shell.d/launch-shell-test.sh index 695000db..a5e0cf1c 100755 --- a/test/shell.d/launch-shell-test.sh +++ b/test/shell.d/launch-shell-test.sh @@ -28,6 +28,8 @@ cat >"$fake_bin/quickshell" <<'SH' #!/bin/bash printf '%s\n' "$*" >>"$OMARCHY_TEST_QS_LOG" +printf 'watcher=%s popup=%s\n' \ + "${QS_DISABLE_FILE_WATCHER:-unset}" "${QS_NO_RELOAD_POPUP:-unset}" >>"$OMARCHY_TEST_QS_ENV_LOG" launches=$(wc -l <"$OMARCHY_TEST_QS_LOG") status=$(awk -v n="$launches" 'NR == n { print; found = 1 } END { if (!found) print "0" }' <<<"$OMARCHY_TEST_QS_STATUSES") @@ -77,17 +79,20 @@ SH chmod +x "$fake_bin/quickshell" "$fake_bin/systemd-cat" "$fake_bin/hyprctl" "$fake_bin/logger" qs_log="$test_tmp/quickshell.log" +qs_env_log="$test_tmp/quickshell-env.log" logger_log="$test_tmp/logger.log" qs_terminated="$test_tmp/quickshell-terminated" hyprctl_misses="$test_tmp/hyprctl-misses" launch_shell() { : >"$qs_log" + : >"$qs_env_log" : >"$logger_log" PATH="$fake_bin:$PATH" \ OMARCHY_PATH="$shell_root" \ OMARCHY_TEST_QS_LOG="$qs_log" \ + OMARCHY_TEST_QS_ENV_LOG="$qs_env_log" \ OMARCHY_TEST_QS_STATUSES="$1" \ OMARCHY_TEST_COMPOSITOR_GONE="${2:-0}" \ OMARCHY_TEST_LOGGER_LOG="$logger_log" \ @@ -106,6 +111,12 @@ launch_shell '0' || fail "a clean launch succeeds" grep -F -- "-n -p $shell_root/shell" "$qs_log" >/dev/null || fail "the shell launches from OMARCHY_PATH" pass "a shell that exits cleanly is left alone" +# A misspelled variable would leave Quickshell hot-reloading the tree pacman +# rewrites underneath it, which is what crashes the restart that follows. +[[ $(<"$qs_env_log") == "watcher=1 popup=1" ]] || + fail "the shell launches with Quickshell's own reloading off" "$(<"$qs_env_log")" +pass "the shell launches with Quickshell's config watcher and reload popup off" + # Qt leaves through _exit(), so Quickshell's crash handler never relaunches it. launch_shell $'255\n0' || fail "a shell that died on a Wayland error is relaunched" [[ $(launches) == 2 ]] || fail "the dead shell is relaunched exactly once" "$(<"$qs_log")" @@ -130,11 +141,13 @@ pass "a compositor too busy to answer is not mistaken for one that is gone" # A signal mid-backoff only reaches the trap once the sleep is over. : >"$qs_log" +: >"$qs_env_log" : >"$logger_log" PATH="$fake_bin:$PATH" \ OMARCHY_PATH="$shell_root" \ OMARCHY_TEST_QS_LOG="$qs_log" \ +OMARCHY_TEST_QS_ENV_LOG="$qs_env_log" \ OMARCHY_TEST_QS_STATUSES=$'255\n0' \ OMARCHY_TEST_COMPOSITOR_GONE=0 \ OMARCHY_TEST_LOGGER_LOG="$logger_log" \ @@ -156,12 +169,14 @@ pass "a signal during backoff stops the supervisor before it relaunches" # Stopping the launcher used to stop the shell, back when it exec'd Quickshell. : >"$qs_log" +: >"$qs_env_log" : >"$logger_log" rm -f "$qs_terminated" PATH="$fake_bin:$PATH" \ OMARCHY_PATH="$shell_root" \ OMARCHY_TEST_QS_LOG="$qs_log" \ +OMARCHY_TEST_QS_ENV_LOG="$qs_env_log" \ OMARCHY_TEST_QS_STATUSES='run' \ OMARCHY_TEST_COMPOSITOR_GONE=0 \ OMARCHY_TEST_LOGGER_LOG="$logger_log" \