diff options
Diffstat (limited to 'internal/pkg/exec.go')
| -rw-r--r-- | internal/pkg/exec.go | 808 |
1 files changed, 0 insertions, 808 deletions
diff --git a/internal/pkg/exec.go b/internal/pkg/exec.go deleted file mode 100644 index b40ae84b..00000000 --- a/internal/pkg/exec.go +++ /dev/null @@ -1,808 +0,0 @@ -package pkg - -import ( - "bufio" - "bytes" - "context" - "errors" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "runtime" - "slices" - "strconv" - "sync" - "syscall" - "time" - "unique" - - "hakurei.app/check" - "hakurei.app/container" - "hakurei.app/container/seccomp" - "hakurei.app/container/std" - "hakurei.app/ext" - "hakurei.app/fhs" - "hakurei.app/message" -) - -// AbsWork is the container pathname [TContext.GetWorkDir] is mounted on. -var AbsWork = fhs.AbsRoot.Append("work/") - -const ( - // EnvJobs is the name of the environment variable holding a decimal - // representation of the preferred job count. Its value must not affect cure - // outcome. - EnvJobs = "CURE_JOBS" - // EnvLoad is the name of the environment variable holding a decimal - // representation of the preferred loadavg target. Its value must not affect - // cure outcome. - EnvLoad = "CURE_LOAD" -) - -// ExecPath is a slice of [Artifact] and the [check.Absolute] pathname to make -// it available at under in the container. -type ExecPath struct { - // Pathname in the container mount namespace. - P *check.Absolute - // Artifacts to mount on the pathname, must contain at least one [Artifact]. - // If there are multiple entries or W is true, P is set up as an overlay - // mount, and entries of A must not implement [FileArtifact]. - A []Artifact - // Whether to make the mount point writable via the temp directory. - W bool -} - -// GetArtifactFunc is the function signature of [FContext.GetArtifact]. -type GetArtifactFunc func(Artifact) (*check.Absolute, unique.Handle[Checksum]) - -// PromoteLayers returns artifacts with identical-by-content layers promoted to -// the highest priority instance, as if mounted via [ExecPath]. -func PromoteLayers( - artifacts []Artifact, - getArtifact GetArtifactFunc, - report func(i int, d Artifact), -) []*check.Absolute { - layers := make([]*check.Absolute, 0, len(artifacts)) - checksums := make(map[unique.Handle[Checksum]]struct{}, len(artifacts)) - for i := range artifacts { - d := artifacts[len(artifacts)-1-i] - pathname, checksum := getArtifact(d) - if _, ok := checksums[checksum]; ok { - report(len(artifacts)-1-i, d) - continue - } - checksums[checksum] = struct{}{} - layers = append(layers, pathname) - } - slices.Reverse(layers) - return layers -} - -// layers returns pathnames collected from A deduplicated via [PromoteLayers]. -func (p *ExecPath) layers( - msg message.Msg, - getArtifact GetArtifactFunc, - ident func(a Artifact) unique.Handle[ID], -) []*check.Absolute { - return PromoteLayers(p.A, getArtifact, func(i int, d Artifact) { - if msg.IsVerbose() { - msg.Verbosef("promoted layer %d as %s", i, reportName(d, ident(d))) - } - }) -} - -// Path returns a populated [ExecPath]. -func Path(pathname *check.Absolute, writable bool, a ...Artifact) ExecPath { - return ExecPath{pathname, a, writable} -} - -// MustPath is like [Path], but takes a string pathname via [check.MustAbs]. -func MustPath(pathname string, writable bool, a ...Artifact) ExecPath { - return ExecPath{check.MustAbs(pathname), a, writable} -} - -var ( - binfmt map[string]container.BinfmtEntry - binfmtMu sync.RWMutex -) - -// RegisterArch arranges for [KindExec] and [KindExecNet] to support a new -// architecture via a binfmt_misc entry. Each architecture must be registered -// at most once. -func RegisterArch(arch string, e container.BinfmtEntry) { - if arch == "" { - panic(UnsupportedArchError(arch)) - } - - binfmtMu.Lock() - defer binfmtMu.Unlock() - - if binfmt == nil { - binfmt = make(map[string]container.BinfmtEntry) - } - - if _, ok := binfmt[arch]; ok { - panic("attempting to register " + strconv.Quote(arch) + " twice") - } - binfmt[arch] = e -} - -const ( - // ExecTimeoutDefault replaces out of range [NewExec] timeout values. - ExecTimeoutDefault = 15 * time.Minute - // ExecTimeoutMax is the arbitrary upper bound of [NewExec] timeout. - ExecTimeoutMax = 48 * time.Hour -) - -// An execArtifact is an [Artifact] that produces output by running a program -// part of another [Artifact] in a [container] to produce its output. -// -// Methods of execArtifact does not modify any struct field or underlying arrays -// referred to by slices. -type execArtifact struct { - // Caller-supplied user-facing reporting name, guaranteed to be nonzero - // during initialisation. - name string - // Target architecture. - arch string - // Caller-supplied inner mount points. - paths []ExecPath - - // Passed through to [container.Params]. - dir *check.Absolute - // Passed through to [container.Params]. - env []string - // Passed through to [container.Params]. - path *check.Absolute - // Passed through to [container.Params]. - args []string - - // Duration the initial process is allowed to run. The zero value is - // equivalent to [ExecTimeoutDefault]. - timeout time.Duration - - // Caller-supplied exclusivity value, returned as is by IsExclusive. - exclusive bool -} - -var _ fmt.Stringer = new(execArtifact) - -// execMeasuredArtifact is like execArtifact but implements [KnownChecksum] and -// has its resulting container optionally keep the host net namespace. -type execMeasuredArtifact struct { - checksum Checksum - - // Whether to keep host net namespace. - hostNet bool - - execArtifact -} - -var _ KnownChecksum = new(execMeasuredArtifact) - -// Checksum returns the caller-supplied checksum. -func (a *execMeasuredArtifact) Checksum() Checksum { return a.checksum } - -// Kind returns [KindExecNet], or [KindExec] if hostNet is false. -func (a *execMeasuredArtifact) Kind() Kind { - if a == nil || a.hostNet { - return KindExecNet - } - return KindExec -} - -// Cure cures the [Artifact] in the container described by the caller. The -// container optionally retains host networking. -func (a *execMeasuredArtifact) Cure(f *FContext) error { - return a.cure(f, a.hostNet) -} - -// ErrNetChecksum is panicked by [NewExec] if host net namespace is requested -// with a nil checksum. -var ErrNetChecksum = errors.New("attempting to keep net namespace without checksum") - -// NewExec returns a new [Artifact] that executes the program path in a -// container with specified paths bind mounted read-only in order. A private -// instance of /proc and /dev is made available to the container. -// -// The working and temporary directories are both created and mounted writable -// on [AbsWork] and [fhs.AbsTmp] respectively. If one or more paths target -// [AbsWork], the final entry is set up as a writable overlay mount on /work for -// which the upperdir is the host side work directory. In this configuration, -// the W field is ignored, and the program must avoid causing whiteout files to -// be created. Cure fails if upperdir ends up with entries other than directory, -// regular or symlink. -// -// If checksum is non-nil, the resulting [Artifact] implements [KnownChecksum] -// and its container optionally runs in the host net namespace. -// -// The container is allowed to run for the specified duration before the initial -// process and all processes originating from it is terminated. A zero or -// negative timeout value is equivalent tp [ExecTimeoutDefault], a timeout value -// greater than [ExecTimeoutMax] is equivalent to [ExecTimeoutMax]. -// -// The user-facing name and exclusivity value are not accessible from the -// container and does not affect curing outcome. Because of this, it is omitted -// from parameter data for computing identifier. -func NewExec( - name, arch string, - checksum *Checksum, - timeout time.Duration, - hostNet, exclusive bool, - - dir *check.Absolute, - env []string, - pathname *check.Absolute, - args []string, - - paths ...ExecPath, -) Artifact { - if name == "" { - name = "exec-" + filepath.Base(pathname.String()) - } - if arch == "" { - arch = runtime.GOARCH - } - if timeout <= 0 { - timeout = ExecTimeoutDefault - } - if timeout > ExecTimeoutMax { - timeout = ExecTimeoutMax - } - a := execArtifact{name, arch, paths, dir, env, pathname, args, timeout, exclusive} - if checksum == nil { - if hostNet { - panic(ErrNetChecksum) - } - return &a - } - return &execMeasuredArtifact{*checksum, hostNet, a} -} - -// Kind returns the hardcoded [Kind] constant. -func (*execArtifact) Kind() Kind { return KindExec } - -// Params writes paths, executable pathname and args. -func (a *execArtifact) Params(ctx *IContext) { - ctx.WriteString(a.arch) - ctx.WriteString(a.name) - - ctx.WriteUint32(uint32(len(a.paths))) - for _, p := range a.paths { - if p.P != nil { - ctx.WriteString(p.P.String()) - } else { - ctx.WriteString("invalid P\x00") - } - - ctx.WriteUint32(uint32(len(p.A))) - for _, d := range p.A { - ctx.WriteIdent(d) - } - - if p.W { - ctx.WriteUint32(1) - } else { - ctx.WriteUint32(0) - } - } - - ctx.WriteString(a.dir.String()) - - ctx.WriteUint32(uint32(len(a.env))) - for _, e := range a.env { - ctx.WriteString(e) - } - - ctx.WriteString(a.path.String()) - - ctx.WriteUint32(uint32(len(a.args))) - for _, arg := range a.args { - ctx.WriteString(arg) - } - - ctx.WriteUint32(uint32(a.timeout & 0xffffffff)) - ctx.WriteUint32(uint32(a.timeout >> 32)) - - if a.exclusive { - ctx.WriteUint32(1) - } else { - ctx.WriteUint32(0) - } -} - -// UnsupportedArchError describes an unsupported or invalid architecture. -type UnsupportedArchError string - -func (e UnsupportedArchError) Error() string { - if e == "" { - return "invalid architecture name" - } - return "unsupported architecture " + string(e) -} - -// readExecArtifact interprets IR values and returns the address of execArtifact -// or execNetArtifact. -func readExecArtifact(r *IRReader, net bool) Artifact { - r.DiscardAll() - - arch := r.ReadString() - if arch == "" { - panic(UnsupportedArchError(arch)) - } - - name := r.ReadString() - - sz := r.ReadUint32() - if sz > irMaxDeps { - panic(ErrIRDepend) - } - paths := make([]ExecPath, sz) - for i := range paths { - paths[i].P = check.MustAbs(r.ReadString()) - - sz = r.ReadUint32() - if sz > irMaxDeps { - panic(ErrIRDepend) - } - paths[i].A = make([]Artifact, sz) - for j := range paths[i].A { - paths[i].A[j] = r.ReadIdent() - } - - paths[i].W = r.ReadUint32() != 0 - } - - dir := check.MustAbs(r.ReadString()) - - sz = r.ReadUint32() - if sz > irMaxValues { - panic(ErrIRValues) - } - env := make([]string, sz) - for i := range env { - env[i] = r.ReadString() - } - - pathname := check.MustAbs(r.ReadString()) - - sz = r.ReadUint32() - if sz > irMaxValues { - panic(ErrIRValues) - } - args := make([]string, sz) - for i := range args { - args[i] = r.ReadString() - } - - timeout := time.Duration(r.ReadUint32()) - timeout |= time.Duration(r.ReadUint32()) << 32 - - exclusive := r.ReadUint32() != 0 - - checksum, ok := r.Finalise() - var checksumP *Checksum - if ok { - checksumP = new(checksum.Value()) - } - - if net && !ok { - panic(ErrExpectedChecksum) - } - - return NewExec( - name, arch, checksumP, timeout, net, exclusive, dir, env, pathname, args, paths..., - ) -} - -func init() { - register(KindExec, - func(r *IRReader) Artifact { return readExecArtifact(r, false) }) - register(KindExecNet, - func(r *IRReader) Artifact { return readExecArtifact(r, true) }) -} - -// Inputs returns a slice of all artifacts collected from caller-supplied -// [ExecPath]. -func (a *execArtifact) Inputs() []Artifact { - artifacts := make([][]Artifact, 0, len(a.paths)) - for _, p := range a.paths { - artifacts = append(artifacts, p.A) - } - return slices.Concat(artifacts...) -} - -// IsExclusive returns the caller-supplied exclusivity value. -func (a *execArtifact) IsExclusive() bool { return a.exclusive } - -// String returns the caller-supplied reporting name. -func (a *execArtifact) String() string { return a.name } - -// Cure cures the [Artifact] in the container described by the caller. -func (a *execArtifact) Cure(f *FContext) (err error) { - return a.cure(f, false) -} - -const ( - // execWaitDelay is passed through to [container.Params]. - execWaitDelay = time.Nanosecond -) - -// scanLinesCR is like [bufio.ScanLines], but also treats a bare \r as an -// end-of-line marker. -func scanLinesCR(data []byte, atEOF bool) (advance int, token []byte, err error) { - if atEOF && len(data) == 0 { - return 0, nil, nil - } - ri, ni := bytes.IndexByte(data, '\r'), bytes.IndexByte(data, '\n') - - if ri >= 0 && (ni < 0 || ri < ni) { - if ri+1 == ni { - // We have a full \r\n-terminated line. - return ri + 2, data[:ri], nil - } - // We have a bare \r, probably some kind of progress indicator. - return ri + 1, data[:ri], nil - } - if ni >= 0 && (ri < 0 || ni < ri) { - // We have a full newline-terminated line. - return ni + 1, data[:ni], nil - } - // If we're at EOF, we have a final, non-terminated line. Return it. - if atEOF { - return len(data), data, nil - } - // Request more data. - return 0, nil, nil -} - -// scanVerbose prefixes program output for a verbose [message.Msg]. -func scanVerbose( - msg message.Msg, - cancel context.CancelFunc, - done chan<- struct{}, - prefix, suffix string, - r io.Reader, -) { - defer close(done) - s := bufio.NewScanner(r) - s.Split(scanLinesCR) - s.Buffer( - make([]byte, bufio.MaxScanTokenSize), - bufio.MaxScanTokenSize<<12, - ) - for s.Scan() { - msg.Verbose(prefix, s.Text()+suffix) - } - if err := s.Err(); err != nil && !errors.Is(err, os.ErrClosed) { - cancel() - msg.Verbose("*"+prefix, err.Error()+suffix) - } -} - -var ( - // ErrInvalidPaths is returned for an [Artifact] of [KindExec] or - // [KindExecNet] specified with invalid paths. - ErrInvalidPaths = errors.New("invalid mount point") -) - -// SeccompPresets is the [seccomp] presets used by exec artifacts. -const SeccompPresets = std.PresetStrict & - ^(std.PresetDenyNS | std.PresetDenyDevel) - -// makeContainer sets up the specified temp and work directories and returns the -// corresponding [container.Container] that would have run for cure. -func (a *execArtifact) makeContainer( - ctx context.Context, - msg message.Msg, - flags, jobs, load int, - hostNet bool, - temp, work *check.Absolute, - getArtifact GetArtifactFunc, - ident func(a Artifact) unique.Handle[ID], -) (z *container.Container, err error) { - overlayWorkIndex := -1 - for i, p := range a.paths { - if p.P == nil || len(p.A) == 0 { - return nil, ErrInvalidPaths - } - if p.P.Is(AbsWork) { - overlayWorkIndex = i - } - } - - var artifactCount int - for _, p := range a.paths { - artifactCount += len(p.A) - } - - z = container.New(ctx, msg) - z.WaitDelay = execWaitDelay - z.SeccompPresets = SeccompPresets - z.SeccompFlags |= seccomp.AllowMultiarch - z.ParentPerm = 0700 - z.HostNet = hostNet - z.HostAbstract = flags&CHostAbstract != 0 - z.Hostname = "cure" - z.SetScheduler = flags&CSchedIdle != 0 - z.SchedPolicy = ext.SCHED_IDLE - if z.HostNet { - z.Hostname = "cure-net" - } - z.Quiet = flags&CSuppressInit != 0 - z.Uid, z.Gid = (1<<10)-1, (1<<10)-1 - z.Dir, z.Path, z.Args = a.dir, a.path, a.args - z.Env = slices.Concat(a.env, []string{ - EnvJobs + "=" + strconv.Itoa(jobs), - EnvLoad + "=" + strconv.Itoa(load), - }) - z.Grow(len(a.paths) + 4) - - if a.arch != runtime.GOARCH { - binfmtMu.RLock() - e, ok := binfmt[a.arch] - binfmtMu.RUnlock() - if !ok { - return nil, UnsupportedArchError(a.arch) - } - z.Binfmt = []container.BinfmtEntry{e} - z.InitAsRoot = true - } - - for i, b := range a.paths { - if i == overlayWorkIndex { - if err = os.MkdirAll(work.String(), 0700); err != nil { - return - } - tempWork := temp.Append(".work") - if err = os.MkdirAll(tempWork.String(), 0700); err != nil { - return - } - z.Overlay( - AbsWork, - work, - tempWork, - b.layers(msg, getArtifact, ident)..., - ) - continue - } - - if a.paths[i].W { - tempUpper, tempWork := temp.Append( - ".upper", strconv.Itoa(i), - ), temp.Append( - ".work", strconv.Itoa(i), - ) - if err = os.MkdirAll(tempUpper.String(), 0700); err != nil { - return - } - if err = os.MkdirAll(tempWork.String(), 0700); err != nil { - return - } - z.Overlay(b.P, tempUpper, tempWork, b.layers(msg, getArtifact, ident)...) - } else if len(b.A) == 1 { - pathname, _ := getArtifact(b.A[0]) - z.Bind(pathname, b.P, 0) - } else { - z.OverlayReadonly(b.P, b.layers(msg, getArtifact, ident)...) - } - } - if overlayWorkIndex < 0 { - z.Bind( - work, - AbsWork, - std.BindWritable|std.BindEnsure, - ) - } - z.Bind( - temp, - fhs.AbsTmp, - std.BindWritable|std.BindEnsure, - ) - z.Proc(fhs.AbsProc).Dev(fhs.AbsDev, true) - 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, - hostname string, - 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 *execMeasuredArtifact: - e = &f.execArtifact - hostNet = f.hostNet - - default: - return ErrNotExec - } - - deps := Collect(a.Inputs()) - 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, - c.attr.Flags, - c.attr.Jobs, - c.attr.Load, - 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 stdin == os.Stdin { - if s, ok := os.LookupEnv("TERM"); ok { - z.Env = append(z.Env, "TERM="+s) - } - } - if hostname != "" { - z.Hostname = hostname - } - - 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) - defer cancel() - - msg := f.GetMessage() - var z *container.Container - if z, err = a.makeContainer( - ctx, msg, f.cache.attr.Flags, f.GetJobs(), f.GetLoad(), hostNet, - f.GetTempDir(), f.GetWorkDir(), - f.GetArtifact, - f.cache.Ident, - ); err != nil { - return - } - - var status io.Writer - if status, err = f.GetStatusWriter(); err != nil { - return - } - - if msg.IsVerbose() { - var stdout, stderr io.ReadCloser - if stdout, err = z.StdoutPipe(); err != nil { - return - } - if stderr, err = z.StderrPipe(); err != nil { - _ = stdout.Close() - return - } - - brStdout, brStderr := f.cache.getReader(stdout), f.cache.getReader(stderr) - stdoutDone, stderrDone := make(chan struct{}), make(chan struct{}) - - var suffix, prefixO, prefixE string - if f.cache.attr.Flags&CColourOutput != 0 { - suffix = "\x1b[0m" - prefixO = "\x1b[1;37m(" + a.name + ")\x1b[0m" - prefixE = "\x1b[1;97m(" + a.name + ")\x1b[0m" - } else { - prefixO = "(" + a.name + ":1)" - prefixE = "(" + a.name + ":2)" - } - - go scanVerbose( - msg, cancel, stdoutDone, - prefixO, suffix, - io.TeeReader(brStdout, status), - ) - go scanVerbose( - msg, cancel, stderrDone, - prefixE, suffix, - io.TeeReader(brStderr, status), - ) - defer func() { - if err != nil && !errors.As(err, new(*exec.ExitError)) { - _ = stdout.Close() - _ = stderr.Close() - } - - <-stdoutDone - <-stderrDone - f.cache.putReader(brStdout) - f.cache.putReader(brStderr) - }() - } else { - z.Stdout, z.Stderr = status, status - } - - if err = z.Start(); err != nil { - return - } - if err = z.Serve(); err != nil { - return - } - if err = z.Wait(); err != nil { - return - } - - // do not allow empty directories to succeed - for { - err = syscall.Rmdir(f.GetWorkDir().String()) - if err != syscall.EINTR { - break - } - } - if err != nil && errors.Is(err, syscall.ENOTEMPTY) { - err = nil - } - return -} |
