Make a dead lock client diagnosable and recoverable (#6630)

* Persist the Omarchy shell log across sessions

Quickshell only logs to its instance runtime dir on tmpfs, so when the
shell dies the idle/lock event trail is gone after a reboot (#6628).
Launch the shell through omarchy-launch-shell, which pipes stdout/stderr
into the journal under the omarchy-shell tag — bounded, timestamped, and
persistent — and surface that log in omarchy-debug-idle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Recover a locked session whose lock client died

When the shell dies while the session is locked, Hyprland's failsafe
keeps the session locked with no lock client left, and
omarchy-restart-shell refused to run in exactly that state, leaving
reboot as the only way back in (#6628). Gate the refusal on the lock
service actually holding (or acquiring) the lock rather than on the
session's LOCK state — a dead shell and a crash-handler relaunch that
holds no lock both fail that check — then restart the shell, re-acquire
the session lock, and wait for it to report secure, the same
secure-poll omarchy-system-sleep-lock uses, so the user can
authenticate out of the failsafe. Enable Hyprland's
allow_session_lock_restore so the compositor accepts the replacement
lock client.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-08-09 12:59:32 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 3aa7c1ee13
commit 1ded25fd45
6 changed files with 124 additions and 9 deletions
+3
View File
@@ -27,6 +27,9 @@ section "Recent idle logs"
quickshell --no-color log -p "$OMARCHY_PATH/shell" --any-display --tail "$lines" --log-times -r 'quickshell.wayland.idle_notify=true' 2>&1 \ quickshell --no-color log -p "$OMARCHY_PATH/shell" --any-display --tail "$lines" --log-times -r 'quickshell.wayland.idle_notify=true' 2>&1 \
| grep -Ei 'omarchy idle|idle_notify|screensaver|lock|error|warn|failed' || true | grep -Ei 'omarchy idle|idle_notify|screensaver|lock|error|warn|failed' || true
section "Persisted shell log"
journalctl -t omarchy-shell -n "$lines" --no-pager --quiet 2>/dev/null || true
section "Relevant processes" section "Relevant processes"
ps -eo pid=,args= \ ps -eo pid=,args= \
| grep -E 'quickshell -n -p|omarchy-system-sleep-monitor|systemd-inhibit.*Lock screen before suspend|org\.omarchy\.screensaver|omarchy-screensaver|(^|/| )tte( |$)' \ | grep -E 'quickshell -n -p|omarchy-system-sleep-monitor|systemd-inhibit.*Lock screen before suspend|org\.omarchy\.screensaver|omarchy-screensaver|(^|/| )tte( |$)' \
+9
View File
@@ -0,0 +1,9 @@
#!/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.
exec systemd-cat -t omarchy-shell -- quickshell -n -p "$OMARCHY_PATH/shell"
+47 -3
View File
@@ -18,11 +18,49 @@ if [[ -z ${HYPRLAND_INSTANCE_SIGNATURE:-} ]]; then
[[ -n $hypr_dir ]] && export HYPRLAND_INSTANCE_SIGNATURE=${hypr_dir##*/} [[ -n $hypr_dir ]] && export HYPRLAND_INSTANCE_SIGNATURE=${hypr_dir##*/}
fi fi
# Restarting a live lock client would kill the lock screen and strand the
# session behind Hyprland's failsafe. But a LOCK session without an active
# locker — the shell died, or its crash handler re-execed a fresh instance
# that holds no lock — sits in that failsafe with no way to authenticate,
# and a restart plus re-lock is the only way back in without a reboot. So
# ask the lock service rather than merely pinging the shell: only a locker
# that reports the lock secure or in progress is worth preserving.
relock=0
if [[ $(hyprctl -j monitors 2>/dev/null) == *'"LOCK"'* ]]; then if [[ $(hyprctl -j monitors 2>/dev/null) == *'"LOCK"'* ]]; then
echo "Refusing to restart Omarchy shell while the session is locked." >&2 locking=$(OMARCHY_PATH="$session_omarchy_path" OMARCHY_SHELL_IPC_TIMEOUT=0.5s omarchy-shell lock status 2>/dev/null |
exit 1 jq -r '.secure or .requested' 2>/dev/null)
if [[ $locking == "true" ]]; then
echo "Refusing to restart Omarchy shell while the session is locked." >&2
exit 1
fi
relock=1
fi fi
# The lock plugin loads asynchronously, so a fresh shell answers ping before
# it can lock, and may even refuse early lock requests while its plugins or
# PAM config are still loading. Mirror omarchy-system-sleep-lock: request the
# lock and poll until the session reports secure, re-requesting as needed, so
# recovery never claims success while the failsafe is still up. The deadline
# is generous because slow plugin discovery delays the lock IPC target.
relock_session() {
local state deadline=$((SECONDS + 30))
while (( SECONDS < deadline )); do
state=$(OMARCHY_PATH="$session_omarchy_path" OMARCHY_SHELL_IPC_TIMEOUT=0.5s omarchy-shell lock status 2>/dev/null |
jq -r 'if .secure == true then "secure" elif .requested == true then "locking" else "idle" end' 2>/dev/null)
case $state in
secure) return 0 ;;
locking) ;;
*) OMARCHY_PATH="$session_omarchy_path" OMARCHY_SHELL_IPC_TIMEOUT=0.5s omarchy-shell lock lock >/dev/null 2>&1 ;;
esac
sleep 0.1
done
return 1
}
# Each kill stops the oldest matching instance and only returns once it has # Each kill stops the oldest matching instance and only returns once it has
# fully exited, so the no-duplicate launch below can't race a dying shell. # fully exited, so the no-duplicate launch below can't race a dying shell.
# Requires our quickshell-git build; 0.3.0's kill returns immediately. # Requires our quickshell-git build; 0.3.0's kill returns immediately.
@@ -30,10 +68,16 @@ while timeout 5 quickshell kill -p "$CONFIG_DIR" --any-display >/dev/null 2>&1;
# Spawn from Hyprland so the shell inherits the canonical session environment, # Spawn from Hyprland so the shell inherits the canonical session environment,
# not transient variables from a terminal, SSH connection, or development tool. # not transient variables from a terminal, SSH connection, or development tool.
hyprctl dispatch 'hl.dsp.exec_cmd("quickshell -n -p $OMARCHY_PATH/shell")' >/dev/null hyprctl dispatch 'hl.dsp.exec_cmd("omarchy-launch-shell")' >/dev/null
for (( attempt = 0; attempt < 20; attempt++ )); do for (( attempt = 0; attempt < 20; attempt++ )); do
if OMARCHY_PATH="$session_omarchy_path" OMARCHY_SHELL_IPC_TIMEOUT=0.5s omarchy-shell shell ping >/dev/null 2>&1; then if OMARCHY_PATH="$session_omarchy_path" OMARCHY_SHELL_IPC_TIMEOUT=0.5s omarchy-shell shell ping >/dev/null 2>&1; then
# The session stays compositor-locked after the old lock client died, so
# re-acquire the lock and let the user authenticate out of it.
if (( relock )) && ! relock_session; then
echo "Omarchy shell restarted, but the session lock was not re-secured." >&2
exit 1
fi
# Invitation toasts (like Voxtype/fingerprint setup) die with the old # Invitation toasts (like Voxtype/fingerprint setup) die with the old
# shell, and their notify-send waiters hang forever: the dying server # shell, and their notify-send waiters hang forever: the dying server
# never emits NotificationClosed. A still-running omarchy-*-invitation # never emits NotificationClosed. A still-running omarchy-*-invitation
+1 -1
View File
@@ -3,7 +3,7 @@ hl.on("hyprland.start", function()
hl.exec_cmd("systemctl --user import-environment $(env | cut -d'=' -f 1)") hl.exec_cmd("systemctl --user import-environment $(env | cut -d'=' -f 1)")
hl.exec_cmd("dbus-update-activation-environment --systemd --all") hl.exec_cmd("dbus-update-activation-environment --systemd --all")
hl.exec_cmd("quickshell -n -p $OMARCHY_PATH/shell") hl.exec_cmd("omarchy-launch-shell")
hl.exec_cmd("omarchy-first-run") hl.exec_cmd("omarchy-first-run")
hl.exec_cmd("omarchy-powerprofiles-init") hl.exec_cmd("omarchy-powerprofiles-init")
hl.exec_cmd(o.launch("omarchy-hyprland-monitor-watch")) hl.exec_cmd(o.launch("omarchy-hyprland-monitor-watch"))
+3
View File
@@ -110,6 +110,9 @@ hl.config({
anr_missed_pings = 3, anr_missed_pings = 3,
on_focus_under_fullscreen = 1, on_focus_under_fullscreen = 1,
initial_workspace_tracking = 0, initial_workspace_tracking = 0,
-- Let a fresh shell re-acquire the session lock after the lock client
-- died, so omarchy-restart-shell can recover the LOCK failsafe.
allow_session_lock_restore = true,
}, },
cursor = { cursor = {
+61 -5
View File
@@ -61,6 +61,7 @@ runtime_dir="$test_tmp/runtime"
mkdir -p "$restart_root/shell" "$restart_bin" "$runtime_dir" mkdir -p "$restart_root/shell" "$restart_bin" "$runtime_dir"
touch "$restart_root/shell/shell.qml" touch "$restart_root/shell/shell.qml"
ln -s "$ROOT/bin/omarchy-shell" "$restart_bin/omarchy-shell" ln -s "$ROOT/bin/omarchy-shell" "$restart_bin/omarchy-shell"
ln -s "$ROOT/bin/omarchy-launch-shell" "$restart_bin/omarchy-launch-shell"
ln -s "$ROOT/bin/omarchy-cmd-missing" "$restart_bin/omarchy-cmd-missing" ln -s "$ROOT/bin/omarchy-cmd-missing" "$restart_bin/omarchy-cmd-missing"
cat >"$restart_bin/qs" <<'SH' cat >"$restart_bin/qs" <<'SH'
@@ -74,6 +75,17 @@ case "$*" in
grep -Fx '303' "$OMARCHY_TEST_QS_STATE" >/dev/null && grep -Fx '303' "$OMARCHY_TEST_QS_STATE" >/dev/null &&
printf 'ok\n' printf 'ok\n'
;; ;;
*'lock lock')
touch "$OMARCHY_TEST_QS_STATE.locked"
printf 'ok\n'
;;
*'lock status')
if [[ -f $OMARCHY_TEST_QS_STATE.locked ]]; then
printf '{"secure": true, "requested": true}\n'
else
printf '{"secure": false, "requested": false}\n'
fi
;;
esac esac
SH SH
@@ -110,13 +122,24 @@ if [[ ${1:-} == "-j" && ${2:-} == "monitors" ]]; then
elif [[ ${1:-} == "dispatch" && ${2:-} == hl.dsp.exec_cmd* ]]; then elif [[ ${1:-} == "dispatch" && ${2:-} == hl.dsp.exec_cmd* ]]; then
printf '%s\n' "${2:-}" >>"$OMARCHY_TEST_DISPATCH_LOG" printf '%s\n' "${2:-}" >>"$OMARCHY_TEST_DISPATCH_LOG"
OMARCHY_PATH="$OMARCHY_TEST_SESSION_PATH" \ OMARCHY_PATH="$OMARCHY_TEST_SESSION_PATH" \
env -u OMARCHY_TEST_TRANSIENT_ENV quickshell -n -p "$OMARCHY_TEST_SESSION_PATH/shell" env -u OMARCHY_TEST_TRANSIENT_ENV omarchy-launch-shell
printf 'ok\n' printf 'ok\n'
elif [[ ${1:-} == "dispatch" ]]; then elif [[ ${1:-} == "dispatch" ]]; then
exit 1 exit 1
fi fi
SH SH
# Keep the test hermetic where journald has no usable stream socket.
cat >"$restart_bin/systemd-cat" <<'SH'
#!/bin/bash
while (( $# > 0 )); do
[[ $1 == "--" ]] && { shift; break; }
shift
done
exec "$@"
SH
cat >"$restart_bin/systemctl" <<'SH' cat >"$restart_bin/systemctl" <<'SH'
#!/bin/bash #!/bin/bash
@@ -129,7 +152,7 @@ else
fi fi
SH SH
chmod +x "$restart_bin/qs" "$restart_bin/quickshell" "$restart_bin/hyprctl" "$restart_bin/systemctl" chmod +x "$restart_bin/qs" "$restart_bin/quickshell" "$restart_bin/hyprctl" "$restart_bin/systemd-cat" "$restart_bin/systemctl"
sleep 30 & sleep 30 &
restart_pid_one=$! restart_pid_one=$!
@@ -167,12 +190,13 @@ restart_pid_two=""
[[ $(grep -c '^-n -p ' "$restart_log") == 1 ]] || fail "restart launches one fresh shell process" [[ $(grep -c '^-n -p ' "$restart_log") == 1 ]] || fail "restart launches one fresh shell process"
grep -F "kill -p $restart_root/shell --any-display" "$restart_log" >/dev/null || fail "restart stops the shell from the session checkout" grep -F "kill -p $restart_root/shell --any-display" "$restart_log" >/dev/null || fail "restart stops the shell from the session checkout"
[[ $(<"$restart_env_log") == "unset" ]] || fail "restart uses the Hyprland session environment for the fresh shell" [[ $(<"$restart_env_log") == "unset" ]] || fail "restart uses the Hyprland session environment for the fresh shell"
grep -F 'hl.dsp.exec_cmd("quickshell -n -p $OMARCHY_PATH/shell")' "$dispatch_log" >/dev/null || fail "restart launches the fresh shell through Hyprland" grep -F 'hl.dsp.exec_cmd("omarchy-launch-shell")' "$dispatch_log" >/dev/null || fail "restart launches the fresh shell through Hyprland"
grep -F "ipc -n -p $restart_root/shell call -- shell ping" "$ipc_log" >/dev/null || fail "restart checks readiness in the session checkout" grep -F "ipc -n -p $restart_root/shell call -- shell ping" "$ipc_log" >/dev/null || fail "restart checks readiness in the session checkout"
pass "restart replaces duplicate shell instances from the session checkout" pass "restart replaces duplicate shell instances from the session checkout"
: >"$restart_log" : >"$restart_log"
printf '404\n' >"$restart_state" printf '303\n' >"$restart_state"
touch "$restart_state.locked"
locked_error=$(PATH="$restart_bin:$PATH" \ locked_error=$(PATH="$restart_bin:$PATH" \
OMARCHY_PATH="$restart_root" \ OMARCHY_PATH="$restart_root" \
@@ -186,6 +210,38 @@ locked_error=$(PATH="$restart_bin:$PATH" \
"$ROOT/bin/omarchy-restart-shell" 2>&1) && fail "restart refuses while the shell lock is active" "$ROOT/bin/omarchy-restart-shell" 2>&1) && fail "restart refuses while the shell lock is active"
[[ $locked_error == "Refusing to restart Omarchy shell while the session is locked." ]] || fail "locked restart explains why it was refused" "$locked_error" [[ $locked_error == "Refusing to restart Omarchy shell while the session is locked." ]] || fail "locked restart explains why it was refused" "$locked_error"
[[ $(<"$restart_state") == 404 ]] || fail "locked restart preserves the running shell" [[ $(<"$restart_state") == 303 ]] || fail "locked restart preserves the running shell"
[[ ! -s $restart_log ]] || fail "locked restart does not stop or launch Quickshell" [[ ! -s $restart_log ]] || fail "locked restart does not stop or launch Quickshell"
pass "restart preserves the shell while its lock is active" pass "restart preserves the shell while its lock is active"
# A LOCK session without an active locker — dead shell or a crash-handler
# relaunch holding no lock — is the failsafe: restart must proceed,
# re-acquire the session lock, and wait for it to report secure.
sleep 30 &
restart_pid_one=$!
printf '%s\n' "$restart_pid_one" >"$restart_state"
rm -f "$restart_state.locked"
: >"$restart_log"
: >"$ipc_log"
PATH="$restart_bin:$PATH" \
OMARCHY_PATH="$restart_root" \
XDG_RUNTIME_DIR="$runtime_dir" \
OMARCHY_TEST_SESSION_LOCKED=1 \
OMARCHY_TEST_QS_STATE="$restart_state" \
OMARCHY_TEST_QS_LOG="$restart_log" \
OMARCHY_TEST_QS_ENV_LOG="$restart_env_log" \
OMARCHY_TEST_DISPATCH_LOG="$dispatch_log" \
OMARCHY_TEST_IPC_LOG="$ipc_log" \
OMARCHY_TEST_SESSION_PATH="$restart_root" \
timeout 5 "$ROOT/bin/omarchy-restart-shell" || fail "locked restart recovers when the lock client is dead"
if kill -0 "$restart_pid_one" 2>/dev/null; then
fail "dead-lock recovery stops the stale shell instance"
fi
wait "$restart_pid_one" 2>/dev/null || true
restart_pid_one=""
[[ $(<"$restart_state") == 303 ]] || fail "dead-lock recovery leaves one fresh shell instance"
grep -F "ipc -n -p $restart_root/shell call -- lock lock" "$ipc_log" >/dev/null || fail "dead-lock recovery re-acquires the session lock"
grep -F "ipc -n -p $restart_root/shell call -- lock status" "$ipc_log" >/dev/null || fail "dead-lock recovery waits for the lock to become secure"
pass "restart recovers a locked session whose lock client died"