Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea3f0d21fa | ||
|
|
6aa9b147b8 | ||
|
|
a17aed4b3d |
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.1.1
|
VERSION ?= 1.2.1
|
||||||
|
|
||||||
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:
|
||||||
|
|||||||
@@ -74,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,
|
||||||
@@ -83,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
|
||||||
|
|
||||||
@@ -134,8 +137,15 @@ Three layers, in order of how much they can actually promise:
|
|||||||
|
|
||||||
**A notification goes out before the transaction starts** — "Installing
|
**A notification goes out before the transaction starts** — "Installing
|
||||||
updates, please leave the computer switched on until this is done" — and is
|
updates, please leave the computer switched on until this is done" — and is
|
||||||
replaced in place by the result when the run finishes, so it costs one bubble
|
withdrawn again when the result arrives, so it costs one bubble rather than
|
||||||
rather than two. This exists because of what the next paragraph does *not* do.
|
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 or
|
`systemd-inhibit --what=sleep:shutdown --mode=block` lock, so closing the lid or
|
||||||
@@ -170,6 +180,49 @@ 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.
|
||||||
|
- The position inside the repository step comes from pacman's own
|
||||||
|
`(120/260) upgrading foo` lines. 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
|
||||||
@@ -203,7 +256,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.
|
||||||
@@ -226,8 +279,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
|
||||||
|
|
||||||
|
|||||||
@@ -100,14 +100,57 @@ 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.
|
||||||
|
|
||||||
|
The position within the repository step is read from pacman's own
|
||||||
|
"(120/260) upgrading foo" output. Its 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
|
Before a transaction starts, a notification says an update is running and asks
|
||||||
for the machine to be left on. It is replaced in place by the result once the
|
for the machine to be left on. It is withdrawn again when the result arrives,
|
||||||
run finishes.
|
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
|
||||||
|
|||||||
+34
-10
@@ -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,28 @@ msgstr ""
|
|||||||
msgid "Without a command an interactive menu is shown."
|
msgid "Without a command an interactive menu is shown."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. Progress bar
|
||||||
|
msgid "Repository packages"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "AUR packages"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Flatpaks"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "AppImages"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Cleaning up"
|
||||||
|
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 +265,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 ""
|
||||||
|
|
||||||
@@ -291,9 +318,6 @@ msgstr ""
|
|||||||
msgid "Notify when something goes wrong"
|
msgid "Notify when something goes wrong"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Notify when a restart is needed"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Time between update runs"
|
msgid "Time between update runs"
|
||||||
msgstr ""
|
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,28 @@ 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
|
||||||
|
msgid "Repository packages"
|
||||||
|
msgstr "Paketquellen"
|
||||||
|
|
||||||
|
msgid "AUR packages"
|
||||||
|
msgstr "AUR-Pakete"
|
||||||
|
|
||||||
|
msgid "Flatpaks"
|
||||||
|
msgstr "Flatpaks"
|
||||||
|
|
||||||
|
msgid "AppImages"
|
||||||
|
msgstr "AppImages"
|
||||||
|
|
||||||
|
msgid "Cleaning up"
|
||||||
|
msgstr "Wird aufgeräumt"
|
||||||
|
|
||||||
|
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 +266,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"
|
||||||
|
|
||||||
@@ -293,9 +320,6 @@ msgstr "Melden nach erfolgreichem Update"
|
|||||||
msgid "Notify when something goes wrong"
|
msgid "Notify when something goes wrong"
|
||||||
msgstr "Melden, wenn etwas schiefgeht"
|
msgstr "Melden, wenn etwas schiefgeht"
|
||||||
|
|
||||||
msgid "Notify when a restart is needed"
|
|
||||||
msgstr "Melden, wenn ein Neustart nötig ist"
|
|
||||||
|
|
||||||
msgid "Time between update runs"
|
msgid "Time between update runs"
|
||||||
msgstr "Abstand zwischen Update-Läufen"
|
msgstr "Abstand zwischen Update-Läufen"
|
||||||
|
|
||||||
|
|||||||
@@ -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,11 +94,21 @@ 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
|
# 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
|
# request is refused and the desktop answers with an untranslated polkit
|
||||||
# password prompt that never mentions updates. This message is replaced by the
|
# password prompt that never mentions updates. The message is withdrawn again
|
||||||
# result when the run finishes, so it costs one bubble, not two.
|
# 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
|
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
|
||||||
@@ -108,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,138 @@
|
|||||||
|
#!/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"
|
||||||
|
# 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 == "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,16 @@ 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=(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[@]}"
|
||||||
|
|
||||||
if ! cau_pacman_update; then
|
if ! cau_pacman_update; then
|
||||||
failed=1
|
failed=1
|
||||||
fi
|
fi
|
||||||
@@ -196,6 +210,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 +236,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_tagged run yes 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 +245,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_tagged run yes 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 +258,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 +266,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
|
||||||
|
|||||||
+1
-2
@@ -130,11 +130,10 @@ cau_config_load() {
|
|||||||
CFG_NOTIFY_START=no; cau_config_bool NotifyOnStart yes && CFG_NOTIFY_START=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 '')"
|
||||||
|
|||||||
+1
-2
@@ -131,11 +131,10 @@ CAU_SETTINGS=(
|
|||||||
"NotifyOnStart|bool|yes|Notify when an update starts"
|
"NotifyOnStart|bool|yes|Notify when an update starts"
|
||||||
"NotifyOnSuccess|bool|yes|Notify after a successful update"
|
"NotifyOnSuccess|bool|yes|Notify after a successful update"
|
||||||
"NotifyOnError|bool|yes|Notify when something goes wrong"
|
"NotifyOnError|bool|yes|Notify when something goes wrong"
|
||||||
"NotifyReboot|bool|yes|Notify when a restart is needed"
|
|
||||||
"UpdateInterval|choice:6h 12h 1d 2d 1w|1d|Time between update runs"
|
"UpdateInterval|choice:6h 12h 1d 2d 1w|1d|Time between update runs"
|
||||||
"SkipWhenGaming|bool|yes|Postpone while a game is running"
|
"SkipWhenGaming|bool|yes|Postpone while a game is running"
|
||||||
"RequireAC|bool|no|Only update on mains power"
|
"RequireAC|bool|no|Only update on mains power"
|
||||||
"MinBatteryPercent|choice:0 20 30 40 50 60 70 80|40|Minimum battery level (%)"
|
"MinBatteryPercent|choice:0 20 30 40 50 60 70 80|30|Minimum battery level (%)"
|
||||||
"UpdateAUR|bool|yes|Update AUR packages"
|
"UpdateAUR|bool|yes|Update AUR packages"
|
||||||
"UpdateFlatpak|bool|yes|Update Flatpaks"
|
"UpdateFlatpak|bool|yes|Update Flatpaks"
|
||||||
"UpdateAppImages|bool|yes|Update AppImages"
|
"UpdateAppImages|bool|yes|Update AppImages"
|
||||||
|
|||||||
+74
-19
@@ -15,30 +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() {
|
||||||
cau_notify_tagged '' yes "$@"
|
cau_notify_tagged '' yes "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
# cau_notify_tagged <tag> <queue: yes|no> <urgency> <title> <body> [args...]
|
# 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...]
|
||||||
#
|
#
|
||||||
# A tag makes this notification replace the previous one carrying the same tag
|
# linger=yes keeps the message on screen until somebody dismisses it; anything
|
||||||
# rather than stacking a second bubble beside it - that is what turns
|
# else lets it time out on its own. The line it draws is whether the machine
|
||||||
# "installing updates" into "system updated" in place instead of leaving two
|
# still needs a person: a finished update is over and done with and should not
|
||||||
# messages that contradict each other.
|
# 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.
|
# 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.
|
# Telling a user at next login that an update started an hour ago is noise.
|
||||||
cau_notify_tagged() {
|
cau_notify_tagged() {
|
||||||
local tag="$1" queue="$2" urgency="$3" title="$4" body="$5"
|
local tag="$1" queue="$2" urgency="$3" linger="$4" title="$5" body="$6"
|
||||||
shift 5
|
shift 6
|
||||||
local -a args=("$@")
|
local -a args=("$@")
|
||||||
local delivered=0 user uid locale t b prev newid
|
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
|
||||||
@@ -47,17 +85,20 @@ cau_notify_tagged() {
|
|||||||
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[@]}")"
|
||||||
|
|
||||||
prev=0
|
|
||||||
if [[ -n $tag ]]; then
|
if [[ -n $tag ]]; then
|
||||||
prev="$(cau_state_read "notify_id_${tag}_${user}" 0)"
|
prev="$(cau_state_read "notify_id_${tag}_${user}" 0)"
|
||||||
[[ $prev =~ ^[0-9]+$ ]] || prev=0
|
if [[ $prev =~ ^[0-9]+$ ]] && (( prev > 0 )); then
|
||||||
|
cau_notify_close "$user" "$uid" "$prev"
|
||||||
|
fi
|
||||||
|
cau_state_clear "notify_id_${tag}_${user}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if newid="$(cau_as_user "$user" "$uid" notify-send \
|
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" \
|
||||||
--print-id --replace-id="$prev" \
|
"${expiry[@]}" \
|
||||||
|
--print-id \
|
||||||
-- "$t" "$b" 2>/dev/null)"
|
-- "$t" "$b" 2>/dev/null)"
|
||||||
then
|
then
|
||||||
delivered=1
|
delivered=1
|
||||||
@@ -70,10 +111,10 @@ cau_notify_tagged() {
|
|||||||
(( delivered )) && return 0
|
(( delivered )) && return 0
|
||||||
[[ $queue == yes ]] || 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.
|
||||||
#
|
#
|
||||||
@@ -81,13 +122,13 @@ cau_notify_tagged() {
|
|||||||
# 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'/ }"
|
||||||
@@ -109,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
|
||||||
@@ -123,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,8 @@ cau_appimage_update() {
|
|||||||
local user uid count rc=0
|
local user uid count rc=0
|
||||||
local -a cmd
|
local -a cmd
|
||||||
|
|
||||||
|
cau_progress_step appimage "AppImages"
|
||||||
|
|
||||||
while read -r user uid; do
|
while read -r user uid; do
|
||||||
[[ -n $user ]] || continue
|
[[ -n $user ]] || continue
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ cau_aur_update() {
|
|||||||
|
|
||||||
cau_aur_ready || return 0
|
cau_aur_ready || return 0
|
||||||
|
|
||||||
|
cau_progress_step aur "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"
|
||||||
@@ -134,10 +136,15 @@ cau_aur_update() {
|
|||||||
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 with no counter this side of
|
||||||
|
# its output, so the bar sits at the start of the step until it is done.
|
||||||
|
cau_progress_item 0 "$pending"
|
||||||
mapfile -t args < <(cau_aur_helper_args)
|
mapfile -t args < <(cau_aur_helper_args)
|
||||||
|
|
||||||
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_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
|
||||||
|
|||||||
@@ -28,14 +28,18 @@ cau_flatpak_update() {
|
|||||||
|
|
||||||
cau_have flatpak || return 0
|
cau_have flatpak || return 0
|
||||||
|
|
||||||
|
cau_progress_step flatpak "Flatpaks"
|
||||||
|
|
||||||
# refresh appstream metadata first so remote-ls sees current versions
|
# refresh appstream metadata first so remote-ls sees current versions
|
||||||
cau_run_logged flatpak update --appstream --system --noninteractive || true
|
cau_run_logged flatpak update --appstream --system --noninteractive || true
|
||||||
|
|
||||||
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"
|
||||||
if cau_run_logged flatpak update --system --noninteractive --assumeyes; then
|
if cau_run_logged flatpak update --system --noninteractive --assumeyes; then
|
||||||
CAU_FLATPAK_COUNT=$(( CAU_FLATPAK_COUNT + pending ))
|
CAU_FLATPAK_COUNT=$(( CAU_FLATPAK_COUNT + pending ))
|
||||||
|
cau_progress_item "$pending"
|
||||||
else
|
else
|
||||||
cau_warn "System Flatpak update failed"
|
cau_warn "System Flatpak update failed"
|
||||||
rc=1
|
rc=1
|
||||||
|
|||||||
+89
-3
@@ -27,6 +27,72 @@ 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:]]+'
|
||||||
|
|
||||||
|
# _cau_pacman_progress_watch <logfile>
|
||||||
|
# Feeds the desktop's progress bar by watching pacman work.
|
||||||
|
#
|
||||||
|
# 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=''
|
||||||
|
|
||||||
|
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)"
|
||||||
|
[[ -n $line ]] || continue
|
||||||
|
|
||||||
|
# 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%...}"
|
||||||
|
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 +101,28 @@ cau_pacman_flags() {
|
|||||||
_cau_pacman_exec() {
|
_cau_pacman_exec() {
|
||||||
local log="$1"
|
local log="$1"
|
||||||
shift
|
shift
|
||||||
|
local rc watcher=0
|
||||||
|
|
||||||
|
# Only worth a second process and a grep per second if a bar exists to feed.
|
||||||
|
if cau_progress_active; then
|
||||||
|
_cau_pacman_progress_watch "$log" &
|
||||||
|
watcher=$!
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -n $CAU_INTERACTIVE ]]; then
|
if [[ -n $CAU_INTERACTIVE ]]; then
|
||||||
pacman "$@" 2>&1 | tee "$log"
|
pacman "$@" 2>&1 | tee "$log"
|
||||||
return "${PIPESTATUS[0]}"
|
rc="${PIPESTATUS[0]}"
|
||||||
|
else
|
||||||
|
pacman "$@" > "$log" 2>&1
|
||||||
|
rc=$?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
pacman "$@" > "$log" 2>&1
|
if (( watcher )); then
|
||||||
|
kill "$watcher" 2>/dev/null
|
||||||
|
wait "$watcher" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
return "$rc"
|
||||||
}
|
}
|
||||||
|
|
||||||
# cau_pacman_pending
|
# cau_pacman_pending
|
||||||
@@ -117,6 +198,8 @@ cau_pacman_update() {
|
|||||||
local log kind
|
local log kind
|
||||||
local -a flags
|
local -a flags
|
||||||
|
|
||||||
|
cau_progress_step repo "Repository packages"
|
||||||
|
|
||||||
if ! cau_pacman_pending; then
|
if ! cau_pacman_pending; then
|
||||||
cau_info "No repository updates pending"
|
cau_info "No repository updates pending"
|
||||||
return 0
|
return 0
|
||||||
@@ -126,6 +209,7 @@ 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)"
|
||||||
|
cau_progress_item 0 "$CAU_PACMAN_COUNT"
|
||||||
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.
|
||||||
@@ -141,7 +225,7 @@ cau_pacman_update() {
|
|||||||
# updates and, on a German system, is not even translated. Somebody who was
|
# updates and, on a German system, is not even translated. Somebody who was
|
||||||
# simply told beforehand does not end up staring at that.
|
# simply told beforehand does not end up staring at that.
|
||||||
if [[ $CFG_NOTIFY_START == yes ]]; then
|
if [[ $CFG_NOTIFY_START == yes ]]; then
|
||||||
cau_notify_tagged run no normal \
|
cau_notify_tagged run no normal no \
|
||||||
"Installing updates" \
|
"Installing updates" \
|
||||||
"%d packages are being updated. Please leave the computer switched on until this is done." \
|
"%d packages are being updated. Please leave the computer switched on until this is done." \
|
||||||
"${CAU_PACMAN_COUNT:-0}"
|
"${CAU_PACMAN_COUNT:-0}"
|
||||||
@@ -258,6 +342,8 @@ cau_pacman_pacnew_count() {
|
|||||||
cau_pacman_cleanup() {
|
cau_pacman_cleanup() {
|
||||||
local -a orphans
|
local -a orphans
|
||||||
|
|
||||||
|
cau_progress_step cleanup "Cleaning up"
|
||||||
|
|
||||||
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,267 @@
|
|||||||
|
# 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: repositories dominate, 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.
|
||||||
|
declare -A CAU_PROGRESS_WEIGHTS=(
|
||||||
|
[repo]=70 [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_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
|
||||||
|
|
||||||
|
for step in "${CAU_PROGRESS_PLAN[@]}"; do
|
||||||
|
[[ $step == "$id" ]] && break
|
||||||
|
base=$(( base + ${CAU_PROGRESS_WEIGHTS[$step]:-0} ))
|
||||||
|
done
|
||||||
|
|
||||||
|
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_item <processed> [total]
|
||||||
|
# How far through the current step we are.
|
||||||
|
cau_progress_item() {
|
||||||
|
local processed="$1" total="${2:-$CAU_PROGRESS_TOTAL}" pct scaled
|
||||||
|
|
||||||
|
(( ${#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"
|
||||||
|
scaled=$(( CAU_PROGRESS_BASE * 100 + CAU_PROGRESS_SPAN * 100 * processed / total ))
|
||||||
|
else
|
||||||
|
scaled=$(( CAU_PROGRESS_BASE * 100 ))
|
||||||
|
fi
|
||||||
|
|
||||||
|
pct=$(( scaled / CAU_PROGRESS_SCALE ))
|
||||||
|
(( pct > 100 )) && pct=100
|
||||||
|
|
||||||
|
# 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_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
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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