Files
omarchycn/bin/omarchy-launch-shell
T
0900855a28 Stop hot-reloading the shell when a package upgrade rewrites it (#6751)
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) <noreply@anthropic.com>
2026-08-12 16:42:37 +02:00

92 lines
2.7 KiB
Bash
Executable File

#!/bin/bash
# omarchy:summary=Launch the Omarchy shell with its log kept in the journal
# omarchy:hidden=true
# Quickshell only logs to its instance runtime dir (tmpfs), so when the shell
# dies the idle/lock event trail is gone after a reboot. The journal keeps it
# across sessions, bounded and timestamped, under the omarchy-shell tag.
#
# 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() {
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
while true; do
wait "$shell_pid"
status=$?
# An interrupted wait and a shell killed by that signal report alike.
kill -0 "$shell_pid" 2>/dev/null || break
done
shell_pid=""
return $status
}
# A compositor busy reconfiguring outputs can miss a query without being gone,
# and that is when the shell dies.
compositor_alive() {
local attempt
for attempt in 1 2 3; do
hyprctl -j monitors >/dev/null 2>&1 && return 0
(( attempt < 3 )) && sleep 0.5
done
return 1
}
# Quickshell relaunches itself from its signal handlers, but Qt leaves through
# _exit() when the Wayland connection fails, raising no signal: no crash report,
# no relaunch, no bar. Supervise those deaths. A clean exit is a deliberate stop
# (omarchy-restart-shell starts its own replacement); a signal here means the
# session is going, and has to reach the shell the launcher used to exec.
terminating=0
shell_pid=""
stop() {
terminating=1
[[ -n $shell_pid ]] && kill -TERM "$shell_pid" 2>/dev/null
return 0
}
trap stop HUP INT TERM
attempts=0
window_started=$SECONDS
while true; do
# A signal during the backoff only reaches the trap once the sleep is over.
(( terminating )) && exit 0
run_shell
status=$?
(( terminating )) && exit 0
(( status == 0 )) && exit 0
# Relaunching into a session already tearing down burns the attempt budget.
compositor_alive || exit 0
if (( SECONDS - window_started > 60 )); then
attempts=0
window_started=$SECONDS
fi
if (( ++attempts > 5 )); then
logger -t omarchy-shell "Giving up on the Omarchy shell after $attempts relaunches in under a minute."
exit 1
fi
logger -t omarchy-shell "Omarchy shell exited with status $status; relaunching."
sleep 1
done