Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6aa9b147b8 | ||
|
|
a17aed4b3d |
No files matched your search
@@ -8,7 +8,7 @@
|
||||
# Overridable so a packager can pass the version it is actually building
|
||||
# (`make VERSION=$pkgver`). The literal below is the fallback for builds
|
||||
# straight from a checkout, and is what a release tag has to carry.
|
||||
VERSION ?= 1.1.1
|
||||
VERSION ?= 1.2.0
|
||||
|
||||
PREFIX ?= /usr
|
||||
DESTDIR ?=
|
||||
@@ -64,6 +64,13 @@ check:
|
||||
else \
|
||||
echo "shellcheck not found - skipped"; \
|
||||
fi
|
||||
@if command -v python3 >/dev/null 2>&1; then \
|
||||
python3 -m py_compile src/cachy-auto-update-progress \
|
||||
&& echo "ok src/cachy-auto-update-progress"; \
|
||||
rm -rf src/__pycache__; \
|
||||
else \
|
||||
echo "python3 not found - skipped"; \
|
||||
fi
|
||||
@if command -v visudo >/dev/null 2>&1; then \
|
||||
visudo -cf res/sudoers/cachy-auto-update >/dev/null && echo "ok sudoers"; \
|
||||
fi
|
||||
@@ -72,6 +79,9 @@ install: build
|
||||
# executables
|
||||
install -Dm755 src/cachy-auto-update "$(DESTDIR)$(BINDIR)/cachy-auto-update"
|
||||
install -Dm755 src/cachy-auto-update-run "$(DESTDIR)$(LIBEXECDIR)/cachy-auto-update-run"
|
||||
# holds a session bus connection open so the update can drive a progress bar
|
||||
install -Dm755 src/cachy-auto-update-progress \
|
||||
"$(DESTDIR)$(LIBEXECDIR)/cachy-auto-update-progress"
|
||||
# the version and the resolved lib path are baked in at install time
|
||||
sed -i -e 's|@VERSION@|$(VERSION)|g' \
|
||||
-e 's|@LIBDIR@|$(LIBDIR)|g' \
|
||||
@@ -114,6 +124,10 @@ install: build
|
||||
install -Dm644 res/autostart/cachy-auto-update-notify.desktop \
|
||||
"$(DESTDIR)$(XDGAUTOSTART)/cachy-auto-update-notify.desktop"
|
||||
|
||||
# names and illustrates the progress bar; hidden from the application menu
|
||||
install -Dm644 res/applications/cachy-auto-update.desktop \
|
||||
"$(DESTDIR)$(DATADIR)/applications/cachy-auto-update.desktop"
|
||||
|
||||
# translations
|
||||
@for l in $(LINGUAS); do \
|
||||
if [ -f "po/$$l.mo" ]; then \
|
||||
@@ -138,6 +152,7 @@ uninstall:
|
||||
rm -f "$(DESTDIR)$(SYSTEMDDIR)/cachy-auto-update.service"
|
||||
rm -f "$(DESTDIR)$(SYSTEMDDIR)/cachy-auto-update.timer"
|
||||
rm -f "$(DESTDIR)$(XDGAUTOSTART)/cachy-auto-update-notify.desktop"
|
||||
rm -f "$(DESTDIR)$(DATADIR)/applications/cachy-auto-update.desktop"
|
||||
rm -f "$(DESTDIR)$(MANDIR)/man1/cachy-auto-update.1"
|
||||
|
||||
clean:
|
||||
|
||||
@@ -74,7 +74,7 @@ deliberately.
|
||||
|
||||
A run is postponed — and retried an hour later — when:
|
||||
|
||||
- the battery is below 40 % (ignored on mains power; desktops without a battery
|
||||
- the battery is below 30 % (ignored on mains power; desktops without a battery
|
||||
are never affected),
|
||||
- a game is running: GameMode, a known game process, or anything holding a
|
||||
blocking idle inhibitor,
|
||||
@@ -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
|
||||
fastest way to find out why nothing is happening.
|
||||
|
||||
The machine is **never** restarted on its own. When a kernel update needs a
|
||||
restart, you get a notification saying so.
|
||||
The machine is **never** restarted on its own. A kernel update that needs a
|
||||
restart is reported by `cachy-auto-update status`, not by a notification: the
|
||||
running kernel loses its module tree the moment pacman unpacks the new one, so
|
||||
a bubble would arrive while the run is still building AUR packages and pulling
|
||||
Flatpaks — and reads as an invitation to restart in the middle of it.
|
||||
|
||||
## About the password question
|
||||
|
||||
@@ -134,8 +137,15 @@ 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
|
||||
replaced in place by the result when the run finishes, so it costs one bubble
|
||||
rather than two. This exists because of what the next paragraph does *not* do.
|
||||
withdrawn again when the result arrives, so it costs one bubble rather than
|
||||
two. This exists because of what the next paragraph does *not* do.
|
||||
|
||||
**A progress bar sits in the notification area for the whole run**, the same
|
||||
one Dolphin puts there while it copies files: which step is running, which
|
||||
package is being unpacked, how far along the whole thing is. A twenty-minute
|
||||
run that shows nothing looks indistinguishable from a hung one, and that is
|
||||
what gets a machine switched off in the middle of a transaction. See
|
||||
[The progress bar](#the-progress-bar).
|
||||
|
||||
**Suspend and a normal shutdown are blocked.** The run holds a
|
||||
`systemd-inhibit --what=sleep:shutdown --mode=block` lock, so closing the lid or
|
||||
@@ -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
|
||||
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
|
||||
|
||||
`/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
|
||||
silently destroy something that was put there on purpose.
|
||||
- **Reboots** — you get a notification, never a surprise restart.
|
||||
- **Reboots** — `status` tells you one is due, never a surprise restart.
|
||||
|
||||
Signature failures trigger one keyring refresh and one retry, since a stale
|
||||
keyring blocks everything else until it is fixed.
|
||||
@@ -226,8 +279,13 @@ sudo systemd-sysusers && sudo systemd-tmpfiles --create
|
||||
sudo cachy-auto-update enable
|
||||
```
|
||||
|
||||
`make check` runs `bash -n` over everything, `shellcheck` when available, and
|
||||
validates the sudoers drop-in with `visudo -c`.
|
||||
`make check` runs `bash -n` over every shell file, `py_compile` over the
|
||||
progress helper, `shellcheck` when available, and validates the sudoers drop-in
|
||||
with `visudo -c`.
|
||||
|
||||
Everything is optional at runtime and degrades to doing less rather than
|
||||
failing: `pacman-contrib` for `checkupdates`, an AUR helper, `flatpak`, Gear
|
||||
Lever, `libnotify` for notifications, and `python-gobject` for the progress bar.
|
||||
|
||||
## Relationship to cachy-update
|
||||
|
||||
|
||||
@@ -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
|
||||
running are skipped.
|
||||
|
||||
The machine is never restarted automatically. When a kernel update makes a
|
||||
restart necessary, a notification says so.
|
||||
The machine is never restarted automatically. A kernel update that makes a
|
||||
restart necessary is reported by *cachy-auto-update status*, not by a
|
||||
notification: the running kernel loses its module tree as soon as pacman
|
||||
unpacks the new one, so a bubble would fire while the run is still working
|
||||
through AUR packages and Flatpaks, and reads as an invitation to restart in the
|
||||
middle of it.
|
||||
|
||||
# PROGRESS
|
||||
|
||||
For as long as a run is working, the notification area carries a live progress
|
||||
entry: the step being performed, how many items it has got through, an overall
|
||||
percentage, and the package currently being unpacked under "Details". This is a
|
||||
job in the sense of *org.kde.JobViewServer*, the same mechanism a file manager
|
||||
uses while copying, rather than a notification - which is what makes it a bar
|
||||
instead of a line of text.
|
||||
|
||||
The desktop withdraws a job as soon as the D-Bus connection that requested it
|
||||
closes, so a helper process runs inside each graphical session for the duration
|
||||
of the update and holds that connection open. It requires _python-gobject_.
|
||||
Where that is missing, or on a desktop with no job interface, there is no
|
||||
progress entry and nothing else is affected.
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
run finishes.
|
||||
for the machine to be left on. It is withdrawn again when the result arrives,
|
||||
so one bubble is used rather than two.
|
||||
|
||||
While a transaction is running, *cachy-auto-update* holds a
|
||||
*systemd-inhibit*(1) lock on _sleep_ and _shutdown_ in blocking mode, so a
|
||||
|
||||
+23
-10
@@ -168,7 +168,7 @@ msgstr ""
|
||||
msgid "base-devel is missing - AUR packages cannot be built without it."
|
||||
msgstr ""
|
||||
|
||||
msgid "cachy-update also notifies about available updates. Turn its notifications off? [y/N]"
|
||||
msgid "cachy-update also notifies about available updates. Turn its notifications off? [Y/n]"
|
||||
msgstr ""
|
||||
|
||||
msgid "cachy-update's update check has been disabled."
|
||||
@@ -218,6 +218,28 @@ msgstr ""
|
||||
msgid "Without a command an interactive menu is shown."
|
||||
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
|
||||
msgid "System updated"
|
||||
msgstr ""
|
||||
@@ -232,12 +254,6 @@ msgstr ""
|
||||
msgid "Something went wrong while updating. Run 'cachy-auto-update log' for details."
|
||||
msgstr ""
|
||||
|
||||
msgid "Restart recommended"
|
||||
msgstr ""
|
||||
|
||||
msgid "A new kernel was installed. Please restart when it suits you."
|
||||
msgstr ""
|
||||
|
||||
msgid "Package database locked"
|
||||
msgstr ""
|
||||
|
||||
@@ -291,9 +307,6 @@ msgstr ""
|
||||
msgid "Notify when something goes wrong"
|
||||
msgstr ""
|
||||
|
||||
msgid "Notify when a restart is needed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Time between update runs"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -169,8 +169,8 @@ msgstr "Kein AUR-Helper gefunden – für AUR-Updates paru oder yay installieren
|
||||
msgid "base-devel is missing - AUR packages cannot be built without it."
|
||||
msgstr "base-devel fehlt – ohne das lassen sich AUR-Pakete nicht bauen."
|
||||
|
||||
msgid "cachy-update also notifies about available updates. Turn its notifications off? [y/N]"
|
||||
msgstr "cachy-update benachrichtigt ebenfalls über verfügbare Updates. Dessen Benachrichtigungen abschalten? [j/N]"
|
||||
msgid "cachy-update also notifies about available updates. Turn its notifications off? [Y/n]"
|
||||
msgstr "cachy-update benachrichtigt ebenfalls über verfügbare Updates. Dessen Benachrichtigungen abschalten? [J/n]"
|
||||
|
||||
msgid "cachy-update's update check has been disabled."
|
||||
msgstr "Die Update-Prüfung von cachy-update wurde abgeschaltet."
|
||||
@@ -219,6 +219,28 @@ msgstr "Version anzeigen"
|
||||
msgid "Without a command an interactive menu is shown."
|
||||
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
|
||||
msgid "System updated"
|
||||
msgstr "System aktualisiert"
|
||||
@@ -233,12 +255,6 @@ msgstr "Update fehlgeschlagen"
|
||||
msgid "Something went wrong while updating. Run 'cachy-auto-update log' for details."
|
||||
msgstr "Beim Update ist etwas schiefgelaufen. Details mit „cachy-auto-update log“."
|
||||
|
||||
msgid "Restart recommended"
|
||||
msgstr "Neustart empfohlen"
|
||||
|
||||
msgid "A new kernel was installed. Please restart when it suits you."
|
||||
msgstr "Es wurde ein neuer Kernel installiert. Bitte bei Gelegenheit neu starten."
|
||||
|
||||
msgid "Package database locked"
|
||||
msgstr "Paketdatenbank gesperrt"
|
||||
|
||||
@@ -293,9 +309,6 @@ msgstr "Melden nach erfolgreichem Update"
|
||||
msgid "Notify when something goes wrong"
|
||||
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"
|
||||
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
|
||||
# power and on machines without a battery.
|
||||
MinBatteryPercent=40
|
||||
MinBatteryPercent=30
|
||||
|
||||
# Update only while plugged in. Stricter than MinBatteryPercent.
|
||||
RequireAC=no
|
||||
@@ -94,11 +94,21 @@ RemoveOrphans=no
|
||||
# ---------------------------------------------------------------------------
|
||||
# Notification detail
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# How long a message stays on screen is not configurable, because it follows
|
||||
# from what the message is for: anything reporting that the machine still needs
|
||||
# a person - a failed update, a paused package queue, a package that had to be
|
||||
# skipped - waits until it is dismissed, since a message like that is only ever
|
||||
# useful if it is still there when somebody comes back. Everything else, an
|
||||
# update that simply worked included, times out on its own.
|
||||
|
||||
# Say that an update has started. Worth keeping on: while one runs, a shutdown
|
||||
# request is refused and the desktop answers with an untranslated polkit
|
||||
# password prompt that never mentions updates. This message is replaced by the
|
||||
# result when the run finishes, so it costs one bubble, not two.
|
||||
# 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
|
||||
@@ -108,6 +118,9 @@ NotifyOnSuccess=yes
|
||||
# Report failures.
|
||||
NotifyOnError=yes
|
||||
|
||||
# Point out that a kernel update needs a restart. The machine is never
|
||||
# restarted automatically.
|
||||
NotifyReboot=yes
|
||||
# A kernel update that needs a restart is not announced by a notification. The
|
||||
# running kernel loses its module tree as soon as pacman unpacks the new one,
|
||||
# so that would fire while the run is still working through AUR packages and
|
||||
# Flatpaks, and reads as an invitation to restart in the middle of it.
|
||||
# `cachy-auto-update status` reports it instead. The machine is never restarted
|
||||
# automatically either way.
|
||||
+10
-2
@@ -117,9 +117,17 @@ cau_offer_disable_cachy_update() {
|
||||
(( found )) || return 0
|
||||
|
||||
printf '\n %s\n ' \
|
||||
"$(cau_msg "cachy-update also notifies about available updates. Turn its notifications off? [y/N]")"
|
||||
"$(cau_msg "cachy-update also notifies about available updates. Turn its notifications off? [Y/n]")"
|
||||
read -r answer || return 0
|
||||
[[ ${answer,,} == y ]] || return 0
|
||||
|
||||
# Defaults to yes: once updates install themselves, cachy-update's "N
|
||||
# updates available" is purely noise. "j" is accepted too - the prompt is
|
||||
# translated, so a German user types the German letter and used to have
|
||||
# that silently read as "no".
|
||||
case "${answer,,}" in
|
||||
''|y|yes|j|ja) ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
|
||||
while read -r user uid; do
|
||||
[[ -n $user ]] || continue
|
||||
|
||||
@@ -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@}"
|
||||
|
||||
for _mod in common config users conditions locks notify \
|
||||
for _mod in common config users conditions locks notify progress \
|
||||
pkg_pacman pkg_aur pkg_flatpak pkg_appimage; do
|
||||
# shellcheck source=/dev/null
|
||||
if ! source "$CAU_LIBDIR/$_mod.sh"; then
|
||||
@@ -71,6 +71,10 @@ cau_log_open
|
||||
# all landed or none did; only our own bookkeeping is at risk here.
|
||||
_cau_interrupted() {
|
||||
cau_error "Update run interrupted ($1)"
|
||||
# Before anything else: a progress entry whose owner merely disappears is
|
||||
# reported by the desktop as an application crash, so a killed run would
|
||||
# leave a failure message behind on top of everything else.
|
||||
cau_progress_end failed "The update was stopped before it finished."
|
||||
cau_state_write last_result interrupted
|
||||
exit 130
|
||||
}
|
||||
@@ -121,7 +125,7 @@ fi
|
||||
# before the busy check, because otherwise every future run would defer on it
|
||||
# forever and the machine would quietly stop updating.
|
||||
if cau_recover_stale_lock; then
|
||||
cau_notify normal \
|
||||
cau_notify normal no \
|
||||
"Finishing an interrupted update" \
|
||||
"The last update was cut short, most likely because the machine was switched off. It is being finished now."
|
||||
fi
|
||||
@@ -132,7 +136,7 @@ CAU_SKIP_REASON=''
|
||||
if cau_package_manager_busy; then
|
||||
if cau_track_stale_lock; then
|
||||
cau_warn "pacman's database lock appears to be stale"
|
||||
cau_notify normal \
|
||||
cau_notify normal yes \
|
||||
"Package database locked" \
|
||||
"pacman's lock file looks left over from an interrupted update. Updates are paused until it is cleared."
|
||||
fi
|
||||
@@ -178,6 +182,16 @@ fi
|
||||
cau_info "Starting update run"
|
||||
failed=0
|
||||
|
||||
# Open the desktop's progress bar, told up front which steps this run will
|
||||
# perform. Only those count towards the bar, so a machine with no Flatpaks
|
||||
# does not sit at 85% for the last second of the run.
|
||||
progress_steps=(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
|
||||
failed=1
|
||||
fi
|
||||
@@ -196,6 +210,15 @@ fi
|
||||
|
||||
cau_pacman_cleanup
|
||||
|
||||
# The work is over; the bar goes away and the result takes over from here. No
|
||||
# label on a failure: the notification below carries that, and it stays up
|
||||
# until it is dismissed.
|
||||
if (( failed )); then
|
||||
cau_progress_end failed
|
||||
else
|
||||
cau_progress_end ok
|
||||
fi
|
||||
|
||||
pacnew="$(cau_pacman_pacnew_count)"
|
||||
if [[ $pacnew =~ ^[0-9]+$ ]] && (( pacnew > 0 )); then
|
||||
cau_info "$pacnew .pacnew file(s) present; left untouched"
|
||||
@@ -213,7 +236,7 @@ cau_state_write last_counts \
|
||||
if (( failed )); then
|
||||
cau_state_write last_result failed
|
||||
cau_error "Update run finished with errors"
|
||||
[[ $CFG_NOTIFY_ERROR == yes ]] && cau_notify_tagged run yes critical \
|
||||
[[ $CFG_NOTIFY_ERROR == yes ]] && cau_notify_tagged run yes critical yes \
|
||||
"Update failed" \
|
||||
"Something went wrong while updating. Run 'cachy-auto-update log' for details."
|
||||
else
|
||||
@@ -222,7 +245,8 @@ else
|
||||
cau_info "Update run finished successfully ($total item(s) updated)"
|
||||
|
||||
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
|
||||
|
||||
@@ -234,7 +258,7 @@ if [[ -n $CAU_PACMAN_HELD ]]; then
|
||||
cau_state_write held_back "$CAU_PACMAN_HELD"
|
||||
cau_warn "Held back: $CAU_PACMAN_HELD"
|
||||
if [[ $CAU_PACMAN_HELD != "$prev_held" && $CFG_NOTIFY_ERROR == yes ]]; then
|
||||
cau_notify normal "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." \
|
||||
"$CAU_PACMAN_HELD"
|
||||
fi
|
||||
@@ -242,12 +266,15 @@ else
|
||||
cau_state_clear held_back
|
||||
fi
|
||||
|
||||
# Recorded, deliberately not announced. The running kernel loses its module
|
||||
# tree the moment pacman unpacks the new one, so this turns true partway
|
||||
# through a run that still has AUR builds and Flatpaks ahead of it - and a
|
||||
# "restart recommended" bubble arriving then reads as an invitation to restart
|
||||
# while the update is still going. `cachy-auto-update status` and the menu say
|
||||
# so instead, where nobody is being interrupted mid-transaction.
|
||||
if cau_pacman_reboot_needed; then
|
||||
cau_state_write reboot_needed 1
|
||||
cau_info "A kernel update needs a restart"
|
||||
[[ $CFG_NOTIFY_REBOOT == yes ]] && cau_notify normal \
|
||||
"Restart recommended" \
|
||||
"A new kernel was installed. Please restart when it suits you."
|
||||
else
|
||||
cau_state_write reboot_needed 0
|
||||
fi
|
||||
|
||||
+1
-2
@@ -130,11 +130,10 @@ cau_config_load() {
|
||||
CFG_NOTIFY_START=no; cau_config_bool NotifyOnStart yes && CFG_NOTIFY_START=yes
|
||||
CFG_NOTIFY_SUCCESS=no; cau_config_bool NotifyOnSuccess yes && CFG_NOTIFY_SUCCESS=yes
|
||||
CFG_NOTIFY_ERROR=no; cau_config_bool NotifyOnError yes && CFG_NOTIFY_ERROR=yes
|
||||
CFG_NOTIFY_REBOOT=no; cau_config_bool NotifyReboot yes && CFG_NOTIFY_REBOOT=yes
|
||||
|
||||
CFG_INTERVAL="$(cau_config_get UpdateInterval 1d)"
|
||||
CFG_INTERVAL_SECONDS="$(cau_duration_to_seconds "$CFG_INTERVAL" 86400)"
|
||||
CFG_MIN_BATTERY="$(cau_config_int MinBatteryPercent 40)"
|
||||
CFG_MIN_BATTERY="$(cau_config_int MinBatteryPercent 30)"
|
||||
CFG_KEEP_OLD="$(cau_config_int KeepOldPackages 3)"
|
||||
CFG_AUR_HELPER="$(cau_config_get AURHelper auto)"
|
||||
CFG_IGNORE_PKG="$(cau_config_get IgnorePkg '')"
|
||||
|
||||
+1
-2
@@ -131,11 +131,10 @@ 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"
|
||||
"NotifyReboot|bool|yes|Notify when a restart is needed"
|
||||
"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|40|Minimum battery level (%)"
|
||||
"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"
|
||||
|
||||
+74
-19
@@ -15,30 +15,68 @@
|
||||
CAU_NOTIFY_ICON="system-software-update"
|
||||
CAU_NOTIFY_QUEUE_MAX=20
|
||||
|
||||
# cau_notify <urgency> <title-msgid> <body-msgid> [body printf args...]
|
||||
# cau_notify <urgency> <linger: yes|no> <title-msgid> <body-msgid> [args...]
|
||||
# Never fails: a machine without libnotify, or with nobody logged in, is a
|
||||
# normal state, not an error.
|
||||
cau_notify() {
|
||||
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
|
||||
# rather than stacking a second bubble beside it - that is what turns
|
||||
# "installing updates" into "system updated" in place instead of leaving two
|
||||
# messages that contradict each other.
|
||||
# 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" title="$4" body="$5"
|
||||
shift 5
|
||||
local tag="$1" queue="$2" urgency="$3" linger="$4" title="$5" body="$6"
|
||||
shift 6
|
||||
local -a args=("$@")
|
||||
local delivered=0 user uid locale t b prev newid
|
||||
|
||||
[[ $CFG_NOTIFICATIONS == yes ]] || return 0
|
||||
|
||||
# -1 is "whatever the server thinks", which is what a message nobody has to
|
||||
# act on wants; 0 is "until dismissed".
|
||||
local -a expiry=(--expire-time=-1)
|
||||
[[ $linger == yes ]] && expiry=(--expire-time=0)
|
||||
|
||||
while read -r user uid; do
|
||||
[[ -n $user ]] || continue
|
||||
cau_as_user "$user" "$uid" sh -c 'command -v notify-send >/dev/null' || continue
|
||||
@@ -47,17 +85,20 @@ cau_notify_tagged() {
|
||||
t="$(cau_msg_in "$locale" "$title")"
|
||||
b="$(cau_msg_in "$locale" "$body" "${args[@]}")"
|
||||
|
||||
prev=0
|
||||
if [[ -n $tag ]]; then
|
||||
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
|
||||
|
||||
if newid="$(cau_as_user "$user" "$uid" notify-send \
|
||||
--app-name="$CAU_PRETTY" \
|
||||
--icon="$CAU_NOTIFY_ICON" \
|
||||
--urgency="$urgency" \
|
||||
--print-id --replace-id="$prev" \
|
||||
"${expiry[@]}" \
|
||||
--print-id \
|
||||
-- "$t" "$b" 2>/dev/null)"
|
||||
then
|
||||
delivered=1
|
||||
@@ -70,10 +111,10 @@ cau_notify_tagged() {
|
||||
(( delivered )) && return 0
|
||||
[[ $queue == yes ]] || return 0
|
||||
|
||||
cau_notify_enqueue "$urgency" "$title" "$body" "${args[@]}"
|
||||
cau_notify_enqueue "$urgency" "$linger" "$title" "$body" "${args[@]}"
|
||||
}
|
||||
|
||||
# cau_notify_enqueue <urgency> <title-msgid> <body-msgid> [args...]
|
||||
# cau_notify_enqueue <urgency> <linger> <title-msgid> <body-msgid> [args...]
|
||||
# Tab-separated records, oldest first. The file is world-readable on purpose:
|
||||
# the login-time delivery runs unprivileged and only ever reads it.
|
||||
#
|
||||
@@ -81,13 +122,13 @@ cau_notify_tagged() {
|
||||
# progress by "last key seen", so two records sharing a key could make the
|
||||
# second one unreachable forever if a login landed between them.
|
||||
cau_notify_enqueue() {
|
||||
local urgency="$1" title="$2" body="$3"
|
||||
shift 3
|
||||
local urgency="$1" linger="$2" title="$3" body="$4"
|
||||
shift 4
|
||||
local record tmp
|
||||
|
||||
mkdir -p "$CAU_STATEDIR" 2>/dev/null || return 0
|
||||
|
||||
record="$(date +%s%N)"$'\t'"$urgency"$'\t'"$title"$'\t'"$body"
|
||||
record="$(date +%s%N)"$'\t'"$urgency"$'\t'"$linger"$'\t'"$title"$'\t'"$body"
|
||||
local arg
|
||||
for arg in "$@"; do
|
||||
record+=$'\t'"${arg//$'\t'/ }"
|
||||
@@ -109,8 +150,8 @@ cau_notify_enqueue() {
|
||||
# entry. State about what has already been seen lives in the user's own home,
|
||||
# so no write access to /var/lib is needed and each user is tracked separately.
|
||||
cau_notify_deliver_queue() {
|
||||
local seen_file seen ts urgency title body
|
||||
local -a args
|
||||
local seen_file seen ts urgency linger title body rest
|
||||
local -a args expiry
|
||||
|
||||
[[ -r $CAU_NOTIFY_QUEUE ]] || return 0
|
||||
cau_have notify-send || return 0
|
||||
@@ -123,20 +164,34 @@ cau_notify_deliver_queue() {
|
||||
[[ $seen =~ ^[0-9]+$ ]] || seen=0
|
||||
|
||||
local newest="$seen"
|
||||
while IFS=$'\t' read -r ts urgency title body rest; do
|
||||
while IFS=$'\t' read -r ts urgency linger title body rest; do
|
||||
[[ $ts =~ ^[0-9]+$ ]] || continue
|
||||
(( ts > seen )) || continue
|
||||
|
||||
# Records spooled before the linger field existed have the title where
|
||||
# the flag now sits. Shift them back rather than announcing an update
|
||||
# under the headline "yes".
|
||||
if [[ $linger != yes && $linger != no ]]; then
|
||||
rest="${body}${rest:+$'\t'}${rest:-}"
|
||||
body="$title"
|
||||
title="$linger"
|
||||
linger=no
|
||||
fi
|
||||
|
||||
# remaining tab-separated fields are the body's printf arguments
|
||||
args=()
|
||||
if [[ -n ${rest:-} ]]; then
|
||||
IFS=$'\t' read -r -a args <<< "$rest"
|
||||
fi
|
||||
|
||||
expiry=(--expire-time=-1)
|
||||
[[ $linger == yes ]] && expiry=(--expire-time=0)
|
||||
|
||||
notify-send \
|
||||
--app-name="$CAU_PRETTY" \
|
||||
--icon="$CAU_NOTIFY_ICON" \
|
||||
--urgency="${urgency:-normal}" \
|
||||
"${expiry[@]}" \
|
||||
-- "$(cau_msg "$title")" "$(cau_msg "$body" "${args[@]}")" 2>/dev/null || true
|
||||
|
||||
(( ts > newest )) && newest="$ts"
|
||||
|
||||
@@ -43,6 +43,8 @@ cau_appimage_update() {
|
||||
local user uid count rc=0
|
||||
local -a cmd
|
||||
|
||||
cau_progress_step appimage "AppImages"
|
||||
|
||||
while read -r user uid; do
|
||||
[[ -n $user ]] || continue
|
||||
|
||||
|
||||
@@ -126,6 +126,8 @@ cau_aur_update() {
|
||||
|
||||
cau_aur_ready || return 0
|
||||
|
||||
cau_progress_step aur "AUR packages"
|
||||
|
||||
pending="$(cau_aur_pending)"
|
||||
if (( pending == 0 )); then
|
||||
cau_info "No AUR updates pending"
|
||||
@@ -134,10 +136,15 @@ cau_aur_update() {
|
||||
fi
|
||||
|
||||
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)
|
||||
|
||||
if cau_run_logged cau_as_build_user "$CAU_AUR_HELPER" "${args[@]}"; then
|
||||
CAU_AUR_COUNT="$pending"
|
||||
cau_progress_item "$pending"
|
||||
cau_state_clear aur_failures
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -28,14 +28,18 @@ cau_flatpak_update() {
|
||||
|
||||
cau_have flatpak || return 0
|
||||
|
||||
cau_progress_step flatpak "Flatpaks"
|
||||
|
||||
# refresh appstream metadata first so remote-ls sees current versions
|
||||
cau_run_logged flatpak update --appstream --system --noninteractive || true
|
||||
|
||||
pending="$(cau_flatpak_pending_system)"
|
||||
if (( pending > 0 )); then
|
||||
cau_info "Updating $pending system Flatpak(s)"
|
||||
cau_progress_item 0 "$pending"
|
||||
if cau_run_logged flatpak update --system --noninteractive --assumeyes; then
|
||||
CAU_FLATPAK_COUNT=$(( CAU_FLATPAK_COUNT + pending ))
|
||||
cau_progress_item "$pending"
|
||||
else
|
||||
cau_warn "System Flatpak update failed"
|
||||
rc=1
|
||||
|
||||
+57
-3
@@ -27,6 +27,40 @@ cau_pacman_flags() {
|
||||
done
|
||||
}
|
||||
|
||||
# _cau_pacman_progress_watch <logfile>
|
||||
# Feeds the desktop's progress bar from pacman's own transaction counter.
|
||||
# --noprogressbar makes pacman print one "(120/260) upgrading foo" line per
|
||||
# package, and that is the only live measure of how far a transaction has got:
|
||||
# checkupdates knows the total beforehand, but nothing else knows the position.
|
||||
#
|
||||
# The other (n/m) sequences pacman prints - checking keys in keyring, checking
|
||||
# package integrity, loading package files - are deliberately not matched. Each
|
||||
# counts up to the same total, so following them would run the bar to the end
|
||||
# three times over before the first package was unpacked.
|
||||
#
|
||||
# Read from the log by polling 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 more here than the
|
||||
# second or so of latency it costs.
|
||||
_cau_pacman_progress_watch() {
|
||||
local log="$1" line last='' pkg
|
||||
|
||||
while :; do
|
||||
sleep 1
|
||||
|
||||
line="$(grep -aoE '^\([[:space:]]*[0-9]+/[0-9]+\) (upgrading|installing|reinstalling|downgrading|removing) [^[:space:]]+' \
|
||||
"$log" 2>/dev/null | tail -n1)"
|
||||
[[ -n $line && $line != "$last" ]] || continue
|
||||
last="$line"
|
||||
|
||||
[[ $line =~ ^\([[:space:]]*([0-9]+)/([0-9]+)\)[[:space:]]+[a-z]+[[:space:]]+(.+)$ ]] || continue
|
||||
|
||||
cau_progress_item "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}"
|
||||
pkg="${BASH_REMATCH[3]%...}"
|
||||
cau_progress_detail "Package" "$pkg"
|
||||
done
|
||||
}
|
||||
|
||||
# _cau_pacman_exec <logfile> <pacman args...>
|
||||
# Captures pacman's output for classification, and streams it as well when a
|
||||
# person is watching. Upgrading a few hundred packages takes minutes; without
|
||||
@@ -35,13 +69,28 @@ cau_pacman_flags() {
|
||||
_cau_pacman_exec() {
|
||||
local log="$1"
|
||||
shift
|
||||
local rc watcher=0
|
||||
|
||||
# Only worth a second process and a grep per second if a bar exists to feed.
|
||||
if cau_progress_active; then
|
||||
_cau_pacman_progress_watch "$log" &
|
||||
watcher=$!
|
||||
fi
|
||||
|
||||
if [[ -n $CAU_INTERACTIVE ]]; then
|
||||
pacman "$@" 2>&1 | tee "$log"
|
||||
return "${PIPESTATUS[0]}"
|
||||
rc="${PIPESTATUS[0]}"
|
||||
else
|
||||
pacman "$@" > "$log" 2>&1
|
||||
rc=$?
|
||||
fi
|
||||
|
||||
pacman "$@" > "$log" 2>&1
|
||||
if (( watcher )); then
|
||||
kill "$watcher" 2>/dev/null
|
||||
wait "$watcher" 2>/dev/null
|
||||
fi
|
||||
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
# cau_pacman_pending
|
||||
@@ -117,6 +166,8 @@ cau_pacman_update() {
|
||||
local log kind
|
||||
local -a flags
|
||||
|
||||
cau_progress_step repo "Repository packages"
|
||||
|
||||
if ! cau_pacman_pending; then
|
||||
cau_info "No repository updates pending"
|
||||
return 0
|
||||
@@ -126,6 +177,7 @@ cau_pacman_update() {
|
||||
CAU_PACMAN_COUNT="$(grep -c . <<< "$CAU_PACMAN_PENDING")"
|
||||
[[ $CAU_PACMAN_COUNT =~ ^[0-9]+$ ]] || CAU_PACMAN_COUNT=0
|
||||
cau_info "Updating $CAU_PACMAN_COUNT repository package(s)"
|
||||
cau_progress_item 0 "$CAU_PACMAN_COUNT"
|
||||
else
|
||||
# checkupdates is unavailable, so the list is unknown and pacman is
|
||||
# asked to work it out itself.
|
||||
@@ -141,7 +193,7 @@ cau_pacman_update() {
|
||||
# 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 \
|
||||
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}"
|
||||
@@ -258,6 +310,8 @@ cau_pacman_pacnew_count() {
|
||||
cau_pacman_cleanup() {
|
||||
local -a orphans
|
||||
|
||||
cau_progress_step cleanup "Cleaning up"
|
||||
|
||||
if [[ $CFG_REMOVE_ORPHANS == yes ]]; then
|
||||
mapfile -t orphans < <(pacman -Qtdq 2>/dev/null)
|
||||
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