diff options
-rw-r--r-- | runsc/boot/loader.go | 16 | ||||
-rw-r--r-- | runsc/cmd/kill.go | 17 | ||||
-rw-r--r-- | runsc/container/container.go | 32 | ||||
-rw-r--r-- | runsc/container/container_test.go | 58 | ||||
-rw-r--r-- | runsc/container/multi_container_test.go | 12 | ||||
-rw-r--r-- | runsc/container/test_app.go | 10 |
6 files changed, 124 insertions, 21 deletions
diff --git a/runsc/boot/loader.go b/runsc/boot/loader.go index 0a3f67774..fa169d090 100644 --- a/runsc/boot/loader.go +++ b/runsc/boot/loader.go @@ -756,8 +756,22 @@ func (l *Loader) signalProcess(cid string, pid, signo int32, sendToFGProcess boo ep, ok := l.processes[eid] l.mu.Unlock() + // The caller may be signaling a process not started directly via exec. + // In this case, find the process in the container's PID namespace and + // signal it. if !ok { - return fmt.Errorf("failed to signal container %q PID %d: no such PID", cid, pid) + ep, ok := l.processes[execID{cid: cid}] + if !ok { + return fmt.Errorf("no container with ID: %q", cid) + } + tg := ep.tg.PIDNamespace().ThreadGroupWithID(kernel.ThreadID(pid)) + if tg == nil { + return fmt.Errorf("failed to signal container %q PID %d: no such process", cid, pid) + } + if tg.Leader().ContainerID() != cid { + return fmt.Errorf("process %d is part of a different container: %q", pid, tg.Leader().ContainerID()) + } + return tg.SendSignal(&arch.SignalInfo{Signo: signo}) } if !sendToFGProcess { diff --git a/runsc/cmd/kill.go b/runsc/cmd/kill.go index dcb2988e3..7a98d10a2 100644 --- a/runsc/cmd/kill.go +++ b/runsc/cmd/kill.go @@ -31,6 +31,7 @@ import ( // Kill implements subcommands.Command for the "kill" command. type Kill struct { all bool + pid int } // Name implements subcommands.Command.Name. @@ -51,6 +52,7 @@ func (*Kill) Usage() string { // SetFlags implements subcommands.Command.SetFlags. func (k *Kill) SetFlags(f *flag.FlagSet) { f.BoolVar(&k.all, "all", false, "send the specified signal to all processes inside the container") + f.IntVar(&k.pid, "pid", 0, "send the specified signal to a specific process") } // Execute implements subcommands.Command.Execute. @@ -63,6 +65,10 @@ func (k *Kill) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) id := f.Arg(0) conf := args[0].(*boot.Config) + if k.pid != 0 && k.all { + Fatalf("it is invalid to specify both --all and --pid") + } + c, err := container.Load(conf.RootDir, id) if err != nil { Fatalf("error loading container: %v", err) @@ -80,8 +86,15 @@ func (k *Kill) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) if err != nil { Fatalf("%v", err) } - if err := c.Signal(sig, k.all); err != nil { - Fatalf("%v", err) + + if k.pid != 0 { + if err := c.SignalProcess(sig, int32(k.pid)); err != nil { + Fatalf("failed to signal pid %d: %v", k.pid, err) + } + } else { + if err := c.SignalContainer(sig, k.all); err != nil { + Fatalf("%v", err) + } } return subcommands.ExitSuccess } diff --git a/runsc/container/container.go b/runsc/container/container.go index 774cb6e07..0ec4d03c1 100644 --- a/runsc/container/container.go +++ b/runsc/container/container.go @@ -174,7 +174,7 @@ func Load(rootDir, id string) (*Container, error) { } else if c.Status == Running { // Container state should reflect the actual state of the application, so // we don't consider gofer process here. - if err := c.Signal(syscall.Signal(0), false); err != nil { + if err := c.SignalContainer(syscall.Signal(0), false); err != nil { c.changeStatus(Stopped) } } @@ -445,7 +445,7 @@ func (c *Container) SandboxPid() int { func (c *Container) Wait() (syscall.WaitStatus, error) { log.Debugf("Wait on container %q", c.ID) if !c.isSandboxRunning() { - return 0, fmt.Errorf("container is not running") + return 0, fmt.Errorf("sandbox is not running") } return c.Sandbox.Wait(c.ID) } @@ -455,7 +455,7 @@ func (c *Container) Wait() (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.isSandboxRunning() { - return 0, fmt.Errorf("container is not running") + return 0, fmt.Errorf("sandbox is not running") } return c.Sandbox.WaitPID(c.Sandbox.ID, pid, clearStatus) } @@ -465,16 +465,16 @@ func (c *Container) WaitRootPID(pid int32, clearStatus bool) (syscall.WaitStatus 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.isSandboxRunning() { - return 0, fmt.Errorf("container is not running") + return 0, fmt.Errorf("sandbox is not running") } return c.Sandbox.WaitPID(c.ID, pid, clearStatus) } -// Signal sends the signal to the container. If all is true and signal is -// SIGKILL, then waits for all processes to exit before returning. -// Signal returns an error if the container is already stopped. +// SignalContainer sends the signal to the container. If all is true and signal +// is SIGKILL, then waits for all processes to exit before returning. +// SignalContainer returns an error if the container is already stopped. // TODO: Distinguish different error types. -func (c *Container) Signal(sig syscall.Signal, all bool) error { +func (c *Container) SignalContainer(sig syscall.Signal, all bool) error { log.Debugf("Signal container %q: %v", c.ID, sig) // Signaling container in Stopped state is allowed. When all=false, // an error will be returned anyway; when all=true, this allows @@ -485,11 +485,23 @@ func (c *Container) Signal(sig syscall.Signal, all bool) error { return err } if !c.isSandboxRunning() { - return fmt.Errorf("container is not running") + return fmt.Errorf("sandbox is not running") } return c.Sandbox.SignalContainer(c.ID, sig, all) } +// SignalProcess sends sig to a specific process in the container. +func (c *Container) SignalProcess(sig syscall.Signal, pid int32) error { + log.Debugf("Signal process %d in container %q: %v", pid, c.ID, sig) + if err := c.requireStatus("signal a process inside", Running); err != nil { + return err + } + if !c.isSandboxRunning() { + return fmt.Errorf("sandbox is not running") + } + return c.Sandbox.SignalProcess(c.ID, int32(pid), sig, false) +} + // ForwardSignals forwards all signals received by the current process to the // container process inside the sandbox. It returns a function that will stop // forwarding signals. @@ -663,7 +675,7 @@ func (c *Container) waitForStopped() error { b := backoff.WithContext(backoff.NewConstantBackOff(100*time.Millisecond), ctx) op := func() error { if c.isSandboxRunning() { - if err := c.Signal(syscall.Signal(0), false); err == nil { + if err := c.SignalContainer(syscall.Signal(0), false); err == nil { return fmt.Errorf("container is still running") } } diff --git a/runsc/container/container_test.go b/runsc/container/container_test.go index 94572667e..d9cd38c0a 100644 --- a/runsc/container/container_test.go +++ b/runsc/container/container_test.go @@ -354,7 +354,7 @@ func TestLifecycle(t *testing.T) { <-ch time.Sleep(100 * time.Millisecond) // Send the container a SIGTERM which will cause it to stop. - if err := c.Signal(syscall.SIGTERM, false); err != nil { + if err := c.SignalContainer(syscall.SIGTERM, false); err != nil { t.Fatalf("error sending signal %v to container: %v", syscall.SIGTERM, err) } // Wait for it to die. @@ -559,6 +559,62 @@ func TestExec(t *testing.T) { } } +// TestKillPid verifies that we can signal individual exec'd processes. +func TestKillPid(t *testing.T) { + for _, conf := range configs(overlay) { + t.Logf("Running test with conf: %+v", conf) + + app, err := testutil.FindFile("runsc/container/test_app") + if err != nil { + t.Fatal("error finding test_app:", err) + } + + const nProcs = 4 + spec := testutil.NewSpecWithArgs(app, "task-tree", "--depth", strconv.Itoa(nProcs-1), "--width=1", "--pause=true") + rootDir, bundleDir, err := testutil.SetupContainer(spec, conf) + if err != nil { + t.Fatalf("error setting up container: %v", err) + } + defer os.RemoveAll(rootDir) + defer os.RemoveAll(bundleDir) + + // Create and start the container. + cont, err := Create(testutil.UniqueContainerID(), 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) + } + + // Verify that all processes are running. + if err := waitForProcessCount(cont, nProcs); err != nil { + t.Fatalf("timed out waiting for processes to start: %v", err) + } + + // Kill the child process with the largest PID. + procs, err := cont.Processes() + if err != nil { + t.Fatalf("failed to get process list: %v", err) + } + var pid int32 + for _, p := range procs { + if pid < int32(p.PID) { + pid = int32(p.PID) + } + } + if err := cont.SignalProcess(syscall.SIGKILL, pid); err != nil { + t.Fatalf("failed to signal process %d: %v", pid, err) + } + + // Verify that one process is gone. + if err := waitForProcessCount(cont, nProcs-1); err != nil { + t.Fatal(err) + } + } +} + // TestCheckpointRestore creates a container that continuously writes successive integers // to a file. To test checkpoint and restore functionality, the container is // checkpointed and the last number printed to the file is recorded. Then, it is restored in two diff --git a/runsc/container/multi_container_test.go b/runsc/container/multi_container_test.go index 77f8da8b0..1781a4602 100644 --- a/runsc/container/multi_container_test.go +++ b/runsc/container/multi_container_test.go @@ -335,7 +335,7 @@ func TestMultiContainerSignal(t *testing.T) { } // Kill process 2. - if err := containers[1].Signal(syscall.SIGKILL, false); err != nil { + if err := containers[1].SignalContainer(syscall.SIGKILL, false); err != nil { t.Errorf("failed to kill process 2: %v", err) } @@ -368,12 +368,12 @@ func TestMultiContainerSignal(t *testing.T) { // Now that process 2 is gone, ensure we get an error trying to // signal it again. - if err := containers[1].Signal(syscall.SIGKILL, false); err == nil { + if err := containers[1].SignalContainer(syscall.SIGKILL, false); err == nil { t.Errorf("container %q shouldn't exist, but we were able to signal it", containers[1].ID) } // Kill process 1. - if err := containers[0].Signal(syscall.SIGKILL, false); err != nil { + if err := containers[0].SignalContainer(syscall.SIGKILL, false); err != nil { t.Errorf("failed to kill process 1: %v", err) } @@ -395,7 +395,7 @@ func TestMultiContainerSignal(t *testing.T) { } // The sentry should be gone, so signaling should yield an error. - if err := containers[0].Signal(syscall.SIGKILL, false); err == nil { + if err := containers[0].SignalContainer(syscall.SIGKILL, false); err == nil { t.Errorf("sandbox %q shouldn't exist, but we were able to signal it", containers[0].Sandbox.ID) } } @@ -577,7 +577,7 @@ func TestMultiContainerKillAll(t *testing.T) { if tc.killContainer { // First kill the init process to make the container be stopped with // processes still running inside. - containers[1].Signal(syscall.SIGKILL, false) + containers[1].SignalContainer(syscall.SIGKILL, false) op := func() error { c, err := Load(conf.RootDir, ids[1]) if err != nil { @@ -598,7 +598,7 @@ func TestMultiContainerKillAll(t *testing.T) { t.Fatalf("failed to load child container %q: %v", c.ID, err) } // Kill'Em All - if err := c.Signal(syscall.SIGKILL, true); err != nil { + if err := c.SignalContainer(syscall.SIGKILL, true); err != nil { t.Fatalf("failed to send SIGKILL to container %q: %v", c.ID, err) } diff --git a/runsc/container/test_app.go b/runsc/container/test_app.go index 9e4b5326d..cc3b087e1 100644 --- a/runsc/container/test_app.go +++ b/runsc/container/test_app.go @@ -125,6 +125,7 @@ func server(listener net.Listener, out *os.File) { type taskTree struct { depth int width int + pause bool } // Name implements subcommands.Command. @@ -146,6 +147,7 @@ func (*taskTree) Usage() string { func (c *taskTree) SetFlags(f *flag.FlagSet) { f.IntVar(&c.depth, "depth", 1, "number of levels to create") f.IntVar(&c.width, "width", 1, "number of tasks at each level") + f.BoolVar(&c.pause, "pause", false, "whether the tasks should pause perpetually") } // Execute implements subcommands.Command. @@ -164,7 +166,8 @@ func (c *taskTree) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa cmd := exec.Command( "/proc/self/exe", c.Name(), "--depth", strconv.Itoa(c.depth-1), - "--width", strconv.Itoa(c.width)) + "--width", strconv.Itoa(c.width), + "--pause", strconv.FormatBool(c.pause)) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -177,6 +180,11 @@ func (c *taskTree) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa for _, c := range cmds { c.Wait() } + + if c.pause { + select {} + } + return subcommands.ExitSuccess } |