Add cachy-auto-update: unattended background updates for CachyOS
A root systemd service applies pacman, AUR, Flatpak and AppImage updates on its own, gated on battery state, gaming activity and whether anybody else is using the package system. The CLI is deliberately two switches plus status. No user password is stored anywhere: pacman runs as root directly, and the AUR step - which makepkg forbids running as root - drops to a locked system account that sudoers permits to call pacman without a password.
This commit is contained in:
1 parent
327da1dae3
commit
ceb024d7be
28 files changed
+3856
-1
No files matched your search
@@ -0,0 +1,206 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Should we update right now?
|
||||
#
|
||||
# Every check here is allowed to say "not now" and nothing more. The timer
|
||||
# ticks hourly, so a deferral costs nothing: the next tick simply tries again.
|
||||
# That is also why none of these functions ever return a hard failure.
|
||||
|
||||
# Set by the checks below to a human-readable, already translated reason.
|
||||
CAU_SKIP_REASON=''
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Power
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# cau_on_ac
|
||||
# True when running on mains power. systemd-ac-power also returns success when
|
||||
# the machine has neither a battery nor an adapter, which is exactly right for
|
||||
# desktops - hand-rolled sysfs globbing gets that case wrong.
|
||||
cau_on_ac() {
|
||||
if cau_have systemd-ac-power; then
|
||||
systemd-ac-power > /dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
|
||||
local ps online
|
||||
for ps in /sys/class/power_supply/*; do
|
||||
[[ -r "$ps/type" ]] || continue
|
||||
[[ "$(< "$ps/type")" == Mains ]] || continue
|
||||
[[ -r "$ps/online" ]] || continue
|
||||
online="$(< "$ps/online")"
|
||||
[[ $online == 1 ]] && return 0
|
||||
done
|
||||
|
||||
# No mains device found at all: if there is no system battery either this
|
||||
# is a desktop, so assume mains rather than blocking updates forever.
|
||||
cau_battery_percent > /dev/null || return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
# cau_battery_percent
|
||||
# Average charge across the system batteries, or failure when the machine has
|
||||
# none. Peripheral batteries (mice, headsets) advertise type=Battery too and
|
||||
# are filtered out via the scope attribute; when scope is missing entirely -
|
||||
# as on many laptops - the device counts as a system battery.
|
||||
cau_battery_percent() {
|
||||
local ps sum=0 count=0 cap
|
||||
|
||||
for ps in /sys/class/power_supply/*; do
|
||||
[[ -r "$ps/type" ]] || continue
|
||||
[[ "$(< "$ps/type")" == Battery ]] || continue
|
||||
|
||||
if [[ -r "$ps/scope" ]]; then
|
||||
[[ "$(< "$ps/scope")" == System ]] || continue
|
||||
fi
|
||||
if [[ -r "$ps/present" ]]; then
|
||||
[[ "$(< "$ps/present")" == 1 ]] || continue
|
||||
fi
|
||||
[[ -r "$ps/capacity" ]] || continue
|
||||
|
||||
cap="$(< "$ps/capacity")"
|
||||
[[ $cap =~ ^[0-9]+$ ]] || continue
|
||||
sum=$(( sum + cap ))
|
||||
count=$(( count + 1 ))
|
||||
done
|
||||
|
||||
(( count > 0 )) || return 1
|
||||
printf '%s\n' "$(( sum / count ))"
|
||||
}
|
||||
|
||||
# cau_power_ok
|
||||
# Applies RequireAC and MinBatteryPercent.
|
||||
cau_power_ok() {
|
||||
local pct
|
||||
|
||||
if cau_on_ac; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ $CFG_REQUIRE_AC == yes ]]; then
|
||||
CAU_SKIP_REASON="$(cau_msg "running on battery")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
pct="$(cau_battery_percent)" || return 0 # no battery: nothing to gate on
|
||||
|
||||
if (( pct < CFG_MIN_BATTERY )); then
|
||||
CAU_SKIP_REASON="$(cau_msg "battery at %d%%, below the %d%% threshold" \
|
||||
"$pct" "$CFG_MIN_BATTERY")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Is the user in the middle of something?
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Process names that mean a game is actually running. Deliberately excludes
|
||||
# "steam" itself, which sits in the tray all day on a gaming machine.
|
||||
CAU_GAME_PROCESSES=(
|
||||
gamescope
|
||||
wine wine64 wineserver wine-preloader wine64-preloader
|
||||
umu-run proton
|
||||
reaper
|
||||
lutris lutris-wrapper heroic bottles
|
||||
retroarch dolphin-emu pcsx2-qt rpcs3 cemu ryujinx ppsspp citra-qt duckstation-qt
|
||||
)
|
||||
|
||||
# cau_process_running <name>
|
||||
# Exact-name match that ignores kernel threads. The exactness matters: a
|
||||
# substring match on "reaper" hits the kernel's own oom_reaper on every box.
|
||||
cau_process_running() {
|
||||
local name="$1" pid ppid
|
||||
|
||||
while read -r pid; do
|
||||
[[ $pid =~ ^[0-9]+$ ]] || continue
|
||||
(( pid == 2 )) && continue
|
||||
# /proc/<pid>/status rather than /stat: the comm field in /stat can
|
||||
# contain spaces and parentheses, which shifts every column after it.
|
||||
ppid="$(sed -n 's/^PPid:[[:space:]]*//p' "/proc/$pid/status" 2>/dev/null)" || continue
|
||||
[[ $ppid == 2 ]] && continue # kernel thread
|
||||
return 0
|
||||
done < <(pgrep -x -- "$name" 2>/dev/null)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# cau_gamemode_active
|
||||
# Asks GameMode how many clients it is tracking, per graphical session.
|
||||
# GameMode is optional and frequently absent; any failure means "unknown", not
|
||||
# "busy".
|
||||
cau_gamemode_active() {
|
||||
local user uid out count
|
||||
|
||||
cau_have busctl || return 1
|
||||
|
||||
while read -r user uid; do
|
||||
[[ -n $user ]] || continue
|
||||
# timeout runs inside the runuser call because it has to be a real
|
||||
# binary there - it cannot wrap a shell function from out here.
|
||||
out="$(cau_as_user "$user" "$uid" timeout 5 busctl --user --json=short \
|
||||
get-property com.feralinteractive.GameMode \
|
||||
/com/feralinteractive/GameMode \
|
||||
com.feralinteractive.GameMode ClientCount 2>/dev/null)" || continue
|
||||
count="$(sed -nE 's/.*"data"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/p' <<< "$out")"
|
||||
[[ -z $count ]] && count="$(awk '{print $NF}' <<< "$out")"
|
||||
[[ $count =~ ^[0-9]+$ ]] || continue
|
||||
(( count > 0 )) && return 0
|
||||
done < <(cau_active_session_users)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# cau_idle_inhibited
|
||||
# A blocking "idle" inhibitor is what fullscreen games and video players take.
|
||||
cau_idle_inhibited() {
|
||||
cau_have systemd-inhibit || return 1
|
||||
systemd-inhibit --list 2>/dev/null \
|
||||
| awk 'tolower($0) ~ /idle/ && $NF == "block" { found = 1 } END { exit !found }'
|
||||
}
|
||||
|
||||
# cau_busy
|
||||
# True when something is running that an update should not interrupt.
|
||||
cau_busy() {
|
||||
local proc
|
||||
|
||||
if cau_gamemode_active; then
|
||||
CAU_SKIP_REASON="$(cau_msg "a game is running (GameMode)")"
|
||||
return 0
|
||||
fi
|
||||
|
||||
for proc in "${CAU_GAME_PROCESSES[@]}"; do
|
||||
if cau_process_running "$proc"; then
|
||||
CAU_SKIP_REASON="$(cau_msg "a game is running (%s)" "$proc")"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
if cau_idle_inhibited; then
|
||||
CAU_SKIP_REASON="$(cau_msg "an application is blocking idle (fullscreen game or video)")"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scheduling
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# cau_update_due
|
||||
# The timer fires hourly; this decides whether enough time has passed since the
|
||||
# last *successful* run. Deferred runs therefore retry automatically without
|
||||
# any backoff bookkeeping of their own.
|
||||
cau_update_due() {
|
||||
local last now
|
||||
|
||||
last="$(cau_state_read last_success 0)"
|
||||
[[ $last =~ ^[0-9]+$ ]] || last=0
|
||||
(( last == 0 )) && return 0
|
||||
|
||||
now="$(date +%s)"
|
||||
(( now - last >= CFG_INTERVAL_SECONDS ))
|
||||
}
|
||||
Reference in new issue
Block a user