Files
mesh-catalog/modules/pacman/cmd/pacman-tools/history.go
T
jochen d9336d11d0 pacman: the package manager's configuration, mirrors and cache as a module
Mirrors were generated once and never again and caches never cleaned. The
module holds node-package-manager (hq ADR 0207), declares pacman itself, owns
/etc/pacman.conf whole — [options] cannot take an appended block — with the
union of the enabled repositories and improved options, proven by pacman-conf
in its test, and enables reflector.timer (its config owned) and
paccache.timer. Fifteen tools from a Go bundle; transactions run as transient
units so a call's timeout never kills pacman mid-transaction (to-be 42).
2026-10-04 12:50:20 +02:00

126 lines
3.7 KiB
Go

package main
// The package manager's own record, /var/log/pacman.log (novox/hq research 026/05: "installs and
// upgrades from the log"): every install, upgrade, downgrade, reinstall and removal since a date,
// and when the machine was last fully upgraded.
import (
"fmt"
"regexp"
"strings"
"time"
)
// PacmanLog is where pacman writes what it did.
const PacmanLog = "/var/log/pacman.log"
// Event is one package changed by a transaction.
type Event struct {
Time string `json:"time"`
Action string `json:"action"`
Package string `json:"package"`
Version string `json:"version"`
From string `json:"from,omitempty"`
}
var (
logLine = regexp.MustCompile(`^\[([^\]]+)\] \[ALPM\] (installed|upgraded|downgraded|reinstalled|removed) (\S+) \((.*)\)$`)
fullUpdate = regexp.MustCompile(`^\[([^\]]+)\] \[PACMAN\] starting full system upgrade`)
)
// Actions are what a history may be narrowed to.
var Actions = []string{"installed", "upgraded", "downgraded", "reinstalled", "removed"}
func logTime(s string) (time.Time, bool) {
for _, layout := range []string{"2006-01-02T15:04:05-0700", "2006-01-02 15:04"} {
if t, err := time.Parse(layout, s); err == nil {
return t, true
}
}
return time.Time{}, false
}
// ParseLog reads pacman.log's package events since a time, and the last full upgrade it records.
func ParseLog(text string, since time.Time) (events []Event, lastUpgrade time.Time) {
for _, l := range strings.Split(text, "\n") {
if u := fullUpdate.FindStringSubmatch(l); u != nil {
if t, ok := logTime(u[1]); ok {
lastUpgrade = t
}
continue
}
e := logLine.FindStringSubmatch(l)
if e == nil {
continue
}
t, ok := logTime(e[1])
if !ok || t.Before(since) {
continue
}
ev := Event{Time: t.Format(time.RFC3339), Action: e[2], Package: e[3], Version: e[4]}
if from, to, ok := strings.Cut(e[4], " -> "); ok {
ev.From, ev.Version = from, to
}
events = append(events, ev)
}
return events, lastUpgrade
}
// LastUpgrade is when the machine last started a full upgrade, from pacman's log.
func (m *Machine) LastUpgrade() (time.Time, error) {
text, err := m.ReadFile(PacmanLog)
if err != nil {
return time.Time{}, err
}
_, last := ParseLog(string(text), m.Now())
return last, nil
}
// History is the package events since a day (YYYY-MM-DD; thirty days ago by default), narrowed to
// an action and a name, the newest last and at most limit of them.
func (m *Machine) History(since, action, match string, limit int) (map[string]any, error) {
from := m.Now().AddDate(0, 0, -30)
if since != "" {
t, err := time.ParseInLocation("2006-01-02", since, time.Local)
if err != nil {
return nil, fmt.Errorf("since %q is not a day as YYYY-MM-DD", since)
}
from = t
}
if action != "" && !contains(Actions, action) {
return nil, fmt.Errorf("action %q is one of %s", action, strings.Join(Actions, ", "))
}
text, err := m.ReadFile(PacmanLog)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", PacmanLog, err)
}
all, last := ParseLog(string(text), from)
events := []Event{}
counts := map[string]int{}
for _, e := range all {
if action != "" && e.Action != action || match != "" && !strings.Contains(e.Package, match) {
continue
}
events = append(events, e)
counts[e.Action]++
}
truncated := false
if len(events) > limit {
events, truncated = events[len(events)-limit:], true
}
out := map[string]any{"since": from.Format(time.RFC3339), "count": len(events), "by_action": counts, "events": events, "truncated": truncated}
if !last.IsZero() {
out["last_full_upgrade"] = last.Format(time.RFC3339)
}
return out, nil
}
func contains(list []string, want string) bool {
for _, s := range list {
if s == want {
return true
}
}
return false
}