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>
215 lines
7.9 KiB
Bash
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"
|