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.
253 lines
7.6 KiB
Bash
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 ))
|