diff --git a/README.md b/README.md index fb06a92..d0af5f0 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,27 @@ # cachy-auto-update -Unattended background updates for CachyOS — no password prompt, no terminal, -nothing to remember. +Unattended background updates for CachyOS (supports pacman, AUR, Flatpak and AppImages). -Built for the machine you set up for somebody else and would rather not have to -maintain: it updates pacman packages, AUR packages, Flatpaks and AppImages by -itself, stays out of the way while they are gaming or on battery, never touches -the package database while they are using `pacman` by hand, and only speaks up -when something actually needs a human. +Built for the machine you set up for somebody else and would rather not maintain. +Stays out of the way while they are gaming or on battery, never touches the +package database while `pacman` is already running, and only bothers you if something +went wrong. + +## Install + +```bash +paru -S cachy-auto-update +sudo cachy-auto-update enable +``` + +Updates are **off** until you enable them. + +## Usage ```bash sudo cachy-auto-update ``` -That opens a menu with the two switches there are: - ``` CachyOS Auto-Update @@ -34,273 +41,53 @@ That opens a menu with the two switches there are: [q] Quit ``` -Everything is configurable from **[6] Settings** — a cursor list covering all -eighteen options, so nothing needs a text editor. Arrow keys select, Space or -Right changes a value, `q` goes back; changes are written immediately. - -The interface is fully translated; on a German system everything above appears -in German. - -## Install - -```bash -paru -S cachy-auto-update -sudo cachy-auto-update enable -``` - -Updates are **off** until you enable them — a freshly installed package has no -business rebuilding somebody's system before being asked. +Everything is configurable from **[6] Settings**. +The interface is also translated to German (more langs coming soon (maybe)). ## What it updates | | | |---|---| -| Repository packages | `pacman -Syu`, after `checkupdates` confirms there is work | +| Repository packages | `pacman -Syu` | | AUR | `paru` or `yay`, whichever is installed | | Flatpak | system and per-user installations | | AppImages | via [Gear Lever](https://github.com/mijorus/gearlever), if installed | -It also trims the pacman package cache after each run (`paccache`, keeping the -3 most recent versions), because otherwise `/var/cache/pacman/pkg` grows -forever — tens of gigabytes on a machine with a few large packages. Set -`KeepOldPackages=1` if disk space matters more than the ability to downgrade. -`-git`/`-devel` AUR packages and orphan removal exist as options but are off by -default. Orphan removal deletes installed software, and "orphaned" only means -nothing else depends on it — which is also true of something installed -deliberately. +## When it doesn't update -## When it holds back +A run is postponed (retried an hour later) when: -A run is postponed — and retried an hour later — when: +- battery is below 30 % (ignored on mains power), +- a game is running, +- pacman's database is locked or another package manager is running. -- the battery is below 30 % (ignored on mains power; desktops without a battery - are never affected), -- a game is running: GameMode, a known game process, or anything holding a - blocking idle inhibitor, -- pacman's database is locked, or `pacman`/`yay`/`paru`/`pamac` is running. +The machine will obviously **never** automatically restart (this is not windows) -`cachy-auto-update status` prints every one of these individually, which is the -fastest way to find out why nothing is happening. +## Safety measures -The machine is **never** restarted on its own. A kernel update that needs a -restart is reported by `cachy-auto-update status`, not by a notification: the -running kernel loses its module tree the moment pacman unpacks the new one, so -a bubble would arrive while the run is still building AUR packages and pulling -Flatpaks — and reads as an invitation to restart in the middle of it. +- A **progress bar** shows which step is running and which package is being updated. +- **Suspend and shutdown are blocked** via `systemd-inhibit` during updates. +- A `db.lck` left by a crash is removed on the next run. +- On Btrfs with `snapper`/`snap-pac` (the CachyOS default), every transaction + gets a pre/post snapshot. -## About the password question +## Package conflicts -The obvious way to automate `yay`/`paru` is to store the user's password -somewhere. This does not do that, and deliberately so: anything the daemon can -decrypt is exactly what an attacker who reaches the daemon already has, so the -encryption would be decoration. +`AutoResolveConflicts=yes` (default) retries conflicting package replacements +automatically. -Instead: - -- The updater is a **system service running as root**, so `pacman` needs no - escalation at all. -- `makepkg`, `paru` and `yay` refuse to run as root, so the AUR step drops to a - dedicated **locked system account** (`cachy-auto-update`, no password, - `/usr/bin/nologin`, its own home under `/var/lib`). Only root can become it. -- That account gets one line in `/etc/sudoers.d/cachy-auto-update` allowing it - to call `/usr/bin/pacman` without a password — which is what lets the helper - install what it built. - -No user password is stored, encrypted or otherwise. - -If you would rather not have that sudoers rule on the machine, set -`UpdateAUR=no` in the config; everything else keeps working, and the rule -becomes inert. - -## Coexisting with manual package management - -Before touching anything, the updater checks for `/var/lib/pacman/db.lck` and -for a running `pacman`, `yay`, `paru`, `pamac`, `pikaur`, `octopi` or `makepkg`, -and postpones if it finds one. On a day with no pending repository updates the -real pacman lock is never taken at all, because `checkupdates` works against a -private temporary database. - -The reverse direction has an honest limit: if you start `pacman` *while* an -update is already running, you will get the usual "unable to lock database" -message. Nothing outside pacman can prevent that. What the updater does do is -keep the window short, run at low priority, and hold a `systemd-inhibit` lock so -a suspend or shutdown cannot land in the middle of a transaction. - -A leftover `db.lck` from a crashed transaction is never deleted automatically — -guessing wrong there corrupts a live transaction. After it has been seen -unheld on several consecutive runs, you get a notification instead. - -## What if the machine is switched off mid-update - -Three layers, in order of how much they can actually promise: - -**A notification goes out before the transaction starts** — "Installing -updates, please leave the computer switched on until this is done" — and is -withdrawn again when the result arrives, so it costs one bubble rather than -two. This exists because of what the next paragraph does *not* do. - -**A progress bar sits in the notification area for the whole run**, the same -one Dolphin puts there while it copies files: which step is running, which -package is being unpacked, how far along the whole thing is. A twenty-minute -run that shows nothing looks indistinguishable from a hung one, and that is -what gets a machine switched off in the middle of a transaction. See -[The progress bar](#the-progress-bar). - -**Suspend and a normal shutdown are blocked.** The run holds a -`systemd-inhibit --what=sleep:shutdown --mode=block` lock, so closing the lid or -picking "Shut down" cannot interrupt a transaction. Be aware of what that looks -like, though: logind refuses the request and requires the polkit action -`org.freedesktop.login1.power-off-ignore-inhibit`, which is `auth_admin_keep`. -The desktop therefore answers a shutdown attempt with an **administrator -password prompt** reading *"Power off the system while an application is -inhibiting this"* — a string systemd ships untranslated, and one that never -mentions updates. No KDE dialog explains the situation. Only `systemctl -poweroff` in a terminal names the reason. That prompt is exactly why the -notification above is on by default. - -**A hard power-off cannot be prevented by anything.** Holding the power button -or pulling the plug cuts power in firmware. What limits the damage is that -pacman's commit phase is short (about a minute even for a 200-package upgrade) -and that most of a run is downloading, where an interruption costs nothing but -a partial file. - -**The next run repairs it.** A `db.lck` left behind is detected and removed — -but only when it is *provably* dead, meaning it is older than the current boot, -so no process that could hold it still exists. The interrupted upgrade is then -simply run again; pacman reinstalls anything that was caught half-written. A -lock that is merely unheld within the same boot is never removed, only -reported, because there the guess could be wrong. - -This last part matters more than it sounds: without it, a single power cut -during an update would leave a lock file that makes every future run defer, -and the machine would stop updating silently and permanently. - -On a Btrfs system with `snapper` and `snap-pac` — the CachyOS default — every -pacman transaction is bracketed by a pre and post snapshot, so a genuinely -broken upgrade can still be rolled back with `snapper rollback`. - -## How long notifications stay - -A message that means the machine still needs you — an update failed, the -package database is locked, packages had to be held back — **stays until you -dismiss it**. That kind of message is only worth sending if it is still there -when you come back to the machine. - -Everything else times out on its own, a successful update included. Nothing -should have to be clicked away for having gone right. - -Set per message rather than left to the notification daemon. Daemons do keep -critical-urgency messages up and the spec asks them to, but that is a *should*, -it says nothing about the normal-urgency messages here that still need somebody -to act, and urgency separately controls sound and do-not-disturb bypass — a -different question. Queued messages delivered at the next login keep the same -distinction. - -## The progress bar - -While a run is working, the notification area carries a live entry — headline, -item count, percentage, and the package currently being unpacked under -*Details*. It is not a notification but a **job**, the same mechanism Dolphin -uses for file copies, which is what gets you a bar rather than a line of text. - -Two things about how it is put together: - -- The desktop ties a job to the D-Bus connection that asked for it, and - withdraws the job the moment that connection closes. One-shot bus clients — - `gdbus`, `busctl`, `dbus-send` — therefore cannot drive one at all, since - every invocation is a fresh connection that closes immediately. So a small - helper (`cachy-auto-update-progress`) runs inside each graphical session for - the length of the update, holding the connection open and taking instructions - on stdin. It needs **python-gobject**; without it there is simply no bar and - nothing else changes. -- Working out the upgrade, downloading it and unpacking it are three separate - steps. The first is the one that used to look broken: between - `:: Starting full system upgrade...` and the transaction it eventually - prepares, pacman prints nothing at all, and on a large backlog that silence - runs to minutes. A counter frozen at "0 of 161" reads as a stuck update, so - that stretch carries a label and deliberately no counter. On a domestic line - the download is then the longest of the three, and calling the whole thing - "installing" would leave the bar at 4% for six minutes. -- A step that turns out to have no work is dropped from the bar rather than - handed its share for nothing. Packages already in the cache are never - announced, so a run that only has to unpack skips the download step outright - instead of leaping 30% the moment unpacking starts — and likewise for AUR - with nothing pending, or a machine with no Flatpaks. The weights only ever - have to be right about the steps that actually run. -- **Expanding *Details* shows the run log's last five lines, live.** The - percentage says the update is alive; these say what it is alive doing. It - matters most where there is nothing to count — an AUR package compiling for - a quarter of an hour talks constantly, and all of it used to go into a file - nobody was looking at. The job model behind this interface carries exactly - two description fields (`descriptionValue1` and `2` — there is no third), so - one holds the current package and the other the tail; newlines inside a value - do render, which is what makes five lines fit in one field. -- pacman's output is line-buffered through `stdbuf`. Writing to a log rather - than a terminal, libc would hand it over in 4 KB blocks instead, and 4 KB of - `upgrading foo...` is on the order of a hundred and sixty packages arriving - at once — which is how a bar comes to sit still and then jump to the end. -- Neither counted phase gets a counter from pacman on an unattended run, so - both are counted a line at a time — `foo-1.2-1-x86_64 downloading...` and - `upgrading foo...`. The database sync just before prints the same shape - (` core downloading...`) with the suffix that would give it away already - stripped, so counting starts only after pacman's `:: Retrieving packages...` - header. pacman's other `(n/m)` sequences — checking keys, package integrity, - loading files — each count to the same total, so only the transaction verbs - are followed; otherwise the bar would reach the end three times before the - first package was unpacked. - -This is Plasma's job interface. On a desktop that does not implement it the -helper exits quietly and the ordinary notifications carry on as before. - -## Configuration - -`/etc/cachy-auto-update/cachy-auto-update.conf`, one `Key=Value` per line, every -option documented in the file. The file is parsed rather than sourced, so a -stray line cannot turn into code executed by root. - -```ini -Enabled=yes -Notifications=yes -UpdateInterval=1d -MinBatteryPercent=40 -SkipWhenGaming=yes -UpdateAUR=yes -UpdateFlatpak=yes -UpdateAppImages=yes -AutoResolveConflicts=yes -IgnorePkg= -``` - -## How package conflicts are handled - -`pacman -Syu --noconfirm` already answers "Replace X with Y?" affirmatively, so -ordinary replacements happen silently — which is the point. - -What `--noconfirm` declines is `:: X and Y are in conflict. Remove Y? [y/N]`, -and that aborts the whole transaction. With `AutoResolveConflicts=yes` (the -default) the transaction is retried once with that question answered too, and -everything removed is written to the log. - -Two things are deliberately *not* automated: - -- **File conflicts** (`exists in filesystem`) — forcing `--overwrite` could - silently destroy something that was put there on purpose. -- **Reboots** — `status` tells you one is due, never a surprise restart. - -Signature failures trigger one keyring refresh and one retry, since a stale -keyring blocks everything else until it is fixed. +File conflicts (`exists in filesystem`) are **not** force-overwritten. +Signature failures trigger one keyring refresh and one retry. ## Logs ```bash -cachy-auto-update log # the last run in full -cachy-auto-update log -a # the rolling log +cachy-auto-update log # last run +cachy-auto-update log -a # rolling log journalctl -u cachy-auto-update ``` -Both work without root. - ## Building from source ```bash @@ -309,25 +96,18 @@ sudo systemd-sysusers && sudo systemd-tmpfiles --create sudo cachy-auto-update enable ``` -`make check` runs `bash -n` over every shell file, `py_compile` over the -progress helper, `shellcheck` when available, and validates the sudoers drop-in -with `visudo -c`. +`make check` runs shellcheck, syntax checks, and validates the sudoers drop-in. -Everything is optional at runtime and degrades to doing less rather than -failing: `pacman-contrib` for `checkupdates`, an AUR helper, `flatpak`, Gear -Lever, `libnotify` for notifications, and `python-gobject` for the progress bar. +Optional runtime dependencies (degrades gracefully without them): +`pacman-contrib`, an AUR helper, `flatpak`, Gear Lever, `libnotify`, +`python-gobject`. ## Relationship to cachy-update -[cachy-update](https://github.com/CachyOS/cachy-update) is CachyOS's interactive -updater; every one of its steps sits behind a prompt and its timer only ever -*checks* for updates and notifies. This tool is the unattended counterpart and -reimplements the same command sequence non-interactively, adding the battery, -gaming and lock awareness that unattended operation needs. - -The two coexist. `cachy-auto-update enable` offers once to switch off -cachy-update's own "N updates available" notification, since it becomes noise -when updates install themselves. +[cachy-update](https://github.com/CachyOS/cachy-update) is CachyOS's +interactive updater which checks for updates and notifies, but that needs manual intervention. +This is the unattended counterpart. The two can coexist; `enable` offers +to switch off cachy-update's notification since it becomes obsolete. ## License