From 92f9864c72976b388075be4015006296eff40449 Mon Sep 17 00:00:00 2001 From: Chemineer <144575703+chemineer1@users.noreply.github.com> Date: Wed, 23 Sep 2026 20:06:48 -0500 Subject: [PATCH 1/3] Prevent lid closure from interrupting shutdown --- bin/omarchy-system-shutdown | 28 +++++---- test/shell.d/system-power-test.sh | 97 +++++++++++++++++++++++-------- 2 files changed, 91 insertions(+), 34 deletions(-) diff --git a/bin/omarchy-system-shutdown b/bin/omarchy-system-shutdown index 0143760e..caa67882 100755 --- a/bin/omarchy-system-shutdown +++ b/bin/omarchy-system-shutdown @@ -4,14 +4,22 @@ # omarchy:examples=omarchy shutdown | omarchy system shutdown # omarchy:aliases=omarchy shutdown -# Schedule the shutdown in the user manager so closing the terminal's systemd -# scope cannot terminate it before it runs. -systemd-run --user --collect --quiet --on-active="2s" --timer-property=AccuracySec=100ms systemctl poweroff --no-wall || exit 1 +if [[ ${1:-} == "--inhibited" ]]; then + # Close windows in the background so they cannot delay power-off. + ( + omarchy-osd -i shutdown -m "Shutting down" -d 5000 + omarchy-state clear re*-required + omarchy-hyprland-window-close-all + ) & -omarchy-osd -i shutdown -m "Shutting down" -d 5000 - -omarchy-state clear re*-required - -# Now close all windows -omarchy-hyprland-window-close-all -sleep 1 # Allow apps like Chrome to shutdown correctly + sleep 2 # Allow apps like Chrome to shut down correctly + systemctl poweroff --no-wall +else + # Schedule the shutdown in the user manager so closing the terminal's systemd + # scope cannot terminate it before it runs. + # Block lid handling explicitly: it ignores high-level sleep inhibitors. + systemd-run --user --collect --quiet --property=Type=exec --property=RuntimeMaxSec=30s \ + systemd-inhibit --what=sleep:idle:handle-lid-switch \ + --who=Omarchy --why="Shutdown in progress" --mode=block \ + "$OMARCHY_PATH/bin/omarchy-system-shutdown" --inhibited +fi diff --git a/test/shell.d/system-power-test.sh b/test/shell.d/system-power-test.sh index 2c65b5e1..c988d0a2 100755 --- a/test/shell.d/system-power-test.sh +++ b/test/shell.d/system-power-test.sh @@ -8,20 +8,52 @@ trap 'rm -rf "$test_tmp"' EXIT mock_bin="$test_tmp/bin" call_log="$test_tmp/calls.log" mkdir -p "$mock_bin" +export PATH="$mock_bin:$PATH" CALL_LOG="$call_log" OMARCHY_PATH="$ROOT" cat >"$mock_bin/systemd-run" <<'SH' #!/bin/bash printf 'systemd-run %s\n' "$*" >>"$CALL_LOG" [[ ${FAIL_SYSTEMD_RUN:-false} == "true" ]] && exit 1 -exit 0 +[[ " $* " == *" --on-active="* ]] && exit 0 +while [[ $1 == --* ]]; do shift; done +# Capture the service command so its exit status is tested separately. +printf '%q ' "$@" >"$CALL_LOG.worker" SH -for command in omarchy-state omarchy-hyprland-window-close-all sleep; do +cat >"$mock_bin/systemd-inhibit" <<'SH' +#!/bin/bash + +printf 'systemd-inhibit %s\n' "$*" >>"$CALL_LOG" +[[ ${FAIL_INHIBIT:-false} == "true" ]] && exit 1 +while [[ $1 == --* ]]; do shift; done +touch "$CALL_LOG.inhibited" +trap 'rm -f "$CALL_LOG.inhibited"' EXIT +"$@" +SH + +for command in omarchy-state omarchy-hyprland-window-close-all omarchy-osd sleep systemctl; do cat >"$mock_bin/$command" <<'SH' #!/bin/bash -printf '%s %s\n' "$(basename "$0")" "$*" >>"$CALL_LOG" +command=${0##*/} +printf '%s %s\n' "$command" "$*" >>"$CALL_LOG" +case $command in + sleep) + if [[ $1 == "2" ]]; then + # Synchronize with background preparation without a fixed test-time sleep. + for (( attempt = 0; attempt < 200; attempt++ )); do + grep -q '^omarchy-hyprland-window-close-all ' "$CALL_LOG" && break + /usr/bin/sleep 0.01 + done + touch "$CALL_LOG.grace" + fi + ;; + systemctl) + [[ $* == "poweroff --no-wall" && -f $CALL_LOG.inhibited && -f $CALL_LOG.grace ]] || exit 2 + [[ ${FAIL_POWEROFF:-false} != "true" ]] + ;; +esac SH done chmod +x "$mock_bin"/* @@ -30,34 +62,36 @@ run_power_command() { local action="$1" : >"$call_log" - PATH="$mock_bin:$PATH" CALL_LOG="$call_log" "$ROOT/bin/omarchy-system-$action" -} - -assert_power_calls() { - local action="$1" - local systemctl_action="$2" - local expected_log="$test_tmp/$action-expected.log" - - cat >"$expected_log" <"$test_tmp/reboot-expected.log" +diff -u "$test_tmp/reboot-expected.log" "$call_log" || fail "reboot runs after being scheduled outside the terminal scope" +pass "reboot runs after being scheduled outside the terminal scope" -run_power_command shutdown -assert_power_calls shutdown poweroff +run_power_command shutdown || fail "shutdown service is scheduled" +grep -q '^systemd-run --user --collect --quiet --property=Type=exec --property=RuntimeMaxSec=30s systemd-inhibit ' "$CALL_LOG" || fail "shutdown uses a bounded service outside the launching terminal" +bash "$CALL_LOG.worker" || fail "protected shutdown succeeds" +grep -q '^systemd-inhibit --what=sleep:idle:handle-lid-switch .* --mode=block .* --inhibited$' "$CALL_LOG" || fail "shutdown blocks sleep and lid handling" +inhibit_line=$(grep -n '^systemd-inhibit ' "$CALL_LOG" | cut -d: -f1) +osd_line=$(grep -n '^omarchy-osd ' "$CALL_LOG" | cut -d: -f1) +(( inhibit_line < osd_line )) || fail "inhibition precedes preparation" +grep -q '^omarchy-state clear re\*-required$' "$CALL_LOG" || fail "shutdown clears restart state" +grep -q '^omarchy-hyprland-window-close-all ' "$CALL_LOG" || fail "shutdown closes windows" +grep -q '^systemctl poweroff --no-wall$' "$CALL_LOG" || fail "poweroff runs after the grace period while inhibited" +[[ ! -f $CALL_LOG.inhibited ]] || fail "accepted poweroff releases inhibition" +pass "shutdown stays inhibited through preparation and the poweroff request" for action in reboot shutdown; do : >"$call_log" - if PATH="$mock_bin:$PATH" CALL_LOG="$call_log" FAIL_SYSTEMD_RUN=true "$ROOT/bin/omarchy-system-$action"; then + if FAIL_SYSTEMD_RUN=true "$ROOT/bin/omarchy-system-$action"; then fail "$action aborts when scheduling fails" fi @@ -66,3 +100,18 @@ for action in reboot shutdown; do fi pass "$action leaves state and windows alone when scheduling fails" done + +for failure in FAIL_INHIBIT FAIL_POWEROFF; do + : >"$call_log" + rm -f "$CALL_LOG.grace" + if env "$failure=true" bash "$CALL_LOG.worker"; then + fail "$failure propagates to the service" + fi + [[ ! -f $CALL_LOG.inhibited ]] || fail "$failure releases inhibition" + if [[ $failure == "FAIL_INHIBIT" ]]; then + (( $(wc -l <"$call_log") == 1 )) || fail "inhibitor failure leaves applications alone" + else + grep -q '^systemctl poweroff --no-wall$' "$CALL_LOG" || fail "rejection test reaches poweroff" + fi + pass "$failure leaves no inhibitor behind" +done From eaaca77efb1d1b6cfda37a2b60c6a26772562fd6 Mon Sep 17 00:00:00 2001 From: Chemineer <144575703+chemineer1@users.noreply.github.com> Date: Fri, 25 Sep 2026 20:06:46 -0500 Subject: [PATCH 2/3] Report shutdown failures and test blocked window closing --- bin/omarchy-system-shutdown | 15 +++++++--- test/shell.d/system-power-test.sh | 50 +++++++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/bin/omarchy-system-shutdown b/bin/omarchy-system-shutdown index caa67882..f65ac9e7 100755 --- a/bin/omarchy-system-shutdown +++ b/bin/omarchy-system-shutdown @@ -14,12 +14,19 @@ if [[ ${1:-} == "--inhibited" ]]; then sleep 2 # Allow apps like Chrome to shut down correctly systemctl poweroff --no-wall +elif [[ ${1:-} == "--inhibit" ]]; then + # Report failures from inside the service, independently of the launching terminal. + # Block lid handling explicitly: it ignores high-level sleep inhibitors. + systemd-inhibit --what=sleep:idle:handle-lid-switch \ + --who=Omarchy --why="Shutdown in progress" --mode=block \ + "$OMARCHY_PATH/bin/omarchy-system-shutdown" --inhibited || { + status=$? + omarchy-notification-send -u critical "Shutdown failed" "Could not complete shutdown. Please try again." + exit "$status" + } else # Schedule the shutdown in the user manager so closing the terminal's systemd # scope cannot terminate it before it runs. - # Block lid handling explicitly: it ignores high-level sleep inhibitors. systemd-run --user --collect --quiet --property=Type=exec --property=RuntimeMaxSec=30s \ - systemd-inhibit --what=sleep:idle:handle-lid-switch \ - --who=Omarchy --why="Shutdown in progress" --mode=block \ - "$OMARCHY_PATH/bin/omarchy-system-shutdown" --inhibited + "$OMARCHY_PATH/bin/omarchy-system-shutdown" --inhibit fi diff --git a/test/shell.d/system-power-test.sh b/test/shell.d/system-power-test.sh index c988d0a2..027b8ad8 100755 --- a/test/shell.d/system-power-test.sh +++ b/test/shell.d/system-power-test.sh @@ -25,20 +25,36 @@ cat >"$mock_bin/systemd-inhibit" <<'SH' #!/bin/bash printf 'systemd-inhibit %s\n' "$*" >>"$CALL_LOG" -[[ ${FAIL_INHIBIT:-false} == "true" ]] && exit 1 +[[ ${FAIL_INHIBIT:-false} == "true" ]] && exit 17 while [[ $1 == --* ]]; do shift; done touch "$CALL_LOG.inhibited" trap 'rm -f "$CALL_LOG.inhibited"' EXIT "$@" SH -for command in omarchy-state omarchy-hyprland-window-close-all omarchy-osd sleep systemctl; do +for command in omarchy-state omarchy-hyprland-window-close-all omarchy-osd omarchy-notification-send sleep systemctl; do cat >"$mock_bin/$command" <<'SH' #!/bin/bash command=${0##*/} printf '%s %s\n' "$command" "$*" >>"$CALL_LOG" case $command in + omarchy-hyprland-window-close-all) + if [[ ${BLOCK_WINDOW_CLOSE:-false} == "true" ]]; then + for (( attempt = 0; attempt < 200; attempt++ )); do + if [[ -f $CALL_LOG.poweroff ]]; then + touch "$CALL_LOG.close-finished" + exit 0 + fi + /usr/bin/sleep 0.01 + done + touch "$CALL_LOG.close-timeout" + exit 1 + fi + ;; + omarchy-notification-send) + [[ ${FAIL_NOTIFICATION:-false} != "true" ]] || exit 42 + ;; sleep) if [[ $1 == "2" ]]; then # Synchronize with background preparation without a fixed test-time sleep. @@ -51,6 +67,7 @@ case $command in ;; systemctl) [[ $* == "poweroff --no-wall" && -f $CALL_LOG.inhibited && -f $CALL_LOG.grace ]] || exit 2 + touch "$CALL_LOG.poweroff" [[ ${FAIL_POWEROFF:-false} != "true" ]] ;; esac @@ -77,7 +94,7 @@ diff -u "$test_tmp/reboot-expected.log" "$call_log" || fail "reboot runs after b pass "reboot runs after being scheduled outside the terminal scope" run_power_command shutdown || fail "shutdown service is scheduled" -grep -q '^systemd-run --user --collect --quiet --property=Type=exec --property=RuntimeMaxSec=30s systemd-inhibit ' "$CALL_LOG" || fail "shutdown uses a bounded service outside the launching terminal" +grep -q '^systemd-run --user --collect --quiet --property=Type=exec --property=RuntimeMaxSec=30s .* --inhibit$' "$CALL_LOG" || fail "shutdown uses a bounded service outside the launching terminal" bash "$CALL_LOG.worker" || fail "protected shutdown succeeds" grep -q '^systemd-inhibit --what=sleep:idle:handle-lid-switch .* --mode=block .* --inhibited$' "$CALL_LOG" || fail "shutdown blocks sleep and lid handling" inhibit_line=$(grep -n '^systemd-inhibit ' "$CALL_LOG" | cut -d: -f1) @@ -87,8 +104,19 @@ grep -q '^omarchy-state clear re\*-required$' "$CALL_LOG" || fail "shutdown clea grep -q '^omarchy-hyprland-window-close-all ' "$CALL_LOG" || fail "shutdown closes windows" grep -q '^systemctl poweroff --no-wall$' "$CALL_LOG" || fail "poweroff runs after the grace period while inhibited" [[ ! -f $CALL_LOG.inhibited ]] || fail "accepted poweroff releases inhibition" +! grep -q '^omarchy-notification-send ' "$CALL_LOG" || fail "successful shutdown sends no failure notification" pass "shutdown stays inhibited through preparation and the poweroff request" +: >"$call_log" +rm -f "$CALL_LOG.grace" "$CALL_LOG.poweroff" +BLOCK_WINDOW_CLOSE=true bash "$CALL_LOG.worker" || fail "shutdown proceeds while window closing is blocked" +for (( attempt = 0; attempt < 200; attempt++ )); do + [[ -f $CALL_LOG.close-finished || -f $CALL_LOG.close-timeout ]] && break + /usr/bin/sleep 0.01 +done +[[ -f $CALL_LOG.close-finished && ! -f $CALL_LOG.close-timeout ]] || fail "poweroff releases the blocked window helper before its timeout" +pass "blocked window closing cannot delay poweroff" + for action in reboot shutdown; do : >"$call_log" if FAIL_SYSTEMD_RUN=true "$ROOT/bin/omarchy-system-$action"; then @@ -104,14 +132,24 @@ done for failure in FAIL_INHIBIT FAIL_POWEROFF; do : >"$call_log" rm -f "$CALL_LOG.grace" - if env "$failure=true" bash "$CALL_LOG.worker"; then + env "$failure=true" bash "$CALL_LOG.worker" + status=$? + expected_status=1 + [[ $failure == "FAIL_INHIBIT" ]] && expected_status=17 + if (( status != expected_status )); then fail "$failure propagates to the service" fi + grep -q '^omarchy-notification-send -u critical Shutdown failed Could not complete shutdown. Please try again.$' "$CALL_LOG" || fail "$failure notifies the user" [[ ! -f $CALL_LOG.inhibited ]] || fail "$failure releases inhibition" if [[ $failure == "FAIL_INHIBIT" ]]; then - (( $(wc -l <"$call_log") == 1 )) || fail "inhibitor failure leaves applications alone" + (( $(wc -l <"$call_log") == 2 )) || fail "inhibitor failure leaves applications alone" else grep -q '^systemctl poweroff --no-wall$' "$CALL_LOG" || fail "rejection test reaches poweroff" fi - pass "$failure leaves no inhibitor behind" + pass "$failure notifies the user and leaves no inhibitor behind" done + +: >"$call_log" +FAIL_INHIBIT=true FAIL_NOTIFICATION=true bash "$CALL_LOG.worker" +[[ $? == 17 ]] || fail "notification failure preserves the shutdown error" +pass "notification failure preserves the shutdown error" From bedc2a39792d0af05f80446af4806f7482c38327 Mon Sep 17 00:00:00 2001 From: Chemineer <144575703+chemineer1@users.noreply.github.com> Date: Sun, 27 Sep 2026 09:25:39 -0500 Subject: [PATCH 3/3] Keep shutdown worker on the same script path --- bin/omarchy-system-shutdown | 2 +- test/shell.d/system-power-test.sh | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/bin/omarchy-system-shutdown b/bin/omarchy-system-shutdown index f65ac9e7..74b091f5 100755 --- a/bin/omarchy-system-shutdown +++ b/bin/omarchy-system-shutdown @@ -19,7 +19,7 @@ elif [[ ${1:-} == "--inhibit" ]]; then # Block lid handling explicitly: it ignores high-level sleep inhibitors. systemd-inhibit --what=sleep:idle:handle-lid-switch \ --who=Omarchy --why="Shutdown in progress" --mode=block \ - "$OMARCHY_PATH/bin/omarchy-system-shutdown" --inhibited || { + "$0" --inhibited || { status=$? omarchy-notification-send -u critical "Shutdown failed" "Could not complete shutdown. Please try again." exit "$status" diff --git a/test/shell.d/system-power-test.sh b/test/shell.d/system-power-test.sh index 027b8ad8..baad2b80 100755 --- a/test/shell.d/system-power-test.sh +++ b/test/shell.d/system-power-test.sh @@ -107,6 +107,15 @@ grep -q '^systemctl poweroff --no-wall$' "$CALL_LOG" || fail "poweroff runs afte ! grep -q '^omarchy-notification-send ' "$CALL_LOG" || fail "successful shutdown sends no failure notification" pass "shutdown stays inhibited through preparation and the poweroff request" +: >"$call_log" +rm -f "$CALL_LOG.grace" "$CALL_LOG.poweroff" +# Dev link/unlink can leave the user manager pointing at a removed checkout. +OMARCHY_PATH="$test_tmp/removed-checkout" bash "$CALL_LOG.worker" || fail "shutdown uses its own script when the service environment is stale" +[[ -f $CALL_LOG.poweroff ]] || fail "shutdown reaches poweroff with a stale service environment" +[[ ! -f $CALL_LOG.inhibited ]] || fail "shutdown releases inhibition with a stale service environment" +! grep -q '^omarchy-notification-send ' "$CALL_LOG" || fail "stale service environment sends no failure notification" +pass "shutdown uses its own script when the service environment is stale" + : >"$call_log" rm -f "$CALL_LOG.grace" "$CALL_LOG.poweroff" BLOCK_WINDOW_CLOSE=true bash "$CALL_LOG.worker" || fail "shutdown proceeds while window closing is blocked"