runsc: Enable waiting on exited processes.

This makes `runsc wait` behave more like waitpid()/wait4() in that:
- Once a process has run to completion, you can wait on it and get its exit
  code.
- Processes not waited on will consume memory (like a zombie process)

PiperOrigin-RevId: 213358916
Change-Id: I5b5eca41ce71eea68e447380df8c38361a4d1558
This commit is contained in:
Kevin Krakauer 2018-09-17 16:24:05 -07:00 committed by Shentubot
parent ab6fa44588
commit bb88c187c5
11 changed files with 248 additions and 86 deletions

View File

@ -87,7 +87,7 @@ type ExecArgs struct {
// Exec runs a new task.
func (proc *Proc) Exec(args *ExecArgs, waitStatus *uint32) error {
newTG, err := proc.execAsync(args)
newTG, _, err := proc.execAsync(args)
if err != nil {
return err
}
@ -100,11 +100,13 @@ func (proc *Proc) Exec(args *ExecArgs, waitStatus *uint32) error {
// ExecAsync runs a new task, but doesn't wait for it to finish. It is defined
// as a function rather than a method to avoid exposing execAsync as an RPC.
func ExecAsync(proc *Proc, args *ExecArgs) (*kernel.ThreadGroup, error) {
func ExecAsync(proc *Proc, args *ExecArgs) (*kernel.ThreadGroup, kernel.ThreadID, error) {
return proc.execAsync(args)
}
func (proc *Proc) execAsync(args *ExecArgs) (*kernel.ThreadGroup, error) {
// execAsync runs a new task, but doesn't wait for it to finish. It returns the
// newly created thread group and its PID.
func (proc *Proc) execAsync(args *ExecArgs) (*kernel.ThreadGroup, kernel.ThreadID, error) {
// Import file descriptors.
l := limits.NewLimitSet()
fdm := proc.Kernel.NewFDMap()
@ -144,7 +146,7 @@ func (proc *Proc) execAsync(args *ExecArgs) (*kernel.ThreadGroup, error) {
paths := fs.GetPath(initArgs.Envv)
f, err := proc.Kernel.RootMountNamespace().ResolveExecutablePath(ctx, initArgs.WorkingDirectory, initArgs.Argv[0], paths)
if err != nil {
return nil, fmt.Errorf("error finding executable %q in PATH %v: %v", initArgs.Argv[0], paths, err)
return nil, 0, fmt.Errorf("error finding executable %q in PATH %v: %v", initArgs.Argv[0], paths, err)
}
initArgs.Filename = f
}
@ -156,7 +158,7 @@ func (proc *Proc) execAsync(args *ExecArgs) (*kernel.ThreadGroup, error) {
// Import the given file FD. This dups the FD as well.
file, err := host.ImportFile(ctx, int(f.Fd()), mounter, enableIoctl)
if err != nil {
return nil, err
return nil, 0, err
}
defer file.DecRef()
@ -164,7 +166,7 @@ func (proc *Proc) execAsync(args *ExecArgs) (*kernel.ThreadGroup, error) {
f.Close()
if err := fdm.NewFDAt(kdefs.FD(appFD), file, kernel.FDFlags{}, l); err != nil {
return nil, err
return nil, 0, err
}
}

View File

@ -596,13 +596,13 @@ func (ctx *createProcessContext) Value(key interface{}) interface{} {
//
// CreateProcess has no analogue in Linux; it is used to create the initial
// application task, as well as processes started by the control server.
func (k *Kernel) CreateProcess(args CreateProcessArgs) (*ThreadGroup, error) {
func (k *Kernel) CreateProcess(args CreateProcessArgs) (*ThreadGroup, ThreadID, error) {
k.extMu.Lock()
defer k.extMu.Unlock()
log.Infof("EXEC: %v", args.Argv)
if k.mounts == nil {
return nil, fmt.Errorf("no kernel MountNamespace")
return nil, 0, fmt.Errorf("no kernel MountNamespace")
}
tg := NewThreadGroup(k.tasks.Root, NewSignalHandlers(), linux.SIGCHLD, args.Limits, k.monotonicClock)
@ -622,7 +622,7 @@ func (k *Kernel) CreateProcess(args CreateProcessArgs) (*ThreadGroup, error) {
var err error
wd, err = k.mounts.FindInode(ctx, root, nil, args.WorkingDirectory, args.MaxSymlinkTraversals)
if err != nil {
return nil, fmt.Errorf("failed to find initial working directory %q: %v", args.WorkingDirectory, err)
return nil, 0, fmt.Errorf("failed to find initial working directory %q: %v", args.WorkingDirectory, err)
}
defer wd.DecRef()
}
@ -630,10 +630,10 @@ func (k *Kernel) CreateProcess(args CreateProcessArgs) (*ThreadGroup, error) {
if args.Filename == "" {
// Was anything provided?
if len(args.Argv) == 0 {
return nil, fmt.Errorf("no filename or command provided")
return nil, 0, fmt.Errorf("no filename or command provided")
}
if !filepath.IsAbs(args.Argv[0]) {
return nil, fmt.Errorf("'%s' is not an absolute path", args.Argv[0])
return nil, 0, fmt.Errorf("'%s' is not an absolute path", args.Argv[0])
}
args.Filename = args.Argv[0]
}
@ -641,7 +641,7 @@ func (k *Kernel) CreateProcess(args CreateProcessArgs) (*ThreadGroup, error) {
// Create a fresh task context.
tc, err := k.LoadTaskImage(ctx, k.mounts, root, wd, args.MaxSymlinkTraversals, args.Filename, args.Argv, args.Envv, k.featureSet)
if err != nil {
return nil, err
return nil, 0, err
}
// Take a reference on the FDMap, which will be transferred to
@ -663,17 +663,18 @@ func (k *Kernel) CreateProcess(args CreateProcessArgs) (*ThreadGroup, error) {
}
t, err := k.tasks.NewTask(config)
if err != nil {
return nil, err
return nil, 0, err
}
// Success.
tgid := k.tasks.Root.IDOfThreadGroup(tg)
if k.started {
tid := k.tasks.Root.IDOfTask(t)
t.Start(tid)
} else if k.globalInit == nil {
k.globalInit = tg
}
return tg, nil
return tg, tgid, nil
}
// Start starts execution of all tasks in k.

View File

@ -242,32 +242,11 @@ type ExecArgs struct {
// returns the pid of the new process.
func (cm *containerManager) ExecuteAsync(args *ExecArgs, pid *int32) error {
log.Debugf("containerManager.ExecuteAsync: %+v", args)
// Get the container Root Dirent from the Task, since we must run this
// process with the same Root.
cm.l.mu.Lock()
tg, ok := cm.l.containerRootTGs[args.CID]
cm.l.mu.Unlock()
if !ok {
return fmt.Errorf("cannot exec in container %q: no such container", args.CID)
}
tg.Leader().WithMuLocked(func(t *kernel.Task) {
args.Root = t.FSContext().RootDirectory()
})
if args.Root != nil {
defer args.Root.DecRef()
}
// Start the process.
proc := control.Proc{Kernel: cm.l.k}
newTG, err := control.ExecAsync(&proc, &args.ExecArgs)
tgid, err := cm.l.executeAsync(&args.ExecArgs, args.CID)
if err != nil {
return fmt.Errorf("error executing: %+v: %v", args, err)
return err
}
// Return the pid of the newly-created process.
ts := cm.l.k.TaskSet()
*pid = int32(ts.Root.IDOfThreadGroup(newTG))
*pid = int32(tgid)
return nil
}
@ -409,12 +388,16 @@ type WaitPIDArgs struct {
// CID is the container ID.
CID string
// ClearStatus determines whether the exit status of the process should
// be cleared when WaitPID returns.
ClearStatus bool
}
// WaitPID waits for the process with PID 'pid' in the sandbox.
func (cm *containerManager) WaitPID(args *WaitPIDArgs, waitStatus *uint32) error {
log.Debugf("containerManager.Wait")
return cm.l.waitPID(kernel.ThreadID(args.PID), args.CID, waitStatus)
return cm.l.waitPID(kernel.ThreadID(args.PID), args.CID, args.ClearStatus, waitStatus)
}
// SignalArgs are arguments to the Signal method.

View File

@ -31,6 +31,7 @@ import (
"gvisor.googlesource.com/gvisor/pkg/cpuid"
"gvisor.googlesource.com/gvisor/pkg/log"
"gvisor.googlesource.com/gvisor/pkg/sentry/arch"
"gvisor.googlesource.com/gvisor/pkg/sentry/control"
"gvisor.googlesource.com/gvisor/pkg/sentry/inet"
"gvisor.googlesource.com/gvisor/pkg/sentry/kernel"
"gvisor.googlesource.com/gvisor/pkg/sentry/kernel/auth"
@ -103,7 +104,7 @@ type Loader struct {
// sandboxID is the ID for the whole sandbox.
sandboxID string
// mu guards containerRootTGs.
// mu guards containerRootTGs and execProcesses.
mu sync.Mutex
// containerRootTGs maps container IDs to their root processes. It
@ -111,7 +112,24 @@ type Loader struct {
// call methods on particular containers.
//
// containerRootTGs is guarded by mu.
//
// TODO: When containers are removed via `runsc delete`,
// containerRootTGs should be cleaned up.
containerRootTGs map[string]*kernel.ThreadGroup
// execProcesses maps each invocation of exec to the process it spawns.
//
// execProcesses is guardded by mu.
//
// TODO: When containers are removed via `runsc delete`,
// execProcesses should be cleaned up.
execProcesses map[execID]*kernel.ThreadGroup
}
// execID uniquely identifies a sentry process.
type execID struct {
cid string
pid kernel.ThreadID
}
func init() {
@ -385,7 +403,8 @@ func (l *Loader) run() error {
}
// Create the root container init task.
if _, err := l.k.CreateProcess(l.rootProcArgs); err != nil {
_, _, err := l.k.CreateProcess(l.rootProcArgs)
if err != nil {
return fmt.Errorf("failed to create init process: %v", err)
}
@ -393,6 +412,11 @@ func (l *Loader) run() error {
l.rootProcArgs.FDMap.DecRef()
}
if l.execProcesses != nil {
return fmt.Errorf("there shouldn't already be a cache of exec'd processes, but found: %v", l.execProcesses)
}
l.execProcesses = make(map[execID]*kernel.ThreadGroup)
// Start signal forwarding only after an init process is created.
l.stopSignalForwarding = l.startSignalForwarding()
@ -467,7 +491,7 @@ func (l *Loader) startContainer(k *kernel.Kernel, spec *specs.Spec, conf *Config
return fmt.Errorf("error setting executable path for %+v: %v", procArgs, err)
}
tg, err := l.k.CreateProcess(procArgs)
tg, _, err := l.k.CreateProcess(procArgs)
if err != nil {
return fmt.Errorf("failed to create process in sentry: %v", err)
}
@ -482,6 +506,40 @@ func (l *Loader) startContainer(k *kernel.Kernel, spec *specs.Spec, conf *Config
return nil
}
func (l *Loader) executeAsync(args *control.ExecArgs, cid string) (kernel.ThreadID, error) {
// Get the container Root Dirent from the Task, since we must run this
// process with the same Root.
l.mu.Lock()
tg, ok := l.containerRootTGs[cid]
l.mu.Unlock()
if !ok {
return 0, fmt.Errorf("cannot exec in container %q: no such container", cid)
}
tg.Leader().WithMuLocked(func(t *kernel.Task) {
args.Root = t.FSContext().RootDirectory()
})
if args.Root != nil {
defer args.Root.DecRef()
}
// Start the process.
proc := control.Proc{Kernel: l.k}
tg, tgid, err := control.ExecAsync(&proc, args)
if err != nil {
return 0, fmt.Errorf("error executing: %+v: %v", args, err)
}
// Insert the process into execProcesses so that we can wait on it
// later.
l.mu.Lock()
defer l.mu.Unlock()
eid := execID{cid: cid, pid: tgid}
l.execProcesses[eid] = tg
log.Debugf("updated execProcesses: %v", l.execProcesses)
return tgid, nil
}
// TODO: Per-container namespaces must be supported for -pid.
// waitContainer waits for the root process of a container to exit.
@ -500,39 +558,59 @@ func (l *Loader) waitContainer(cid string, waitStatus *uint32) error {
// consider the container exited.
// TODO: Multiple calls to waitContainer() should return
// the same exit status.
defer func() {
l.mu.Lock()
defer l.mu.Unlock()
// TODO: Containers don't map 1:1 with their root
// processes. Container exits should be managed explicitly
// rather than via PID.
delete(l.containerRootTGs, cid)
}()
l.wait(tg, waitStatus)
ws := l.wait(tg)
*waitStatus = ws
l.mu.Lock()
defer l.mu.Unlock()
delete(l.containerRootTGs, cid)
return nil
}
func (l *Loader) waitPID(tgid kernel.ThreadID, cid string, waitStatus *uint32) error {
func (l *Loader) waitPID(tgid kernel.ThreadID, cid string, clearStatus bool, waitStatus *uint32) error {
// TODO: Containers all currently share a PID namespace.
// When per-container PID namespaces are supported, wait should use cid
// to find the appropriate PID namespace.
/*if cid != l.sandboxID {
return errors.New("non-sandbox PID namespaces are not yet implemented")
}*/
// TODO: This won't work if the exec process already exited.
tg := l.k.TaskSet().Root.ThreadGroupWithID(kernel.ThreadID(tgid))
// If the process was started via runsc exec, it will have an
// entry in l.execProcesses.
l.mu.Lock()
eid := execID{cid: cid, pid: tgid}
tg, ok := l.execProcesses[eid]
l.mu.Unlock()
if ok {
ws := l.wait(tg)
*waitStatus = ws
if clearStatus {
// Remove tg from the cache.
l.mu.Lock()
delete(l.execProcesses, eid)
log.Debugf("updated execProcesses (removal): %v", l.execProcesses)
l.mu.Unlock()
}
return nil
}
// This process wasn't created by runsc exec or start, so just find it
// by pid and hope it hasn't exited yet.
tg = l.k.TaskSet().Root.ThreadGroupWithID(kernel.ThreadID(tgid))
if tg == nil {
return fmt.Errorf("no thread group with ID %d", tgid)
}
l.wait(tg, waitStatus)
ws := l.wait(tg)
*waitStatus = ws
return nil
}
// wait waits for the process with TGID 'tgid' in a container's PID namespace
// to exit.
func (l *Loader) wait(tg *kernel.ThreadGroup, waitStatus *uint32) {
func (l *Loader) wait(tg *kernel.ThreadGroup) uint32 {
tg.WaitExited()
*waitStatus = tg.ExitStatus().Status()
return tg.ExitStatus().Status()
}
func (l *Loader) setRootContainerID(cid string) {

View File

@ -111,11 +111,11 @@ func createLoader() (*Loader, func(), error) {
// TestRun runs a simple application in a sandbox and checks that it succeeds.
func TestRun(t *testing.T) {
s, cleanup, err := createLoader()
l, cleanup, err := createLoader()
if err != nil {
t.Fatalf("error creating loader: %v", err)
}
defer s.Destroy()
defer l.Destroy()
defer cleanup()
// Start a goroutine to read the start chan result, otherwise Run will
@ -124,12 +124,13 @@ func TestRun(t *testing.T) {
var wg sync.WaitGroup
wg.Add(1)
go func() {
resultChanErr = <-s.ctrl.manager.startResultChan
resultChanErr = <-l.ctrl.manager.startResultChan
wg.Done()
}()
// Run the container..
if err := s.Run(); err != nil {
// Run the container.
l.setRootContainerID("foo")
if err := l.Run(); err != nil {
t.Errorf("error running container: %v", err)
}
@ -140,7 +141,7 @@ func TestRun(t *testing.T) {
}
// Wait for the application to exit. It should succeed.
if status := s.WaitExit(); status.Code != 0 || status.Signo != 0 {
if status := l.WaitExit(); status.Code != 0 || status.Signo != 0 {
t.Errorf("application exited with status %+v, want 0", status)
}
}
@ -148,24 +149,24 @@ func TestRun(t *testing.T) {
// TestStartSignal tests that the controller Start message will cause
// WaitForStartSignal to return.
func TestStartSignal(t *testing.T) {
s, cleanup, err := createLoader()
l, cleanup, err := createLoader()
if err != nil {
t.Fatalf("error creating loader: %v", err)
}
defer s.Destroy()
defer l.Destroy()
defer cleanup()
// We aren't going to wait on this application, so the control server
// needs to be shut down manually.
defer s.ctrl.srv.Stop()
defer l.ctrl.srv.Stop()
// Start a goroutine that calls WaitForStartSignal and writes to a
// channel when it returns.
waitFinished := make(chan struct{})
go func() {
s.WaitForStartSignal()
l.WaitForStartSignal()
// Pretend that Run() executed and returned no error.
s.ctrl.manager.startResultChan <- nil
l.ctrl.manager.startResultChan <- nil
waitFinished <- struct{}{}
}()
@ -181,7 +182,7 @@ func TestStartSignal(t *testing.T) {
// Trigger the control server StartRoot method.
cid := "foo"
if err := s.ctrl.manager.StartRoot(&cid, nil); err != nil {
if err := l.ctrl.manager.StartRoot(&cid, nil); err != nil {
t.Errorf("error calling StartRoot: %v", err)
}

View File

@ -49,6 +49,7 @@ type Exec struct {
extraKGIDs stringSlice
caps stringSlice
detach bool
clearStatus bool
processPath string
pidFile string
internalPidFile string
@ -100,6 +101,9 @@ func (ex *Exec) SetFlags(f *flag.FlagSet) {
f.StringVar(&ex.pidFile, "pid-file", "", "filename that the container pid will be written to")
f.StringVar(&ex.internalPidFile, "internal-pid-file", "", "filename that the container-internal pid will be written to")
f.StringVar(&ex.consoleSocket, "console-socket", "", "path to an AF_UNIX socket which will receive a file descriptor referencing the master end of the console's pseudoterminal")
// clear-status is expected to only be set when we fork due to --detach being set.
f.BoolVar(&ex.clearStatus, "clear-status", true, "clear the status of the exec'd process upon completion")
}
// Execute implements subcommands.Command.Execute. It starts a process in an
@ -163,7 +167,7 @@ func (ex *Exec) Execute(_ context.Context, f *flag.FlagSet, args ...interface{})
}
// Wait for the process to exit.
ws, err := c.WaitPID(pid)
ws, err := c.WaitPID(pid, ex.clearStatus)
if err != nil {
Fatalf("error waiting on pid %d: %v", pid, err)
}
@ -194,10 +198,16 @@ func (ex *Exec) execAndWait(waitStatus *syscall.WaitStatus) subcommands.ExitStat
// Add the rest of the args, excluding the "detach" flag.
for _, a := range os.Args[1:] {
if !strings.Contains(a, "detach") {
if strings.Contains(a, "detach") {
// Replace with the "clear-status" flag, which tells
// the new process it's a detached child and shouldn't
// clear the exit status of the sentry process.
args = append(args, "--clear-status=false")
} else {
args = append(args, a)
}
}
cmd := exec.Command(binPath, args...)
// Exec stdio defaults to current process stdio.

View File

@ -88,14 +88,14 @@ func (wt *Wait) Execute(_ context.Context, f *flag.FlagSet, args ...interface{})
waitStatus = ws
// Wait on a PID in the root PID namespace.
case wt.rootPID != unsetPID:
ws, err := c.WaitRootPID(int32(wt.rootPID))
ws, err := c.WaitRootPID(int32(wt.rootPID), true /* clearStatus */)
if err != nil {
Fatalf("error waiting on PID in root PID namespace %d in container %q: %v", wt.rootPID, c.ID, err)
}
waitStatus = ws
// Wait on a PID in the container's PID namespace.
case wt.pid != unsetPID:
ws, err := c.WaitPID(int32(wt.pid))
ws, err := c.WaitPID(int32(wt.pid), true /* clearStatus */)
if err != nil {
Fatalf("error waiting on PID %d in container %q: %v", wt.pid, c.ID, err)
}

View File

@ -398,22 +398,22 @@ func (c *Container) Wait() (syscall.WaitStatus, error) {
// WaitRootPID waits for process 'pid' in the sandbox's PID namespace and
// returns its WaitStatus.
func (c *Container) WaitRootPID(pid int32) (syscall.WaitStatus, error) {
func (c *Container) WaitRootPID(pid int32, clearStatus bool) (syscall.WaitStatus, error) {
log.Debugf("Wait on pid %d in sandbox %q", pid, c.Sandbox.ID)
if c.Sandbox == nil || !c.Sandbox.IsRunning() {
return 0, fmt.Errorf("container sandbox is not running")
}
return c.Sandbox.WaitPID(pid, c.Sandbox.ID)
return c.Sandbox.WaitPID(c.Sandbox.ID, pid, clearStatus)
}
// WaitPID waits for process 'pid' in the container's PID namespace and returns
// its WaitStatus.
func (c *Container) WaitPID(pid int32) (syscall.WaitStatus, error) {
func (c *Container) WaitPID(pid int32, clearStatus bool) (syscall.WaitStatus, error) {
log.Debugf("Wait on pid %d in container %q", pid, c.ID)
if c.Sandbox == nil || !c.Sandbox.IsRunning() {
return 0, fmt.Errorf("container sandbox is not running")
}
return c.Sandbox.WaitPID(pid, c.ID)
return c.Sandbox.WaitPID(c.ID, pid, clearStatus)
}
// Signal sends the signal to the container.

View File

@ -551,7 +551,7 @@ func TestExec(t *testing.T) {
args := &control.ExecArgs{
Filename: "/bin/sleep",
Argv: []string{"sleep", "5"},
Argv: []string{"/bin/sleep", "5"},
WorkingDirectory: "/",
KUID: uid,
}
@ -1598,7 +1598,7 @@ func (cont *Container) executeSync(args *control.ExecArgs) (syscall.WaitStatus,
if err != nil {
return 0, fmt.Errorf("error executing: %v", err)
}
ws, err := cont.WaitPID(pid)
ws, err := cont.WaitPID(pid, true /* clearStatus */)
if err != nil {
return 0, fmt.Errorf("error waiting: %v", err)
}

View File

@ -163,16 +163,15 @@ func TestMultiContainerWait(t *testing.T) {
go func(c *Container) {
defer wg.Done()
const pid = 2
if ws, err := c.WaitPID(pid); err != nil {
if ws, err := c.WaitPID(pid, true /* clearStatus */); err != nil {
t.Errorf("failed to wait for PID %d: %v", pid, err)
} else if es := ws.ExitStatus(); es != 0 {
t.Errorf("PID %d exited with non-zero status %d", pid, es)
}
if _, err := c.WaitPID(pid); err == nil {
if _, err := c.WaitPID(pid, true /* clearStatus */); err == nil {
t.Errorf("wait for stopped PID %d should fail", pid)
}
// TODO: use 'container[1]' when PID namespace is supported.
}(containers[0])
}(containers[1])
}
wg.Wait()
@ -184,6 +183,93 @@ func TestMultiContainerWait(t *testing.T) {
}
}
// TestExecWait ensures what we can wait containers and individual processes in the
// sandbox that have already exited.
func TestExecWait(t *testing.T) {
rootDir, err := testutil.SetupRootDir()
if err != nil {
t.Fatalf("error creating root dir: %v", err)
}
defer os.RemoveAll(rootDir)
// The first container should run the entire duration of the test.
cmd1 := []string{"sleep", "100"}
// We'll wait on the second container, which is much shorter lived.
cmd2 := []string{"sleep", "1"}
specs, ids := createSpecs(cmd1, cmd2)
// Setup the containers.
var containers []*Container
for i, spec := range specs {
conf := testutil.TestConfig()
bundleDir, err := testutil.SetupContainerInRoot(rootDir, spec, conf)
if err != nil {
t.Fatalf("error setting up container: %v", err)
}
defer os.RemoveAll(bundleDir)
cont, err := Create(ids[i], spec, conf, bundleDir, "", "")
if err != nil {
t.Fatalf("error creating container: %v", err)
}
defer cont.Destroy()
if err := cont.Start(conf); err != nil {
t.Fatalf("error starting container: %v", err)
}
containers = append(containers, cont)
}
// Check via ps that multiple processes are running.
expectedPL := []*control.Process{
{PID: 1, Cmd: "sleep"},
{PID: 2, Cmd: "sleep"},
}
if err := waitForProcessList(containers[0], expectedPL); err != nil {
t.Fatalf("failed to wait for sleep to start: %v", err)
}
// Wait for the second container to finish.
if err := waitForProcessList(containers[0], expectedPL[:1]); err != nil {
t.Fatalf("failed to wait for second container to stop: %v", err)
}
// Get the second container exit status.
if ws, err := containers[1].Wait(); err != nil {
t.Fatalf("failed to wait for process %s: %v", containers[1].Spec.Process.Args, err)
} else if es := ws.ExitStatus(); es != 0 {
t.Fatalf("process %s exited with non-zero status %d", containers[1].Spec.Process.Args, es)
}
if _, err := containers[1].Wait(); err == nil {
t.Fatalf("wait for stopped process %s should fail", containers[1].Spec.Process.Args)
}
// Execute another process in the first container.
args := &control.ExecArgs{
Filename: "/bin/sleep",
Argv: []string{"/bin/sleep", "1"},
WorkingDirectory: "/",
KUID: 0,
}
pid, err := containers[0].Execute(args)
if err != nil {
t.Fatalf("error executing: %v", err)
}
// Wait for the exec'd process to exit.
if err := waitForProcessList(containers[0], expectedPL[:1]); err != nil {
t.Fatalf("failed to wait for second container to stop: %v", err)
}
// Get the exit status from the exec'd process.
if ws, err := containers[0].WaitPID(pid, true /* clearStatus */); err != nil {
t.Fatalf("failed to wait for process %+v with pid %d: %v", args, pid, err)
} else if es := ws.ExitStatus(); es != 0 {
t.Fatalf("process %+v exited with non-zero status %d", args, es)
}
if _, err := containers[0].WaitPID(pid, true /* clearStatus */); err == nil {
t.Fatalf("wait for stopped process %+v should fail", args)
}
}
// TestMultiContainerMount tests that bind mounts can be used with multiple
// containers.
func TestMultiContainerMount(t *testing.T) {

View File

@ -522,7 +522,7 @@ func (s *Sandbox) Wait(cid string) (syscall.WaitStatus, error) {
// WaitPID waits for process 'pid' in the container's sandbox and returns its
// WaitStatus.
func (s *Sandbox) WaitPID(pid int32, cid string) (syscall.WaitStatus, error) {
func (s *Sandbox) WaitPID(cid string, pid int32, clearStatus bool) (syscall.WaitStatus, error) {
log.Debugf("Waiting for PID %d in sandbox %q", pid, s.ID)
var ws syscall.WaitStatus
conn, err := s.sandboxConnect()
@ -532,8 +532,9 @@ func (s *Sandbox) WaitPID(pid int32, cid string) (syscall.WaitStatus, error) {
defer conn.Close()
args := &boot.WaitPIDArgs{
PID: pid,
CID: cid,
PID: pid,
CID: cid,
ClearStatus: clearStatus,
}
if err := conn.Call(boot.ContainerWaitPID, args, &ws); err != nil {
return ws, fmt.Errorf("error waiting on PID %d in sandbox %q: %v", pid, s.ID, err)