mirror of
https://github.com/henrygd/beszel.git
synced 2026-10-21 22:02:34 +00:00
feat(agent): Handle update package check for xbps package manager (#2522)
Co-authored-by: hank <hank@henrygd.me>
This commit is contained in:
@@ -24,6 +24,9 @@ const (
|
||||
// pacmanSyncInterval limits how often checkupdates downloads fresh sync
|
||||
// databases. Checks in between reuse the last synced copy.
|
||||
pacmanSyncInterval = 12 * time.Hour
|
||||
// xbpsSyncInterval limits how often xbps-install fetches remote repository
|
||||
// data. Checks in between reuse the last fetched update list.
|
||||
xbpsSyncInterval = 12 * time.Hour
|
||||
)
|
||||
|
||||
// packageUpdatesResult is the outcome of one package manager check.
|
||||
@@ -154,6 +157,8 @@ func detectPackageManager(dataDir string) (string, packageUpdatesCheck) {
|
||||
return "pacman", newPacmanCheck(dataDir)
|
||||
case commandExists("apk"):
|
||||
return "apk", checkApk
|
||||
case commandExists("xbps-install"):
|
||||
return "xbps", newXbpsCheck()
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
@@ -509,3 +514,88 @@ func splitApkNameVersion(s string) (name, version string) {
|
||||
}
|
||||
return s[:ver], s[ver+1:]
|
||||
}
|
||||
|
||||
// newXbpsCheck fetches remote repository data into memory (-M) every
|
||||
// xbpsSyncInterval. The system's on-disk copy is only refreshed by xbps-install -S,
|
||||
// so it is often stale, and xbps cannot keep the fetched data. Checks in between
|
||||
// reuse the last fetched update list and drop packages whose installed version
|
||||
// changed since, so local upgrades show up right away. xbps has no security
|
||||
// metadata, so the security count is omitted.
|
||||
func newXbpsCheck() packageUpdatesCheck {
|
||||
// checks never overlap (packageUpdatesManager.running), so no lock is needed
|
||||
var lastSync time.Time
|
||||
var synced []system.PackageUpdate
|
||||
return func(ctx context.Context) (packageUpdatesResult, error) {
|
||||
sync := lastSync.IsZero() || time.Since(lastSync) >= xbpsSyncInterval
|
||||
var updates []system.PackageUpdate
|
||||
if sync {
|
||||
out, err := runPackageCommand(ctx, nil, "xbps-install", "-Mun")
|
||||
if err != nil {
|
||||
return packageUpdatesResult{}, err
|
||||
}
|
||||
updates = parseXbpsSimulate(out)
|
||||
}
|
||||
out, err := runPackageCommand(ctx, nil, "xbps-query", "-l")
|
||||
if err != nil {
|
||||
return packageUpdatesResult{}, err
|
||||
}
|
||||
installed := parseXbpsInstalled(out)
|
||||
if sync {
|
||||
for i := range updates {
|
||||
updates[i].Current = installed[updates[i].Name]
|
||||
}
|
||||
synced = updates
|
||||
lastSync = time.Now()
|
||||
}
|
||||
var packages []system.PackageUpdate
|
||||
for _, pkg := range synced {
|
||||
// skip packages upgraded or removed since the last sync
|
||||
if ver, ok := installed[pkg.Name]; ok && ver == pkg.Current {
|
||||
packages = append(packages, pkg)
|
||||
}
|
||||
}
|
||||
return packageUpdatesResult{counts: []uint16{uint16(len(packages))}, packages: packages}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// parseXbpsSimulate parses upgrades in `xbps-install -Mun` output. Upgrade lines look like
|
||||
// "libgbm-26.2.4_1 update x86_64 https://void.sakamoto.pl/current 18488 6762"
|
||||
func parseXbpsSimulate(out string) (packages []system.PackageUpdate) {
|
||||
scanner := bufio.NewScanner(strings.NewReader(out))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 || fields[1] != "update" {
|
||||
continue
|
||||
}
|
||||
name, ver := parseXbpsNameVersion(fields[0])
|
||||
packages = append(packages, system.PackageUpdate{Name: name, Available: ver})
|
||||
}
|
||||
return packages
|
||||
}
|
||||
|
||||
// parseXbpsInstalled maps package names to installed versions from `xbps-query -l`
|
||||
// output. Lines look like "ii xbps-0.60.7_1 XBPS package system utilities".
|
||||
func parseXbpsInstalled(out string) map[string]string {
|
||||
installed := make(map[string]string)
|
||||
scanner := bufio.NewScanner(strings.NewReader(out))
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
name, ver := parseXbpsNameVersion(fields[1])
|
||||
installed[name] = ver
|
||||
}
|
||||
return installed
|
||||
}
|
||||
|
||||
// parseXbpsNameVersion splits name-version to tuple (name, version)
|
||||
// Names may contain dashes, but versions do not.
|
||||
func parseXbpsNameVersion(s string) (name, ver string) {
|
||||
i := strings.LastIndexByte(s, '-')
|
||||
if i < 0 {
|
||||
return s, ""
|
||||
}
|
||||
return s[:i], s[i+1:]
|
||||
}
|
||||
|
||||
@@ -191,6 +191,66 @@ func TestParseRpmInstalled(t *testing.T) {
|
||||
assert.Equal(t, "5.14.0-503.el9", installed["kernel.x86_64"])
|
||||
}
|
||||
|
||||
func TestParseXbpsSimulate(t *testing.T) {
|
||||
packages := parseXbpsSimulate(readPackageUpdatesTestData(t, "xbps_void_check_update.txt"))
|
||||
assert.Len(t, packages, 3)
|
||||
assert.Equal(t, system.PackageUpdate{Name: "libmariadbclient", Available: "12.3.2_2"}, findPackage(t, packages, "libmariadbclient"))
|
||||
|
||||
packages = parseXbpsSimulate(readPackageUpdatesTestData(t, "xbps_void_install_update.txt"))
|
||||
assert.Len(t, packages, 7)
|
||||
assert.Equal(t, system.PackageUpdate{Name: "ca-certificates", Available: "20250419+3.127_1"}, findPackage(t, packages, "ca-certificates"))
|
||||
assert.Equal(t, system.PackageUpdate{Name: "libcrypto3", Available: "3.6.5_1"}, findPackage(t, packages, "libcrypto3"))
|
||||
// new dependencies are not updates
|
||||
assert.Empty(t, parseXbpsSimulate("libfoo-1.0_1 install x86_64 https://repo-default.voidlinux.org/current 100 50\n"))
|
||||
assert.Empty(t, parseXbpsSimulate("\n"))
|
||||
}
|
||||
|
||||
func TestParseXbpsInstalled(t *testing.T) {
|
||||
installed := parseXbpsInstalled(readPackageUpdatesTestData(t, "xbps_void_query_installed.txt"))
|
||||
assert.Len(t, installed, 29)
|
||||
assert.Equal(t, "0.60.7_1", installed["xbps"])
|
||||
assert.Equal(t, "0.131_1", installed["xbps-triggers"])
|
||||
assert.Equal(t, "20250419+3.127_1", installed["ca-certificates"])
|
||||
}
|
||||
|
||||
func TestXbpsCheckSync(t *testing.T) {
|
||||
binDir := t.TempDir()
|
||||
logFile := filepath.Join(binDir, "calls.log")
|
||||
installedFile := filepath.Join(binDir, "installed.txt")
|
||||
writeInstalled := func(lines string) {
|
||||
require.NoError(t, os.WriteFile(installedFile, []byte(lines), 0o644))
|
||||
}
|
||||
writeInstalled("ii bash-5.2.021_1 GNU Bourne Again Shell\nii libgbm-26.2.3_1 Mesa gbm library\n")
|
||||
fakeCommands(t, map[string]string{
|
||||
"xbps-install": `echo "xbps-install $*" >> ` + logFile + `
|
||||
echo "bash-5.3.9_1 update x86_64 https://repo-default.voidlinux.org/current 100 50"
|
||||
echo "libgbm-26.2.4_1 update x86_64 https://repo-default.voidlinux.org/current 100 50"`,
|
||||
"xbps-query": `cat ` + installedFile,
|
||||
})
|
||||
check := newXbpsCheck()
|
||||
|
||||
// first check fetches remote data
|
||||
result, err := check(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []uint16{2}, result.counts)
|
||||
assert.False(t, result.securityKnown)
|
||||
assert.Equal(t, []system.PackageUpdate{
|
||||
{Name: "bash", Current: "5.2.021_1", Available: "5.3.9_1"},
|
||||
{Name: "libgbm", Current: "26.2.3_1", Available: "26.2.4_1"},
|
||||
}, result.packages)
|
||||
|
||||
// later checks reuse the fetched list and drop locally upgraded packages
|
||||
writeInstalled("ii bash-5.3.9_1 GNU Bourne Again Shell\nii libgbm-26.2.3_1 Mesa gbm library\n")
|
||||
result, err = check(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []uint16{1}, result.counts)
|
||||
assert.Equal(t, []system.PackageUpdate{{Name: "libgbm", Current: "26.2.3_1", Available: "26.2.4_1"}}, result.packages)
|
||||
|
||||
data, err := os.ReadFile(logFile)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "xbps-install -Mun\n", string(data))
|
||||
}
|
||||
|
||||
func TestCheckDnf(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, updates, security, installed string
|
||||
@@ -255,6 +315,7 @@ func TestDetectPackageManager(t *testing.T) {
|
||||
{"zypper-aptitude", map[string]string{"apt-get": "", "zypper": ""}, "zypper"},
|
||||
{"zypper-aptitude with dpkg", map[string]string{"apt-get": "", "dpkg-query": "exit 1", "zypper": ""}, "zypper"},
|
||||
{"dnf", map[string]string{"dnf": "", "rpm": ""}, "dnf"},
|
||||
{"xbps", map[string]string{"xbps-install": ""}, "xbps"},
|
||||
{"none", nil, ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
libgbm-26.2.4_1 update x86_64 https://void.sakamoto.pl/current 18488 6762
|
||||
libmariadbclient-12.3.2_2 update x86_64 https://void.sakamoto.pl/current 27749752 7203074
|
||||
libpipewire-1.6.9_1 update x86_64 https://void.sakamoto.pl/current 4895536 1155261
|
||||
@@ -0,0 +1,7 @@
|
||||
acl-2.4.0_1 update x86_64 https://repo-default.voidlinux.org/current 43080 20653
|
||||
attr-2.6.0_1 update x86_64 https://repo-default.voidlinux.org/current 27348 9495
|
||||
ca-certificates-20250419+3.127_1 update x86_64 https://repo-default.voidlinux.org/current 273335 164765
|
||||
libarchive-3.8.9_1 update x86_64 https://repo-default.voidlinux.org/current 817232 380316
|
||||
libcrypto3-3.6.5_1 update x86_64 https://repo-default.voidlinux.org/current 6443040 2193502
|
||||
libssl3-3.6.5_1 update x86_64 https://repo-default.voidlinux.org/current 1065048 444864
|
||||
openssl-3.6.5_1 update x86_64 https://repo-default.voidlinux.org/current 2270125 635762
|
||||
@@ -0,0 +1,29 @@
|
||||
ii acl-2.4.0_1 Access Control List filesystem support
|
||||
ii attr-2.6.0_1 Extended attribute support library for ACL support
|
||||
ii base-files-0.146_1 Void Linux base system files
|
||||
ii bzip2-1.0.8_2 Freely available, patent free, high-quality data compressor
|
||||
ii ca-certificates-20250419+3.127_1 Common CA certificates for SSL/TLS from Mozilla
|
||||
ii coreutils-9.4_3 GNU core utilities
|
||||
ii dash-0.5.12_3 POSIX-compliant Unix shell, much smaller than GNU bash
|
||||
ii gawk-5.3.2_1 GNU awk utility
|
||||
ii glibc-2.41_1 GNU C library
|
||||
ii gmp-6.3.0_1 Library for arbitrary precision arithmetic
|
||||
ii grep-3.12_1 GNU grep utility
|
||||
ii libarchive-3.8.9_1 Library to read/write several different streaming archive formats
|
||||
ii libcap-2.78_1 POSIX.1e capabilities suite
|
||||
ii libcrypto3-3.6.5_1 Toolkit for Secure Sockets Layer and Transport Layer Security - crypto library
|
||||
ii liblz4-1.10.0_1 LZ4 compression library
|
||||
ii liblzma-5.8.3_1 XZ-format compression library
|
||||
ii libpcre2-10.47_1 Perl Compatible Regular Expressions (2nd version) - shared libraries
|
||||
ii libreadline8-8.2.010_1 GNU Readline Library
|
||||
ii libssl3-3.6.5_1 Toolkit for Secure Sockets Layer and Transport Layer Security - SSL/TLS library
|
||||
ii libxbps-0.60.7_1 XBPS package system utilities - runtime library
|
||||
ii libxcrypt-compat-4.4.38_1 Modern library for one-way hashing of passwords - legacy compatibility
|
||||
ii libzstd-1.5.7_1 Fast real-time compression algorithm
|
||||
ii ncurses-libs-6.6_1 System V Release 4.0 curses emulation library -- shared libraries
|
||||
ii openssl-3.6.5_1 Toolkit for Secure Sockets Layer and Transport Layer Security
|
||||
ii run-parts-5.23.2_1 Run scripts or programs in a directory
|
||||
ii sed-4.10_1 GNU stream editor
|
||||
ii xbps-0.60.7_1 XBPS package system utilities
|
||||
ii xbps-triggers-0.131_1 XBPS triggers for Void Linux
|
||||
ii zlib-1.3.2_1 Compression/decompression Library
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { classifyVersionChange } from "./package-updates"
|
||||
|
||||
// version pairs are taken from real apt, dnf, zypper, pacman and apk output
|
||||
// version pairs are taken from real apt, dnf, zypper, pacman, apk and xbps output
|
||||
test("major, minor and patch use the first differing upstream component", () => {
|
||||
expect(classifyVersionChange("1.10.7-1", "2.0.0-1")).toBe("major")
|
||||
expect(classifyVersionChange("1.10.7-1", "1.11.0-1")).toBe("minor")
|
||||
@@ -10,6 +10,7 @@ test("major, minor and patch use the first differing upstream component", () =>
|
||||
expect(classifyVersionChange("0.21.7-1", "0.21.8.2-1")).toBe("patch")
|
||||
expect(classifyVersionChange("3.3.0-r2", "3.3.7-r0")).toBe("patch")
|
||||
expect(classifyVersionChange("0.7.36-2.fc42", "0.7.37-2.fc42")).toBe("patch")
|
||||
expect(classifyVersionChange("26.2.3_1", "26.2.4_1")).toBe("patch")
|
||||
// missing components count as zero
|
||||
expect(classifyVersionChange("1.2", "1.2.1")).toBe("patch")
|
||||
expect(classifyVersionChange("1.2", "1.3.0")).toBe("minor")
|
||||
@@ -32,6 +33,7 @@ test("revision-only changes", () => {
|
||||
expect(classifyVersionChange("1.36.1-r28", "1.36.1-r31")).toBe("revision")
|
||||
expect(classifyVersionChange("4.4-150400.25.22", "4.4-150400.27.3.2")).toBe("revision")
|
||||
expect(classifyVersionChange("42-30", "42-31")).toBe("revision")
|
||||
expect(classifyVersionChange("12.3.2_1", "12.3.2_2")).toBe("revision")
|
||||
expect(
|
||||
classifyVersionChange("84.87+git20180409.04c9dae-150300.10.20.1", "84.87+git20180409.04c9dae-150300.10.23.1")
|
||||
).toBe("revision")
|
||||
|
||||
@@ -16,7 +16,8 @@ interface SplitVersion {
|
||||
/**
|
||||
* Splits a distro version string into epoch, upstream version and packaging revision.
|
||||
* Works for the Debian ("1:2.3.4-1ubuntu1"), RPM ("2:9.2.390-1.fc42"), pacman ("1.3.7-1")
|
||||
* and apk ("1.2.5-r3") formats. The revision follows the last "-".
|
||||
* apk ("1.2.5-r3") and xbps ("26.2.4_1") formats. The revision follows the last "-",
|
||||
* or the last "_" for xbps, whose versions have no "-".
|
||||
*/
|
||||
function splitVersion(version: string): SplitVersion {
|
||||
let epoch = 0
|
||||
@@ -25,9 +26,12 @@ function splitVersion(version: string): SplitVersion {
|
||||
epoch = Number(epochMatch[1])
|
||||
version = version.slice(epochMatch[0].length)
|
||||
}
|
||||
const dash = version.lastIndexOf("-")
|
||||
if (dash > 0) {
|
||||
return { epoch, upstream: version.slice(0, dash), revision: version.slice(dash + 1) }
|
||||
let sep = version.lastIndexOf("-")
|
||||
if (sep < 0) {
|
||||
sep = version.lastIndexOf("_")
|
||||
}
|
||||
if (sep > 0) {
|
||||
return { epoch, upstream: version.slice(0, sep), revision: version.slice(sep + 1) }
|
||||
}
|
||||
return { epoch, upstream: version, revision: "" }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user