Files
omarchycn/test/shell.d/update-lock-test.sh
T
fd23ca023a Prune the package cache before updating (#6734)
The pacman cache grows without bound across updates, and nothing in the
update flow ever reclaimed it. On a machine that has been updating for a
while it reaches several gigabytes of superseded versions that nothing
will ever install again.

Prune it with paccache -rk2 as the first step of an update. Both halves
of that placement are load-bearing.

Keeping two versions rather than one preserves the rollback path. The
cache is Arch's only offline downgrade: when an update breaks a single
package, reinstalling its predecessor from here is the surgical fix,
where a snapshot rollback would revert every other package too. Pruning
before the packages update means the installed version is still the
newest cached, so it survives along with a spare. Retention is by
version order and never consults what is installed, so that holds while
the installed version is among the two newest cached; a deliberate
downgrade or repeated failed transactions can stack newer archives on
top of it.

Running before the snapshot is what actually frees the space. The cache
sits on the snapshotted root subvolume, so a prune taken afterwards
leaves the fresh snapshot holding those extents and reclaims nothing
until it ages out of the number cleanup.

A failed prune warns and continues. Cache housekeeping should not trip
the update's ERR trap and tell the user their update went wrong.

This runs after omarchy-update-requires-free-space, so it reclaims space
during healthy updates but does not rescue a machine already under the
10 GiB gate.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 11:56:02 +02:00

215 lines
7.9 KiB
Bash

#!/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"
test_home="$test_tmp/home"
runtime_dir="$test_tmp/runtime"
mkdir -p "$stub_bin" "$test_home" "$runtime_dir"
run_with_lock_env() {
HOME="$test_home" \
XDG_RUNTIME_DIR="$runtime_dir" \
XDG_STATE_HOME="$test_tmp/state" \
PATH="$stub_bin:$ROOT/bin:$PATH" \
"$@"
}
write_stub() {
local name="$1"
local body="$2"
cat >"$stub_bin/$name" <<SH
#!/bin/bash
$body
SH
chmod +x "$stub_bin/$name"
}
for command in \
omarchy-toggle-idle \
pkexec \
systemd-inhibit \
omarchy-update-pkg-prune \
omarchy-update-dev \
omarchy-update-keyring \
omarchy-update-system-pkgs \
omarchy-migrate \
omarchy-update-aur-pkgs \
omarchy-update-mise \
omarchy-update-orphan-pkgs \
omarchy-hook \
omarchy-update-analyze-logs \
omarchy-shell \
omarchy-update-restart; do
write_stub "$command" 'exit 0'
done
write_stub omarchy-update-available 'exit 1'
write_stub pkexec 'exec "$@"'
# omarchy-update should hold the lock before snapshotting, so a second update
# cannot even enter its pre-update snapshot.
update_snapshot_marker="$test_tmp/update-snapshot-started"
write_stub omarchy-snapshot 'echo started >"$TEST_MARKER"; sleep 2; exit 0'
OMARCHY_UPDATE_LOGGED=1 TEST_MARKER="$update_snapshot_marker" run_with_lock_env "$ROOT/bin/omarchy-update" -y >"$test_tmp/update-first.out" 2>&1 &
update_pid=$!
for _ in {1..50}; do
[[ -f $update_snapshot_marker ]] && break
sleep 0.05
done
[[ -f $update_snapshot_marker ]] || fail "first omarchy-update reached snapshot under lock"
set +e
OMARCHY_UPDATE_LOGGED=1 TEST_MARKER="$test_tmp/update-second-snapshot-started" run_with_lock_env "$ROOT/bin/omarchy-update" -y >"$test_tmp/update-second.out" 2>&1
update_second_status=$?
set -e
wait "$update_pid"
[[ $update_second_status -ne 0 ]] || fail "second omarchy-update exits non-zero while update lock is held"
grep -q "already running" "$test_tmp/update-second.out" || fail "second omarchy-update reports held update lock"
[[ ! -f $test_tmp/update-second-snapshot-started ]] || fail "second omarchy-update did not snapshot while lock was held"
pass "omarchy-update prevents overlapping top-level updates"
# The sleep inhibitor deliberately outlives the step that starts it, so it must
# not inherit the update lock. An update killed before restore_update_inhibitors
# would otherwise leave the inhibitor holding the flock forever, blocking every
# later update and silencing omarchy-migrate-notify, which reads the same lock.
inhibit_pid_file="$test_tmp/inhibit-pid"
keyring_marker="$test_tmp/keyring-started"
write_stub omarchy-snapshot 'exit 0'
write_stub systemd-inhibit 'echo "$$" >"$INHIBIT_PID_FILE"; exec sleep 30'
write_stub omarchy-update-keyring 'echo started >"$TEST_MARKER"; sleep 3; exit 0'
OMARCHY_UPDATE_LOGGED=1 TEST_MARKER="$keyring_marker" INHIBIT_PID_FILE="$inhibit_pid_file" \
run_with_lock_env "$ROOT/bin/omarchy-update" -y >"$test_tmp/update-inhibit.out" 2>&1 &
inhibit_update_pid=$!
for _ in {1..100}; do
[[ -s $inhibit_pid_file && -f $keyring_marker ]] && break
sleep 0.05
done
[[ -s $inhibit_pid_file ]] || fail "update starts its sleep inhibitor"
inhibitor_pid=$(<"$inhibit_pid_file")
kill -0 "$inhibitor_pid" 2>/dev/null || fail "sleep inhibitor is still running when its descriptors are inspected"
lock_target=$(readlink -f "$runtime_dir/omarchy-update.lock")
inhibitor_holds_lock=0
for fd in /proc/"$inhibitor_pid"/fd/*; do
[[ -e $fd ]] || continue
[[ $(readlink -f "$fd" 2>/dev/null) == "$lock_target" ]] && inhibitor_holds_lock=1
done
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"
if (( EUID != 0 )); then
sudo_log="$test_tmp/sudo.log"
pkexec_marker="$test_tmp/pkexec-used"
terminal_inhibit_pid_file="$test_tmp/terminal-inhibit-pid"
write_stub sudo '
printf "%s\n" "$*" >>"$SUDO_LOG"
if [[ $1 == "-v" ]]; then
exit 0
fi
exec "$@"'
write_stub pkexec 'touch "$PKEXEC_MARKER"; exec "$@"'
# start leaves the inhibitor running on purpose, but script tears the pty down
# the moment its command returns, which SIGHUPs that inhibitor before it can
# exec. Keep the session open from the inside until the stub has logged.
terminal_driver="$test_tmp/terminal-stay-awake"
cat >"$terminal_driver" <<'SH'
#!/bin/bash
omarchy-update-stay-awake start
for _ in {1..200}; do
grep -q '^systemd-inhibit ' "$SUDO_LOG" && break
sleep 0.05
done
SH
chmod +x "$terminal_driver"
SUDO_LOG="$sudo_log" PKEXEC_MARKER="$pkexec_marker" INHIBIT_PID_FILE="$terminal_inhibit_pid_file" \
run_with_lock_env script -qefc "$terminal_driver" /dev/null >/dev/null
grep -qx -- '-v' "$sudo_log" || fail "terminal sleep inhibition validates sudo in the foreground"
grep -q '^systemd-inhibit ' "$sudo_log" || fail "terminal sleep inhibition runs through sudo"
[[ ! -e $pkexec_marker ]] || fail "terminal sleep inhibition does not use pkexec"
run_with_lock_env "$ROOT/bin/omarchy-update-stay-awake" stop
pass "terminal updates use sudo instead of Polkit for sleep inhibition"
fi
# 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'
write_stub omarchy-update-keyring 'exit 0'
write_stub omarchy-toggle-idle '
state_file="$HOME/.local/state/omarchy/indicators/stay-awake"
case "$1" in
stay-awake)
mkdir -p "$(dirname "$state_file")"
touch "$state_file"
;;
allow-idle)
rm -f "$state_file"
;;
esac'
write_stub omarchy-update-restart '
state_file="$HOME/.local/state/omarchy/indicators/stay-awake"
if [[ ${EXPECT_STAY_AWAKE:-0} == "1" ]]; then
[[ -f $state_file ]]
else
[[ ! -f $state_file ]]
fi'
rm -f "$test_home/.local/state/omarchy/indicators/stay-awake"
OMARCHY_UPDATE_LOGGED=1 run_with_lock_env "$ROOT/bin/omarchy-update" -y
[[ ! -f $test_home/.local/state/omarchy/indicators/stay-awake ]] || fail "update clears its Stay Awake state before restart handling"
mkdir -p "$test_home/.local/state/omarchy/indicators"
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"