Files
cachy-auto-update/src/lib/locks.sh
T

150 lines
4.7 KiB
Bash

# shellcheck shell=bash
#
# Staying out of the way of a human using pacman/yay/paru.
#
# The contract this file implements: the machine's owner may run any package
# manager at any time and must never see a lock error caused by us. We can only
# guarantee that in one direction: by never *starting* while somebody else is
# mid-transaction. So the checks here run before anything is touched, and a
# refusal simply defers the run to the next hourly tick.
# Package managers that take /var/lib/pacman/db.lck. checkupdates is absent on
# purpose: it works against a private temporary database and never touches the
# real lock.
CAU_PACKAGE_MANAGERS=(
pacman pacman-static
yay paru pikaur aurman trizen
pamac pamac-daemon pamac-manager pamac-tray
octopi octopi-helper
makepkg
)
# cau_acquire_lock
# Guards against two runs overlapping (a slow run still going when the next
# tick fires). Non-blocking: if another run holds it, this one is pointless.
cau_acquire_lock() {
mkdir -p "$CAU_RUNDIR" 2>/dev/null || return 1
exec {CAU_LOCK_FD}> "$CAU_LOCKFILE" || return 1
flock -n "$CAU_LOCK_FD" || return 1
return 0
}
# cau_pacman_lock_holder
# Prints the pids currently holding pacman's database lock, if any.
cau_pacman_lock_holder() {
[[ -e $CAU_PACMAN_LOCK ]] || return 1
cau_have fuser || return 0 # cannot tell; assume it is held
fuser "$CAU_PACMAN_LOCK" 2>/dev/null | tr -s ' ' '\n' | grep -E '^[0-9]+$' || return 0
}
# cau_package_manager_busy
# True when somebody else is using the package system right now. Sets
# CAU_SKIP_REASON.
cau_package_manager_busy() {
local pm
if [[ -e $CAU_PACMAN_LOCK ]]; then
CAU_SKIP_REASON="$(cau_msg "pacman's database is locked by another process")"
return 0
fi
for pm in "${CAU_PACKAGE_MANAGERS[@]}"; do
if cau_process_running "$pm"; then
CAU_SKIP_REASON="$(cau_msg "%s is currently running" "$pm")"
return 0
fi
done
return 1
}
# cau_pacman_lock_is_stale
# True only when the lock provably cannot belong to anything alive.
#
# The rigorous test is the boot time: no process that existed before the
# current boot can still be running, so a db.lck older than boot is abandoned
# by definition. That is exactly what a power cut during an update leaves
# behind. A lock that is merely unheld *within* this boot is not provable in
# the same way, so it is only reported (see cau_track_stale_lock) and never
# removed; guessing wrong there would corrupt a live transaction.
#
# The fuser check is kept as a second condition purely to survive a backwards
# clock jump making a live lock look pre-boot.
cau_pacman_lock_is_stale() {
local boot lock
[[ -e $CAU_PACMAN_LOCK ]] || return 1
boot="$(awk '/^btime /{print $2}' /proc/stat 2>/dev/null)"
[[ $boot =~ ^[0-9]+$ ]] || return 1
lock="$(stat -c %Y "$CAU_PACMAN_LOCK" 2>/dev/null)" || return 1
[[ $lock =~ ^[0-9]+$ ]] || return 1
(( lock < boot )) || return 1
[[ -z "$(cau_pacman_lock_holder)" ]]
}
# cau_recover_stale_lock
# Clears a provably abandoned lock so an interrupted update can be finished on
# the next run. Without this, one power cut during an update stops every future
# update permanently and silently. That is the worst possible outcome for a
# machine nobody is watching.
cau_recover_stale_lock() {
cau_pacman_lock_is_stale || return 1
cau_warn "Found a pacman lock older than this boot. An update was cut short"
rm -f "$CAU_PACMAN_LOCK" 2>/dev/null || {
cau_error "Could not remove the stale pacman lock"
return 1
}
cau_state_clear stale_lock_count
cau_info "Stale lock removed; the interrupted update will be finished now"
return 0
}
# cau_track_stale_lock
# A db.lck with no process behind it but created during this boot: a crashed
# pacman rather than a power cut. Not provable, so it is counted, and after
# enough consecutive sightings the user is told to clean it up.
CAU_STALE_LOCK_RUNS=3
cau_track_stale_lock() {
local count
if [[ ! -e $CAU_PACMAN_LOCK ]]; then
cau_state_clear stale_lock_count
return 1
fi
if [[ -n "$(cau_pacman_lock_holder)" ]]; then
# genuinely held; not stale
cau_state_clear stale_lock_count
return 1
fi
count="$(cau_state_read stale_lock_count 0)"
[[ $count =~ ^[0-9]+$ ]] || count=0
count=$(( count + 1 ))
cau_state_write stale_lock_count "$count"
(( count >= CAU_STALE_LOCK_RUNS ))
}
# cau_inhibit_prefix
# Builds the command prefix that keeps logind from suspending or shutting the
# machine down mid-transaction. Falls back to running unprotected rather than
# refusing to update at all.
cau_inhibit_prefix() {
if cau_have systemd-inhibit; then
printf '%s\n' systemd-inhibit
printf '%s\n' --what=sleep:shutdown
printf '%s\n' --mode=block
printf '%s\n' "--who=$CAU_PRETTY"
printf '%s\n' "--why=$(cau_msg_in C "applying system updates")"
printf '%s\n' --
fi
}