diff --git a/bin/omarchy-restart-shell b/bin/omarchy-restart-shell index 9891e7e8..4ca6ba29 100755 --- a/bin/omarchy-restart-shell +++ b/bin/omarchy-restart-shell @@ -82,26 +82,50 @@ while timeout 5 quickshell kill -p "$CONFIG_DIR" --any-display >/dev/null 2>&1; # not transient variables from a terminal, SSH connection, or development tool. hyprctl dispatch 'hl.dsp.exec_cmd("omarchy-launch-shell")' >/dev/null +shell_ready=0 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 && - { (( notifications_were_running == 0 )) || notifications_ready; }; 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 - # shell, and their notify-send waiters hang forever: the dying server - # never emits NotificationClosed. A still-running omarchy-*-invitation - # unit is therefore an unanswered invitation — re-run it so its toast - # reappears on the new shell. Answered invitations have already exited - # and been collected, so the glob no longer matches them. - systemctl --user try-restart 'omarchy-*-invitation.service' 2>/dev/null || true - exit 0 + if OMARCHY_PATH="$session_omarchy_path" OMARCHY_SHELL_IPC_TIMEOUT=0.5s omarchy-shell shell ping >/dev/null 2>&1; then + shell_ready=1 + break fi sleep 0.1 done +if (( shell_ready == 0 )); then + echo "Omarchy shell did not become ready after restart." >&2 + exit 1 +fi -echo "Omarchy shell did not become ready after restart." >&2 -exit 1 +# The session stays compositor-locked after the old lock client died, so +# re-acquire the lock and let the user authenticate out of it. This comes +# first and depends on nothing else: a user stranded behind the failsafe must +# not wait on the notification plugin, which may be slow or absent. +if (( relock )) && ! relock_session; then + echo "Omarchy shell restarted, but the session lock was not re-secured." >&2 + exit 1 +fi + +# Core IPC answers before the notification plugin has registered its bus +# name, so wait for it separately before one-time toasts are sent. +if (( notifications_were_running )); then + notifications_restored=0 + for (( attempt = 0; attempt < 20; attempt++ )); do + if notifications_ready; then + notifications_restored=1 + break + fi + sleep 0.1 + done + if (( notifications_restored == 0 )); then + echo "Omarchy shell restarted, but its notification service did not become ready." >&2 + exit 1 + fi +fi + +# Invitation toasts (like Voxtype/fingerprint setup) die with the old +# shell, and their notify-send waiters hang forever: the dying server +# never emits NotificationClosed. A still-running omarchy-*-invitation +# unit is therefore an unanswered invitation — re-run it so its toast +# reappears on the new shell. Answered invitations have already exited +# and been collected, so the glob no longer matches them. +systemctl --user try-restart 'omarchy-*-invitation.service' 2>/dev/null || true +exit 0 diff --git a/test/shell.d/restart-shell-test.sh b/test/shell.d/restart-shell-test.sh index 31901736..e6d81e62 100755 --- a/test/shell.d/restart-shell-test.sh +++ b/test/shell.d/restart-shell-test.sh @@ -181,6 +181,12 @@ else [[ ! -f $OMARCHY_TEST_NOTIFICATION_CHECKS ]] || read -r checks <"$OMARCHY_TEST_NOTIFICATION_CHECKS" (( checks += 1 )) printf '%s\n' "$checks" >"$OMARCHY_TEST_NOTIFICATION_CHECKS" + # The service was running before the restart and, when asked to, never + # comes back afterwards. + if [[ ${OMARCHY_TEST_NOTIFICATIONS_DIE:-0} == 1 ]]; then + (( checks == 1 )) && echo 'b true' || echo 'b false' + exit 0 + fi if (( checks == 1 || checks >= 4 )); then echo 'b true' else @@ -285,3 +291,35 @@ restart_pid_one="" 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" + +# Lock recovery must not wait on the notification plugin: a stranded user gets +# the lock back even when notifications never return, and the restart then +# reports the missing service rather than claiming success. +sleep 30 & +restart_pid_one=$! +printf '%s\n' "$restart_pid_one" >"$restart_state" +rm -f "$restart_state.locked" "$test_tmp/notification-checks" +: >"$restart_log" +: >"$ipc_log" + +if 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" \ + OMARCHY_TEST_NOTIFICATION_CHECKS="$test_tmp/notification-checks" \ + OMARCHY_TEST_NOTIFICATIONS_DIE=1 \ + timeout 10 "$ROOT/bin/omarchy-restart-shell" >"$test_tmp/dead-notifications.out" 2>&1; then + fail "a restart whose notification service never returns must not report success" +fi +wait "$restart_pid_one" 2>/dev/null || true +restart_pid_one="" +grep -F "ipc -n -p $restart_root/shell call -- lock lock" "$ipc_log" >/dev/null || fail "lock recovery waited on the notification service" "$(cat "$ipc_log")" +[[ -f $restart_state.locked ]] || fail "lock recovery did not re-secure the session without notifications" +grep -q "notification service did not become ready" "$test_tmp/dead-notifications.out" || fail "a missing notification service is not reported" "$(cat "$test_tmp/dead-notifications.out")" +pass "restart recovers the lock even when the notification service never returns"