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 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-07-19 10:09:54 -07:00
co-authored by Claude Fable 5
parent 3edf254a12
commit 17c8af3f14
6 changed files with 169 additions and 113 deletions
+2 -1
View File
@@ -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
+20 -74
View File
@@ -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
+9 -1
View File
@@ -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"*)
+8 -35
View File
@@ -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() {
+2 -2
View File
@@ -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.
+128
View File
@@ -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"