Split update coordination helpers

This commit is contained in:
David Heinemeier Hansson
2026-07-29 20:04:49 -07:00
parent 695614ce6a
commit 8554801537
7 changed files with 295 additions and 98 deletions
+11 -76
View File
@@ -12,66 +12,19 @@ if [[ -z ${OMARCHY_UPDATE_LOGGED:-} ]]; then
exec env OMARCHY_UPDATE_LOGGED=1 script -qefc "$script_command" "/tmp/omarchy-update.log" exec env OMARCHY_UPDATE_LOGGED=1 script -qefc "$script_command" "/tmp/omarchy-update.log"
fi fi
acquire_update_lock() { if ! omarchy-update-lock held; then
local lock_dir="${XDG_RUNTIME_DIR:-/tmp}" exec omarchy-update-lock run "$0" "$@"
local lock_path="$lock_dir/omarchy-update.lock" fi
local lock_fd_path=""
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 omarchy-update-requires-free-space
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
exec {OMARCHY_UPDATE_LOCK_FD}>"$lock_path" if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
if ! flock -n "$OMARCHY_UPDATE_LOCK_FD"; then omarchy-snapshot create || (($? == 127))
echo "An Omarchy update is already running."
exit 1
fi
export OMARCHY_UPDATE_LOCK_FD
}
update_disabled_idle=0 omarchy-update-stay-awake start
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-dev omarchy-update-dev
omarchy-update-keyring omarchy-update-keyring
@@ -83,31 +36,13 @@ run_update_pipeline() {
omarchy-update-orphan-pkgs omarchy-update-orphan-pkgs
omarchy-update-analyze-logs omarchy-update-analyze-logs
omarchy-update-status
# 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
# Release update-owned inhibitors before offering a reboot. A confirmed # Release update-owned inhibitors before offering a reboot. A confirmed
# reboot can terminate this process before its EXIT trap gets a chance to # reboot can terminate this process before its EXIT trap gets a chance to
# remove the persistent Stay Awake marker. # remove the persistent Stay Awake marker.
restore_update_inhibitors omarchy-update-stay-awake stop
trap - EXIT trap - EXIT
omarchy-update-restart 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 fi
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
# omarchy:summary=Run a command while holding the Omarchy update lock
# omarchy:args=<held|run> [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 <command> [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 <held|run> [command] [args...]" >&2
exit 2
;;
esac
+12
View File
@@ -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
+136
View File
@@ -0,0 +1,136 @@
#!/bin/bash
# omarchy:summary=Manage sleep and idle inhibition during an update
# omarchy:args=<start|stop>
# 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=$(</proc/"$process_pid"/stat)
process_stat="${process_stat##*) }"
read -r -a stat_fields <<<"$process_stat"
(( ${#stat_fields[@]} > 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=$(</proc/"$process_pid"/stat)
process_stat="${process_stat##*) }"
printf '%s\n' "${process_stat%% *}"
}
stop() {
local inhibit_pid=""
local recorded_start_time=""
local current_start_time=""
local idle_owner=""
local current_idle_owner=""
if [[ -s $idle_owner_file ]]; then
idle_owner=$(<"$idle_owner_file")
if [[ -f $stay_awake_state ]]; then
current_idle_owner=$(<"$stay_awake_state")
fi
if [[ -n $idle_owner && $current_idle_owner == "$idle_owner" ]]; then
omarchy-toggle-idle allow-idle >/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 <start|stop>" >&2
exit 2
;;
esac
+19 -22
View File
@@ -21,7 +21,7 @@ The design goal is:
| Path | Owner | Purpose | | 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`. | | `/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/current/` | user | Generated active theme, selected theme name, and current background symlink. |
| `~/.local/state/omarchy/migrations/` | user | Per-user migration markers. | | `~/.local/state/omarchy/migrations/` | user | Per-user migration markers. |
@@ -113,25 +113,19 @@ High-level flow:
```text ```text
omarchy-update omarchy-update
├─ ensure transcript logging through script(1) → /tmp/omarchy-update.log ├─ 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 ├─ omarchy-update-requires-free-space
│ └─ check free space on / and warn below the configured threshold │ └─ check free space on / and warn below the configured threshold
├─ confirm unless -y ├─ confirm unless -y
├─ create snapper snapshot, if snapper is installed ├─ create snapper snapshot, if snapper is installed
run update pipeline omarchy-update-stay-awake start
├─ block system sleep and temporarily enable shell stay-awake mode ├─ run package updates, migrations, hooks, and log analysis
├─ omarchy-update-dev ├─ omarchy-update-status
├─ omarchy-update-keyring │ └─ refresh or clear the shell update indicator
├─ omarchy-update-system-pkgs ├─ omarchy-update-stay-awake stop
├─ omarchy-migrate │ └─ release the sleep inhibitor and restore shell idle state, if changed
─ omarchy-hook post-update ─ omarchy-update-restart
├─ 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
``` ```
Important behavior: 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 silence another user's notification. Missing an update and showing a redundant
toast is the better failure. toast is the better failure.
Suppression is why `omarchy-update` starts its sleep inhibitor with the lock Suppression is why `omarchy-update-stay-awake` starts its sleep inhibitor with
descriptor closed. That inhibitor outlives the step that starts it, so an update the lock descriptor closed. That inhibitor outlives the step that starts it, so
killed before `restore_update_inhibitors` would otherwise leave it holding the an update killed before cleanup would otherwise leave it holding the flock
flock indefinitely — blocking later updates and, now that the notifier reads the indefinitely — blocking later updates and, now that the notifier reads the same
same lock, silencing migration notifications at every login. lock, silencing migration notifications at every login.
Fallbacks: Fallbacks:
@@ -249,7 +243,10 @@ scripts.
| Binary | Current purpose | Keep? / Question | | 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-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-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. | | `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. |
+31
View File
@@ -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" (( 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" 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 # 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. # reboot the machine, rather than relying on an EXIT trap during shutdown.
write_stub omarchy-snapshot 'exit 0' 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 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" [[ -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" 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"
+39
View File
@@ -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"