From f71a48a84157d0f8e7df67bbe9e8e4b3e61d7842 Mon Sep 17 00:00:00 2001 From: UmedjonBA Date: Sat, 29 Aug 2026 22:24:06 +0300 Subject: [PATCH] arch-updater: maintenance section (cache prune, orphans, fwupd), keyring-first, free-space gate, sleep inhibit, run-log analysis - Maintenance section under the package list: cache size with one-click paccache prune, orphaned packages with confirmed pacman -Rns removal, and a firmware-updates shortcut when fwupd is installed. - Refresh archlinux-keyring right before each update run (toggleable). - Refuse to start an update when the root filesystem is too full. - Hold a systemd sleep/idle inhibitor during background runs. - Scan the finished run's log for known failure signatures (failed initramfs generation, failed transactions, signature errors) and warn in the panel - critically for the initramfs case. Version 2.1.0. Co-Authored-By: Claude Fable 5 --- arch-updater/CHANGELOG.md | 8 + arch-updater/README.md | 73 ++++++++- arch-updater/panel.luau | 112 ++++++++++++- arch-updater/plugin.toml | 48 +++++- arch-updater/service.luau | 264 +++++++++++++++++++++++++++++- arch-updater/translations/en.json | 41 ++++- 6 files changed, 532 insertions(+), 14 deletions(-) diff --git a/arch-updater/CHANGELOG.md b/arch-updater/CHANGELOG.md index c512341e..c8e8b843 100644 --- a/arch-updater/CHANGELOG.md +++ b/arch-updater/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to Arch Updater are documented here. The panel's changelog icon (the history icon next to Check) shows this same file. +## 2.1.0 - 2026-08-29 + +- Added: a maintenance section under the package list — package cache size with one-click pruning (paccache, configurable versions to keep), orphaned packages with confirmed removal (pacman -Rns), and a firmware-updates shortcut when fwupd is installed. +- Added: the Arch keyring is refreshed right before each update run (toggleable), so packages signed with new keys don't fail after a long gap between updates. +- Added: a free-disk-space check before starting an update; the run is refused with a clear message when the root filesystem is too full. +- Added: background update runs hold a systemd sleep/idle inhibitor, so a closed laptop lid can't interrupt a pacman transaction halfway. +- Added: the update log is scanned after every run for known failure signatures — a failed initramfs generation (critical: warns before you reboot), failed transactions, and package signature errors. + ## 2.0.1 - 2026-08-21 - Added: a changelog view in the panel. Click the history icon next to Check to see what changed in each release. It also opens automatically once an update finishes, unless turned off in settings (Show changelog after updating). diff --git a/arch-updater/README.md b/arch-updater/README.md index d1ee9882..1a9c230a 100644 --- a/arch-updater/README.md +++ b/arch-updater/README.md @@ -8,6 +8,10 @@ work as normal (the default, same behavior as before), or fully in the background — polkit asks for your password, the panel shows a live log tail and a progress bar, and a notification reports the result. Either way the run is logged and recorded in an update history with per-package rollback. +A maintenance section keeps an eye on the package cache, orphaned packages +and (with fwupd) firmware updates, and the update log is scanned for known +failure signatures — a failed initramfs generation warns you before you +reboot into it. ## Plugin @@ -21,11 +25,14 @@ run is logged and recorded in an update history with per-package rollback. - `pacman-contrib` on `PATH` (for `checkupdates` and `pactree`), required. - `pacman`, `sh`, `awk`, `sed`, `grep`, `tail`, `head`, `tee`, `wc`, `date`, - `rm`, `install`, `test`, `cat`, `kill` and `uname`, required — base tools - from any standard Arch install (coreutils and friends), used to run and - parse the checks, build the download size estimate, check the running - kernel, follow and open the update log, install the optional polkit rule, - and detect whether a terminal update run's process is still alive. + `rm`, `install`, `test`, `cat`, `kill`, `cut`, `du`, `df` and `uname`, + required — base tools from any standard Arch install (coreutils and + friends), used to run and parse the checks, build the download size + estimate, check the running kernel, measure the package cache and free + disk space, follow and open the update log, install the optional polkit + rule, and detect whether a terminal update run's process is still alive. +- `systemd-inhibit` (systemd), used to block sleep during background update + runs; skipped when absent. - `pkexec` (polkit) with an authentication agent, required for the background update mode and for rollback. Noctalia's built-in polkit agent works out of the box. @@ -33,6 +40,8 @@ run is logged and recorded in an update history with per-package rollback. Auto-detected by default, see the **AUR helper** setting. - `flatpak`, optional, for the Flatpak check and update. - `xdg-open`, optional, to open a package page or the Arch news page. +- `fwupdmgr` (fwupd), optional — when installed, the maintenance section + offers firmware updates in a terminal window. - `less`, plus `sudo` and a terminal emulator, for the terminal update mode (the default), the **Retry in terminal** fallback and the **Open full log** viewer: Noctalia's own terminal detection (`$TERMINAL`, then the @@ -112,6 +121,44 @@ changed: anything declined during an interactive terminal run is left out of the entry, so the history never offers to "undo" an update that did not happen. Failed runs are not recorded. +### Maintenance + +After each check, a small section under the package list reports the state +of the system around updates (all gathered without root): + +- **Package cache** — its size, and how many superseded versions a prune + would remove (from `paccache`'s dry run) with the space they'd free. The + trash button runs the prune through one `pkexec` call: it keeps the + configured number of versions of each installed package (**Cached + versions to keep**, default 2) and drops cached files of uninstalled + packages entirely. +- **Orphaned packages** — the `pacman -Qtdq` list. The trash button (a + second click confirms, like rollback) removes them with their unneeded + dependencies via `pkexec pacman -Rns`; the tooltip lists what would go. + The scan repeats afterwards, since removing orphans can orphan the next + layer. +- **Firmware updates** — shown only when `fwupdmgr` is installed; the + button opens a terminal running `fwupdmgr refresh; fwupdmgr update`. + +Around the update run itself: + +- **Keyring first** (on by default): each run starts with + `pacman -Sy --needed archlinux-keyring`, so packages signed with new keys + don't fail after a long gap between updates. +- **Free-space check**: an update is refused up front when the root + filesystem has less than **Minimum free space** (default 1 GiB) plus + twice the estimated download size free — better than pacman dying on a + full disk halfway through a transaction. +- **Sleep inhibitor**: background runs hold a systemd sleep/idle inhibitor + so a closed lid can't interrupt a transaction. +- **Log analysis**: when a run finishes, its log is scanned for known + failure signatures — a failed initramfs generation (shown in red and + raised as a critical notification: check it before rebooting), a failed + pacman transaction, and package signature errors. + +The whole section can be turned off with the **Maintenance section** +setting. + ### Activity graph The optional activity graph (off by default, **Show activity graph**) @@ -186,6 +233,11 @@ setting. | `show_activity_graph` | `bool` | `false` | Track pending-update counts across checks and show them as a small graph. Off also stops recording. | | `activity_history_length` | `int` | `10` | How many recent checks the activity graph keeps (3–30). | | `update_mode` | `select` | `terminal` | How **Update** runs the upgrade: in a terminal window (interactive, like before) or in the background (non-interactive). | +| `keyring_first` | `bool` | `true` | Refresh `archlinux-keyring` (`pacman -Sy --needed`) right before each update run. | +| `inhibit_sleep` | `bool` | `true` | Hold a systemd sleep/idle inhibitor for the duration of a background update run. | +| `min_free_mib` | `int` | `1024` | Refuse to start an update when root has less than this (plus twice the download estimate) free. `0` disables. | +| `maintenance_enabled` | `bool` | `true` | Scan and show the maintenance section (cache, orphans, firmware) after each check. | +| `cache_keep` | `int` | `2` | How many versions of each installed package the cache prune keeps (1–5). | | `rollback_auto_ignore` | `bool` | `false` | After a successful rollback, add the rolled-back packages to the plugin's ignore list. | | `hide_polkit_hint` | `bool` | `false` | Hide the panel line offering to install the polkit keep-authorization rule. | | `log_lines` | `int` | `14` | How many of the latest update-log lines the panel shows during a run (6–30). | @@ -205,6 +257,10 @@ noctalia msg plugin yuuto/arch-updater:service all update_terminal noctalia msg plugin yuuto/arch-updater:service all dismiss noctalia msg plugin yuuto/arch-updater:service all ignore:NAME noctalia msg plugin yuuto/arch-updater:service all unignore:NAME +noctalia msg plugin yuuto/arch-updater:service all prune_cache +noctalia msg plugin yuuto/arch-updater:service all remove_orphans +noctalia msg plugin yuuto/arch-updater:service all fwupd_update +noctalia msg plugin yuuto/arch-updater:service all maintenance ``` `update` follows the **Update mode** setting; `update_background` and @@ -228,6 +284,13 @@ panel-managed ignore list. reverse-dependency warning. After a successful run, `pacman -Q` verifies which packages actually changed before the history entry is written. **Open full log** shows the log with `less` in your terminal. + Maintenance scan: `du -sb` and `df -Pk` for sizes, `pacman -Qtdq` for + orphans, `paccache -dk` (dry run) for prunable versions; the finished + run's log is checked with `grep` for known failure signatures. Prune: + one `pkexec sh -c 'paccache -rk; paccache -ruk0'`; orphan removal: + `pkexec pacman -Rns` on the scanned list; firmware: + `fwupdmgr refresh; fwupdmgr update` in your terminal. Background runs + are wrapped in `systemd-inhibit --what=sleep:idle`. - **Privileges.** Escalation happens only through polkit: `pkexec pacman` for repo packages and rollback, and the AUR helper escalates its install steps through `pkexec` itself (`--sudo pkexec`). AUR builds run diff --git a/arch-updater/panel.luau b/arch-updater/panel.luau index 4789d3e7..646c2a1a 100644 --- a/arch-updater/panel.luau +++ b/arch-updater/panel.luau @@ -701,7 +701,7 @@ end -- Two-click rollback: the first click arms the button (it turns into a -- confirm label), the second sends the request. Anything else re-renders -- the armed state away. -local function rollbackButton(key, tooltip, onConfirm) +local function rollbackButton(key, tooltip, onConfirm, glyph) if armedKey == key then return ui.button({ key = key .. "-armed", @@ -715,7 +715,7 @@ local function rollbackButton(key, tooltip, onConfirm) end return ui.button({ key = key, - glyph = "undo", variant = "ghost", controlSize = "sm", width = 22, height = 22, glyphSize = 12, + glyph = glyph or "undo", variant = "ghost", controlSize = "sm", width = 22, height = 22, glyphSize = 12, tooltip = tooltip, onClick = function() armedKey = key @@ -976,6 +976,90 @@ local function extras() return lines end +-- ── Maintenance: cache, orphans, firmware ──────────────────────────────────── + +local function formatGib(bytes) + return string.format("%.1f", (tonumber(bytes) or 0) / 1073741824) +end + +local function maintenanceSection() + if snapshot == nil or type(snapshot.maintenance) ~= "table" or busy() or changelogOpen or openedRunAt ~= nil then + return nil + end + local m = snapshot.maintenance + local busyM = snapshot.maintenanceBusy == true + local rows = {} + + -- Cache size plus what a prune would free (paccache dry run). + if tonumber(m.cacheBytes) ~= nil then + local text = tr("maint_cache", { size = formatGib(m.cacheBytes) }) + if (tonumber(m.pruneCount) or 0) > 0 then + text = text .. " · " .. tr("maint_cache_prunable", { count = tostring(m.pruneCount), saved = tostring(m.pruneSaved or "") }) + end + local row = { + ui.glyph({ name = "database", size = 13, color = "on_surface_variant" }), + ui.label({ text = text, fontSize = 12, color = "on_surface_variant", flexGrow = 1, maxLines = 2 }), + } + if (tonumber(m.pruneCount) or 0) > 0 then + table.insert(row, ui.button({ + key = "maint-prune" .. (busyM and "-off" or ""), + glyph = "trash", variant = "ghost", controlSize = "sm", width = 22, height = 22, glyphSize = 12, + enabled = not busyM, + tooltip = tr("tip_prune"), + onClick = function() + request("prune_cache") + end, + })) + end + table.insert(rows, ui.row({ key = "maint-cache", gap = 6, align = "center" }, row)) + end + + -- Orphans go through the same armed confirm as rollback: their removal + -- is a root pacman -Rns. + local orphans = type(m.orphans) == "table" and m.orphans or {} + if #orphans > 0 and not busyM then + local names = table.concat(orphans, ", ") + if #names > 200 then + names = names:sub(1, 200) .. "…" + end + table.insert(rows, ui.row({ key = "maint-orphans", gap = 6, align = "center" }, { + ui.glyph({ name = "package-off", size = 13, color = "on_surface_variant" }), + ui.label({ + text = noctalia.trp("maint_orphans", #orphans, { count = #orphans }), + fontSize = 12, color = "on_surface_variant", flexGrow = 1, + }), + rollbackButton("maint-orphans-rm", tr("tip_orphans", { names = names }), function() + request("remove_orphans") + end, "trash"), + })) + end + + -- Firmware (fwupd): shown only when the tool is on the system; the + -- update itself is interactive, so it opens a terminal. + if m.fwupd == true then + table.insert(rows, ui.row({ key = "maint-fwupd", gap = 6, align = "center" }, { + ui.glyph({ name = "cpu", size = 13, color = "on_surface_variant" }), + ui.label({ text = tr("maint_fwupd"), fontSize = 12, color = "on_surface_variant", flexGrow = 1 }), + ui.button({ + glyph = "external-link", variant = "ghost", controlSize = "sm", width = 22, height = 22, glyphSize = 12, + tooltip = tr("tip_fwupd"), + onClick = function() + request("fwupd_update") + end, + }), + })) + end + + if busyM then + table.insert(rows, ui.label({ key = "maint-busy", text = tr("maint_busy"), fontSize = 11, color = "secondary" })) + end + + if #rows == 0 then + return nil + end + return ui.column({ key = "maintenance", gap = 4 }, rows) +end + local function body() local children = {} @@ -1019,6 +1103,11 @@ local function body() table.insert(children, line) end + local maint = maintenanceSection() + if maint ~= nil then + table.insert(children, maint) + end + if not busy() then local clean = cleanSources() if #clean > 0 then @@ -1087,6 +1176,25 @@ render = function() ui.label({ text = text, color = color, maxLines = 2 }), } + -- Known failure signatures found in the last run's log: a broken + -- initramfs must be seen before the next reboot, not after. + if not busy() and snapshot ~= nil and type(snapshot.logWarnings) == "table" then + for _, tag in ipairs(snapshot.logWarnings) do + local key = tag == "INITRAMFS" and "warn_initramfs" + or tag == "COMMIT" and "warn_commit" + or tag == "SIGNATURE" and "warn_signature" + or nil + if key ~= nil then + table.insert(children, ui.label({ + text = tr(key), + fontSize = 11, + color = tag == "SIGNATURE" and "warning" or "error", + maxLines = 2, + })) + end + end + end + for _, node in ipairs(body()) do table.insert(children, node) end diff --git a/arch-updater/plugin.toml b/arch-updater/plugin.toml index bf93293e..95908ec2 100644 --- a/arch-updater/plugin.toml +++ b/arch-updater/plugin.toml @@ -1,12 +1,12 @@ id = "yuuto/arch-updater" name = "Arch Updater" -version = "2.0.1" +version = "2.1.0" plugin_api = 9 author = "yuuto" license = "MIT" icon = "package" -description = "Check pacman, AUR and Flatpak updates, then upgrade in a terminal or in the background, with run history and rollback." -dependencies = ["pacman-contrib", "awk", "cat", "date", "flatpak", "grep", "head", "install", "kill", "less", "pacman", "paru", "pkexec", "rm", "sed", "sh", "sudo", "tail", "tee", "test", "uname", "wc", "xdg-open", "yay"] +description = "Check pacman, AUR and Flatpak updates, upgrade in a terminal or the background, with history, rollback and maintenance." +dependencies = ["pacman-contrib", "awk", "cat", "cut", "date", "df", "du", "flatpak", "grep", "head", "install", "kill", "less", "pacman", "paru", "pkexec", "rm", "sed", "sh", "sudo", "systemd-inhibit", "tail", "tee", "test", "uname", "wc", "xdg-open", "yay"] tags = ["arch", "bar", "panel", "launcher", "system", "utility"] # ── General ────────────────────────────────────────────────────────────────── @@ -123,6 +123,48 @@ label_key = "settings.show_changelog_after_update.label" description_key = "settings.show_changelog_after_update.description" default = true +[[setting]] +key = "keyring_first" +type = "bool" +label_key = "settings.keyring_first.label" +description_key = "settings.keyring_first.description" +default = true + +[[setting]] +key = "inhibit_sleep" +type = "bool" +label_key = "settings.inhibit_sleep.label" +description_key = "settings.inhibit_sleep.description" +default = true + +[[setting]] +key = "min_free_mib" +type = "int" +label_key = "settings.min_free_mib.label" +description_key = "settings.min_free_mib.description" +default = 1024 +min = 0 +max = 16384 + +# ── Maintenance ────────────────────────────────────────────────────────────── + +[[setting]] +key = "maintenance_enabled" +type = "bool" +label_key = "settings.maintenance_enabled.label" +description_key = "settings.maintenance_enabled.description" +default = true + +[[setting]] +key = "cache_keep" +type = "int" +label_key = "settings.cache_keep.label" +description_key = "settings.cache_keep.description" +default = 2 +min = 1 +max = 5 +visible_when = { key = "maintenance_enabled", values = ["true"] } + [[setting]] key = "rollback_auto_ignore" type = "bool" diff --git a/arch-updater/service.luau b/arch-updater/service.luau index e11d9bc2..0657f149 100644 --- a/arch-updater/service.luau +++ b/arch-updater/service.luau @@ -101,6 +101,9 @@ local runKind = "update" -- update|rollback: what the current run does local runMode = "background" -- background|terminal: how the current run was launched local runPackages = nil -- packages of the current run, for the history entry local probeResult = nil -- cache/required-by info for one run, {at, pkgs} +local maintenance = nil -- { cacheBytes, pruneCount, pruneSaved, orphans, availKiB, fwupd } +local maintenanceBusy = false -- a prune / orphan-removal operation is running +local logWarnings = {} -- known failure signatures found in the last run's log local startCheck local checkNews @@ -553,6 +556,9 @@ local function publish() runMode = runMode, probe = probeResult, flatpakEnabled = cfg("flatpak_enabled") == true and noctalia.commandExists("flatpak"), + maintenance = maintenance, + maintenanceBusy = maintenanceBusy, + logWarnings = logWarnings, logTail = logTail, logPath = logPath(), runExit = runExit, @@ -565,6 +571,7 @@ end local checkFlatpak local checkSize local checkReboot +local checkMaintenance local finishCheck local function failCheck(message) @@ -576,7 +583,7 @@ end checkReboot = function() if cfg("check_reboot_needed") ~= true then rebootRecommended = false - finishCheck() + checkMaintenance() return end -- A kernel upgrade replaces the whole /usr/lib/modules/ tree. @@ -587,14 +594,76 @@ checkReboot = function() [[test -d "/usr/lib/modules/$(uname -r)" && echo present || echo missing]], function(result) rebootRecommended = trim(result.stdout or "") == "missing" - finishCheck() + checkMaintenance() end, FAST_TIMEOUT_MS ) if not started then rebootRecommended = false + checkMaintenance() + end +end + +-- ── Maintenance: cache size, prunable versions, orphans, free space ────────── + +local function cacheKeep() + return math.max(1, math.min(5, tonumber(cfg("cache_keep")) or 2)) +end + +-- One cheap scan (no root): cache size and prunable-version count from +-- paccache's dry run, orphaned packages, and the root filesystem's free +-- space (which also gates update runs, see freeSpaceShortfall). +local function runMaintenanceScan(done) + local parts = { + [[printf '::CACHE %s\n' "$(du -sb /var/cache/pacman/pkg 2>/dev/null | cut -f1)"]], + [[printf '::AVAIL %s\n' "$(df -Pk / 2>/dev/null | awk 'NR==2{print $4}')"]], + [[pacman -Qtdq 2>/dev/null | sed 's/^/::ORPHAN /']], + } + if noctalia.commandExists("paccache") then + table.insert(parts, "LC_ALL=C paccache -dk" .. cacheKeep() + .. [[ 2>/dev/null | sed -n 's/.*finished dry run: \([0-9]*\) candidates (disk space saved: \([^)]*\)).*/::PRUNE \1|\2/p']]) + end + local started = noctalia.runAsync(table.concat(parts, "; "), function(result) + local scanned = { + cacheBytes = nil, + pruneCount = 0, + pruneSaved = "", + orphans = {}, + availKiB = nil, + fwupd = noctalia.commandExists("fwupdmgr"), + } + for line in (result.stdout or ""):gmatch("[^\n]+") do + local cache = line:match("^::CACHE (%d+)") + local avail = line:match("^::AVAIL (%d+)") + local orphan = line:match("^::ORPHAN (%S+)") + local pruneN, pruneSaved = line:match("^::PRUNE (%d+)|(.*)$") + if cache ~= nil then + scanned.cacheBytes = tonumber(cache) + elseif avail ~= nil then + scanned.availKiB = tonumber(avail) + elseif orphan ~= nil and validName(orphan) then + table.insert(scanned.orphans, orphan) + elseif pruneN ~= nil then + scanned.pruneCount = tonumber(pruneN) or 0 + scanned.pruneSaved = trim(pruneSaved) + end + end + maintenance = scanned + done() + end, SIZE_TIMEOUT_MS) + if not started then + done() + end +end + +checkMaintenance = function() + -- nil (setting not materialized yet) means the manifest default: on. + if cfg("maintenance_enabled") == false then + maintenance = nil finishCheck() + return end + runMaintenanceScan(finishCheck) end checkSize = function() @@ -1042,6 +1111,13 @@ local function buildBackgroundCommand() local helper = resolveAurHelper() local parts = {} + -- Fresh keyring first: after a few weeks without updates the old keyring + -- can no longer verify newly signed packages, and the whole run dies on + -- "signature is unknown trust". --needed makes this a no-op when the + -- keyring is already current. nil (unmaterialized setting) = default on. + if cfg("keyring_first") ~= false then + table.insert(parts, "pkexec pacman -Sy --needed" .. quietFlags .. " archlinux-keyring") + end if helper ~= nil and helper ~= "custom" and noctalia.commandExists(helper) then local autoAnswers = helper == "paru" and " --skipreview" or " --answerdiff None --answerclean None --answeredit None" @@ -1067,6 +1143,11 @@ local function buildTerminalCommand() local helper = resolveAurHelper() local parts = {} + -- Same keyring-first refresh as the background command, via sudo since + -- the terminal run has a tty for the password. + if cfg("keyring_first") ~= false then + table.insert(parts, "sudo pacman -Sy --needed --noconfirm archlinux-keyring") + end if helper ~= nil and helper ~= "custom" and noctalia.commandExists(helper) then table.insert(parts, helper .. " -Syu" .. ignoreFlag) else @@ -1158,10 +1239,73 @@ local function clearRunMeta() end end +-- Refuses to start a run when the root filesystem is too full: pacman +-- aborting halfway on ENOSPC is exactly the broken state this plugin exists +-- to avoid. Returns "have GiB", "need GiB" strings, or nil when fine (or +-- unknown - never block on missing data). min_free_mib = 0 disables the gate. +local function freeSpaceShortfall() + local minFree = tonumber(cfg("min_free_mib")) or 1024 + if minFree <= 0 or maintenance == nil or tonumber(maintenance.availKiB) == nil then + return nil + end + local requiredKiB = minFree * 1024 + if type(downloadSizeMiB) == "number" then + -- Twice the download: the archives land in the cache and unpack. + requiredKiB += math.floor(downloadSizeMiB * 2 * 1024) + end + if maintenance.availKiB < requiredKiB then + return string.format("%.1f", maintenance.availKiB / 1048576), string.format("%.1f", requiredKiB / 1048576) + end + return nil +end + +local function blockOnFreeSpace() + local have, need = freeSpaceShortfall() + if have == nil then + return false + end + phase = "error" + errMsg = tr("err_no_space", { have = have, need = need }) + publish() + noctalia.notifyError(tr("title"), errMsg) + return true +end + +-- Known failure signatures in the finished run's log. An initramfs that +-- failed to generate can leave the machine unbootable, so that one also +-- raises a critical notification; the panel shows the rest. +local function analyzeRunLog() + local path = logPath() + if path == nil then + return + end + local cmd = "log=" .. shellQuote(path) .. "; " + .. [[if grep -qE 'Updating linux initcpios|Running build hook' "$log" 2>/dev/null && ! grep -q 'Initcpio image generation successful' "$log" 2>/dev/null; then echo INITRAMFS; fi]] + .. [[; grep -q 'error: failed to commit transaction' "$log" 2>/dev/null && echo COMMIT]] + .. [[; grep -qiE 'signature (from .* )?is (unknown trust|invalid)' "$log" 2>/dev/null && echo SIGNATURE; true]] + noctalia.runAsync(cmd, function(result) + local warnings = {} + for line in (result.stdout or ""):gmatch("[^\n]+") do + local tag = trim(line) + if tag == "INITRAMFS" or tag == "COMMIT" or tag == "SIGNATURE" then + table.insert(warnings, tag) + if tag == "INITRAMFS" then + noctalia.notifyError(tr("title"), tr("warn_initramfs")) + end + end + end + if #warnings > 0 or #logWarnings > 0 then + logWarnings = warnings + publish() + end + end, FAST_TIMEOUT_MS) +end + local function beginRun(kind, expectTotal, mode) phase = "running" errMsg = nil step = "" + logWarnings = {} runKind = kind or "update" runMode = mode or "background" runExit = nil @@ -1209,9 +1353,20 @@ local function runUpdate() -- so the escalated pacman inherits it. Terminal runs deliberately keep -- the user's locale (their window, their language), at the cost of the -- progress count there. + if blockOnFreeSpace() then + return + end + local quoted = shellQuote(path) + local body = "{ export LC_ALL=C; " .. buildBackgroundCommand() .. " ; } >> " .. quoted .. " 2>&1" + -- systemd-inhibit keeps the machine from sleeping mid-transaction: a + -- laptop lid closed during a pacman commit is a half-written system. + if cfg("inhibit_sleep") ~= false and noctalia.commandExists("systemd-inhibit") then + body = "systemd-inhibit --what=sleep:idle --who=arch-updater --why='System update in progress' sh -c " + .. shellQuote(body) + end local script = "printf '::START %s\\n' \"$(date +%s)\" > " .. quoted - .. "; { export LC_ALL=C; " .. buildBackgroundCommand() .. " ; } >> " .. quoted .. " 2>&1" + .. "; " .. body .. "; printf '::EXIT %s\\n' \"$?\" >> " .. quoted if not noctalia.runAsync(script) then @@ -1229,6 +1384,9 @@ local function runUpdateTerminal() if phase == "running" or phase == "checking" then return end + if blockOnFreeSpace() then + return + end local path = logPath() if path == nil then return @@ -1310,6 +1468,11 @@ end finishRun = function(code) runExit = code runStaleS = 0 + -- The log is complete once ::EXIT landed; scan it for known failure + -- signatures either way - a run can exit 0 with a broken initramfs. + if code ~= TERMINAL_CLOSED_CODE then + analyzeRunLog() + end if code == 0 then if runKind == "rollback" then -- One atomic pacman -U transaction: exit 0 means it all applied. @@ -1689,6 +1852,93 @@ done]] end, SIZE_TIMEOUT_MS) end +-- ── Maintenance actions ────────────────────────────────────────────────────── + +local function refreshMaintenance() + if cfg("maintenance_enabled") == false then + return + end + runMaintenanceScan(publish) +end + +-- Prunes superseded package versions from the cache (keeping cache_keep of +-- each installed package) and every version of uninstalled ones. One pkexec +-- sh so polkit asks once, not per paccache call. +local function pruneCache() + if maintenanceBusy or not noctalia.commandExists("paccache") or not noctalia.commandExists("pkexec") then + return + end + maintenanceBusy = true + publish() + local cmd = "pkexec sh -c " .. shellQuote("paccache -rk" .. cacheKeep() .. "; paccache -ruk0") + local started = noctalia.runAsync(cmd, function(result) + maintenanceBusy = false + if result.exitCode == 0 then + noctalia.notify(tr("title"), tr("notify_prune_ok")) + runMaintenanceScan(publish) + elseif result.exitCode ~= 126 then + -- 126 = the polkit dialog was dismissed; anything else failed. + noctalia.notifyError(tr("title"), tr("err_prune_failed")) + publish() + else + publish() + end + end, 120000) + if not started then + maintenanceBusy = false + publish() + end +end + +-- Removes the orphans the last scan found (pacman -Qtdq), recursively with +-- their now-unneeded deps. The panel arms a confirm before sending this. +local function removeOrphans() + if maintenanceBusy or maintenance == nil or #maintenance.orphans == 0 or not noctalia.commandExists("pkexec") then + return + end + local names = {} + for _, name in ipairs(maintenance.orphans) do + if validName(name) then + table.insert(names, shellQuote(name)) + end + end + if #names == 0 then + return + end + maintenanceBusy = true + publish() + local cmd = "pkexec pacman -Rns --noconfirm --noprogressbar --color never " .. table.concat(names, " ") + local started = noctalia.runAsync(cmd, function(result) + maintenanceBusy = false + if result.exitCode == 0 then + noctalia.notify(tr("title"), tr("notify_orphans_ok")) + -- Removing orphans can orphan their own former deps; the rescan + -- shows the next layer instead of pretending it's all gone. + runMaintenanceScan(publish) + elseif result.exitCode ~= 126 then + noctalia.notifyError(tr("title"), tr("err_orphans_failed")) + publish() + else + publish() + end + end, 120000) + if not started then + maintenanceBusy = false + publish() + end +end + +-- Firmware updates are interactive by nature (reboots, prompts), so they +-- get a terminal window rather than a background run. +local function fwupdUpdate() + if not noctalia.commandExists("fwupdmgr") then + return + end + if not launchTerminal("fwupdmgr refresh; fwupdmgr update; echo; echo " .. shellQuote(tr("run.press_key")) .. "; read -n 1") then + noctalia.notifyError(tr("title"), tr("err_no_terminal")) + end +end + -- ── Requests, lifecycle ────────────────────────────────────────────────────── -- Persists in IGNORE_FILE and takes effect immediately by moving the @@ -1778,6 +2028,14 @@ local function handle(action, payload) openLog() elseif action == "polkit_install" then installPolkitRule() + elseif action == "prune_cache" then + pruneCache() + elseif action == "remove_orphans" then + removeOrphans() + elseif action == "fwupd_update" then + fwupdUpdate() + elseif action == "maintenance" then + refreshMaintenance() elseif action == "dismiss" then sources.pacman = { n = 0, items = {} } sources.aur = { n = 0, items = {}, helper = sources.aur.helper } diff --git a/arch-updater/translations/en.json b/arch-updater/translations/en.json index 1ddb85a7..8a58c018 100644 --- a/arch-updater/translations/en.json +++ b/arch-updater/translations/en.json @@ -47,9 +47,12 @@ "err_flatpak_failed": "Flatpak check failed, see the system log for details", "err_no_checkupdates": "checkupdates not found, install pacman-contrib and check your PATH", "err_no_pkexec": "pkexec not found, install polkit to update in the background", + "err_no_space": "Not enough free disk space: {have} GiB free, about {need} GiB needed. Pruning the package cache may help.", "err_no_terminal": "No terminal emulator found, set one in the plugin settings", "err_no_xdg_open": "xdg-open not found, cannot open the package page", + "err_orphans_failed": "Orphan removal failed, see the log", "err_polkit_install": "Could not install the polkit rule, see the system log for details", + "err_prune_failed": "Cache pruning failed, see the log", "err_run_failed": "Update failed (exit {code}), check the log or retry in a terminal", "err_spawn": "Could not run checkupdates", "err_terminal_closed": "The terminal window closed before the update finished, check the log or retry in a terminal", @@ -68,12 +71,22 @@ }, "log_title": "Update log", "log_waiting": "Waiting for the polkit password prompt and the first output…", + "maint_busy": "Maintenance running…", + "maint_cache": "Package cache: {size} GiB", + "maint_cache_prunable": "{count} old versions ({saved}) prunable", + "maint_fwupd": "Firmware updates (fwupd)", + "maint_orphans": { + "one": "1 orphaned package", + "other": "{count} orphaned packages" + }, "more_packages": "+{count} more", "news_unread": { "one": "1 unread Arch news post: \"{title}\"", "other": "{count} unread Arch news posts, latest: \"{title}\"" }, + "notify_orphans_ok": "Orphaned packages removed", "notify_polkit_ok": "Polkit rule installed: one password per update run from now on", + "notify_prune_ok": "Package cache pruned", "notify_rollback_ok": "Rollback finished successfully", "notify_run_ok": "Update finished successfully", "notify_updates": { @@ -123,6 +136,10 @@ "description": "Check for updates automatically every N hours. 0 (default) never checks on its own.", "label": "Auto-check interval (hours)" }, + "cache_keep": { + "label": "Cached versions to keep", + "description": "How many versions of each installed package the cache prune keeps. Uninstalled packages' cached files are removed entirely." + }, "check_arch_news": { "description": "Fetch the Arch Linux news feed and flag unread posts in the panel.", "label": "Check Arch Linux news" @@ -151,10 +168,26 @@ "description": "Package names to leave out of the count and pass as --ignore to the update itself, in addition to pacman.conf's own IgnorePkg.", "label": "Ignore packages" }, + "inhibit_sleep": { + "label": "Block sleep during updates", + "description": "Background update runs hold a systemd sleep/idle inhibitor, so a closed lid can't interrupt a pacman transaction halfway." + }, + "keyring_first": { + "label": "Refresh the keyring first", + "description": "Run pacman -Sy --needed archlinux-keyring right before each update, so packages signed with new keys don't fail after a long gap between updates." + }, "log_lines": { "description": "How many of the latest update-log lines the panel shows while an update is running.", "label": "Log lines in the panel" }, + "maintenance_enabled": { + "label": "Maintenance section", + "description": "After each check, also report the package cache size (with one-click pruning), orphaned packages (with removal) and firmware updates when fwupd is installed." + }, + "min_free_mib": { + "label": "Minimum free space (MiB)", + "description": "Refuse to start an update when the root filesystem has less than this free, plus twice the estimated download size. 0 disables the check." + }, "notify_on_updates": { "description": "Send a desktop notification when a check finds packages to upgrade.", "label": "Notify when updates are found" @@ -221,10 +254,13 @@ "tip_check": "Check for updates now", "tip_close": "Close", "tip_copy": "Copy name and versions", + "tip_fwupd": "Check and install firmware updates in a terminal window", "tip_history_segment": "Click to see this run's packages", "tip_ignore": "Ignore this package: hide it from the count and skip it on update", "tip_open_page": "Open package page", + "tip_orphans": "Remove them with their unneeded dependencies (pacman -Rns): {names}", "tip_polkit_install": "Install the polkit rule so updates can run in the background with one password prompt", + "tip_prune": "Remove superseded package versions from the cache (root, one polkit prompt)", "tip_rollback": "Roll back to {version}, together with its dependencies from this run. pacman refuses the whole transaction if it would break other packages.", "tip_rollback_run": "Downgrade every package of this run in one transaction", "tip_run_terminal": "Open a terminal window where prompts work normally", @@ -243,5 +279,8 @@ "tooltip_reboot_key": "Reboot", "tooltip_reboot_value": "recommended", "tooltip_status": "Status", - "up_to_date": "Up to date: {sources}" + "up_to_date": "Up to date: {sources}", + "warn_commit": "The last run had a failed pacman transaction — check the log", + "warn_initramfs": "The last run may have FAILED to generate the initramfs — check the log before rebooting", + "warn_signature": "The last run hit package signature errors — a keyring refresh usually fixes this" }