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:
co-authored by
Claude Fable 5
parent
3aa7c1ee13
commit
1ded25fd45
@@ -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( |$)' \
|
||||||
|
|||||||
Executable
+9
@@ -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"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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,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"
|
||||||
|
|||||||
Reference in New Issue
Block a user