containerd/process.go
Brian Goff 205625473f Close wait chan after sending status
This allows the caller to receive multiple times without blocking after
the first call.

This can be useful in cases like this:
```go
  ch, _  := task.Wait(ctx)
  defer func() { <-ch } // don't return until task is done
  task.Start(ctx)

  if err := doSomething(task); err != nil {
    return err
  }

  status := <- ch
  // do stuff with status
```

Since this channel is created in the `Wait()` calls and never accessible
outside, this should be safe.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2017-10-16 16:55:43 -04:00

212 lines
5.3 KiB
Go

package containerd
import (
"context"
"strings"
"syscall"
"time"
"github.com/containerd/containerd/api/services/tasks/v1"
"github.com/containerd/containerd/errdefs"
"github.com/pkg/errors"
)
// Process represents a system process
type Process interface {
// Pid is the system specific process id
Pid() uint32
// Start starts the process executing the user's defined binary
Start(context.Context) error
// Delete removes the process and any resources allocated returning the exit status
Delete(context.Context, ...ProcessDeleteOpts) (*ExitStatus, error)
// Kill sends the provided signal to the process
Kill(context.Context, syscall.Signal, ...KillOpts) error
// Wait asynchronously waits for the process to exit, and sends the exit code to the returned channel
Wait(context.Context) (<-chan ExitStatus, error)
// CloseIO allows various pipes to be closed on the process
CloseIO(context.Context, ...IOCloserOpts) error
// Resize changes the width and heigh of the process's terminal
Resize(ctx context.Context, w, h uint32) error
// IO returns the io set for the process
IO() IO
// Status returns the executing status of the process
Status(context.Context) (Status, error)
}
// ExitStatus encapsulates a process' exit status.
// It is used by `Wait()` to return either a process exit code or an error
type ExitStatus struct {
code uint32
exitedAt time.Time
err error
}
// Result returns the exit code and time of the exit status.
// An error may be returned here to which indicates there was an error
// at some point while waiting for the exit status. It does not signify
// an error with the process itself.
// If an error is returned, the process may still be running.
func (s ExitStatus) Result() (uint32, time.Time, error) {
return s.code, s.exitedAt, s.err
}
// ExitCode returns the exit code of the process.
// This is only valid is Error() returns nil
func (s ExitStatus) ExitCode() uint32 {
return s.code
}
// ExitTime returns the exit time of the process
// This is only valid is Error() returns nil
func (s ExitStatus) ExitTime() time.Time {
return s.exitedAt
}
// Error returns the error, if any, that occured while waiting for the
// process.
func (s ExitStatus) Error() error {
return s.err
}
type process struct {
id string
task *task
pid uint32
io IO
}
func (p *process) ID() string {
return p.id
}
// Pid returns the pid of the process
// The pid is not set until start is called and returns
func (p *process) Pid() uint32 {
return p.pid
}
// Start starts the exec process
func (p *process) Start(ctx context.Context) error {
r, err := p.task.client.TaskService().Start(ctx, &tasks.StartRequest{
ContainerID: p.task.id,
ExecID: p.id,
})
if err != nil {
p.io.Cancel()
p.io.Wait()
p.io.Close()
return errdefs.FromGRPC(err)
}
p.pid = r.Pid
return nil
}
func (p *process) Kill(ctx context.Context, s syscall.Signal, opts ...KillOpts) error {
var i KillInfo
for _, o := range opts {
if err := o(ctx, p, &i); err != nil {
return err
}
}
_, err := p.task.client.TaskService().Kill(ctx, &tasks.KillRequest{
Signal: uint32(s),
ContainerID: p.task.id,
ExecID: p.id,
All: i.All,
})
return errdefs.FromGRPC(err)
}
func (p *process) Wait(ctx context.Context) (<-chan ExitStatus, error) {
c := make(chan ExitStatus, 1)
go func() {
defer close(c)
r, err := p.task.client.TaskService().Wait(ctx, &tasks.WaitRequest{
ContainerID: p.task.id,
ExecID: p.id,
})
if err != nil {
c <- ExitStatus{
code: UnknownExitStatus,
err: err,
}
return
}
c <- ExitStatus{
code: r.ExitStatus,
exitedAt: r.ExitedAt,
}
}()
return c, nil
}
func (p *process) CloseIO(ctx context.Context, opts ...IOCloserOpts) error {
r := &tasks.CloseIORequest{
ContainerID: p.task.id,
ExecID: p.id,
}
var i IOCloseInfo
for _, o := range opts {
o(&i)
}
r.Stdin = i.Stdin
_, err := p.task.client.TaskService().CloseIO(ctx, r)
return errdefs.FromGRPC(err)
}
func (p *process) IO() IO {
return p.io
}
func (p *process) Resize(ctx context.Context, w, h uint32) error {
_, err := p.task.client.TaskService().ResizePty(ctx, &tasks.ResizePtyRequest{
ContainerID: p.task.id,
Width: w,
Height: h,
ExecID: p.id,
})
return errdefs.FromGRPC(err)
}
func (p *process) Delete(ctx context.Context, opts ...ProcessDeleteOpts) (*ExitStatus, error) {
for _, o := range opts {
if err := o(ctx, p); err != nil {
return nil, err
}
}
status, err := p.Status(ctx)
if err != nil {
return nil, err
}
switch status.Status {
case Running, Paused, Pausing:
return nil, errors.Wrapf(errdefs.ErrFailedPrecondition, "process must be stopped before deletion")
}
r, err := p.task.client.TaskService().DeleteProcess(ctx, &tasks.DeleteProcessRequest{
ContainerID: p.task.id,
ExecID: p.id,
})
if err != nil {
return nil, errdefs.FromGRPC(err)
}
if p.io != nil {
p.io.Wait()
p.io.Close()
}
return &ExitStatus{code: r.ExitStatus, exitedAt: r.ExitedAt}, nil
}
func (p *process) Status(ctx context.Context) (Status, error) {
r, err := p.task.client.TaskService().Get(ctx, &tasks.GetRequest{
ContainerID: p.task.id,
ExecID: p.id,
})
if err != nil {
return Status{}, errdefs.FromGRPC(err)
}
return Status{
Status: ProcessStatus(strings.ToLower(r.Process.Status.String())),
ExitStatus: r.Process.ExitStatus,
}, nil
}