aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/pkg
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-03-26 15:00:25 +0900
committerOphestra <cat@gensokyo.uk>2026-03-26 15:05:04 +0900
commite661260607573009d0f7b0cbcbb8ee561fc08f3f (patch)
tree8a4f1393ed289acc023add65ba42eaf7b701b4f6 /internal/pkg
parent044490e0a521036ce10fed0b6813a8ff912237b5 (diff)
internal/pkg: enter exec container
This enables much easier troubleshooting of failing cures. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/pkg')
-rw-r--r--internal/pkg/exec.go98
-rw-r--r--internal/pkg/pkg.go11
2 files changed, 108 insertions, 1 deletions
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.