diff --git a/bin/omarchy-restart-shell b/bin/omarchy-restart-shell index dfc21724620..16ad4f1f42c 100755 --- a/bin/omarchy-restart-shell +++ b/bin/omarchy-restart-shell @@ -67,9 +67,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, # not transient variables from a terminal, SSH connection, or development tool. -hyprctl dispatch 'hl.dsp.exec_cmd("omarchy-launch-shell")' >/dev/null +launch_shell() { + hyprctl dispatch 'hl.dsp.exec_cmd("omarchy-launch-shell")' >/dev/null +} + +launch_shell -for (( attempt = 0; attempt < 20; attempt++ )); do +# Plugin discovery and first-load QML compilation can take several seconds. +ready_deadline=$((SECONDS + 30)) +next_launch=$((SECONDS + 2)) +while (( SECONDS < ready_deadline )); do 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. @@ -86,6 +93,14 @@ for (( attempt = 0; attempt < 20; attempt++ )); do systemctl --user try-restart 'omarchy-*-invitation.service' 2>/dev/null || true exit 0 fi + + # Quickshell can release its IPC target before its singleton lock. A launch + # in that gap exits immediately, so retry until one replacement stays up. + if (( SECONDS >= next_launch )); then + launch_shell + next_launch=$((SECONDS + 2)) + fi + sleep 0.1 done diff --git a/test/shell.d/restart-shell-test.sh b/test/shell.d/restart-shell-test.sh index 094c74f6449..365744e052e 100755 --- a/test/shell.d/restart-shell-test.sh +++ b/test/shell.d/restart-shell-test.sh @@ -73,6 +73,7 @@ restart_state="$test_tmp/restart-pids" restart_log="$test_tmp/restart.log" restart_env_log="$test_tmp/restart-env.log" dispatch_log="$test_tmp/dispatch.log" +dispatch_count="$test_tmp/dispatch-count" ipc_log="$test_tmp/ipc.log" runtime_dir="$test_tmp/runtime" mkdir -p "$restart_root/shell" "$restart_bin" "$runtime_dir" @@ -89,9 +90,19 @@ printf '%s\n' "$*" >>"$OMARCHY_TEST_IPC_LOG" case "$*" in *'shell ping') - [[ $* == *"-p $OMARCHY_TEST_SESSION_PATH/shell"* ]] && + if [[ -n ${OMARCHY_TEST_QS_READY_AFTER:-} ]]; then + attempts=$(<"$OMARCHY_TEST_QS_READY_COUNT") + attempts=$((attempts + 1)) + printf '%s\n' "$attempts" >"$OMARCHY_TEST_QS_READY_COUNT" + fi + + if [[ $* == *"-p $OMARCHY_TEST_SESSION_PATH/shell"* ]] && grep -Fx '303' "$OMARCHY_TEST_QS_STATE" >/dev/null && + (( ${attempts:-1} >= ${OMARCHY_TEST_QS_READY_AFTER:-1} )); then printf 'ok\n' + else + exit 1 + fi ;; *'lock lock') touch "$OMARCHY_TEST_QS_STATE.locked" @@ -141,8 +152,15 @@ if [[ ${1:-} == "-j" && ${2:-} == "monitors" ]]; then fi elif [[ ${1:-} == "dispatch" && ${2:-} == hl.dsp.exec_cmd* ]]; then printf '%s\n' "${2:-}" >>"$OMARCHY_TEST_DISPATCH_LOG" - OMARCHY_PATH="$OMARCHY_TEST_SESSION_PATH" \ - env -u OMARCHY_TEST_TRANSIENT_ENV omarchy-launch-shell + if [[ -n ${OMARCHY_TEST_DISPATCH_COUNT:-} ]]; then + attempts=$(<"$OMARCHY_TEST_DISPATCH_COUNT") + attempts=$((attempts + 1)) + printf '%s\n' "$attempts" >"$OMARCHY_TEST_DISPATCH_COUNT" + fi + if (( ${attempts:-1} > ${OMARCHY_TEST_DISPATCH_FAILS:-0} )); then + OMARCHY_PATH="$OMARCHY_TEST_SESSION_PATH" \ + env -u OMARCHY_TEST_TRANSIENT_ENV omarchy-launch-shell + fi printf 'ok\n' elif [[ ${1:-} == "dispatch" ]]; then exit 1 @@ -214,6 +232,52 @@ grep -F 'hl.dsp.exec_cmd("omarchy-launch-shell")' "$dispatch_log" >/dev/null || 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" +# Plugin discovery and first-load QML compilation can take longer than the old +# 20-attempt readiness budget. Keep polling long enough for a slow shell that +# eventually becomes IPC-responsive. +ready_count="$test_tmp/ready-count" +printf '0\n' >"$ready_count" +printf '303\n' >"$restart_state" + +PATH="$restart_bin:$PATH" \ +OMARCHY_PATH="$restart_root" \ +XDG_RUNTIME_DIR="$runtime_dir" \ +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_QS_READY_AFTER=25 \ +OMARCHY_TEST_QS_READY_COUNT="$ready_count" \ + timeout 8 "$ROOT/bin/omarchy-restart-shell" || fail "restart waits for a slow shell to become ready" + +(( $(<"$ready_count") >= 25 )) || fail "restart keeps polling beyond the old readiness budget" +pass "restart waits for a slow shell to become ready" + +# Quickshell can release its IPC target before releasing its singleton lock. +# When that happens the first replacement exits with "instance already +# running", so waiting for readiness alone can never recover the bar. +printf '0\n' >"$dispatch_count" +: >"$restart_state" + +PATH="$restart_bin:$PATH" \ +OMARCHY_PATH="$restart_root" \ +XDG_RUNTIME_DIR="$runtime_dir" \ +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_DISPATCH_COUNT="$dispatch_count" \ +OMARCHY_TEST_DISPATCH_FAILS=1 \ +OMARCHY_TEST_IPC_LOG="$ipc_log" \ +OMARCHY_TEST_SESSION_PATH="$restart_root" \ + timeout 8 "$ROOT/bin/omarchy-restart-shell" || fail "restart retries a replacement that loses the singleton-lock race" + +(( $(<"$dispatch_count") >= 2 )) || fail "restart retries shell launch after the first replacement exits" +[[ $(<"$restart_state") == 303 ]] || fail "retried launch leaves one fresh shell instance" +pass "restart retries a replacement that loses the singleton-lock race" + : >"$restart_log" printf '303\n' >"$restart_state" touch "$restart_state.locked"