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).
126 lines
3.7 KiB
Go
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
|
|
}
|