diff --git a/cmd/app/app.go b/cmd/app/app.go index 9fa49696..a8508676 100644 --- a/cmd/app/app.go +++ b/cmd/app/app.go @@ -31,7 +31,12 @@ func parsePair(s string) (source, target *check.Absolute, err error) { // parse decodes a high-level configuration stream and returns its // corresponding [hst.Config]. -func parse(id string, base *check.Absolute, r io.Reader) (*hst.Config, error) { +func parse( + id string, + base *check.Absolute, + r io.Reader, + templateP *string, +) (*hst.Config, error) { shell := fhs.AbsRoot.Append("bin", "zsh") home := hst.AbsPrivateTmp.Append("home") @@ -99,6 +104,9 @@ func parse(id string, base *check.Absolute, r io.Reader) (*hst.Config, error) { } else if v, err := strconv.Atoi(identity); err != nil { return nil, err } else { + if templateP != nil { + *templateP = template + } c.Identity = v root.Upper = base.Append("template", template) } diff --git a/cmd/app/app_test.go b/cmd/app/app_test.go index f4811801..1aab4759 100644 --- a/cmd/app/app_test.go +++ b/cmd/app/app_test.go @@ -130,6 +130,7 @@ talk com.canonical.Unity tc.name, base, strings.NewReader(tc.data), + nil, ) if !reflect.DeepEqual(err, tc.err) { diff --git a/cmd/app/lock.go b/cmd/app/lock.go new file mode 100644 index 00000000..d43ede93 --- /dev/null +++ b/cmd/app/lock.go @@ -0,0 +1,111 @@ +package main + +import ( + "errors" + "log" + "os" + "strconv" + "strings" + + "hakurei.app/check" + "hakurei.app/fhs" + "hakurei.app/hst" + "hakurei.app/internal/env" + "hakurei.app/internal/lockedfile" + "hakurei.app/internal/outcome" +) + +// MutationConflictError describes an active mutable instance. +type MutationConflictError string + +func (e MutationConflictError) Error() string { + return "mutable instance active at " + string(e) +} + +// informTemplate guards intention of a template or its derivatives. +func informTemplate(base *check.Absolute, name string, mutable bool) (func() error, error) { + mu := lockedfile.MutexAt(base.Append("lock", name).String()) + if unlock, err := mu.Lock(); err != nil { + return nil, err + } else { + defer unlock() + } + + marker := base.Append("lock", "."+name) + if p, err := os.ReadFile(marker.String()); err == nil { + if _, err = os.Stat(fhs.AbsProc.Append(string(p)).String()); err == nil { + return nil, MutationConflictError(p) + } else if !errors.Is(err, os.ErrNotExist) { + return nil, err + } + log.Printf("removing stale marker by %s", string(p)) + if err = os.Remove(marker.String()); err != nil { + return nil, err + } + } else if !errors.Is(err, os.ErrNotExist) { + return nil, err + } + + if !mutable { + return nil, nil + } + + var active []hst.ID + var sc hst.Paths + env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil)) + entries, copyError := outcome.NewStore(&sc).All() + var s hst.State + for eh := range entries { + if _, err := eh.Load(&s); err != nil { + return nil, err + } + + if s.Validate(0) != nil || len(s.Container.Filesystem) < 1 { + continue + } + root, ok := s.Container.Filesystem[0].FilesystemConfig.(*hst.FSOverlay) + if !ok || root == nil { + continue + } + + if !root.Target.Is(fhs.AbsRoot) || + len(root.Lower) != 1 || + !root.Lower[0].Is(base.Append("initial")) || + !root.Upper.Is(base.Append("template", name)) || + root.Work != nil { + continue + } + + active = append(active, s.ID) + } + if err := copyError(); err != nil { + return nil, err + } + + if len(active) != 0 { + var buf strings.Builder + buf.WriteString("derivative instances still active:") + for _, id := range active { + buf.WriteString("\n\t") + buf.WriteString(id.String()) + } + return nil, errors.New(buf.String()) + } + + return func() error { return os.RemoveAll(marker.String()) }, os.WriteFile( + marker.String(), + []byte(strconv.Itoa(os.Getpid())), + 0400, + ) +} + +// acquireTemplate obtains exclusivity of a template. +func acquireTemplate(base *check.Absolute, name string) (remove func() error, err error) { + return informTemplate(base, name, true) +} + +// enterTemplate checks against exclusivity of a template. +func enterTemplate(base *check.Absolute, name string) error { + _, err := informTemplate(base, name, false) + return err +} diff --git a/cmd/app/main.go b/cmd/app/main.go index c9331e39..095863a8 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -128,7 +128,12 @@ func main() { config.Container.Home = a } - return run(ctx, msg, &config) + remove, err := acquireTemplate(base, args[0]) + if err != nil { + return err + } + err = run(ctx, msg, &config) + return errors.Join(err, remove()) }, ).Flag( &flagShell, @@ -180,7 +185,8 @@ func main() { r = io.MultiReader(f, common) } - config, err = parse(args[0], base, r) + var name string + config, err = parse(args[0], base, r, &name) if closeErr := f.Close(); err == nil { err = closeErr } @@ -197,6 +203,9 @@ func main() { config.Container.Args[2] = flagCommand } + if err = enterTemplate(base, name); err != nil { + return err + } return run(ctx, msg, config, args[1:]...) }, ).