diff --git a/bin/omarchy-update b/bin/omarchy-update index a1ae5942..ccf0884b 100755 --- a/bin/omarchy-update +++ b/bin/omarchy-update @@ -12,66 +12,19 @@ if [[ -z ${OMARCHY_UPDATE_LOGGED:-} ]]; then exec env OMARCHY_UPDATE_LOGGED=1 script -qefc "$script_command" "/tmp/omarchy-update.log" fi -acquire_update_lock() { - local lock_dir="${XDG_RUNTIME_DIR:-/tmp}" - local lock_path="$lock_dir/omarchy-update.lock" - local lock_fd_path="" +if ! omarchy-update-lock held; then + exec omarchy-update-lock run "$0" "$@" +fi - mkdir -p "$lock_dir" 2>/dev/null || true +trap 'echo ""; echo -e "\033[0;31mSomething went wrong during the update!\n\nPlease review the output above carefully, correct the error, and retry the update.\n\nIf you need assistance, get help from the community at https://omarchy.org/discord\033[0m"' ERR +trap 'omarchy-update-stay-awake stop' EXIT - if [[ -n ${OMARCHY_UPDATE_LOCK_FD:-} && -e /proc/$$/fd/$OMARCHY_UPDATE_LOCK_FD ]]; then - lock_fd_path=$(readlink -f "/proc/$$/fd/$OMARCHY_UPDATE_LOCK_FD" 2>/dev/null || true) - if [[ $lock_fd_path == "$(readlink -m "$lock_path")" ]] && flock -n "$OMARCHY_UPDATE_LOCK_FD"; then - return 0 - fi - fi +omarchy-update-requires-free-space - exec {OMARCHY_UPDATE_LOCK_FD}>"$lock_path" - if ! flock -n "$OMARCHY_UPDATE_LOCK_FD"; then - echo "An Omarchy update is already running." - exit 1 - fi - export OMARCHY_UPDATE_LOCK_FD -} +if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then + omarchy-snapshot create || (($? == 127)) -update_disabled_idle=0 -sleep_inhibit_pid="" - -disable_sleep_for_update() { - omarchy-cmd-present systemd-inhibit || return 0 - - systemd-inhibit \ - --what=sleep:idle \ - --who=omarchy-update \ - --why="Omarchy update in progress" \ - --mode=block \ - sleep infinity >/dev/null 2>&1 {OMARCHY_UPDATE_LOCK_FD}>&- & - sleep_inhibit_pid=$! -} - -disable_idle_for_update() { - local stay_awake_state="$HOME/.local/state/omarchy/indicators/stay-awake" - - if [[ ! -f $stay_awake_state ]]; then - omarchy-toggle-idle stay-awake >/dev/null 2>&1 || true - update_disabled_idle=1 - fi -} - -restore_update_inhibitors() { - if (( update_disabled_idle )); then - omarchy-toggle-idle allow-idle >/dev/null 2>&1 || true - fi - - if [[ -n $sleep_inhibit_pid ]]; then - kill "$sleep_inhibit_pid" >/dev/null 2>&1 || true - wait "$sleep_inhibit_pid" >/dev/null 2>&1 || true - fi -} - -run_update_pipeline() { - disable_sleep_for_update - disable_idle_for_update + omarchy-update-stay-awake start omarchy-update-dev omarchy-update-keyring @@ -83,31 +36,13 @@ run_update_pipeline() { omarchy-update-orphan-pkgs omarchy-update-analyze-logs - - # Re-check after updates so the status bar reflects any remaining updates. - if omarchy-update-available >/dev/null; then - omarchy-shell -q omarchy.system-update refresh - else - omarchy-shell -q omarchy.system-update clear - fi + omarchy-update-status # Release update-owned inhibitors before offering a reboot. A confirmed # reboot can terminate this process before its EXIT trap gets a chance to # remove the persistent Stay Awake marker. - restore_update_inhibitors + omarchy-update-stay-awake stop trap - EXIT omarchy-update-restart -} - -acquire_update_lock - -omarchy-update-requires-free-space - -trap restore_update_inhibitors EXIT -trap 'echo ""; echo -e "\033[0;31mSomething went wrong during the update!\n\nPlease review the output above carefully, correct the error, and retry the update.\n\nIf you need assistance, get help from the community at https://omarchy.org/discord\033[0m"' ERR - -if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then - omarchy-snapshot create || (($? == 127)) - run_update_pipeline fi diff --git a/bin/omarchy-update-lock b/bin/omarchy-update-lock new file mode 100755 index 00000000..ba3c97b6 --- /dev/null +++ b/bin/omarchy-update-lock @@ -0,0 +1,47 @@ +#!/bin/bash + +# omarchy:summary=Run a command while holding the Omarchy update lock +# omarchy:args= [command] [args...] +# omarchy:hidden=true + +set -e + +lock_dir="${XDG_RUNTIME_DIR:-/tmp}" +lock_path="$lock_dir/omarchy-update.lock" + +lock_is_held() { + local lock_fd_path="" + + [[ -n ${OMARCHY_UPDATE_LOCK_FD:-} && -e /proc/$$/fd/$OMARCHY_UPDATE_LOCK_FD ]] || return 1 + + lock_fd_path=$(readlink -f "/proc/$$/fd/$OMARCHY_UPDATE_LOCK_FD" 2>/dev/null || true) + [[ $lock_fd_path == "$(readlink -m "$lock_path")" ]] && + flock -n "$OMARCHY_UPDATE_LOCK_FD" +} + +case "${1:-}" in + held) + lock_is_held + ;; + run) + shift + if (( $# == 0 )); then + echo "Usage: omarchy-update-lock run [args...]" >&2 + exit 2 + fi + + mkdir -p "$lock_dir" 2>/dev/null || true + exec {OMARCHY_UPDATE_LOCK_FD}>"$lock_path" + if ! flock -n "$OMARCHY_UPDATE_LOCK_FD"; then + echo "An Omarchy update is already running." + exit 1 + fi + + export OMARCHY_UPDATE_LOCK_FD + exec "$@" + ;; + *) + echo "Usage: omarchy-update-lock [command] [args...]" >&2 + exit 2 + ;; +esac diff --git a/bin/omarchy-update-status b/bin/omarchy-update-status new file mode 100755 index 00000000..1ff5ea8d --- /dev/null +++ b/bin/omarchy-update-status @@ -0,0 +1,12 @@ +#!/bin/bash + +# omarchy:summary=Refresh the shell update status +# omarchy:hidden=true + +set -e + +if omarchy-update-available >/dev/null; then + omarchy-shell -q omarchy.system-update refresh +else + omarchy-shell -q omarchy.system-update clear +fi diff --git a/bin/omarchy-update-stay-awake b/bin/omarchy-update-stay-awake new file mode 100755 index 00000000..c78872c4 --- /dev/null +++ b/bin/omarchy-update-stay-awake @@ -0,0 +1,136 @@ +#!/bin/bash + +# omarchy:summary=Manage sleep and idle inhibition during an update +# omarchy:args= +# omarchy:hidden=true + +set -e + +state_dir="${XDG_RUNTIME_DIR:-/tmp/omarchy-$UID}/omarchy-update-stay-awake" +idle_owner_file="$state_dir/idle-owner" +inhibit_pid_file="$state_dir/inhibit-pid" +stay_awake_state="$HOME/.local/state/omarchy/indicators/stay-awake" + +process_start_time() { + local process_pid="$1" + local process_stat="" + local stat_fields=() + + [[ -r /proc/$process_pid/stat ]] || return 1 + process_stat=$( 19 )) || return 1 + printf '%s\n' "${stat_fields[19]}" +} + +process_state() { + local process_pid="$1" + local process_stat="" + + [[ -r /proc/$process_pid/stat ]] || return 1 + process_stat=$(/dev/null 2>&1 || true + fi + rm -f "$idle_owner_file" + fi + + if [[ -s $inhibit_pid_file ]]; then + read -r inhibit_pid recorded_start_time <"$inhibit_pid_file" || true + if [[ $inhibit_pid =~ ^[0-9]+$ ]]; then + current_start_time=$(process_start_time "$inhibit_pid" 2>/dev/null || true) + fi + if [[ -n $recorded_start_time && $current_start_time == "$recorded_start_time" ]]; then + kill "$inhibit_pid" >/dev/null 2>&1 || true + + for (( attempt = 0; attempt < 50; attempt++ )); do + current_start_time=$(process_start_time "$inhibit_pid" 2>/dev/null || true) + [[ $current_start_time == "$recorded_start_time" ]] || break + [[ $(process_state "$inhibit_pid" 2>/dev/null || true) != "Z" ]] || break + sleep 0.02 + done + + current_start_time=$(process_start_time "$inhibit_pid" 2>/dev/null || true) + if [[ $current_start_time == "$recorded_start_time" ]] && + [[ $(process_state "$inhibit_pid" 2>/dev/null || true) != "Z" ]]; then + echo "Failed to stop the Omarchy update sleep inhibitor." >&2 + return 1 + fi + fi + rm -f "$inhibit_pid_file" + fi + + rmdir "$state_dir" 2>/dev/null || true +} + +start() { + local inhibit_pid="" + local inhibit_start_time="" + local idle_owner="$$:$RANDOM:$RANDOM" + + stop + mkdir -p "$state_dir" + + if omarchy-cmd-present systemd-inhibit; then + if [[ -n ${OMARCHY_UPDATE_LOCK_FD:-} ]]; then + systemd-inhibit \ + --what=sleep:idle \ + --who=omarchy-update \ + --why="Omarchy update in progress" \ + --mode=block \ + sleep infinity >/dev/null 2>&1 {OMARCHY_UPDATE_LOCK_FD}>&- & + else + systemd-inhibit \ + --what=sleep:idle \ + --who=omarchy-update \ + --why="Omarchy update in progress" \ + --mode=block \ + sleep infinity >/dev/null 2>&1 & + fi + inhibit_pid=$! + inhibit_start_time=$(process_start_time "$inhibit_pid" 2>/dev/null || true) + if [[ -n $inhibit_start_time ]]; then + printf '%s %s\n' "$inhibit_pid" "$inhibit_start_time" >"$inhibit_pid_file" + fi + fi + + if [[ ! -f $stay_awake_state ]]; then + printf '%s\n' "$idle_owner" >"$idle_owner_file" + mkdir -p "$(dirname "$stay_awake_state")" + if omarchy-toggle-idle stay-awake >/dev/null 2>&1; then + printf '%s\n' "$idle_owner" >"$stay_awake_state" + else + rm -f "$idle_owner_file" + fi + fi +} + +case "${1:-}" in + start) + start + ;; + stop) + stop + ;; + *) + echo "Usage: omarchy-update-stay-awake " >&2 + exit 2 + ;; +esac diff --git a/docs/update-process.md b/docs/update-process.md index 9a0825c8..80520acc 100644 --- a/docs/update-process.md +++ b/docs/update-process.md @@ -21,7 +21,7 @@ The design goal is: | Path | Owner | Purpose | | --- | --- | --- | -| `${XDG_RUNTIME_DIR:-/tmp}/omarchy-update.lock` | user | Prevent overlapping update runs. Owned by `omarchy-update`; compatibility wrappers inherit/respect it. | +| `${XDG_RUNTIME_DIR:-/tmp}/omarchy-update.lock` | user | Prevent overlapping update runs. Owned by `omarchy-update-lock`; compatibility wrappers inherit/respect it. | | `/tmp/omarchy-update.log` | user | Transcript of `omarchy update`, used by `omarchy-update-analyze-logs`. | | `~/.local/state/omarchy/current/` | user | Generated active theme, selected theme name, and current background symlink. | | `~/.local/state/omarchy/migrations/` | user | Per-user migration markers. | @@ -113,25 +113,19 @@ High-level flow: ```text omarchy-update ├─ ensure transcript logging through script(1) → /tmp/omarchy-update.log - ├─ acquire update lock + ├─ omarchy-update-lock + │ └─ acquire the update lock and run omarchy-update inside it ├─ omarchy-update-requires-free-space │ └─ check free space on / and warn below the configured threshold ├─ confirm unless -y ├─ create snapper snapshot, if snapper is installed - └─ run update pipeline - ├─ block system sleep and temporarily enable shell stay-awake mode - ├─ omarchy-update-dev - ├─ omarchy-update-keyring - ├─ omarchy-update-system-pkgs - ├─ omarchy-migrate - ├─ omarchy-hook post-update - ├─ omarchy-update-aur-pkgs - ├─ omarchy-update-mise - ├─ omarchy-update-orphan-pkgs - ├─ omarchy-update-analyze-logs - ├─ omarchy-update-available, then refresh/clear shell indicator - ├─ omarchy-update-restart - └─ release sleep inhibitor and restore shell idle state, if changed + ├─ omarchy-update-stay-awake start + ├─ run package updates, migrations, hooks, and log analysis + ├─ omarchy-update-status + │ └─ refresh or clear the shell update indicator + ├─ omarchy-update-stay-awake stop + │ └─ release the sleep inhibitor and restore shell idle state, if changed + └─ omarchy-update-restart ``` Important behavior: @@ -184,11 +178,11 @@ file belongs to whoever created it first, so honouring it would let one user silence another user's notification. Missing an update and showing a redundant toast is the better failure. -Suppression is why `omarchy-update` starts its sleep inhibitor with the lock -descriptor closed. That inhibitor outlives the step that starts it, so an update -killed before `restore_update_inhibitors` would otherwise leave it holding the -flock indefinitely — blocking later updates and, now that the notifier reads the -same lock, silencing migration notifications at every login. +Suppression is why `omarchy-update-stay-awake` starts its sleep inhibitor with +the lock descriptor closed. That inhibitor outlives the step that starts it, so +an update killed before cleanup would otherwise leave it holding the flock +indefinitely — blocking later updates and, now that the notifier reads the same +lock, silencing migration notifications at every login. Fallbacks: @@ -249,7 +243,10 @@ scripts. | Binary | Current purpose | Keep? / Question | | --- | --- | --- | -| `omarchy-update` | Public user command. Adds transcript logging, lock, confirmation, snapshot, sleep/idle inhibitors, package updates, migrations, hooks, update-state refresh, and restart checks. | **Keep.** This is the blessed entry point and owns the update pipeline. | +| `omarchy-update` | Public user command. Adds transcript logging, confirmation, snapshot, and restart checks around the locked, sleep-inhibited update pipeline. | **Keep.** This is the blessed entry point and orchestrates the update pipeline. | +| `omarchy-update-lock` | Hidden command wrapper that holds the per-user update lock while its child runs. | **Keep internal/hidden.** Isolates update concurrency and lock descriptor handling. | +| `omarchy-update-stay-awake` | Hidden helper that starts or stops update-owned sleep and idle inhibition, restoring only the state it changed. | **Keep internal/hidden.** Keeps inhibitor ownership and cleanup together. | +| `omarchy-update-status` | Hidden helper that refreshes or clears the shell update indicator after rechecking available updates. | **Keep internal/hidden.** Keeps shell status synchronization out of the main pipeline. | | `omarchy-update-perform` | Hidden compatibility wrapper for `omarchy-update -y`. | **Temporary.** Keep only for old callers; new code should call `omarchy-update` directly. | | `omarchy-update-confirm` | Gum confirmation copy for `omarchy update`. | **Question.** Could be inlined into `omarchy-update`; separate file only helps keep copy isolated. | | `omarchy-update-dev` | Fast-forwards the active dev-linked checkout from its configured upstream; no-ops for package-backed installs. | **Keep.** Runs before package updates so a checkout conflict stops the update before system mutation. | diff --git a/test/shell.d/update-lock-test.sh b/test/shell.d/update-lock-test.sh index 4d333ec5..64699e1c 100644 --- a/test/shell.d/update-lock-test.sh +++ b/test/shell.d/update-lock-test.sh @@ -136,6 +136,10 @@ wait "$inhibit_update_pid" (( inhibitor_holds_lock == 0 )) || fail "update keeps the update lock out of the sleep inhibitor it leaves running" pass "omarchy-update keeps the update lock out of its sleep inhibitor" +kill -0 "$inhibitor_pid" 2>/dev/null && + fail "update waits for its sleep inhibitor to stop before continuing" +pass "omarchy-update waits for its sleep inhibitor to stop" + # Update-owned Stay Awake state must be cleared before the restart helper can # reboot the machine, rather than relying on an EXIT trap during shutdown. write_stub omarchy-snapshot 'exit 0' @@ -168,3 +172,30 @@ touch "$test_home/.local/state/omarchy/indicators/stay-awake" OMARCHY_UPDATE_LOGGED=1 EXPECT_STAY_AWAKE=1 run_with_lock_env "$ROOT/bin/omarchy-update" -y [[ -f $test_home/.local/state/omarchy/indicators/stay-awake ]] || fail "update preserves pre-existing Stay Awake state" pass "omarchy-update restores only its own Stay Awake state before restart handling" + +# Stale cleanup state from a killed update must not override a Stay Awake choice +# the user made afterward. +stay_awake_helper_state="$runtime_dir/omarchy-update-stay-awake" +stay_awake_state="$test_home/.local/state/omarchy/indicators/stay-awake" +mkdir -p "$stay_awake_helper_state" "$(dirname "$stay_awake_state")" +printf '%s\n' "old-update-owner" >"$stay_awake_helper_state/idle-owner" +printf '%s\n' "user-choice" >"$stay_awake_state" + +run_with_lock_env "$ROOT/bin/omarchy-update-stay-awake" stop +[[ $(<"$stay_awake_state") == "user-choice" ]] || + fail "stale update ownership does not remove a newer Stay Awake choice" +pass "stale update ownership preserves a newer Stay Awake choice" + +# A stale PID is safe even if it has been reused by another process. +sleep 30 & +unrelated_pid=$! +unrelated_start_time=$(awk '{ print $22 }' "/proc/$unrelated_pid/stat") +mkdir -p "$stay_awake_helper_state" +printf '%s %s\n' "$unrelated_pid" "$((unrelated_start_time + 1))" >"$stay_awake_helper_state/inhibit-pid" + +run_with_lock_env "$ROOT/bin/omarchy-update-stay-awake" stop +kill -0 "$unrelated_pid" 2>/dev/null || + fail "stale inhibitor state does not terminate a reused PID" +kill "$unrelated_pid" +wait "$unrelated_pid" 2>/dev/null || true +pass "stale inhibitor state does not terminate a reused PID" diff --git a/test/shell.d/update-status-test.sh b/test/shell.d/update-status-test.sh new file mode 100755 index 00000000..95541fd1 --- /dev/null +++ b/test/shell.d/update-status-test.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "$0")/base-test.sh" + +test_tmp=$(mktemp -d) +trap 'rm -rf "$test_tmp"' EXIT + +stub_bin="$test_tmp/bin" +shell_calls="$test_tmp/shell-calls" +mkdir -p "$stub_bin" + +cat >"$stub_bin/omarchy-update-available" <<'SH' +#!/bin/bash + +exit "${UPDATE_AVAILABLE_STATUS:-0}" +SH + +cat >"$stub_bin/omarchy-shell" <<'SH' +#!/bin/bash + +printf '%s\n' "$*" >>"$SHELL_CALLS" +SH + +chmod +x "$stub_bin/omarchy-update-available" "$stub_bin/omarchy-shell" + +PATH="$stub_bin:$PATH" SHELL_CALLS="$shell_calls" UPDATE_AVAILABLE_STATUS=0 \ + "$ROOT/bin/omarchy-update-status" +grep -Fx -- "-q omarchy.system-update refresh" "$shell_calls" >/dev/null || + fail "update status refreshes the shell indicator when updates remain" +pass "update status refreshes the shell indicator when updates remain" + +: >"$shell_calls" +PATH="$stub_bin:$PATH" SHELL_CALLS="$shell_calls" UPDATE_AVAILABLE_STATUS=1 \ + "$ROOT/bin/omarchy-update-status" +grep -Fx -- "-q omarchy.system-update clear" "$shell_calls" >/dev/null || + fail "update status clears the shell indicator when no updates remain" +pass "update status clears the shell indicator when no updates remain"