diff --git a/cmd/mbf/cache.go b/cmd/mbf/cache.go new file mode 100644 index 00000000..16e77cbc --- /dev/null +++ b/cmd/mbf/cache.go @@ -0,0 +1,90 @@ +package main + +import ( + "context" + "os" + "path/filepath" + + "hakurei.app/check" + "hakurei.app/internal/pkg" + "hakurei.app/message" +) + +// cache refers to an instance of [pkg.Cache] that might be open. +type cache struct { + ctx context.Context + msg message.Msg + + // Should generally not be used directly. + c *pkg.Cache + + cures, jobs int + hostAbstract, idle bool + + base string +} + +// open opens the underlying [pkg.Cache]. +func (cache *cache) open() (err error) { + if cache.c != nil { + return os.ErrInvalid + } + + if cache.base == "" { + cache.base = "cache" + } + var base *check.Absolute + if cache.base, err = filepath.Abs(cache.base); err != nil { + return + } else if base, err = check.NewAbs(cache.base); err != nil { + return + } + + var flags int + if cache.idle { + flags |= pkg.CSchedIdle + } + if cache.hostAbstract { + flags |= pkg.CHostAbstract + } + + done := make(chan struct{}) + defer close(done) + go func() { + select { + case <-cache.ctx.Done(): + os.Exit(2) + + case <-done: + return + } + }() + + cache.msg.Verbosef("opening cache at %s", base) + cache.c, err = pkg.Open( + cache.ctx, + cache.msg, + flags, + cache.cures, + cache.jobs, + base, + ) + return +} + +// Close closes the underlying [pkg.Cache] if it is open. +func (cache *cache) Close() { + if cache.c != nil { + cache.c.Close() + } +} + +// Do calls f on the underlying cache and returns its error value. +func (cache *cache) Do(f func(cache *pkg.Cache) error) error { + if cache.c == nil { + if err := cache.open(); err != nil { + return err + } + } + return f(cache.c) +} diff --git a/cmd/mbf/cache_test.go b/cmd/mbf/cache_test.go new file mode 100644 index 00000000..0f036542 --- /dev/null +++ b/cmd/mbf/cache_test.go @@ -0,0 +1,38 @@ +package main + +import ( + "context" + "log" + "os" + "testing" + + "hakurei.app/internal/pkg" + "hakurei.app/message" +) + +func TestCache(t *testing.T) { + t.Parallel() + + cm := cache{ + ctx: context.Background(), + msg: message.New(log.New(os.Stderr, "check: ", 0)), + base: t.TempDir(), + + hostAbstract: true, idle: true, + } + defer cm.Close() + cm.Close() + + if err := cm.open(); err != nil { + t.Fatalf("open: error = %v", err) + } + if err := cm.open(); err != os.ErrInvalid { + t.Errorf("(duplicate) open: error = %v", err) + } + + if err := cm.Do(func(cache *pkg.Cache) error { + return cache.Scrub(0) + }); err != nil { + t.Errorf("Scrub: error = %v", err) + } +} diff --git a/cmd/mbf/main.go b/cmd/mbf/main.go index a9d9a0c8..bfa2aa07 100644 --- a/cmd/mbf/main.go +++ b/cmd/mbf/main.go @@ -54,14 +54,13 @@ func main() { log.Fatal("this program must not run as root") } - var cache *pkg.Cache ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) defer stop() + + var cm cache defer func() { - if cache != nil { - cache.Close() - } + cm.Close() if r := recover(); r != nil { fmt.Println(r) @@ -71,60 +70,34 @@ func main() { var ( flagQuiet bool - flagCures int - flagJobs int - flagBase string - flagIdle bool - - flagHostAbstract bool ) - c := command.New(os.Stderr, log.Printf, "mbf", func([]string) (err error) { + c := command.New(os.Stderr, log.Printf, "mbf", func([]string) error { msg.SwapVerbose(!flagQuiet) - - flagBase = os.ExpandEnv(flagBase) - if flagBase == "" { - flagBase = "cache" - } - - var base *check.Absolute - if flagBase, err = filepath.Abs(flagBase); err != nil { - return - } else if base, err = check.NewAbs(flagBase); err != nil { - return - } - - var flags int - if flagIdle { - flags |= pkg.CSchedIdle - } - if flagHostAbstract { - flags |= pkg.CHostAbstract - } - cache, err = pkg.Open(ctx, msg, flags, flagCures, flagJobs, base) - - return + cm.ctx, cm.msg = ctx, msg + cm.base = os.ExpandEnv(cm.base) + return nil }).Flag( &flagQuiet, "q", command.BoolFlag(false), "Do not print cure messages", ).Flag( - &flagCures, + &cm.cures, "cures", command.IntFlag(0), "Maximum number of dependencies to cure at any given time", ).Flag( - &flagJobs, + &cm.jobs, "jobs", command.IntFlag(0), "Preferred number of jobs to run, when applicable", ).Flag( - &flagBase, + &cm.base, "d", command.StringFlag("$MBF_CACHE_DIR"), "Directory to store cured artifacts", ).Flag( - &flagIdle, + &cm.idle, "sched-idle", command.BoolFlag(false), "Set SCHED_IDLE scheduling policy", ).Flag( - &flagHostAbstract, + &cm.hostAbstract, "host-abstract", command.BoolFlag( os.Getenv("MBF_HOST_ABSTRACT") != "", ), @@ -156,7 +129,9 @@ func main() { if flagShifts < 0 || flagShifts > 31 { flagShifts = 12 } - return cache.Scrub(runtime.NumCPU() << flagShifts) + return cm.Do(func(cache *pkg.Cache) error { + return cache.Scrub(runtime.NumCPU() << flagShifts) + }) }, ).Flag( &flagShifts, @@ -223,7 +198,10 @@ func main() { if flagStatus { if r == nil { var f io.ReadSeekCloser - f, err = cache.OpenStatus(rosa.Std.Load(p)) + err = cm.Do(func(cache *pkg.Cache) (err error) { + f, err = cache.OpenStatus(rosa.Std.Load(p)) + return + }) if err != nil { if errors.Is(err, os.ErrNotExist) { fmt.Println( @@ -239,7 +217,7 @@ func main() { return } } - } else { + } else if err = cm.Do(func(cache *pkg.Cache) (err error) { status, n := r.ArtifactOf(cache.Ident(rosa.Std.Load(p))) if status == nil { fmt.Println( @@ -252,6 +230,9 @@ func main() { return } } + return + }); err != nil { + return } } @@ -306,7 +287,9 @@ func main() { if ext.Isatty(int(w.Fd())) { return errors.New("output appears to be a terminal") } - return rosa.WriteReport(msg, w, cache) + return cm.Do(func(cache *pkg.Cache) error { + return rosa.WriteReport(msg, w, cache) + }) }, ) @@ -406,23 +389,32 @@ func main() { checksum [2]unique.Handle[pkg.Checksum] ) - if pathname, _, err = cache.Cure( - (t - 2).Load(rosa.Clang), - ); err != nil { - return err + if err = cm.Do(func(cache *pkg.Cache) (err error) { + pathname, _, err = cache.Cure( + (t - 2).Load(rosa.Clang), + ) + return + }); err != nil { + return } log.Println("stage1:", pathname) - if pathname, checksum[0], err = cache.Cure( - (t - 1).Load(rosa.Clang), - ); err != nil { - return err + if err = cm.Do(func(cache *pkg.Cache) (err error) { + pathname, checksum[0], err = cache.Cure( + (t - 1).Load(rosa.Clang), + ) + return + }); err != nil { + return } log.Println("stage2:", pathname) - if pathname, checksum[1], err = cache.Cure( - t.Load(rosa.Clang), - ); err != nil { - return err + if err = cm.Do(func(cache *pkg.Cache) (err error) { + pathname, checksum[1], err = cache.Cure( + t.Load(rosa.Clang), + ) + return + }); err != nil { + return } log.Println("stage3:", pathname) @@ -439,10 +431,13 @@ func main() { } if flagStage0 { - if pathname, _, err = cache.Cure( - t.Load(rosa.Stage0), - ); err != nil { - return err + if err = cm.Do(func(cache *pkg.Cache) (err error) { + pathname, _, err = cache.Cure( + t.Load(rosa.Stage0), + ) + return + }); err != nil { + return } log.Println(pathname) } @@ -487,7 +482,11 @@ func main() { switch { default: - pathname, _, err := cache.Cure(rosa.Std.Load(p)) + var pathname *check.Absolute + err := cm.Do(func(cache *pkg.Cache) (err error) { + pathname, _, err = cache.Cure(rosa.Std.Load(p)) + return + }) if err != nil { return err } @@ -527,7 +526,9 @@ func main() { return err } - if err = cache.EncodeAll(f, rosa.Std.Load(p)); err != nil { + if err = cm.Do(func(cache *pkg.Cache) error { + return cache.EncodeAll(f, rosa.Std.Load(p)) + }); err != nil { _ = f.Close() return err } @@ -535,13 +536,15 @@ func main() { return f.Close() case flagEnter: - return cache.EnterExec( - ctx, - rosa.Std.Load(p), - true, os.Stdin, os.Stdout, os.Stderr, - rosa.AbsSystem.Append("bin", "mksh"), - "sh", - ) + return cm.Do(func(cache *pkg.Cache) error { + return cache.EnterExec( + ctx, + rosa.Std.Load(p), + true, os.Stdin, os.Stdout, os.Stderr, + rosa.AbsSystem.Append("bin", "mksh"), + "sh", + ) + }) } }, ). @@ -595,7 +598,10 @@ func main() { root := make(pkg.Collect, 0, 6+len(args)) root = rosa.Std.AppendPresets(root, presets...) - if _, _, err := cache.Cure(&root); err == nil { + if err := cm.Do(func(cache *pkg.Cache) error { + _, _, err := cache.Cure(&root) + return err + }); err == nil { return errors.New("unreachable") } else if !pkg.IsCollected(err) { return err @@ -607,11 +613,22 @@ func main() { } cured := make(map[pkg.Artifact]cureRes) for _, a := range root { - pathname, checksum, err := cache.Cure(a) - if err != nil { + if err := cm.Do(func(cache *pkg.Cache) error { + pathname, checksum, err := cache.Cure(a) + if err == nil { + cured[a] = cureRes{pathname, checksum} + } + return err + }); err != nil { + return err + } + } + + // explicitly open for direct error-free use from this point + if cm.c == nil { + if err := cm.open(); err != nil { return err } - cured[a] = cureRes{pathname, checksum} } layers := pkg.PromoteLayers(root, func(a pkg.Artifact) ( @@ -621,7 +638,7 @@ func main() { res := cured[a] return res.pathname, res.checksum }, func(i int, d pkg.Artifact) { - r := pkg.Encode(cache.Ident(d).Value()) + r := pkg.Encode(cm.c.Ident(d).Value()) if s, ok := d.(fmt.Stringer); ok { if name := s.String(); name != "" { r += "-" + name @@ -711,9 +728,7 @@ func main() { ) c.MustParse(os.Args[1:], func(err error) { - if cache != nil { - cache.Close() - } + cm.Close() if w, ok := err.(interface{ Unwrap() []error }); !ok { log.Fatal(err) } else {