207 lines
6.2 KiB
Bash
207 lines
6.2 KiB
Bash
# 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 ))
|
|
}
|