From 17c8af3f14b173e716180e538de9b07a473d5a63 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 19 Jul 2026 10:09:00 -0700 Subject: [PATCH] Simplify shell restart around quickshell instance management omarchy-restart-shell now kills by config dir via quickshell kill and relaunches with --no-duplicate, polling kill until every instance is gone since kill returns without waiting and -n silently exits if one remains. The locked-session refusal reads Hyprland monitor state alone, which covers a hung shell holding the lock; the redundant shell IPC probe, availability guards, and omarchy-shell path indirection are gone. Shell IPC calls now time out (2s default, OMARCHY_SHELL_IPC_TIMEOUT to override) so probing an unresponsive shell fails fast instead of hanging the caller. omarchy-hyprland-launch checks dispatch output for "ok" rather than the exit code, which is 0 even on Lua errors, so a failed dispatch falls through to the bash -lc fallback instead of silently launching nothing. The Quattro upgrade cutover delegates to omarchy-restart-shell instead of carrying its own copy of the lock check, pkill, and readiness loop. Co-Authored-By: Claude Fable 5 --- bin/omarchy-hyprland-launch | 3 +- bin/omarchy-restart-shell | 94 +++++---------------- bin/omarchy-shell | 10 ++- bin/omarchy-upgrade-to-quattro | 43 ++-------- shell/README.md | 4 +- test/shell.d/restart-shell-test.sh | 128 +++++++++++++++++++++++++++++ 6 files changed, 169 insertions(+), 113 deletions(-) create mode 100755 test/shell.d/restart-shell-test.sh diff --git a/bin/omarchy-hyprland-launch b/bin/omarchy-hyprland-launch index 5dcb45bf..6d932090 100755 --- a/bin/omarchy-hyprland-launch +++ b/bin/omarchy-hyprland-launch @@ -21,7 +21,8 @@ active_workspace() { if [[ -n ${HYPRLAND_INSTANCE_SIGNATURE:-} ]] && omarchy-cmd-present hyprctl && omarchy-cmd-present jq; then workspace="$(active_workspace)" - if [[ -n $workspace ]] && hyprctl dispatch "function() hl.exec_cmd($(lua_quote "$cmd"), { workspace = $(lua_quote "$workspace") }) end" >/dev/null 2>&1; then + # hyprctl dispatch exits 0 even on Lua errors, so check for "ok" output. + if [[ -n $workspace && $(hyprctl dispatch "function() hl.exec_cmd($(lua_quote "$cmd"), { workspace = $(lua_quote "$workspace") }) end" 2>/dev/null) == ok ]]; then exit 0 fi fi diff --git a/bin/omarchy-restart-shell b/bin/omarchy-restart-shell index f978dc28..0dfd557d 100755 --- a/bin/omarchy-restart-shell +++ b/bin/omarchy-restart-shell @@ -3,20 +3,9 @@ # omarchy:summary=Restart the Omarchy shell # omarchy:examples=omarchy restart shell -omarchy_root="${OMARCHY_PATH:-/usr/share/omarchy}" -if [[ ! -f $omarchy_root/shell/shell.qml && -f /usr/share/omarchy/shell/shell.qml ]]; then - omarchy_root=/usr/share/omarchy -fi - -CONFIG_DIR="$omarchy_root/shell" +CONFIG_DIR="$OMARCHY_PATH/shell" [[ -f $CONFIG_DIR/shell.qml ]] || { echo "Omarchy shell config not found: $CONFIG_DIR" >&2; exit 1; } -export OMARCHY_PATH="$omarchy_root" -export PATH="$omarchy_root/bin:/usr/local/bin:/usr/bin:/bin:$PATH" - -omarchy_shell_bin=/usr/bin/omarchy-shell -[[ -x $omarchy_shell_bin ]] || omarchy_shell_bin=omarchy-shell - # Allow running from outside the session (e.g. over ssh) by deriving the # Hyprland instance signature from the newest instance runtime dir. if [[ -z ${HYPRLAND_INSTANCE_SIGNATURE:-} ]]; then @@ -24,71 +13,28 @@ if [[ -z ${HYPRLAND_INSTANCE_SIGNATURE:-} ]]; then [[ -n $hypr_dir ]] && export HYPRLAND_INSTANCE_SIGNATURE=${hypr_dir##*/} fi -hyprland_session_locked() { - local monitors - - if monitors=$(hyprctl -j monitors 2>/dev/null); then - [[ $monitors == *'"LOCK"'* ]] && return 0 - fi - - monitors=$(hyprctl monitors 2>/dev/null) || return 1 - [[ $monitors == *"session lock"* ]] -} - -shell_quote() { - printf "'" - printf "%s" "$1" | sed "s/'/'\\\\''/g" - printf "'" -} - -lua_quote() { - local value=$1 - value=${value//\\/\\\\} - value=${value//\"/\\\"} - value=${value//$'\n'/\\n} - printf '"%s"' "$value" -} - -launch_shell() { - local launch_command - - launch_command="env OMARCHY_PATH=$(shell_quote "$omarchy_root") PATH=$(shell_quote "$PATH") quickshell -n -p $(shell_quote "$CONFIG_DIR")" - - # hyprctl dispatch exits 0 even on Lua errors, so check for "ok" output. - if [[ -n ${HYPRLAND_INSTANCE_SIGNATURE:-} ]] && omarchy-cmd-present hyprctl && - [[ $(hyprctl dispatch "hl.dsp.exec_cmd($(lua_quote "$launch_command"))" 2>/dev/null) == ok ]]; then - return 0 - fi - - setsid quickshell -n -p "$CONFIG_DIR" >/dev/null 2>&1 & -} - -restore_hyprland_session_lock() { - local attempt - - for (( attempt = 0; attempt < 50; attempt++ )); do - if "$omarchy_shell_bin" shell ping >/dev/null 2>&1; then - "$omarchy_shell_bin" lock lock >/dev/null 2>&1 || true - return 0 - fi - sleep 0.1 - done - - echo "Omarchy shell restarted while Hyprland was session-locked, but lock IPC did not become ready." >&2 -} - -hyprland_was_session_locked=0 -hyprland_session_locked && hyprland_was_session_locked=1 - -if [[ $("$omarchy_shell_bin" lock isLocked 2>/dev/null || true) == "true" ]]; then +if [[ $(hyprctl -j monitors 2>/dev/null) == *'"LOCK"'* ]]; then echo "Refusing to restart Omarchy shell while the session is locked." >&2 exit 1 fi -pkill -x quickshell 2>/dev/null || true -sleep 0.2 -launch_shell +for (( attempt = 0; attempt < 50; attempt++ )); do + quickshell kill -p "$CONFIG_DIR" --any-display >/dev/null 2>&1 || break + sleep 0.1 +done -if (( hyprland_was_session_locked )); then - restore_hyprland_session_lock +if (( attempt == 50 )); then + echo "Could not stop all Omarchy shell instances." >&2 + exit 1 fi + +printf -v launch_command 'setsid quickshell -n -p %q >/dev/null 2>&1 &' "$CONFIG_DIR" +omarchy-hyprland-launch "$launch_command" + +for (( attempt = 0; attempt < 20; attempt++ )); do + OMARCHY_SHELL_IPC_TIMEOUT=0.5s omarchy-shell shell ping >/dev/null 2>&1 && exit 0 + sleep 0.1 +done + +echo "Omarchy shell did not become ready after restart." >&2 +exit 1 diff --git a/bin/omarchy-shell b/bin/omarchy-shell index ed7729b2..8b6c9011 100755 --- a/bin/omarchy-shell +++ b/bin/omarchy-shell @@ -47,7 +47,15 @@ fi # The -- keeps function names that shadow qs subcommands (e.g. show) as # positionals. qs reports connection failures with a nonzero exit, but IPC-level # failures (unknown target/function, bad arguments) go to stdout with exit 0. -output=$(qs -p "$OMARCHY_PATH/shell" ipc call -- "$@" 2>/dev/null) || fail "omarchy-shell is not running" +ipc_timeout=${OMARCHY_SHELL_IPC_TIMEOUT:-2s} +output=$(timeout --kill-after=1s "$ipc_timeout" qs -p "$OMARCHY_PATH/shell" ipc call -- "$@" 2>/dev/null) +ipc_status=$? + +if (( ipc_status == 124 || ipc_status == 137 )); then + fail "omarchy-shell is not responding" +elif (( ipc_status != 0 )); then + fail "omarchy-shell is not running" +fi case $output in "Target not found." | "Function not found." | "Too few arguments provided"* | "Too many arguments provided"*) diff --git a/bin/omarchy-upgrade-to-quattro b/bin/omarchy-upgrade-to-quattro index c4feedd2..473b34a5 100755 --- a/bin/omarchy-upgrade-to-quattro +++ b/bin/omarchy-upgrade-to-quattro @@ -976,12 +976,10 @@ start_omarchy_shell_session() { # Reboot is still the real cutover, but start the new shell in the current # Wayland session when possible so users do not sit without a bar after the # retired waybar/walker/elephant processes are stopped. - local quickshell_bin omarchy_shell_bin shell_log - quickshell_bin=$(command -v quickshell || true) - [[ -n $quickshell_bin ]] || { warn "quickshell is not available; Omarchy shell will start after reboot."; return 1; } - omarchy_shell_bin=/usr/bin/omarchy-shell - [[ -x $omarchy_shell_bin ]] || omarchy_shell_bin=$(command -v omarchy-shell || true) - [[ -n $omarchy_shell_bin ]] || { warn "omarchy-shell is not available; Omarchy shell will start after reboot."; return 1; } + local restart_shell_bin shell_log + command -v quickshell >/dev/null || { warn "quickshell is not available; Omarchy shell will start after reboot."; return 1; } + restart_shell_bin=/usr/share/omarchy/bin/omarchy-restart-shell + [[ -x $restart_shell_bin ]] || { warn "omarchy-restart-shell is not available; Omarchy shell will start after reboot."; return 1; } [[ -f /usr/share/omarchy/shell/shell.qml ]] || { warn "/usr/share/omarchy/shell is missing; Omarchy shell will start after reboot."; return 1; } [[ -d $target_runtime_dir ]] || { warn "No running user session found; Omarchy shell will start after reboot."; return 1; } @@ -1013,37 +1011,12 @@ start_omarchy_shell_session() { OMARCHY_PATH=/usr/share/omarchy \ PATH="$package_path" \ bash -c ' - omarchy_shell_bin=$1 - quickshell_bin=$2 - shell_log=$3 - if [[ $("$omarchy_shell_bin" lock isLocked 2>/dev/null || true) == "true" ]]; then - exit 2 - fi - pkill -x quickshell 2>/dev/null || true - sleep 0.2 + restart_shell_bin=$1 + shell_log=$2 : >"$shell_log" - setsid "$quickshell_bin" -n -p /usr/share/omarchy/shell >>"$shell_log" 2>&1 & - ' bash "$omarchy_shell_bin" "$quickshell_bin" "$shell_log" \ + "$restart_shell_bin" >>"$shell_log" 2>&1 + ' bash "$restart_shell_bin" "$shell_log" \ || { warn "Could not start Omarchy shell in the current session; it will start after reboot. See $shell_log"; return 1; } - - local attempt - for attempt in {1..60}; do - if run_as_user env \ - HOME="$target_home" \ - USER="$target_user" \ - LOGNAME="$target_user" \ - XDG_RUNTIME_DIR="$target_runtime_dir" \ - DBUS_SESSION_BUS_ADDRESS="unix:path=$target_runtime_dir/bus" \ - OMARCHY_PATH=/usr/share/omarchy \ - PATH="$package_path" \ - "$omarchy_shell_bin" shell ping >/dev/null 2>&1; then - return 0 - fi - sleep 0.25 - done - - warn "Omarchy shell did not respond in the current session; it will start after reboot. See $shell_log" - return 1 } run_hyprctl_session() { diff --git a/shell/README.md b/shell/README.md index d57c681f..704bd71c 100644 --- a/shell/README.md +++ b/shell/README.md @@ -180,8 +180,8 @@ quickshell ipc -p $OMARCHY_PATH/shell call shell ping ``` Hyprland autostart launches the shell directly with `quickshell -p -$OMARCHY_PATH/shell`. Use `omarchy-restart-shell` (`quickshell reload`) to -reload the long-running shell process. +$OMARCHY_PATH/shell`. Use `omarchy-restart-shell` to stop every running +instance of that config and launch one fresh shell process. A convenience wrapper, [`omarchy-shell`](../bin/omarchy-shell), forwards IPC calls to the running shell. It does not start the shell. diff --git a/test/shell.d/restart-shell-test.sh b/test/shell.d/restart-shell-test.sh new file mode 100755 index 00000000..90d5cc4f --- /dev/null +++ b/test/shell.d/restart-shell-test.sh @@ -0,0 +1,128 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +test_tmp=$(mktemp -d) +trap 'rm -rf "$test_tmp"' EXIT + +wrapper_root="$test_tmp/wrapper-root" +wrapper_bin="$test_tmp/wrapper-bin" +mkdir -p "$wrapper_root/shell" "$wrapper_bin" +touch "$wrapper_root/shell/shell.qml" + +cat >"$wrapper_bin/qs" <<'SH' +#!/bin/bash + +if [[ ${OMARCHY_TEST_QS_HANG:-0} == 1 ]]; then + sleep 5 +else + printf 'ok\n' +fi +SH +chmod +x "$wrapper_bin/qs" + +wrapper_error=$(PATH="$wrapper_bin:$PATH" \ + OMARCHY_PATH="$wrapper_root" \ + OMARCHY_SHELL_IPC_TIMEOUT=0.1s \ + OMARCHY_TEST_QS_HANG=1 \ + "$ROOT/bin/omarchy-shell" shell ping 2>&1) && fail "hung shell IPC returns a failure" +[[ $wrapper_error == "omarchy-shell is not responding" ]] || fail "hung shell IPC reports that the shell is unresponsive" "$wrapper_error" +pass "shell IPC calls time out when Quickshell is unresponsive" + +restart_root="$test_tmp/restart-root" +restart_bin="$restart_root/bin" +restart_state="$test_tmp/restart-pids" +restart_log="$test_tmp/restart.log" +ipc_log="$test_tmp/ipc.log" +runtime_dir="$test_tmp/runtime" +mkdir -p "$restart_root/shell" "$restart_bin" "$runtime_dir" +touch "$restart_root/shell/shell.qml" +ln -s "$ROOT/bin/omarchy-shell" "$restart_bin/omarchy-shell" +ln -s "$ROOT/bin/omarchy-cmd-missing" "$restart_bin/omarchy-cmd-missing" + +cat >"$restart_bin/omarchy-hyprland-launch" <<'SH' +#!/bin/bash + +bash -c "$1" +SH + +cat >"$restart_bin/qs" <<'SH' +#!/bin/bash + +printf '%s\n' "$*" >>"$OMARCHY_TEST_IPC_LOG" + +case "$*" in + *'shell ping') + grep -Fx '303' "$OMARCHY_TEST_QS_STATE" >/dev/null && printf 'ok\n' + ;; +esac +SH + +cat >"$restart_bin/quickshell" <<'SH' +#!/bin/bash + +printf '%s\n' "$*" >>"$OMARCHY_TEST_QS_LOG" + +case " $* " in + *' kill -p '*) + pid=$(head -n 1 "$OMARCHY_TEST_QS_STATE") + [[ $pid =~ ^[0-9]+$ ]] || exit 1 + printf 'stopped:%s\n' "$pid" >>"$OMARCHY_TEST_QS_LOG" + awk 'NR > 1' "$OMARCHY_TEST_QS_STATE" >"$OMARCHY_TEST_QS_STATE.next" + mv "$OMARCHY_TEST_QS_STATE.next" "$OMARCHY_TEST_QS_STATE" + ;; + *' -n -p '*) + printf '303\n' >"$OMARCHY_TEST_QS_STATE" + ;; +esac +SH + +cat >"$restart_bin/hyprctl" <<'SH' +#!/bin/bash + +if [[ ${1:-} == "-j" && ${2:-} == "monitors" ]]; then + if [[ ${OMARCHY_TEST_SESSION_LOCKED:-0} == 1 ]]; then + printf '[{"activeWorkspace":{"name":"LOCK"}}]\n' + else + printf '[]\n' + fi +fi +SH + +chmod +x "$restart_bin/qs" "$restart_bin/quickshell" "$restart_bin/hyprctl" "$restart_bin/omarchy-hyprland-launch" + +printf '101\n202\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_IPC_LOG="$ipc_log" \ + timeout 5 "$ROOT/bin/omarchy-restart-shell" + +grep -F 'stopped:101' "$restart_log" >/dev/null || fail "restart stops the first matching shell instance" +grep -F 'stopped:202' "$restart_log" >/dev/null || fail "restart stops duplicate matching shell instances" +[[ $(<"$restart_state") == 303 ]] || fail "restart leaves exactly one fresh shell instance" +[[ $(grep -c '^-n -p ' "$restart_log") == 1 ]] || fail "restart launches one fresh shell process" +grep -F 'shell ping' "$ipc_log" >/dev/null || fail "restart waits for fresh shell IPC readiness" +pass "restart replaces duplicate shell instances" + +: >"$restart_log" +printf '404\n' >"$restart_state" + +locked_error=$(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_IPC_LOG="$ipc_log" \ + "$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" +[[ $(<"$restart_state") == 404 ]] || fail "locked restart preserves the running shell" +[[ ! -s $restart_log ]] || fail "locked restart does not stop or launch Quickshell" +pass "restart preserves the shell while its lock is active"