From 16d37973ebc8f36ef613c0885879648cceaf1c45 Mon Sep 17 00:00:00 2001 From: Kevin Krakauer Date: Thu, 28 Jun 2018 14:55:46 -0700 Subject: runsc: Add the "wait" subcommand. Users can now call "runsc wait " to wait on a particular process inside the container. -pid can also be used to wait on a specific PID. Manually tested the wait subcommand for a single waiter and multiple waiters (simultaneously 2 processes waiting on the container and 2 processes waiting on a PID within the container). PiperOrigin-RevId: 202548978 Change-Id: Idd507c2cdea613c3a14879b51cfb0f7ea3fb3d4c --- runsc/boot/controller.go | 21 +++++++- runsc/boot/loader.go | 49 ++++++++++++++---- runsc/cmd/BUILD | 1 + runsc/cmd/wait.go | 103 ++++++++++++++++++++++++++++++++++++++ runsc/container/container.go | 14 ++++++ runsc/container/container_test.go | 49 +++++++++++++----- runsc/main.go | 1 + runsc/sandbox/sandbox.go | 24 ++++++++- 8 files changed, 236 insertions(+), 26 deletions(-) create mode 100644 runsc/cmd/wait.go diff --git a/runsc/boot/controller.go b/runsc/boot/controller.go index 56829c605..ff75a382e 100644 --- a/runsc/boot/controller.go +++ b/runsc/boot/controller.go @@ -61,6 +61,10 @@ const ( // and return its ExitStatus. ContainerWait = "containerManager.Wait" + // ContainerWaitPID is used to wait on a process with a certain PID in + // the sandbox and return its ExitStatus. + ContainerWaitPID = "containerManager.WaitPID" + // NetworkCreateLinksAndRoutes is the URPC endpoint for creating links // and routes in a network stack. NetworkCreateLinksAndRoutes = "Network.CreateLinksAndRoutes" @@ -234,7 +238,22 @@ func (cm *containerManager) Resume(_, _ *struct{}) error { // Wait waits for the init process in the given container. func (cm *containerManager) Wait(cid *string, waitStatus *uint32) error { log.Debugf("containerManager.Wait") - return cm.l.wait(cid, waitStatus) + return cm.l.waitContainer(*cid, waitStatus) +} + +// WaitPIDArgs are arguments to the WaitPID method. +type WaitPIDArgs struct { + // PID is the PID in the container's PID namespace. + PID int32 + + // CID is the container ID. + CID string +} + +// 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) } // SignalArgs are arguments to the Signal method. diff --git a/runsc/boot/loader.go b/runsc/boot/loader.go index f359a0eb0..014908179 100644 --- a/runsc/boot/loader.go +++ b/runsc/boot/loader.go @@ -83,6 +83,9 @@ type Loader struct { // rootProcArgs refers to the root sandbox init task. rootProcArgs kernel.CreateProcessArgs + // sandboxID is the ID for the whole sandbox. + sandboxID string + // mu guards containerRootTGIDs. mu sync.Mutex @@ -452,23 +455,46 @@ func (l *Loader) startContainer(args *StartArgs, k *kernel.Kernel) (kernel.Threa return tgid, nil } -// wait waits for the init process in the given container. -func (l *Loader) wait(cid *string, waitStatus *uint32) error { +// TODO: Per-container namespaces must be supported +// for -pid. + +// waitContainer waits for the root process of a container to exit. +func (l *Loader) waitContainer(cid string, waitStatus *uint32) error { + // Don't defer unlock, as doing so would make it impossible for + // multiple clients to wait on the same container. l.mu.Lock() - defer l.mu.Unlock() - tgid, ok := l.containerRootTGIDs[*cid] + tgid, ok := l.containerRootTGIDs[cid] + l.mu.Unlock() if !ok { - return fmt.Errorf("can't find process for container %q in %v", *cid, l.containerRootTGIDs) + return fmt.Errorf("can't find process for container %q in %v", cid, l.containerRootTGIDs) } - - // TODO: Containers don't map 1:1 with their root - // processes. Container exits should be managed explicitly - // rather than via PID. // If the thread either has already exited or exits during waiting, // consider the container exited. - defer delete(l.containerRootTGIDs, *cid) + 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.containerRootTGIDs, cid) + }() + return l.wait(tgid, cid, waitStatus) +} + +func (l *Loader) waitPID(tgid kernel.ThreadID, cid string, 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") + } + return l.wait(tgid, cid, waitStatus) +} - tg := l.k.TaskSet().Root.ThreadGroupWithID(tgid) +// wait waits for the process with TGID 'tgid' in a container's PID namespace +// to exit. +func (l *Loader) wait(tgid kernel.ThreadID, cid string, waitStatus *uint32) error { + tg := l.k.TaskSet().Root.ThreadGroupWithID(kernel.ThreadID(tgid)) if tg == nil { return fmt.Errorf("no thread group with ID %d", tgid) } @@ -482,6 +508,7 @@ func (l *Loader) setRootContainerID(cid string) { defer l.mu.Unlock() // The root container has PID 1. l.containerRootTGIDs = map[string]kernel.ThreadID{cid: 1} + l.sandboxID = cid } // WaitForStartSignal waits for a start signal from the control server. diff --git a/runsc/cmd/BUILD b/runsc/cmd/BUILD index 747793efc..18e95284b 100644 --- a/runsc/cmd/BUILD +++ b/runsc/cmd/BUILD @@ -25,6 +25,7 @@ go_library( "run.go", "start.go", "state.go", + "wait.go", ], importpath = "gvisor.googlesource.com/gvisor/runsc/cmd", visibility = [ diff --git a/runsc/cmd/wait.go b/runsc/cmd/wait.go new file mode 100644 index 000000000..8437457c4 --- /dev/null +++ b/runsc/cmd/wait.go @@ -0,0 +1,103 @@ +// Copyright 2018 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "syscall" + + "context" + "flag" + "github.com/google/subcommands" + "gvisor.googlesource.com/gvisor/runsc/boot" + "gvisor.googlesource.com/gvisor/runsc/container" +) + +const ( + unsetPID = -1 +) + +// Wait implements subcommands.Command for the "wait" command. +type Wait struct { + rootPID int + pid int +} + +// Name implements subcommands.Command.Name. +func (*Wait) Name() string { + return "wait" +} + +// Synopsis implements subcommands.Command.Synopsis. +func (*Wait) Synopsis() string { + return "wait on a process inside a container" +} + +// Usage implements subcommands.Command.Usage. +func (*Wait) Usage() string { + return `wait [flags] ` +} + +// SetFlags implements subcommands.Command.SetFlags. +func (wt *Wait) SetFlags(f *flag.FlagSet) { + f.IntVar(&wt.rootPID, "rootpid", unsetPID, "select a PID in the sandbox root PID namespace to wait on instead of the container's root process") + f.IntVar(&wt.pid, "pid", unsetPID, "select a PID in the container's PID namespace to wait on instead of the container's root process") +} + +// Execute implements subcommands.Command.Execute. It waits for a process in a +// container to exit before returning. +func (wt *Wait) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { + if f.NArg() != 1 { + f.Usage() + return subcommands.ExitUsageError + } + // You can't specify both -pid and -rootpid. + if wt.rootPID != unsetPID && wt.pid != unsetPID { + Fatalf("only up to one of -pid and -rootPid can be set") + } + + id := f.Arg(0) + conf := args[0].(*boot.Config) + + c, err := container.Load(conf.RootDir, id) + if err != nil { + Fatalf("error loading container: %v", err) + } + + waitStatus := args[1].(*syscall.WaitStatus) + switch { + // Wait on the whole container. + case wt.rootPID == unsetPID && wt.pid == unsetPID: + ws, err := c.Wait() + if err != nil { + Fatalf("error waiting on container %q: %v", c.ID, err) + } + *waitStatus = ws + // Wait on a PID in the root PID namespace. + case wt.rootPID != unsetPID: + ws, err := c.WaitRootPID(int32(wt.rootPID)) + 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)) + if err != nil { + Fatalf("error waiting on PID %d in container %q: %v", wt.pid, c.ID, err) + } + *waitStatus = ws + } + return subcommands.ExitSuccess +} diff --git a/runsc/container/container.go b/runsc/container/container.go index b2ea78084..042c76577 100644 --- a/runsc/container/container.go +++ b/runsc/container/container.go @@ -353,6 +353,20 @@ func (c *Container) Wait() (syscall.WaitStatus, error) { return c.Sandbox.Wait(c.ID) } +// WaitRootPID waits for process 'pid' in the sandbox's PID namespace and +// returns its WaitStatus. +func (c *Container) WaitRootPID(pid int32) (syscall.WaitStatus, error) { + log.Debugf("Wait on pid %d in sandbox %q", pid, c.Sandbox.ID) + return c.Sandbox.WaitPID(pid, c.Sandbox.ID) +} + +// WaitPID waits for process 'pid' in the container's PID namespace and returns +// its WaitStatus. +func (c *Container) WaitPID(pid int32) (syscall.WaitStatus, error) { + log.Debugf("Wait on pid %d in container %q", pid, c.ID) + return c.Sandbox.WaitPID(pid, c.ID) +} + // Signal sends the signal to the container. func (c *Container) Signal(sig syscall.Signal) error { log.Debugf("Signal container %q", c.ID) diff --git a/runsc/container/container_test.go b/runsc/container/container_test.go index 11285a123..ae500e7d0 100644 --- a/runsc/container/container_test.go +++ b/runsc/container/container_test.go @@ -75,9 +75,9 @@ func procListsEqual(got, want []*control.Process) bool { pd1 := got[i] pd2 := want[i] // Zero out unimplemented and timing dependant fields. - pd1.Time, pd2.Time = "", "" - pd1.STime, pd2.STime = "", "" - pd1.C, pd2.C = 0, 0 + pd1.Time = "" + pd1.STime = "" + pd1.C = 0 if *pd1 != *pd2 { return false } @@ -1074,16 +1074,39 @@ func TestMultiContainerWait(t *testing.T) { t.Errorf("failed to wait for sleep to start: %v", err) } - // Wait on the short lived container. - if ws, err := containers[1].Wait(); err != nil { - t.Fatalf("failed to wait for process %q: %v", strings.Join(containers[1].Spec.Process.Args, " "), err) - } else if es := ws.ExitStatus(); es != 0 { - t.Fatalf("process %q exited with non-zero status %d", strings.Join(containers[1].Spec.Process.Args, " "), es) - } + // Wait on the short lived container from multiple goroutines. + wg := sync.WaitGroup{} + for i := 0; i < 3; i++ { + wg.Add(1) + go func() { + defer wg.Done() + if ws, err := containers[1].Wait(); err != nil { + t.Errorf("failed to wait for process %q: %v", strings.Join(containers[1].Spec.Process.Args, " "), err) + } else if es := ws.ExitStatus(); es != 0 { + t.Errorf("process %q exited with non-zero status %d", strings.Join(containers[1].Spec.Process.Args, " "), es) + } - // After Wait returns, ensure that the root container is running and - // the child has finished. - if err := waitForProcessList(containers[0], expectedPL[:1]); err != nil { - t.Errorf("failed to wait for %q to start: %v", strings.Join(containers[0].Spec.Process.Args, " "), err) + // After Wait returns, ensure that the root container is running and + // the child has finished. + if err := waitForProcessList(containers[0], expectedPL[:1]); err != nil { + t.Errorf("failed to wait for %q to start: %v", strings.Join(containers[0].Spec.Process.Args, " "), err) + } + }() + } + + // Also wait via PID. + for i := 0; i < 3; i++ { + wg.Add(1) + go func() { + defer wg.Done() + const pid = 2 + if ws, err := containers[0].WaitPID(pid); 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) + } + }() } + + wg.Wait() } diff --git a/runsc/main.go b/runsc/main.go index 563ef8c67..dfb338b0f 100644 --- a/runsc/main.go +++ b/runsc/main.go @@ -84,6 +84,7 @@ func main() { subcommands.Register(new(cmd.Run), "") subcommands.Register(new(cmd.Start), "") subcommands.Register(new(cmd.State), "") + subcommands.Register(new(cmd.Wait), "") // Register internal commands with the internal group name. This causes // them to be sorted below the user-facing commands with empty group. diff --git a/runsc/sandbox/sandbox.go b/runsc/sandbox/sandbox.go index e1e7b39d1..9200fbee9 100644 --- a/runsc/sandbox/sandbox.go +++ b/runsc/sandbox/sandbox.go @@ -432,7 +432,29 @@ func (s *Sandbox) Wait(cid string) (syscall.WaitStatus, error) { defer conn.Close() if err := conn.Call(boot.ContainerWait, &cid, &ws); err != nil { - return ws, fmt.Errorf("err waiting on container %q: %v", cid, err) + return ws, fmt.Errorf("error waiting on container %q: %v", cid, err) + } + return ws, nil +} + +// 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) { + log.Debugf("Waiting for PID %d in sandbox %q", pid, s.ID) + var ws syscall.WaitStatus + conn, err := s.connect() + if err != nil { + return ws, err + } + defer conn.Close() + + args := &boot.WaitPIDArgs{ + PID: pid, + CID: cid, + } + + 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) } if s.IsRootContainer(cid) { -- cgit v1.2.3