diff --git a/internal/pkg/exec.go b/internal/pkg/exec.go index 244b353b..f1385529 100644 --- a/internal/pkg/exec.go +++ b/internal/pkg/exec.go @@ -491,6 +491,104 @@ func (a *execArtifact) makeContainer( return } +var ( + // ErrExecBusy is returned entering [Cache.EnterExec] while another + // goroutine has not yet returned from it. + ErrExecBusy = errors.New("scratch directories in use") + // ErrNotExec is returned for unsupported implementations of [Artifact] + // passed to [Cache.EnterExec]. + ErrNotExec = errors.New("attempting to run a non-exec artifact") +) + +// EnterExec runs the container of an [Artifact] of [KindExec] or [KindExecNet] +// with its entry point, argument, and standard streams replaced with values +// supplied by the caller. +func (c *Cache) EnterExec( + ctx context.Context, + a Artifact, + retainSession bool, + stdin io.Reader, + stdout, stderr io.Writer, + path *check.Absolute, + args ...string, +) (err error) { + if !c.inExec.CompareAndSwap(false, true) { + return ErrExecBusy + } + defer c.inExec.Store(false) + + var hostNet bool + var e *execArtifact + switch f := a.(type) { + case *execArtifact: + e = f + + case *execNetArtifact: + e = &f.execArtifact + hostNet = true + + default: + return ErrNotExec + } + + deps := Collect(a.Dependencies()) + if _, _, err = c.Cure(&deps); err == nil { + return errors.New("unreachable") + } else if !IsCollected(err) { + return + } + + dm := make(map[Artifact]cureRes) + for i, p := range deps { + var res cureRes + res.pathname, res.checksum, err = c.Cure(p) + if err != nil { + return + } + dm[deps[i]] = res + } + + scratch := c.base.Append(dirExecScratch) + temp, work := scratch.Append("temp"), scratch.Append("work") + // work created during makeContainer + if err = os.MkdirAll(temp.String(), 0700); err != nil { + return + } + defer func() { + if chmodErr, removeErr := removeAll(scratch); chmodErr != nil || removeErr != nil { + err = errors.Join(err, chmodErr, removeErr) + } + }() + + var z *container.Container + z, err = e.makeContainer( + ctx, c.msg, + hostNet, + temp, work, + func(a Artifact) (*check.Absolute, unique.Handle[Checksum]) { + if res, ok := dm[a]; ok { + return res.pathname, res.checksum + } + panic(InvalidLookupError(c.Ident(a).Value())) + }, + c.Ident, + ) + if err != nil { + return + } + z.Stdin, z.Stdout, z.Stderr = stdin, stdout, stderr + z.Path, z.Args = path, args + z.RetainSession = retainSession + + if err = z.Start(); err != nil { + return + } + if err = z.Serve(); err != nil { + return + } + return z.Wait() +} + // cure is like Cure but allows optional host net namespace. func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) { ctx, cancel := context.WithTimeout(f.Unwrap(), a.timeout) diff --git a/internal/pkg/pkg.go b/internal/pkg/pkg.go index 9a8dc9fd..a8fb91ba 100644 --- a/internal/pkg/pkg.go +++ b/internal/pkg/pkg.go @@ -22,6 +22,7 @@ import ( "slices" "strings" "sync" + "sync/atomic" "syscall" "testing" "unique" @@ -466,6 +467,11 @@ const ( // pathnames allocated during [Cache.Cure]. dirTemp = "temp" + // dirExecScratch is the directory name appended to Cache.base for scratch + // space setting up the container started by [Cache.EnterExec]. Exclusivity + // via Cache.inExec. + dirExecScratch = "scratch" + // checksumLinknamePrefix is prepended to the encoded [Checksum] value // of an [Artifact] when creating a symbolic link to dirChecksum. checksumLinknamePrefix = "../" + dirChecksum + "/" @@ -481,7 +487,7 @@ type cureRes struct { // subject to the cures limit. Values pointed to by result addresses are safe // to access after the [sync.WaitGroup] associated with this pendingArtifactDep // is done. pendingArtifactDep must not be reused or modified after it is sent -// to Cache.cureDep. +// to cure. type pendingArtifactDep struct { // Dependency artifact populated during [Cache.Cure]. a Artifact @@ -553,6 +559,9 @@ type Cache struct { unlock func() // Synchronises calls to Close. closeOnce sync.Once + + // Whether EnterExec has not yet returned. + inExec atomic.Bool } // IsStrict returns whether the [Cache] strictly verifies checksums.