Compare commits

..
23 Commits
Author SHA1 Message Date
Felitendo 776a39074d docs: show new ui as screenshots in the readme 2026-09-28 15:08:14 +02:00
Felitendo c5a4ffbe08 chore: 1.3.2 2026-09-28 14:40:06 +02:00
Felitendo 1078bd52ab fix: hide the progress bar when start notices are off
Fixes #2
2026-09-28 14:40:00 +02:00
Felitendo 9a471a005d chore: show feature requests before bug reports 2026-09-26 21:19:28 +02:00
Felitendo b78f1629ad chore: commit different topics separately 2026-09-25 09:30:38 +02:00
Felitendo 0f5e41cc5c chore: switch to ko-fi 2026-09-24 13:21:19 +02:00
Felitendo 1e02e0dfa8 chore: add issue templates 2026-09-24 12:39:49 +02:00
Felitendo 47d5862b5f docs: commit and push only after a local check 2026-09-24 08:42:01 +02:00
Felitendo f4b7a26dec chore: 1.3.1 2026-09-24 02:01:04 +02:00
Felitendo 7cd6eafafe docs: cut the readme down, fold the details 2026-09-24 02:00:35 +02:00
Felitendo 08ffaad00b fix: say 1 minute ago, not 1 minutes ago 2026-09-24 02:00:35 +02:00
Felitendo 1f0fb22d1c docs: show the menu as real screenshots 2026-09-24 02:00:35 +02:00
Felitendo b4038baf5b docs: add a changelog and the release notes flow 2026-09-24 02:00:35 +02:00
Felitendo d2b04f66ea docs: add commit rules to claude.md 2026-09-24 01:13:07 +02:00
Felitendo f1b7658e82 docs: coffee button and bug report link 2026-09-24 01:13:07 +02:00
Felitendo 6dd674c05f chore: switch funding to buy me a coffee 2026-09-24 01:13:07 +02:00
Felitendo b1e1cf0b54 docs: add icon and centered readme header 2026-09-21 14:58:44 +02:00
Felitendo c40dfe0238 docs: add claude.md and avoid dashes 2026-09-21 14:00:02 +02:00
Felitendo dbdbfe5cd5 chore: move to LoonixTools 2026-09-21 13:07:21 +02:00
Felitendo 368c741d99 docs: trim the readme to the essentials 2026-09-04 12:55:25 +02:00
Felitendo 162b30ba38 cachy-auto-update 1.3.0
The progress bar stops freezing on a count it made up, drops the steps that
have no work, and carries the run log's last lines under Details.
2026-08-20 19:57:40 +02:00
Felitendo c8666da714 Show the run log's last lines under Details, live
The percentage says an update is alive. It does not say what it is alive
doing, and for the longest stretch of a run there was no way to find out:
an AUR package compiling for a quarter of an hour talks constantly, and
every word of it went into a file nobody was looking at.

So the run log's last five lines now travel with the progress entry and
sit under Details, refreshed every two seconds for as long as the update
lasts. Polled rather than followed, for the same reason the pacman
watcher polls: only the last few lines are ever displayed, so every line
in between would be work nobody sees, and re-reading the tail whole makes
truncation and rotation of the log a non-event.

The job model behind this interface carries exactly two description
fields - descriptionValue1 and 2, there is no third - so one stays with
the package being worked on and the other takes the tail. Newlines inside
a field do render, which is what lets five lines share one of them. They
travel tab separated because the protocol is one instruction per line,
and a tab inside a log line becomes a space first: either renders as
whitespace, but a line split in half renders as nonsense. Five lines
clipped to 120 columns also keeps an instruction well inside PIPE_BUF,
which is what makes the write atomic against the runner sending its own
progress down the same pipe.
2026-08-20 19:55:43 +02:00
Felitendo f9cd8a0ace Keep the progress bar moving for the whole run
Three stretches of a run had no way to report anything, and the bar
handled each of them badly.

pacman prints nothing at all between "starting full system upgrade" and
the transaction it eventually prepares. On a 161-package backlog that
silence ran to three minutes and nineteen seconds, and the bar spent all
of it frozen on "0 of 161" - a counter seeded from checkupdates before
there was anything to count. Working out the upgrade is now a step of its
own, with a label and deliberately no item count, because the number was
the part that was lying.

A step that turns out to have no work is dropped from the bar instead of
being handed its share for nothing. Packages already in the cache are
never announced by pacman, so a run that only has to unpack used to jump
thirty points the moment unpacking started; the same went for AUR with
nothing pending and for machines with no Gear Lever or no Flatpaks.

pacman's output is line-buffered through stdbuf. Writing to a log rather
than a terminal, libc released it in 4KB blocks - around a hundred and
sixty "upgrading foo..." lines at a time - so the bar sat still and then
leapt to the end of the step in one poll.

What is left is work whose length genuinely cannot be known: resolving a
transaction, and an AUR helper compiling for a quarter of an hour. Those
now creep along a curve that approaches the end of their step without
reaching it. The item counter stays put throughout - it is the field that
would be lying if it moved - and any real report overtakes the creep. A
bar that has not moved since it appeared is read as a hang, and somebody
who reads it that way reaches for the power button mid-update.

