Files
cachy-auto-update/src/cachy-auto-update-run
T
Felitendo 243115ea19 Show what a run is doing, and record when one is cut short
A run that held back a blocker then upgraded 214 packages printed one line and
then nothing for nearly five minutes while pacman downloaded and installed. It
looked hung, so it got killed - during pacman's uninterruptible commit phase,
which meant the packages landed but our bookkeeping never did. The menu then
kept showing a failure from a previous run on a fully up-to-date machine.

pacman, the AUR helper and flatpak now stream their output when a person is
watching, and the progress bar is left enabled for that case. Timer runs are
unchanged: quiet, --noprogressbar, everything captured in the log.

Interactivity is decided once at startup rather than tested at the point of
use. cau_pacman_flags runs inside a process substitution, so its stdout is
always a pipe and a -t 1 check there would have silently always been false.

INT/TERM/HUP now record last_result=interrupted, so a run that is stopped says
so instead of leaving the previous verdict standing.
2026-08-08 15:24:42 +02:00

253 lines
7.6 KiB
Bash

#!/usr/bin/env bash
#
# cachy-auto-update-run - one unattended update pass
#
# Started by cachy-auto-update.service as root. The timer fires hourly and this
# decides whether anything should happen; that is what makes deferrals free.
# Refusing to run is the normal, expected outcome most of the time, so it is
# never treated as an error.
#
# Copyright (C) 2026 Felitendo
# SPDX-License-Identifier: GPL-3.0-or-later
set -uo pipefail
CAU_LIBDIR="${CAU_LIBDIR:-@LIBDIR@}"
for _mod in common config users conditions locks notify \
pkg_pacman pkg_aur pkg_flatpak pkg_appimage; do
# shellcheck source=/dev/null
if ! source "$CAU_LIBDIR/$_mod.sh"; then
printf 'cachy-auto-update-run: cannot load %s/%s.sh\n' "$CAU_LIBDIR" "$_mod" >&2
exit 14
fi
done
unset _mod
FORCE=0
DRY_RUN=0
for arg in "$@"; do
case "$arg" in
--force) FORCE=1 ;;
--dry-run) DRY_RUN=1 ;;
--debug) CAU_DEBUG=1 ;;
esac
done
if [[ $EUID -ne 0 ]]; then
printf 'cachy-auto-update-run: must run as root\n' >&2
exit 2
fi
# Force a neutral locale here rather than relying on the unit's Environment=,
# so a run started by hand behaves exactly like one started by the timer.
# pacman failures are classified by matching its output, and on a German system
# that output is German. Text aimed at a person does not come through here - a
# notification is rendered in the recipient's own locale by cau_msg_in.
export LC_ALL=C LANGUAGE=
# Re-exec under an inhibitor so a suspend or shutdown cannot land in the middle
# of a pacman transaction and leave the database half-written. Done before
# anything else so the whole pass is covered, including the lock.
if [[ -z ${CAU_INHIBITED:-} ]] && command -v systemd-inhibit > /dev/null 2>&1; then
export CAU_INHIBITED=1
exec systemd-inhibit \
--what=sleep:shutdown \
--mode=block \
--who="CachyOS Auto-Update" \
--why="Applying system updates" \
-- "$0" "$@"
fi
cau_config_load
cau_log_open
# Record an interruption rather than leaving the previous run's verdict behind.
# Without this, killing an interactive run leaves last_result at whatever it was
# before - so the menu can keep reporting a problem from hours ago while the
# machine is in fact fully up to date, which is worse than saying nothing.
# pacman makes the commit phase itself uninterruptible, so the packages either
# all landed or none did; only our own bookkeeping is at risk here.
_cau_interrupted() {
cau_error "Update run interrupted ($1)"
cau_state_write last_result interrupted
exit 130
}
trap '_cau_interrupted SIGINT' INT
trap '_cau_interrupted SIGTERM' TERM
trap '_cau_interrupted SIGHUP' HUP
# ---------------------------------------------------------------------------
# Should this run happen at all?
# ---------------------------------------------------------------------------
defer() {
cau_info "Deferred: $1"
exit 0
}
if ! cau_acquire_lock; then
cau_debug "Another run is already in progress"
exit 0
fi
if (( ! FORCE )); then
if [[ $CFG_ENABLED != yes ]]; then
cau_debug "Automatic updates are disabled"
exit 0
fi
if ! cau_update_due; then
cau_debug "Not due yet (interval $CFG_INTERVAL)"
exit 0
fi
fi
cau_state_write last_run "$(date +%s)"
if (( ! FORCE )); then
CAU_SKIP_REASON=''
cau_power_ok || defer "$CAU_SKIP_REASON"
if [[ $CFG_SKIP_GAMING == yes ]]; then
CAU_SKIP_REASON=''
if cau_busy; then
defer "$CAU_SKIP_REASON"
fi
fi
fi
# This one is checked even with --force: proceeding anyway would just hand the
# user a lock error instead of doing anything useful.
CAU_SKIP_REASON=''
if cau_package_manager_busy; then
if cau_track_stale_lock; then
cau_warn "pacman's database lock appears to be stale"
cau_notify normal \
"Package database locked" \
"pacman's lock file looks left over from an interrupted update. Updates are paused until it is cleared."
fi
defer "$CAU_SKIP_REASON"
fi
cau_state_clear stale_lock_count
# ---------------------------------------------------------------------------
# Dry run
# ---------------------------------------------------------------------------
if (( DRY_RUN )); then
printf '%s\n' "Would update on this machine:"
if cau_pacman_pending; then
printf ' repository packages: %s\n' "$(grep -c . <<< "$CAU_PACMAN_PENDING")"
sed 's/^/ /' <<< "$CAU_PACMAN_PENDING"
else
printf ' repository packages: 0\n'
fi
if [[ $CFG_AUR == yes ]] && cau_aur_ready; then
printf ' AUR packages: %s (%s)\n' "$(cau_aur_pending)" "$CAU_AUR_HELPER"
else
printf ' AUR packages: skipped\n'
fi
if [[ $CFG_FLATPAK == yes ]] && cau_have flatpak; then
printf ' system Flatpaks: %s\n' "$(cau_flatpak_pending_system)"
else
printf ' system Flatpaks: skipped\n'
fi
printf ' AppImages: %s\n' \
"$([[ $CFG_APPIMAGE == yes ]] && printf 'checked via Gear Lever' || printf 'skipped')"
exit 0
fi
# ---------------------------------------------------------------------------
# Do the work
# ---------------------------------------------------------------------------
cau_info "Starting update run"
failed=0
if ! cau_pacman_update; then
failed=1
fi
if [[ $CFG_AUR == yes ]]; then
cau_aur_update || failed=1
fi
if [[ $CFG_FLATPAK == yes ]]; then
cau_flatpak_update || failed=1
fi
if [[ $CFG_APPIMAGE == yes ]]; then
cau_appimage_update || true # best effort; never marks the run as failed
fi
cau_pacman_cleanup
pacnew="$(cau_pacman_pacnew_count)"
if [[ $pacnew =~ ^[0-9]+$ ]] && (( pacnew > 0 )); then
cau_info "$pacnew .pacnew file(s) present; left untouched"
fi
# ---------------------------------------------------------------------------
# Record and report
# ---------------------------------------------------------------------------
total=$(( CAU_PACMAN_COUNT + CAU_AUR_COUNT + CAU_FLATPAK_COUNT + CAU_APPIMAGE_COUNT ))
cau_state_write last_counts \
"$CAU_PACMAN_COUNT $CAU_AUR_COUNT $CAU_FLATPAK_COUNT $CAU_APPIMAGE_COUNT"
if (( failed )); then
cau_state_write last_result failed
cau_error "Update run finished with errors"
[[ $CFG_NOTIFY_ERROR == yes ]] && cau_notify critical \
"Update failed" \
"Something went wrong while updating. Run 'cachy-auto-update log' for details."
else
cau_state_write last_result ok
cau_state_write last_success "$(date +%s)"
cau_info "Update run finished successfully ($total item(s) updated)"
if (( total > 0 )) && [[ $CFG_NOTIFY_SUCCESS == yes ]]; then
cau_notify low "System updated" "%d updates were installed." "$total"
fi
fi
# Packages skipped so the rest of the upgrade could proceed. The notification
# only fires when the set changes: a blocker waiting on an upstream fix would
# otherwise produce the same message every single day.
prev_held="$(cau_state_read held_back '')"
if [[ -n $CAU_PACMAN_HELD ]]; then
cau_state_write held_back "$CAU_PACMAN_HELD"
cau_warn "Held back: $CAU_PACMAN_HELD"
if [[ $CAU_PACMAN_HELD != "$prev_held" && $CFG_NOTIFY_ERROR == yes ]]; then
cau_notify normal "Some packages were held back" \
"%s could not be updated and was skipped. Everything else is up to date." \
"$CAU_PACMAN_HELD"
fi
else
cau_state_clear held_back
fi
if cau_pacman_reboot_needed; then
cau_state_write reboot_needed 1
cau_info "A kernel update needs a restart"
[[ $CFG_NOTIFY_REBOOT == yes ]] && cau_notify normal \
"Restart recommended" \
"A new kernel was installed. Please restart when it suits you."
else
cau_state_write reboot_needed 0
fi
# Keep the state readable for the unprivileged `status` and `log` commands.
# Only the plain files: the build account's home lives in here too and must
# keep its own ownership and mode.
chmod 0755 "$CAU_STATEDIR" 2>/dev/null || true
find "$CAU_STATEDIR" -maxdepth 1 -type f -exec chmod 0644 {} + 2>/dev/null || true
exit $(( failed ? 1 : 0 ))