diff --git a/internal/pkg/exec.go b/internal/pkg/exec.go index db0cf9f..2ded00e 100644 --- a/internal/pkg/exec.go +++ b/internal/pkg/exec.go @@ -42,6 +42,13 @@ func MustPath(pathname string, writable bool, a ...Artifact) ExecPath { return ExecPath{check.MustAbs(pathname), a, writable} } +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. // @@ -59,6 +66,11 @@ type execArtifact struct { 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. This value is never encoded in Params + // because it cannot affect outcome. + timeout time.Duration } // execNetArtifact is like execArtifact but implements [KnownChecksum] and has @@ -88,10 +100,9 @@ func (a *execNetArtifact) Cure(f *FContext) error { return a.cure(f, true) } -// 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. +// 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 @@ -103,8 +114,14 @@ func (a *execNetArtifact) Cure(f *FContext) error { // // If checksum is non-nil, the resulting [Artifact] implements [KnownChecksum] // and its container 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]. func NewExec( checksum *Checksum, + timeout time.Duration, dir *check.Absolute, env []string, @@ -113,7 +130,13 @@ func NewExec( paths ...ExecPath, ) Artifact { - a := execArtifact{paths, dir, env, path, args} + if timeout <= 0 { + timeout = ExecTimeoutDefault + } + if timeout > ExecTimeoutMax { + timeout = ExecTimeoutMax + } + a := execArtifact{paths, dir, env, path, args, timeout} if checksum == nil { return &a } @@ -197,7 +220,7 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) { artifactCount += len(p.A) } - ctx, cancel := context.WithCancel(f.Unwrap()) + ctx, cancel := context.WithTimeout(f.Unwrap(), a.timeout) defer cancel() z := container.New(ctx, f.GetMessage()) diff --git a/internal/pkg/exec_test.go b/internal/pkg/exec_test.go index 22e0e2f..b422da3 100644 --- a/internal/pkg/exec_test.go +++ b/internal/pkg/exec_test.go @@ -38,7 +38,7 @@ func TestExec(t *testing.T) { cureMany(t, c, []cureStep{ {"container", pkg.NewExec( - nil, + nil, 0, pkg.AbsWork, []string{"HAKUREI_TEST=1"}, check.MustAbs("/opt/bin/testtool"), @@ -61,7 +61,7 @@ func TestExec(t *testing.T) { ), ignorePathname, wantChecksumOffline, nil}, {"error passthrough", pkg.NewExec( - nil, + nil, 0, pkg.AbsWork, []string{"HAKUREI_TEST=1"}, check.MustAbs("/opt/bin/testtool"), @@ -77,7 +77,7 @@ func TestExec(t *testing.T) { ), nil, pkg.Checksum{}, errors.Join(stub.UniqueError(0xcafe))}, {"invalid paths", pkg.NewExec( - nil, + nil, 0, pkg.AbsWork, []string{"HAKUREI_TEST=1"}, check.MustAbs("/opt/bin/testtool"), @@ -90,7 +90,7 @@ func TestExec(t *testing.T) { // check init failure passthrough var exitError *exec.ExitError if _, _, err := c.Cure(pkg.NewExec( - nil, + nil, 0, pkg.AbsWork, nil, check.MustAbs("/opt/bin/testtool"), @@ -112,7 +112,7 @@ func TestExec(t *testing.T) { ) cureMany(t, c, []cureStep{ {"container", pkg.NewExec( - &wantChecksum, + &wantChecksum, 0, pkg.AbsWork, []string{"HAKUREI_TEST=1"}, check.MustAbs("/opt/bin/testtool"), @@ -144,7 +144,7 @@ func TestExec(t *testing.T) { cureMany(t, c, []cureStep{ {"container", pkg.NewExec( - nil, + nil, 0, pkg.AbsWork, []string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"}, check.MustAbs("/opt/bin/testtool"), @@ -170,7 +170,7 @@ func TestExec(t *testing.T) { cureMany(t, c, []cureStep{ {"container", pkg.NewExec( - nil, + nil, 0, pkg.AbsWork, []string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"}, check.MustAbs("/work/bin/testtool"), @@ -201,7 +201,7 @@ func TestExec(t *testing.T) { cureMany(t, c, []cureStep{ {"container", pkg.NewExec( - nil, + nil, 0, pkg.AbsWork, []string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"}, check.MustAbs("/opt/bin/testtool"),