Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
162b30ba38 | ||
|
|
c8666da714 | ||
|
|
f9cd8a0ace | ||
|
|
8834abe648 | ||
|
|
b796b2711a | ||
|
|
6538c1510d | ||
|
|
ea3f0d21fa | ||
|
|
6aa9b147b8 | ||
|
|
a17aed4b3d | ||
|
|
2fa830284e | ||
|
|
c308b7e286 | ||
|
|
2eff62f9fd |
No files matched your search
@@ -8,7 +8,7 @@
|
|||||||
# Overridable so a packager can pass the version it is actually building
|
# Overridable so a packager can pass the version it is actually building
|
||||||
# (`make VERSION=$pkgver`). The literal below is the fallback for builds
|
# (`make VERSION=$pkgver`). The literal below is the fallback for builds
|
||||||
# straight from a checkout, and is what a release tag has to carry.
|
# straight from a checkout, and is what a release tag has to carry.
|
||||||
VERSION ?= 1.0.8
|
VERSION ?= 1.3.0
|
||||||
|
|
||||||
PREFIX ?= /usr
|
PREFIX ?= /usr
|
||||||
DESTDIR ?=
|
DESTDIR ?=
|
||||||
@@ -64,6 +64,13 @@ check:
|
|||||||
else \
|
else \
|
||||||
echo "shellcheck not found - skipped"; \
|
echo "shellcheck not found - skipped"; \
|
||||||
fi
|
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 \
|
@if command -v visudo >/dev/null 2>&1; then \
|
||||||
visudo -cf res/sudoers/cachy-auto-update >/dev/null && echo "ok sudoers"; \
|
visudo -cf res/sudoers/cachy-auto-update >/dev/null && echo "ok sudoers"; \
|
||||||
fi
|
fi
|
||||||
@@ -72,6 +79,9 @@ install: build
|
|||||||
# executables
|
# executables
|
||||||
install -Dm755 src/cachy-auto-update "$(DESTDIR)$(BINDIR)/cachy-auto-update"
|
install -Dm755 src/cachy-auto-update "$(DESTDIR)$(BINDIR)/cachy-auto-update"
|
||||||
install -Dm755 src/cachy-auto-update-run "$(DESTDIR)$(LIBEXECDIR)/cachy-auto-update-run"
|
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
|
# the version and the resolved lib path are baked in at install time
|
||||||
sed -i -e 's|@VERSION@|$(VERSION)|g' \
|
sed -i -e 's|@VERSION@|$(VERSION)|g' \
|
||||||
-e 's|@LIBDIR@|$(LIBDIR)|g' \
|
-e 's|@LIBDIR@|$(LIBDIR)|g' \
|
||||||
@@ -114,6 +124,10 @@ install: build
|
|||||||
install -Dm644 res/autostart/cachy-auto-update-notify.desktop \
|
install -Dm644 res/autostart/cachy-auto-update-notify.desktop \
|
||||||
"$(DESTDIR)$(XDGAUTOSTART)/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
|
# translations
|
||||||
@for l in $(LINGUAS); do \
|
@for l in $(LINGUAS); do \
|
||||||
if [ -f "po/$$l.mo" ]; then \
|
if [ -f "po/$$l.mo" ]; then \
|
||||||
@@ -138,6 +152,7 @@ uninstall:
|
|||||||
rm -f "$(DESTDIR)$(SYSTEMDDIR)/cachy-auto-update.service"
|
rm -f "$(DESTDIR)$(SYSTEMDDIR)/cachy-auto-update.service"
|
||||||
rm -f "$(DESTDIR)$(SYSTEMDDIR)/cachy-auto-update.timer"
|
rm -f "$(DESTDIR)$(SYSTEMDDIR)/cachy-auto-update.timer"
|
||||||
rm -f "$(DESTDIR)$(XDGAUTOSTART)/cachy-auto-update-notify.desktop"
|
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"
|
rm -f "$(DESTDIR)$(MANDIR)/man1/cachy-auto-update.1"
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
|
|||||||
@@ -30,9 +30,14 @@ That opens a menu with the two switches there are:
|
|||||||
[3] Update now
|
[3] Update now
|
||||||
[4] Show log
|
[4] Show log
|
||||||
[5] Show current conditions
|
[5] Show current conditions
|
||||||
|
[6] Settings
|
||||||
[q] Quit
|
[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
|
The interface is fully translated; on a German system everything above appears
|
||||||
in German.
|
in German.
|
||||||
|
|
||||||
@@ -69,7 +74,7 @@ deliberately.
|
|||||||
|
|
||||||
A run is postponed — and retried an hour later — when:
|
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),
|
are never affected),
|
||||||
- a game is running: GameMode, a known game process, or anything holding a
|
- a game is running: GameMode, a known game process, or anything holding a
|
||||||
blocking idle inhibitor,
|
blocking idle inhibitor,
|
||||||
@@ -78,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
|
`cachy-auto-update status` prints every one of these individually, which is the
|
||||||
fastest way to find out why nothing is happening.
|
fastest way to find out why nothing is happening.
|
||||||
|
|
||||||
The machine is **never** restarted on its own. When a kernel update needs a
|
The machine is **never** restarted on its own. A kernel update that needs a
|
||||||
restart, you get a notification saying so.
|
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
|
## About the password question
|
||||||
|
|
||||||
@@ -127,10 +135,29 @@ unheld on several consecutive runs, you get a notification instead.
|
|||||||
|
|
||||||
Three layers, in order of how much they can actually promise:
|
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
|
**Suspend and a normal shutdown are blocked.** The run holds a
|
||||||
`systemd-inhibit --what=sleep:shutdown --mode=block` lock, so closing the lid,
|
`systemd-inhibit --what=sleep:shutdown --mode=block` lock, so closing the lid or
|
||||||
picking "Shut down" from the menu or a short press of the power button will not
|
picking "Shut down" cannot interrupt a transaction. Be aware of what that looks
|
||||||
interrupt a transaction — the desktop says something is still busy instead.
|
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
|
**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
|
or pulling the plug cuts power in firmware. What limits the damage is that
|
||||||
@@ -153,6 +180,79 @@ On a Btrfs system with `snapper` and `snap-pac` — the CachyOS default — ever
|
|||||||
pacman transaction is bracketed by a pre and post snapshot, so a genuinely
|
pacman transaction is bracketed by a pre and post snapshot, so a genuinely
|
||||||
broken upgrade can still be rolled back with `snapper rollback`.
|
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
|
## Configuration
|
||||||
|
|
||||||
`/etc/cachy-auto-update/cachy-auto-update.conf`, one `Key=Value` per line, every
|
`/etc/cachy-auto-update/cachy-auto-update.conf`, one `Key=Value` per line, every
|
||||||
@@ -186,7 +286,7 @@ Two things are deliberately *not* automated:
|
|||||||
|
|
||||||
- **File conflicts** (`exists in filesystem`) — forcing `--overwrite` could
|
- **File conflicts** (`exists in filesystem`) — forcing `--overwrite` could
|
||||||
silently destroy something that was put there on purpose.
|
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
|
Signature failures trigger one keyring refresh and one retry, since a stale
|
||||||
keyring blocks everything else until it is fixed.
|
keyring blocks everything else until it is fixed.
|
||||||
@@ -209,8 +309,13 @@ sudo systemd-sysusers && sudo systemd-tmpfiles --create
|
|||||||
sudo cachy-auto-update enable
|
sudo cachy-auto-update enable
|
||||||
```
|
```
|
||||||
|
|
||||||
`make check` runs `bash -n` over everything, `shellcheck` when available, and
|
`make check` runs `bash -n` over every shell file, `py_compile` over the
|
||||||
validates the sudoers drop-in with `visudo -c`.
|
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
|
## Relationship to cachy-update
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
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
|
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;
|
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).
|
whether a tick does anything is decided by _UpdateInterval_ (daily by default).
|
||||||
@@ -97,15 +100,97 @@ inside each user's own account. AppImages are updated through Gear Lever, for
|
|||||||
users with a graphical session, and AppImages whose application is currently
|
users with a graphical session, and AppImages whose application is currently
|
||||||
running are skipped.
|
running are skipped.
|
||||||
|
|
||||||
The machine is never restarted automatically. When a kernel update makes a
|
The machine is never restarted automatically. A kernel update that makes a
|
||||||
restart necessary, a notification says so.
|
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
|
# 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
|
While a transaction is running, *cachy-auto-update* holds a
|
||||||
*systemd-inhibit*(1) lock on _sleep_ and _shutdown_ in blocking mode, so 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.
|
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
|
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
|
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
|
it is older than the current boot, since no process able to hold it can still
|
||||||
|
|||||||
+119
-7
@@ -168,12 +168,23 @@ msgstr ""
|
|||||||
msgid "base-devel is missing - AUR packages cannot be built without it."
|
msgid "base-devel is missing - AUR packages cannot be built without it."
|
||||||
msgstr ""
|
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 ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "cachy-update's update check has been disabled."
|
msgid "cachy-update's update check has been disabled."
|
||||||
msgstr ""
|
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."
|
msgid "No log yet."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -218,6 +229,44 @@ msgstr ""
|
|||||||
msgid "Without a command an interactive menu is shown."
|
msgid "Without a command an interactive menu is shown."
|
||||||
msgstr ""
|
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
|
#. Notifications
|
||||||
msgid "System updated"
|
msgid "System updated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -232,12 +281,6 @@ msgstr ""
|
|||||||
msgid "Something went wrong while updating. Run 'cachy-auto-update log' for details."
|
msgid "Something went wrong while updating. Run 'cachy-auto-update log' for details."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Restart recommended"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "A new kernel was installed. Please restart when it suits you."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Package database locked"
|
msgid "Package database locked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -263,3 +306,72 @@ msgstr ""
|
|||||||
|
|
||||||
msgid "The last update was cut short, most likely because the machine was switched off. It is being finished now."
|
msgid "The last update was cut short, most likely because the machine was switched off. It is being finished now."
|
||||||
msgstr ""
|
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 ""
|
||||||
@@ -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."
|
msgid "base-devel is missing - AUR packages cannot be built without it."
|
||||||
msgstr "base-devel fehlt – ohne das lassen sich AUR-Pakete nicht bauen."
|
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]"
|
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]"
|
msgstr "cachy-update benachrichtigt ebenfalls über verfügbare Updates. Dessen Benachrichtigungen abschalten? [J/n]"
|
||||||
|
|
||||||
msgid "cachy-update's update check has been disabled."
|
msgid "cachy-update's update check has been disabled."
|
||||||
msgstr "Die Update-Prüfung von cachy-update wurde abgeschaltet."
|
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."
|
msgid "No log yet."
|
||||||
msgstr "Noch kein Protokoll vorhanden."
|
msgstr "Noch kein Protokoll vorhanden."
|
||||||
|
|
||||||
@@ -219,6 +230,44 @@ msgstr "Version anzeigen"
|
|||||||
msgid "Without a command an interactive menu is shown."
|
msgid "Without a command an interactive menu is shown."
|
||||||
msgstr "Ohne Befehl wird ein interaktives Menü angezeigt."
|
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
|
#. Notifications
|
||||||
msgid "System updated"
|
msgid "System updated"
|
||||||
msgstr "System aktualisiert"
|
msgstr "System aktualisiert"
|
||||||
@@ -233,12 +282,6 @@ msgstr "Update fehlgeschlagen"
|
|||||||
msgid "Something went wrong while updating. Run 'cachy-auto-update log' for details."
|
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“."
|
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"
|
msgid "Package database locked"
|
||||||
msgstr "Paketdatenbank gesperrt"
|
msgstr "Paketdatenbank gesperrt"
|
||||||
|
|
||||||
@@ -264,3 +307,73 @@ 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."
|
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."
|
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
|
# Never update while the battery is below this percentage. Ignored on mains
|
||||||
# power and on machines without a battery.
|
# power and on machines without a battery.
|
||||||
MinBatteryPercent=40
|
MinBatteryPercent=30
|
||||||
|
|
||||||
# Update only while plugged in. Stricter than MinBatteryPercent.
|
# Update only while plugged in. Stricter than MinBatteryPercent.
|
||||||
RequireAC=no
|
RequireAC=no
|
||||||
@@ -94,6 +94,22 @@ RemoveOrphans=no
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Notification detail
|
# 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
|
# Say something after a successful update. Nothing is ever shown when there
|
||||||
# was nothing to do.
|
# was nothing to do.
|
||||||
@@ -102,6 +118,9 @@ NotifyOnSuccess=yes
|
|||||||
# Report failures.
|
# Report failures.
|
||||||
NotifyOnError=yes
|
NotifyOnError=yes
|
||||||
|
|
||||||
# Point out that a kernel update needs a restart. The machine is never
|
# A kernel update that needs a restart is not announced by a notification. The
|
||||||
# restarted automatically.
|
# running kernel loses its module tree as soon as pacman unpacks the new one,
|
||||||
NotifyReboot=yes
|
# 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.
|
||||||
+52
-2
@@ -84,6 +84,48 @@ cau_do_enable() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
cau_offer_disable_cachy_update
|
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.
|
# 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
|
(( found )) || return 0
|
||||||
|
|
||||||
printf '\n %s\n ' \
|
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
|
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
|
while read -r user uid; do
|
||||||
[[ -n $user ]] || continue
|
[[ -n $user ]] || continue
|
||||||
|
|||||||
@@ -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@}"
|
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
|
pkg_pacman pkg_aur pkg_flatpak pkg_appimage; do
|
||||||
# shellcheck source=/dev/null
|
# shellcheck source=/dev/null
|
||||||
if ! source "$CAU_LIBDIR/$_mod.sh"; then
|
if ! source "$CAU_LIBDIR/$_mod.sh"; then
|
||||||
@@ -71,6 +71,10 @@ cau_log_open
|
|||||||
# all landed or none did; only our own bookkeeping is at risk here.
|
# all landed or none did; only our own bookkeeping is at risk here.
|
||||||
_cau_interrupted() {
|
_cau_interrupted() {
|
||||||
cau_error "Update run interrupted ($1)"
|
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
|
cau_state_write last_result interrupted
|
||||||
exit 130
|
exit 130
|
||||||
}
|
}
|
||||||
@@ -121,7 +125,7 @@ fi
|
|||||||
# before the busy check, because otherwise every future run would defer on it
|
# before the busy check, because otherwise every future run would defer on it
|
||||||
# forever and the machine would quietly stop updating.
|
# forever and the machine would quietly stop updating.
|
||||||
if cau_recover_stale_lock; then
|
if cau_recover_stale_lock; then
|
||||||
cau_notify normal \
|
cau_notify normal no \
|
||||||
"Finishing an interrupted update" \
|
"Finishing an interrupted update" \
|
||||||
"The last update was cut short, most likely because the machine was switched off. It is being finished now."
|
"The last update was cut short, most likely because the machine was switched off. It is being finished now."
|
||||||
fi
|
fi
|
||||||
@@ -132,7 +136,7 @@ CAU_SKIP_REASON=''
|
|||||||
if cau_package_manager_busy; then
|
if cau_package_manager_busy; then
|
||||||
if cau_track_stale_lock; then
|
if cau_track_stale_lock; then
|
||||||
cau_warn "pacman's database lock appears to be stale"
|
cau_warn "pacman's database lock appears to be stale"
|
||||||
cau_notify normal \
|
cau_notify normal yes \
|
||||||
"Package database locked" \
|
"Package database locked" \
|
||||||
"pacman's lock file looks left over from an interrupted update. Updates are paused until it is cleared."
|
"pacman's lock file looks left over from an interrupted update. Updates are paused until it is cleared."
|
||||||
fi
|
fi
|
||||||
@@ -178,6 +182,21 @@ fi
|
|||||||
cau_info "Starting update run"
|
cau_info "Starting update run"
|
||||||
failed=0
|
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
|
if ! cau_pacman_update; then
|
||||||
failed=1
|
failed=1
|
||||||
fi
|
fi
|
||||||
@@ -196,6 +215,15 @@ fi
|
|||||||
|
|
||||||
cau_pacman_cleanup
|
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)"
|
pacnew="$(cau_pacman_pacnew_count)"
|
||||||
if [[ $pacnew =~ ^[0-9]+$ ]] && (( pacnew > 0 )); then
|
if [[ $pacnew =~ ^[0-9]+$ ]] && (( pacnew > 0 )); then
|
||||||
cau_info "$pacnew .pacnew file(s) present; left untouched"
|
cau_info "$pacnew .pacnew file(s) present; left untouched"
|
||||||
@@ -213,7 +241,7 @@ cau_state_write last_counts \
|
|||||||
if (( failed )); then
|
if (( failed )); then
|
||||||
cau_state_write last_result failed
|
cau_state_write last_result failed
|
||||||
cau_error "Update run finished with errors"
|
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" \
|
"Update failed" \
|
||||||
"Something went wrong while updating. Run 'cachy-auto-update log' for details."
|
"Something went wrong while updating. Run 'cachy-auto-update log' for details."
|
||||||
else
|
else
|
||||||
@@ -222,7 +250,8 @@ else
|
|||||||
cau_info "Update run finished successfully ($total item(s) updated)"
|
cau_info "Update run finished successfully ($total item(s) updated)"
|
||||||
|
|
||||||
if (( total > 0 )) && [[ $CFG_NOTIFY_SUCCESS == yes ]]; then
|
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
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -234,7 +263,7 @@ if [[ -n $CAU_PACMAN_HELD ]]; then
|
|||||||
cau_state_write held_back "$CAU_PACMAN_HELD"
|
cau_state_write held_back "$CAU_PACMAN_HELD"
|
||||||
cau_warn "Held back: $CAU_PACMAN_HELD"
|
cau_warn "Held back: $CAU_PACMAN_HELD"
|
||||||
if [[ $CAU_PACMAN_HELD != "$prev_held" && $CFG_NOTIFY_ERROR == yes ]]; then
|
if [[ $CAU_PACMAN_HELD != "$prev_held" && $CFG_NOTIFY_ERROR == yes ]]; then
|
||||||
cau_notify normal "Some packages were held back" \
|
cau_notify normal yes "Some packages were held back" \
|
||||||
"%s could not be updated and was skipped. Everything else is up to date." \
|
"%s could not be updated and was skipped. Everything else is up to date." \
|
||||||
"$CAU_PACMAN_HELD"
|
"$CAU_PACMAN_HELD"
|
||||||
fi
|
fi
|
||||||
@@ -242,12 +271,15 @@ else
|
|||||||
cau_state_clear held_back
|
cau_state_clear held_back
|
||||||
fi
|
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
|
if cau_pacman_reboot_needed; then
|
||||||
cau_state_write reboot_needed 1
|
cau_state_write reboot_needed 1
|
||||||
cau_info "A kernel update needs a restart"
|
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
|
else
|
||||||
cau_state_write reboot_needed 0
|
cau_state_write reboot_needed 0
|
||||||
fi
|
fi
|
||||||
|
|||||||
+43
-1
@@ -75,13 +75,55 @@ cau_msg() {
|
|||||||
cau_msg_in "$(cau_ui_locale)" "$@"
|
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 <locale> <msgid> [printf args...]
|
||||||
cau_msg_in() {
|
cau_msg_in() {
|
||||||
local locale="$1" msgid="$2" translated
|
local locale="$1" msgid="$2" translated cachekey
|
||||||
shift 2
|
shift 2
|
||||||
|
|
||||||
|
cachekey="${locale}"$'\x1f'"${msgid}"
|
||||||
|
if [[ -n ${CAU_MSG_CACHE[$cachekey]+set} ]]; then
|
||||||
|
translated="${CAU_MSG_CACHE[$cachekey]}"
|
||||||
|
else
|
||||||
translated="$(LC_ALL="$locale" LANGUAGE="${locale%%.*}" gettext -- "$msgid" 2>/dev/null)"
|
translated="$(LC_ALL="$locale" LANGUAGE="${locale%%.*}" gettext -- "$msgid" 2>/dev/null)"
|
||||||
[[ -n $translated ]] || translated="$msgid"
|
[[ -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
|
# shellcheck disable=SC2059 # the format string is the translated message
|
||||||
printf -- "$translated" "$@"
|
printf -- "$translated" "$@"
|
||||||
|
|||||||
+42
-13
@@ -6,14 +6,39 @@
|
|||||||
# root-run daemon, and sourcing it would turn a stray line into arbitrary code
|
# 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.
|
# execution. The format is one "Key=Value" per line, '#' starts a comment.
|
||||||
|
|
||||||
# cau_config_get <Key> [default]
|
# The file is cached and parsed in-process rather than shelled out to sed on
|
||||||
cau_config_get() {
|
# every lookup. The settings screen reads every key on every redraw, and a fork
|
||||||
local key="$1" default="${2:-}" val
|
# 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_lookup <Key> [default]
|
||||||
"$CAU_CONFIG" 2>/dev/null | tail -n1)"
|
# 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
|
# strip a trailing comment and surrounding whitespace/quotes
|
||||||
val="${val%%#*}"
|
val="${val%%#*}"
|
||||||
@@ -22,11 +47,14 @@ cau_config_get() {
|
|||||||
val="${val%\"}"
|
val="${val%\"}"
|
||||||
val="${val#\"}"
|
val="${val#\"}"
|
||||||
|
|
||||||
if [[ -n $val ]]; then
|
[[ -n $val ]] && CAU_CONFIG_VALUE="$val"
|
||||||
printf '%s\n' "$val"
|
return 0
|
||||||
else
|
}
|
||||||
printf '%s\n' "$default"
|
|
||||||
fi
|
# cau_config_get <Key> [default]
|
||||||
|
cau_config_get() {
|
||||||
|
_cau_config_lookup "$@"
|
||||||
|
printf '%s\n' "$CAU_CONFIG_VALUE"
|
||||||
}
|
}
|
||||||
|
|
||||||
# cau_config_bool <Key> <default: yes|no>
|
# cau_config_bool <Key> <default: yes|no>
|
||||||
@@ -78,6 +106,7 @@ cau_config_set() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
mv -f "$tmp" "$CAU_CONFIG"
|
mv -f "$tmp" "$CAU_CONFIG"
|
||||||
|
CAU_CONFIG_CACHED=0
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -98,13 +127,13 @@ cau_config_load() {
|
|||||||
CFG_RESOLVE_CONFLICTS=no; cau_config_bool AutoResolveConflicts yes && CFG_RESOLVE_CONFLICTS=yes
|
CFG_RESOLVE_CONFLICTS=no; cau_config_bool AutoResolveConflicts yes && CFG_RESOLVE_CONFLICTS=yes
|
||||||
CFG_CLEAN_CACHE=no; cau_config_bool CleanCache yes && 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_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_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_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="$(cau_config_get UpdateInterval 1d)"
|
||||||
CFG_INTERVAL_SECONDS="$(cau_duration_to_seconds "$CFG_INTERVAL" 86400)"
|
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_KEEP_OLD="$(cau_config_int KeepOldPackages 3)"
|
||||||
CFG_AUR_HELPER="$(cau_config_get AURHelper auto)"
|
CFG_AUR_HELPER="$(cau_config_get AURHelper auto)"
|
||||||
CFG_IGNORE_PKG="$(cau_config_get IgnorePkg '')"
|
CFG_IGNORE_PKG="$(cau_config_get IgnorePkg '')"
|
||||||
|
|||||||
+339
-18
@@ -7,6 +7,316 @@
|
|||||||
|
|
||||||
CAU_UNIT="cachy-auto-update.timer"
|
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>
|
# _cau_row <label> <value>
|
||||||
# printf's %-28s pads by bytes, so a label containing "ü" comes out one column
|
# 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
|
# short. ${#s} counts characters in a UTF-8 locale, so the padding is computed
|
||||||
@@ -141,6 +451,10 @@ cau_ui_status_conditions() {
|
|||||||
cau_ui_menu() {
|
cau_ui_menu() {
|
||||||
local choice
|
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
|
while true; do
|
||||||
cau_config_load
|
cau_config_load
|
||||||
|
|
||||||
@@ -153,45 +467,52 @@ cau_ui_menu() {
|
|||||||
printf ' [3] %s\n' "$(cau_msg "Update now")"
|
printf ' [3] %s\n' "$(cau_msg "Update now")"
|
||||||
printf ' [4] %s\n' "$(cau_msg "Show log")"
|
printf ' [4] %s\n' "$(cau_msg "Show log")"
|
||||||
printf ' [5] %s\n' "$(cau_msg "Show current conditions")"
|
printf ' [5] %s\n' "$(cau_msg "Show current conditions")"
|
||||||
|
printf ' [6] %s\n' "$(cau_msg "Settings")"
|
||||||
printf ' [q] %s\n' "$(cau_msg "Quit")"
|
printf ' [q] %s\n' "$(cau_msg "Quit")"
|
||||||
printf '\n > '
|
printf '\n > '
|
||||||
|
|
||||||
# One keypress, no Enter. -s keeps the raw character out of the
|
# One keypress, no Enter. A failing read means EOF (Ctrl-D, or a
|
||||||
# display so the echo below is the only thing printed, and a failing
|
# script piping input), which quits.
|
||||||
# read means EOF (Ctrl-D, or a script piping input) - that quits.
|
choice="$(cau_read_key)" || {
|
||||||
read -rsn1 choice || { printf '\n'; return 0; }
|
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"
|
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
|
case "$choice" in
|
||||||
# The two switches flip and return straight to the menu, where the
|
# The two switches flip and return straight to the menu, where the
|
||||||
# status block shows the result. Only a warning or an error holds
|
# status block shows the result. Only a warning or an error holds
|
||||||
# the screen (see CAU_UI_NEEDS_ACK).
|
# the screen (see CAU_UI_NEEDS_ACK).
|
||||||
1)
|
1)
|
||||||
CAU_UI_NEEDS_ACK=''
|
CAU_UI_NEEDS_ACK=''
|
||||||
if [[ $CFG_ENABLED == yes ]]; then cau_do_disable; else cau_do_enable; fi
|
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
|
if [[ -n $CAU_UI_NEEDS_ACK ]]; then cau_pause; fi
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
CAU_UI_NEEDS_ACK=''
|
CAU_UI_NEEDS_ACK=''
|
||||||
if [[ $CFG_NOTIFICATIONS == yes ]]; then
|
if [[ $CFG_NOTIFICATIONS == yes ]]; then
|
||||||
cau_do_notifications off
|
cau_ui_cooked cau_do_notifications off
|
||||||
else
|
else
|
||||||
cau_do_notifications on
|
cau_ui_cooked cau_do_notifications on
|
||||||
fi
|
fi
|
||||||
if [[ -n $CAU_UI_NEEDS_ACK ]]; then cau_pause; fi
|
if [[ -n $CAU_UI_NEEDS_ACK ]]; then cau_pause; fi
|
||||||
;;
|
;;
|
||||||
3) cau_do_run --force; cau_pause ;;
|
3) cau_ui_cooked cau_do_run --force; cau_pause ;;
|
||||||
4) cau_do_log; cau_pause ;;
|
4) cau_ui_cooked cau_do_log; cau_pause ;;
|
||||||
5) cau_ui_status_conditions; cau_pause ;;
|
5) cau_ui_status_conditions; cau_pause ;;
|
||||||
q|Q) return 0 ;;
|
6) cau_ui_settings ;;
|
||||||
$'\e')
|
q|Q) cau_ui_term_restore; trap - EXIT INT TERM; return 0 ;;
|
||||||
# Arrow keys and friends arrive as ESC [ X. Swallow the rest so
|
# Anything else - Enter, arrow keys, stray characters - just
|
||||||
# one keypress does not redraw the menu three times. Escape is
|
# redraws. Escape is deliberately not a quit key, so a mistyped
|
||||||
# deliberately not a quit key: that would make a stray arrow
|
# arrow key cannot close the menu.
|
||||||
# key close the menu.
|
|
||||||
read -rsn2 -t 0.05 _ 2>/dev/null || true
|
|
||||||
;;
|
|
||||||
# Anything else, Enter included, just redraws.
|
|
||||||
*) ;;
|
*) ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|||||||
+93
-14
@@ -15,17 +15,68 @@
|
|||||||
CAU_NOTIFY_ICON="system-software-update"
|
CAU_NOTIFY_ICON="system-software-update"
|
||||||
CAU_NOTIFY_QUEUE_MAX=20
|
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
|
# Never fails: a machine without libnotify, or with nobody logged in, is a
|
||||||
# normal state, not an error.
|
# normal state, not an error.
|
||||||
cau_notify() {
|
cau_notify() {
|
||||||
local urgency="$1" title="$2" body="$3"
|
cau_notify_tagged '' yes "$@"
|
||||||
shift 3
|
}
|
||||||
|
|
||||||
|
# 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 -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
|
[[ $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
|
while read -r user uid; do
|
||||||
[[ -n $user ]] || continue
|
[[ -n $user ]] || continue
|
||||||
cau_as_user "$user" "$uid" sh -c 'command -v notify-send >/dev/null' || 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")"
|
t="$(cau_msg_in "$locale" "$title")"
|
||||||
b="$(cau_msg_in "$locale" "$body" "${args[@]}")"
|
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" \
|
--app-name="$CAU_PRETTY" \
|
||||||
--icon="$CAU_NOTIFY_ICON" \
|
--icon="$CAU_NOTIFY_ICON" \
|
||||||
--urgency="$urgency" \
|
--urgency="$urgency" \
|
||||||
-- "$t" "$b" 2>/dev/null
|
"${expiry[@]}" \
|
||||||
|
--print-id \
|
||||||
|
-- "$t" "$b" 2>/dev/null)"
|
||||||
then
|
then
|
||||||
delivered=1
|
delivered=1
|
||||||
|
if [[ -n $tag && $newid =~ ^[0-9]+$ ]]; then
|
||||||
|
cau_state_write "notify_id_${tag}_${user}" "$newid"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
done < <(cau_active_session_users)
|
done < <(cau_active_session_users)
|
||||||
|
|
||||||
(( delivered )) && return 0
|
(( 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:
|
# Tab-separated records, oldest first. The file is world-readable on purpose:
|
||||||
# the login-time delivery runs unprivileged and only ever reads it.
|
# 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
|
# progress by "last key seen", so two records sharing a key could make the
|
||||||
# second one unreachable forever if a login landed between them.
|
# second one unreachable forever if a login landed between them.
|
||||||
cau_notify_enqueue() {
|
cau_notify_enqueue() {
|
||||||
local urgency="$1" title="$2" body="$3"
|
local urgency="$1" linger="$2" title="$3" body="$4"
|
||||||
shift 3
|
shift 4
|
||||||
local record tmp
|
local record tmp
|
||||||
|
|
||||||
mkdir -p "$CAU_STATEDIR" 2>/dev/null || return 0
|
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
|
local arg
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
record+=$'\t'"${arg//$'\t'/ }"
|
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,
|
# 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.
|
# so no write access to /var/lib is needed and each user is tracked separately.
|
||||||
cau_notify_deliver_queue() {
|
cau_notify_deliver_queue() {
|
||||||
local seen_file seen ts urgency title body
|
local seen_file seen ts urgency linger title body rest
|
||||||
local -a args
|
local -a args expiry
|
||||||
|
|
||||||
[[ -r $CAU_NOTIFY_QUEUE ]] || return 0
|
[[ -r $CAU_NOTIFY_QUEUE ]] || return 0
|
||||||
cau_have notify-send || return 0
|
cau_have notify-send || return 0
|
||||||
@@ -99,20 +164,34 @@ cau_notify_deliver_queue() {
|
|||||||
[[ $seen =~ ^[0-9]+$ ]] || seen=0
|
[[ $seen =~ ^[0-9]+$ ]] || seen=0
|
||||||
|
|
||||||
local newest="$seen"
|
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 =~ ^[0-9]+$ ]] || continue
|
||||||
(( ts > seen )) || 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
|
# remaining tab-separated fields are the body's printf arguments
|
||||||
args=()
|
args=()
|
||||||
if [[ -n ${rest:-} ]]; then
|
if [[ -n ${rest:-} ]]; then
|
||||||
IFS=$'\t' read -r -a args <<< "$rest"
|
IFS=$'\t' read -r -a args <<< "$rest"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
expiry=(--expire-time=-1)
|
||||||
|
[[ $linger == yes ]] && expiry=(--expire-time=0)
|
||||||
|
|
||||||
notify-send \
|
notify-send \
|
||||||
--app-name="$CAU_PRETTY" \
|
--app-name="$CAU_PRETTY" \
|
||||||
--icon="$CAU_NOTIFY_ICON" \
|
--icon="$CAU_NOTIFY_ICON" \
|
||||||
--urgency="${urgency:-normal}" \
|
--urgency="${urgency:-normal}" \
|
||||||
|
"${expiry[@]}" \
|
||||||
-- "$(cau_msg "$title")" "$(cau_msg "$body" "${args[@]}")" 2>/dev/null || true
|
-- "$(cau_msg "$title")" "$(cau_msg "$body" "${args[@]}")" 2>/dev/null || true
|
||||||
|
|
||||||
(( ts > newest )) && newest="$ts"
|
(( ts > newest )) && newest="$ts"
|
||||||
|
|||||||
@@ -43,6 +43,28 @@ cau_appimage_update() {
|
|||||||
local user uid count rc=0
|
local user uid count rc=0
|
||||||
local -a cmd
|
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
|
while read -r user uid; do
|
||||||
[[ -n $user ]] || continue
|
[[ -n $user ]] || continue
|
||||||
|
|
||||||
|
|||||||
+17
-1
@@ -124,24 +124,40 @@ cau_aur_update() {
|
|||||||
local pending failures
|
local pending failures
|
||||||
local -a args
|
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)"
|
pending="$(cau_aur_pending)"
|
||||||
if (( pending == 0 )); then
|
if (( pending == 0 )); then
|
||||||
cau_info "No AUR updates pending"
|
cau_info "No AUR updates pending"
|
||||||
cau_state_clear aur_failures
|
cau_state_clear aur_failures
|
||||||
|
cau_progress_drop aur
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cau_info "Updating $pending AUR package(s) with $CAU_AUR_HELPER"
|
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)
|
mapfile -t args < <(cau_aur_helper_args)
|
||||||
|
cau_progress_creep_start
|
||||||
|
|
||||||
if cau_run_logged cau_as_build_user "$CAU_AUR_HELPER" "${args[@]}"; then
|
if cau_run_logged cau_as_build_user "$CAU_AUR_HELPER" "${args[@]}"; then
|
||||||
|
cau_progress_creep_stop
|
||||||
CAU_AUR_COUNT="$pending"
|
CAU_AUR_COUNT="$pending"
|
||||||
|
cau_progress_item "$pending"
|
||||||
cau_state_clear aur_failures
|
cau_state_clear aur_failures
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
cau_progress_creep_stop
|
||||||
CAU_AUR_COUNT=0
|
CAU_AUR_COUNT=0
|
||||||
failures="$(cau_state_read aur_failures 0)"
|
failures="$(cau_state_read aur_failures 0)"
|
||||||
[[ $failures =~ ^[0-9]+$ ]] || failures=0
|
[[ $failures =~ ^[0-9]+$ ]] || failures=0
|
||||||
|
|||||||
+16
-2
@@ -26,17 +26,31 @@ cau_flatpak_pending_system() {
|
|||||||
cau_flatpak_update() {
|
cau_flatpak_update() {
|
||||||
local rc=0 pending user uid home count
|
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_run_logged flatpak update --appstream --system --noninteractive || true
|
||||||
|
cau_progress_creep_stop
|
||||||
|
|
||||||
pending="$(cau_flatpak_pending_system)"
|
pending="$(cau_flatpak_pending_system)"
|
||||||
if (( pending > 0 )); then
|
if (( pending > 0 )); then
|
||||||
cau_info "Updating $pending system Flatpak(s)"
|
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
|
if cau_run_logged flatpak update --system --noninteractive --assumeyes; then
|
||||||
|
cau_progress_creep_stop
|
||||||
CAU_FLATPAK_COUNT=$(( CAU_FLATPAK_COUNT + pending ))
|
CAU_FLATPAK_COUNT=$(( CAU_FLATPAK_COUNT + pending ))
|
||||||
|
cau_progress_item "$pending"
|
||||||
else
|
else
|
||||||
|
cau_progress_creep_stop
|
||||||
cau_warn "System Flatpak update failed"
|
cau_warn "System Flatpak update failed"
|
||||||
rc=1
|
rc=1
|
||||||
fi
|
fi
|
||||||
|
|||||||
+211
-4
@@ -27,6 +27,151 @@ cau_pacman_flags() {
|
|||||||
done
|
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...>
|
# _cau_pacman_exec <logfile> <pacman args...>
|
||||||
# Captures pacman's output for classification, and streams it as well when a
|
# Captures pacman's output for classification, and streams it as well when a
|
||||||
# person is watching. Upgrading a few hundred packages takes minutes; without
|
# person is watching. Upgrading a few hundred packages takes minutes; without
|
||||||
@@ -35,13 +180,38 @@ cau_pacman_flags() {
|
|||||||
_cau_pacman_exec() {
|
_cau_pacman_exec() {
|
||||||
local log="$1"
|
local log="$1"
|
||||||
shift
|
shift
|
||||||
|
local rc watcher=0
|
||||||
|
|
||||||
if [[ -n $CAU_INTERACTIVE ]]; then
|
# Only worth a second process and a grep per second if a bar exists to feed.
|
||||||
pacman "$@" 2>&1 | tee "$log"
|
if cau_progress_active; then
|
||||||
return "${PIPESTATUS[0]}"
|
_cau_pacman_progress_watch "$log" &
|
||||||
|
watcher=$!
|
||||||
fi
|
fi
|
||||||
|
|
||||||
pacman "$@" > "$log" 2>&1
|
# 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
|
# cau_pacman_pending
|
||||||
@@ -117,8 +287,18 @@ cau_pacman_update() {
|
|||||||
local log kind
|
local log kind
|
||||||
local -a flags
|
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
|
if ! cau_pacman_pending; then
|
||||||
cau_info "No repository updates pending"
|
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
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -126,6 +306,10 @@ cau_pacman_update() {
|
|||||||
CAU_PACMAN_COUNT="$(grep -c . <<< "$CAU_PACMAN_PENDING")"
|
CAU_PACMAN_COUNT="$(grep -c . <<< "$CAU_PACMAN_PENDING")"
|
||||||
[[ $CAU_PACMAN_COUNT =~ ^[0-9]+$ ]] || CAU_PACMAN_COUNT=0
|
[[ $CAU_PACMAN_COUNT =~ ^[0-9]+$ ]] || CAU_PACMAN_COUNT=0
|
||||||
cau_info "Updating $CAU_PACMAN_COUNT repository package(s)"
|
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
|
else
|
||||||
# checkupdates is unavailable, so the list is unknown and pacman is
|
# checkupdates is unavailable, so the list is unknown and pacman is
|
||||||
# asked to work it out itself.
|
# asked to work it out itself.
|
||||||
@@ -133,6 +317,20 @@ cau_pacman_update() {
|
|||||||
cau_info "Running a full system upgrade (pending list unavailable)"
|
cau_info "Running a full system upgrade (pending list unavailable)"
|
||||||
fi
|
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)
|
mapfile -t flags < <(cau_pacman_flags)
|
||||||
log="$(mktemp)" || return 1
|
log="$(mktemp)" || return 1
|
||||||
|
|
||||||
@@ -144,6 +342,13 @@ cau_pacman_update() {
|
|||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
if _cau_pacman_exec "$log" -Syu "${flags[@]}" "${extra[@]}"; then
|
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
|
cat "$log" >> "$CAU_RUNLOG" 2>/dev/null
|
||||||
grep -E '^(removing|replacing) ' "$log" 2>/dev/null \
|
grep -E '^(removing|replacing) ' "$log" 2>/dev/null \
|
||||||
| while read -r line; do cau_info " $line"; done
|
| while read -r line; do cau_info " $line"; done
|
||||||
@@ -244,6 +449,8 @@ cau_pacman_pacnew_count() {
|
|||||||
cau_pacman_cleanup() {
|
cau_pacman_cleanup() {
|
||||||
local -a orphans
|
local -a orphans
|
||||||
|
|
||||||
|
cau_progress_step cleanup "Cleaning up after the update"
|
||||||
|
|
||||||
if [[ $CFG_REMOVE_ORPHANS == yes ]]; then
|
if [[ $CFG_REMOVE_ORPHANS == yes ]]; then
|
||||||
mapfile -t orphans < <(pacman -Qtdq 2>/dev/null)
|
mapfile -t orphans < <(pacman -Qtdq 2>/dev/null)
|
||||||
if (( ${#orphans[@]} )); then
|
if (( ${#orphans[@]} )); then
|
||||||
|
|||||||
@@ -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