issue 1841/S10: return a node to a run's record, saying first what each package goes back to and from where

pacman_rollback plans from the bus: each package now -> record from the node's cache, else the archive's
exact version, else the archive's repository on the record's date; added packages listed and removed only
when asked; foreign packages untouched. apply is an ask the node's keeper runs as a rollback run through the
root helper's new Rollback act (RootActs, upgrades.go), with rollback.started, rollback.finished and the
after-upgrade check (TakeAsk's rollback case, transaction.go).
This commit is contained in:
2026-10-22 14:44:24 +02:00
parent 71f47c3291
commit 43fccb843c
8 changed files with 554 additions and 0 deletions
@@ -195,6 +195,10 @@ func (noRoot) Journal(unit string, n int) ([]string, error) {
return nil, errors.New("the root helper's client is not built in yet: no journal can be read")
}
func (noRoot) Rollback(run string, plan []RollbackItem) error {
return errors.New("the root helper's client is not built in yet: no rollback can start")
}
func (noRoot) Maps() ([]Mapped, error) {
return nil, errors.New("the root helper's client is not built in yet: no process's maps can be read")
}
+36
View File
@@ -265,6 +265,42 @@ func tools(m *Machine) []stdio.Tool {
return ListUpgrades(upgradeState(StateRuns), v[0], v[1], v[2], v[3], n)
},
},
{
Name: "pacman_rollback",
Description: "Return a node to the package record a run took (ADR 0508 §7). Without apply it says what that would change: each package " +
"from its version now to the record's and where that version comes from (the node's package cache, the Arch Linux Archive's copy " +
"of that exact version, or the archive's repository on the record's date), the packages installed since (removed only with " +
"remove_added), and no foreign package. With apply it asks the node's keeper to carry it out as a run of its own, followed by " +
"the after-upgrade check; pacman_upgrades follows it. This node's by default.",
Input: schema(map[string]any{
"node": nodeArg,
"run": map[string]any{"type": "string", "description": "the run whose record to return to (pacman_upgrades lists them)"},
"remove_added": map[string]any{"type": "boolean", "description": "also remove the packages installed since the record"},
"apply": map[string]any{"type": "boolean", "description": "carry it out"},
}, "run"),
Run: func(args map[string]any) (any, error) {
node, err := nodeOf(args)
if err != nil {
return nil, err
}
run, err := text(args, "run", true)
if err != nil {
return nil, err
}
if !nodeName.MatchString(run) {
return nil, fmt.Errorf("run %q is not a run's id", run)
}
remove, err := flag(args, "remove_added")
if err != nil {
return nil, err
}
apply, err := flag(args, "apply")
if err != nil {
return nil, err
}
return m.Rollback(node, run, remove, apply)
},
},
{
Name: "pacman_orphans",
Description: "Packages installed as dependencies that nothing requires any more (pacman -Qdt), with versions.",
+300
View File
@@ -0,0 +1,300 @@
package main
// A rollback (novox/hq ADR 0508 §7; acceptance 1841/AC8): a node is returned to the record a run took, and says
// first what that would change. The plan compares the record's packages with the node's `installed` now: each
// package whose version differs, or that is gone since, is brought back to the record's version, from the node's
// package cache when it holds that file, else from the Arch Linux Archive's copy of that exact version, else from
// the archive's repository as it stood on the record's date. The packages installed since are listed, and are
// removed only when asked. A foreign package, in the record or now, is never in the plan (ADR 0507 §7).
//
// The archive keeps every version its repositories published, so a version is taken from it by its file's name,
// which needs the package's architecture: the architecture of any of the package's files in the cache, else of
// its installed version (the local database). A package whose architecture is not known on this machine is
// taken from the dated repository, where pacman finds it by name. Nothing is fetched to make the plan; a file
// the archive does not hold fails the transaction, which the run then says.
//
// With apply, pacman_rollback writes an ask; the node's keeper takes it (TakeAsk), makes the plan again on the
// node itself (its cache is the one that counts), and runs it as a run of kind rollback: one transaction started
// by the root helper with the plan in the request (RootActs.Rollback), `rollback.started` and
// `rollback.finished`, then the after-upgrade check against the record (Follow, checkAfter).
import (
"encoding/json"
"errors"
"fmt"
"path"
"regexp"
"sort"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// ArchiveURL is the Arch Linux Archive.
const ArchiveURL = "https://archive.archlinux.org"
// SourceRemoved marks a plan item that removes a package installed since the record (to "" and no location):
// only when the rollback was asked to remove what the upgrade added.
const SourceRemoved = "removed"
// RollbackPlan is what a rollback to a run's record would change.
type RollbackPlan struct {
Node string `json:"node"`
Of string `json:"of"`
// RecordAt is when the record was taken; the dated repository is that day's.
RecordAt time.Time `json:"record_at"`
// Items brings each package back, or removes one installed since when RemoveAdded.
Items []RollbackItem `json:"items"`
// Added is the packages installed since the record, not foreign.
Added []PackageVersion `json:"added"`
RemoveAdded bool `json:"remove_added"`
// Foreign is how many foreign packages the plan leaves untouched.
Foreign int `json:"foreign_untouched"`
// CacheRead says whether the node's package cache was read: only on the node itself.
CacheRead bool `json:"cache_read"`
}
// cacheFile is `<name>-<version>-<arch>.pkg.tar.<ext>`; a name may hold dashes, a version and an arch none.
var cacheFile = regexp.MustCompile(`^(.+)-([^-]+-[^-]+)-([^-]+)\.pkg\.tar\.(zst|xz)$`)
// packageCache is the node's cache: by package name, by version, the file's path; and each name's arch.
type packageCache struct {
files map[string]map[string]string
arch map[string]string
}
// readCache lists the cache's package files.
func (m *Machine) readCache() (packageCache, error) {
c := packageCache{files: map[string]map[string]string{}, arch: map[string]string{}}
out, err := m.Out("find", CacheDir, "-mindepth", "1", "-maxdepth", "1", "-name", "*.pkg.tar.*", "-printf", "%f\n")
if err != nil {
return c, err
}
for _, l := range lines(out) {
f := cacheFile.FindStringSubmatch(strings.TrimSpace(l))
if f == nil || !packageName.MatchString(f[1]) || !packageVersion.MatchString(f[2]) {
continue
}
if c.files[f[1]] == nil {
c.files[f[1]] = map[string]string{}
}
if _, ok := c.files[f[1]][f[2]]; !ok || f[4] == "zst" {
c.files[f[1]][f[2]] = path.Join(CacheDir, f[0])
}
c.arch[f[1]] = f[3]
}
return c, nil
}
// installedArch is an installed package's architecture from the local database, or "".
func (m *Machine) installedArch(name, version string) string {
desc, err := m.ReadFile(path.Join(PacmanDBDir, "local", name+"-"+version, "desc"))
if err != nil {
return ""
}
ls := strings.Split(string(desc), "\n")
for i, l := range ls {
if strings.TrimSpace(l) == "%ARCH%" && i+1 < len(ls) {
if a := strings.TrimSpace(ls[i+1]); repoName.MatchString(a) {
return a
}
}
}
return ""
}
// source is where a package's record version comes from.
func source(name, version, repo string, arch string, cache *packageCache, recordAt time.Time) (string, string) {
if cache != nil {
if p, ok := cache.files[name][version]; ok {
return SourceCache, p
}
}
if arch != "" {
file := name + "-" + version + "-" + arch + ".pkg.tar.zst"
return SourceArchive, ArchiveURL + "/packages/" + name[:1] + "/" + name + "/" + file
}
day := recordAt.UTC().Format("2006/01/02")
return SourceRepository, ArchiveURL + "/repos/" + day + "/" + repo + "/os/$arch"
}
// keyGetter reads one key: a Bucket the keeper writes, or a StateReader a verb reads.
type keyGetter interface {
Get(key string) (*stdio.StateEntry, error)
}
// PlanRollback is the plan for returning node to the record of run, from the states: `records.<node>.<run>` and
// `installed.<node>`, which must be current. local says this is the node itself, whose cache and local database
// are read.
func PlanRollback(m *Machine, installed, records keyGetter, node, run string, removeAdded, local bool) (RollbackPlan, error) {
p := RollbackPlan{Node: node, Of: run, Items: []RollbackItem{}, Added: []PackageVersion{}, RemoveAdded: removeAdded}
if e, err := installed.Get(NotCurrentKey(node)); err != nil {
return p, err
} else if e != nil {
return p, fmt.Errorf("the packages of %s on the bus are not current: a plan from them would not be what is installed", node)
}
var rec Record
if e, err := records.Get(NodeKey(node, run)); err != nil {
return p, err
} else if e == nil {
return p, fmt.Errorf("%s has no record of run %s: it was never taken, or is older than record-days", node, run)
} else if err := json.Unmarshal(e.Value, &rec); err != nil {
return p, fmt.Errorf("the record of run %s is not readable: %w", run, err)
}
var now InstalledValue
if e, err := installed.Get(node); err != nil {
return p, err
} else if e == nil {
return p, fmt.Errorf("no packages of %s are on the bus", node)
} else if err := json.Unmarshal(e.Value, &now); err != nil {
return p, fmt.Errorf("installed.%s is not readable: %w", node, err)
}
p.RecordAt = rec.At
var cache *packageCache
if local {
c, err := m.readCache()
if err != nil {
return p, fmt.Errorf("reading the package cache: %w", err)
}
cache, p.CacheRead = &c, true
}
foreign := map[string]bool{}
nowBy := map[string]InstalledPackage{}
for _, ip := range now.Packages {
nowBy[ip.Name] = ip
if ip.Repo == RepoForeign {
foreign[ip.Name] = true
}
}
recBy := map[string]InstalledPackage{}
for _, ip := range rec.Installed.Packages {
recBy[ip.Name] = ip
if ip.Repo == RepoForeign {
foreign[ip.Name] = true
}
}
p.Foreign = len(foreign)
for _, want := range rec.Installed.Packages {
if foreign[want.Name] {
continue
}
have, installedNow := nowBy[want.Name]
if installedNow && have.Version == want.Version {
continue
}
arch := ""
if cache != nil {
arch = cache.arch[want.Name]
}
if arch == "" && local && installedNow {
arch = m.installedArch(have.Name, have.Version)
}
src, loc := source(want.Name, want.Version, want.Repo, arch, cache, rec.At)
p.Items = append(p.Items, RollbackItem{Name: want.Name, From: have.Version, To: want.Version, Source: src, Location: loc})
}
for _, have := range now.Packages {
if _, inRecord := recBy[have.Name]; inRecord || foreign[have.Name] {
continue
}
p.Added = append(p.Added, PackageVersion{have.Name, have.Version})
if removeAdded {
p.Items = append(p.Items, RollbackItem{Name: have.Name, From: have.Version, Source: SourceRemoved})
}
}
sort.Slice(p.Items, func(i, j int) bool { return p.Items[i].Name < p.Items[j].Name })
return p, nil
}
// AskRollback writes an ask for a rollback of node to the record of run, for that node's keeper to take.
func AskRollback(asks Bucket, node, run string, removeAdded bool, now time.Time) (Ask, error) {
a := Ask{ID: AskID(now), Node: node, Act: ActRollback, At: now.UTC(), Of: run, RemoveAdded: removeAdded}
if _, err := asks.Create(NodeKey(node, a.ID), a); err != nil {
return Ask{}, fmt.Errorf("writing the ask: %w", err)
}
return a, nil
}
// StartRollback runs a rollback's steps up to its transaction's start: the plan made again on the node, and the
// transaction started by the root helper with it. The end and the check follow as for an upgrade (Follow).
func (u *Upgrades) StartRollback(of string, removeAdded bool, ask string) (Run, error) {
now := u.Machine.Now().UTC()
r := &openRun{Run: Run{Node: u.Node, Run: RunID(now), Kind: RunRollback, From: FromAsk, Ask: ask, Of: of}}
runs, err := u.ownRuns()
if err != nil {
return r.Run, err
}
for _, o := range runs {
if o.Run.Run == r.Run.Run {
return r.Run, fmt.Errorf("run %s of %s is there already: one run a second", o.Run.Run, u.Node)
}
if !ended(o.Run) && o.From != FromLog {
return r.Run, u.refuse(r, fmt.Sprintf("run %s is still going (its last step is %s): one run at a time", o.Run.Run, lastStep(o.Run)))
}
}
if u.Machine.locked() {
return r.Run, u.refuse(r, "another pacman holds "+DBLock+": a transaction is running, or one was killed and left its lock")
}
plan, err := PlanRollback(u.Machine, u.Installed, u.Records, u.Node, of, removeAdded, true)
if err != nil {
return r.Run, u.refuse(r, err.Error())
}
if len(plan.Items) == 0 {
return r.Run, u.refuse(r, "the node's packages are those of the record already: there is nothing to bring back")
}
unit := TransactionUnit(RunRollback, r.Run.Run)
if err := u.Root.Rollback(r.Run.Run, plan.Items); err != nil {
return r.Run, u.end(r, Step{Step: StepFailed, At: u.Machine.Now().UTC(), Error: "the transaction could not be started: " + err.Error()})
}
if err := u.step(r, Step{Step: StepStarted, At: now, Unit: unit, Plan: plan.Items}); err != nil {
return r.Run, err
}
return r.Run, u.emit(EventRollbackStarted, RollbackStarted{Node: u.Node, Run: r.Run.Run, Of: of, Unit: unit})
}
// takeRollback carries out an ask for a rollback; the ask is deleted once a run names it.
func (u *Upgrades) takeRollback(key string, a Ask) error {
if a.Of == "" {
return errors.Join(fmt.Errorf("ask %s names no run to return to", key), u.Asks.Delete(key))
}
r, err := u.StartRollback(a.Of, a.RemoveAdded, a.ID)
if len(r.Steps) == 0 {
return err
}
if derr := u.Asks.Delete(key); derr != nil {
err = errors.Join(err, fmt.Errorf("deleting the ask %s: %w", key, derr))
}
return err
}
// Rollback is pacman_rollback: the plan, read from the bus (and on the node itself its cache), and with apply
// the ask that has the node's keeper carry it out.
func (m *Machine) Rollback(node, run string, removeAdded, apply bool) (map[string]any, error) {
if node == "" {
node = m.Node
}
if node == "" {
return nil, errors.New("node is required: this bundle does not know its own")
}
if err := m.states(); err != nil {
return nil, err
}
plan, err := PlanRollback(m, m.States(StateInstalled), m.States(StateRecords), node, run, removeAdded, node == m.Node)
if err != nil {
return nil, err
}
out := map[string]any{"plan": plan}
if !apply {
return out, nil
}
if len(plan.Items) == 0 {
return nil, fmt.Errorf("the packages of %s are those of the record already: there is nothing to bring back", node)
}
a, err := AskRollback(upgradeState(StateAsks), node, run, removeAdded, m.Now())
if err != nil {
return nil, err
}
out["asked"] = a
out["follow"] = "pacman_upgrades with node " + node + " lists the rollback run that names this ask; its keeper makes the plan again from its own cache"
return out, nil
}
@@ -0,0 +1,195 @@
package main
// A rollback to a record (novox/hq ADR 0508 §7; acceptance 1841/AC8): the plan says each package now -> record with
// its source, the packages added and no foreign package; apply is an ask the node's keeper runs as one
// transaction, emitting rollback.started and rollback.finished and ending with the after-upgrade check.
import (
"context"
"encoding/json"
"strings"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
var recordAt = time.Date(2026, 10, 3, 4, 0, 12, 0, time.UTC)
// rollbackRecord is node-a's record of run 20261003T040012Z; rollbackNow its packages since.
var (
rollbackRecord = Record{Node: "node-a", Run: "20261003T040012Z", At: recordAt, Installed: InstalledValue{At: recordAt, Packages: []InstalledPackage{
{"bash", "5.3.15-1", true, "core"}, {"glibc", "2.42-1", false, "core"}, {"linux", "7.2.8.arch1-2", true, "core"},
{"made-up-old-library", "1.0-1", false, "extra"}, {"made-up-foreign", "1-1", true, RepoForeign},
}}}
rollbackNow = InstalledValue{At: recordAt.Add(time.Hour), Packages: []InstalledPackage{
{"bash", "5.3.20-1", true, "core"}, {"glibc", "2.42-1", false, "core"}, {"linux", "7.3.1.arch1-1", true, "core"},
{"made-up-foreign", "2-1", true, RepoForeign}, {"made-up-library", "1.0-1", false, "extra"},
}}
)
// rollbackCache is the node's package cache: bash's recorded version, and linux at its new version only.
const rollbackCache = "bash-5.3.15-1-x86_64.pkg.tar.zst\nbash-5.3.20-1-x86_64.pkg.tar.zst\nlinux-7.3.1.arch1-1-x86_64.pkg.tar.zst\n" +
"not-a-package-file\n"
func rollbackStates(t *testing.T) (*fakeStates, *Machine, *[]call) {
t.Helper()
states := &fakeStates{}
states.store(StateRecords).Put(NodeKey("node-a", rollbackRecord.Run), rollbackRecord)
states.store(StateInstalled).Put("node-a", rollbackNow)
var calls []call
m := machine(byLine(map[string]Ran{
"find " + CacheDir + " -mindepth 1 -maxdepth 1 -name *.pkg.tar.* -printf %f\n": {Stdout: rollbackCache},
}, &calls), 1000)
m.Node, m.States = "node-a", readerOf(states)
return states, m, &calls
}
func TestThePlanSaysEachPackageNowToTheRecordWithItsSourceTheAddedAndNoForeign(t *testing.T) {
_, m, calls := rollbackStates(t)
p, err := PlanRollback(m, m.States(StateInstalled), m.States(StateRecords), "node-a", rollbackRecord.Run, false, true)
if err != nil {
t.Fatal(err)
}
want := []RollbackItem{
{"bash", "5.3.20-1", "5.3.15-1", SourceCache, CacheDir + "/bash-5.3.15-1-x86_64.pkg.tar.zst"},
{"linux", "7.3.1.arch1-1", "7.2.8.arch1-2", SourceArchive, "https://archive.archlinux.org/packages/l/linux/linux-7.2.8.arch1-2-x86_64.pkg.tar.zst"},
{"made-up-old-library", "", "1.0-1", SourceRepository, "https://archive.archlinux.org/repos/2026/10/03/extra/os/$arch"},
}
if len(p.Items) != len(want) {
t.Fatalf("items: %+v", p.Items)
}
for i := range want {
if p.Items[i] != want[i] {
t.Errorf("item %d: %+v, want %+v", i, p.Items[i], want[i])
}
}
if len(p.Added) != 1 || p.Added[0] != (PackageVersion{"made-up-library", "1.0-1"}) || p.Foreign != 1 || !p.CacheRead {
t.Errorf("added %+v, foreign %d, cache %v", p.Added, p.Foreign, p.CacheRead)
}
for _, c := range *calls {
if strings.Contains(c.String(), "made-up-foreign") {
t.Errorf("a command named the foreign package: %s", c)
}
}
// asked to remove what was added: the added package is removed too.
p, _ = PlanRollback(m, m.States(StateInstalled), m.States(StateRecords), "node-a", rollbackRecord.Run, true, true)
if len(p.Items) != 4 || p.Items[2] != (RollbackItem{Name: "made-up-library", From: "1.0-1", Source: SourceRemoved}) {
t.Errorf("with remove_added: %+v", p.Items)
}
}
func TestAnotherNodesPlanReadsNoCacheAndTakesUnknownArchitecturesFromTheDatedRepository(t *testing.T) {
_, m, calls := rollbackStates(t)
m.Node = "node-b"
r, err := m.Rollback("node-a", rollbackRecord.Run, false, false)
if err != nil {
t.Fatal(err)
}
p := r["plan"].(RollbackPlan)
if p.CacheRead || len(*calls) != 0 {
t.Errorf("another node's cache was read: %v", *calls)
}
for _, i := range p.Items {
if i.Source != SourceRepository || !strings.HasPrefix(i.Location, "https://archive.archlinux.org/repos/2026/10/03/") {
t.Errorf("%+v", i)
}
}
}
func TestNoPlanFromPackagesNotCurrentOrWithoutARecord(t *testing.T) {
states, m, _ := rollbackStates(t)
if _, err := m.Rollback("node-a", "20250101T000000Z", false, false); err == nil || !strings.Contains(err.Error(), "no record") {
t.Errorf("no record: %v", err)
}
states.store(StateInstalled).Put(NotCurrentKey("node-a"), NotCurrent{Bytes: 300000, Since: recordAt})
if _, err := m.Rollback("node-a", rollbackRecord.Run, false, false); err == nil || !strings.Contains(err.Error(), "not current") {
t.Errorf("not current: %v", err)
}
}
func TestApplyWritesAnAskForTheNodesKeeper(t *testing.T) {
_, m, _ := rollbackStates(t)
asks := newBucket()
saved := upgradeState
upgradeState = func(name string) Bucket { return asks }
t.Cleanup(func() { upgradeState = saved })
r, err := m.Rollback("", rollbackRecord.Run, true, true)
if err != nil {
t.Fatal(err)
}
a := r["asked"].(Ask)
var got Ask
if !asks.value(NodeKey("node-a", a.ID), &got) || got.Act != ActRollback || got.Of != rollbackRecord.Run || !got.RemoveAdded {
t.Errorf("the ask: %+v", got)
}
}
func TestAnAskedRollbackIsOneTransactionWithItsEventsAndTheCheck(t *testing.T) {
k := newKeeperBed(t)
k.records.Put(NodeKey("node-a", rollbackRecord.Run), rollbackRecord)
k.installed.Put("node-a", rollbackNow)
inner := k.u.Machine.Run
k.u.Machine.Run = func(ctx context.Context, name string, args ...string) Ran {
if name == "find" {
return Ran{Stdout: rollbackCache}
}
return inner(ctx, name, args...)
}
a := Ask{ID: "20261004T120000Z-abc123", Node: "node-a", Act: ActRollback, At: k.now, Of: rollbackRecord.Run}
if err := k.takeAsk(t, a); err != nil {
t.Fatal(err)
}
var r Run
keys, _ := k.runs.Keys()
for _, key := range keys {
var x Run
k.runs.value(key, &x)
if x.Kind == RunRollback {
r = x
}
}
if r.Of != rollbackRecord.Run || r.Ask != a.ID || steps(r) != "started" || len(r.Steps[0].Plan) != 3 {
t.Fatalf("the rollback run: %+v", r)
}
if strings.Join(k.root.started, ";") != "rollback "+r.Run+" plan=bash:5.3.15-1:cache,linux:7.2.8.arch1-2:archive,made-up-old-library:1.0-1:repository" {
t.Errorf("the root helper was asked: %v", k.root.started)
}
if _, there := k.asks.values[NodeKey("node-a", a.ID)]; there {
t.Error("the ask is deleted once the run names it")
}
k.unitState, k.now = "LoadState=not-found\n", k.now.Add(time.Minute)
if err := k.u.Follow(); err != nil {
t.Fatal(err)
}
k.runs.value(NodeKey("node-a", r.Run), &r)
if steps(r) != "started finished checked-after" || r.Steps[2].Check == nil {
t.Errorf("steps: %s", steps(r))
}
if got := strings.Join(k.events.names(), " "); got != "rollback.started rollback.finished upgrade.checked" {
t.Errorf("events: %s", got)
}
// the same record again: nothing to bring back once the packages are the record's.
k.installed.Put("node-a", rollbackRecord.Installed)
k.now = k.now.Add(time.Minute)
if err := k.takeAsk(t, Ask{ID: "20261004T120200Z-def456", Node: "node-a", Act: ActRollback, At: k.now, Of: rollbackRecord.Run}); err != nil {
t.Fatal(err)
}
var again Run
k.runs.value(NodeKey("node-a", "20261004T120200Z"), &again)
if steps(again) != "refused" || !strings.Contains(again.Steps[0].Why, "nothing to bring back") {
t.Errorf("a rollback with nothing to do: %+v", again)
}
}
// takeAsk writes an ask and hands its change to the keeper, as the watch of `asks` would.
func (k *keeperBed) takeAsk(t *testing.T, a Ask) error {
t.Helper()
key := NodeKey(a.Node, a.ID)
rev, err := k.asks.Create(key, a)
if err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(a)
return k.u.TakeAsk(stdio.StateChange{State: StateAsks, Key: key, Op: "put", Value: raw, Revision: rev})
}
@@ -380,6 +380,8 @@ func (u *Upgrades) TakeAsk(c stdio.StateChange) error {
err = errors.Join(err, fmt.Errorf("deleting the ask %s: %w", c.Key, derr))
}
return err
case ActRollback:
return u.takeRollback(c.Key, a)
default:
return fmt.Errorf("ask %s asks %q, which this keeper does not carry out yet", c.Key, a.Act)
}
@@ -30,6 +30,18 @@ func (f *fakeRoot) Transaction(kind, run string, ignore []string) error {
return nil
}
func (f *fakeRoot) Rollback(run string, plan []RollbackItem) error {
if f.fail != nil {
return f.fail
}
var names []string
for _, i := range plan {
names = append(names, i.Name+":"+i.To+":"+i.Source)
}
f.started = append(f.started, RunRollback+" "+run+" plan="+strings.Join(names, ","))
return nil
}
func (f *fakeRoot) Journal(unit string, n int) ([]string, error) { return f.journal, nil }
func (f *fakeRoot) Maps() ([]Mapped, error) { return f.mapped, f.mapsErr }
@@ -39,6 +39,10 @@ type RootActs interface {
// Transaction starts the run's transaction as a unit of the service manager, named TransactionUnit(kind,
// run), leaving out the packages ignore names, and answers once it started.
Transaction(kind, run string, ignore []string) error
// Rollback starts a rollback's transaction as a unit of the service manager, named TransactionUnit(RunRollback,
// run), carrying out the plan in the request (ADR 0510: the plan travels with the verb), and answers once it
// started.
Rollback(run string, plan []RollbackItem) error
// Journal is the last n lines a transaction unit logged.
Journal(unit string, n int) ([]string, error)
// Maps is every process still mapping a replaced file, by its unit or container and its executable.
+1
View File
@@ -73,6 +73,7 @@
"pacman_upgrade",
"pacman_upgrade_health",
"pacman_upgrades",
"pacman_rollback",
"pacman_orphans",
"pacman_remove_orphans",
"pacman_cache",