The bar is also monotonic now. Dropping a step rescales the run, and the
conflict-recovery loop restarts pacman and its tally from the top; both
are honest, neither is a reason to show a bar that retreats.
2026-08-20 19:45:56 +02:00
31 changed files with 1472 additions and 485 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
ko_fi: felitendo
@@ -0,0 +1,35 @@
name: 💡 Feature request
description: An idea to make cachy-auto-update better.
labels:
- enhancement
body:
- type: checkboxes
id: checks
attributes:
label: Before you start
options:
- label: I searched the issues and this is not requested yet.
required: true
- type: textarea
id: problem
attributes:
label: What problem would this solve?
description: What are you trying to do, and what gets in the way?
validations:
required: true
- type: textarea
id: idea
attributes:
label: What would you like?
description: How it could work. A rough idea is fine.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Other ways you thought of
- type: textarea
id: more
attributes:
label: Anything else?
description: Mockups, screenshots, or other tools that do it well.
+91
View File
@@ -0,0 +1,91 @@
name: 🐛 Bug report
description: Something does not work as it should.
labels:
- bug
body:
- type: markdown
attributes:
value: Thanks for taking the time! The more you fill in, the faster it can be fixed.
- type: checkboxes
id: checks
attributes:
label: Before you start
options:
- label: I searched the issues and this is not reported yet.
required: true
- label: I use the latest version.
required: true
- type: textarea
id: what
attributes:
label: What happened?
description: What did you do, and what went wrong?
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
placeholder: |
1.
2.
3.
- type: textarea
id: expected
attributes:
label: What did you expect?
validations:
required: true
- type: dropdown
id: part
attributes:
label: What is affected?
multiple: true
options:
- Packages (pacman)
- AUR
- Flatpak
- AppImages
- When it runs or waits
- Notifications
- The menu
- Other
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: The output of `cachy-auto-update --version`.
placeholder: cachy-auto-update 1.3.1
validations:
required: true
- type: dropdown
id: install
attributes:
label: How did you install it?
options:
- AUR
- Built from source
- Other
validations:
required: true
- type: input
id: system
attributes:
label: System
description: Distribution, desktop, and paru or yay.
placeholder: CachyOS, KDE Plasma 6.4, paru
validations:
required: true
- type: textarea
id: logs
attributes:
label: Status and logs
description: The output of `cachy-auto-update status` and `cachy-auto-update log`. If it did not run at all, also `journalctl -u cachy-auto-update`.
render: shell
- type: textarea
id: more
attributes:
label: Anything else?
description: Screenshots, videos or anything else that could help.
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env bash
#
# Builds the GitHub release notes for a tag, the way big projects such as
# Immich lay them out: the hand-written entry from CHANGELOG.md (a welcome,
# the highlights), a support section, then every commit since the last
# release, sorted by kind, and a link to the full changelog.
#
# .github/release-notes.sh v1.2.0 the notes
# .github/release-notes.sh --title v1.2.0 the title (checks the entry exists)
#
# Commit authors come from the GitHub API through gh. Without it the list
# goes without them.
set -euo pipefail
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
title=0
if [[ ${1:-} == --title ]]; then
title=1
shift
fi
tag="${1:?usage: release-notes.sh [--title] vX.Y.Z}"
if ! grep -qx "## $tag" CHANGELOG.md; then
echo "CHANGELOG.md has no entry for $tag. Add \"## $tag\" first." >&2
exit 1
fi
if (( title )); then
printf '%s\n' "$tag"
exit 0
fi
repo="${GITHUB_REPOSITORY:-$(git remote get-url origin | sed -E 's#^.*github\.com[:/]##; s#\.git$##')}"
name="${repo#*/}"
# The entry: up to the next one, without the date line (GitHub shows the
# date), one heading level up, outside code blocks.
entry="$(awk -v h="## $tag" '
$0 == h { on = 1; next }
on && /^## / { exit }
!on { next }
/^```/ { code = !code }
!code && /^_[0-9]{4}-[0-9]{2}-[0-9]{2}_$/ { next }
!code && /^###/ { sub(/^#/, "") }
{ print }
' CHANGELOG.md | sed -e '/./,$!d')"
# The commits: from the last release, or from the start. A tag that does not
# exist yet is a preview of what HEAD would become.
if git rev-parse -q --verify "refs/tags/$tag" > /dev/null; then
target="$tag"
prev="$(git describe --tags --abbrev=0 --match 'v[0-9]*' "$tag^" 2>/dev/null || true)"
else
target="$(git rev-parse HEAD)"
prev="$(git describe --tags --abbrev=0 --match 'v[0-9]*' HEAD 2>/dev/null || true)"
fi
declare -A login=()
if command -v gh > /dev/null; then
if [[ -n $prev ]]; then
api=(api "repos/$repo/compare/$prev...$target" --jq '.commits[] | [.sha, (.author.login // "")] | @tsv')
else
api=(api --paginate "repos/$repo/commits?sha=$target&per_page=100" --jq '.[] | [.sha, (.author.login // "")] | @tsv')
fi
while IFS=$'\t' read -r sha who; do
[[ -n $who ]] && login[$sha]="$who"
done < <(gh "${api[@]}" 2>/dev/null || true)
fi
declare -A list=()
count_maint=0
while IFS=$'\t' read -r sha subject body; do
# Version bumps say nothing a reader needs.
[[ $subject =~ ^(chore:\ )?($name\ )?v?[0-9]+\.[0-9]+\.[0-9]+$ ]] && continue
case "$subject" in
*!:*) kind=breaking ;;
feat:* | feat\(*) kind=feat ;;
fix:* | fix\(*) kind=fix ;;
perf:* | perf\(*) kind=enh ;;
docs:* | docs\(*) kind=docs ;;
chore* | ci:* | ci\(* | build* | refactor* | test* | style*) kind=maint ;;
# Older commits without a prefix, sorted by what they say.
*README* | *readme* | *Readme*) kind=docs ;;
Add\ * | Put\ * | Introduce\ *) kind=feat ;;
Fix\ * | Repair\ * | Recover\ * | Stop\ *) kind=fix ;;
*\ *) kind=enh ;;
*) kind=maint ;;
esac
[[ $body == *"BREAKING CHANGE"* ]] && kind=breaking
[[ $subject == "Initial commit" ]] && kind=maint
line="* $subject"
[[ -n ${login[$sha]:-} ]] && line+=" by @${login[$sha]}"
line+=" in https://github.com/$repo/commit/$sha"
list[$kind]+="$line"$'\n'
[[ $kind == maint ]] && count_maint=$((count_maint + 1))
done < <(git log --reverse --no-merges --format='%H%x09%s%x09%b%x1e' "${prev:+$prev..}$target" | tr '\n\036' ' \n' | sed 's/^ //')
changes=''
for pair in "breaking:🚨 Breaking Changes" "feat:🚀 Features" "enh:🌟 Enhancements" "fix:🐛 Bug fixes" "docs:📚 Documentation"; do
kind="${pair%%:*}"
[[ -n ${list[$kind]:-} ]] || continue
changes+="### ${pair#*:}"$'\n'"${list[$kind]}"
done
if [[ -n ${list[maint]:-} ]]; then
changes+=$'\n'"<details><summary>🧰 Maintenance ($count_maint)</summary>"$'\n\n'"${list[maint]}"$'\n'"</details>"$'\n'
fi
if [[ -n $prev ]]; then
full="**Full Changelog**: https://github.com/$repo/compare/$prev...$tag"
else
full="**Full Changelog**: https://github.com/$repo/commits/$tag"
fi
# A release with highlights is a big one: it gets a heading and the support
# section, as in Immich. A patch is a sentence or two and the list.
if grep -q '^## Highlights$' <<< "$entry"; then
cat <<EOF
# 🚀 $name $tag
$entry
## ☕ Support $name
If $name is useful to you, you can buy me a coffee. It keeps these tools going. Found a bug or have an idea? Tell me in the [issues](https://github.com/$repo/issues).
<a href="https://ko-fi.com/felitendo"><img src="https://storage.ko-fi.com/cdn/kofi5.png?v=6" alt="Buy me a coffee on Ko-fi" height="48"></a>
----
EOF
else
printf '%s\n\n' "$entry"
fi
printf "## What's Changed\n%s\n\n%s\n" "$changes" "$full"
+148
View File
@@ -0,0 +1,148 @@
# Changelog
What each release brings, newest first. The [GitHub releases](https://github.com/LoonixTools/cachy-auto-update/releases) add every commit that went into it.
## v1.3.2
_2026-09-28_
A small patch. With **Notify when an update starts** turned off, the progress bar no longer pops up when an update starts either.
## v1.3.1
_2026-09-24_
A small patch. The menu now says "1 minute ago" instead of "1 minutes ago".
## v1.3.0
_2026-08-20_
Welcome to cachy-auto-update `v1.3.0`! This release is all about the progress bar. It now moves through the whole run, and you can finally see what an update is doing while it works.
### Highlights
- A progress bar that never stands still
- Live log under Details
- Steps with nothing to do are skipped
## v1.2.2
_2026-08-14_
A small patch for the progress bar. It now counts downloads too, and every step says clearly that an update is running, for example **Updates werden heruntergeladen** instead of just "Paketquellen".
## v1.2.1
_2026-08-14_
A quick fix for the new progress bar, which stayed at "0 of 218 items" for the whole run in `v1.2.0`. It now counts every package.
`cachy-auto-update enable` can also switch off CachyOS's **Reboot recommended!** popup, which shows up in the middle of an update. It only asks. To undo it, delete the link in `/etc/pacman.d/hooks`.
## v1.2.0
_2026-08-14_
Welcome to cachy-auto-update `v1.2.0`! Updates now show up on your desktop with a real progress bar, and the notifications around them finally behave.
### 🚨 Breaking changes
- The `NotifyReboot` setting is gone. Its "restart recommended" notice came while the update was still running, which invited a restart halfway through. `cachy-auto-update status` still tells you when a restart is due.
### Highlights
- A progress bar on the desktop (needs `python-gobject`)
- Notifications that stay only when they should
## v1.1.2
_2026-08-08_
Better defaults. The minimum battery level is now 30 % instead of 40 %, and the prompt to silence cachy-update's own "N updates available" notification now defaults to yes. German users can answer it with `j` now, too.
## v1.1.1
_2026-08-08_
The settings screen now reacts instantly. Moving the cursor went from 435 ms to 7.5 ms per key press.
## v1.1.0
_2026-08-08_
Welcome to cachy-auto-update `v1.1.0`! Every option can now be changed from the menu, so nothing needs a text editor any more.
### Highlights
- A settings screen
## v1.0.9
_2026-08-08_
cachy-auto-update now says so before an update starts, and asks you to leave the computer on (`NotifyOnStart`, on by default). The result then replaces that message instead of showing up next to it.
## v1.0.8
_2026-08-08_
An important fix for machines nobody watches. A power cut during an update left pacman's lock file behind, and every later run gave up because of it. A lock from before the last boot is now removed and the update runs again. A lock from the current boot is left alone.
## v1.0.7
_2026-08-08_
The package cache is now trimmed by default. It keeps the last three versions of each package (`KeepOldPackages=3`, like Arch), so it no longer grows to 23 GB. Unused Flatpak runtimes moved to `RemoveOrphans`, which stays off, and the log shows how much space a trim freed.
## v1.0.6
_2026-08-08_
A run started by hand now shows what pacman, the AUR helper and Flatpak are doing, while timer runs stay quiet. A run that gets stopped now shows as "interrupted" in the menu.
## v1.0.5
_2026-08-08_
One package that cannot be updated no longer holds back all the others. It is held back for this run, and everything else updates. On one machine, 213 updates were stuck behind a single package. Held-back packages are shown in the menu and in `cachy-auto-update status`.
## v1.0.4
_2026-08-08_
Switching automatic updates or notifications on or off no longer asks you to press a key. The menu just redraws with the new state.
## v1.0.3
_2026-08-08_
The menu now acts on a single key press, no Enter needed. Enter and the arrow keys can no longer close it by accident.
## v1.0.2
_2026-08-08_
`cachy-auto-update --version` now shows the right version. `v1.0.1` still called itself 1.0.0.
## v1.0.1
_2026-08-08_
The first fixes. pacman's errors were misread on systems that are not in English, "Update now" closed the menu instead of going back to it, and the log was unclear when `checkupdates` is missing.
## v1.0.0
_2026-08-08_
Welcome to the very first release of cachy-auto-update! It keeps CachyOS up to date on its own: pacman, AUR, Flatpak and AppImages, installed in the background with no password prompt and nothing for you to do.
<p align="center">
<img width="620" alt="The cachy-auto-update menu in Konsole" src="https://raw.githubusercontent.com/LoonixTools/cachy-auto-update/a56e804c41923d7531d578728f43d152ec275caa/res/screenshots/menu.png">
</p>
### Highlights
- Updates at the right moment
- Speaks up only when it needs you
- No stored password
+79
View File
@@ -0,0 +1,79 @@
# CLAUDE.md
## Style
- Keep everything short: replies, explanations, comments, docs.
- Use simple English: short sentences, common words.
- Avoid em dashes (—). Do not just swap them for "-" either. Rewrite the sentence instead, for
example with a comma, a colon, brackets or two sentences.
## Commits
- English only.
- Conventional Commits prefix: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `ci:`, `build:`.
- Subject as short as possible. Imperative, lowercase, no trailing period.
- Body only when something genuinely cannot be inferred from the diff.
- Never add `Co-Authored-By`, "Generated with" or any other AI attribution to commits or PR descriptions.
- Do not commit or push before I have checked the changes locally and said they are fine. Then
commit and push. Several attempts at the same thing make one commit, and attempts that did not
work make none. Different things done in one session get a commit each. This also goes for
releases and tags.
## Releases
Releases look like the ones of big projects such as Immich. `.github/release-notes.sh <tag>` builds
the notes: the entry from `CHANGELOG.md` (welcome and highlights), a support section, every commit
since the last release sorted by its prefix (`feat`, `fix`, `docs`, ...) with author and link, and
the full changelog link. So commit subjects end up in public: keep them clear.
1. Read `git log <last tag>..HEAD` and pick the version: only fixes → patch, something new → minor,
something that breaks or needs the user to act → major.
2. Bump the version and add the entry at the top of `CHANGELOG.md`, in one commit (`chore: 1.4.0`).
3. Push, tag and create the release:
```bash
git tag v1.4.0 && git push origin main v1.4.0
gh release create v1.4.0 --title v1.4.0 --notes "$(.github/release-notes.sh v1.4.0)"
```
4. PKGBUILDS picks up the new release for the AUR on its own.
A minor or major release (has `### Highlights`, gets a heading and the support section):
```markdown
## v1.4.0
_2026-09-24_
Welcome to cachy-auto-update `v1.4.0`! One or two sentences on what this release is about.
<p align="center">
<img width="480" alt="What the picture shows" src="https://raw.githubusercontent.com/LoonixTools/cachy-auto-update/v1.4.0/<path>">
</p>
### 🚨 Breaking changes
- Only if there are any: what changed, and what the user has to do.
### Highlights
- First highlight, a few words
- Second highlight
```
A picture under the welcome is optional. To show the menu or another screen of the program, use a
real screenshot of it running in Konsole, in English. Never a text copy of the screen. The same goes
for the README. The highlights stay a plain list: no heading or text per highlight, the list of
commits explains the rest.
A patch release is just a sentence or two, for example: "A small patch. The menu no longer closes
when you press Enter." The list of commits follows on its own.
- Write for users: what they notice, not how the code does it. Friendly and simple.
- Commands and settings they type go in backticks, buttons and labels in bold.
- To change an old release: edit its entry, commit, then
`gh release edit <tag> --title <tag> --notes "$(.github/release-notes.sh <tag>)"`.
## README
- New UI (a window, a screen, a menu, a setting, a notification) gets a screenshot in the README,
next to the text about it. Like for releases: a real screenshot of it running, in English.
- When a screen changes, take its screenshot again. The README never shows an old one.
+6 -6
View File
@@ -1,4 +1,4 @@
# cachy-auto-update - build and install # cachy-auto-update: build and install
# #
# Everything here is plain shell; "building" only means compiling the gettext # Everything here is plain shell; "building" only means compiling the gettext
# catalogs and rendering the man page. Both are optional in the sense that the # catalogs and rendering the man page. Both are optional in the sense that the
@@ -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.2.2 VERSION ?= 1.3.2
PREFIX ?= /usr PREFIX ?= /usr
DESTDIR ?= DESTDIR ?=
@@ -43,14 +43,14 @@ po/%.mo: po/%.po
ifdef MSGFMT ifdef MSGFMT
$(MSGFMT) --check --output-file=$@ $< $(MSGFMT) --check --output-file=$@ $<
else else
@echo "msgfmt not found - skipping $@" @echo "msgfmt not found, skipping $@"
endif endif
$(MANPAGE): doc/cachy-auto-update.1.scd $(MANPAGE): doc/cachy-auto-update.1.scd
ifdef SCDOC ifdef SCDOC
$(SCDOC) < $< > $@ $(SCDOC) < $< > $@
else else
@echo "scdoc not found - skipping $@" @echo "scdoc not found, skipping $@"
endif endif
# Syntax-check every shell file, and run shellcheck when it is available. # Syntax-check every shell file, and run shellcheck when it is available.
@@ -62,14 +62,14 @@ check:
shellcheck -x -e SC1090,SC1091 src/cachy-auto-update src/cachy-auto-update-run $(LIBS); \ shellcheck -x -e SC1090,SC1091 src/cachy-auto-update src/cachy-auto-update-run $(LIBS); \
echo "ok shellcheck"; \ echo "ok shellcheck"; \
else \ else \
echo "shellcheck not found - skipped"; \ echo "shellcheck not found, skipped"; \
fi fi
@if command -v python3 >/dev/null 2>&1; then \ @if command -v python3 >/dev/null 2>&1; then \
python3 -m py_compile src/cachy-auto-update-progress \ python3 -m py_compile src/cachy-auto-update-progress \
&& echo "ok src/cachy-auto-update-progress"; \ && echo "ok src/cachy-auto-update-progress"; \
rm -rf src/__pycache__; \ rm -rf src/__pycache__; \
else \ else \
echo "python3 not found - skipped"; \ echo "python3 not found, skipped"; \
fi 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"; \
+69 -271
View File
@@ -1,45 +1,24 @@
# cachy-auto-update <p align="center">
<img width="200" src="res/cachy-auto-update.svg" alt="cachy-auto-update">
</p>
Unattended background updates for CachyOS — no password prompt, no terminal, <h1 align="center">cachy-auto-update</h1>
nothing to remember.
Built for the machine you set up for somebody else and would rather not have to <h3 align="center">Unattended background updates for CachyOS.</h3>
maintain: it updates pacman packages, AUR packages, Flatpaks and AppImages by
itself, stays out of the way while they are gaming or on battery, never touches
the package database while they are using `pacman` by hand, and only speaks up
when something actually needs a human.
```bash <p align="center">
sudo cachy-auto-update pacman, AUR, Flatpak and AppImages. In the background, with no password prompt.
``` </p>
That opens a menu with the two switches there are: <h5 align="center">
<a href="#install">Install</a> |
<a href="#how-to-use">How to use</a> |
<a href="https://github.com/LoonixTools/cachy-auto-update/issues">Report a bug</a>
</h5>
``` <p align="center">
CachyOS Auto-Update <a href="https://ko-fi.com/felitendo"><img src="https://storage.ko-fi.com/cdn/kofi5.png?v=6" alt="Buy me a coffee on Ko-fi" height="48"></a>
</p>
Automatic updates ON
Notifications ON
Last check 3 hours ago
Last successful update Sat 08 Aug 2026 04:12:03 CEST (23 packages)
Next scheduled run Sat 08 Aug 2026 05:00:00 CEST
[1] Toggle automatic updates
[2] Toggle notifications
[3] Update now
[4] Show log
[5] Show current conditions
[6] Settings
[q] Quit
```
Everything is configurable from **[6] Settings** — a cursor list covering all
eighteen options, so nothing needs a text editor. Arrow keys select, Space or
Right changes a value, `q` goes back; changes are written immediately.
The interface is fully translated; on a German system everything above appears
in German.
## Install ## Install
@@ -48,238 +27,75 @@ paru -S cachy-auto-update
sudo cachy-auto-update enable sudo cachy-auto-update enable
``` ```
Updates are **off** until you enable them — a freshly installed package has no ## How to use
business rebuilding somebody's system before being asked.
## What it updates ```bash
sudo cachy-auto-update
```
<p align="center">
<img src="res/screenshots/menu.png" alt="The cachy-auto-update menu in Konsole: automatic updates and notifications on" width="620">
</p>
Press **1** to turn automatic updates on or off. **6** has all the settings.
## When does it update?
| | | | | |
|---|---| |---|---|
| Repository packages | `pacman -Syu`, after `checkupdates` confirms there is work | | Battery below 30 % | ⏸️ Waits (on power it runs) |
| AUR | `paru` or `yay`, whichever is installed | | A game is running | ⏸️ Waits |
| Flatpak | system and per-user installations | | pacman, yay or paru is running | ⏸️ Waits |
| AppImages | via [Gear Lever](https://github.com/mijorus/gearlever), if installed | | Shutdown or suspend during an update | 🚫 Blocked until it is done |
| Restart after an update | ❌ Never on its own, you get a notification |
It also trims the pacman package cache after each run (`paccache`, keeping the ## More
3 most recent versions), because otherwise `/var/cache/pacman/pkg` grows
forever — tens of gigabytes on a machine with a few large packages. Set
`KeepOldPackages=1` if disk space matters more than the ability to downgrade.
`-git`/`-devel` AUR packages and orphan removal exist as options but are off by <details>
default. Orphan removal deletes installed software, and "orphaned" only means <summary>What it updates</summary>
nothing else depends on it — which is also true of something installed
deliberately.
## When it holds back | | |
|---|---|
| Packages | `pacman -Syu` |
| AUR | `paru` or `yay` |
| Flatpak | system and user installs |
| AppImages | through [Gear Lever](https://github.com/mijorus/gearlever) |
A run is postponed — and retried an hour later — when: </details>
- the battery is below 30 % (ignored on mains power; desktops without a battery <details>
are never affected), <summary>Safety</summary>
- a game is running: GameMode, a known game process, or anything holding a
blocking idle inhibitor,
- pacman's database is locked, or `pacman`/`yay`/`paru`/`pamac` is running.
`cachy-auto-update status` prints every one of these individually, which is the - A progress bar shows the step and the package.
fastest way to find out why nothing is happening. - On Btrfs with snapper (the CachyOS default) every update gets a snapshot before and after.
- A lock file left by a power cut is removed on the next run.
- Package replacements are handled on their own. File conflicts are never overwritten.
- A signature error refreshes the keyring once and tries again.
The machine is **never** restarted on its own. A kernel update that needs a </details>
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 <details>
<summary>Logs</summary>
The obvious way to automate `yay`/`paru` is to store the user's password
somewhere. This does not do that, and deliberately so: anything the daemon can
decrypt is exactly what an attacker who reaches the daemon already has, so the
encryption would be decoration.
Instead:
- The updater is a **system service running as root**, so `pacman` needs no
escalation at all.
- `makepkg`, `paru` and `yay` refuse to run as root, so the AUR step drops to a
dedicated **locked system account** (`cachy-auto-update`, no password,
`/usr/bin/nologin`, its own home under `/var/lib`). Only root can become it.
- That account gets one line in `/etc/sudoers.d/cachy-auto-update` allowing it
to call `/usr/bin/pacman` without a password — which is what lets the helper
install what it built.
No user password is stored, encrypted or otherwise.
If you would rather not have that sudoers rule on the machine, set
`UpdateAUR=no` in the config; everything else keeps working, and the rule
becomes inert.
## Coexisting with manual package management
Before touching anything, the updater checks for `/var/lib/pacman/db.lck` and
for a running `pacman`, `yay`, `paru`, `pamac`, `pikaur`, `octopi` or `makepkg`,
and postpones if it finds one. On a day with no pending repository updates the
real pacman lock is never taken at all, because `checkupdates` works against a
private temporary database.
The reverse direction has an honest limit: if you start `pacman` *while* an
update is already running, you will get the usual "unable to lock database"
message. Nothing outside pacman can prevent that. What the updater does do is
keep the window short, run at low priority, and hold a `systemd-inhibit` lock so
a suspend or shutdown cannot land in the middle of a transaction.
A leftover `db.lck` from a crashed transaction is never deleted automatically —
guessing wrong there corrupts a live transaction. After it has been seen
unheld on several consecutive runs, you get a notification instead.
## What if the machine is switched off mid-update
Three layers, in order of how much they can actually promise:
**A notification goes out before the transaction starts** — "Installing
updates, please leave the computer switched on until this is done" — and is
withdrawn again when the result arrives, so it costs one bubble rather than
two. This exists because of what the next paragraph does *not* do.
**A progress bar sits in the notification area for the whole run**, the same
one Dolphin puts there while it copies files: which step is running, which
package is being unpacked, how far along the whole thing is. A twenty-minute
run that shows nothing looks indistinguishable from a hung one, and that is
what gets a machine switched off in the middle of a transaction. See
[The progress bar](#the-progress-bar).
**Suspend and a normal shutdown are blocked.** The run holds a
`systemd-inhibit --what=sleep:shutdown --mode=block` lock, so closing the lid or
picking "Shut down" cannot interrupt a transaction. Be aware of what that looks
like, though: logind refuses the request and requires the polkit action
`org.freedesktop.login1.power-off-ignore-inhibit`, which is `auth_admin_keep`.
The desktop therefore answers a shutdown attempt with an **administrator
password prompt** reading *"Power off the system while an application is
inhibiting this"* — a string systemd ships untranslated, and one that never
mentions updates. No KDE dialog explains the situation. Only `systemctl
poweroff` in a terminal names the reason. That prompt is exactly why the
notification above is on by default.
**A hard power-off cannot be prevented by anything.** Holding the power button
or pulling the plug cuts power in firmware. What limits the damage is that
pacman's commit phase is short (about a minute even for a 200-package upgrade)
and that most of a run is downloading, where an interruption costs nothing but
a partial file.
**The next run repairs it.** A `db.lck` left behind is detected and removed —
but only when it is *provably* dead, meaning it is older than the current boot,
so no process that could hold it still exists. The interrupted upgrade is then
simply run again; pacman reinstalls anything that was caught half-written. A
lock that is merely unheld within the same boot is never removed, only
reported, because there the guess could be wrong.
This last part matters more than it sounds: without it, a single power cut
during an update would leave a lock file that makes every future run defer,
and the machine would stop updating silently and permanently.
On a Btrfs system with `snapper` and `snap-pac` — the CachyOS default — every
pacman transaction is bracketed by a pre and post snapshot, so a genuinely
broken upgrade can still be rolled back with `snapper rollback`.
## How long notifications stay
A message that means the machine still needs you — an update failed, the
package database is locked, packages had to be held back — **stays until you
dismiss it**. That kind of message is only worth sending if it is still there
when you come back to the machine.
Everything else times out on its own, a successful update included. Nothing
should have to be clicked away for having gone right.
Set per message rather than left to the notification daemon. Daemons do keep
critical-urgency messages up and the spec asks them to, but that is a *should*,
it says nothing about the normal-urgency messages here that still need somebody
to act, and urgency separately controls sound and do-not-disturb bypass — a
different question. Queued messages delivered at the next login keep the same
distinction.
## The progress bar
While a run is working, the notification area carries a live entry — headline,
item count, percentage, and the package currently being unpacked under
*Details*. It is not a notification but a **job**, the same mechanism Dolphin
uses for file copies, which is what gets you a bar rather than a line of text.
Two things about how it is put together:
- The desktop ties a job to the D-Bus connection that asked for it, and
withdraws the job the moment that connection closes. One-shot bus clients —
`gdbus`, `busctl`, `dbus-send` — therefore cannot drive one at all, since
every invocation is a fresh connection that closes immediately. So a small
helper (`cachy-auto-update-progress`) runs inside each graphical session for
the length of the update, holding the connection open and taking instructions
on stdin. It needs **python-gobject**; without it there is simply no bar and
nothing else changes.
- Downloading and unpacking are two separate steps on the bar. On a domestic
line the download is the longer of the two, and calling the whole thing
"installing" leaves the bar sitting at 4% for six minutes, which reads as a
hang rather than as progress.
- Neither phase carries a counter on an unattended run, so both are counted a
line at a time — `foo-1.2-1-x86_64 downloading...` and `upgrading foo...`.
The database sync just before prints the same shape (` core downloading...`)
with the suffix that would give it away already stripped, so counting starts
only after pacman's `:: Retrieving packages...` header. pacman's other
`(n/m)` sequences — checking keys, package integrity, loading files — each
count to the same total, so only the transaction verbs are followed;
otherwise the bar would reach the end three times before the first package
was unpacked.
This is Plasma's job interface. On a desktop that does not implement it the
helper exits quietly and the ordinary notifications carry on as before.
## Configuration
`/etc/cachy-auto-update/cachy-auto-update.conf`, one `Key=Value` per line, every
option documented in the file. The file is parsed rather than sourced, so a
stray line cannot turn into code executed by root.
```ini
Enabled=yes
Notifications=yes
UpdateInterval=1d
MinBatteryPercent=40
SkipWhenGaming=yes
UpdateAUR=yes
UpdateFlatpak=yes
UpdateAppImages=yes
AutoResolveConflicts=yes
IgnorePkg=
```
## How package conflicts are handled
`pacman -Syu --noconfirm` already answers "Replace X with Y?" affirmatively, so
ordinary replacements happen silently — which is the point.
What `--noconfirm` declines is `:: X and Y are in conflict. Remove Y? [y/N]`,
and that aborts the whole transaction. With `AutoResolveConflicts=yes` (the
default) the transaction is retried once with that question answered too, and
everything removed is written to the log.
Two things are deliberately *not* automated:
- **File conflicts** (`exists in filesystem`) — forcing `--overwrite` could
silently destroy something that was put there on purpose.
- **Reboots** — `status` tells you one is due, never a surprise restart.
Signature failures trigger one keyring refresh and one retry, since a stale
keyring blocks everything else until it is fixed.
## Logs
```bash ```bash
cachy-auto-update log # the last run in full cachy-auto-update log # last run
cachy-auto-update log -a # the rolling log cachy-auto-update log -a # all runs
journalctl -u cachy-auto-update journalctl -u cachy-auto-update
``` ```
Both work without root. </details>
## Building from source <details>
<summary>cachy-update</summary>
[cachy-update](https://github.com/CachyOS/cachy-update) tells you about updates, you install them.
This installs them for you. Both work side by side, and `enable` offers to silence
cachy-update's notification.
</details>
<details>
<summary>Build from source</summary>
```bash ```bash
make && sudo make install make && sudo make install
@@ -287,26 +103,8 @@ sudo systemd-sysusers && sudo systemd-tmpfiles --create
sudo cachy-auto-update enable sudo cachy-auto-update enable
``` ```
`make check` runs `bash -n` over every shell file, `py_compile` over the Optional: `pacman-contrib`, an AUR helper, `flatpak`, Gear Lever, `libnotify`, `python-gobject`.
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 </details>
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
[cachy-update](https://github.com/CachyOS/cachy-update) is CachyOS's interactive
updater; every one of its steps sits behind a prompt and its timer only ever
*checks* for updates and notifies. This tool is the unattended counterpart and
reimplements the same command sequence non-interactively, adding the battery,
gaming and lock awareness that unattended operation needs.
The two coexist. `cachy-auto-update enable` offers once to switch off
cachy-update's own "N updates available" notification, since it becomes noise
when updates install themselves.
## License
GPL-3.0-or-later. GPL-3.0-or-later.
+45 -26
View File
@@ -15,7 +15,7 @@ think about it: repository packages, AUR packages, Flatpaks and AppImages are
updated in the background, with no password prompt and no terminal. updated in the background, with no password prompt and no terminal.
Run without a command it opens a small interactive menu with the two switches Run without a command it opens a small interactive menu with the two switches
that matter - automatic updates on/off and notifications on/off - plus the that matter (automatic updates on/off and notifications on/off), plus the
current status, and a settings screen covering every remaining option so the current status, and a settings screen covering every remaining option so the
configuration file never has to be edited by hand. In the settings screen the configuration file never has to be edited by hand. In the settings screen the
arrow keys select, Space or Right changes a value, and _q_ goes back; every arrow keys select, Space or Right changes a value, and _q_ goes back; every
@@ -23,8 +23,8 @@ change is written out immediately.
The actual work is done by a systemd system service. The timer ticks hourly; The actual work is done by a systemd system service. The timer ticks hourly;
whether a tick does anything is decided by _UpdateInterval_ (daily by default). whether a tick does anything is decided by _UpdateInterval_ (daily by default).
A run that is postponed - low battery, a game running, somebody else using A run that is postponed (low battery, a game running, somebody else using
pacman - is simply retried at the next tick. pacman) is simply retried at the next tick.
# COMMANDS # COMMANDS
@@ -64,7 +64,7 @@ Before anything is installed, a run is postponed when:
- the battery is below _MinBatteryPercent_ (ignored on mains power, and on - the battery is below _MinBatteryPercent_ (ignored on mains power, and on
machines without a battery); machines without a battery);
- _RequireAC_ is set and the machine is not plugged in; - _RequireAC_ is set and the machine is not plugged in;
- a game is running - detected via GameMode, a list of known game processes, or - a game is running, detected via GameMode, a list of known game processes, or
an application holding a blocking idle inhibitor; an application holding a blocking idle inhibitor;
- pacman's database is locked, or pacman, yay, paru, pamac or a similar tool is - pacman's database is locked, or pacman, yay, paru, pamac or a similar tool is
running. running.
@@ -84,13 +84,13 @@ A package that has to replace another one is handled silently. If pacman would
stop to ask whether a conflicting package may be removed, the transaction is stop to ask whether a conflicting package may be removed, the transaction is
retried once with that question answered affirmatively, unless retried once with that question answered affirmatively, unless
_AutoResolveConflicts_ is turned off. Files on disk that collide with a package _AutoResolveConflicts_ is turned off. Files on disk that collide with a package
are *not* forced - that stays a human decision. A signature failure triggers one are *not* forced. That stays a human decision. A signature failure triggers one
keyring refresh and one retry. keyring refresh and one retry.
AUR packages are built and installed as the locked *cachy-auto-update* system AUR packages are built and installed as the locked *cachy-auto-update* system
account, because *makepkg*(8) refuses to run as root. That account has no account, because *makepkg*(8) refuses to run as root. That account has no
password and no shell, and is allowed - through _/etc/sudoers.d/cachy-auto-update_ password and no shell. Through _/etc/sudoers.d/cachy-auto-update_ it is allowed
- to invoke *pacman* without one. No user password is ever stored anywhere. to invoke *pacman* without one. No user password is ever stored anywhere.
A failed AUR build is not reported the first time it happens; only a failure A failed AUR build is not reported the first time it happens; only a failure
that repeats is worth waking somebody up for. that repeats is worth waking somebody up for.
@@ -113,8 +113,9 @@ 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 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 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 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 uses while copying, rather than a notification. That is what makes it a bar
instead of a line of text. instead of a line of text. The desktop pops it up as it opens, so it follows
_NotifyOnStart_ and is not shown when that is off.
The desktop withdraws a job as soon as the D-Bus connection that requested it 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 closes, so a helper process runs inside each graphical session for the duration
@@ -122,24 +123,42 @@ 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 Where that is missing, or on a desktop with no job interface, there is no
progress entry and nothing else is affected. progress entry and nothing else is affected.
Fetching the packages and unpacking them are two steps rather than one. On a Working out the upgrade, fetching it and unpacking it are three steps rather
domestic line the download is the longer of the two, and a bar that called the than one. Between "Starting full system upgrade" and the transaction it
whole thing "installing" would sit near its beginning for minutes at a time eventually prepares, pacman prints nothing at all, and on a large backlog that
looking stuck. silence runs to minutes; that stretch therefore carries a label of its own and
deliberately no item count, because a counter frozen at "0 of 161" reads as a
stuck update. On a domestic line the download is then the longest of the three,
and a bar that called the whole thing "installing" would sit near its beginning
for minutes at a time looking stuck.
Neither phase gets a counter from pacman on an unattended run, so both are Expanding "Details" shows the last five lines of the run log as they are
counted here, a line at a time: "foo-1.2-1-x86_64 downloading..." for the written, alongside the package currently being worked on. The percentage says
first, "upgrading foo..." for the second. Packages already in the cache are the update is alive; these lines say what it is alive doing, which matters most
never announced, so the download step regularly ends short of its total and during the stretches that have nothing countable to report, above all an AUR
gives up the rest of its share when unpacking begins. pacman's other (n/m) package being compiled. The job interface carries exactly two description
sequences - checking keys, package integrity, loading package files - each fields, so those are the two things shown.
count up to the same total and are deliberately ignored.
A step that turns out to have no work is dropped from the bar instead of being
handed its share for nothing. Packages already in the cache are never
announced, so a run with everything already fetched skips the download step
outright rather than jumping when unpacking starts; the same applies to AUR
with nothing pending, or a machine with no Flatpaks installed.
Neither counted phase gets a counter from pacman on an unattended run, so both
are counted here, a line at a time: "foo-1.2-1-x86_64 downloading..." for the
first, "upgrading foo..." for the second. pacman's output is line-buffered
through *stdbuf*(1) so those lines arrive as they happen. Writing to a log
rather than a terminal, libc would otherwise release them in 4KB blocks, around
a hundred and sixty packages at a time. pacman's other (n/m) sequences
(checking keys, package integrity, loading package files) each count up to the
same total and are deliberately ignored.
# HOW LONG NOTIFICATIONS STAY # HOW LONG NOTIFICATIONS STAY
A message that reports the machine still needing a person - an update that A message that reports the machine still needing a person stays on screen
failed, a package database left locked, packages that had to be held back - until it is dismissed. For example: an update that failed, a package database
stays on screen until it is dismissed. A message like that is only worth left locked, or packages that had to be held back. A message like that is only worth
sending if it is still there when somebody comes back to the machine. 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. Everything else times out by itself, an update that simply worked included.
@@ -170,10 +189,10 @@ _org.freedesktop.login1.power-off-ignore-inhibit_, which is _auth_admin_keep_,
so the desktop presents an administrator password prompt reading "Power off the so the desktop presents an administrator password prompt reading "Power off the
system while an application is inhibiting this". That string is shipped system while an application is inhibiting this". That string is shipped
untranslated by systemd and does not mention updates, and no KDE dialog untranslated by systemd and does not mention updates, and no KDE dialog
explains the situation either - only *systemctl*(1) names the inhibitor and its explains the situation either. Only *systemctl*(1) names the inhibitor and its
reason. Hence the notification. reason. Hence the notification.
A hard power-off - holding the power button, or losing mains power - is not A hard power-off (holding the power button, or losing mains power) is not
preventable. On the next run a leftover _/var/lib/pacman/db.lck_ is removed if preventable. On the next run a leftover _/var/lib/pacman/db.lck_ is removed if
it is older than the current boot, since no process able to hold it can still it is older than the current boot, since no process able to hold it can still
exist; the upgrade is then repeated and pacman reinstalls whatever was caught exist; the upgrade is then repeated and pacman reinstalls whatever was caught
@@ -212,4 +231,4 @@ _/var/lib/cachy-auto-update/_
# AUTHOR # AUTHOR
Felitendo. Source and bug reports: Felitendo. Source and bug reports:
https://github.com/Felitendo/cachy-auto-update https://github.com/LoonixTools/cachy-auto-update
+41 -11
View File
@@ -5,7 +5,8 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: cachy-auto-update 1.0.0\n" "Project-Id-Version: cachy-auto-update 1.0.0\n"
"Report-Msgid-Bugs-To: https://github.com/Felitendo/cachy-auto-update/issues\n" "Report-Msgid-Bugs-To: https://github.com/LoonixTools/cachy-auto-update/"
"issues\n"
"POT-Creation-Date: 2026-08-08 00:00+0200\n" "POT-Creation-Date: 2026-08-08 00:00+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
@@ -53,19 +54,28 @@ msgstr ""
msgid "just now" msgid "just now"
msgstr "" msgstr ""
msgid "1 minute ago"
msgstr ""
#, c-format #, c-format
msgid "%d minutes ago" msgid "%d minutes ago"
msgstr "" msgstr ""
msgid "1 hour ago"
msgstr ""
#, c-format #, c-format
msgid "%d hours ago" msgid "%d hours ago"
msgstr "" msgstr ""
msgid "1 day ago"
msgstr ""
#, c-format #, c-format
msgid "%d days ago" msgid "%d days ago"
msgstr "" msgstr ""
msgid "The last run reported a problem - see 'cachy-auto-update log'." msgid "The last run reported a problem. See 'cachy-auto-update log'."
msgstr "" msgstr ""
msgid "A restart is recommended to finish a kernel update." msgid "A restart is recommended to finish a kernel update."
@@ -162,19 +172,23 @@ msgstr ""
msgid "/etc/sudoers.d/cachy-auto-update is invalid; refusing to enable." msgid "/etc/sudoers.d/cachy-auto-update is invalid; refusing to enable."
msgstr "" msgstr ""
msgid "No AUR helper found - install paru or yay for AUR updates." msgid "No AUR helper found. Install paru or yay for AUR updates."
msgstr "" msgstr ""
msgid "base-devel is missing - AUR packages cannot be built without it." msgid "base-devel is missing, so AUR packages cannot be built."
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]" msgid ""
"CachyOS shows a \"Reboot recommended\" notification while an update is still "
"running. Turn it off? [Y/n]"
msgstr "" msgstr ""
#, c-format #, c-format
@@ -234,6 +248,12 @@ msgstr ""
#. looking for it - it appears next to whatever they were doing - so each one #. looking for it - it appears next to whatever they were doing - so each one
#. says outright that this is an update running, rather than naming the kind of #. says outright that this is an update running, rather than naming the kind of
#. package on its own. #. package on its own.
msgid "Checking for updates"
msgstr ""
msgid "Preparing the update"
msgstr ""
msgid "Downloading updates" msgid "Downloading updates"
msgstr "" msgstr ""
@@ -252,6 +272,9 @@ msgstr ""
msgid "Cleaning up after the update" msgid "Cleaning up after the update"
msgstr "" msgstr ""
msgid "Log"
msgstr ""
msgid "Package" msgid "Package"
msgstr "" msgstr ""
@@ -269,13 +292,16 @@ msgstr ""
msgid "Update failed" msgid "Update failed"
msgstr "" 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 "Package database locked" msgid "Package database locked"
msgstr "" msgstr ""
msgid "pacman's lock file looks left over from an interrupted update. Updates are paused until it is cleared." msgid ""
"pacman's lock file looks left over from an interrupted update. Updates are "
"paused until it is cleared."
msgstr "" msgstr ""
msgid "Some packages were held back" msgid "Some packages were held back"
@@ -295,13 +321,17 @@ msgstr ""
msgid "Finishing an interrupted update" msgid "Finishing an interrupted update"
msgstr "" msgstr ""
msgid "The last update was cut short, most likely because the machine was switched off. It is being finished now." msgid ""
"The last update was cut short, most likely because the machine was switched "
"off. It is being finished now."
msgstr "" msgstr ""
msgid "Installing updates" msgid "Installing updates"
msgstr "" msgstr ""
msgid "%d packages are being updated. Please leave the computer switched on until this is done." msgid ""
"%d packages are being updated. Please leave the computer switched on until "
"this is done."
msgstr "" msgstr ""
msgid "Settings" msgid "Settings"
@@ -310,7 +340,7 @@ msgstr ""
msgid "(none)" msgid "(none)"
msgstr "" msgstr ""
msgid "Up/Down select - Space or Right changes - q goes back" msgid "Up/Down: select, Space or Right: change, q: back"
msgstr "" msgstr ""
msgid "Package names separated by spaces, empty to clear:" msgid "Package names separated by spaces, empty to clear:"
+70 -24
View File
@@ -5,7 +5,8 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: cachy-auto-update 1.0.0\n" "Project-Id-Version: cachy-auto-update 1.0.0\n"
"Report-Msgid-Bugs-To: https://github.com/Felitendo/cachy-auto-update/issues\n" "Report-Msgid-Bugs-To: https://github.com/LoonixTools/cachy-auto-update/"
"issues\n"
"POT-Creation-Date: 2026-08-08 00:00+0200\n" "POT-Creation-Date: 2026-08-08 00:00+0200\n"
"PO-Revision-Date: 2026-08-08 00:00+0200\n" "PO-Revision-Date: 2026-08-08 00:00+0200\n"
"Last-Translator: Felitendo\n" "Last-Translator: Felitendo\n"
@@ -54,20 +55,29 @@ msgstr "%d Pakete"
msgid "just now" msgid "just now"
msgstr "gerade eben" msgstr "gerade eben"
msgid "1 minute ago"
msgstr "vor 1 Minute"
#, c-format #, c-format
msgid "%d minutes ago" msgid "%d minutes ago"
msgstr "vor %d Minuten" msgstr "vor %d Minuten"
msgid "1 hour ago"
msgstr "vor 1 Stunde"
#, c-format #, c-format
msgid "%d hours ago" msgid "%d hours ago"
msgstr "vor %d Stunden" msgstr "vor %d Stunden"
msgid "1 day ago"
msgstr "vor 1 Tag"
#, c-format #, c-format
msgid "%d days ago" msgid "%d days ago"
msgstr "vor %d Tagen" msgstr "vor %d Tagen"
msgid "The last run reported a problem - see 'cachy-auto-update log'." msgid "The last run reported a problem. See 'cachy-auto-update log'."
msgstr "Beim letzten Lauf gab es ein Problem – siehe „cachy-auto-update log“." msgstr "Beim letzten Lauf gab es ein Problem. Siehe „cachy-auto-update log“."
msgid "A restart is recommended to finish a kernel update." msgid "A restart is recommended to finish a kernel update."
msgstr "Ein Neustart wird empfohlen, um ein Kernel-Update abzuschließen." msgstr "Ein Neustart wird empfohlen, um ein Kernel-Update abzuschließen."
@@ -161,26 +171,37 @@ msgid "Could not enable the systemd timer."
msgstr "Der systemd-Timer konnte nicht aktiviert werden." msgstr "Der systemd-Timer konnte nicht aktiviert werden."
msgid "/etc/sudoers.d/cachy-auto-update is invalid; refusing to enable." msgid "/etc/sudoers.d/cachy-auto-update is invalid; refusing to enable."
msgstr "/etc/sudoers.d/cachy-auto-update ist fehlerhaft – wird nicht aktiviert." msgstr ""
"/etc/sudoers.d/cachy-auto-update ist fehlerhaft und wird nicht aktiviert."
msgid "No AUR helper found - install paru or yay for AUR updates." msgid "No AUR helper found. Install paru or yay for AUR updates."
msgstr "Kein AUR-Helper gefunden – für AUR-Updates paru oder yay installieren." 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, so AUR packages cannot be built."
msgstr "base-devel fehlt – ohne das lassen sich AUR-Pakete nicht bauen." msgstr "base-devel fehlt, daher lassen sich keine AUR-Pakete bauen."
msgid "cachy-update also notifies about available updates. Turn its notifications off? [Y/n]" msgid ""
msgstr "cachy-update benachrichtigt ebenfalls über verfügbare Updates. Dessen Benachrichtigungen abschalten? [J/n]" "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." 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]" msgid ""
msgstr "CachyOS zeigt eine Benachrichtigung \"Neustart empfohlen\", während ein Update noch läuft. Abschalten? [J/n]" "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 #, c-format
msgid "The reboot notification has been turned off. Undo it by deleting %s." 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." msgstr ""
"Die Neustart-Benachrichtigung wurde abgeschaltet. Rückgängig durch Löschen "
"von %s."
#, c-format #, c-format
msgid "Could not write %s." msgid "Could not write %s."
@@ -235,6 +256,12 @@ msgstr "Ohne Befehl wird ein interaktives Menü angezeigt."
#. looking for it - it appears next to whatever they were doing - so each one #. looking for it - it appears next to whatever they were doing - so each one
#. says outright that this is an update running, rather than naming the kind of #. says outright that this is an update running, rather than naming the kind of
#. package on its own. #. package on its own.
msgid "Checking for updates"
msgstr "Nach Updates wird gesucht"
msgid "Preparing the update"
msgstr "Update wird vorbereitet"
msgid "Downloading updates" msgid "Downloading updates"
msgstr "Updates werden heruntergeladen" msgstr "Updates werden heruntergeladen"
@@ -253,6 +280,9 @@ msgstr "AppImages werden aktualisiert"
msgid "Cleaning up after the update" msgid "Cleaning up after the update"
msgstr "Aufräumen nach dem Update" msgstr "Aufräumen nach dem Update"
msgid "Log"
msgstr "Protokoll"
msgid "Package" msgid "Package"
msgstr "Paket" msgstr "Paket"
@@ -270,21 +300,29 @@ msgstr "%d Updates wurden installiert."
msgid "Update failed" msgid "Update failed"
msgstr "Update fehlgeschlagen" msgstr "Update fehlgeschlagen"
msgid "Something went wrong while updating. Run 'cachy-auto-update log' for details." msgid ""
msgstr "Beim Update ist etwas schiefgelaufen. Details mit „cachy-auto-update log“." "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 "Package database locked" msgid "Package database locked"
msgstr "Paketdatenbank gesperrt" msgstr "Paketdatenbank gesperrt"
msgid "pacman's lock file looks left over from an interrupted update. Updates are paused until it is cleared." msgid ""
msgstr "Die Sperrdatei von pacman scheint von einem abgebrochenen Update übrig zu sein. Bis sie entfernt ist, pausieren die Updates." "pacman's lock file looks left over from an interrupted update. Updates are "
"paused until it is cleared."
msgstr ""
"Die Sperrdatei von pacman scheint von einem abgebrochenen Update übrig zu "
"sein. Bis sie entfernt ist, pausieren die Updates."
msgid "Some packages were held back" msgid "Some packages were held back"
msgstr "Einige Pakete wurden zurückgehalten" msgstr "Einige Pakete wurden zurückgehalten"
#, c-format #, c-format
msgid "%s could not be updated and was skipped. Everything else is up to date." msgid "%s could not be updated and was skipped. Everything else is up to date."
msgstr "%s konnte nicht aktualisiert werden und wurde übersprungen. Alles andere ist aktuell." msgstr ""
"%s konnte nicht aktualisiert werden und wurde übersprungen. Alles andere ist "
"aktuell."
#, c-format #, c-format
msgid "Held back: %s" msgid "Held back: %s"
@@ -296,15 +334,23 @@ msgstr "Der letzte Lauf wurde abgebrochen, bevor er fertig war."
msgid "Finishing an interrupted update" msgid "Finishing an interrupted update"
msgstr "Abgebrochenes Update wird beendet" msgstr "Abgebrochenes Update wird beendet"
msgid "The last update was cut short, most likely because the machine was switched off. It is being finished now." msgid ""
msgstr "Das letzte Update wurde unterbrochen, vermutlich weil der Rechner ausgeschaltet wurde. Es wird jetzt zu Ende geführt." "The last update was cut short, most likely because the machine was switched "
"off. It is being finished now."
msgstr ""
"Das letzte Update wurde unterbrochen, vermutlich weil der Rechner "
"ausgeschaltet wurde. Es wird jetzt zu Ende geführt."
msgid "Installing updates" msgid "Installing updates"
msgstr "Updates werden installiert" msgstr "Updates werden installiert"
#, c-format #, c-format
msgid "%d packages are being updated. Please leave the computer switched on until this is done." msgid ""
msgstr "%d Pakete werden gerade aktualisiert. Bitte den Rechner so lange eingeschaltet lassen." "%d packages are being updated. Please leave the computer switched on until "
"this is done."
msgstr ""
"%d Pakete werden gerade aktualisiert. Bitte den Rechner so lange "
"eingeschaltet lassen."
msgid "Settings" msgid "Settings"
msgstr "Einstellungen" msgstr "Einstellungen"
@@ -312,8 +358,8 @@ msgstr "Einstellungen"
msgid "(none)" msgid "(none)"
msgstr "(keine)" msgstr "(keine)"
msgid "Up/Down select - Space or Right changes - q goes back" msgid "Up/Down: select, Space or Right: change, q: back"
msgstr "Hoch/Runter wählen – Leertaste oder Rechts ändert – q zurück" msgstr "Hoch/Runter: wählen, Leertaste oder Rechts: ändern, q: zurück"
msgid "Package names separated by spaces, empty to clear:" msgid "Package names separated by spaces, empty to clear:"
msgstr "Paketnamen mit Leerzeichen getrennt, leer zum Löschen:" msgstr "Paketnamen mit Leerzeichen getrennt, leer zum Löschen:"
+1 -1
View File
@@ -1,5 +1,5 @@
[Desktop Entry] [Desktop Entry]
# Not here to put an entry in the application menu - hence NoDisplay. This is # 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 # 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 # 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 # job request, and without one the job is filed under whatever the helper
+251
View File
@@ -0,0 +1,251 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-48.2 -1.2 553.3 553.3" width="512" height="512">
<!-- The C is the CachyOS logo, taken unchanged from cachyos-settings (/usr/share/icons/cachyos.svg), without its bubbles. -->
<defs>
<linearGradient id="bo-badge" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#5fd0ff"/><stop offset="1" stop-color="#0c62a0"/></linearGradient>
<mask id="bo-gap"><rect x="-100" y="-100" width="712" height="712" fill="#fff"/><circle cx="356" cy="372" r="100" fill="#000"/></mask>
<filter id="bo-soft" x="-20%" y="-20%" width="140%" height="150%"><feDropShadow dx="0" dy="10" stdDeviation="12" flood-color="#04213a" flood-opacity=".35"/></filter>
</defs>
<g filter="url(#bo-soft)">
<g mask="url(#bo-gap)">
<svg x="0" y="0" width="512" height="512"
version="1.1"
viewBox="0 0 64 64"
id="svg13595"
sodipodi:docname="finalTemplate2.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs13599">
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4353"
id="linearGradient5185"
x1="994.80933"
y1="1533.2783"
x2="982.34351"
y2="1556.7653"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.8798364,0,0,1.8798364,-1461.3663,-2735.8963)" />
<linearGradient
inkscape:collect="always"
id="linearGradient4353">
<stop
style="stop-color:#020202;stop-opacity:1;"
offset="0"
id="stop4349" />
<stop
style="stop-color:#020202;stop-opacity:0;"
offset="1"
id="stop4351" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4353"
id="linearGradient9102"
x1="1031.1788"
y1="1597.9056"
x2="1018.4292"
y2="1574.7378"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.8798364,0,0,1.8798364,-1475.8928,-2775.7298)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4353"
id="linearGradient11890"
x1="940.43298"
y1="1612.4667"
x2="930.58917"
y2="1594.4696"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.8798364,0,0,1.8798364,-1461.3663,-2735.8963)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4353"
id="linearGradient11670"
x1="965.60468"
y1="1571.4271"
x2="951.66113"
y2="1571.345"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.8798364,0,0,1.8798364,-1461.3663,-2735.8963)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4353"
id="linearGradient13770"
x1="946.23389"
y1="1655.8641"
x2="961.37311"
y2="1655.8458"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.8798364,0,0,1.8798364,-1461.3663,-2735.8963)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4353"
id="linearGradient12421"
x1="936.33533"
y1="1628.7738"
x2="933.3775"
y2="1622.9775"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.8798364,0,0,1.8798364,-1461.3663,-2735.8963)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4353"
id="linearGradient13391"
x1="950.3302"
y1="1618.6165"
x2="941.97058"
y2="1645.7675"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.8798364,0,0,1.8798364,-1461.3663,-2735.8963)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4353"
id="linearGradient13599"
x1="1008.2007"
y1="1681.3291"
x2="1015.7307"
y2="1668.3738"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.8798364,0,0,1.8798364,-1461.3663,-2735.8963)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient18299"
id="linearGradient18175"
x1="1148.342"
y1="1585.5081"
x2="1145.4373"
y2="1629.9749"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.149118,0,0,1.149118,-688.72735,-1522.6311)" />
<linearGradient
inkscape:collect="always"
id="linearGradient18299">
<stop
style="stop-color:#008066;stop-opacity:0"
offset="0"
id="stop18295" />
<stop
style="stop-color:#00ffcc;stop-opacity:1"
offset="1"
id="stop18297" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient18299"
id="linearGradient18632"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.87237591,0,0,0.87237591,-453.21965,-1141.1036)"
x1="1148.342"
y1="1585.5081"
x2="1145.4373"
y2="1629.9749" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient18299"
id="linearGradient18659"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.44920702,0,0,0.44920702,64.088025,-548.51323)"
x1="1148.342"
y1="1585.5081"
x2="1145.4373"
y2="1629.9749" />
</defs>
<g
id="g3823"
transform="matrix(0.14586725,0,0,0.14586725,-33.936112,-11.122661)">
<path
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.497373px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 340.70181,142.33354 H 527.16034 L 479.95858,224.0895 H 378.83956 l -42.13335,72.97713 42.72023,73.99374 h 197.24578 l -48.40146,83.83394 H 336.69907 L 244.27316,294.80782 332.75762,141.54814 Z"
id="path3295-6-2-7-3" />
<path
style="fill:#00aa88;stroke:none;stroke-width:0.497373px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 332.75762,141.54814 147.20096,82.54136 47.20176,-81.75596 z"
id="path6337"
sodipodi:nodetypes="cccc" />
<path
style="fill:#00aa88;stroke:none;stroke-width:0.497373px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 379.42644,371.06037 -42.72737,83.83394 h 191.57169 l 48.40146,-83.83394 z"
id="path6372" />
<path
style="fill:#00ccff;stroke:none;stroke-width:0.497373px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 332.75762,141.54814 479.95858,224.0895 H 378.83956 l -42.13335,72.97713 42.72023,73.99374 -42.72737,83.83394 -92.42591,-160.08649 88.48446,-153.25968"
id="path6473" />
<path
style="fill:url(#linearGradient5185);fill-opacity:1;stroke:none;stroke-width:0.497373px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 479.95858,224.0895 332.75762,141.54814 321.5632,161.7389 433.15254,223.62048 Z"
id="path5111" />
<path
style="fill:url(#linearGradient9102);fill-opacity:1;stroke:none;stroke-width:0.497373px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 375.85512,218.12064 148.32079,-81.75596 -47.20175,81.75596 z"
id="path8399-8" />
<path
style="fill:#00aa88;fill-opacity:1;stroke:none;stroke-width:0.497373px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 378.83956,224.0895 527.16034,142.33354 479.95858,224.0895 Z"
id="path8399" />
<path
style="fill:#00aa88;stroke:none;stroke-width:0.497373px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 244.27316,294.80782 134.5664,-70.71832 -42.13353,72.97713 z"
id="path8568" />
<path
style="fill:url(#linearGradient11890);fill-opacity:1;stroke:none;stroke-width:0.497373px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 244.27316,294.80782 134.5664,-70.71832 -9.0907,-14.22153 -115.97106,67.22709 z"
id="path11882" />
<path
style="fill:#00aa88;fill-opacity:1;stroke:none;stroke-width:0.497373px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 378.83956,224.0895 -46.08194,-82.54136 3.94841,155.51849 z"
id="path8467" />
<path
style="fill:#00ccff;fill-opacity:1;stroke:none;stroke-width:0.497373px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 379.42606,371.06037 148.8447,83.83394 48.40146,-83.83394 z"
id="path8916" />
<path
style="fill:url(#linearGradient11670);fill-opacity:1;stroke:none;stroke-width:0.497373px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 332.75762,141.54814 -11.19442,20.19076 1.07394,134.6664 14.06907,0.66133 z"
id="path11629" />
<path
style="fill:#00aa88;fill-opacity:1;stroke:none;stroke-width:0.497373px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 336.70603,297.06663 -70.56831,35.38867 70.56135,122.43901 z"
id="path12053" />
<path
style="fill:url(#linearGradient13770);fill-opacity:1;stroke:none;stroke-width:0.497373px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 336.70603,297.06663 v 157.82768 l 17.29054,-33.74438 1.33544,-92.51916 z"
id="path13729" />
<path
style="fill:url(#linearGradient12421);fill-opacity:1;stroke:none;stroke-width:0.497373px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 266.13772,332.4553 70.56831,-35.38867 -14.06889,-0.66133 -61.22739,29.25326 z"
id="path12347" />
<path
style="fill:#00aa88;fill-opacity:1;stroke:none;stroke-width:0.497373px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 266.13772,332.4553 113.28872,38.60507 -42.72041,-73.99374 z"
id="path13027" />
<path
style="fill:url(#linearGradient13391);fill-opacity:1;stroke:none;stroke-width:0.497373px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 379.42644,371.06037 -113.28872,-38.60507 12.30147,21.24102 93.10152,33.81657 z"
id="path13350" />
<path
style="fill:url(#linearGradient13599);fill-opacity:1;stroke:none;stroke-width:0.497373px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 379.42644,371.06037 148.84432,83.83394 17.83946,-31.7525 -95.79666,-52.89916 z"
id="path13525" />
</g>
</svg>
</g>
</g>
<g filter="url(#bo-soft)">
<circle cx="356" cy="372" r="84" fill="url(#bo-badge)"/>
<path d="M315.6 357.3 A43 43 0 0 1 382.5 338.1" fill="none" stroke="#fff" stroke-width="16" stroke-linecap="round"/><path d="M396.4 357.3 L370.7 353.2 L394.3 323.0 Z" fill="#fff" stroke="#fff" stroke-width="4" stroke-linejoin="round"/><path d="M396.4 386.7 A43 43 0 0 1 329.5 405.9" fill="none" stroke="#fff" stroke-width="16" stroke-linecap="round"/><path d="M315.6 386.7 L341.3 390.8 L317.7 421.0 Z" fill="#fff" stroke="#fff" stroke-width="4" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

+6 -6
View File
@@ -7,7 +7,7 @@
# Documentation: man cachy-auto-update # Documentation: man cachy-auto-update
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Main switches - normally set through `cachy-auto-update enable/disable` # Main switches, normally set through `cachy-auto-update enable/disable`
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Apply updates automatically in the background. # Apply updates automatically in the background.
@@ -73,7 +73,7 @@ IgnorePkg=
# installed packages, and every cached version of packages that are no longer # installed packages, and every cached version of packages that are no longer
# installed. Nothing that is currently installed is ever touched, so this only # installed. Nothing that is currently installed is ever touched, so this only
# costs the ability to downgrade further back than KeepOldPackages. # costs the ability to downgrade further back than KeepOldPackages.
# Without it /var/cache/pacman/pkg grows forever - tens of gigabytes on a # Without it /var/cache/pacman/pkg grows forever: tens of gigabytes on a
# machine with a few large packages. # machine with a few large packages.
CleanCache=yes CleanCache=yes
@@ -97,8 +97,8 @@ RemoveOrphans=no
# #
# How long a message stays on screen is not configurable, because it follows # 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 # 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 # 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 # 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 # useful if it is still there when somebody comes back. Everything else, an
# update that simply worked included, times out on its own. # update that simply worked included, times out on its own.
@@ -107,8 +107,8 @@ RemoveOrphans=no
# password prompt that never mentions updates. The message is withdrawn again # password prompt that never mentions updates. The message is withdrawn again
# when the result arrives, 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 # Also turns the progress bar in the notification area on or off. The desktop
# optional and appears whenever the desktop supports one. # pops it up as it opens, so it counts as a start notice too.
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
Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

+3 -3
View File
@@ -1,7 +1,7 @@
[Unit] [Unit]
Description=CachyOS unattended update run Description=CachyOS unattended update run
Documentation=man:cachy-auto-update(1) Documentation=man:cachy-auto-update(1)
Documentation=https://github.com/Felitendo/cachy-auto-update Documentation=https://github.com/LoonixTools/cachy-auto-update
After=network-online.target After=network-online.target
Wants=network-online.target Wants=network-online.target
ConditionPathExists=/etc/cachy-auto-update/cachy-auto-update.conf ConditionPathExists=/etc/cachy-auto-update/cachy-auto-update.conf
@@ -28,8 +28,8 @@ TimeoutStartSec=4h
# Deliberately *not* sandboxed the way paccache.service is: this unit installs # Deliberately *not* sandboxed the way paccache.service is: this unit installs
# packages across the whole filesystem and downloads them over the network, so # packages across the whole filesystem and downloads them over the network, so
# ProtectSystem=, PrivateNetwork= and friends would break it. NoNewPrivileges # ProtectSystem=, PrivateNetwork= and friends would break it. NoNewPrivileges
# in particular has to stay off - the AUR step relies on the build account # in particular has to stay off, because the AUR step relies on the build
# calling sudo. # account calling sudo.
NoNewPrivileges=no NoNewPrivileges=no
ProtectHostname=yes ProtectHostname=yes
+1 -1
View File
@@ -5,7 +5,7 @@ Documentation=man:cachy-auto-update(1)
[Timer] [Timer]
# The tick is hourly, but an actual update only happens once per # The tick is hourly, but an actual update only happens once per
# UpdateInterval (default: daily). That combination is what makes a postponed # UpdateInterval (default: daily). That combination is what makes a postponed
# run - low battery, a game running, somebody else using pacman - retry by # run (low battery, a game running, somebody else using pacman) retry by
# itself an hour later without any backoff bookkeeping. # itself an hour later without any backoff bookkeeping.
OnBootSec=15min OnBootSec=15min
OnCalendar=hourly OnCalendar=hourly
+6 -6
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# #
# cachy-auto-update - unattended updates for CachyOS # cachy-auto-update: unattended updates for CachyOS
# #
# The command line front end. Everything it does is either flipping a switch in # The command line front end. Everything it does is either flipping a switch in
# /etc/cachy-auto-update/cachy-auto-update.conf, driving the systemd timer, or # /etc/cachy-auto-update/cachy-auto-update.conf, driving the systemd timer, or
@@ -77,10 +77,10 @@ cau_do_enable() {
cau_config_load cau_config_load
if [[ $CFG_AUR == yes ]] && ! cau_aur_detect_quiet; then if [[ $CFG_AUR == yes ]] && ! cau_aur_detect_quiet; then
cau_note "$(cau_msg "No AUR helper found - install paru or yay for AUR updates.")" cau_note "$(cau_msg "No AUR helper found. Install paru or yay for AUR updates.")"
fi fi
if [[ $CFG_AUR == yes ]] && ! cau_have makepkg; then if [[ $CFG_AUR == yes ]] && ! cau_have makepkg; then
cau_note "$(cau_msg "base-devel is missing - AUR packages cannot be built without it.")" cau_note "$(cau_msg "base-devel is missing, so AUR packages cannot be built.")"
fi fi
cau_offer_disable_cachy_update cau_offer_disable_cachy_update
@@ -89,7 +89,7 @@ cau_do_enable() {
# CachyOS ships a pacman hook that pops up "Reboot recommended!" the moment a # 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 # kernel, driver or systemd package is unpacked. During a manual upgrade that
# is fine - the transaction is the last thing happening. During an unattended # is fine, because 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 # 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 # Flatpaks to pull, and a notification asking for a restart right then is an
# invitation to cut the update in half. # invitation to cut the update in half.
@@ -141,7 +141,7 @@ cau_aur_detect_quiet() {
# cachy-update ships an enabled user timer that only ever says "N updates # cachy-update ships an enabled user timer that only ever says "N updates
# available". Once updates apply themselves that notification is pure noise, # available". Once updates apply themselves that notification is pure noise,
# so offer to silence it - but only ask, never decide. # so offer to silence it. But only ask, never decide.
cau_offer_disable_cachy_update() { cau_offer_disable_cachy_update() {
local user uid answer found=0 local user uid answer found=0
@@ -163,7 +163,7 @@ cau_offer_disable_cachy_update() {
read -r answer || return 0 read -r answer || return 0
# Defaults to yes: once updates install themselves, cachy-update's "N # Defaults to yes: once updates install themselves, cachy-update's "N
# updates available" is purely noise. "j" is accepted too - the prompt is # 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 # translated, so a German user types the German letter and used to have
# that silently read as "no". # that silently read as "no".
case "${answer,,}" in case "${answer,,}" in
+17 -6
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# #
# cachy-auto-update-progress - the update's progress bar on the desktop # 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 # 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 # session. Reads one instruction per line on stdin and turns it into the same
@@ -8,6 +8,7 @@
# #
# info<TAB>text headline, already translated by the caller # info<TAB>text headline, already translated by the caller
# detail<TAB>name<TAB>value a labelled line under "Details" # detail<TAB>name<TAB>value a labelled line under "Details"
# log<TAB>name<TAB>line<TAB>line... the run log's last lines, one per field
# total<TAB>n how many items this step has # total<TAB>n how many items this step has
# done<TAB>n how many of them are finished # done<TAB>n how many of them are finished
# percent<TAB>n overall progress, 0-100 # percent<TAB>n overall progress, 0-100
@@ -15,7 +16,7 @@
# #
# Why a separate process at all: the desktop ties the progress entry to the # 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 # 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 # connection goes away. One-shot callers (gdbus, busctl, dbus-send) therefore
# cannot drive one, because each invocation is its own connection that closes # 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 # 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. # for as long as the update takes, and read its orders from somewhere else.
@@ -53,7 +54,7 @@ class Job:
reply = self.bus.call_sync( reply = self.bus.call_sync(
JOB_SERVICE, JOB_PATH, "org.kde.JobViewServerV2", "requestView", JOB_SERVICE, JOB_PATH, "org.kde.JobViewServerV2", "requestView",
# capabilities 0: no cancel and no pause button. Neither can be # capabilities 0: no cancel and no pause button. Neither can be
# honoured - pacman's commit phase is not interruptible - and a # honoured, because pacman's commit phase is not interruptible. A
# button that does nothing is worse than no button. # button that does nothing is worse than no button.
GLib.Variant("(sia{sv})", (DESKTOP_ENTRY, 0, {})), GLib.Variant("(sia{sv})", (DESKTOP_ENTRY, 0, {})),
GLib.VariantType("(o)"), Gio.DBusCallFlags.NONE, -1, None) GLib.VariantType("(o)"), Gio.DBusCallFlags.NONE, -1, None)
@@ -67,8 +68,8 @@ class Job:
JOB_SERVICE, self.path, "org.kde.JobViewV2", method, variant, JOB_SERVICE, self.path, "org.kde.JobViewV2", method, variant,
None, Gio.DBusCallFlags.NONE, -1, None) None, Gio.DBusCallFlags.NONE, -1, None)
except GLib.Error: except GLib.Error:
# The desktop went away mid-update - a logout, or a plasmashell # The desktop went away mid-update (a logout, or a plasmashell
# restart. The update carries on without a bar. # restart). The update carries on without a bar.
self.path = None self.path = None
def close(self, message=""): def close(self, message=""):
@@ -95,7 +96,7 @@ def main():
try: try:
job.open() job.open()
except GLib.Error: except GLib.Error:
# No job server on this desktop - anything that is not Plasma. Same # No job server on this desktop (anything that is not Plasma). Same
# deal as a missing binding: drain stdin, stay out of the way. # deal as a missing binding: drain stdin, stay out of the way.
for _ in sys.stdin: for _ in sys.stdin:
pass pass
@@ -113,6 +114,16 @@ def main():
elif cmd == "detail" and len(fields) > 2: elif cmd == "detail" and len(fields) > 2:
job.call("setDescriptionField", job.call("setDescriptionField",
GLib.Variant("(uss)", (0, arg, fields[2]))) GLib.Variant("(uss)", (0, arg, fields[2])))
elif cmd == "log" and len(fields) > 2:
# Field 1, and there is no field 2: the job model behind this
# interface carries exactly two, and anything further is
# discarded without complaint at the other end.
#
# The tail arrives one line per field, because the protocol
# itself is one instruction per line. Joined back up with the
# newlines the job view does render.
job.call("setDescriptionField",
GLib.Variant("(uss)", (1, arg, "\n".join(fields[2:]))))
elif cmd == "total": elif cmd == "total":
job.call("setTotalAmount", GLib.Variant("(ts)", (int(arg), unit))) job.call("setTotalAmount", GLib.Variant("(ts)", (int(arg), unit)))
elif cmd == "done": elif cmd == "done":
+10 -5
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# #
# cachy-auto-update-run - one unattended update pass # cachy-auto-update-run: one unattended update pass
# #
# Started by cachy-auto-update.service as root. The timer fires hourly and this # Started by cachy-auto-update.service as root. The timer fires hourly and this
# decides whether anything should happen; that is what makes deferrals free. # decides whether anything should happen; that is what makes deferrals free.
@@ -43,7 +43,7 @@ fi
# Force a neutral locale here rather than relying on the unit's Environment=, # Force a neutral locale here rather than relying on the unit's Environment=,
# so a run started by hand behaves exactly like one started by the timer. # so a run started by hand behaves exactly like one started by the timer.
# pacman failures are classified by matching its output, and on a German system # pacman failures are classified by matching its output, and on a German system
# that output is German. Text aimed at a person does not come through here - a # that output is German. Text aimed at a person does not come through here: a
# notification is rendered in the recipient's own locale by cau_msg_in. # notification is rendered in the recipient's own locale by cau_msg_in.
export LC_ALL=C LANGUAGE= export LC_ALL=C LANGUAGE=
@@ -65,7 +65,7 @@ cau_log_open
# Record an interruption rather than leaving the previous run's verdict behind. # Record an interruption rather than leaving the previous run's verdict behind.
# Without this, killing an interactive run leaves last_result at whatever it was # Without this, killing an interactive run leaves last_result at whatever it was
# before - so the menu can keep reporting a problem from hours ago while the # before. Then the menu can keep reporting a problem from hours ago while the
# machine is in fact fully up to date, which is worse than saying nothing. # machine is in fact fully up to date, which is worse than saying nothing.
# pacman makes the commit phase itself uninterruptible, so the packages either # pacman makes the commit phase itself uninterruptible, so the packages either
# 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.
@@ -185,13 +185,18 @@ failed=0
# Open the desktop's progress bar, told up front which steps this run will # 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 # 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. # does not sit at 85% for the last second of the run.
progress_steps=(download repo) progress_steps=(resolve download repo)
[[ $CFG_AUR == yes ]] && progress_steps+=(aur) [[ $CFG_AUR == yes ]] && progress_steps+=(aur)
[[ $CFG_FLATPAK == yes ]] && progress_steps+=(flatpak) [[ $CFG_FLATPAK == yes ]] && progress_steps+=(flatpak)
[[ $CFG_APPIMAGE == yes ]] && progress_steps+=(appimage) [[ $CFG_APPIMAGE == yes ]] && progress_steps+=(appimage)
progress_steps+=(cleanup) progress_steps+=(cleanup)
cau_progress_begin "${progress_steps[@]}" cau_progress_begin "${progress_steps[@]}"
# And carry the run log's last lines along with it, so "Details" shows what the
# update is doing during the stretches that have nothing to count. Most of all
# an AUR package building for a quarter of an hour.
cau_progress_tail_start "$CAU_RUNLOG"
if ! cau_pacman_update; then if ! cau_pacman_update; then
failed=1 failed=1
fi fi
@@ -268,7 +273,7 @@ fi
# Recorded, deliberately not announced. The running kernel loses its module # Recorded, deliberately not announced. The running kernel loses its module
# tree the moment pacman unpacks the new one, so this turns true partway # 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 # through a run that still has AUR builds and Flatpaks ahead of it. A
# "restart recommended" bubble arriving then reads as an invitation to restart # "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 # while the update is still going. `cachy-auto-update status` and the menu say
# so instead, where nobody is being interrupted mid-transaction. # so instead, where nobody is being interrupted mid-transaction.
+14 -11
View File
@@ -49,8 +49,8 @@ export TEXTDOMAINDIR="${CAU_LOCALEDIR}"
# #
# Standard POSIX precedence, deliberately: LC_ALL wins outright, and LC_ALL=C # Standard POSIX precedence, deliberately: LC_ALL wins outright, and LC_ALL=C
# really does mean English. The service sets LC_ALL=C so that pacman and upower # really does mean English. The service sets LC_ALL=C so that pacman and upower
# stay parseable, which makes the log English - correct, since the log is a # stay parseable, which makes the log English. That is correct, since the log
# technical artefact. Anything aimed at a person (a desktop notification) does # is a technical artefact. Anything aimed at a person (a desktop notification) does
# not go through here at all; it names the recipient's own locale explicitly # not go through here at all; it names the recipient's own locale explicitly
# via cau_msg_in and cau_user_locale. # via cau_msg_in and cau_user_locale.
cau_ui_locale() { cau_ui_locale() {
@@ -117,8 +117,8 @@ cau_msg_in() {
fi fi
# With no arguments the message is plain text, not a format string. Feeding # With no arguments the message is plain text, not a format string. Feeding
# it to printf anyway turns any literal percent sign in it - "Battery (%)", # it to printf anyway turns any literal percent sign in it (like
# "100 % done" - into an invalid conversion, and that is a trap every # "Battery (%)" or "100 % done") into an invalid conversion, and that is a trap every
# translator would eventually walk into. # translator would eventually walk into.
if (( $# == 0 )); then if (( $# == 0 )); then
printf '%s' "$translated" printf '%s' "$translated"
@@ -184,7 +184,7 @@ cau_log_open() {
# Runs a command, capturing its combined output in the run log. Returns the # Runs a command, capturing its combined output in the run log. Returns the
# command's exit status. # command's exit status.
# #
# When a person is watching - `cachy-auto-update run` from a terminal - the # When a person is watching (`cachy-auto-update run` from a terminal), the
# output is shown as well. Building an AUR package or pulling a few hundred # output is shown as well. Building an AUR package or pulling a few hundred
# megabytes of Flatpak can take minutes, and silence for that long is # megabytes of Flatpak can take minutes, and silence for that long is
# indistinguishable from a hang. # indistinguishable from a hang.
@@ -207,8 +207,8 @@ cau_run_logged() {
# Set by cau_bad and cau_note. The menu redraws immediately after an action, # Set by cau_bad and cau_note. The menu redraws immediately after an action,
# which would wipe the screen; this marks that something was printed that the # which would wipe the screen; this marks that something was printed that the
# user still has to read, so only those cases wait for a keypress. A plain # user still has to read, so only those cases wait for a keypress. A plain
# success needs no acknowledgement - the status block at the top of the menu # success needs no acknowledgement, because the status block at the top of the
# already shows the new state. # menu already shows the new state.
CAU_UI_NEEDS_ACK='' CAU_UI_NEEDS_ACK=''
cau_say() { printf '%s\n' "$*"; } cau_say() { printf '%s\n' "$*"; }
@@ -281,10 +281,13 @@ cau_time_ago() {
delta=$(( now - ts )) delta=$(( now - ts ))
(( delta < 0 )) && delta=0 (( delta < 0 )) && delta=0
if (( delta < 60 )); then cau_msg "just now" if (( delta < 60 )); then cau_msg "just now"
elif (( delta < 3600 )); then cau_msg "%d minutes ago" "$(( delta / 60 ))" elif (( delta < 120 )); then cau_msg "1 minute ago"
elif (( delta < 86400 )); then cau_msg "%d hours ago" "$(( delta / 3600 ))" elif (( delta < 3600 )); then cau_msg "%d minutes ago" "$(( delta / 60 ))"
else cau_msg "%d days ago" "$(( delta / 86400 ))" elif (( delta < 7200 )); then cau_msg "1 hour ago"
elif (( delta < 86400 )); then cau_msg "%d hours ago" "$(( delta / 3600 ))"
elif (( delta < 172800 )); then cau_msg "1 day ago"
else cau_msg "%d days ago" "$(( delta / 86400 ))"
fi fi
printf '\n' printf '\n'
} }
+4 -4
View File
@@ -16,7 +16,7 @@ CAU_SKIP_REASON=''
# cau_on_ac # cau_on_ac
# True when running on mains power. systemd-ac-power also returns success when # True when running on mains power. systemd-ac-power also returns success when
# the machine has neither a battery nor an adapter, which is exactly right for # the machine has neither a battery nor an adapter, which is exactly right for
# desktops - hand-rolled sysfs globbing gets that case wrong. # desktops. Hand-rolled sysfs globbing gets that case wrong.
cau_on_ac() { cau_on_ac() {
if cau_have systemd-ac-power; then if cau_have systemd-ac-power; then
systemd-ac-power > /dev/null 2>&1 systemd-ac-power > /dev/null 2>&1
@@ -41,8 +41,8 @@ cau_on_ac() {
# cau_battery_percent # cau_battery_percent
# Average charge across the system batteries, or failure when the machine has # Average charge across the system batteries, or failure when the machine has
# none. Peripheral batteries (mice, headsets) advertise type=Battery too and # none. Peripheral batteries (mice, headsets) advertise type=Battery too and
# are filtered out via the scope attribute; when scope is missing entirely - # are filtered out via the scope attribute. When scope is missing entirely (as
# as on many laptops - the device counts as a system battery. # on many laptops), the device counts as a system battery.
cau_battery_percent() { cau_battery_percent() {
local ps sum=0 count=0 cap local ps sum=0 count=0 cap
@@ -139,7 +139,7 @@ cau_gamemode_active() {
while read -r user uid; do while read -r user uid; do
[[ -n $user ]] || continue [[ -n $user ]] || continue
# timeout runs inside the runuser call because it has to be a real # timeout runs inside the runuser call because it has to be a real
# binary there - it cannot wrap a shell function from out here. # binary there. It cannot wrap a shell function from out here.
out="$(cau_as_user "$user" "$uid" timeout 5 busctl --user --json=short \ out="$(cau_as_user "$user" "$uid" timeout 5 busctl --user --json=short \
get-property com.feralinteractive.GameMode \ get-property com.feralinteractive.GameMode \
/com/feralinteractive/GameMode \ /com/feralinteractive/GameMode \
+6 -6
View File
@@ -4,8 +4,8 @@
# #
# The contract this file implements: the machine's owner may run any package # The contract this file implements: the machine's owner may run any package
# manager at any time and must never see a lock error caused by us. We can only # manager at any time and must never see a lock error caused by us. We can only
# guarantee that in one direction - by never *starting* while somebody else is # guarantee that in one direction: by never *starting* while somebody else is
# mid-transaction - so the checks here run before anything is touched, and a # mid-transaction. So the checks here run before anything is touched, and a
# refusal simply defers the run to the next hourly tick. # refusal simply defers the run to the next hourly tick.
# Package managers that take /var/lib/pacman/db.lck. checkupdates is absent on # Package managers that take /var/lib/pacman/db.lck. checkupdates is absent on
@@ -65,7 +65,7 @@ cau_package_manager_busy() {
# #
# The rigorous test is the boot time: no process that existed before the # The rigorous test is the boot time: no process that existed before the
# current boot can still be running, so a db.lck older than boot is abandoned # current boot can still be running, so a db.lck older than boot is abandoned
# by definition - which is exactly what a power cut during an update leaves # by definition. That is exactly what a power cut during an update leaves
# behind. A lock that is merely unheld *within* this boot is not provable in # behind. A lock that is merely unheld *within* this boot is not provable in
# the same way, so it is only reported (see cau_track_stale_lock) and never # the same way, so it is only reported (see cau_track_stale_lock) and never
# removed; guessing wrong there would corrupt a live transaction. # removed; guessing wrong there would corrupt a live transaction.
@@ -90,12 +90,12 @@ cau_pacman_lock_is_stale() {
# cau_recover_stale_lock # cau_recover_stale_lock
# Clears a provably abandoned lock so an interrupted update can be finished on # Clears a provably abandoned lock so an interrupted update can be finished on
# the next run. Without this, one power cut during an update stops every future # the next run. Without this, one power cut during an update stops every future
# update permanently and silently - the worst possible outcome for a machine # update permanently and silently. That is the worst possible outcome for a
# nobody is watching. # machine nobody is watching.
cau_recover_stale_lock() { cau_recover_stale_lock() {
cau_pacman_lock_is_stale || return 1 cau_pacman_lock_is_stale || return 1
cau_warn "Found a pacman lock older than this boot - an update was cut short" cau_warn "Found a pacman lock older than this boot. An update was cut short"
rm -f "$CAU_PACMAN_LOCK" 2>/dev/null || { rm -f "$CAU_PACMAN_LOCK" 2>/dev/null || {
cau_error "Could not remove the stale pacman lock" cau_error "Could not remove the stale pacman lock"
return 1 return 1
+8 -8
View File
@@ -32,7 +32,7 @@ cau_ui_term_restore() {
} }
# Runs an action with the terminal handed back to normal line mode, so anything # Runs an action with the terminal handed back to normal line mode, so anything
# it prints - or prompts for - behaves the way a program expects. # it prints or prompts for behaves the way a program expects.
cau_ui_cooked() { cau_ui_cooked() {
cau_ui_term_restore cau_ui_term_restore
"$@" "$@"
@@ -213,8 +213,8 @@ _cau_setting_cycle() {
# A cursor list rather than a numbered menu: there are eighteen settings, and # A cursor list rather than a numbered menu: there are eighteen settings, and
# numbering them would run out of digits and force paging. # numbering them would run out of digits and force paging.
# #
# The frame is assembled in memory and written once. Everything constant - the # The frame is assembled in memory and written once. Everything constant (the
# specs, the translated labels, the clear sequence - is resolved before the # specs, the translated labels, the clear sequence) is resolved before the
# loop, and the values are re-read only after something actually changes. # loop, and the values are re-read only after something actually changes.
# Drawing the naive way cost a command substitution per label per frame, which # Drawing the naive way cost a command substitution per label per frame, which
# measured 435 ms per keypress: arrow keys felt like the console was reloading, # measured 435 ms per keypress: arrow keys felt like the console was reloading,
@@ -239,7 +239,7 @@ cau_ui_settings() {
local title hint local title hint
cau_msg_into "$locale" "Settings"; title="$CAU_MSG_RESULT" cau_msg_into "$locale" "Settings"; title="$CAU_MSG_RESULT"
cau_msg_into "$locale" "Up/Down select - Space or Right changes - q goes back" cau_msg_into "$locale" "Up/Down: select, Space or Right: change, q: back"
hint="$CAU_MSG_RESULT" hint="$CAU_MSG_RESULT"
# the terminfo clear string, fetched once instead of forking per frame # the terminfo clear string, fetched once instead of forking per frame
@@ -320,8 +320,8 @@ cau_ui_edit_text() {
# _cau_row <label> <value> # _cau_row <label> <value>
# printf's %-28s pads by bytes, so a label containing "ü" comes out one column # printf's %-28s pads by bytes, so a label containing "ü" comes out one column
# short. ${#s} counts characters in a UTF-8 locale, so the padding is computed # short. ${#s} counts characters in a UTF-8 locale, so the padding is computed
# here instead - and applied inline, because command substitution would eat the # here instead. It is applied inline, because command substitution would eat
# trailing spaces again. # the trailing spaces again.
_cau_row() { _cau_row() {
local label="$1" value="$2" pad local label="$1" value="$2" pad
pad=$(( 28 - ${#label} )) pad=$(( 28 - ${#label} ))
@@ -389,7 +389,7 @@ cau_ui_status() {
if [[ $result == failed ]]; then if [[ $result == failed ]]; then
printf '\n %s%s%s\n' "$CAU_C_YELLOW" \ printf '\n %s%s%s\n' "$CAU_C_YELLOW" \
"$(cau_msg "The last run reported a problem - see 'cachy-auto-update log'.")" \ "$(cau_msg "The last run reported a problem. See 'cachy-auto-update log'.")" \
"$CAU_C_RESET" "$CAU_C_RESET"
elif [[ $result == interrupted ]]; then elif [[ $result == interrupted ]]; then
printf '\n %s%s%s\n' "$CAU_C_YELLOW" \ printf '\n %s%s%s\n' "$CAU_C_YELLOW" \
@@ -510,7 +510,7 @@ cau_ui_menu() {
5) cau_ui_status_conditions; cau_pause ;; 5) cau_ui_status_conditions; cau_pause ;;
6) cau_ui_settings ;; 6) cau_ui_settings ;;
q|Q) cau_ui_term_restore; trap - EXIT INT TERM; return 0 ;; q|Q) cau_ui_term_restore; trap - EXIT INT TERM; return 0 ;;
# Anything else - Enter, arrow keys, stray characters - just # Anything else (Enter, arrow keys, stray characters) just
# redraws. Escape is deliberately not a quit key, so a mistyped # redraws. Escape is deliberately not a quit key, so a mistyped
# arrow key cannot close the menu. # arrow key cannot close the menu.
*) ;; *) ;;
+7 -7
View File
@@ -3,10 +3,10 @@
# Desktop notifications from a root system service. # Desktop notifications from a root system service.
# #
# Two paths exist: # Two paths exist:
# * live - somebody has a graphical session, so notify-send is run inside # * live: somebody has a graphical session, so notify-send is run inside
# it via runuser with the session bus address set; # it via runuser with the session bus address set;
# * queued - nobody is logged in, so the message is appended to a spool that # * queued: nobody is logged in, so the message is appended to a spool that
# the XDG autostart entry replays at the next login. # the XDG autostart entry replays at the next login.
# #
# Messages travel as a msgid plus printf arguments rather than as finished # Messages travel as a msgid plus printf arguments rather than as finished
# text, so a notification queued at 04:00 is still rendered in whatever locale # text, so a notification queued at 04:00 is still rendered in whatever locale
@@ -44,8 +44,8 @@ cau_notify_close() {
# had to be skipped is only ever seen if it waits. # had to be skipped is only ever seen if it waits.
# #
# Set explicitly rather than left to the server. Notification daemons do keep # Set explicitly rather than left to the server. Notification daemons do keep
# critical-urgency messages up - the spec asks them to, and Plasma obliges - # 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 # 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 # 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. # and whether do-not-disturb is overridden. Those are not the same question.
# #
@@ -136,7 +136,7 @@ cau_notify_enqueue() {
printf '%s\n' "$record" >> "$CAU_NOTIFY_QUEUE" 2>/dev/null || return 0 printf '%s\n' "$record" >> "$CAU_NOTIFY_QUEUE" 2>/dev/null || return 0
# keep the spool bounded - nobody wants three weeks of backlog at login # keep the spool bounded: nobody wants three weeks of backlog at login
if (( $(wc -l < "$CAU_NOTIFY_QUEUE" 2>/dev/null || echo 0) > CAU_NOTIFY_QUEUE_MAX )); then if (( $(wc -l < "$CAU_NOTIFY_QUEUE" 2>/dev/null || echo 0) > CAU_NOTIFY_QUEUE_MAX )); then
tmp="$(mktemp "${CAU_NOTIFY_QUEUE}.XXXXXX")" || return 0 tmp="$(mktemp "${CAU_NOTIFY_QUEUE}.XXXXXX")" || return 0
tail -n "$CAU_NOTIFY_QUEUE_MAX" "$CAU_NOTIFY_QUEUE" > "$tmp" tail -n "$CAU_NOTIFY_QUEUE_MAX" "$CAU_NOTIFY_QUEUE" > "$tmp"
+22 -2
View File
@@ -4,14 +4,14 @@
# #
# AppImages have no package manager of their own; Gear Lever is what tracks # AppImages have no package manager of their own; Gear Lever is what tracks
# where each one came from and how to fetch a new build. Its CLI is a first # where each one came from and how to fetch a new build. Its CLI is a first
# class interface - `--update --all -y` is exactly the unattended entry point # class interface: `--update --all -y` is exactly the unattended entry point
# we need, and it skips AppImages whose application is currently running rather # we need, and it skips AppImages whose application is currently running rather
# than pulling the file out from under it (we deliberately do not pass # than pulling the file out from under it (we deliberately do not pass
# --force). # --force).
# #
# This only runs for users with a live graphical session: Gear Lever is a # This only runs for users with a live graphical session: Gear Lever is a
# Flatpak GTK application and needs the session's runtime directory. Nothing is # Flatpak GTK application and needs the session's runtime directory. Nothing is
# lost by waiting - the next hourly tick will catch it once they log in. # lost by waiting, because the next hourly tick will catch it once they log in.
CAU_APPIMAGE_ID="it.mijorus.gearlever" CAU_APPIMAGE_ID="it.mijorus.gearlever"
CAU_APPIMAGE_COUNT=0 CAU_APPIMAGE_COUNT=0
@@ -43,6 +43,26 @@ cau_appimage_update() {
local user uid count rc=0 local user uid count rc=0
local -a cmd local -a cmd
# Is there a Gear Lever on this machine at all? Asked before the step is
# announced rather than discovered inside the loop: on a machine without
# one (the common case, it is an optional dependency), a step that exists
# only to hand its share of the bar straight to the next one is a jump the
# bar does not need. Stops at the first user who has it, so the extra probe
# costs anything only in the case it is there to remove.
local found=0
while read -r user uid; do
[[ -n $user ]] || continue
if _cau_gearlever_cmd "$user" "$uid" > /dev/null; then
found=1
break
fi
done < <(cau_active_session_users)
if (( ! found )); then
cau_progress_drop appimage
return 0
fi
cau_progress_step appimage "Updating AppImages" cau_progress_step appimage "Updating AppImages"
while read -r user uid; do while read -r user uid; do
+15 -6
View File
@@ -2,15 +2,15 @@
# #
# AUR packages. # AUR packages.
# #
# makepkg - and therefore paru and yay - refuse to run as root, so this is the # makepkg (and therefore paru and yay) refuse to run as root, so this is the
# one part of the run that cannot happen in the service's own context. It is # one part of the run that cannot happen in the service's own context. It is
# executed as the locked "cachy-auto-update" system account instead, which # executed as the locked "cachy-auto-update" system account instead, which
# sysusers.d creates with no password and no shell. That account is granted # sysusers.d creates with no password and no shell. That account is granted
# NOPASSWD access to /usr/bin/pacman through /etc/sudoers.d/cachy-auto-update, # NOPASSWD access to /usr/bin/pacman through /etc/sudoers.d/cachy-auto-update,
# which is what lets the helper install what it built without a human present. # which is what lets the helper install what it built without a human present.
# #
# The alternative - stashing the user's password somewhere the daemon can read # The alternative (stashing the user's password somewhere the daemon can read
# it - buys nothing: whatever can decrypt it is exactly what an attacker would # it) buys nothing: whatever can decrypt it is exactly what an attacker would
# already have. # already have.
CAU_AUR_COUNT=0 CAU_AUR_COUNT=0
@@ -124,7 +124,9 @@ cau_aur_update() {
local pending failures local pending failures
local -a args local -a args
cau_aur_ready || return 0 # No helper, no base-devel, no bar: a step that cannot run should not be
# holding a share of it.
cau_aur_ready || { cau_progress_drop aur; return 0; }
cau_progress_step aur "Updating AUR packages" cau_progress_step aur "Updating AUR packages"
@@ -132,23 +134,30 @@ cau_aur_update() {
if (( pending == 0 )); then if (( pending == 0 )); then
cau_info "No AUR updates pending" cau_info "No AUR updates pending"
cau_state_clear aur_failures cau_state_clear aur_failures
cau_progress_drop aur
return 0 return 0
fi fi
cau_info "Updating $pending AUR package(s) with $CAU_AUR_HELPER" cau_info "Updating $pending AUR package(s) with $CAU_AUR_HELPER"
# The helper builds each package from source with no counter this side of # The helper builds each package from source and prints plenty about it,
# its output, so the bar sits at the start of the step until it is done. # none of it countable from this side. A single large package can take ten
# minutes, so the bar creeps through the step rather than sitting at its
# start for all of them; the item count stays where it is, because that is
# the number that would be lying if it moved.
cau_progress_item 0 "$pending" cau_progress_item 0 "$pending"
mapfile -t args < <(cau_aur_helper_args) mapfile -t args < <(cau_aur_helper_args)
cau_progress_creep_start
if cau_run_logged cau_as_build_user "$CAU_AUR_HELPER" "${args[@]}"; then if cau_run_logged cau_as_build_user "$CAU_AUR_HELPER" "${args[@]}"; then
cau_progress_creep_stop
CAU_AUR_COUNT="$pending" CAU_AUR_COUNT="$pending"
cau_progress_item "$pending" cau_progress_item "$pending"
cau_state_clear aur_failures cau_state_clear aur_failures
return 0 return 0
fi fi
cau_progress_creep_stop
CAU_AUR_COUNT=0 CAU_AUR_COUNT=0
failures="$(cau_state_read aur_failures 0)" failures="$(cau_state_read aur_failures 0)"
[[ $failures =~ ^[0-9]+$ ]] || failures=0 [[ $failures =~ ^[0-9]+$ ]] || failures=0
+14 -4
View File
@@ -5,7 +5,7 @@
# System-wide installations are updated directly as root. That side-steps a # System-wide installations are updated directly as root. That side-steps a
# real obstacle: the shipped polkit rule for Flatpak only grants install and # real obstacle: the shipped polkit rule for Flatpak only grants install and
# uninstall, and only to a subject that is active, local and in the wheel # uninstall, and only to a subject that is active, local and in the wheel
# group - none of which is true for an unattended service. Being root means # group. None of that is true for an unattended service. Being root means
# polkit is never consulted in the first place. # polkit is never consulted in the first place.
# #
# Per-user installations live in the user's home and are updated inside their # Per-user installations live in the user's home and are updated inside their
@@ -26,21 +26,31 @@ cau_flatpak_pending_system() {
cau_flatpak_update() { cau_flatpak_update() {
local rc=0 pending user uid home count local rc=0 pending user uid home count
cau_have flatpak || return 0 cau_have flatpak || { cau_progress_drop flatpak; return 0; }
cau_progress_step flatpak "Updating Flatpak apps" cau_progress_step flatpak "Updating Flatpak apps"
# refresh appstream metadata first so remote-ls sees current versions # refresh appstream metadata first so remote-ls sees current versions.
# Nothing is countable until that has finished, so the bar creeps rather
# than waiting at the start of the step for it.
cau_progress_creep_start
cau_run_logged flatpak update --appstream --system --noninteractive || true cau_run_logged flatpak update --appstream --system --noninteractive || true
cau_progress_creep_stop
pending="$(cau_flatpak_pending_system)" pending="$(cau_flatpak_pending_system)"
if (( pending > 0 )); then if (( pending > 0 )); then
cau_info "Updating $pending system Flatpak(s)" cau_info "Updating $pending system Flatpak(s)"
cau_progress_item 0 "$pending" cau_progress_item 0 "$pending"
# Started after the count above and stopped before the one below, so the
# only reports this side makes while a ticker is running are further
# along than the ticker ever gets.
cau_progress_creep_start
if cau_run_logged flatpak update --system --noninteractive --assumeyes; then if cau_run_logged flatpak update --system --noninteractive --assumeyes; then
cau_progress_creep_stop
CAU_FLATPAK_COUNT=$(( CAU_FLATPAK_COUNT + pending )) CAU_FLATPAK_COUNT=$(( CAU_FLATPAK_COUNT + pending ))
cau_progress_item "$pending" cau_progress_item "$pending"
else else
cau_progress_creep_stop
cau_warn "System Flatpak update failed" cau_warn "System Flatpak update failed"
rc=1 rc=1
fi fi
@@ -67,7 +77,7 @@ cau_flatpak_update() {
fi fi
done < <(cau_human_users) done < <(cau_human_users)
# Unused runtimes are the Flatpak equivalent of orphaned packages - this is # Unused runtimes are the Flatpak equivalent of orphaned packages. This is
# removal of installed software, not cache trimming, so it belongs behind # removal of installed software, not cache trimming, so it belongs behind
# RemoveOrphans rather than CleanCache. # RemoveOrphans rather than CleanCache.
if [[ $CFG_REMOVE_ORPHANS == yes ]]; then if [[ $CFG_REMOVE_ORPHANS == yes ]]; then
+106 -48
View File
@@ -39,7 +39,7 @@ CAU_PACMAN_OP_RE='^(\([[:space:]]*[0-9]+/[0-9]+\) )?(upgrading|installing|reinst
# #
# glibc-2.44+r24+g16be1518495f-1-x86_64_v3 downloading... # glibc-2.44+r24+g16be1518495f-1-x86_64_v3 downloading...
# #
# and nothing else - no counter, no total - so the position here is counted the # and nothing else: no counter, no total. So the position here is counted the
# same way the transaction is. # same way the transaction is.
# #
# The database sync a few lines earlier prints the very same shape (" core # The database sync a few lines earlier prints the very same shape (" core
@@ -52,11 +52,18 @@ CAU_PACMAN_DL_AWK='
END { print n + 0, name }' END { print n + 0, name }'
# _cau_pacman_progress_watch <logfile> # _cau_pacman_progress_watch <logfile>
# Feeds the desktop's progress bar by watching pacman work, through both of the # Feeds the desktop's progress bar by watching pacman work, through the three
# phases a pacman run has: first everything is fetched, then everything is # phases a pacman run has: first it works out what the upgrade consists of,
# unpacked. They are two steps on the bar rather than one, because they are two # then everything is fetched, then everything is unpacked. Three steps on the
# steps to sit through - a run that has been "installing updates" at 4% for six # bar rather than one, because they are three stretches to sit through. And
# minutes has not hung, it is still downloading, and the bar should say so. # each one is silent in its own way.
#
# The first is the one that used to look like a hang. Between "starting full
# system upgrade" and the transaction it eventually prepares, pacman says
# nothing whatsoever, and on a large backlog that silence is minutes long. The
# only honest thing to show there is a label and no counter at all: a tally
# frozen at "0 of 161" reads as a stuck update, where "Preparing the update"
# with no number reads as what it actually is.
# #
# In the transaction, pacman announces each package twice over, in one of two # In the transaction, pacman announces each package twice over, in one of two
# shapes, and which one depends on a flag this program sets itself: # shapes, and which one depends on a flag this program sets itself:
@@ -66,7 +73,7 @@ CAU_PACMAN_DL_AWK='
# #
# Only the second carries a counter, and the unattended runs that this bar # 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 # 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 # 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 # "Package (218)" header pacman prints before it starts. That header is the
# better number anyway: checkupdates counts packages with an update available # better number anyway: checkupdates counts packages with an update available
# and knows nothing about the new dependencies pulled in alongside them. # and knows nothing about the new dependencies pulled in alongside them.
@@ -83,7 +90,8 @@ CAU_PACMAN_DL_AWK='
_cau_pacman_progress_watch() { _cau_pacman_progress_watch() {
local log="$1" local log="$1"
local total="${CAU_PACMAN_COUNT:-0}" announced processed line pkg last='' local total="${CAU_PACMAN_COUNT:-0}" announced processed line pkg last=''
local phase=download fetched shown='' local phase=resolve fetched shown='' labelled=''
local t0=$SECONDS
while :; do while :; do
sleep 1 sleep 1
@@ -94,49 +102,73 @@ _cau_pacman_progress_watch() {
line="$(grep -aoE "$CAU_PACMAN_OP_RE" "$log" 2>/dev/null | tail -n1)" line="$(grep -aoE "$CAU_PACMAN_OP_RE" "$log" 2>/dev/null | tail -n1)"
# Nothing unpacked yet, so this is still the download - or the database if [[ -n $line ]]; then
# sync ahead of it, which the awk above declines to count. # Unpacking has started, so whatever came before it is over. If
if [[ -z $line ]]; then # nothing was ever retrieved (every package already sitting in the
# cache, which is normal after a run that was interrupted once
# already), then the download step never happened,
# and it is dropped rather than handed its whole share of the bar in
# exchange for no work at all.
if [[ $phase != install ]]; then
[[ $phase == download ]] || cau_progress_drop download
phase=install
cau_progress_step repo "Updating system packages" "$total"
fi
# Nothing new since the last look. Checked before the counting grep
# because on a large upgrade this loop spends most of its life here.
[[ $line != "$last" ]] || continue
last="$line"
processed="$(grep -acE "$CAU_PACMAN_OP_RE" "$log" 2>/dev/null)"
[[ $processed =~ ^[0-9]+$ ]] || continue
# Where pacman does carry a counter, believe it over the tally: it
# is the same number, but it also knows the true total.
if [[ $line =~ ^\([[:space:]]*([0-9]+)/([0-9]+)\) ]]; then
processed="${BASH_REMATCH[1]}"
total="${BASH_REMATCH[2]}"
fi
cau_progress_item "$processed" "$total"
pkg="${line##* }"
cau_progress_detail "Package" "${pkg%...}"
continue
fi
# Fetching. The header is what tells this apart from the database sync
# a few lines earlier, which prints the very same shape.
if grep -qa '^:: Retrieving packages' "$log" 2>/dev/null; then
if [[ $phase == resolve ]]; then
phase=download
cau_progress_step download "Downloading updates" "$total"
fi
read -r fetched pkg < <(awk "$CAU_PACMAN_DL_AWK" "$log" 2>/dev/null) read -r fetched pkg < <(awk "$CAU_PACMAN_DL_AWK" "$log" 2>/dev/null)
[[ $fetched =~ ^[0-9]+$ ]] && (( fetched > 0 )) || continue [[ $fetched =~ ^[0-9]+$ ]] || continue
[[ $fetched != "$shown" ]] || continue [[ $fetched != "$shown" ]] || continue
shown="$fetched" shown="$fetched"
cau_progress_item "$fetched" "$total" cau_progress_item "$fetched" "$total"
# Down to the bare name, as the transaction reports it: the file # Down to the bare name, as the transaction reports it: the file
# pacman names here carries version, release and architecture. # pacman names here carries version, release and architecture.
cau_progress_detail "Package" "${pkg%-*-*-*}" [[ -n $pkg ]] && cau_progress_detail "Package" "${pkg%-*-*-*}"
continue continue
fi fi
# The first package being unpacked ends the download step. Its share of # Still resolving. pacman does mark the point where it stops syncing
# the bar is given up wherever it had got to - packages already in the # databases and starts working out the upgrade, and that is the half
# cache are fetched in no time at all and never print a line, so the # worth naming, because it is the half that takes the minutes.
# tally regularly stops short of the total it was promised. if [[ $phase == resolve && $labelled != upgrade ]] \
if [[ $phase == download ]]; then && grep -qa '^:: Starting full system upgrade' "$log" 2>/dev/null; then
phase=install labelled=upgrade
cau_progress_step repo "Updating system packages" "$total" cau_progress_step resolve "Preparing the update"
fi fi
# Nothing new since the last look. Checked before the counting grep # Nothing countable happens in here at all, so the bar creeps instead.
# because on a large upgrade this loop spends most of its life here. # This is the stretch that used to look like a hung update.
[[ $line != "$last" ]] || continue cau_progress_creep $(( SECONDS - t0 ))
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 done
} }
@@ -156,11 +188,21 @@ _cau_pacman_exec() {
watcher=$! watcher=$!
fi fi
# Line-buffered on purpose. pacman writes to a file or through a pipe here,
# never to a terminal, so libc buffers it in 4KB blocks. And 4KB of
# "upgrading foo..." is on the order of a hundred and sixty packages. The
# watcher would see nothing at all, then a hundred and sixty lines at once,
# which is exactly how a bar comes to sit still and then leap to the end.
# Guarded rather than assumed: without coreutils there is no bar to feed
# either, but there is still an update to run.
local -a buffered=()
cau_have stdbuf && buffered=(stdbuf -oL)
if [[ -n $CAU_INTERACTIVE ]]; then if [[ -n $CAU_INTERACTIVE ]]; then
pacman "$@" 2>&1 | tee "$log" "${buffered[@]}" pacman "$@" 2>&1 | tee "$log"
rc="${PIPESTATUS[0]}" rc="${PIPESTATUS[0]}"
else else
pacman "$@" > "$log" 2>&1 "${buffered[@]}" pacman "$@" > "$log" 2>&1
rc=$? rc=$?
fi fi
@@ -245,12 +287,18 @@ cau_pacman_update() {
local log kind local log kind
local -a flags local -a flags
# The download comes first and the watcher moves on to the repo step once # checkupdates goes first, against its own private database, and the bar
# pacman starts unpacking. # says so rather than naming a step that has not begun. The watcher takes
cau_progress_step download "Downloading updates" # over from here and moves on to the download and repo steps as pacman
# actually reaches them.
cau_progress_step resolve "Checking for updates"
if ! cau_pacman_pending; then if ! cau_pacman_pending; then
cau_info "No repository updates pending" cau_info "No repository updates pending"
# Neither of the two steps this would have led to is going to happen,
# so the rest of the run gets their share of the bar instead of
# watching it jump 70% the moment Flatpaks start.
cau_progress_drop download repo
return 0 return 0
fi fi
@@ -258,7 +306,10 @@ cau_pacman_update() {
CAU_PACMAN_COUNT="$(grep -c . <<< "$CAU_PACMAN_PENDING")" CAU_PACMAN_COUNT="$(grep -c . <<< "$CAU_PACMAN_PENDING")"
[[ $CAU_PACMAN_COUNT =~ ^[0-9]+$ ]] || CAU_PACMAN_COUNT=0 [[ $CAU_PACMAN_COUNT =~ ^[0-9]+$ ]] || CAU_PACMAN_COUNT=0
cau_info "Updating $CAU_PACMAN_COUNT repository package(s)" cau_info "Updating $CAU_PACMAN_COUNT repository package(s)"
cau_progress_item 0 "$CAU_PACMAN_COUNT" # Deliberately no item count yet. Until pacman has prepared a
# transaction there is nothing being worked through, and "0 of 161"
# against a bar that cannot move for the next few minutes is the exact
# impression the resolve step exists to avoid.
else else
# checkupdates is unavailable, so the list is unknown and pacman is # checkupdates is unavailable, so the list is unknown and pacman is
# asked to work it out itself. # asked to work it out itself.
@@ -270,7 +321,7 @@ cau_pacman_update() {
# #
# While an upgrade runs, a shutdown request is refused by logind and the # While an upgrade runs, a shutdown request is refused by logind and the
# desktop answers with a polkit password prompt reading "Power off the # desktop answers with a polkit password prompt reading "Power off the
# system while an application is inhibiting this" - which never mentions # system while an application is inhibiting this". That never mentions
# 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
@@ -291,6 +342,13 @@ cau_pacman_update() {
while true; do while true; do
if _cau_pacman_exec "$log" -Syu "${flags[@]}" "${extra[@]}"; then if _cau_pacman_exec "$log" -Syu "${flags[@]}" "${extra[@]}"; then
# The watcher decided the same thing in a subshell, so a step it
# dropped is still in the plan out here. Same question, same answer,
# and the two copies agree on what the rest of the run is scaled
# against. Only on the way out: a failed attempt is about to be
# retried, and that retry may well download after all.
grep -qa '^:: Retrieving packages' "$log" 2>/dev/null \
|| cau_progress_drop download
cat "$log" >> "$CAU_RUNLOG" 2>/dev/null cat "$log" >> "$CAU_RUNLOG" 2>/dev/null
grep -E '^(removing|replacing) ' "$log" 2>/dev/null \ grep -E '^(removing|replacing) ' "$log" 2>/dev/null \
| while read -r line; do cau_info " $line"; done | while read -r line; do cau_info " $line"; done
@@ -335,7 +393,7 @@ cau_pacman_update() {
dependency) dependency)
# Something installed still depends on a package the repos want # Something installed still depends on a package the repos want
# to drop or replace - almost always an AUR package that has not # to drop or replace, almost always an AUR package that has not
# caught up yet. Nothing here can fix that, and it is not worth # caught up yet. Nothing here can fix that, and it is not worth
# failing over: letting one stuck package block every other # failing over: letting one stuck package block every other
# update indefinitely is far worse on an unattended machine. # update indefinitely is far worse on an unattended machine.
@@ -410,7 +468,7 @@ cau_pacman_cleanup() {
reclaim="$( { paccache -d --nocolor -k"$CFG_KEEP_OLD"; paccache -du --nocolor -k0; } 2>&1 \ reclaim="$( { paccache -d --nocolor -k"$CFG_KEEP_OLD"; paccache -du --nocolor -k0; } 2>&1 \
| grep -oE 'disk space saved: [^)]*' | paste -sd', ' -)" | grep -oE 'disk space saved: [^)]*' | paste -sd', ' -)"
cau_info "Trimming the package cache (keeping $CFG_KEEP_OLD old version(s))${reclaim:+ - $reclaim}" cau_info "Trimming the package cache (keeping $CFG_KEEP_OLD old version(s))${reclaim:+: $reclaim}"
cau_run_logged paccache -r --nocolor -k"$CFG_KEEP_OLD" || cau_warn "paccache -r failed" cau_run_logged paccache -r --nocolor -k"$CFG_KEEP_OLD" || cau_warn "paccache -r failed"
cau_run_logged paccache -ru --nocolor -k0 || cau_warn "paccache -ru failed" cau_run_logged paccache -ru --nocolor -k0 || cau_warn "paccache -ru failed"
fi fi
+259 -23
View File
@@ -3,8 +3,8 @@
# The update's progress bar on the desktop. # The update's progress bar on the desktop.
# #
# An unattended upgrade can take twenty minutes, and for most of that a user is # 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 - # 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 # (the same widget that shows a bar while Dolphin copies files), so how far
# along the run is stays visible the whole time. # along the run is stays visible the whole time.
# #
# The desktop ends the progress entry as soon as the D-Bus connection that # The desktop ends the progress entry as soon as the D-Bus connection that
@@ -14,8 +14,8 @@
# of those pipes, plus the arithmetic that turns "package 120 of 260 in the # of those pipes, plus the arithmetic that turns "package 120 of 260 in the
# repository step" into one number for the bar. # repository step" into one number for the bar.
# #
# Absent anywhere along the way - no session, no Plasma, no Python bindings - # If anything is missing along the way (no session, no Plasma, no Python
# this does nothing at all and the update proceeds exactly as before. # bindings), this does nothing at all and the update proceeds exactly as before.
CAU_PROGRESS_HELPER="${CAU_LIBEXECDIR}/cachy-auto-update-progress" CAU_PROGRESS_HELPER="${CAU_LIBEXECDIR}/cachy-auto-update-progress"
@@ -26,12 +26,19 @@ CAU_PROGRESS_FIFOS=()
CAU_PROGRESS_LOCALES=() CAU_PROGRESS_LOCALES=()
# What each step is worth on the bar. Rough shares of a typical run rather than # What each step is worth on the bar. Rough shares of a typical run rather than
# anything measured: the repositories dominate - fetching them and unpacking # anything measured: the repositories dominate (fetching them and unpacking
# them about equally, on a domestic line - and the cleanup is a rounding error. # them about equally, on a domestic line), and the cleanup is a rounding error.
# They do not have to add up to 100 - only the steps a given run will actually # 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. # perform are counted, and the total is normalised against those.
#
# "resolve" is everything pacman does before it has a transaction: syncing the
# databases and working out what the upgrade actually consists of. It is
# usually seconds, which is why it is worth so little. But on a large backlog
# it is minutes, and those minutes used to be spent looking at a bar that had
# not moved yet.
declare -A CAU_PROGRESS_WEIGHTS=( declare -A CAU_PROGRESS_WEIGHTS=(
[download]=30 [repo]=40 [aur]=15 [flatpak]=10 [appimage]=3 [cleanup]=2 [resolve]=10 [download]=30 [repo]=40 [aur]=15 [flatpak]=10 [appimage]=3
[cleanup]=2
) )
CAU_PROGRESS_PLAN=() CAU_PROGRESS_PLAN=()
@@ -88,6 +95,8 @@ cau_progress_begin() {
(( CAU_PROGRESS_SCALE > 0 )) || return 0 (( CAU_PROGRESS_SCALE > 0 )) || return 0
[[ $CFG_NOTIFICATIONS == yes ]] || return 0 [[ $CFG_NOTIFICATIONS == yes ]] || return 0
# The desktop pops the bar up as it opens, which reads as a start notice.
[[ $CFG_NOTIFY_START == yes ]] || return 0
[[ -x $CAU_PROGRESS_HELPER ]] || return 0 [[ -x $CAU_PROGRESS_HELPER ]] || return 0
mkdir -p "$CAU_RUNDIR" 2>/dev/null || return 0 mkdir -p "$CAU_RUNDIR" 2>/dev/null || return 0
@@ -133,6 +142,49 @@ cau_progress_active() {
(( ${#CAU_PROGRESS_FDS[@]} )) (( ${#CAU_PROGRESS_FDS[@]} ))
} }
# cau_progress_drop <step-id...>
# Takes steps out of the plan and rescales the bar to what is left.
#
# Which steps a run will perform is only half known up front. The other half
# turns up while it runs: nothing to download because every package was already
# in the cache, no AUR updates pending, no Flatpaks installed. A step like that
# keeps its whole share of the bar and then hands it over in a single jump the
# moment the next one starts. That is precisely the stutter this is here to
# remove. Dropping it hands its share to the steps that do have work instead,
# so the bar advances at a steady pace rather than leaping across the gaps.
#
# It is also what lets the weights above stay rough: they never have to be
# right about a step that does not run, only about the ones that do.
#
# Only ever called for a step that has not started, so nothing already behind
# the bar is rescaled and the bar does not travel backwards.
cau_progress_drop() {
local drop step
local -a kept=()
(( ${#CAU_PROGRESS_FDS[@]} )) || return 0
for step in "${CAU_PROGRESS_PLAN[@]}"; do
for drop in "$@"; do
[[ $step == "$drop" ]] && continue 2
done
kept+=("$step")
done
(( ${#kept[@]} == ${#CAU_PROGRESS_PLAN[@]} )) && return 0
CAU_PROGRESS_PLAN=("${kept[@]}")
CAU_PROGRESS_SCALE=0
for step in "${CAU_PROGRESS_PLAN[@]}"; do
CAU_PROGRESS_SCALE=$(( CAU_PROGRESS_SCALE + ${CAU_PROGRESS_WEIGHTS[$step]:-0} ))
done
# Nothing left to weigh against would divide by zero further down. Cannot
# happen while cleanup is unconditional, but this is cheaper than relying
# on that staying true.
(( CAU_PROGRESS_SCALE > 0 )) || CAU_PROGRESS_SCALE=1
}
# cau_progress_step <step-id> <label-msgid> [item-count] # 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 # 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 # that reported fewer items than it promised still completes rather than
@@ -143,11 +195,17 @@ cau_progress_step() {
(( ${#CAU_PROGRESS_FDS[@]} )) || return 0 (( ${#CAU_PROGRESS_FDS[@]} )) || return 0
local found=0
for step in "${CAU_PROGRESS_PLAN[@]}"; do for step in "${CAU_PROGRESS_PLAN[@]}"; do
[[ $step == "$id" ]] && break [[ $step == "$id" ]] && { found=1; break; }
base=$(( base + ${CAU_PROGRESS_WEIGHTS[$step]:-0} )) base=$(( base + ${CAU_PROGRESS_WEIGHTS[$step]:-0} ))
done done
# A step that was dropped for having no work is not a step to move to.
# Without this the loop above would fall off the end of the plan and hand
# back the sum of every weight, i.e. send the bar straight to 100%.
(( found )) || return 0
CAU_PROGRESS_BASE=$base CAU_PROGRESS_BASE=$base
CAU_PROGRESS_SPAN=${CAU_PROGRESS_WEIGHTS[$id]:-0} CAU_PROGRESS_SPAN=${CAU_PROGRESS_WEIGHTS[$id]:-0}
CAU_PROGRESS_TOTAL=$total CAU_PROGRESS_TOTAL=$total
@@ -170,10 +228,38 @@ cau_progress_step() {
cau_progress_item 0 cau_progress_item 0
} }
# _cau_progress_pct <numerator> <denominator>
# How far through the current step we are, as a share of its span, turned into
# one number for the whole run and sent on if it has moved.
_cau_progress_pct() {
local num="$1" den="$2" pct scaled
if (( den > 0 )); then
scaled=$(( CAU_PROGRESS_BASE * 100 + CAU_PROGRESS_SPAN * 100 * num / den ))
else
scaled=$(( CAU_PROGRESS_BASE * 100 ))
fi
pct=$(( scaled / CAU_PROGRESS_SCALE ))
(( pct > 100 )) && pct=100
# Never backwards. Two honest things can ask for that: dropping a step
# rescales the run against a smaller total, and the conflict-recovery loop
# restarts pacman (and with it the item tally) from the top. Both are
# real, neither is a reason to show somebody a bar that retreats.
(( pct < CAU_PROGRESS_SHOWN )) && pct=$CAU_PROGRESS_SHOWN
# Only when the whole number changes. Percent is the one field the runner
# would otherwise rewrite for every package on a 500-package upgrade.
(( pct == CAU_PROGRESS_SHOWN )) && return 0
CAU_PROGRESS_SHOWN=$pct
_cau_progress_line 'percent\t%s' "$pct"
}
# cau_progress_item <processed> [total] # cau_progress_item <processed> [total]
# How far through the current step we are. # How far through the current step we are.
cau_progress_item() { cau_progress_item() {
local processed="$1" total="${2:-$CAU_PROGRESS_TOTAL}" pct scaled local processed="$1" total="${2:-$CAU_PROGRESS_TOTAL}"
(( ${#CAU_PROGRESS_FDS[@]} )) || return 0 (( ${#CAU_PROGRESS_FDS[@]} )) || return 0
[[ $processed =~ ^[0-9]+$ ]] || return 0 [[ $processed =~ ^[0-9]+$ ]] || return 0
@@ -185,24 +271,91 @@ cau_progress_item() {
_cau_progress_line 'total\t%s' "$total" _cau_progress_line 'total\t%s' "$total"
fi fi
_cau_progress_line 'done\t%s' "$processed" _cau_progress_line 'done\t%s' "$processed"
scaled=$(( CAU_PROGRESS_BASE * 100 + CAU_PROGRESS_SPAN * 100 * processed / total )) _cau_progress_pct "$processed" "$total"
else else
scaled=$(( CAU_PROGRESS_BASE * 100 )) _cau_progress_pct 0 0
fi fi
}
pct=$(( scaled / CAU_PROGRESS_SCALE )) # cau_progress_creep <seconds-elapsed>
(( pct > 100 )) && pct=100 # Moves the bar through a step whose length cannot be known in advance.
#
# Some of a run has no counter to offer and never will. pacman prints nothing
# whatsoever between "starting full system upgrade" and the transaction it
# eventually prepares; an AUR helper compiling a package prints plenty, none of
# it countable. On a large backlog either is minutes. There is no honest number
# to show for that. But a bar that has not moved since it appeared is read as
# a hang, and somebody who reads it that way reaches for the power button in
# the middle of an update. That is the failure this is here to prevent.
#
# So it creeps, along a curve that approaches the end of the step without ever
# reaching it: half the step's share after HALFLIFE seconds, three quarters
# after three times that, the whole of it never. Nothing is claimed that is not
# known: the item counter stays empty throughout, and it is the field that
# would be lying if it moved. The step still finishes the instant real work
# reports in, because every real report is further along than the creep.
#
# Confined to the step's own span, so a creep can never overtake the step that
# comes after it however long it is left running.
CAU_PROGRESS_CREEP_HALFLIFE=45
# Only when the whole number changes. Percent is the one field the runner cau_progress_creep() {
# would otherwise rewrite for every package on a 500-package upgrade. local elapsed="$1"
(( pct == CAU_PROGRESS_SHOWN )) && return 0
CAU_PROGRESS_SHOWN=$pct (( ${#CAU_PROGRESS_FDS[@]} )) || return 0
_cau_progress_line 'percent\t%s' "$pct" [[ $elapsed =~ ^[0-9]+$ ]] || return 0
_cau_progress_pct "$elapsed" $(( elapsed + CAU_PROGRESS_CREEP_HALFLIFE ))
}
# cau_progress_creep_start / cau_progress_creep_stop
# The same, for a step that blocks in one long call instead of polling: the
# ticker runs alongside it and is stopped when it returns. Only one at a time,
# and starting a second one replaces the first.
CAU_PROGRESS_CREEP_PID=0
CAU_PROGRESS_CREEP_T0=0
cau_progress_creep_start() {
cau_progress_creep_stop
(( ${#CAU_PROGRESS_FDS[@]} )) || return 0
CAU_PROGRESS_CREEP_T0=$SECONDS
local t0=$SECONDS
{
# Waiting without forking a sleep every two seconds, for the same
# reason cau_progress_begin opens its fifo read-write: a pipe held open
# at both ends never reports end-of-file, so a timed read on it blocks
# for exactly the timeout and nothing else. A forked sleep would also
# survive the kill below (it is a child of this subshell, not this
# subshell) and inherit the fifo's write end, which would keep the
# helper from seeing the end of its input until the sleep ran out.
local nap
exec {nap}<> <(:)
while :; do
read -r -t 2 -u "$nap" _ || true
cau_progress_creep $(( SECONDS - t0 ))
done
} &
CAU_PROGRESS_CREEP_PID=$!
}
cau_progress_creep_stop() {
(( CAU_PROGRESS_CREEP_PID )) || return 0
kill "$CAU_PROGRESS_CREEP_PID" 2>/dev/null
wait "$CAU_PROGRESS_CREEP_PID" 2>/dev/null
CAU_PROGRESS_CREEP_PID=0
# The ticker moved the bar from inside a subshell, so this side never saw
# it happen and still believes the bar is where it was left. Catching up
# costs one recomputation, since the curve is a function of elapsed time and
# nothing else. Without it the next ordinary report from here would be
# measured against a stale percentage and send the bar backwards.
cau_progress_creep $(( SECONDS - CAU_PROGRESS_CREEP_T0 ))
} }
# cau_progress_detail <label-msgid> <value> # cau_progress_detail <label-msgid> <value>
# A labelled line under the entry's "Details" - which package is being unpacked # A labelled line under the entry's "Details", for example which package is
# right now, say. # being unpacked right now.
cau_progress_detail() { cau_progress_detail() {
local label="$1" value="$2" i fd local label="$1" value="$2" i fd
@@ -216,6 +369,83 @@ cau_progress_detail() {
done done
} }
# How many lines of the run log the job entry carries, and how wide each one
# is allowed to be. Both are display limits rather than arbitrary ones: five
# lines is about what fits under a notification popup before it starts pushing
# the buttons off the bottom, and a line long enough to be elided anyway is
# only costing room in the pipe. Together they also keep one instruction well
# inside PIPE_BUF, which is what makes the write atomic against the runner
# writing its own progress down the same pipe.
CAU_PROGRESS_TAIL_LINES=5
CAU_PROGRESS_TAIL_COLS=120
CAU_PROGRESS_TAIL_PID=0
# cau_progress_log <label-msgid> <text>
# The second description field, holding the last few lines of the run log.
#
# The protocol is one instruction per line, so the tail travels tab separated
# and is put back together on the other side. Tabs inside a log line would
# split it in two on the way, so they become spaces first. The job view
# renders either as whitespace, and a line broken in half renders as nonsense.
cau_progress_log() {
local label="$1" text="$2" i fd
(( ${#CAU_PROGRESS_FDS[@]} )) || return 0
text="${text//$'\t'/ }"
text="${text//$'\n'/$'\t'}"
for i in "${!CAU_PROGRESS_FDS[@]}"; do
fd="${CAU_PROGRESS_FDS[$i]}"
[[ -n $fd ]] || continue
cau_msg_into "${CAU_PROGRESS_LOCALES[$i]}" "$label"
printf 'log\t%s\t%s\n' "$CAU_MSG_RESULT" "$text" >&"$fd" 2>/dev/null || true
done
}
# cau_progress_tail_start <logfile> / cau_progress_tail_stop
# Follows the run log for as long as the update lasts, so expanding "Details"
# shows what the update is actually doing right now.
#
# This is the answer to the part of a run that has no counter and never will:
# an AUR helper compiling for a quarter of an hour says plenty about what it is
# up to, none of it countable, and all of it going into a log file nobody is
# looking at. The percentage says the update is alive; these lines say what it
# is alive doing.
#
# Polled rather than followed with tail -F, for the same reason the pacman
# watcher polls: only the last few lines are ever displayed, so every line in
# between is work nobody would see. Re-read whole and compared, which also
# makes truncation and rotation of the log a non-event.
cau_progress_tail_start() {
local file="$1"
cau_progress_tail_stop
(( ${#CAU_PROGRESS_FDS[@]} )) || return 0
cau_have tail || return 0
{
local nap last='' now
exec {nap}<> <(:)
while :; do
read -r -t 2 -u "$nap" _ || true
now="$(tail -n "$CAU_PROGRESS_TAIL_LINES" "$file" 2>/dev/null \
| cut -c "1-${CAU_PROGRESS_TAIL_COLS}")"
[[ -n $now && $now != "$last" ]] || continue
last="$now"
cau_progress_log "Log" "$now"
done
} &
CAU_PROGRESS_TAIL_PID=$!
}
cau_progress_tail_stop() {
(( CAU_PROGRESS_TAIL_PID )) || return 0
kill "$CAU_PROGRESS_TAIL_PID" 2>/dev/null
wait "$CAU_PROGRESS_TAIL_PID" 2>/dev/null
CAU_PROGRESS_TAIL_PID=0
}
# cau_progress_end [outcome: ok|failed] [failure-msgid] # cau_progress_end [outcome: ok|failed] [failure-msgid]
# Closes the entry. Must run on every exit path, including a killed run: an # 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 # entry whose owner merely vanishes is reported by the desktop as "the
@@ -235,8 +465,14 @@ cau_progress_end() {
local outcome="${1:-ok}" msgid="${2:-}" local outcome="${1:-ok}" msgid="${2:-}"
local i fd local i fd
# The last step never consumes its own share - nothing reports items for # Before the descriptors go: anything still running in the background holds
# the cleanup - so the bar would stop a few percent short of the end and # its own copy of them, so the helper would not see the end of its input
# until it exited. And it is about to be waited on.
cau_progress_creep_stop
cau_progress_tail_stop
# The last step never consumes its own share (nothing reports items for
# the cleanup), so the bar would stop a few percent short of the end and
# vanish there. Only on the way out of a run that actually worked, though: # 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. # filling the bar for a failed update says the opposite of what happened.
[[ $outcome == ok ]] && _cau_progress_line 'percent\t100' [[ $outcome == ok ]] && _cau_progress_line 'percent\t100'