// Package update runs a full system update wrapped in a snapper pre/post // snapshot pair, so a broken update can be rolled back from the boot menu. package update import ( "context" "fmt" "io" "os/exec" "dasos/tools/internal/i18n" "dasos/tools/internal/snapper" "dasos/tools/internal/xbps" ) // Description marks the snapshots in "snapper list". const Description = "das-update" // Reporter receives the progress of Apply. type Reporter interface { Step(title string) // Output receives the output of the commands Apply runs. Output() io.Writer // Snapshot is called once the "pre" snapshot exists. Snapshot(n int) } // Result describes a finished update. type Result struct { Updates []xbps.Update Snapshot int // number of the "pre" snapshot, 0 without snapshots Reboot bool // the kernel or core libraries changed } // Updater holds the system operations, replaced in tests. type Updater struct { Run func(ctx context.Context, out io.Writer, name string, args ...string) error Pending func(ctx context.Context) ([]xbps.Update, error) Snapshots func() bool Pre func(ctx context.Context, description string) (int, error) Post func(ctx context.Context, pre int, description string) error } // System returns the Updater for the running system. It needs root. func System() *Updater { return &Updater{ Run: run, Pending: pending, Snapshots: snapper.Available, Pre: snapper.Pre, Post: snapper.Post, } } func run(ctx context.Context, out io.Writer, name string, args ...string) error { cmd := exec.CommandContext(ctx, name, args...) cmd.Stdout = out cmd.Stderr = out if err := cmd.Run(); err != nil { return fmt.Errorf("%s: %w", name, err) } return nil } // pending lists updates against the index synced by the first step. func pending(ctx context.Context) ([]xbps.Update, error) { out, err := exec.CommandContext(ctx, "xbps-install", "-un").CombinedOutput() if err != nil { return nil, fmt.Errorf("xbps-install -un: %w: %s", err, out) } return xbps.ParseDryRun(string(out)), nil } // Apply updates the whole system. Without pending updates it does nothing // and returns an empty Result. When snapshots are set up, a failure to take // the "pre" snapshot stops the update: updating without a way back is // exactly what das-update exists to prevent. func (u *Updater) Apply(ctx context.Context, r Reporter) (Result, error) { var res Result r.Step(i18n.T("update.step.sync")) if err := u.Run(ctx, r.Output(), "xbps-install", "-S"); err != nil { return res, err } updates, err := u.Pending(ctx) if err != nil { // An outdated xbps refuses to plan the update; it is updated first anyway. updates = []xbps.Update{{Name: "xbps", Action: "update"}} } if len(updates) == 0 { return res, nil } res.Updates = updates res.Reboot = xbps.NeedsReboot(updates) if u.Snapshots() { r.Step(i18n.T("update.step.pre")) n, err := u.Pre(ctx, Description) if err != nil { return res, err } res.Snapshot = n r.Snapshot(n) } else { r.Step(i18n.T("update.nosnapshots")) } err = u.install(ctx, r, updates) if res.Snapshot > 0 { // Taken after a failure too: the pair shows what the update changed. r.Step(i18n.T("update.step.post")) if perr := u.Post(ctx, res.Snapshot, Description); perr != nil && err == nil { err = perr } } return res, err } func (u *Updater) install(ctx context.Context, r Reporter, updates []xbps.Update) error { if xbps.SelfUpdate(updates) { r.Step(i18n.T("update.step.xbps")) if err := u.Run(ctx, r.Output(), "xbps-install", "-yu", "xbps"); err != nil { return err } } r.Step(i18n.T("update.step.install")) return u.Run(ctx, r.Output(), "xbps-install", "-yu") }