Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
162b30ba38 | ||
|
|
c8666da714 | ||
|
|
f9cd8a0ace | ||
|
|
8834abe648 | ||
|
|
b796b2711a | ||
|
|
6538c1510d | ||
|
|
ea3f0d21fa | ||
|
|
6aa9b147b8 | ||
|
|
a17aed4b3d | ||
|
|
2fa830284e | ||
|
|
c308b7e286 | ||
|
|
2eff62f9fd | ||
|
|
0f91a79ba6 | ||
|
|
6514c4d859 | ||
|
|
243115ea19 | ||
|
|
9fd2458d20 | ||
|
|
f593cc1933 | ||
|
|
20fbadbc95 | ||
|
|
e42fb4f61c | ||
|
|
459cbbf8e7 |
No files matched your search
@@ -5,7 +5,10 @@
|
||||
# targets degrade to a no-op when msgfmt/scdoc are missing, so the tree stays
|
||||
# usable for development without the build dependencies installed.
|
||||
|
||||
VERSION := 1.0.0
|
||||
# Overridable so a packager can pass the version it is actually building
|
||||
# (`make VERSION=$pkgver`). The literal below is the fallback for builds
|
||||
# straight from a checkout, and is what a release tag has to carry.
|
||||
VERSION ?= 1.3.0
|
||||
|
||||
PREFIX ?= /usr
|
||||
DESTDIR ?=
|
||||
@@ -61,6 +64,13 @@ check:
|
||||
else \
|
||||
echo "shellcheck not found - skipped"; \
|
||||
fi
|
||||
@if command -v python3 >/dev/null 2>&1; then \
|
||||
python3 -m py_compile src/cachy-auto-update-progress \
|
||||
&& echo "ok src/cachy-auto-update-progress"; \
|
||||
rm -rf src/__pycache__; \
|
||||
else \
|
||||
echo "python3 not found - skipped"; \
|
||||
fi
|
||||
@if command -v visudo >/dev/null 2>&1; then \
|
||||
visudo -cf res/sudoers/cachy-auto-update >/dev/null && echo "ok sudoers"; \
|
||||
fi
|
||||
@@ -69,6 +79,9 @@ install: build
|
||||
# executables
|
||||
install -Dm755 src/cachy-auto-update "$(DESTDIR)$(BINDIR)/cachy-auto-update"
|
||||
install -Dm755 src/cachy-auto-update-run "$(DESTDIR)$(LIBEXECDIR)/cachy-auto-update-run"
|
||||
# holds a session bus connection open so the update can drive a progress bar
|
||||
install -Dm755 src/cachy-auto-update-progress \
|
||||
"$(DESTDIR)$(LIBEXECDIR)/cachy-auto-update-progress"
|
||||
# the version and the resolved lib path are baked in at install time
|
||||
sed -i -e 's|@VERSION@|$(VERSION)|g' \
|
||||
-e 's|@LIBDIR@|$(LIBDIR)|g' \
|
||||
@@ -111,6 +124,10 @@ install: build
|
||||
install -Dm644 res/autostart/cachy-auto-update-notify.desktop \
|
||||
"$(DESTDIR)$(XDGAUTOSTART)/cachy-auto-update-notify.desktop"
|
||||
|
||||
# names and illustrates the progress bar; hidden from the application menu
|
||||
install -Dm644 res/applications/cachy-auto-update.desktop \
|
||||
"$(DESTDIR)$(DATADIR)/applications/cachy-auto-update.desktop"
|
||||
|
||||
# translations
|
||||
@for l in $(LINGUAS); do \
|
||||
if [ -f "po/$$l.mo" ]; then \
|
||||
@@ -135,6 +152,7 @@ uninstall:
|
||||
rm -f "$(DESTDIR)$(SYSTEMDDIR)/cachy-auto-update.service"
|
||||
rm -f "$(DESTDIR)$(SYSTEMDDIR)/cachy-auto-update.timer"
|
||||
rm -f "$(DESTDIR)$(XDGAUTOSTART)/cachy-auto-update-notify.desktop"
|
||||
rm -f "$(DESTDIR)$(DATADIR)/applications/cachy-auto-update.desktop"
|
||||
rm -f "$(DESTDIR)$(MANDIR)/man1/cachy-auto-update.1"
|
||||
|
||||
clean:
|
||||
|
||||
@@ -30,9 +30,14 @@ That opens a menu with the two switches there are:
|
||||
[3] Update now
|
||||
[4] Show log
|
||||
[5] Show current conditions
|
||||
[6] Settings
|
||||
[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.
|
||||
|
||||
@@ -55,14 +60,21 @@ business rebuilding somebody's system before being asked.
|
||||
| Flatpak | system and per-user installations |
|
||||
| AppImages | via [Gear Lever](https://github.com/mijorus/gearlever), if installed |
|
||||
|
||||
`-git`/`-devel` AUR packages, cache trimming and orphan removal exist as
|
||||
options but are off by default.
|
||||
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 holds back
|
||||
|
||||
A run is postponed — and retried an hour later — when:
|
||||
|
||||
- the battery is below 40 % (ignored on mains power; desktops without a battery
|
||||
- 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,
|
||||
@@ -71,8 +83,11 @@ A run is postponed — and retried an hour later — when:
|
||||
`cachy-auto-update status` prints every one of these individually, which is the
|
||||
fastest way to find out why nothing is happening.
|
||||
|
||||
The machine is **never** restarted on its own. When a kernel update needs a
|
||||
restart, you get a notification saying so.
|
||||
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.
|
||||
|
||||
## About the password question
|
||||
|
||||
@@ -116,6 +131,128 @@ 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
|
||||
@@ -149,7 +286,7 @@ Two things are deliberately *not* automated:
|
||||
|
||||
- **File conflicts** (`exists in filesystem`) — forcing `--overwrite` could
|
||||
silently destroy something that was put there on purpose.
|
||||
- **Reboots** — you get a notification, never a surprise restart.
|
||||
- **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.
|
||||
@@ -172,8 +309,13 @@ sudo systemd-sysusers && sudo systemd-tmpfiles --create
|
||||
sudo cachy-auto-update enable
|
||||
```
|
||||
|
||||
`make check` runs `bash -n` over everything, `shellcheck` when available, and
|
||||
validates the sudoers drop-in with `visudo -c`.
|
||||
`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`.
|
||||
|
||||
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.
|
||||
|
||||
## Relationship to cachy-update
|
||||
|
||||
|
||||
+108
-3
@@ -16,7 +16,10 @@ updated in the background, with no password prompt and no terminal.
|
||||
|
||||
Run without a command it opens a small interactive menu with the two switches
|
||||
that matter - automatic updates on/off and notifications on/off - plus the
|
||||
current status.
|
||||
current status, and a settings screen covering every remaining option so the
|
||||
configuration file never has to be edited by hand. In the settings screen the
|
||||
arrow keys select, Space or Right changes a value, and _q_ goes back; every
|
||||
change is written out immediately.
|
||||
|
||||
The actual work is done by a systemd system service. The timer ticks hourly;
|
||||
whether a tick does anything is decided by _UpdateInterval_ (daily by default).
|
||||
@@ -97,8 +100,110 @@ inside each user's own account. AppImages are updated through Gear Lever, for
|
||||
users with a graphical session, and AppImages whose application is currently
|
||||
running are skipped.
|
||||
|
||||
The machine is never restarted automatically. When a kernel update makes a
|
||||
restart necessary, a notification says so.
|
||||
The machine is never restarted automatically. A kernel update that makes a
|
||||
restart necessary is reported by *cachy-auto-update status*, not by a
|
||||
notification: the running kernel loses its module tree as soon as pacman
|
||||
unpacks the new one, so a bubble would fire while the run is still working
|
||||
through AUR packages and Flatpaks, and reads as an invitation to restart in the
|
||||
middle of it.
|
||||
|
||||
# PROGRESS
|
||||
|
||||
For as long as a run is working, the notification area carries a live progress
|
||||
entry: the step being performed, how many items it has got through, an overall
|
||||
percentage, and the package currently being unpacked under "Details". This is a
|
||||
job in the sense of *org.kde.JobViewServer*, the same mechanism a file manager
|
||||
uses while copying, rather than a notification - which is what makes it a bar
|
||||
instead of a line of text.
|
||||
|
||||
The desktop withdraws a job as soon as the D-Bus connection that requested it
|
||||
closes, so a helper process runs inside each graphical session for the duration
|
||||
of the update and holds that connection open. It requires _python-gobject_.
|
||||
Where that is missing, or on a desktop with no job interface, there is no
|
||||
progress entry and nothing else is affected.
|
||||
|
||||
Working out the upgrade, fetching it and unpacking it are three steps rather
|
||||
than one. 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; that stretch therefore carries a label of its own and
|
||||
deliberately no item count, because a counter frozen at "0 of 161" reads as a
|
||||
stuck update. On a domestic line the download is then the longest of the three,
|
||||
and a bar that called the whole thing "installing" would sit near its beginning
|
||||
for minutes at a time looking stuck.
|
||||
|
||||
Expanding "Details" shows the last five lines of the run log as they are
|
||||
written, alongside the package currently being worked on. The percentage says
|
||||
the update is alive; these lines say what it is alive doing, which matters most
|
||||
during the stretches that have nothing countable to report - an AUR package
|
||||
being compiled, above all. The job interface carries exactly two description
|
||||
fields, so those are the two things shown.
|
||||
|
||||
A step that turns out to have no work is dropped from the bar instead of being
|
||||
handed its share for nothing. Packages already in the cache are never
|
||||
announced, so a run with everything already fetched skips the download step
|
||||
outright rather than jumping when unpacking starts; the same applies to AUR
|
||||
with nothing pending, or a machine with no Flatpaks installed.
|
||||
|
||||
Neither counted phase gets a counter from pacman on an unattended run, so both
|
||||
are counted here, a line at a time: "foo-1.2-1-x86_64 downloading..." for the
|
||||
first, "upgrading foo..." for the second. pacman's output is line-buffered
|
||||
through *stdbuf*(1) so those lines arrive as they happen - writing to a log
|
||||
rather than a terminal, libc would otherwise release them in 4KB blocks, around
|
||||
a hundred and sixty packages at a time. pacman's other (n/m) sequences -
|
||||
checking keys, package integrity, loading package files - each count up to the
|
||||
same total and are deliberately ignored.
|
||||
|
||||
# HOW LONG NOTIFICATIONS STAY
|
||||
|
||||
A message that reports the machine still needing a person - an update that
|
||||
failed, a package database left locked, packages that had to be held back -
|
||||
stays on screen until it is dismissed. A message like that is only worth
|
||||
sending if it is still there when somebody comes back to the machine.
|
||||
|
||||
Everything else times out by itself, an update that simply worked included.
|
||||
Nothing should have to be clicked away for having gone right.
|
||||
|
||||
This is set per message rather than left to the notification daemon. Daemons do
|
||||
keep critical-urgency messages up, and the specification asks them to, but that
|
||||
is a recommendation, it does not cover the normal-urgency messages here that
|
||||
still need somebody to act, and urgency separately governs sound and whether
|
||||
do-not-disturb is overridden.
|
||||
|
||||
Where nobody is logged in the message is spooled and delivered at the next
|
||||
login, with the same distinction preserved.
|
||||
|
||||
# INTERRUPTED UPDATES
|
||||
|
||||
Before a transaction starts, a notification says an update is running and asks
|
||||
for the machine to be left on. It is withdrawn again when the result arrives,
|
||||
so one bubble is used rather than two.
|
||||
|
||||
While a transaction is running, *cachy-auto-update* holds a
|
||||
*systemd-inhibit*(1) lock on _sleep_ and _shutdown_ in blocking mode, so a
|
||||
suspend, a lid close or a normal shutdown request cannot cut it short.
|
||||
|
||||
What a user sees when they try anyway is worth knowing: logind refuses the
|
||||
request and falls back to the polkit action
|
||||
_org.freedesktop.login1.power-off-ignore-inhibit_, which is _auth_admin_keep_,
|
||||
so the desktop presents an administrator password prompt reading "Power off the
|
||||
system while an application is inhibiting this". That string is shipped
|
||||
untranslated by systemd and does not mention updates, and no KDE dialog
|
||||
explains the situation either - only *systemctl*(1) names the inhibitor and its
|
||||
reason. Hence the notification.
|
||||
|
||||
A hard power-off - holding the power button, or losing mains power - is not
|
||||
preventable. On the next run a leftover _/var/lib/pacman/db.lck_ is removed if
|
||||
it is older than the current boot, since no process able to hold it can still
|
||||
exist; the upgrade is then repeated and pacman reinstalls whatever was caught
|
||||
half-written. A lock file that is unheld but was created during the current
|
||||
boot is reported rather than removed, because there is no way to prove it is
|
||||
abandoned.
|
||||
|
||||
Without that recovery a single power cut would leave a lock that makes every
|
||||
subsequent run defer, silently stopping updates for good.
|
||||
|
||||
On Btrfs with *snapper*(8) and *snap-pac*, each pacman transaction is bracketed
|
||||
by a pre and post snapshot, so a broken upgrade remains rollbackable.
|
||||
|
||||
# FILES
|
||||
|
||||
|
||||
+140
-8
@@ -134,7 +134,7 @@ msgstr ""
|
||||
msgid "Quit"
|
||||
msgstr ""
|
||||
|
||||
msgid "Press Enter to continue..."
|
||||
msgid "Press any key to continue..."
|
||||
msgstr ""
|
||||
|
||||
#. Commands
|
||||
@@ -168,12 +168,23 @@ msgstr ""
|
||||
msgid "base-devel is missing - AUR packages cannot be built without it."
|
||||
msgstr ""
|
||||
|
||||
msgid "cachy-update also notifies about available updates. Turn its notifications off? [y/N]"
|
||||
msgid "cachy-update also notifies about available updates. Turn its notifications off? [Y/n]"
|
||||
msgstr ""
|
||||
|
||||
msgid "cachy-update's update check has been disabled."
|
||||
msgstr ""
|
||||
|
||||
msgid "CachyOS shows a \"Reboot recommended\" notification while an update is still running. Turn it off? [Y/n]"
|
||||
msgstr ""
|
||||
|
||||
#, c-format
|
||||
msgid "The reboot notification has been turned off. Undo it by deleting %s."
|
||||
msgstr ""
|
||||
|
||||
#, c-format
|
||||
msgid "Could not write %s."
|
||||
msgstr ""
|
||||
|
||||
msgid "No log yet."
|
||||
msgstr ""
|
||||
|
||||
@@ -218,6 +229,44 @@ msgstr ""
|
||||
msgid "Without a command an interactive menu is shown."
|
||||
msgstr ""
|
||||
|
||||
#. Progress bar
|
||||
#. The headline of the desktop's progress entry. Whoever reads it has not gone
|
||||
#. looking for it - it appears next to whatever they were doing - so each one
|
||||
#. says outright that this is an update running, rather than naming the kind of
|
||||
#. package on its own.
|
||||
msgid "Checking for updates"
|
||||
msgstr ""
|
||||
|
||||
msgid "Preparing the update"
|
||||
msgstr ""
|
||||
|
||||
msgid "Downloading updates"
|
||||
msgstr ""
|
||||
|
||||
msgid "Updating system packages"
|
||||
msgstr ""
|
||||
|
||||
msgid "Updating AUR packages"
|
||||
msgstr ""
|
||||
|
||||
msgid "Updating Flatpak apps"
|
||||
msgstr ""
|
||||
|
||||
msgid "Updating AppImages"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cleaning up after the update"
|
||||
msgstr ""
|
||||
|
||||
msgid "Log"
|
||||
msgstr ""
|
||||
|
||||
msgid "Package"
|
||||
msgstr ""
|
||||
|
||||
msgid "The update was stopped before it finished."
|
||||
msgstr ""
|
||||
|
||||
#. Notifications
|
||||
msgid "System updated"
|
||||
msgstr ""
|
||||
@@ -232,14 +281,97 @@ msgstr ""
|
||||
msgid "Something went wrong while updating. Run 'cachy-auto-update log' for details."
|
||||
msgstr ""
|
||||
|
||||
msgid "Restart recommended"
|
||||
msgstr ""
|
||||
|
||||
msgid "A new kernel was installed. Please restart when it suits you."
|
||||
msgstr ""
|
||||
|
||||
msgid "Package database locked"
|
||||
msgstr ""
|
||||
|
||||
msgid "pacman's lock file looks left over from an interrupted update. Updates are paused until it is cleared."
|
||||
msgstr ""
|
||||
|
||||
msgid "Some packages were held back"
|
||||
msgstr ""
|
||||
|
||||
#, c-format
|
||||
msgid "%s could not be updated and was skipped. Everything else is up to date."
|
||||
msgstr ""
|
||||
|
||||
#, c-format
|
||||
msgid "Held back: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "The last run was stopped before it finished."
|
||||
msgstr ""
|
||||
|
||||
msgid "Finishing an interrupted update"
|
||||
msgstr ""
|
||||
|
||||
msgid "The last update was cut short, most likely because the machine was switched off. It is being finished now."
|
||||
msgstr ""
|
||||
|
||||
msgid "Installing updates"
|
||||
msgstr ""
|
||||
|
||||
msgid "%d packages are being updated. Please leave the computer switched on until this is done."
|
||||
msgstr ""
|
||||
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "(none)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Up/Down select - Space or Right changes - q goes back"
|
||||
msgstr ""
|
||||
|
||||
msgid "Package names separated by spaces, empty to clear:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Notify when an update starts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Notify after a successful update"
|
||||
msgstr ""
|
||||
|
||||
msgid "Notify when something goes wrong"
|
||||
msgstr ""
|
||||
|
||||
msgid "Time between update runs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Postpone while a game is running"
|
||||
msgstr ""
|
||||
|
||||
msgid "Only update on mains power"
|
||||
msgstr ""
|
||||
|
||||
msgid "Minimum battery level (%)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Update AUR packages"
|
||||
msgstr ""
|
||||
|
||||
msgid "Update Flatpaks"
|
||||
msgstr ""
|
||||
|
||||
msgid "Update AppImages"
|
||||
msgstr ""
|
||||
|
||||
msgid "Also rebuild -git packages"
|
||||
msgstr ""
|
||||
|
||||
msgid "AUR helper"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resolve package conflicts automatically"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trim the package cache"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cached versions to keep"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove packages nothing needs any more"
|
||||
msgstr ""
|
||||
|
||||
msgid "Never update these packages"
|
||||
msgstr ""
|
||||
@@ -135,8 +135,8 @@ msgstr "Aktuelle Bedingungen anzeigen"
|
||||
msgid "Quit"
|
||||
msgstr "Beenden"
|
||||
|
||||
msgid "Press Enter to continue..."
|
||||
msgstr "Zum Fortfahren Enter drücken …"
|
||||
msgid "Press any key to continue..."
|
||||
msgstr "Zum Fortfahren eine Taste drücken …"
|
||||
|
||||
#. Commands
|
||||
msgid "Automatic updates are on."
|
||||
@@ -169,12 +169,23 @@ msgstr "Kein AUR-Helper gefunden – für AUR-Updates paru oder yay installieren
|
||||
msgid "base-devel is missing - AUR packages cannot be built without it."
|
||||
msgstr "base-devel fehlt – ohne das lassen sich AUR-Pakete nicht bauen."
|
||||
|
||||
msgid "cachy-update also notifies about available updates. Turn its notifications off? [y/N]"
|
||||
msgstr "cachy-update benachrichtigt ebenfalls über verfügbare Updates. Dessen Benachrichtigungen abschalten? [j/N]"
|
||||
msgid "cachy-update also notifies about available updates. Turn its notifications off? [Y/n]"
|
||||
msgstr "cachy-update benachrichtigt ebenfalls über verfügbare Updates. Dessen Benachrichtigungen abschalten? [J/n]"
|
||||
|
||||
msgid "cachy-update's update check has been disabled."
|
||||
msgstr "Die Update-Prüfung von cachy-update wurde abgeschaltet."
|
||||
|
||||
msgid "CachyOS shows a \"Reboot recommended\" notification while an update is still running. Turn it off? [Y/n]"
|
||||
msgstr "CachyOS zeigt eine Benachrichtigung \"Neustart empfohlen\", während ein Update noch läuft. Abschalten? [J/n]"
|
||||
|
||||
#, c-format
|
||||
msgid "The reboot notification has been turned off. Undo it by deleting %s."
|
||||
msgstr "Die Neustart-Benachrichtigung wurde abgeschaltet. Rückgängig durch Löschen von %s."
|
||||
|
||||
#, c-format
|
||||
msgid "Could not write %s."
|
||||
msgstr "%s konnte nicht geschrieben werden."
|
||||
|
||||
msgid "No log yet."
|
||||
msgstr "Noch kein Protokoll vorhanden."
|
||||
|
||||
@@ -219,6 +230,44 @@ msgstr "Version anzeigen"
|
||||
msgid "Without a command an interactive menu is shown."
|
||||
msgstr "Ohne Befehl wird ein interaktives Menü angezeigt."
|
||||
|
||||
#. Progress bar
|
||||
#. The headline of the desktop's progress entry. Whoever reads it has not gone
|
||||
#. looking for it - it appears next to whatever they were doing - so each one
|
||||
#. says outright that this is an update running, rather than naming the kind of
|
||||
#. package on its own.
|
||||
msgid "Checking for updates"
|
||||
msgstr "Nach Updates wird gesucht"
|
||||
|
||||
msgid "Preparing the update"
|
||||
msgstr "Update wird vorbereitet"
|
||||
|
||||
msgid "Downloading updates"
|
||||
msgstr "Updates werden heruntergeladen"
|
||||
|
||||
msgid "Updating system packages"
|
||||
msgstr "Systempakete werden aktualisiert"
|
||||
|
||||
msgid "Updating AUR packages"
|
||||
msgstr "AUR-Pakete werden aktualisiert"
|
||||
|
||||
msgid "Updating Flatpak apps"
|
||||
msgstr "Flatpak-Programme werden aktualisiert"
|
||||
|
||||
msgid "Updating AppImages"
|
||||
msgstr "AppImages werden aktualisiert"
|
||||
|
||||
msgid "Cleaning up after the update"
|
||||
msgstr "Aufräumen nach dem Update"
|
||||
|
||||
msgid "Log"
|
||||
msgstr "Protokoll"
|
||||
|
||||
msgid "Package"
|
||||
msgstr "Paket"
|
||||
|
||||
msgid "The update was stopped before it finished."
|
||||
msgstr "Das Update wurde vor dem Ende abgebrochen."
|
||||
|
||||
#. Notifications
|
||||
msgid "System updated"
|
||||
msgstr "System aktualisiert"
|
||||
@@ -233,14 +282,98 @@ msgstr "Update fehlgeschlagen"
|
||||
msgid "Something went wrong while updating. Run 'cachy-auto-update log' for details."
|
||||
msgstr "Beim Update ist etwas schiefgelaufen. Details mit „cachy-auto-update log“."
|
||||
|
||||
msgid "Restart recommended"
|
||||
msgstr "Neustart empfohlen"
|
||||
|
||||
msgid "A new kernel was installed. Please restart when it suits you."
|
||||
msgstr "Es wurde ein neuer Kernel installiert. Bitte bei Gelegenheit neu starten."
|
||||
|
||||
msgid "Package database locked"
|
||||
msgstr "Paketdatenbank gesperrt"
|
||||
|
||||
msgid "pacman's lock file looks left over from an interrupted update. Updates are paused until it is cleared."
|
||||
msgstr "Die Sperrdatei von pacman scheint von einem abgebrochenen Update übrig zu sein. Bis sie entfernt ist, pausieren die Updates."
|
||||
|
||||
msgid "Some packages were held back"
|
||||
msgstr "Einige Pakete wurden zurückgehalten"
|
||||
|
||||
#, c-format
|
||||
msgid "%s could not be updated and was skipped. Everything else is up to date."
|
||||
msgstr "%s konnte nicht aktualisiert werden und wurde übersprungen. Alles andere ist aktuell."
|
||||
|
||||
#, c-format
|
||||
msgid "Held back: %s"
|
||||
msgstr "Zurückgehalten: %s"
|
||||
|
||||
msgid "The last run was stopped before it finished."
|
||||
msgstr "Der letzte Lauf wurde abgebrochen, bevor er fertig war."
|
||||
|
||||
msgid "Finishing an interrupted update"
|
||||
msgstr "Abgebrochenes Update wird beendet"
|
||||
|
||||
msgid "The last update was cut short, most likely because the machine was switched off. It is being finished now."
|
||||
msgstr "Das letzte Update wurde unterbrochen, vermutlich weil der Rechner ausgeschaltet wurde. Es wird jetzt zu Ende geführt."
|
||||
|
||||
msgid "Installing updates"
|
||||
msgstr "Updates werden installiert"
|
||||
|
||||
#, c-format
|
||||
msgid "%d packages are being updated. Please leave the computer switched on until this is done."
|
||||
msgstr "%d Pakete werden gerade aktualisiert. Bitte den Rechner so lange eingeschaltet lassen."
|
||||
|
||||
msgid "Settings"
|
||||
msgstr "Einstellungen"
|
||||
|
||||
msgid "(none)"
|
||||
msgstr "(keine)"
|
||||
|
||||
msgid "Up/Down select - Space or Right changes - q goes back"
|
||||
msgstr "Hoch/Runter wählen – Leertaste oder Rechts ändert – q zurück"
|
||||
|
||||
msgid "Package names separated by spaces, empty to clear:"
|
||||
msgstr "Paketnamen mit Leerzeichen getrennt, leer zum Löschen:"
|
||||
|
||||
msgid "Notify when an update starts"
|
||||
msgstr "Melden, wenn ein Update beginnt"
|
||||
|
||||
msgid "Notify after a successful update"
|
||||
msgstr "Melden nach erfolgreichem Update"
|
||||
|
||||
msgid "Notify when something goes wrong"
|
||||
msgstr "Melden, wenn etwas schiefgeht"
|
||||
|
||||
msgid "Time between update runs"
|
||||
msgstr "Abstand zwischen Update-Läufen"
|
||||
|
||||
msgid "Postpone while a game is running"
|
||||
msgstr "Verschieben, solange ein Spiel läuft"
|
||||
|
||||
msgid "Only update on mains power"
|
||||
msgstr "Nur am Stromnetz aktualisieren"
|
||||
|
||||
msgid "Minimum battery level (%)"
|
||||
msgstr "Mindest-Akkustand (%)"
|
||||
|
||||
msgid "Update AUR packages"
|
||||
msgstr "AUR-Pakete aktualisieren"
|
||||
|
||||
msgid "Update Flatpaks"
|
||||
msgstr "Flatpaks aktualisieren"
|
||||
|
||||
msgid "Update AppImages"
|
||||
msgstr "AppImages aktualisieren"
|
||||
|
||||
msgid "Also rebuild -git packages"
|
||||
msgstr "Auch -git-Pakete neu bauen"
|
||||
|
||||
msgid "AUR helper"
|
||||
msgstr "AUR-Helfer"
|
||||
|
||||
msgid "Resolve package conflicts automatically"
|
||||
msgstr "Paketkonflikte automatisch auflösen"
|
||||
|
||||
msgid "Trim the package cache"
|
||||
msgstr "Paket-Cache beschneiden"
|
||||
|
||||
msgid "Cached versions to keep"
|
||||
msgstr "Behaltene Versionen im Cache"
|
||||
|
||||
msgid "Remove packages nothing needs any more"
|
||||
msgstr "Nicht mehr benötigte Pakete entfernen"
|
||||
|
||||
msgid "Never update these packages"
|
||||
msgstr "Diese Pakete nie aktualisieren"
|
||||
@@ -0,0 +1,14 @@
|
||||
[Desktop Entry]
|
||||
# Not here to put an entry in the application menu - hence NoDisplay. This is
|
||||
# how the desktop learns what to call the update while it runs: the progress
|
||||
# bar in the notification area is labelled from the desktop entry named in the
|
||||
# job request, and without one the job is filed under whatever the helper
|
||||
# process happens to be called.
|
||||
Type=Application
|
||||
Name=CachyOS Auto-Update
|
||||
Comment=Unattended background updates
|
||||
Exec=cachy-auto-update
|
||||
Icon=system-software-update
|
||||
Terminal=true
|
||||
Categories=System;PackageManager;
|
||||
NoDisplay=true
|
||||
@@ -31,7 +31,7 @@ UpdateInterval=1d
|
||||
|
||||
# Never update while the battery is below this percentage. Ignored on mains
|
||||
# power and on machines without a battery.
|
||||
MinBatteryPercent=40
|
||||
MinBatteryPercent=30
|
||||
|
||||
# Update only while plugged in. Stricter than MinBatteryPercent.
|
||||
RequireAC=no
|
||||
@@ -66,21 +66,50 @@ AutoResolveConflicts=yes
|
||||
IgnorePkg=
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Housekeeping - all off by default
|
||||
# Housekeeping
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Trim the pacman package cache after an update.
|
||||
CleanCache=no
|
||||
# Trim the pacman package cache after an update: drop the older versions of
|
||||
# installed packages, and every cached version of packages that are no longer
|
||||
# installed. Nothing that is currently installed is ever touched, so this only
|
||||
# costs the ability to downgrade further back than KeepOldPackages.
|
||||
# Without it /var/cache/pacman/pkg grows forever - tens of gigabytes on a
|
||||
# machine with a few large packages.
|
||||
CleanCache=yes
|
||||
|
||||
# How many old versions per package to keep when CleanCache is on.
|
||||
# How many old versions per package to keep when CleanCache is on. 3 is the
|
||||
# Arch default and leaves room to downgrade. Set it to 1 if disk space matters
|
||||
# more than that; on a machine with a handful of multi-gigabyte packages the
|
||||
# difference is easily ten gigabytes.
|
||||
KeepOldPackages=3
|
||||
|
||||
# Remove packages that were installed as dependencies and are no longer needed.
|
||||
# Remove packages nothing depends on any more: pacman packages that were pulled
|
||||
# in as dependencies and are now unreferenced, plus Flatpak runtimes no
|
||||
# installed application uses.
|
||||
# Off by default, and unlike CleanCache this one deletes installed software:
|
||||
# "orphaned" only means no other package requires it, which is also true of
|
||||
# something installed deliberately as a dependency of nothing.
|
||||
RemoveOrphans=no
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Notification detail
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# How long a message stays on screen is not configurable, because it follows
|
||||
# from what the message is for: anything reporting that the machine still needs
|
||||
# a person - a failed update, a paused package queue, a package that had to be
|
||||
# skipped - waits until it is dismissed, since a message like that is only ever
|
||||
# useful if it is still there when somebody comes back. Everything else, an
|
||||
# update that simply worked included, times out on its own.
|
||||
|
||||
# Say that an update has started. Worth keeping on: while one runs, a shutdown
|
||||
# request is refused and the desktop answers with an untranslated polkit
|
||||
# password prompt that never mentions updates. The message is withdrawn again
|
||||
# when the result arrives, so it costs one bubble, not two.
|
||||
#
|
||||
# Separate from the progress bar in the notification area, which is not
|
||||
# optional and appears whenever the desktop supports one.
|
||||
NotifyOnStart=yes
|
||||
|
||||
# Say something after a successful update. Nothing is ever shown when there
|
||||
# was nothing to do.
|
||||
@@ -89,6 +118,9 @@ NotifyOnSuccess=yes
|
||||
# Report failures.
|
||||
NotifyOnError=yes
|
||||
|
||||
# Point out that a kernel update needs a restart. The machine is never
|
||||
# restarted automatically.
|
||||
NotifyReboot=yes
|
||||
# A kernel update that needs a restart is not announced by a notification. The
|
||||
# running kernel loses its module tree as soon as pacman unpacks the new one,
|
||||
# so that would fire while the run is still working through AUR packages and
|
||||
# Flatpaks, and reads as an invitation to restart in the middle of it.
|
||||
# `cachy-auto-update status` reports it instead. The machine is never restarted
|
||||
# automatically either way.
|
||||
+55
-3
@@ -84,6 +84,48 @@ cau_do_enable() {
|
||||
fi
|
||||
|
||||
cau_offer_disable_cachy_update
|
||||
cau_offer_disable_reboot_hook
|
||||
}
|
||||
|
||||
# CachyOS ships a pacman hook that pops up "Reboot recommended!" the moment a
|
||||
# kernel, driver or systemd package is unpacked. During a manual upgrade that
|
||||
# is fine - the transaction is the last thing happening. During an unattended
|
||||
# one it lands in the middle: the run still has AUR packages to build and
|
||||
# Flatpaks to pull, and a notification asking for a restart right then is an
|
||||
# invitation to cut the update in half.
|
||||
#
|
||||
# pacman lets a hook be overridden by name from /etc/pacman.d/hooks, which takes
|
||||
# precedence over /usr/share/libalpm/hooks, and a symlink to /dev/null there
|
||||
# disables it. That is the standard way to switch off a distribution hook, and
|
||||
# it is undone by deleting the link.
|
||||
#
|
||||
# This one is offered, never decided: it is another package's behaviour and it
|
||||
# applies to manual upgrades too.
|
||||
CAU_REBOOT_HOOK="cachyos-reboot-required.hook"
|
||||
|
||||
cau_offer_disable_reboot_hook() {
|
||||
local system="/usr/share/libalpm/hooks/$CAU_REBOOT_HOOK"
|
||||
local override="/etc/pacman.d/hooks/$CAU_REBOOT_HOOK"
|
||||
local answer
|
||||
|
||||
[[ -t 0 && -t 1 ]] || return 0
|
||||
[[ -f $system ]] || return 0
|
||||
[[ -e $override || -L $override ]] && return 0
|
||||
|
||||
printf '\n %s\n ' \
|
||||
"$(cau_msg "CachyOS shows a \"Reboot recommended\" notification while an update is still running. Turn it off? [Y/n]")"
|
||||
read -r answer || return 0
|
||||
|
||||
case "${answer,,}" in
|
||||
''|y|yes|j|ja) ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
|
||||
if mkdir -p /etc/pacman.d/hooks 2>/dev/null && ln -sfn /dev/null "$override" 2>/dev/null; then
|
||||
cau_ok "$(cau_msg "The reboot notification has been turned off. Undo it by deleting %s." "$override")"
|
||||
else
|
||||
cau_bad "$(cau_msg "Could not write %s." "$override")"
|
||||
fi
|
||||
}
|
||||
|
||||
# Whether an AUR helper exists at all, without the logging cau_aur_detect does.
|
||||
@@ -117,9 +159,17 @@ cau_offer_disable_cachy_update() {
|
||||
(( found )) || return 0
|
||||
|
||||
printf '\n %s\n ' \
|
||||
"$(cau_msg "cachy-update also notifies about available updates. Turn its notifications off? [y/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
|
||||
|
||||
# Defaults to yes: once updates install themselves, cachy-update's "N
|
||||
# updates available" is purely noise. "j" is accepted too - the prompt is
|
||||
# translated, so a German user types the German letter and used to have
|
||||
# that silently read as "no".
|
||||
case "${answer,,}" in
|
||||
''|y|yes|j|ja) ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
|
||||
while read -r user uid; do
|
||||
[[ -n $user ]] || continue
|
||||
@@ -170,7 +220,9 @@ cau_do_status() {
|
||||
|
||||
cau_do_run() {
|
||||
cau_need_root
|
||||
exec "$CAU_LIBEXECDIR/cachy-auto-update-run" "$@"
|
||||
# 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() {
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# cachy-auto-update-progress - the update's progress bar on the desktop
|
||||
#
|
||||
# Started by the update runner, once per logged-in user, inside that user's
|
||||
# session. Reads one instruction per line on stdin and turns it into the same
|
||||
# progress entry the desktop shows while Dolphin copies files:
|
||||
#
|
||||
# info<TAB>text headline, already translated by the caller
|
||||
# detail<TAB>name<TAB>value a labelled line under "Details"
|
||||
# log<TAB>name<TAB>line<TAB>line... the run log's last lines, one per field
|
||||
# total<TAB>n how many items this step has
|
||||
# done<TAB>n how many of them are finished
|
||||
# percent<TAB>n overall progress, 0-100
|
||||
# end[<TAB>message] finish; a message marks the job as failed
|
||||
#
|
||||
# Why a separate process at all: the desktop ties the progress entry to the
|
||||
# D-Bus connection that asked for it and withdraws the entry the moment that
|
||||
# connection goes away. One-shot callers - gdbus, busctl, dbus-send - therefore
|
||||
# cannot drive one, because each invocation is its own connection that closes
|
||||
# again immediately. So something has to sit there and hold the connection open
|
||||
# for as long as the update takes, and read its orders from somewhere else.
|
||||
#
|
||||
# Copyright (C) 2026 Felitendo
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import sys
|
||||
|
||||
try:
|
||||
from gi.repository import Gio, GLib
|
||||
except ImportError:
|
||||
# No GLib bindings: the update itself is unaffected, there is just no bar.
|
||||
# Stdin is still drained, because the runner writes into a pipe and a
|
||||
# reader that walks away would eventually block the update behind a full
|
||||
# pipe buffer.
|
||||
for _ in sys.stdin:
|
||||
pass
|
||||
sys.exit(0)
|
||||
|
||||
JOB_SERVICE = "org.kde.JobViewServer"
|
||||
JOB_PATH = "/JobViewServer"
|
||||
DESKTOP_ENTRY = "cachy-auto-update"
|
||||
|
||||
|
||||
class Job:
|
||||
"""The progress entry, or a do-nothing stand-in where there is no desk."""
|
||||
|
||||
def __init__(self):
|
||||
self.bus = None
|
||||
self.path = None
|
||||
|
||||
def open(self):
|
||||
self.bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||
reply = self.bus.call_sync(
|
||||
JOB_SERVICE, JOB_PATH, "org.kde.JobViewServerV2", "requestView",
|
||||
# capabilities 0: no cancel and no pause button. Neither can be
|
||||
# honoured - pacman's commit phase is not interruptible - and a
|
||||
# button that does nothing is worse than no button.
|
||||
GLib.Variant("(sia{sv})", (DESKTOP_ENTRY, 0, {})),
|
||||
GLib.VariantType("(o)"), Gio.DBusCallFlags.NONE, -1, None)
|
||||
self.path = reply.unpack()[0]
|
||||
|
||||
def call(self, method, variant):
|
||||
if self.path is None:
|
||||
return
|
||||
try:
|
||||
self.bus.call_sync(
|
||||
JOB_SERVICE, self.path, "org.kde.JobViewV2", method, variant,
|
||||
None, Gio.DBusCallFlags.NONE, -1, None)
|
||||
except GLib.Error:
|
||||
# The desktop went away mid-update - a logout, or a plasmashell
|
||||
# restart. The update carries on without a bar.
|
||||
self.path = None
|
||||
|
||||
def close(self, message=""):
|
||||
# Always terminate explicitly. A job whose owner simply disappears is
|
||||
# reported by the desktop as "the application closed unexpectedly",
|
||||
# which would turn every successful update into a failure notice.
|
||||
#
|
||||
# The error code is what decides how the entry is labelled; passing a
|
||||
# message to terminate() on its own still files the job as completed,
|
||||
# which next to "the update was stopped before it finished" reads as a
|
||||
# contradiction.
|
||||
#
|
||||
# 100 is KJob::UserDefinedError, and the value does matter: 1 is
|
||||
# KJob::KilledJobError, which the desktop discards without showing
|
||||
# anything on the grounds that whoever killed the job already knows.
|
||||
if message:
|
||||
self.call("setError", GLib.Variant("(u)", (100,)))
|
||||
self.call("terminate", GLib.Variant("(s)", (message,)))
|
||||
self.path = None
|
||||
|
||||
|
||||
def main():
|
||||
job = Job()
|
||||
try:
|
||||
job.open()
|
||||
except GLib.Error:
|
||||
# No job server on this desktop - anything that is not Plasma. Same
|
||||
# deal as a missing binding: drain stdin, stay out of the way.
|
||||
for _ in sys.stdin:
|
||||
pass
|
||||
return 0
|
||||
|
||||
unit = "items"
|
||||
try:
|
||||
for line in sys.stdin:
|
||||
fields = line.rstrip("\n").split("\t")
|
||||
cmd = fields[0]
|
||||
arg = fields[1] if len(fields) > 1 else ""
|
||||
|
||||
if cmd == "info":
|
||||
job.call("setInfoMessage", GLib.Variant("(s)", (arg,)))
|
||||
elif cmd == "detail" and len(fields) > 2:
|
||||
job.call("setDescriptionField",
|
||||
GLib.Variant("(uss)", (0, arg, fields[2])))
|
||||
elif cmd == "log" and len(fields) > 2:
|
||||
# Field 1, and there is no field 2: the job model behind this
|
||||
# interface carries exactly two, and anything further is
|
||||
# discarded without complaint at the other end.
|
||||
#
|
||||
# The tail arrives one line per field, because the protocol
|
||||
# itself is one instruction per line. Joined back up with the
|
||||
# newlines the job view does render.
|
||||
job.call("setDescriptionField",
|
||||
GLib.Variant("(uss)", (1, arg, "\n".join(fields[2:]))))
|
||||
elif cmd == "total":
|
||||
job.call("setTotalAmount", GLib.Variant("(ts)", (int(arg), unit)))
|
||||
elif cmd == "done":
|
||||
job.call("setProcessedAmount", GLib.Variant("(ts)", (int(arg), unit)))
|
||||
elif cmd == "percent":
|
||||
job.call("setPercent", GLib.Variant("(u)", (int(arg),)))
|
||||
elif cmd == "end":
|
||||
job.close(arg)
|
||||
break
|
||||
except (ValueError, IndexError):
|
||||
# A malformed line is a bug on the writing side, not a reason to leave
|
||||
# a stuck progress bar on somebody's desktop.
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
job.close()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -14,7 +14,7 @@ set -uo pipefail
|
||||
|
||||
CAU_LIBDIR="${CAU_LIBDIR:-@LIBDIR@}"
|
||||
|
||||
for _mod in common config users conditions locks notify \
|
||||
for _mod in common config users conditions locks notify progress \
|
||||
pkg_pacman pkg_aur pkg_flatpak pkg_appimage; do
|
||||
# shellcheck source=/dev/null
|
||||
if ! source "$CAU_LIBDIR/$_mod.sh"; then
|
||||
@@ -40,6 +40,13 @@ if [[ $EUID -ne 0 ]]; then
|
||||
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.
|
||||
@@ -56,6 +63,25 @@ 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)"
|
||||
# Before anything else: a progress entry whose owner merely disappears is
|
||||
# reported by the desktop as an application crash, so a killed run would
|
||||
# leave a failure message behind on top of everything else.
|
||||
cau_progress_end failed "The update was stopped before it finished."
|
||||
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?
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -95,13 +121,22 @@ if (( ! FORCE )); then
|
||||
fi
|
||||
fi
|
||||
|
||||
# A lock left behind by a power cut during a previous update. It is cleared
|
||||
# before the busy check, because otherwise every future run would defer on it
|
||||
# forever and the machine would quietly stop updating.
|
||||
if cau_recover_stale_lock; then
|
||||
cau_notify normal no \
|
||||
"Finishing an interrupted update" \
|
||||
"The last update was cut short, most likely because the machine was switched off. It is being finished now."
|
||||
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 \
|
||||
cau_notify normal yes \
|
||||
"Package database locked" \
|
||||
"pacman's lock file looks left over from an interrupted update. Updates are paused until it is cleared."
|
||||
fi
|
||||
@@ -147,6 +182,21 @@ fi
|
||||
cau_info "Starting update run"
|
||||
failed=0
|
||||
|
||||
# Open the desktop's progress bar, told up front which steps this run will
|
||||
# perform. Only those count towards the bar, so a machine with no Flatpaks
|
||||
# does not sit at 85% for the last second of the run.
|
||||
progress_steps=(resolve download repo)
|
||||
[[ $CFG_AUR == yes ]] && progress_steps+=(aur)
|
||||
[[ $CFG_FLATPAK == yes ]] && progress_steps+=(flatpak)
|
||||
[[ $CFG_APPIMAGE == yes ]] && progress_steps+=(appimage)
|
||||
progress_steps+=(cleanup)
|
||||
cau_progress_begin "${progress_steps[@]}"
|
||||
|
||||
# And carry the run log's last lines along with it, so "Details" shows what the
|
||||
# update is doing during the stretches that have nothing to count - an AUR
|
||||
# package building for a quarter of an hour, most of all.
|
||||
cau_progress_tail_start "$CAU_RUNLOG"
|
||||
|
||||
if ! cau_pacman_update; then
|
||||
failed=1
|
||||
fi
|
||||
@@ -165,6 +215,15 @@ fi
|
||||
|
||||
cau_pacman_cleanup
|
||||
|
||||
# The work is over; the bar goes away and the result takes over from here. No
|
||||
# label on a failure: the notification below carries that, and it stays up
|
||||
# until it is dismissed.
|
||||
if (( failed )); then
|
||||
cau_progress_end failed
|
||||
else
|
||||
cau_progress_end ok
|
||||
fi
|
||||
|
||||
pacnew="$(cau_pacman_pacnew_count)"
|
||||
if [[ $pacnew =~ ^[0-9]+$ ]] && (( pacnew > 0 )); then
|
||||
cau_info "$pacnew .pacnew file(s) present; left untouched"
|
||||
@@ -182,7 +241,7 @@ cau_state_write last_counts \
|
||||
if (( failed )); then
|
||||
cau_state_write last_result failed
|
||||
cau_error "Update run finished with errors"
|
||||
[[ $CFG_NOTIFY_ERROR == yes ]] && cau_notify critical \
|
||||
[[ $CFG_NOTIFY_ERROR == yes ]] && cau_notify_tagged run yes critical yes \
|
||||
"Update failed" \
|
||||
"Something went wrong while updating. Run 'cachy-auto-update log' for details."
|
||||
else
|
||||
@@ -191,16 +250,36 @@ else
|
||||
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"
|
||||
cau_notify_tagged run yes low no \
|
||||
"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 yes "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
|
||||
|
||||
# Recorded, deliberately not announced. The running kernel loses its module
|
||||
# tree the moment pacman unpacks the new one, so this turns true partway
|
||||
# through a run that still has AUR builds and Flatpaks ahead of it - and a
|
||||
# "restart recommended" bubble arriving then reads as an invitation to restart
|
||||
# while the update is still going. `cachy-auto-update status` and the menu say
|
||||
# so instead, where nobody is being interrupted mid-transaction.
|
||||
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
|
||||
|
||||
+72
-7
@@ -75,13 +75,55 @@ 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
|
||||
local locale="$1" msgid="$2" translated cachekey
|
||||
shift 2
|
||||
|
||||
translated="$(LC_ALL="$locale" LANGUAGE="${locale%%.*}" gettext -- "$msgid" 2>/dev/null)"
|
||||
[[ -n $translated ]] || translated="$msgid"
|
||||
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 - "Battery (%)",
|
||||
# "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" "$@"
|
||||
@@ -91,7 +133,14 @@ cau_msg_in() {
|
||||
# Output and logging
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if [[ -t 1 && -z ${NO_COLOR:-} ]]; then
|
||||
# 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'
|
||||
@@ -132,10 +181,19 @@ cau_log_open() {
|
||||
CAU_LOG_OPEN=1
|
||||
}
|
||||
|
||||
# Runs a command, streaming its combined output into the run log. Returns the
|
||||
# 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
|
||||
@@ -146,11 +204,18 @@ cau_run_logged() {
|
||||
# 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 - 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() { printf '%s✘%s %s\n' "$CAU_C_RED" "$CAU_C_RESET" "$*" >&2; }
|
||||
cau_note() { printf '%s•%s %s\n' "$CAU_C_DIM" "$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
|
||||
|
||||
+43
-14
@@ -6,14 +6,39 @@
|
||||
# root-run daemon, and sourcing it would turn a stray line into arbitrary code
|
||||
# execution. The format is one "Key=Value" per line, '#' starts a comment.
|
||||
|
||||
# cau_config_get <Key> [default]
|
||||
cau_config_get() {
|
||||
local key="$1" default="${2:-}" val
|
||||
# The file is cached and parsed in-process rather than shelled out to sed on
|
||||
# every lookup. The settings screen reads every key on every redraw, and a fork
|
||||
# per key made the redraw slow enough to swallow keystrokes.
|
||||
CAU_CONFIG_CACHE=''
|
||||
CAU_CONFIG_CACHED=0
|
||||
|
||||
[[ -r $CAU_CONFIG ]] || { printf '%s\n' "$default"; return; }
|
||||
_cau_config_slurp() {
|
||||
(( CAU_CONFIG_CACHED )) && return 0
|
||||
CAU_CONFIG_CACHE=''
|
||||
[[ -r $CAU_CONFIG ]] && CAU_CONFIG_CACHE="$(< "$CAU_CONFIG")"
|
||||
CAU_CONFIG_CACHED=1
|
||||
return 0
|
||||
}
|
||||
|
||||
val="$(sed -nE "s/^[[:space:]]*${key}[[:space:]]*=[[:space:]]*(.*)$/\\1/p" \
|
||||
"$CAU_CONFIG" 2>/dev/null | tail -n1)"
|
||||
# _cau_config_lookup <Key> [default]
|
||||
# Result in CAU_CONFIG_VALUE. Assigning rather than printing matters on the
|
||||
# settings screen, which reads every key on every frame: a command substitution
|
||||
# there is a fork, and forks were the entire cost of a redraw.
|
||||
CAU_CONFIG_VALUE=''
|
||||
|
||||
_cau_config_lookup() {
|
||||
local key="$1" default="${2:-}" val='' line
|
||||
|
||||
CAU_CONFIG_VALUE="$default"
|
||||
[[ -r $CAU_CONFIG ]] || return 0
|
||||
_cau_config_slurp
|
||||
|
||||
# last assignment wins, matching the previous sed|tail behaviour
|
||||
while IFS= read -r line; do
|
||||
[[ $line == *"$key"* ]] || continue
|
||||
[[ $line =~ ^[[:space:]]*"$key"[[:space:]]*=(.*)$ ]] || continue
|
||||
val="${BASH_REMATCH[1]}"
|
||||
done <<< "$CAU_CONFIG_CACHE"
|
||||
|
||||
# strip a trailing comment and surrounding whitespace/quotes
|
||||
val="${val%%#*}"
|
||||
@@ -22,11 +47,14 @@ cau_config_get() {
|
||||
val="${val%\"}"
|
||||
val="${val#\"}"
|
||||
|
||||
if [[ -n $val ]]; then
|
||||
printf '%s\n' "$val"
|
||||
else
|
||||
printf '%s\n' "$default"
|
||||
fi
|
||||
[[ -n $val ]] && CAU_CONFIG_VALUE="$val"
|
||||
return 0
|
||||
}
|
||||
|
||||
# cau_config_get <Key> [default]
|
||||
cau_config_get() {
|
||||
_cau_config_lookup "$@"
|
||||
printf '%s\n' "$CAU_CONFIG_VALUE"
|
||||
}
|
||||
|
||||
# cau_config_bool <Key> <default: yes|no>
|
||||
@@ -78,6 +106,7 @@ cau_config_set() {
|
||||
fi
|
||||
|
||||
mv -f "$tmp" "$CAU_CONFIG"
|
||||
CAU_CONFIG_CACHED=0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -96,15 +125,15 @@ cau_config_load() {
|
||||
CFG_APPIMAGE=no; cau_config_bool UpdateAppImages yes && CFG_APPIMAGE=yes
|
||||
CFG_DEVEL=no; cau_config_bool UpdateDevel no && CFG_DEVEL=yes
|
||||
CFG_RESOLVE_CONFLICTS=no; cau_config_bool AutoResolveConflicts yes && CFG_RESOLVE_CONFLICTS=yes
|
||||
CFG_CLEAN_CACHE=no; cau_config_bool CleanCache no && CFG_CLEAN_CACHE=yes
|
||||
CFG_CLEAN_CACHE=no; cau_config_bool CleanCache yes && CFG_CLEAN_CACHE=yes
|
||||
CFG_REMOVE_ORPHANS=no; cau_config_bool RemoveOrphans no && CFG_REMOVE_ORPHANS=yes
|
||||
CFG_NOTIFY_START=no; cau_config_bool NotifyOnStart yes && CFG_NOTIFY_START=yes
|
||||
CFG_NOTIFY_SUCCESS=no; cau_config_bool NotifyOnSuccess yes && CFG_NOTIFY_SUCCESS=yes
|
||||
CFG_NOTIFY_ERROR=no; cau_config_bool NotifyOnError yes && CFG_NOTIFY_ERROR=yes
|
||||
CFG_NOTIFY_REBOOT=no; cau_config_bool NotifyReboot yes && CFG_NOTIFY_REBOOT=yes
|
||||
|
||||
CFG_INTERVAL="$(cau_config_get UpdateInterval 1d)"
|
||||
CFG_INTERVAL_SECONDS="$(cau_duration_to_seconds "$CFG_INTERVAL" 86400)"
|
||||
CFG_MIN_BATTERY="$(cau_config_int MinBatteryPercent 40)"
|
||||
CFG_MIN_BATTERY="$(cau_config_int MinBatteryPercent 30)"
|
||||
CFG_KEEP_OLD="$(cau_config_int KeepOldPackages 3)"
|
||||
CFG_AUR_HELPER="$(cau_config_get AURHelper auto)"
|
||||
CFG_IGNORE_PKG="$(cau_config_get IgnorePkg '')"
|
||||
|
||||
+48
-4
@@ -60,11 +60,55 @@ cau_package_manager_busy() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# cau_pacman_lock_is_stale
|
||||
# True only when the lock provably cannot belong to anything alive.
|
||||
#
|
||||
# The rigorous test is the boot time: no process that existed before the
|
||||
# current boot can still be running, so a db.lck older than boot is abandoned
|
||||
# by definition - which is exactly what a power cut during an update leaves
|
||||
# behind. A lock that is merely unheld *within* this boot is not provable in
|
||||
# the same way, so it is only reported (see cau_track_stale_lock) and never
|
||||
# removed; guessing wrong there would corrupt a live transaction.
|
||||
#
|
||||
# The fuser check is kept as a second condition purely to survive a backwards
|
||||
# clock jump making a live lock look pre-boot.
|
||||
cau_pacman_lock_is_stale() {
|
||||
local boot lock
|
||||
|
||||
[[ -e $CAU_PACMAN_LOCK ]] || return 1
|
||||
|
||||
boot="$(awk '/^btime /{print $2}' /proc/stat 2>/dev/null)"
|
||||
[[ $boot =~ ^[0-9]+$ ]] || return 1
|
||||
|
||||
lock="$(stat -c %Y "$CAU_PACMAN_LOCK" 2>/dev/null)" || return 1
|
||||
[[ $lock =~ ^[0-9]+$ ]] || return 1
|
||||
|
||||
(( lock < boot )) || return 1
|
||||
[[ -z "$(cau_pacman_lock_holder)" ]]
|
||||
}
|
||||
|
||||
# cau_recover_stale_lock
|
||||
# Clears a provably abandoned lock so an interrupted update can be finished on
|
||||
# the next run. Without this, one power cut during an update stops every future
|
||||
# update permanently and silently - the worst possible outcome for a machine
|
||||
# nobody is watching.
|
||||
cau_recover_stale_lock() {
|
||||
cau_pacman_lock_is_stale || return 1
|
||||
|
||||
cau_warn "Found a pacman lock older than this boot - an update was cut short"
|
||||
rm -f "$CAU_PACMAN_LOCK" 2>/dev/null || {
|
||||
cau_error "Could not remove the stale pacman lock"
|
||||
return 1
|
||||
}
|
||||
cau_state_clear stale_lock_count
|
||||
cau_info "Stale lock removed; the interrupted update will be finished now"
|
||||
return 0
|
||||
}
|
||||
|
||||
# cau_track_stale_lock
|
||||
# A db.lck with no process behind it is left over from a crashed transaction.
|
||||
# Removing it automatically would be reckless - if the guess is wrong it
|
||||
# corrupts a live transaction - so instead it is counted, and after enough
|
||||
# consecutive sightings the user is told to clean it up.
|
||||
# A db.lck with no process behind it but created during this boot: a crashed
|
||||
# pacman rather than a power cut. Not provable, so it is counted, and after
|
||||
# enough consecutive sightings the user is told to clean it up.
|
||||
CAU_STALE_LOCK_RUNS=3
|
||||
|
||||
cau_track_stale_lock() {
|
||||
|
||||
+362
-12
@@ -7,6 +7,316 @@
|
||||
|
||||
CAU_UNIT="cachy-auto-update.timer"
|
||||
|
||||
# Terminal mode.
|
||||
#
|
||||
# bash flips the terminal into non-canonical mode for each `read -sn1` and back
|
||||
# out again in between. That gap matters: in canonical mode DEL is the ERASE
|
||||
# character, so the line discipline eats it instead of delivering it, and a
|
||||
# backspace typed while the interface was between reads simply vanished.
|
||||
# Holding non-canonical mode for the whole interface removes the gap.
|
||||
CAU_TERM_SAVED=''
|
||||
|
||||
cau_ui_term_raw() {
|
||||
cau_have stty || return 0
|
||||
[[ -t 0 ]] || return 0
|
||||
[[ -n $CAU_TERM_SAVED ]] && return 0
|
||||
|
||||
CAU_TERM_SAVED="$(stty -g 2>/dev/null)" || { CAU_TERM_SAVED=''; return 0; }
|
||||
stty -icanon -echo min 1 time 0 2>/dev/null || true
|
||||
}
|
||||
|
||||
cau_ui_term_restore() {
|
||||
[[ -n $CAU_TERM_SAVED ]] || return 0
|
||||
stty "$CAU_TERM_SAVED" 2>/dev/null || true
|
||||
CAU_TERM_SAVED=''
|
||||
}
|
||||
|
||||
# Runs an action with the terminal handed back to normal line mode, so anything
|
||||
# it prints - or prompts for - behaves the way a program expects.
|
||||
cau_ui_cooked() {
|
||||
cau_ui_term_restore
|
||||
"$@"
|
||||
local rc=$?
|
||||
cau_ui_term_raw
|
||||
return $rc
|
||||
}
|
||||
|
||||
# cau_read_key
|
||||
# One keypress, resolved to a symbolic name: a literal character, or one of
|
||||
# up/down/left/right/enter/space/escape. Arrow keys arrive as ESC [ A, so the
|
||||
# tail of the sequence is consumed here rather than being mistaken for three
|
||||
# separate presses.
|
||||
cau_read_key() {
|
||||
local k rest
|
||||
|
||||
IFS= read -rsn1 k || return 1
|
||||
|
||||
case "$k" in
|
||||
$'\e')
|
||||
if IFS= read -rsn2 -t 0.05 rest; then
|
||||
case "$rest" in
|
||||
'[A') printf 'up\n' ;;
|
||||
'[B') printf 'down\n' ;;
|
||||
'[C') printf 'right\n' ;;
|
||||
'[D') printf 'left\n' ;;
|
||||
*) printf 'escape\n' ;;
|
||||
esac
|
||||
else
|
||||
printf 'escape\n'
|
||||
fi
|
||||
;;
|
||||
# Enter is an empty read in cooked mode and a carriage return in raw
|
||||
# mode, depending on whether the terminal is translating it.
|
||||
''|$'\r') printf 'enter\n' ;;
|
||||
$'\x7f'|$'\b') printf 'backspace\n' ;;
|
||||
' ') printf 'space\n' ;;
|
||||
*) printf '%s\n' "$k" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# cau_ui_read_line <initial>
|
||||
# A minimal line editor built on cau_read_key, with the result in
|
||||
# CAU_LINE_RESULT.
|
||||
#
|
||||
# This exists instead of bash's own `read -r` because mixing line mode into a
|
||||
# single-key interface breaks it: after one cooked-mode read the following
|
||||
# `read -sn1` stops receiving keystrokes entirely, reproducibly, on a real pty.
|
||||
# Never leaving single-character mode side-steps that completely.
|
||||
CAU_LINE_RESULT=''
|
||||
|
||||
cau_ui_read_line() {
|
||||
local buf="${1:-}" key
|
||||
|
||||
CAU_LINE_RESULT=''
|
||||
printf '%s' "$buf"
|
||||
|
||||
while true; do
|
||||
key="$(cau_read_key)" || { printf '\n'; return 1; }
|
||||
|
||||
case "$key" in
|
||||
enter)
|
||||
printf '\n'
|
||||
CAU_LINE_RESULT="$buf"
|
||||
return 0
|
||||
;;
|
||||
escape)
|
||||
printf '\n'
|
||||
return 1
|
||||
;;
|
||||
backspace)
|
||||
if [[ -n $buf ]]; then
|
||||
buf="${buf%?}"
|
||||
printf '\b \b'
|
||||
fi
|
||||
;;
|
||||
space)
|
||||
buf+=' '
|
||||
printf ' '
|
||||
;;
|
||||
up|down|left|right) ;; # no cursor movement in this editor
|
||||
*)
|
||||
# a single printable character; control keys arrive as names
|
||||
[[ ${#key} -eq 1 ]] || continue
|
||||
buf+="$key"
|
||||
printf '%s' "$key"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Everything that can be changed without opening a text editor.
|
||||
# Format: Key|type|default|label-msgid
|
||||
# type is bool, choice:<space separated values>, or text.
|
||||
CAU_SETTINGS=(
|
||||
"NotifyOnStart|bool|yes|Notify when an update starts"
|
||||
"NotifyOnSuccess|bool|yes|Notify after a successful update"
|
||||
"NotifyOnError|bool|yes|Notify when something goes wrong"
|
||||
"UpdateInterval|choice:6h 12h 1d 2d 1w|1d|Time between update runs"
|
||||
"SkipWhenGaming|bool|yes|Postpone while a game is running"
|
||||
"RequireAC|bool|no|Only update on mains power"
|
||||
"MinBatteryPercent|choice:0 20 30 40 50 60 70 80|30|Minimum battery level (%)"
|
||||
"UpdateAUR|bool|yes|Update AUR packages"
|
||||
"UpdateFlatpak|bool|yes|Update Flatpaks"
|
||||
"UpdateAppImages|bool|yes|Update AppImages"
|
||||
"UpdateDevel|bool|no|Also rebuild -git packages"
|
||||
"AURHelper|choice:auto paru yay pikaur|auto|AUR helper"
|
||||
"AutoResolveConflicts|bool|yes|Resolve package conflicts automatically"
|
||||
"CleanCache|bool|yes|Trim the package cache"
|
||||
"KeepOldPackages|choice:0 1 2 3 5|3|Cached versions to keep"
|
||||
"RemoveOrphans|bool|no|Remove packages nothing needs any more"
|
||||
"IgnorePkg|text||Never update these packages"
|
||||
)
|
||||
|
||||
_cau_is_true() {
|
||||
case "${1,,}" in
|
||||
yes|y|true|1|on|enabled) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# _cau_setting_display <type> <value>
|
||||
# Result in CAU_SETTING_SHOWN. The three constant strings are resolved once by
|
||||
# the caller into CAU_LBL_*; looking them up here would put a translation call
|
||||
# on the per-line path.
|
||||
CAU_SETTING_SHOWN=''
|
||||
CAU_LBL_ON=''
|
||||
CAU_LBL_OFF=''
|
||||
CAU_LBL_NONE=''
|
||||
|
||||
_cau_setting_display() {
|
||||
local type="$1" value="$2"
|
||||
|
||||
case "$type" in
|
||||
bool)
|
||||
if _cau_is_true "$value"; then
|
||||
CAU_SETTING_SHOWN="${CAU_C_GREEN}${CAU_LBL_ON}${CAU_C_RESET}"
|
||||
else
|
||||
CAU_SETTING_SHOWN="${CAU_C_DIM}${CAU_LBL_OFF}${CAU_C_RESET}"
|
||||
fi
|
||||
;;
|
||||
text)
|
||||
if [[ -n $value ]]; then
|
||||
CAU_SETTING_SHOWN="$value"
|
||||
else
|
||||
CAU_SETTING_SHOWN="${CAU_C_DIM}${CAU_LBL_NONE}${CAU_C_RESET}"
|
||||
fi
|
||||
;;
|
||||
*) CAU_SETTING_SHOWN="$value" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# _cau_setting_cycle <type> <value> <direction: 1|-1>
|
||||
# The next value for this setting. Choices wrap around, so one key is enough to
|
||||
# reach everything without needing a second one for the other direction.
|
||||
_cau_setting_cycle() {
|
||||
local type="$1" value="$2" dir="$3"
|
||||
|
||||
if [[ $type == bool ]]; then
|
||||
_cau_is_true "$value" && printf 'no\n' || printf 'yes\n'
|
||||
return
|
||||
fi
|
||||
|
||||
local -a choices
|
||||
read -r -a choices <<< "${type#choice:}"
|
||||
(( ${#choices[@]} )) || { printf '%s\n' "$value"; return; }
|
||||
|
||||
local i idx=0
|
||||
for i in "${!choices[@]}"; do
|
||||
[[ ${choices[i]} == "$value" ]] && { idx=$i; break; }
|
||||
done
|
||||
|
||||
idx=$(( (idx + dir + ${#choices[@]}) % ${#choices[@]} ))
|
||||
printf '%s\n' "${choices[idx]}"
|
||||
}
|
||||
|
||||
# cau_ui_settings
|
||||
# A cursor list rather than a numbered menu: there are eighteen settings, and
|
||||
# numbering them would run out of digits and force paging.
|
||||
#
|
||||
# The frame is assembled in memory and written once. Everything constant - the
|
||||
# specs, the translated labels, the clear sequence - is resolved before the
|
||||
# loop, and the values are re-read only after something actually changes.
|
||||
# Drawing the naive way cost a command substitution per label per frame, which
|
||||
# measured 435 ms per keypress: arrow keys felt like the console was reloading,
|
||||
# because in effect it was.
|
||||
cau_ui_settings() {
|
||||
local count=${#CAU_SETTINGS[@]}
|
||||
local -a names=() types=() defaults=() labels=()
|
||||
local -a values=()
|
||||
local spec name type default label locale i key frame row pad dirty=1 cursor=0
|
||||
|
||||
locale="$(cau_ui_locale)"
|
||||
cau_msg_into "$locale" "ON"; CAU_LBL_ON="$CAU_MSG_RESULT"
|
||||
cau_msg_into "$locale" "OFF"; CAU_LBL_OFF="$CAU_MSG_RESULT"
|
||||
cau_msg_into "$locale" "(none)"; CAU_LBL_NONE="$CAU_MSG_RESULT"
|
||||
|
||||
for spec in "${CAU_SETTINGS[@]}"; do
|
||||
IFS='|' read -r name type default label <<< "$spec"
|
||||
names+=("$name"); types+=("$type"); defaults+=("$default")
|
||||
cau_msg_into "$locale" "$label"
|
||||
labels+=("$CAU_MSG_RESULT")
|
||||
done
|
||||
|
||||
local title hint
|
||||
cau_msg_into "$locale" "Settings"; title="$CAU_MSG_RESULT"
|
||||
cau_msg_into "$locale" "Up/Down select - Space or Right changes - q goes back"
|
||||
hint="$CAU_MSG_RESULT"
|
||||
|
||||
# the terminfo clear string, fetched once instead of forking per frame
|
||||
local clearseq
|
||||
clearseq="$(clear 2>/dev/null)" || clearseq=$'\033[H\033[2J'
|
||||
|
||||
while true; do
|
||||
if (( dirty )); then
|
||||
for i in "${!names[@]}"; do
|
||||
_cau_config_lookup "${names[i]}" "${defaults[i]}"
|
||||
values[i]="$CAU_CONFIG_VALUE"
|
||||
done
|
||||
dirty=0
|
||||
fi
|
||||
|
||||
frame="$clearseq"$'\n'"${CAU_C_BOLD}${CAU_C_BLUE} ${title}${CAU_C_RESET}"$'\n\n'
|
||||
|
||||
local marker selected="${CAU_C_BLUE}▸${CAU_C_RESET} "
|
||||
for i in "${!names[@]}"; do
|
||||
_cau_setting_display "${types[i]}" "${values[i]}"
|
||||
pad=$(( 42 - ${#labels[i]} ))
|
||||
(( pad < 0 )) && pad=0
|
||||
if (( i == cursor )); then marker="$selected"; else marker=' '; fi
|
||||
printf -v row ' %s%s%*s %s' \
|
||||
"$marker" "${labels[i]}" "$pad" '' "$CAU_SETTING_SHOWN"
|
||||
frame+="$row"$'\n'
|
||||
done
|
||||
|
||||
frame+=$'\n'" ${CAU_C_DIM}${hint}${CAU_C_RESET}"$'\n'
|
||||
printf '%s' "$frame"
|
||||
|
||||
key="$(cau_read_key)" || return 0
|
||||
|
||||
type="${types[cursor]}"
|
||||
name="${names[cursor]}"
|
||||
|
||||
case "$key" in
|
||||
up|k) cursor=$(( (cursor - 1 + count) % count )) ;;
|
||||
down|j) cursor=$(( (cursor + 1) % count )) ;;
|
||||
space|enter|right|l)
|
||||
if [[ $type == text ]]; then
|
||||
cau_ui_edit_text "$name" "${values[cursor]}"
|
||||
else
|
||||
cau_config_set "$name" "$(_cau_setting_cycle "$type" "${values[cursor]}" 1)" \
|
||||
|| { cau_bad "$(cau_msg "Could not write the configuration file.")"; cau_pause; }
|
||||
fi
|
||||
dirty=1
|
||||
;;
|
||||
left|h)
|
||||
if [[ $type != text ]]; then
|
||||
cau_config_set "$name" "$(_cau_setting_cycle "$type" "${values[cursor]}" -1)" \
|
||||
|| { cau_bad "$(cau_msg "Could not write the configuration file.")"; cau_pause; }
|
||||
dirty=1
|
||||
fi
|
||||
;;
|
||||
q|Q|escape) return 0 ;;
|
||||
*) ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# cau_ui_edit_text <key> <current>
|
||||
# The one setting that is a free-text list rather than a choice.
|
||||
cau_ui_edit_text() {
|
||||
local name="$1" current="$2"
|
||||
|
||||
printf '\n %s\n' "$(cau_msg "Package names separated by spaces, empty to clear:")"
|
||||
printf ' > '
|
||||
|
||||
# pre-filled with the current value so it can be corrected rather than
|
||||
# retyped; Escape leaves it unchanged
|
||||
if cau_ui_read_line "$current"; then
|
||||
cau_config_set "$name" "$CAU_LINE_RESULT" \
|
||||
|| { cau_bad "$(cau_msg "Could not write the configuration file.")"; cau_pause; }
|
||||
fi
|
||||
}
|
||||
|
||||
# _cau_row <label> <value>
|
||||
# printf's %-28s pads by bytes, so a label containing "ü" comes out one column
|
||||
# short. ${#s} counts characters in a UTF-8 locale, so the padding is computed
|
||||
@@ -81,12 +391,23 @@ cau_ui_status() {
|
||||
printf '\n %s%s%s\n' "$CAU_C_YELLOW" \
|
||||
"$(cau_msg "The last run reported a problem - see 'cachy-auto-update log'.")" \
|
||||
"$CAU_C_RESET"
|
||||
elif [[ $result == interrupted ]]; then
|
||||
printf '\n %s%s%s\n' "$CAU_C_YELLOW" \
|
||||
"$(cau_msg "The last run was stopped before it finished.")" \
|
||||
"$CAU_C_RESET"
|
||||
fi
|
||||
if [[ $reboot == 1 ]]; then
|
||||
printf '\n %s%s%s\n' "$CAU_C_YELLOW" \
|
||||
"$(cau_msg "A restart is recommended to finish a kernel update.")" \
|
||||
"$CAU_C_RESET"
|
||||
fi
|
||||
|
||||
local held
|
||||
held="$(cau_state_read held_back '')"
|
||||
if [[ -n $held ]]; then
|
||||
printf '\n %s%s%s\n' "$CAU_C_YELLOW" \
|
||||
"$(cau_msg "Held back: %s" "$held")" "$CAU_C_RESET"
|
||||
fi
|
||||
}
|
||||
|
||||
# cau_ui_status_conditions
|
||||
@@ -130,6 +451,10 @@ cau_ui_status_conditions() {
|
||||
cau_ui_menu() {
|
||||
local choice
|
||||
|
||||
cau_ui_term_raw
|
||||
# restore the terminal even if this exits through Ctrl-C or an error
|
||||
trap 'cau_ui_term_restore' EXIT INT TERM
|
||||
|
||||
while true; do
|
||||
cau_config_load
|
||||
|
||||
@@ -142,34 +467,59 @@ cau_ui_menu() {
|
||||
printf ' [3] %s\n' "$(cau_msg "Update now")"
|
||||
printf ' [4] %s\n' "$(cau_msg "Show log")"
|
||||
printf ' [5] %s\n' "$(cau_msg "Show current conditions")"
|
||||
printf ' [6] %s\n' "$(cau_msg "Settings")"
|
||||
printf ' [q] %s\n' "$(cau_msg "Quit")"
|
||||
printf '\n > '
|
||||
|
||||
read -r choice || { printf '\n'; return 0; }
|
||||
|
||||
# One keypress, no Enter. A failing read means EOF (Ctrl-D, or a
|
||||
# script piping input), which quits.
|
||||
choice="$(cau_read_key)" || {
|
||||
printf '\n'; cau_ui_term_restore; trap - EXIT INT TERM; return 0
|
||||
}
|
||||
case "$choice" in
|
||||
enter|space|up|down|left|right|escape) choice='' ;;
|
||||
esac
|
||||
printf '%s\n' "$choice"
|
||||
|
||||
# Actions run in normal line mode: they print program output and some
|
||||
# of them prompt, neither of which behaves in raw mode.
|
||||
case "$choice" in
|
||||
# The two switches flip and return straight to the menu, where the
|
||||
# status block shows the result. Only a warning or an error holds
|
||||
# the screen (see CAU_UI_NEEDS_ACK).
|
||||
1)
|
||||
if [[ $CFG_ENABLED == yes ]]; then cau_do_disable; else cau_do_enable; fi
|
||||
cau_pause
|
||||
CAU_UI_NEEDS_ACK=''
|
||||
if [[ $CFG_ENABLED == yes ]]; then
|
||||
cau_ui_cooked cau_do_disable
|
||||
else
|
||||
cau_ui_cooked cau_do_enable
|
||||
fi
|
||||
if [[ -n $CAU_UI_NEEDS_ACK ]]; then cau_pause; fi
|
||||
;;
|
||||
2)
|
||||
CAU_UI_NEEDS_ACK=''
|
||||
if [[ $CFG_NOTIFICATIONS == yes ]]; then
|
||||
cau_do_notifications off
|
||||
cau_ui_cooked cau_do_notifications off
|
||||
else
|
||||
cau_do_notifications on
|
||||
cau_ui_cooked cau_do_notifications on
|
||||
fi
|
||||
cau_pause
|
||||
if [[ -n $CAU_UI_NEEDS_ACK ]]; then cau_pause; fi
|
||||
;;
|
||||
3) cau_do_run --force; cau_pause ;;
|
||||
4) cau_do_log; cau_pause ;;
|
||||
3) cau_ui_cooked cau_do_run --force; cau_pause ;;
|
||||
4) cau_ui_cooked cau_do_log; cau_pause ;;
|
||||
5) cau_ui_status_conditions; cau_pause ;;
|
||||
q|Q|'') return 0 ;;
|
||||
6) cau_ui_settings ;;
|
||||
q|Q) cau_ui_term_restore; trap - EXIT INT TERM; return 0 ;;
|
||||
# Anything else - Enter, arrow keys, stray characters - just
|
||||
# redraws. Escape is deliberately not a quit key, so a mistyped
|
||||
# arrow key cannot close the menu.
|
||||
*) ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
cau_pause() {
|
||||
printf '\n %s' "$(cau_msg "Press Enter to continue...")"
|
||||
read -r _ || true
|
||||
printf '\n %s' "$(cau_msg "Press any key to continue...")"
|
||||
read -rsn1 _ || true
|
||||
printf '\n'
|
||||
}
|
||||
+93
-14
@@ -15,17 +15,68 @@
|
||||
CAU_NOTIFY_ICON="system-software-update"
|
||||
CAU_NOTIFY_QUEUE_MAX=20
|
||||
|
||||
# cau_notify <urgency> <title-msgid> <body-msgid> [body printf args...]
|
||||
# cau_notify <urgency> <linger: yes|no> <title-msgid> <body-msgid> [args...]
|
||||
# Never fails: a machine without libnotify, or with nobody logged in, is a
|
||||
# normal state, not an error.
|
||||
cau_notify() {
|
||||
local urgency="$1" title="$2" body="$3"
|
||||
shift 3
|
||||
cau_notify_tagged '' yes "$@"
|
||||
}
|
||||
|
||||
# cau_notify_close <user> <uid> <id>
|
||||
# notify-send can create and replace notifications but not withdraw one, so
|
||||
# this goes to the bus directly. gdbus comes from glib2, which libnotify itself
|
||||
# links against, so it is present wherever notify-send is.
|
||||
cau_notify_close() {
|
||||
cau_as_user "$1" "$2" gdbus call --session \
|
||||
--dest org.freedesktop.Notifications \
|
||||
--object-path /org/freedesktop/Notifications \
|
||||
--method org.freedesktop.Notifications.CloseNotification \
|
||||
"$3" > /dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# cau_notify_tagged <tag> <queue: yes|no> <urgency> <linger: yes|no> \
|
||||
# <title> <body> [args...]
|
||||
#
|
||||
# linger=yes keeps the message on screen until somebody dismisses it; anything
|
||||
# else lets it time out on its own. The line it draws is whether the machine
|
||||
# still needs a person: a finished update is over and done with and should not
|
||||
# have to be clicked away, while a failure, a paused queue or a package that
|
||||
# had to be skipped is only ever seen if it waits.
|
||||
#
|
||||
# Set explicitly rather than left to the server. Notification daemons do keep
|
||||
# critical-urgency messages up - the spec asks them to, and Plasma obliges -
|
||||
# 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 whether do-not-disturb is overridden. Those are not the same question.
|
||||
#
|
||||
# A tag means "at most one bubble of this kind on screen at a time": the
|
||||
# previous one carrying the same tag is withdrawn first, so "installing
|
||||
# updates" gives way to "system updated" instead of leaving two messages that
|
||||
# contradict each other.
|
||||
#
|
||||
# Withdraw-then-post rather than the obvious --replace-id, because replacing
|
||||
# only works while the old bubble is still on screen. Plasma's server drops a
|
||||
# Notify() whose replaces_id names an expired notification: no bubble, no
|
||||
# error, and the id it hands back is the dead one it just ignored. An update
|
||||
# run lasts minutes and the start bubble times out after seconds, so the
|
||||
# finished message landed in exactly that hole and was never seen. Closing an
|
||||
# id that is already gone is a no-op, which makes this safe either way.
|
||||
#
|
||||
# queue=no is for messages that only mean anything while somebody is looking.
|
||||
# Telling a user at next login that an update started an hour ago is noise.
|
||||
cau_notify_tagged() {
|
||||
local tag="$1" queue="$2" urgency="$3" linger="$4" title="$5" body="$6"
|
||||
shift 6
|
||||
local -a args=("$@")
|
||||
local delivered=0 user uid locale t b
|
||||
local delivered=0 user uid locale t b prev newid
|
||||
|
||||
[[ $CFG_NOTIFICATIONS == yes ]] || return 0
|
||||
|
||||
# -1 is "whatever the server thinks", which is what a message nobody has to
|
||||
# act on wants; 0 is "until dismissed".
|
||||
local -a expiry=(--expire-time=-1)
|
||||
[[ $linger == yes ]] && expiry=(--expire-time=0)
|
||||
|
||||
while read -r user uid; do
|
||||
[[ -n $user ]] || continue
|
||||
cau_as_user "$user" "$uid" sh -c 'command -v notify-send >/dev/null' || continue
|
||||
@@ -34,22 +85,36 @@ cau_notify() {
|
||||
t="$(cau_msg_in "$locale" "$title")"
|
||||
b="$(cau_msg_in "$locale" "$body" "${args[@]}")"
|
||||
|
||||
if cau_as_user "$user" "$uid" notify-send \
|
||||
if [[ -n $tag ]]; then
|
||||
prev="$(cau_state_read "notify_id_${tag}_${user}" 0)"
|
||||
if [[ $prev =~ ^[0-9]+$ ]] && (( prev > 0 )); then
|
||||
cau_notify_close "$user" "$uid" "$prev"
|
||||
fi
|
||||
cau_state_clear "notify_id_${tag}_${user}"
|
||||
fi
|
||||
|
||||
if newid="$(cau_as_user "$user" "$uid" notify-send \
|
||||
--app-name="$CAU_PRETTY" \
|
||||
--icon="$CAU_NOTIFY_ICON" \
|
||||
--urgency="$urgency" \
|
||||
-- "$t" "$b" 2>/dev/null
|
||||
"${expiry[@]}" \
|
||||
--print-id \
|
||||
-- "$t" "$b" 2>/dev/null)"
|
||||
then
|
||||
delivered=1
|
||||
if [[ -n $tag && $newid =~ ^[0-9]+$ ]]; then
|
||||
cau_state_write "notify_id_${tag}_${user}" "$newid"
|
||||
fi
|
||||
fi
|
||||
done < <(cau_active_session_users)
|
||||
|
||||
(( delivered )) && return 0
|
||||
[[ $queue == yes ]] || return 0
|
||||
|
||||
cau_notify_enqueue "$urgency" "$title" "$body" "${args[@]}"
|
||||
cau_notify_enqueue "$urgency" "$linger" "$title" "$body" "${args[@]}"
|
||||
}
|
||||
|
||||
# cau_notify_enqueue <urgency> <title-msgid> <body-msgid> [args...]
|
||||
# cau_notify_enqueue <urgency> <linger> <title-msgid> <body-msgid> [args...]
|
||||
# Tab-separated records, oldest first. The file is world-readable on purpose:
|
||||
# the login-time delivery runs unprivileged and only ever reads it.
|
||||
#
|
||||
@@ -57,13 +122,13 @@ cau_notify() {
|
||||
# progress by "last key seen", so two records sharing a key could make the
|
||||
# second one unreachable forever if a login landed between them.
|
||||
cau_notify_enqueue() {
|
||||
local urgency="$1" title="$2" body="$3"
|
||||
shift 3
|
||||
local urgency="$1" linger="$2" title="$3" body="$4"
|
||||
shift 4
|
||||
local record tmp
|
||||
|
||||
mkdir -p "$CAU_STATEDIR" 2>/dev/null || return 0
|
||||
|
||||
record="$(date +%s%N)"$'\t'"$urgency"$'\t'"$title"$'\t'"$body"
|
||||
record="$(date +%s%N)"$'\t'"$urgency"$'\t'"$linger"$'\t'"$title"$'\t'"$body"
|
||||
local arg
|
||||
for arg in "$@"; do
|
||||
record+=$'\t'"${arg//$'\t'/ }"
|
||||
@@ -85,8 +150,8 @@ cau_notify_enqueue() {
|
||||
# entry. State about what has already been seen lives in the user's own home,
|
||||
# so no write access to /var/lib is needed and each user is tracked separately.
|
||||
cau_notify_deliver_queue() {
|
||||
local seen_file seen ts urgency title body
|
||||
local -a args
|
||||
local seen_file seen ts urgency linger title body rest
|
||||
local -a args expiry
|
||||
|
||||
[[ -r $CAU_NOTIFY_QUEUE ]] || return 0
|
||||
cau_have notify-send || return 0
|
||||
@@ -99,20 +164,34 @@ cau_notify_deliver_queue() {
|
||||
[[ $seen =~ ^[0-9]+$ ]] || seen=0
|
||||
|
||||
local newest="$seen"
|
||||
while IFS=$'\t' read -r ts urgency title body rest; do
|
||||
while IFS=$'\t' read -r ts urgency linger title body rest; do
|
||||
[[ $ts =~ ^[0-9]+$ ]] || continue
|
||||
(( ts > seen )) || continue
|
||||
|
||||
# Records spooled before the linger field existed have the title where
|
||||
# the flag now sits. Shift them back rather than announcing an update
|
||||
# under the headline "yes".
|
||||
if [[ $linger != yes && $linger != no ]]; then
|
||||
rest="${body}${rest:+$'\t'}${rest:-}"
|
||||
body="$title"
|
||||
title="$linger"
|
||||
linger=no
|
||||
fi
|
||||
|
||||
# remaining tab-separated fields are the body's printf arguments
|
||||
args=()
|
||||
if [[ -n ${rest:-} ]]; then
|
||||
IFS=$'\t' read -r -a args <<< "$rest"
|
||||
fi
|
||||
|
||||
expiry=(--expire-time=-1)
|
||||
[[ $linger == yes ]] && expiry=(--expire-time=0)
|
||||
|
||||
notify-send \
|
||||
--app-name="$CAU_PRETTY" \
|
||||
--icon="$CAU_NOTIFY_ICON" \
|
||||
--urgency="${urgency:-normal}" \
|
||||
"${expiry[@]}" \
|
||||
-- "$(cau_msg "$title")" "$(cau_msg "$body" "${args[@]}")" 2>/dev/null || true
|
||||
|
||||
(( ts > newest )) && newest="$ts"
|
||||
|
||||
@@ -43,6 +43,28 @@ cau_appimage_update() {
|
||||
local user uid count rc=0
|
||||
local -a cmd
|
||||
|
||||
# Is there a Gear Lever on this machine at all? Asked before the step is
|
||||
# announced rather than discovered inside the loop: on a machine without
|
||||
# one - the common case, it is an optional dependency - a step that exists
|
||||
# only to hand its share of the bar straight to the next one is a jump the
|
||||
# bar does not need. Stops at the first user who has it, so the extra probe
|
||||
# costs anything only in the case it is there to remove.
|
||||
local found=0
|
||||
while read -r user uid; do
|
||||
[[ -n $user ]] || continue
|
||||
if _cau_gearlever_cmd "$user" "$uid" > /dev/null; then
|
||||
found=1
|
||||
break
|
||||
fi
|
||||
done < <(cau_active_session_users)
|
||||
|
||||
if (( ! found )); then
|
||||
cau_progress_drop appimage
|
||||
return 0
|
||||
fi
|
||||
|
||||
cau_progress_step appimage "Updating AppImages"
|
||||
|
||||
while read -r user uid; do
|
||||
[[ -n $user ]] || continue
|
||||
|
||||
|
||||
+17
-1
@@ -124,24 +124,40 @@ cau_aur_update() {
|
||||
local pending failures
|
||||
local -a args
|
||||
|
||||
cau_aur_ready || return 0
|
||||
# No helper, no base-devel, no bar: a step that cannot run should not be
|
||||
# holding a share of it.
|
||||
cau_aur_ready || { cau_progress_drop aur; return 0; }
|
||||
|
||||
cau_progress_step aur "Updating AUR packages"
|
||||
|
||||
pending="$(cau_aur_pending)"
|
||||
if (( pending == 0 )); then
|
||||
cau_info "No AUR updates pending"
|
||||
cau_state_clear aur_failures
|
||||
cau_progress_drop aur
|
||||
return 0
|
||||
fi
|
||||
|
||||
cau_info "Updating $pending AUR package(s) with $CAU_AUR_HELPER"
|
||||
|
||||
# The helper builds each package from source and prints plenty about it,
|
||||
# none of it countable from this side. A single large package can take ten
|
||||
# minutes, so the bar creeps through the step rather than sitting at its
|
||||
# start for all of them; the item count stays where it is, because that is
|
||||
# the number that would be lying if it moved.
|
||||
cau_progress_item 0 "$pending"
|
||||
mapfile -t args < <(cau_aur_helper_args)
|
||||
cau_progress_creep_start
|
||||
|
||||
if cau_run_logged cau_as_build_user "$CAU_AUR_HELPER" "${args[@]}"; then
|
||||
cau_progress_creep_stop
|
||||
CAU_AUR_COUNT="$pending"
|
||||
cau_progress_item "$pending"
|
||||
cau_state_clear aur_failures
|
||||
return 0
|
||||
fi
|
||||
|
||||
cau_progress_creep_stop
|
||||
CAU_AUR_COUNT=0
|
||||
failures="$(cau_state_read aur_failures 0)"
|
||||
[[ $failures =~ ^[0-9]+$ ]] || failures=0
|
||||
|
||||
+20
-3
@@ -26,17 +26,31 @@ cau_flatpak_pending_system() {
|
||||
cau_flatpak_update() {
|
||||
local rc=0 pending user uid home count
|
||||
|
||||
cau_have flatpak || return 0
|
||||
cau_have flatpak || { cau_progress_drop flatpak; return 0; }
|
||||
|
||||
# refresh appstream metadata first so remote-ls sees current versions
|
||||
cau_progress_step flatpak "Updating Flatpak apps"
|
||||
|
||||
# refresh appstream metadata first so remote-ls sees current versions.
|
||||
# Nothing is countable until that has finished, so the bar creeps rather
|
||||
# than waiting at the start of the step for it.
|
||||
cau_progress_creep_start
|
||||
cau_run_logged flatpak update --appstream --system --noninteractive || true
|
||||
cau_progress_creep_stop
|
||||
|
||||
pending="$(cau_flatpak_pending_system)"
|
||||
if (( pending > 0 )); then
|
||||
cau_info "Updating $pending system Flatpak(s)"
|
||||
cau_progress_item 0 "$pending"
|
||||
# Started after the count above and stopped before the one below, so the
|
||||
# only reports this side makes while a ticker is running are further
|
||||
# along than the ticker ever gets.
|
||||
cau_progress_creep_start
|
||||
if cau_run_logged flatpak update --system --noninteractive --assumeyes; then
|
||||
cau_progress_creep_stop
|
||||
CAU_FLATPAK_COUNT=$(( CAU_FLATPAK_COUNT + pending ))
|
||||
cau_progress_item "$pending"
|
||||
else
|
||||
cau_progress_creep_stop
|
||||
cau_warn "System Flatpak update failed"
|
||||
rc=1
|
||||
fi
|
||||
@@ -63,7 +77,10 @@ cau_flatpak_update() {
|
||||
fi
|
||||
done < <(cau_human_users)
|
||||
|
||||
if [[ $CFG_CLEAN_CACHE == yes ]]; then
|
||||
# Unused runtimes are the Flatpak equivalent of orphaned packages - this is
|
||||
# removal of installed software, not cache trimming, so it belongs behind
|
||||
# RemoveOrphans rather than CleanCache.
|
||||
if [[ $CFG_REMOVE_ORPHANS == yes ]]; then
|
||||
cau_run_logged flatpak uninstall --system --unused --noninteractive --assumeyes || true
|
||||
fi
|
||||
|
||||
|
||||
+352
-61
@@ -10,15 +10,210 @@
|
||||
CAU_PACMAN_COUNT=0
|
||||
CAU_PACMAN_PENDING=''
|
||||
|
||||
# Packages that had to be skipped so the rest of the upgrade could go through.
|
||||
CAU_PACMAN_HELD=''
|
||||
|
||||
# Base flags for every unattended pacman invocation.
|
||||
cau_pacman_flags() {
|
||||
printf '%s\n' --noconfirm --color never --noprogressbar --disable-download-timeout
|
||||
printf '%s\n' --noconfirm --color never --disable-download-timeout
|
||||
|
||||
# A progress bar is worth having when somebody is watching a `run` from a
|
||||
# terminal; in the timer's log it is only carriage-return noise.
|
||||
[[ -n $CAU_INTERACTIVE ]] || printf '%s\n' --noprogressbar
|
||||
|
||||
local pkg
|
||||
for pkg in $CFG_IGNORE_PKG; do
|
||||
printf '%s\n' --ignore "$pkg"
|
||||
done
|
||||
}
|
||||
|
||||
# The verbs pacman puts in front of a package as it works through a
|
||||
# transaction. Matched against English on purpose: the runner forces LC_ALL=C
|
||||
# precisely so pacman's output stays parseable.
|
||||
CAU_PACMAN_OP_RE='^(\([[:space:]]*[0-9]+/[0-9]+\) )?(upgrading|installing|reinstalling|downgrading|removing) [^[:space:]]+'
|
||||
|
||||
# Before any of that, everything has to be fetched, and on a domestic line
|
||||
# that is the longer half of the run: two hundred packages take minutes to
|
||||
# arrive and seconds to unpack. pacman prints one line per package while it
|
||||
# does it,
|
||||
#
|
||||
# glibc-2.44+r24+g16be1518495f-1-x86_64_v3 downloading...
|
||||
#
|
||||
# and nothing else - no counter, no total - so the position here is counted the
|
||||
# same way the transaction is.
|
||||
#
|
||||
# The database sync a few lines earlier prints the very same shape (" core
|
||||
# downloading..."), and pacman strips the suffix that would tell a database
|
||||
# from a package, so the count begins only after the header that separates the
|
||||
# two phases.
|
||||
CAU_PACMAN_DL_AWK='
|
||||
/^:: Retrieving packages/ { retrieving = 1; next }
|
||||
retrieving && / downloading\.\.\.$/ { n++; name = $1 }
|
||||
END { print n + 0, name }'
|
||||
|
||||
# _cau_pacman_progress_watch <logfile>
|
||||
# Feeds the desktop's progress bar by watching pacman work, through the three
|
||||
# phases a pacman run has: first it works out what the upgrade consists of,
|
||||
# then everything is fetched, then everything is unpacked. Three steps on the
|
||||
# bar rather than one, because they are three stretches to sit through - and
|
||||
# each one is silent in its own way.
|
||||
#
|
||||
# The first is the one that used to look like a hang. Between "starting full
|
||||
# system upgrade" and the transaction it eventually prepares, pacman says
|
||||
# nothing whatsoever, and on a large backlog that silence is minutes long. The
|
||||
# only honest thing to show there is a label and no counter at all: a tally
|
||||
# frozen at "0 of 161" reads as a stuck update, where "Preparing the update"
|
||||
# with no number reads as what it actually is.
|
||||
#
|
||||
# In the transaction, pacman announces each package twice over, in one of two
|
||||
# shapes, and which one depends on a flag this program sets itself:
|
||||
#
|
||||
# upgrading glibc... with --noprogressbar, i.e. every timer run
|
||||
# ( 12/218) upgrading glibc [##] with the bar, i.e. an interactive `run`
|
||||
#
|
||||
# Only the second carries a counter, and the unattended runs that this bar
|
||||
# exists for are exactly the ones that do not get it. So the position is
|
||||
# counted here instead - one line per package - and the total taken from the
|
||||
# "Package (218)" header pacman prints before it starts. That header is the
|
||||
# better number anyway: checkupdates counts packages with an update available
|
||||
# and knows nothing about the new dependencies pulled in alongside them.
|
||||
#
|
||||
# What must not be counted is the other (n/m) sequence pacman prints, for
|
||||
# hooks and for checking keys, integrity and file conflicts. Each of those runs
|
||||
# up to its own total, so following them would drive the bar to the end several
|
||||
# times before the first package was unpacked. Requiring one of the verbs above
|
||||
# is what excludes them.
|
||||
#
|
||||
# Read by polling the log rather than from a pipe: the log is written either by
|
||||
# pacman directly or through tee depending on whether a person is watching, and
|
||||
# one reader that works for both is worth the second of latency it costs.
|
||||
_cau_pacman_progress_watch() {
|
||||
local log="$1"
|
||||
local total="${CAU_PACMAN_COUNT:-0}" announced processed line pkg last=''
|
||||
local phase=resolve fetched shown='' labelled=''
|
||||
local t0=$SECONDS
|
||||
|
||||
while :; do
|
||||
sleep 1
|
||||
|
||||
announced="$(grep -aoE '^Packages? \([0-9]+\)' "$log" 2>/dev/null \
|
||||
| head -n1 | grep -oE '[0-9]+')"
|
||||
[[ $announced =~ ^[0-9]+$ ]] && (( announced > 0 )) && total="$announced"
|
||||
|
||||
line="$(grep -aoE "$CAU_PACMAN_OP_RE" "$log" 2>/dev/null | tail -n1)"
|
||||
|
||||
if [[ -n $line ]]; then
|
||||
# Unpacking has started, so whatever came before it is over. If
|
||||
# nothing was ever retrieved - every package already sitting in the
|
||||
# cache, which is the ordinary state of affairs after a run that was
|
||||
# interrupted once already - then the download step never happened,
|
||||
# and it is dropped rather than handed its whole share of the bar in
|
||||
# exchange for no work at all.
|
||||
if [[ $phase != install ]]; then
|
||||
[[ $phase == download ]] || cau_progress_drop download
|
||||
phase=install
|
||||
cau_progress_step repo "Updating system packages" "$total"
|
||||
fi
|
||||
|
||||
# Nothing new since the last look. Checked before the counting grep
|
||||
# because on a large upgrade this loop spends most of its life here.
|
||||
[[ $line != "$last" ]] || continue
|
||||
last="$line"
|
||||
|
||||
processed="$(grep -acE "$CAU_PACMAN_OP_RE" "$log" 2>/dev/null)"
|
||||
[[ $processed =~ ^[0-9]+$ ]] || continue
|
||||
|
||||
# Where pacman does carry a counter, believe it over the tally: it
|
||||
# is the same number, but it also knows the true total.
|
||||
if [[ $line =~ ^\([[:space:]]*([0-9]+)/([0-9]+)\) ]]; then
|
||||
processed="${BASH_REMATCH[1]}"
|
||||
total="${BASH_REMATCH[2]}"
|
||||
fi
|
||||
|
||||
cau_progress_item "$processed" "$total"
|
||||
|
||||
pkg="${line##* }"
|
||||
cau_progress_detail "Package" "${pkg%...}"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Fetching. The header is what tells this apart from the database sync
|
||||
# a few lines earlier, which prints the very same shape.
|
||||
if grep -qa '^:: Retrieving packages' "$log" 2>/dev/null; then
|
||||
if [[ $phase == resolve ]]; then
|
||||
phase=download
|
||||
cau_progress_step download "Downloading updates" "$total"
|
||||
fi
|
||||
|
||||
read -r fetched pkg < <(awk "$CAU_PACMAN_DL_AWK" "$log" 2>/dev/null)
|
||||
[[ $fetched =~ ^[0-9]+$ ]] || continue
|
||||
[[ $fetched != "$shown" ]] || continue
|
||||
shown="$fetched"
|
||||
|
||||
cau_progress_item "$fetched" "$total"
|
||||
# Down to the bare name, as the transaction reports it: the file
|
||||
# pacman names here carries version, release and architecture.
|
||||
[[ -n $pkg ]] && cau_progress_detail "Package" "${pkg%-*-*-*}"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Still resolving. pacman does mark the point where it stops syncing
|
||||
# databases and starts working out the upgrade, and that is the half
|
||||
# worth naming, because it is the half that takes the minutes.
|
||||
if [[ $phase == resolve && $labelled != upgrade ]] \
|
||||
&& grep -qa '^:: Starting full system upgrade' "$log" 2>/dev/null; then
|
||||
labelled=upgrade
|
||||
cau_progress_step resolve "Preparing the update"
|
||||
fi
|
||||
|
||||
# Nothing countable happens in here at all, so the bar creeps instead.
|
||||
# This is the stretch that used to look like a hung update.
|
||||
cau_progress_creep $(( SECONDS - t0 ))
|
||||
done
|
||||
}
|
||||
|
||||
# _cau_pacman_exec <logfile> <pacman args...>
|
||||
# Captures pacman's output for classification, and streams it as well when a
|
||||
# person is watching. Upgrading a few hundred packages takes minutes; without
|
||||
# this an interactive run shows one line and then nothing at all, which is
|
||||
# indistinguishable from a hang and invites someone to kill it mid-transaction.
|
||||
_cau_pacman_exec() {
|
||||
local log="$1"
|
||||
shift
|
||||
local rc watcher=0
|
||||
|
||||
# Only worth a second process and a grep per second if a bar exists to feed.
|
||||
if cau_progress_active; then
|
||||
_cau_pacman_progress_watch "$log" &
|
||||
watcher=$!
|
||||
fi
|
||||
|
||||
# Line-buffered on purpose. pacman writes to a file or through a pipe here,
|
||||
# never to a terminal, so libc buffers it in 4KB blocks - and 4KB of
|
||||
# "upgrading foo..." is on the order of a hundred and sixty packages. The
|
||||
# watcher would see nothing at all, then a hundred and sixty lines at once,
|
||||
# which is exactly how a bar comes to sit still and then leap to the end.
|
||||
# Guarded rather than assumed: without coreutils there is no bar to feed
|
||||
# either, but there is still an update to run.
|
||||
local -a buffered=()
|
||||
cau_have stdbuf && buffered=(stdbuf -oL)
|
||||
|
||||
if [[ -n $CAU_INTERACTIVE ]]; then
|
||||
"${buffered[@]}" pacman "$@" 2>&1 | tee "$log"
|
||||
rc="${PIPESTATUS[0]}"
|
||||
else
|
||||
"${buffered[@]}" pacman "$@" > "$log" 2>&1
|
||||
rc=$?
|
||||
fi
|
||||
|
||||
if (( watcher )); then
|
||||
kill "$watcher" 2>/dev/null
|
||||
wait "$watcher" 2>/dev/null
|
||||
fi
|
||||
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
# cau_pacman_pending
|
||||
# Fills CAU_PACMAN_PENDING and returns 1 when there is nothing to do.
|
||||
cau_pacman_pending() {
|
||||
@@ -55,6 +250,8 @@ _cau_pacman_classify() {
|
||||
|
||||
if grep -qiE 'are in conflict|unresolvable package conflicts' "$log"; then
|
||||
printf 'conflict\n'
|
||||
elif grep -qiE 'could not satisfy dependencies|breaks dependency|unable to satisfy dependency' "$log"; then
|
||||
printf 'dependency\n'
|
||||
elif grep -qiE 'signature from .* is (unknown trust|marginal trust|invalid)|invalid or corrupted package \(PGP signature\)|key ".*" is unknown|keyring is not writable' "$log"; then
|
||||
printf 'keyring\n'
|
||||
elif grep -qiE 'exists in filesystem' "$log"; then
|
||||
@@ -64,6 +261,25 @@ _cau_pacman_classify() {
|
||||
fi
|
||||
}
|
||||
|
||||
# _cau_pacman_blockers <logfile>
|
||||
# The packages standing in the way of an otherwise fine upgrade. pacman names
|
||||
# them in its dependency errors:
|
||||
#
|
||||
# :: removing libperconaserverclient breaks dependency 'libperconaserverclient'
|
||||
# required by heidisql-qt6-bin
|
||||
# :: unable to satisfy dependency 'foo' required by bar
|
||||
#
|
||||
# In the first form the package being removed is the one to keep; in the second
|
||||
# it is the package that cannot be installed.
|
||||
_cau_pacman_blockers() {
|
||||
local log="$1"
|
||||
|
||||
{
|
||||
sed -nE "s/.*removing ([^ ]+) breaks dependency.*/\\1/p" "$log"
|
||||
sed -nE "s/.*unable to satisfy dependency '[^']*' required by ([^ ]+).*/\\1/p" "$log"
|
||||
} | grep -E '^[A-Za-z0-9@._+-]+$' | sort -u
|
||||
}
|
||||
|
||||
# cau_pacman_update
|
||||
# Returns 0 on success (including "nothing to do"), 1 on a failure the user
|
||||
# needs to hear about. CAU_PACMAN_COUNT holds how many packages moved.
|
||||
@@ -71,80 +287,146 @@ cau_pacman_update() {
|
||||
local log kind
|
||||
local -a flags
|
||||
|
||||
# checkupdates goes first, against its own private database, and the bar
|
||||
# says so rather than naming a step that has not begun. The watcher takes
|
||||
# over from here and moves on to the download and repo steps as pacman
|
||||
# actually reaches them.
|
||||
cau_progress_step resolve "Checking for updates"
|
||||
|
||||
if ! cau_pacman_pending; then
|
||||
cau_info "No repository updates pending"
|
||||
# Neither of the two steps this would have led to is going to happen,
|
||||
# so the rest of the run gets their share of the bar instead of
|
||||
# watching it jump 70% the moment Flatpaks start.
|
||||
cau_progress_drop download repo
|
||||
return 0
|
||||
fi
|
||||
|
||||
CAU_PACMAN_COUNT="$(grep -c . <<< "$CAU_PACMAN_PENDING")"
|
||||
[[ $CAU_PACMAN_COUNT =~ ^[0-9]+$ ]] || CAU_PACMAN_COUNT=0
|
||||
cau_info "Updating $CAU_PACMAN_COUNT repository package(s)"
|
||||
if [[ -n $CAU_PACMAN_PENDING ]]; then
|
||||
CAU_PACMAN_COUNT="$(grep -c . <<< "$CAU_PACMAN_PENDING")"
|
||||
[[ $CAU_PACMAN_COUNT =~ ^[0-9]+$ ]] || CAU_PACMAN_COUNT=0
|
||||
cau_info "Updating $CAU_PACMAN_COUNT repository package(s)"
|
||||
# Deliberately no item count yet. Until pacman has prepared a
|
||||
# transaction there is nothing being worked through, and "0 of 161"
|
||||
# against a bar that cannot move for the next few minutes is the exact
|
||||
# impression the resolve step exists to avoid.
|
||||
else
|
||||
# checkupdates is unavailable, so the list is unknown and pacman is
|
||||
# asked to work it out itself.
|
||||
CAU_PACMAN_COUNT=0
|
||||
cau_info "Running a full system upgrade (pending list unavailable)"
|
||||
fi
|
||||
|
||||
# Say so before the transaction starts, not after it finishes.
|
||||
#
|
||||
# While an upgrade runs, a shutdown request is refused by logind and the
|
||||
# desktop answers with a polkit password prompt reading "Power off the
|
||||
# system while an application is inhibiting this" - which never mentions
|
||||
# updates and, on a German system, is not even translated. Somebody who was
|
||||
# simply told beforehand does not end up staring at that.
|
||||
if [[ $CFG_NOTIFY_START == yes ]]; then
|
||||
cau_notify_tagged run no normal no \
|
||||
"Installing updates" \
|
||||
"%d packages are being updated. Please leave the computer switched on until this is done." \
|
||||
"${CAU_PACMAN_COUNT:-0}"
|
||||
fi
|
||||
|
||||
mapfile -t flags < <(cau_pacman_flags)
|
||||
log="$(mktemp)" || return 1
|
||||
|
||||
if pacman -Syu "${flags[@]}" > "$log" 2>&1; then
|
||||
cat "$log" >> "$CAU_RUNLOG" 2>/dev/null
|
||||
rm -f "$log"
|
||||
return 0
|
||||
fi
|
||||
# Recovery loop rather than a single retry: fixing one problem regularly
|
||||
# uncovers the next (a conflict resolved into a dependency error, say).
|
||||
# Each remedy is applied at most once, so this always terminates.
|
||||
local -a extra=() blockers=() tried=()
|
||||
local attempt=0 b
|
||||
|
||||
cat "$log" >> "$CAU_RUNLOG" 2>/dev/null
|
||||
kind="$(_cau_pacman_classify "$log")"
|
||||
cau_warn "pacman -Syu failed ($kind)"
|
||||
|
||||
case "$kind" in
|
||||
keyring)
|
||||
# A stale keyring is the one failure that is always safe to fix
|
||||
# automatically, and it blocks everything else until it is.
|
||||
cau_info "Refreshing keyrings and retrying"
|
||||
local -a keyrings=()
|
||||
pacman -Qq archlinux-keyring &> /dev/null && keyrings+=(archlinux-keyring)
|
||||
pacman -Qq cachyos-keyring &> /dev/null && keyrings+=(cachyos-keyring)
|
||||
if (( ${#keyrings[@]} )); then
|
||||
cau_run_logged pacman -Sy --noconfirm --color never "${keyrings[@]}" || true
|
||||
fi
|
||||
if pacman -Syu "${flags[@]}" > "$log" 2>&1; then
|
||||
cat "$log" >> "$CAU_RUNLOG" 2>/dev/null
|
||||
rm -f "$log"
|
||||
return 0
|
||||
fi
|
||||
while true; do
|
||||
if _cau_pacman_exec "$log" -Syu "${flags[@]}" "${extra[@]}"; then
|
||||
# The watcher decided the same thing in a subshell, so a step it
|
||||
# dropped is still in the plan out here. Same question, same answer,
|
||||
# and the two copies agree on what the rest of the run is scaled
|
||||
# against. Only on the way out: a failed attempt is about to be
|
||||
# retried, and that retry may well download after all.
|
||||
grep -qa '^:: Retrieving packages' "$log" 2>/dev/null \
|
||||
|| cau_progress_drop download
|
||||
cat "$log" >> "$CAU_RUNLOG" 2>/dev/null
|
||||
;;
|
||||
|
||||
conflict)
|
||||
# A package that has to replace another one. --noconfirm already
|
||||
# answers "Replace X with Y?" affirmatively; what it declines is
|
||||
# ":: X and Y are in conflict. Remove Y? [y/N]". --ask is pacman's
|
||||
# question bitmask: 4 = CONFLICT_PKG, 16 = REMOVE_PKGS.
|
||||
if [[ $CFG_RESOLVE_CONFLICTS != yes ]]; then
|
||||
cau_error "Package conflict requires a decision (AutoResolveConflicts is off)"
|
||||
rm -f "$log"
|
||||
return 1
|
||||
fi
|
||||
cau_info "Resolving package conflicts automatically and retrying"
|
||||
if pacman -Syu "${flags[@]}" --ask=20 > "$log" 2>&1; then
|
||||
cat "$log" >> "$CAU_RUNLOG" 2>/dev/null
|
||||
grep -E '^(removing|replacing) ' "$log" 2>/dev/null \
|
||||
| while read -r line; do cau_info " $line"; done
|
||||
rm -f "$log"
|
||||
return 0
|
||||
fi
|
||||
cat "$log" >> "$CAU_RUNLOG" 2>/dev/null
|
||||
;;
|
||||
|
||||
filesystem)
|
||||
# Untracked files in the way. Forcing --overwrite here could
|
||||
# silently clobber something the user put there deliberately, so
|
||||
# this one stays a human decision.
|
||||
cau_error "Files on disk conflict with the update; manual review needed"
|
||||
grep -E '^(removing|replacing) ' "$log" 2>/dev/null \
|
||||
| while read -r line; do cau_info " $line"; done
|
||||
rm -f "$log"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
return 0
|
||||
fi
|
||||
|
||||
cat "$log" >> "$CAU_RUNLOG" 2>/dev/null
|
||||
kind="$(_cau_pacman_classify "$log")"
|
||||
cau_warn "pacman -Syu failed ($kind)"
|
||||
|
||||
if (( ++attempt > 3 )) || [[ " ${tried[*]} " == *" $kind "* ]]; then
|
||||
break
|
||||
fi
|
||||
tried+=("$kind")
|
||||
|
||||
case "$kind" in
|
||||
keyring)
|
||||
# A stale keyring is the one failure that is always safe to fix
|
||||
# automatically, and it blocks everything else until it is.
|
||||
cau_info "Refreshing keyrings and retrying"
|
||||
local -a keyrings=()
|
||||
pacman -Qq archlinux-keyring &> /dev/null && keyrings+=(archlinux-keyring)
|
||||
pacman -Qq cachyos-keyring &> /dev/null && keyrings+=(cachyos-keyring)
|
||||
if (( ${#keyrings[@]} )); then
|
||||
cau_run_logged pacman -Sy --noconfirm --color never "${keyrings[@]}" || true
|
||||
fi
|
||||
;;
|
||||
|
||||
conflict)
|
||||
# A package that has to replace another one. --noconfirm already
|
||||
# answers "Replace X with Y?" affirmatively; what it declines is
|
||||
# ":: X and Y are in conflict. Remove Y? [y/N]". --ask is pacman's
|
||||
# question bitmask: 4 = CONFLICT_PKG, 16 = REMOVE_PKGS.
|
||||
if [[ $CFG_RESOLVE_CONFLICTS != yes ]]; then
|
||||
cau_error "Package conflict requires a decision (AutoResolveConflicts is off)"
|
||||
break
|
||||
fi
|
||||
cau_info "Resolving package conflicts automatically and retrying"
|
||||
extra+=(--ask=20)
|
||||
;;
|
||||
|
||||
dependency)
|
||||
# Something installed still depends on a package the repos want
|
||||
# to drop or replace - almost always an AUR package that has not
|
||||
# caught up yet. Nothing here can fix that, and it is not worth
|
||||
# failing over: letting one stuck package block every other
|
||||
# update indefinitely is far worse on an unattended machine.
|
||||
# Hold the blockers back and upgrade everything else.
|
||||
mapfile -t blockers < <(_cau_pacman_blockers "$log")
|
||||
if (( ${#blockers[@]} == 0 )); then
|
||||
cau_error "Dependency problem with no package to hold back"
|
||||
break
|
||||
fi
|
||||
for b in "${blockers[@]}"; do
|
||||
extra+=(--ignore "$b")
|
||||
done
|
||||
CAU_PACMAN_HELD="${blockers[*]}"
|
||||
cau_warn "Holding back ${blockers[*]} and retrying without them"
|
||||
;;
|
||||
|
||||
filesystem)
|
||||
# Untracked files in the way. Forcing --overwrite here could
|
||||
# silently clobber something the user put there deliberately, so
|
||||
# this one stays a human decision.
|
||||
cau_error "Files on disk conflict with the update; manual review needed"
|
||||
break
|
||||
;;
|
||||
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
rm -f "$log"
|
||||
CAU_PACMAN_COUNT=0
|
||||
CAU_PACMAN_HELD=''
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -167,6 +449,8 @@ cau_pacman_pacnew_count() {
|
||||
cau_pacman_cleanup() {
|
||||
local -a orphans
|
||||
|
||||
cau_progress_step cleanup "Cleaning up after the update"
|
||||
|
||||
if [[ $CFG_REMOVE_ORPHANS == yes ]]; then
|
||||
mapfile -t orphans < <(pacman -Qtdq 2>/dev/null)
|
||||
if (( ${#orphans[@]} )); then
|
||||
@@ -177,7 +461,14 @@ cau_pacman_cleanup() {
|
||||
fi
|
||||
|
||||
if [[ $CFG_CLEAN_CACHE == yes ]] && cau_have paccache; then
|
||||
cau_info "Trimming the package cache"
|
||||
# paccache's dry run reports what a real run would reclaim; logging it
|
||||
# first is the only way to tell afterwards whether trimming is doing
|
||||
# anything, since the real run says little.
|
||||
local reclaim
|
||||
reclaim="$( { paccache -d --nocolor -k"$CFG_KEEP_OLD"; paccache -du --nocolor -k0; } 2>&1 \
|
||||
| grep -oE 'disk space saved: [^)]*' | paste -sd', ' -)"
|
||||
|
||||
cau_info "Trimming the package cache (keeping $CFG_KEEP_OLD old version(s))${reclaim:+ - $reclaim}"
|
||||
cau_run_logged paccache -r --nocolor -k"$CFG_KEEP_OLD" || cau_warn "paccache -r failed"
|
||||
cau_run_logged paccache -ru --nocolor -k0 || cau_warn "paccache -ru failed"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,502 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# The update's progress bar on the desktop.
|
||||
#
|
||||
# An unattended upgrade can take twenty minutes, and for most of that a user is
|
||||
# told only that "an update is running". This drives the desktop's job list -
|
||||
# the same widget that shows a bar while Dolphin copies files - so how far
|
||||
# along the run is stays visible the whole time.
|
||||
#
|
||||
# The desktop ends the progress entry as soon as the D-Bus connection that
|
||||
# asked for it goes away, which no one-shot bus client can survive. So an
|
||||
# actual process per session holds that connection open and takes instructions
|
||||
# on stdin; see cachy-auto-update-progress. Everything below is the writing end
|
||||
# of those pipes, plus the arithmetic that turns "package 120 of 260 in the
|
||||
# repository step" into one number for the bar.
|
||||
#
|
||||
# Absent anywhere along the way - no session, no Plasma, no Python bindings -
|
||||
# this does nothing at all and the update proceeds exactly as before.
|
||||
|
||||
CAU_PROGRESS_HELPER="${CAU_LIBEXECDIR}/cachy-auto-update-progress"
|
||||
|
||||
# One entry per session being driven; the indices line up across all four.
|
||||
CAU_PROGRESS_FDS=()
|
||||
CAU_PROGRESS_PIDS=()
|
||||
CAU_PROGRESS_FIFOS=()
|
||||
CAU_PROGRESS_LOCALES=()
|
||||
|
||||
# What each step is worth on the bar. Rough shares of a typical run rather than
|
||||
# anything measured: the repositories dominate - fetching them and unpacking
|
||||
# them about equally, on a domestic line - and the cleanup is a rounding error.
|
||||
# They do not have to add up to 100 - only the steps a given run will actually
|
||||
# perform are counted, and the total is normalised against those.
|
||||
#
|
||||
# "resolve" is everything pacman does before it has a transaction: syncing the
|
||||
# databases and working out what the upgrade actually consists of. It is
|
||||
# usually seconds, which is why it is worth so little - but on a large backlog
|
||||
# it is minutes, and those minutes used to be spent looking at a bar that had
|
||||
# not moved yet.
|
||||
declare -A CAU_PROGRESS_WEIGHTS=(
|
||||
[resolve]=10 [download]=30 [repo]=40 [aur]=15 [flatpak]=10 [appimage]=3
|
||||
[cleanup]=2
|
||||
)
|
||||
|
||||
CAU_PROGRESS_PLAN=()
|
||||
CAU_PROGRESS_SCALE=0
|
||||
CAU_PROGRESS_BASE=0
|
||||
CAU_PROGRESS_SPAN=0
|
||||
CAU_PROGRESS_TOTAL=0
|
||||
CAU_PROGRESS_SHOWN=-1
|
||||
|
||||
# _cau_progress_send <line>
|
||||
# The same instruction to every session. A session whose helper has exited is
|
||||
# dropped rather than written to: the runner writes into a pipe, and a pipe
|
||||
# nobody is draining fills up and would eventually block the update itself.
|
||||
_cau_progress_send() {
|
||||
local i fd
|
||||
|
||||
for i in "${!CAU_PROGRESS_FDS[@]}"; do
|
||||
fd="${CAU_PROGRESS_FDS[$i]}"
|
||||
[[ -n $fd ]] || continue
|
||||
|
||||
if ! kill -0 "${CAU_PROGRESS_PIDS[$i]}" 2>/dev/null; then
|
||||
CAU_PROGRESS_FDS[$i]=''
|
||||
continue
|
||||
fi
|
||||
|
||||
printf '%s\n' "$1" >&"$fd" 2>/dev/null || CAU_PROGRESS_FDS[$i]=''
|
||||
done
|
||||
}
|
||||
|
||||
# _cau_progress_line <format> [printf args...]
|
||||
# Assembled with printf -v rather than in a command substitution: this is on
|
||||
# the per-package path of a large upgrade, and a fork per line is a fork too
|
||||
# many for something whose entire job is to be unobtrusive.
|
||||
_cau_progress_line() {
|
||||
local line
|
||||
# shellcheck disable=SC2059 # the format is ours; the arguments are numbers
|
||||
printf -v line "$@"
|
||||
_cau_progress_send "$line"
|
||||
}
|
||||
|
||||
# cau_progress_begin <step-id...>
|
||||
# Opens the progress entry in every graphical session, and records which steps
|
||||
# this run is going to perform so the bar can be scaled to them.
|
||||
cau_progress_begin() {
|
||||
local user uid fifo pid
|
||||
local fd=''
|
||||
|
||||
CAU_PROGRESS_PLAN=("$@")
|
||||
CAU_PROGRESS_SCALE=0
|
||||
local step
|
||||
for step in "${CAU_PROGRESS_PLAN[@]}"; do
|
||||
CAU_PROGRESS_SCALE=$(( CAU_PROGRESS_SCALE + ${CAU_PROGRESS_WEIGHTS[$step]:-0} ))
|
||||
done
|
||||
(( CAU_PROGRESS_SCALE > 0 )) || return 0
|
||||
|
||||
[[ $CFG_NOTIFICATIONS == yes ]] || return 0
|
||||
[[ -x $CAU_PROGRESS_HELPER ]] || return 0
|
||||
mkdir -p "$CAU_RUNDIR" 2>/dev/null || return 0
|
||||
|
||||
while read -r user uid; do
|
||||
[[ -n $user ]] || continue
|
||||
|
||||
# The helper speaks D-Bus through GLib's Python bindings. Checked here
|
||||
# rather than left to fail inside the helper, because a helper that
|
||||
# gave up immediately would leave nobody draining the pipe.
|
||||
cau_as_user "$user" "$uid" sh -c \
|
||||
'command -v python3 >/dev/null 2>&1 && python3 -c "import gi" 2>/dev/null' \
|
||||
|| continue
|
||||
|
||||
fifo="${CAU_RUNDIR}/progress.${uid}"
|
||||
rm -f "$fifo" 2>/dev/null
|
||||
mkfifo -m 0600 "$fifo" 2>/dev/null || continue
|
||||
chown "$uid" "$fifo" 2>/dev/null || true
|
||||
|
||||
cau_as_user "$user" "$uid" "$CAU_PROGRESS_HELPER" < "$fifo" > /dev/null 2>&1 &
|
||||
pid=$!
|
||||
|
||||
# Read-write deliberately. Opening the writing end of a fifo blocks
|
||||
# until a reader shows up, so if the helper died on the way in, the
|
||||
# update would hang here for good. O_RDWR never blocks, and the helper
|
||||
# still sees end-of-file once this descriptor is closed.
|
||||
if ! exec {fd}<> "$fifo"; then
|
||||
kill "$pid" 2>/dev/null
|
||||
rm -f "$fifo" 2>/dev/null
|
||||
continue
|
||||
fi
|
||||
|
||||
CAU_PROGRESS_FDS+=("$fd")
|
||||
CAU_PROGRESS_PIDS+=("$pid")
|
||||
CAU_PROGRESS_FIFOS+=("$fifo")
|
||||
CAU_PROGRESS_LOCALES+=("$(cau_user_locale "$user" "$uid")")
|
||||
done < <(cau_active_session_users)
|
||||
}
|
||||
|
||||
# cau_progress_active
|
||||
# Whether anybody is listening. For callers that would otherwise do work whose
|
||||
# only purpose is to feed the bar.
|
||||
cau_progress_active() {
|
||||
(( ${#CAU_PROGRESS_FDS[@]} ))
|
||||
}
|
||||
|
||||
# cau_progress_drop <step-id...>
|
||||
# Takes steps out of the plan and rescales the bar to what is left.
|
||||
#
|
||||
# Which steps a run will perform is only half known up front. The other half
|
||||
# turns up while it runs: nothing to download because every package was already
|
||||
# in the cache, no AUR updates pending, no Flatpaks installed. A step like that
|
||||
# keeps its whole share of the bar and then hands it over in a single jump the
|
||||
# moment the next one starts - which is precisely the stutter this is here to
|
||||
# remove. Dropping it hands its share to the steps that do have work instead,
|
||||
# so the bar advances at a steady pace rather than leaping across the gaps.
|
||||
#
|
||||
# It is also what lets the weights above stay rough: they never have to be
|
||||
# right about a step that does not run, only about the ones that do.
|
||||
#
|
||||
# Only ever called for a step that has not started, so nothing already behind
|
||||
# the bar is rescaled and the bar does not travel backwards.
|
||||
cau_progress_drop() {
|
||||
local drop step
|
||||
local -a kept=()
|
||||
|
||||
(( ${#CAU_PROGRESS_FDS[@]} )) || return 0
|
||||
|
||||
for step in "${CAU_PROGRESS_PLAN[@]}"; do
|
||||
for drop in "$@"; do
|
||||
[[ $step == "$drop" ]] && continue 2
|
||||
done
|
||||
kept+=("$step")
|
||||
done
|
||||
|
||||
(( ${#kept[@]} == ${#CAU_PROGRESS_PLAN[@]} )) && return 0
|
||||
|
||||
CAU_PROGRESS_PLAN=("${kept[@]}")
|
||||
CAU_PROGRESS_SCALE=0
|
||||
for step in "${CAU_PROGRESS_PLAN[@]}"; do
|
||||
CAU_PROGRESS_SCALE=$(( CAU_PROGRESS_SCALE + ${CAU_PROGRESS_WEIGHTS[$step]:-0} ))
|
||||
done
|
||||
|
||||
# Nothing left to weigh against would divide by zero further down. Cannot
|
||||
# happen while cleanup is unconditional, but this is cheaper than relying
|
||||
# on that staying true.
|
||||
(( CAU_PROGRESS_SCALE > 0 )) || CAU_PROGRESS_SCALE=1
|
||||
}
|
||||
|
||||
# cau_progress_step <step-id> <label-msgid> [item-count]
|
||||
# Moves on to the next step. The bar jumps to where that step begins, so a step
|
||||
# that reported fewer items than it promised still completes rather than
|
||||
# leaving a gap.
|
||||
cau_progress_step() {
|
||||
local id="$1" label="$2" total="${3:-0}"
|
||||
local step i fd base=0
|
||||
|
||||
(( ${#CAU_PROGRESS_FDS[@]} )) || return 0
|
||||
|
||||
local found=0
|
||||
for step in "${CAU_PROGRESS_PLAN[@]}"; do
|
||||
[[ $step == "$id" ]] && { found=1; break; }
|
||||
base=$(( base + ${CAU_PROGRESS_WEIGHTS[$step]:-0} ))
|
||||
done
|
||||
|
||||
# A step that was dropped for having no work is not a step to move to.
|
||||
# Without this the loop above would fall off the end of the plan and hand
|
||||
# back the sum of every weight, i.e. send the bar straight to 100%.
|
||||
(( found )) || return 0
|
||||
|
||||
CAU_PROGRESS_BASE=$base
|
||||
CAU_PROGRESS_SPAN=${CAU_PROGRESS_WEIGHTS[$id]:-0}
|
||||
CAU_PROGRESS_TOTAL=$total
|
||||
|
||||
# The label is the one line a user actually reads, so it is rendered in
|
||||
# each session's own locale rather than the run's C locale.
|
||||
for i in "${!CAU_PROGRESS_FDS[@]}"; do
|
||||
fd="${CAU_PROGRESS_FDS[$i]}"
|
||||
[[ -n $fd ]] || continue
|
||||
cau_msg_into "${CAU_PROGRESS_LOCALES[$i]}" "$label"
|
||||
printf 'info\t%s\n' "$CAU_MSG_RESULT" >&"$fd" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Unconditionally, including the zero case: a step with no item count of
|
||||
# its own would otherwise keep displaying the previous step's tally, and
|
||||
# "260 of 260 items" under the heading "Flatpaks" is worse than no count.
|
||||
_cau_progress_line 'total\t%s' "$total"
|
||||
_cau_progress_line 'done\t%s' 0
|
||||
|
||||
cau_progress_item 0
|
||||
}
|
||||
|
||||
# _cau_progress_pct <numerator> <denominator>
|
||||
# How far through the current step we are, as a share of its span, turned into
|
||||
# one number for the whole run and sent on if it has moved.
|
||||
_cau_progress_pct() {
|
||||
local num="$1" den="$2" pct scaled
|
||||
|
||||
if (( den > 0 )); then
|
||||
scaled=$(( CAU_PROGRESS_BASE * 100 + CAU_PROGRESS_SPAN * 100 * num / den ))
|
||||
else
|
||||
scaled=$(( CAU_PROGRESS_BASE * 100 ))
|
||||
fi
|
||||
|
||||
pct=$(( scaled / CAU_PROGRESS_SCALE ))
|
||||
(( pct > 100 )) && pct=100
|
||||
|
||||
# Never backwards. Two honest things can ask for that: dropping a step
|
||||
# rescales the run against a smaller total, and the conflict-recovery loop
|
||||
# restarts pacman - and with it the item tally - from the top. Both are
|
||||
# real, neither is a reason to show somebody a bar that retreats.
|
||||
(( pct < CAU_PROGRESS_SHOWN )) && pct=$CAU_PROGRESS_SHOWN
|
||||
|
||||
# Only when the whole number changes. Percent is the one field the runner
|
||||
# would otherwise rewrite for every package on a 500-package upgrade.
|
||||
(( pct == CAU_PROGRESS_SHOWN )) && return 0
|
||||
CAU_PROGRESS_SHOWN=$pct
|
||||
_cau_progress_line 'percent\t%s' "$pct"
|
||||
}
|
||||
|
||||
# cau_progress_item <processed> [total]
|
||||
# How far through the current step we are.
|
||||
cau_progress_item() {
|
||||
local processed="$1" total="${2:-$CAU_PROGRESS_TOTAL}"
|
||||
|
||||
(( ${#CAU_PROGRESS_FDS[@]} )) || return 0
|
||||
[[ $processed =~ ^[0-9]+$ ]] || return 0
|
||||
|
||||
if [[ $total =~ ^[0-9]+$ ]] && (( total > 0 )); then
|
||||
(( processed > total )) && processed=$total
|
||||
if (( total != CAU_PROGRESS_TOTAL )); then
|
||||
CAU_PROGRESS_TOTAL=$total
|
||||
_cau_progress_line 'total\t%s' "$total"
|
||||
fi
|
||||
_cau_progress_line 'done\t%s' "$processed"
|
||||
_cau_progress_pct "$processed" "$total"
|
||||
else
|
||||
_cau_progress_pct 0 0
|
||||
fi
|
||||
}
|
||||
|
||||
# cau_progress_creep <seconds-elapsed>
|
||||
# Moves the bar through a step whose length cannot be known in advance.
|
||||
#
|
||||
# Some of a run has no counter to offer and never will. pacman prints nothing
|
||||
# whatsoever between "starting full system upgrade" and the transaction it
|
||||
# eventually prepares; an AUR helper compiling a package prints plenty, none of
|
||||
# it countable. On a large backlog either is minutes. There is no honest number
|
||||
# to show for that - but a bar that has not moved since it appeared is read as
|
||||
# a hang, and somebody who reads it that way reaches for the power button in
|
||||
# the middle of an update. That is the failure this is here to prevent.
|
||||
#
|
||||
# So it creeps, along a curve that approaches the end of the step without ever
|
||||
# reaching it: half the step's share after HALFLIFE seconds, three quarters
|
||||
# after three times that, the whole of it never. Nothing is claimed that is not
|
||||
# known - the item counter stays empty throughout, which is the field that
|
||||
# would be lying if it moved - and the step still finishes the instant real
|
||||
# work reports in, because every real report is further along than the creep.
|
||||
#
|
||||
# Confined to the step's own span, so a creep can never overtake the step that
|
||||
# comes after it however long it is left running.
|
||||
CAU_PROGRESS_CREEP_HALFLIFE=45
|
||||
|
||||
cau_progress_creep() {
|
||||
local elapsed="$1"
|
||||
|
||||
(( ${#CAU_PROGRESS_FDS[@]} )) || return 0
|
||||
[[ $elapsed =~ ^[0-9]+$ ]] || return 0
|
||||
|
||||
_cau_progress_pct "$elapsed" $(( elapsed + CAU_PROGRESS_CREEP_HALFLIFE ))
|
||||
}
|
||||
|
||||
# cau_progress_creep_start / cau_progress_creep_stop
|
||||
# The same, for a step that blocks in one long call instead of polling: the
|
||||
# ticker runs alongside it and is stopped when it returns. Only one at a time,
|
||||
# and starting a second one replaces the first.
|
||||
CAU_PROGRESS_CREEP_PID=0
|
||||
CAU_PROGRESS_CREEP_T0=0
|
||||
|
||||
cau_progress_creep_start() {
|
||||
cau_progress_creep_stop
|
||||
(( ${#CAU_PROGRESS_FDS[@]} )) || return 0
|
||||
|
||||
CAU_PROGRESS_CREEP_T0=$SECONDS
|
||||
local t0=$SECONDS
|
||||
{
|
||||
# Waiting without forking a sleep every two seconds, for the same
|
||||
# reason cau_progress_begin opens its fifo read-write: a pipe held open
|
||||
# at both ends never reports end-of-file, so a timed read on it blocks
|
||||
# for exactly the timeout and nothing else. A forked sleep would also
|
||||
# survive the kill below - it is a child of this subshell, not this
|
||||
# subshell - and inherit the fifo's write end, which would keep the
|
||||
# helper from seeing the end of its input until the sleep ran out.
|
||||
local nap
|
||||
exec {nap}<> <(:)
|
||||
while :; do
|
||||
read -r -t 2 -u "$nap" _ || true
|
||||
cau_progress_creep $(( SECONDS - t0 ))
|
||||
done
|
||||
} &
|
||||
CAU_PROGRESS_CREEP_PID=$!
|
||||
}
|
||||
|
||||
cau_progress_creep_stop() {
|
||||
(( CAU_PROGRESS_CREEP_PID )) || return 0
|
||||
kill "$CAU_PROGRESS_CREEP_PID" 2>/dev/null
|
||||
wait "$CAU_PROGRESS_CREEP_PID" 2>/dev/null
|
||||
CAU_PROGRESS_CREEP_PID=0
|
||||
|
||||
# The ticker moved the bar from inside a subshell, so this side never saw
|
||||
# it happen and still believes the bar is where it was left. Catching up
|
||||
# costs one recomputation - the curve is a function of elapsed time and
|
||||
# nothing else - and without it the next ordinary report from here would be
|
||||
# measured against a stale percentage and send the bar backwards.
|
||||
cau_progress_creep $(( SECONDS - CAU_PROGRESS_CREEP_T0 ))
|
||||
}
|
||||
|
||||
# cau_progress_detail <label-msgid> <value>
|
||||
# A labelled line under the entry's "Details" - which package is being unpacked
|
||||
# right now, say.
|
||||
cau_progress_detail() {
|
||||
local label="$1" value="$2" i fd
|
||||
|
||||
(( ${#CAU_PROGRESS_FDS[@]} )) || return 0
|
||||
|
||||
for i in "${!CAU_PROGRESS_FDS[@]}"; do
|
||||
fd="${CAU_PROGRESS_FDS[$i]}"
|
||||
[[ -n $fd ]] || continue
|
||||
cau_msg_into "${CAU_PROGRESS_LOCALES[$i]}" "$label"
|
||||
printf 'detail\t%s\t%s\n' "$CAU_MSG_RESULT" "$value" >&"$fd" 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
|
||||
# How many lines of the run log the job entry carries, and how wide each one
|
||||
# is allowed to be. Both are display limits rather than arbitrary ones: five
|
||||
# lines is about what fits under a notification popup before it starts pushing
|
||||
# the buttons off the bottom, and a line long enough to be elided anyway is
|
||||
# only costing room in the pipe. Together they also keep one instruction well
|
||||
# inside PIPE_BUF, which is what makes the write atomic against the runner
|
||||
# writing its own progress down the same pipe.
|
||||
CAU_PROGRESS_TAIL_LINES=5
|
||||
CAU_PROGRESS_TAIL_COLS=120
|
||||
CAU_PROGRESS_TAIL_PID=0
|
||||
|
||||
# cau_progress_log <label-msgid> <text>
|
||||
# The second description field, holding the last few lines of the run log.
|
||||
#
|
||||
# The protocol is one instruction per line, so the tail travels tab separated
|
||||
# and is put back together on the other side. Tabs inside a log line would
|
||||
# split it in two on the way, so they become spaces first - the job view
|
||||
# renders either as whitespace, and a line broken in half renders as nonsense.
|
||||
cau_progress_log() {
|
||||
local label="$1" text="$2" i fd
|
||||
|
||||
(( ${#CAU_PROGRESS_FDS[@]} )) || return 0
|
||||
|
||||
text="${text//$'\t'/ }"
|
||||
text="${text//$'\n'/$'\t'}"
|
||||
|
||||
for i in "${!CAU_PROGRESS_FDS[@]}"; do
|
||||
fd="${CAU_PROGRESS_FDS[$i]}"
|
||||
[[ -n $fd ]] || continue
|
||||
cau_msg_into "${CAU_PROGRESS_LOCALES[$i]}" "$label"
|
||||
printf 'log\t%s\t%s\n' "$CAU_MSG_RESULT" "$text" >&"$fd" 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
|
||||
# cau_progress_tail_start <logfile> / cau_progress_tail_stop
|
||||
# Follows the run log for as long as the update lasts, so expanding "Details"
|
||||
# shows what the update is actually doing right now.
|
||||
#
|
||||
# This is the answer to the part of a run that has no counter and never will:
|
||||
# an AUR helper compiling for a quarter of an hour says plenty about what it is
|
||||
# up to, none of it countable, and all of it going into a log file nobody is
|
||||
# looking at. The percentage says the update is alive; these lines say what it
|
||||
# is alive doing.
|
||||
#
|
||||
# Polled rather than followed with tail -F, for the same reason the pacman
|
||||
# watcher polls: only the last few lines are ever displayed, so every line in
|
||||
# between is work nobody would see. Re-read whole and compared, which also
|
||||
# makes truncation and rotation of the log a non-event.
|
||||
cau_progress_tail_start() {
|
||||
local file="$1"
|
||||
|
||||
cau_progress_tail_stop
|
||||
(( ${#CAU_PROGRESS_FDS[@]} )) || return 0
|
||||
cau_have tail || return 0
|
||||
|
||||
{
|
||||
local nap last='' now
|
||||
exec {nap}<> <(:)
|
||||
while :; do
|
||||
read -r -t 2 -u "$nap" _ || true
|
||||
now="$(tail -n "$CAU_PROGRESS_TAIL_LINES" "$file" 2>/dev/null \
|
||||
| cut -c "1-${CAU_PROGRESS_TAIL_COLS}")"
|
||||
[[ -n $now && $now != "$last" ]] || continue
|
||||
last="$now"
|
||||
cau_progress_log "Log" "$now"
|
||||
done
|
||||
} &
|
||||
CAU_PROGRESS_TAIL_PID=$!
|
||||
}
|
||||
|
||||
cau_progress_tail_stop() {
|
||||
(( CAU_PROGRESS_TAIL_PID )) || return 0
|
||||
kill "$CAU_PROGRESS_TAIL_PID" 2>/dev/null
|
||||
wait "$CAU_PROGRESS_TAIL_PID" 2>/dev/null
|
||||
CAU_PROGRESS_TAIL_PID=0
|
||||
}
|
||||
|
||||
# cau_progress_end [outcome: ok|failed] [failure-msgid]
|
||||
# Closes the entry. Must run on every exit path, including a killed run: an
|
||||
# entry whose owner merely vanishes is reported by the desktop as "the
|
||||
# application closed unexpectedly", which would end every update with a failure
|
||||
# notice. Safe to call twice, and safe to call when nothing was ever opened.
|
||||
#
|
||||
# ok the bar fills and the entry goes away
|
||||
# failed the entry goes away from wherever the bar had got to
|
||||
# failed <msgid> and the desktop labels it as failed, with that text
|
||||
#
|
||||
# The distinction between the last two is which message the user ends up with.
|
||||
# An ordinary failure already sends a notification that stays until dismissed,
|
||||
# and two messages about one problem is one too many; a run that was killed
|
||||
# sends nothing at all, so there the label is the only thing that explains why
|
||||
# a bar that was at 40% is suddenly gone.
|
||||
cau_progress_end() {
|
||||
local outcome="${1:-ok}" msgid="${2:-}"
|
||||
local i fd
|
||||
|
||||
# Before the descriptors go: anything still running in the background holds
|
||||
# its own copy of them, so the helper would not see the end of its input
|
||||
# until it exited - and it is about to be waited on.
|
||||
cau_progress_creep_stop
|
||||
cau_progress_tail_stop
|
||||
|
||||
# The last step never consumes its own share - nothing reports items for
|
||||
# the cleanup - so the bar would stop a few percent short of the end and
|
||||
# vanish there. Only on the way out of a run that actually worked, though:
|
||||
# filling the bar for a failed update says the opposite of what happened.
|
||||
[[ $outcome == ok ]] && _cau_progress_line 'percent\t100'
|
||||
|
||||
for i in "${!CAU_PROGRESS_FDS[@]}"; do
|
||||
fd="${CAU_PROGRESS_FDS[$i]}"
|
||||
[[ -n $fd ]] || continue
|
||||
|
||||
CAU_MSG_RESULT=''
|
||||
[[ -n $msgid ]] && cau_msg_into "${CAU_PROGRESS_LOCALES[$i]}" "$msgid"
|
||||
|
||||
printf 'end\t%s\n' "$CAU_MSG_RESULT" >&"$fd" 2>/dev/null || true
|
||||
exec {fd}>&-
|
||||
done
|
||||
|
||||
for i in "${!CAU_PROGRESS_PIDS[@]}"; do
|
||||
wait "${CAU_PROGRESS_PIDS[$i]}" 2>/dev/null
|
||||
done
|
||||
|
||||
for i in "${!CAU_PROGRESS_FIFOS[@]}"; do
|
||||
rm -f "${CAU_PROGRESS_FIFOS[$i]}" 2>/dev/null
|
||||
done
|
||||
|
||||
CAU_PROGRESS_FDS=()
|
||||
CAU_PROGRESS_PIDS=()
|
||||
CAU_PROGRESS_FIFOS=()
|
||||
CAU_PROGRESS_LOCALES=()
|
||||
CAU_PROGRESS_SHOWN=-1
|
||||
}
|
||||
Reference in new issue
Block a user