294 lines
10 KiB
Bash
294 lines
10 KiB
Bash
# shellcheck shell=bash
|
|
#
|
|
# Paths, logging, translations and small shared helpers.
|
|
#
|
|
# Sourced by both the CLI and the update runner, so it must not assume it is
|
|
# running as root and must not produce any output on its own.
|
|
|
|
CAU_VERSION="@VERSION@"
|
|
CAU_NAME="cachy-auto-update"
|
|
CAU_PRETTY="CachyOS Auto-Update"
|
|
|
|
# Directories. All of them are overridable so the tree can be exercised
|
|
# straight from a git checkout without installing anything.
|
|
CAU_LIBDIR="${CAU_LIBDIR:-@LIBDIR@}"
|
|
CAU_LIBEXECDIR="${CAU_LIBEXECDIR:-@LIBEXECDIR@}"
|
|
CAU_LOCALEDIR="${CAU_LOCALEDIR:-@LOCALEDIR@}"
|
|
CAU_CONFDIR="${CAU_CONFDIR:-/etc/cachy-auto-update}"
|
|
CAU_CONFIG="${CAU_CONFIG:-${CAU_CONFDIR}/cachy-auto-update.conf}"
|
|
CAU_STATEDIR="${CAU_STATEDIR:-/var/lib/cachy-auto-update}"
|
|
CAU_CACHEDIR="${CAU_CACHEDIR:-/var/cache/cachy-auto-update}"
|
|
CAU_LOGDIR="${CAU_LOGDIR:-/var/log/cachy-auto-update}"
|
|
CAU_RUNDIR="${CAU_RUNDIR:-/run/cachy-auto-update}"
|
|
|
|
CAU_LOGFILE="${CAU_LOGDIR}/cachy-auto-update.log"
|
|
CAU_RUNLOG="${CAU_LOGDIR}/last-run.log"
|
|
CAU_LOCKFILE="${CAU_RUNDIR}/run.lock"
|
|
CAU_NOTIFY_QUEUE="${CAU_STATEDIR}/notify-queue"
|
|
|
|
# The locked system account that builds and installs AUR packages. Created by
|
|
# sysusers.d; its home lives under the state directory.
|
|
CAU_BUILD_USER="cachy-auto-update"
|
|
CAU_BUILD_HOME="${CAU_STATEDIR}/builder"
|
|
|
|
# Pacman's own database lock. Presence means somebody is mid-transaction.
|
|
CAU_PACMAN_LOCK="/var/lib/pacman/db.lck"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Translations
|
|
# ---------------------------------------------------------------------------
|
|
# Messages are authored in English and translated through gettext. The runner
|
|
# is started with LC_ALL=C so that pacman/upower output stays parseable, which
|
|
# means every user-facing string has to opt back into a real locale explicitly
|
|
# via cau_msg / cau_msg_in.
|
|
|
|
export TEXTDOMAIN="cachy-auto-update"
|
|
export TEXTDOMAINDIR="${CAU_LOCALEDIR}"
|
|
|
|
# The locale user-facing text should be rendered in.
|
|
#
|
|
# Standard POSIX precedence, deliberately: LC_ALL wins outright, and LC_ALL=C
|
|
# really does mean English. The service sets LC_ALL=C so that pacman and upower
|
|
# stay parseable, which makes the log English. That is correct, since the log
|
|
# is a technical artefact. Anything aimed at a person (a desktop notification) does
|
|
# not go through here at all; it names the recipient's own locale explicitly
|
|
# via cau_msg_in and cau_user_locale.
|
|
cau_ui_locale() {
|
|
local l="${CAU_UI_LOCALE:-}"
|
|
|
|
if [[ -z $l ]]; then
|
|
l="${LC_ALL:-}"
|
|
[[ -z $l ]] && l="${LC_MESSAGES:-}"
|
|
[[ -z $l ]] && l="${LANG:-}"
|
|
fi
|
|
|
|
if [[ -z $l && -r /etc/locale.conf ]]; then
|
|
l="$(sed -n 's/^LANG=//p' /etc/locale.conf | tr -d '"' | head -n1)"
|
|
fi
|
|
|
|
printf '%s\n' "${l:-C}"
|
|
}
|
|
|
|
# cau_msg <msgid> [printf args...]
|
|
# Translate a string into the UI locale and printf it (no trailing newline).
|
|
cau_msg() {
|
|
cau_msg_in "$(cau_ui_locale)" "$@"
|
|
}
|
|
|
|
# cau_msg_into <locale> <msgid>
|
|
# Plain lookup with the result in CAU_MSG_RESULT and no printf formatting.
|
|
# For callers that redraw many labels per keypress, where wrapping cau_msg in a
|
|
# command substitution would cost a fork per label.
|
|
CAU_MSG_RESULT=''
|
|
|
|
cau_msg_into() {
|
|
local locale="$1" msgid="$2" cachekey
|
|
cachekey="${locale}"$'\x1f'"${msgid}"
|
|
|
|
if [[ -n ${CAU_MSG_CACHE[$cachekey]+set} ]]; then
|
|
CAU_MSG_RESULT="${CAU_MSG_CACHE[$cachekey]}"
|
|
return 0
|
|
fi
|
|
|
|
CAU_MSG_RESULT="$(LC_ALL="$locale" LANGUAGE="${locale%%.*}" gettext -- "$msgid" 2>/dev/null)"
|
|
[[ -n $CAU_MSG_RESULT ]] || CAU_MSG_RESULT="$msgid"
|
|
CAU_MSG_CACHE[$cachekey]="$CAU_MSG_RESULT"
|
|
return 0
|
|
}
|
|
|
|
# Translations are memoized. Every gettext lookup is a fork, and the settings
|
|
# screen redraws forty-odd labels per keypress; without this the redraw takes
|
|
# long enough that a keystroke arriving during it is lost when the terminal
|
|
# switches back to single-character mode.
|
|
declare -A CAU_MSG_CACHE=()
|
|
|
|
# cau_msg_in <locale> <msgid> [printf args...]
|
|
cau_msg_in() {
|
|
local locale="$1" msgid="$2" translated cachekey
|
|
shift 2
|
|
|
|
cachekey="${locale}"$'\x1f'"${msgid}"
|
|
if [[ -n ${CAU_MSG_CACHE[$cachekey]+set} ]]; then
|
|
translated="${CAU_MSG_CACHE[$cachekey]}"
|
|
else
|
|
translated="$(LC_ALL="$locale" LANGUAGE="${locale%%.*}" gettext -- "$msgid" 2>/dev/null)"
|
|
[[ -n $translated ]] || translated="$msgid"
|
|
CAU_MSG_CACHE[$cachekey]="$translated"
|
|
fi
|
|
|
|
# With no arguments the message is plain text, not a format string. Feeding
|
|
# it to printf anyway turns any literal percent sign in it (like
|
|
# "Battery (%)" or "100 % done") into an invalid conversion, and that is a trap every
|
|
# translator would eventually walk into.
|
|
if (( $# == 0 )); then
|
|
printf '%s' "$translated"
|
|
return
|
|
fi
|
|
|
|
# shellcheck disable=SC2059 # the format string is the translated message
|
|
printf -- "$translated" "$@"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Output and logging
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Is a person watching? Decided once, here, while stdout is still whatever the
|
|
# process was started with. Testing `-t 1` at the point of use is unreliable:
|
|
# any function called through $(...) or <(...) sees a pipe on stdout and would
|
|
# conclude nobody is there.
|
|
CAU_INTERACTIVE=''
|
|
[[ -t 1 ]] && CAU_INTERACTIVE=1
|
|
|
|
if [[ -n $CAU_INTERACTIVE && -z ${NO_COLOR:-} ]]; then
|
|
CAU_C_RESET=$'\033[0m'
|
|
CAU_C_BOLD=$'\033[1m'
|
|
CAU_C_DIM=$'\033[2m'
|
|
CAU_C_BLUE=$'\033[38;2;23;147;209m'
|
|
CAU_C_GREEN=$'\033[32m'
|
|
CAU_C_YELLOW=$'\033[33m'
|
|
CAU_C_RED=$'\033[31m'
|
|
else
|
|
CAU_C_RESET='' CAU_C_BOLD='' CAU_C_DIM='' CAU_C_BLUE=''
|
|
CAU_C_GREEN='' CAU_C_YELLOW='' CAU_C_RED=''
|
|
fi
|
|
|
|
# Appends to the persistent log when writable (i.e. when running as root) and
|
|
# always echoes to stderr so journald picks it up for the service.
|
|
cau_log() {
|
|
local level="$1"
|
|
shift
|
|
local line
|
|
line="$(date '+%Y-%m-%d %H:%M:%S') [$level] $*"
|
|
|
|
printf '%s\n' "$line" >&2
|
|
if [[ -n ${CAU_LOG_OPEN:-} ]]; then
|
|
printf '%s\n' "$line" >> "$CAU_LOGFILE" 2>/dev/null || true
|
|
printf '%s\n' "$line" >> "$CAU_RUNLOG" 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
cau_info() { cau_log INFO "$@"; }
|
|
cau_warn() { cau_log WARN "$@"; }
|
|
cau_error() { cau_log ERROR "$@"; }
|
|
cau_debug() { [[ -n ${CAU_DEBUG:-} ]] && cau_log DEBUG "$@"; return 0; }
|
|
|
|
# Opens the log files for this run. Only meaningful as root.
|
|
cau_log_open() {
|
|
mkdir -p "$CAU_LOGDIR" 2>/dev/null || return 0
|
|
chmod 0750 "$CAU_LOGDIR" 2>/dev/null || true
|
|
: > "$CAU_RUNLOG" 2>/dev/null || return 0
|
|
CAU_LOG_OPEN=1
|
|
}
|
|
|
|
# Runs a command, capturing its combined output in the run log. Returns the
|
|
# command's exit status.
|
|
#
|
|
# When a person is watching (`cachy-auto-update run` from a terminal), the
|
|
# output is shown as well. Building an AUR package or pulling a few hundred
|
|
# megabytes of Flatpak can take minutes, and silence for that long is
|
|
# indistinguishable from a hang.
|
|
cau_run_logged() {
|
|
if [[ -n ${CAU_LOG_OPEN:-} ]]; then
|
|
if [[ -n $CAU_INTERACTIVE ]]; then
|
|
"$@" 2>&1 | tee -a "$CAU_RUNLOG"
|
|
return "${PIPESTATUS[0]}"
|
|
fi
|
|
"$@" >> "$CAU_RUNLOG" 2>&1
|
|
else
|
|
"$@" >&2
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Terminal helpers for the CLI
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Set by cau_bad and cau_note. The menu redraws immediately after an action,
|
|
# which would wipe the screen; this marks that something was printed that the
|
|
# user still has to read, so only those cases wait for a keypress. A plain
|
|
# success needs no acknowledgement, because the status block at the top of the
|
|
# menu already shows the new state.
|
|
CAU_UI_NEEDS_ACK=''
|
|
|
|
cau_say() { printf '%s\n' "$*"; }
|
|
cau_head() { printf '\n%s%s%s\n\n' "$CAU_C_BOLD$CAU_C_BLUE" "$*" "$CAU_C_RESET"; }
|
|
cau_ok() { printf '%s✔%s %s\n' "$CAU_C_GREEN" "$CAU_C_RESET" "$*"; }
|
|
cau_bad() { CAU_UI_NEEDS_ACK=1; printf '%s✘%s %s\n' "$CAU_C_RED" "$CAU_C_RESET" "$*" >&2; }
|
|
cau_note() { CAU_UI_NEEDS_ACK=1; printf '%s•%s %s\n' "$CAU_C_DIM" "$CAU_C_RESET" "$*"; }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# State files
|
|
# ---------------------------------------------------------------------------
|
|
# Every piece of state is one small file under CAU_STATEDIR. That keeps the
|
|
# CLI, the runner and the login-time notification delivery from needing any
|
|
# shared parsing code.
|
|
|
|
cau_state_read() {
|
|
local key="$1" default="${2:-}"
|
|
if [[ -r "$CAU_STATEDIR/$key" ]]; then
|
|
cat "$CAU_STATEDIR/$key"
|
|
else
|
|
printf '%s\n' "$default"
|
|
fi
|
|
}
|
|
|
|
cau_state_write() {
|
|
local key="$1"
|
|
shift
|
|
mkdir -p "$CAU_STATEDIR" 2>/dev/null || return 1
|
|
printf '%s\n' "$*" > "$CAU_STATEDIR/$key"
|
|
}
|
|
|
|
cau_state_clear() {
|
|
rm -f "$CAU_STATEDIR/$1" 2>/dev/null || true
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Misc
|
|
# ---------------------------------------------------------------------------
|
|
|
|
cau_have() { command -v "$1" > /dev/null 2>&1; }
|
|
|
|
cau_is_root() { [[ $EUID -eq 0 ]]; }
|
|
|
|
# Turns a duration such as "1d", "6h", "30m" or a bare number of seconds into
|
|
# seconds. Returns the fallback for anything unparseable.
|
|
cau_duration_to_seconds() {
|
|
local v="$1" fallback="${2:-86400}" num unit
|
|
|
|
[[ $v =~ ^([0-9]+)([smhdw]?)$ ]] || { printf '%s\n' "$fallback"; return; }
|
|
num="${BASH_REMATCH[1]}"
|
|
unit="${BASH_REMATCH[2]}"
|
|
|
|
case "$unit" in
|
|
s|'') printf '%s\n' "$num" ;;
|
|
m) printf '%s\n' "$(( num * 60 ))" ;;
|
|
h) printf '%s\n' "$(( num * 3600 ))" ;;
|
|
d) printf '%s\n' "$(( num * 86400 ))" ;;
|
|
w) printf '%s\n' "$(( num * 604800 ))" ;;
|
|
esac
|
|
}
|
|
|
|
# Human-readable "x minutes ago" for a unix timestamp. Empty input yields the
|
|
# translated "never".
|
|
cau_time_ago() {
|
|
local ts="$1" now delta
|
|
|
|
[[ $ts =~ ^[0-9]+$ ]] || { cau_msg "never"; printf '\n'; return; }
|
|
|
|
now="$(date +%s)"
|
|
delta=$(( now - ts ))
|
|
(( delta < 0 )) && delta=0
|
|
|
|
if (( delta < 60 )); then cau_msg "just now"
|
|
elif (( delta < 120 )); then cau_msg "1 minute ago"
|
|
elif (( delta < 3600 )); then cau_msg "%d minutes ago" "$(( delta / 60 ))"
|
|
elif (( delta < 7200 )); then cau_msg "1 hour ago"
|
|
elif (( delta < 86400 )); then cau_msg "%d hours ago" "$(( delta / 3600 ))"
|
|
elif (( delta < 172800 )); then cau_msg "1 day ago"
|
|
else cau_msg "%d days ago" "$(( delta / 86400 ))"
|
|
fi
|
|
printf '\n'
|
|
}
|