diff --git a/container/container.go b/container/container.go index dee4a44..f49faa8 100644 --- a/container/container.go +++ b/container/container.go @@ -64,7 +64,7 @@ type ( Args []string // Deliver SIGINT to the initial process on context cancellation. ForwardCancel bool - // time to wait for linger processes after death of initial process + // Time to wait for processes lingering after the initial process terminates. AdoptWaitDelay time.Duration // Mapped Uid in user namespace. @@ -152,15 +152,13 @@ func (e *StartError) Message() string { // Start starts the container init. The init process blocks until Serve is called. func (p *Container) Start() error { - if p.cmd != nil { + if p == nil || p.cmd == nil || + p.Ops == nil || len(*p.Ops) == 0 { + return errors.New("container: starting an invalid container") + } + if p.cmd.Process != nil { return errors.New("container: already started") } - if p.Ops == nil || len(*p.Ops) == 0 { - return errors.New("container: starting an empty container") - } - - ctx, cancel := context.WithCancel(p.ctx) - p.cancel = cancel // map to overflow id to work around ownership checks if p.Uid < 1 { @@ -182,9 +180,17 @@ func (p *Container) Start() error { p.AdoptWaitDelay = 0 } - p.cmd = exec.CommandContext(ctx, MustExecutable()) + if p.cmd.Stdin == nil { + p.cmd.Stdin = p.Stdin + } + if p.cmd.Stdout == nil { + p.cmd.Stdout = p.Stdout + } + if p.cmd.Stderr == nil { + p.cmd.Stderr = p.Stderr + } + p.cmd.Args = []string{initName} - p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr p.cmd.WaitDelay = p.WaitDelay if p.Cancel != nil { p.cmd.Cancel = func() error { return p.Cancel(p.cmd) } @@ -330,7 +336,7 @@ func (p *Container) Serve() error { // Wait waits for the container init process to exit and releases any resources associated with the [Container]. func (p *Container) Wait() error { - if p.cmd == nil { + if p.cmd == nil || p.cmd.Process == nil { return EINVAL } @@ -342,6 +348,36 @@ func (p *Container) Wait() error { return err } +// StdinPipe calls the [exec.Cmd] method with the same name. +func (p *Container) StdinPipe() (w io.WriteCloser, err error) { + if p.Stdin != nil { + return nil, errors.New("container: Stdin already set") + } + w, err = p.cmd.StdinPipe() + p.Stdin = p.cmd.Stdin + return +} + +// StdoutPipe calls the [exec.Cmd] method with the same name. +func (p *Container) StdoutPipe() (r io.ReadCloser, err error) { + if p.Stdout != nil { + return nil, errors.New("container: Stdout already set") + } + r, err = p.cmd.StdoutPipe() + p.Stdout = p.cmd.Stdout + return +} + +// StderrPipe calls the [exec.Cmd] method with the same name. +func (p *Container) StderrPipe() (r io.ReadCloser, err error) { + if p.Stderr != nil { + return nil, errors.New("container: Stderr already set") + } + r, err = p.cmd.StderrPipe() + p.Stderr = p.cmd.Stderr + return +} + func (p *Container) String() string { return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x", p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets)) @@ -357,7 +393,11 @@ func (p *Container) ProcessState() *os.ProcessState { // New returns the address to a new instance of [Container] that requires further initialisation before use. func New(ctx context.Context) *Container { - return &Container{ctx: ctx, Params: Params{Ops: new(Ops)}} + p := &Container{ctx: ctx, Params: Params{Ops: new(Ops)}} + c, cancel := context.WithCancel(ctx) + p.cancel = cancel + p.cmd = exec.CommandContext(c, MustExecutable()) + return p } // NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields. diff --git a/helper/container_test.go b/helper/container_test.go index 2d1c256..9aaeec8 100644 --- a/helper/container_test.go +++ b/helper/container_test.go @@ -11,10 +11,10 @@ import ( ) func TestContainer(t *testing.T) { - t.Run("start empty container", func(t *testing.T) { + t.Run("start invalid container", func(t *testing.T) { h := helper.New(t.Context(), container.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil) - wantErr := "container: starting an empty container" + wantErr := "container: starting an invalid container" if err := h.Start(); err == nil || err.Error() != wantErr { t.Errorf("Start: error = %v, wantErr %q", err, wantErr) diff --git a/helper/helper_test.go b/helper/helper_test.go index ef4b6bc..45f7bc9 100644 --- a/helper/helper_test.go +++ b/helper/helper_test.go @@ -6,8 +6,10 @@ import ( "fmt" "io" "os" + "reflect" "strconv" "strings" + "syscall" "testing" "time" @@ -47,6 +49,10 @@ func argFChecked(argsFd, statFd int) (args []string) { return } +const ( + containerTimeout = 30 * time.Second +) + // this function tests an implementation of the helper.Helper interface func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper) { oldWaitDelay := helper.WaitDelay @@ -54,18 +60,15 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f t.Cleanup(func() { helper.WaitDelay = oldWaitDelay }) t.Run("start helper with status channel and wait", func(t *testing.T) { - ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) + ctx, cancel := context.WithTimeout(t.Context(), containerTimeout) stdout := new(strings.Builder) h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, true) t.Run("wait not yet started helper", func(t *testing.T) { - defer func() { - r := recover() - if r == nil { - t.Fatalf("Wait did not panic") - } - }() - panic(fmt.Sprintf("unreachable: %v", h.Wait())) + if err := h.Wait(); !reflect.DeepEqual(err, syscall.EINVAL) && + !reflect.DeepEqual(err, errors.New("exec: not started")) { + t.Errorf("Wait: error = %v", err) + } }) t.Log("starting helper stub") @@ -108,7 +111,7 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f }) t.Run("start helper and wait", func(t *testing.T) { - ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) + ctx, cancel := context.WithTimeout(t.Context(), containerTimeout) defer cancel() stdout := new(strings.Builder) h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, false)