Files
hakurei/internal/pkg/exec.go
Ophestra e7e9b4caea
All checks were successful
Test / Create distribution (push) Successful in 53s
Test / Sandbox (push) Successful in 2m49s
Test / ShareFS (push) Successful in 4m44s
Test / Hpkg (push) Successful in 5m10s
Test / Sandbox (race detector) (push) Successful in 5m17s
Test / Hakurei (race detector) (push) Successful in 7m32s
Test / Hakurei (push) Successful in 5m36s
Test / Flake checks (push) Successful in 1m55s
internal/pkg: exec nil path check during cure
This results in os.ErrInvalid instead of a panic, which hopefully improves user experience.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-06 17:46:12 +09:00

237 lines
5.5 KiB
Go

package pkg
import (
"bytes"
"context"
"errors"
"os"
"runtime"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/std"
"hakurei.app/message"
)
// ExecContainerPath is an [Artifact] and the [check.Absolute] pathname to make
// it available under in the container.
type ExecContainerPath struct {
P *check.Absolute
A Artifact
}
// MustPath returns [ExecContainerPath] for pathname and [Artifact] and panics
// if pathname is not absolute.
func MustPath(pathname string, a Artifact) ExecContainerPath {
return ExecContainerPath{check.MustAbs(pathname), a}
}
// An execArtifact is an [Artifact] that produces output by running a program
// part of another [Artifact] in a [container] to produce its output.
type execArtifact struct {
// Caller-supplied context.
ctx context.Context
// Caller-supplied inner read-only bind mounts.
paths []ExecContainerPath
// Caller-supplied logging facility, passed through to [container] and used
// internally to produce verbose output.
msg message.Msg
// Number of [Artifact] to concurrently cure. A value of 0 or lower is
// equivalent to the value returned by [runtime.NumCPU].
cures int
// 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
}
// NewExec returns a new [Artifact] bounded by ctx, it cures all [Artifact]
// in paths at the specified maximum concurrent cures limit. Specified paths are
// bind mounted read-only in the specified order in the resulting container.
// 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 /work and /tmp respectively.
//
// A cures value of 0 or lower is equivalent to the value returned by
// [runtime.NumCPU].
func NewExec(
ctx context.Context,
msg message.Msg,
cures int,
dir *check.Absolute,
env []string,
path *check.Absolute,
args []string,
paths ...ExecContainerPath,
) Artifact {
return &execArtifact{ctx, paths, msg, cures, dir, env, path, args}
}
// Kind returns the hardcoded [Kind] constant.
func (a *execArtifact) Kind() Kind { return KindExec }
// Params returns paths, executable pathname and args concatenated together.
func (a *execArtifact) Params() []byte {
var buf bytes.Buffer
for _, p := range a.paths {
if p.P != nil {
buf.WriteString(p.P.String())
} else {
buf.WriteString("invalid P\x00")
}
if p.A != nil {
id := Ident(p.A)
buf.Write(id[:])
} else {
buf.WriteString("invalid A\x00")
}
}
buf.WriteByte(0)
buf.WriteString(a.dir.String())
buf.WriteByte(0)
for _, e := range a.env {
buf.WriteString(e)
}
buf.WriteByte(0)
buf.WriteString(a.path.String())
buf.WriteByte(0)
for _, arg := range a.args {
buf.WriteString(arg)
}
return buf.Bytes()
}
// Dependencies returns a slice of all artifacts collected from caller-supplied
// [ExecContainerPath].
func (a *execArtifact) Dependencies() []Artifact {
artifacts := make([]Artifact, 0, len(a.paths))
for _, p := range a.paths {
if p.A != nil {
artifacts = append(artifacts, p.A)
}
}
return artifacts
}
// Cure cures the [Artifact] by curing all its dependencies then running the
// container described by the caller.
func (a *execArtifact) Cure(c *CureContext) (err error) {
cures := a.cures
if cures < 1 {
cures = runtime.NumCPU()
}
paths := make([][2]*check.Absolute, len(a.paths))
for i, p := range a.paths {
if p.P == nil || p.A == nil {
return os.ErrInvalid
}
paths[i][1] = p.P
}
if len(paths) > 0 {
type cureArtifact struct {
// Index of pending Artifact in paths.
index int
// Pending artifact.
a Artifact
}
ac := make(chan cureArtifact, len(paths))
for i, p := range a.paths {
ac <- cureArtifact{i, p.A}
}
type cureRes struct {
// Index of result in paths.
index int
// Cured pathname.
pathname *check.Absolute
// Error returned by c.
err error
}
res := make(chan cureRes)
for i := 0; i < cures; i++ {
go func() {
for d := range ac {
// computing and encoding identifier is expensive
if a.msg.IsVerbose() {
a.msg.Verbosef("curing %s...", Encode(Ident(d.a)))
}
var cr cureRes
cr.index = d.index
cr.pathname, _, cr.err = c.Cure(d.a)
res <- cr
}
}()
}
var count int
errs := make([]error, 0, len(paths))
for cr := range res {
count++
if cr.err != nil {
errs = append(errs, cr.err)
} else {
paths[cr.index][0] = cr.pathname
}
if count == len(paths) {
break
}
}
close(ac)
if err = errors.Join(errs...); err != nil {
return
}
}
ctx, cancel := context.WithCancel(a.ctx)
defer cancel()
z := container.New(ctx, a.msg)
z.ForwardCancel = true
z.SeccompPresets |= std.PresetStrict
z.ParentPerm = 0700
z.Hostname = "cure"
z.Uid, z.Gid = (1<<10)-1, (1<<10)-1
if a.msg.IsVerbose() {
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
}
z.Dir, z.Env, z.Path, z.Args = a.dir, a.env, a.path, a.args
z.Grow(len(paths) + 4)
for _, b := range paths {
z.Bind(b[0], b[1], 0)
}
z.Bind(
c.GetWorkDir(),
fhs.AbsRoot.Append("work"),
std.BindWritable|std.BindEnsure,
).Bind(
c.GetTempDir(),
fhs.AbsTmp,
std.BindWritable|std.BindEnsure,
).Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
if err = z.Start(); err != nil {
return
}
if err = z.Serve(); err != nil {
return
}
return z.Wait()
}