163 lines
3.2 KiB
Go
163 lines
3.2 KiB
Go
package system
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
"sync"
|
|
)
|
|
|
|
const (
|
|
// User type is reverted at final launcher exit.
|
|
User = Enablement(ELen)
|
|
// Process type is unconditionally reverted on exit.
|
|
Process = Enablement(ELen + 1)
|
|
)
|
|
|
|
type Criteria struct {
|
|
*Enablements
|
|
}
|
|
|
|
func (ec *Criteria) hasType(o Op) bool {
|
|
// nil criteria: revert everything except User
|
|
if ec.Enablements == nil {
|
|
return o.Type() != User
|
|
}
|
|
|
|
return ec.Has(o.Type())
|
|
}
|
|
|
|
// Op is a reversible system operation.
|
|
type Op interface {
|
|
// Type returns Op's enablement type.
|
|
Type() Enablement
|
|
|
|
// apply the Op
|
|
apply(sys *I) error
|
|
// revert reverses the Op if criteria is met
|
|
revert(sys *I, ec *Criteria) error
|
|
|
|
Is(o Op) bool
|
|
Path() string
|
|
String() string
|
|
}
|
|
|
|
func TypeString(e Enablement) string {
|
|
switch e {
|
|
case User:
|
|
return "User"
|
|
case Process:
|
|
return "Process"
|
|
default:
|
|
return e.String()
|
|
}
|
|
}
|
|
|
|
// New initialises sys with no-op verbose functions.
|
|
func New(uid int) (sys *I) {
|
|
sys = new(I)
|
|
sys.uid = uid
|
|
sys.IsVerbose = func() bool { return false }
|
|
sys.Verbose = func(...any) {}
|
|
sys.Verbosef = func(string, ...any) {}
|
|
sys.WrapErr = func(err error, _ ...any) error { return err }
|
|
return
|
|
}
|
|
|
|
type I struct {
|
|
uid int
|
|
ops []Op
|
|
ctx context.Context
|
|
|
|
IsVerbose func() bool
|
|
Verbose func(v ...any)
|
|
Verbosef func(format string, v ...any)
|
|
WrapErr func(err error, a ...any) error
|
|
|
|
// whether sys has been reverted
|
|
state bool
|
|
|
|
lock sync.Mutex
|
|
}
|
|
|
|
func (sys *I) UID() int { return sys.uid }
|
|
func (sys *I) println(v ...any) { sys.Verbose(v...) }
|
|
func (sys *I) printf(format string, v ...any) { sys.Verbosef(format, v...) }
|
|
func (sys *I) wrapErr(err error, a ...any) error { return sys.WrapErr(err, a...) }
|
|
func (sys *I) wrapErrSuffix(err error, a ...any) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
return sys.wrapErr(err, append(a, err)...)
|
|
}
|
|
|
|
func (sys *I) Equal(v *I) bool {
|
|
if v == nil || sys.uid != v.uid || len(sys.ops) != len(v.ops) {
|
|
return false
|
|
}
|
|
|
|
for i, o := range sys.ops {
|
|
if !o.Is(v.ops[i]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (sys *I) Commit(ctx context.Context) error {
|
|
sys.lock.Lock()
|
|
defer sys.lock.Unlock()
|
|
|
|
if sys.ctx != nil {
|
|
panic("sys instance committed twice")
|
|
}
|
|
sys.ctx = ctx
|
|
|
|
sp := New(sys.uid)
|
|
sp.ops = make([]Op, 0, len(sys.ops)) // prevent copies during commits
|
|
defer func() {
|
|
// sp is set to nil when all ops are applied
|
|
if sp != nil {
|
|
// rollback partial commit
|
|
sys.printf("commit faulted after %d ops, rolling back partial commit", len(sp.ops))
|
|
if err := sp.Revert(&Criteria{nil}); err != nil {
|
|
log.Println("errors returned reverting partial commit:", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
for _, o := range sys.ops {
|
|
if err := o.apply(sys); err != nil {
|
|
return err
|
|
} else {
|
|
// register partial commit
|
|
sp.ops = append(sp.ops, o)
|
|
}
|
|
}
|
|
|
|
// disarm partial commit rollback
|
|
sp = nil
|
|
return nil
|
|
}
|
|
|
|
func (sys *I) Revert(ec *Criteria) error {
|
|
sys.lock.Lock()
|
|
defer sys.lock.Unlock()
|
|
|
|
if sys.state {
|
|
panic("sys instance reverted twice")
|
|
}
|
|
sys.state = true
|
|
|
|
// collect errors
|
|
errs := make([]error, len(sys.ops))
|
|
|
|
for i := range sys.ops {
|
|
errs[i] = sys.ops[len(sys.ops)-i-1].revert(sys, ec)
|
|
}
|
|
|
|
// errors.Join filters nils
|
|
return errors.Join(errs...)
|
|
}
|