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