Files
cachy-auto-update/src/cachy-auto-update
T
Felitendo 459cbbf8e7 Fix locale handling, menu re-entry and empty-checkupdates logging
- The runner now forces LC_ALL=C itself instead of relying on the unit's
  Environment=. pacman failures are classified by matching its output, so a
  run started by hand on a German system was misclassifying every failure.
- 'Update now' in the menu exec'd the runner, which terminated the menu.
- Log a distinct message when checkupdates is unavailable, instead of
  claiming zero pending packages before a full upgrade.
2026-08-08 02:35:34 +02:00

263 lines
7.1 KiB
Bash

#!/usr/bin/env bash
#
# cachy-auto-update - unattended updates for CachyOS
#
# The command line front end. Everything it does is either flipping a switch in
# /etc/cachy-auto-update/cachy-auto-update.conf, driving the systemd timer, or
# handing off to the runner in @LIBEXECDIR@.
#
# Copyright (C) 2026 Felitendo
# SPDX-License-Identifier: GPL-3.0-or-later
set -uo pipefail
CAU_LIBDIR="${CAU_LIBDIR:-@LIBDIR@}"
CAU_LIBEXECDIR="${CAU_LIBEXECDIR:-@LIBEXECDIR@}"
for _mod in common config users conditions locks notify menu; do
# shellcheck source=/dev/null
if ! source "$CAU_LIBDIR/$_mod.sh"; then
printf 'cachy-auto-update: cannot load %s/%s.sh\n' "$CAU_LIBDIR" "$_mod" >&2
exit 14
fi
done
unset _mod
CAU_SELF="$(readlink -f "${BASH_SOURCE[0]}")"
CAU_ARGV=("$@")
# ---------------------------------------------------------------------------
# Privilege
# ---------------------------------------------------------------------------
# Re-executes itself with elevation when a human is at the terminal, so that
# typing `cachy-auto-update` just works instead of printing a lecture.
cau_need_root() {
cau_is_root && return 0
local candidate elevate=''
if [[ -t 0 && -t 1 ]]; then
for candidate in sudo run0 doas; do
if cau_have "$candidate"; then elevate="$candidate"; break; fi
done
fi
if [[ -n $elevate ]]; then
exec "$elevate" "$CAU_SELF" "${CAU_ARGV[@]}"
fi
cau_bad "$(cau_msg "This command has to run as root.")"
exit 2
}
# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------
cau_do_enable() {
cau_need_root
# A broken sudoers drop-in would take down sudo for the whole machine, so
# it is verified before anything is switched on.
if [[ -f /etc/sudoers.d/cachy-auto-update ]]; then
if cau_have visudo && ! visudo -cf /etc/sudoers.d/cachy-auto-update > /dev/null 2>&1; then
cau_bad "$(cau_msg "/etc/sudoers.d/cachy-auto-update is invalid; refusing to enable.")"
return 1
fi
fi
cau_config_set Enabled yes || { cau_bad "$(cau_msg "Could not write the configuration file.")"; return 1; }
if cau_have systemctl; then
systemctl enable --now cachy-auto-update.timer > /dev/null 2>&1 \
|| cau_bad "$(cau_msg "Could not enable the systemd timer.")"
fi
cau_ok "$(cau_msg "Automatic updates are on.")"
cau_config_load
if [[ $CFG_AUR == yes ]] && ! cau_aur_detect_quiet; then
cau_note "$(cau_msg "No AUR helper found - install paru or yay for AUR updates.")"
fi
if [[ $CFG_AUR == yes ]] && ! cau_have makepkg; then
cau_note "$(cau_msg "base-devel is missing - AUR packages cannot be built without it.")"
fi
cau_offer_disable_cachy_update
}
# Whether an AUR helper exists at all, without the logging cau_aur_detect does.
cau_aur_detect_quiet() {
local c
if [[ -n ${CFG_AUR_HELPER:-} && $CFG_AUR_HELPER != auto ]]; then
cau_have "$CFG_AUR_HELPER"
return $?
fi
for c in paru yay pikaur; do cau_have "$c" && return 0; done
return 1
}
# cachy-update ships an enabled user timer that only ever says "N updates
# available". Once updates apply themselves that notification is pure noise,
# so offer to silence it - but only ask, never decide.
cau_offer_disable_cachy_update() {
local user uid answer found=0
[[ -t 0 && -t 1 ]] || return 0
cau_have systemctl || return 0
while read -r user uid; do
[[ -n $user ]] || continue
if cau_as_user "$user" "$uid" systemctl --user is-enabled --quiet arch-update.timer 2>/dev/null; then
found=1
break
fi
done < <(cau_active_session_users)
(( found )) || return 0
printf '\n %s\n ' \
"$(cau_msg "cachy-update also notifies about available updates. Turn its notifications off? [y/N]")"
read -r answer || return 0
[[ ${answer,,} == y ]] || return 0
while read -r user uid; do
[[ -n $user ]] || continue
cau_as_user "$user" "$uid" systemctl --user disable --now arch-update.timer > /dev/null 2>&1 || true
done < <(cau_active_session_users)
cau_ok "$(cau_msg "cachy-update's update check has been disabled.")"
}
cau_do_disable() {
cau_need_root
cau_config_set Enabled no || { cau_bad "$(cau_msg "Could not write the configuration file.")"; return 1; }
if cau_have systemctl; then
systemctl disable --now cachy-auto-update.timer > /dev/null 2>&1 || true
fi
cau_ok "$(cau_msg "Automatic updates are off.")"
}
cau_do_notifications() {
cau_need_root
case "${1:-}" in
on|yes|true|1)
cau_config_set Notifications yes || return 1
cau_ok "$(cau_msg "Notifications are on.")"
;;
off|no|false|0)
cau_config_set Notifications no || return 1
cau_ok "$(cau_msg "Notifications are off.")"
;;
*)
cau_bad "$(cau_msg "Usage: cachy-auto-update notifications on|off")"
return 1
;;
esac
}
cau_do_status() {
cau_config_load
cau_head " $CAU_PRETTY"
cau_ui_status
cau_ui_status_conditions
printf '\n'
}
cau_do_run() {
cau_need_root
# Not exec'd: this is also called from the menu, which has to survive the
# run and redraw afterwards.
"$CAU_LIBEXECDIR/cachy-auto-update-run" "$@"
}
cau_do_log() {
local file="$CAU_RUNLOG" lines=200 follow=0
while (( $# )); do
case "$1" in
-n) lines="${2:-200}"; shift 2 ;;
-n*) lines="${1#-n}"; shift ;;
-f|--follow) follow=1; shift ;;
-a|--all) file="$CAU_LOGFILE"; shift ;;
*) shift ;;
esac
done
if [[ ! -r $file ]]; then
cau_note "$(cau_msg "No log yet.")"
return 0
fi
if (( follow )); then
tail -n "$lines" -f "$file"
else
tail -n "$lines" "$file"
fi
}
cau_do_help() {
cat <<- EOF
$CAU_PRETTY $CAU_VERSION
$(cau_msg "Usage: cachy-auto-update [command]")
$(cau_msg "Commands:")
enable $(cau_msg "Turn automatic updates on")
disable $(cau_msg "Turn automatic updates off")
notifications on|off $(cau_msg "Turn desktop notifications on or off")
status $(cau_msg "Show the current state and conditions")
run [--force] [--dry-run] $(cau_msg "Run an update now")
log [-n N] [-f] [-a] $(cau_msg "Show the log of the last run")
-h, --help $(cau_msg "Show this help")
-V, --version $(cau_msg "Show the version")
$(cau_msg "Without a command an interactive menu is shown.")
EOF
}
# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------
main() {
local cmd="${1:-}"
[[ $# -gt 0 ]] && shift
case "$cmd" in
enable) cau_do_enable "$@" ;;
disable) cau_do_disable "$@" ;;
notifications) cau_do_notifications "$@" ;;
status) cau_do_status "$@" ;;
run) cau_do_run "$@" ;;
log) cau_do_log "$@" ;;
# Invoked unprivileged from the XDG autostart entry to replay
# notifications that were produced while nobody was logged in.
--deliver-notifications)
cau_config_load
cau_notify_deliver_queue
;;
-h|--help|help) cau_do_help ;;
-V|--version) printf '%s %s\n' "$CAU_NAME" "$CAU_VERSION" ;;
'')
cau_need_root
cau_ui_menu
;;
*)
cau_bad "$(cau_msg "Unknown command: %s" "$cmd")"
printf '\n'
cau_do_help
exit 1
;;
esac
}
main "$@"