From d563cc164c118f075e243de1f1ea15fd49f0a928 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Tue, 4 Aug 2015 13:51:48 -0700 Subject: [PATCH] Signal to stop a container. Allow to set the signal to stop a container in `docker run`: - Use `--stop-signal` with docker-run to set the default signal the container will use to exit. Signed-off-by: David Calavera Upstream-commit: 0e50d946a25beb134bce2aaf4a209b5cfcbacf8f Component: engine --- components/engine/api/server/container.go | 25 ++++------------- .../engine/contrib/completion/bash/docker | 1 + .../contrib/completion/fish/docker.fish | 1 + .../engine/contrib/completion/zsh/_docker | 1 + components/engine/daemon/container.go | 23 +++++++++++---- .../engine/daemon/container_unit_test.go | 28 +++++++++++++++++++ components/engine/daemon/daemon.go | 5 ++++ .../docs/reference/api/docker_remote_api.md | 1 + .../docker_cli_run_unix_test.go | 16 +++++++++++ components/engine/man/docker-create.1.md | 4 +++ components/engine/man/docker-inspect.1.md | 3 +- components/engine/man/docker-run.1.md | 6 +++- components/engine/man/docker-stop.1.md | 2 +- components/engine/pkg/signal/signal.go | 21 ++++++++++++++ components/engine/pkg/signal/signal_unix.go | 13 +++++---- .../engine/pkg/signal/signal_windows.go | 2 ++ components/engine/runconfig/config.go | 1 + components/engine/runconfig/parse.go | 3 ++ 18 files changed, 124 insertions(+), 32 deletions(-) diff --git a/components/engine/api/server/container.go b/components/engine/api/server/container.go index 3af7972674..87b1998541 100644 --- a/components/engine/api/server/container.go +++ b/components/engine/api/server/container.go @@ -6,6 +6,7 @@ import ( "net/http" "strconv" "strings" + "syscall" "time" "golang.org/x/net/websocket" @@ -220,32 +221,18 @@ func (s *Server) postContainersKill(ctx context.Context, w http.ResponseWriter, return err } - var sig uint64 + var sig syscall.Signal name := vars["name"] // If we have a signal, look at it. Otherwise, do nothing if sigStr := r.Form.Get("signal"); sigStr != "" { - // Check if we passed the signal as a number: - // The largest legal signal is 31, so let's parse on 5 bits - sigN, err := strconv.ParseUint(sigStr, 10, 5) - if err != nil { - // The signal is not a number, treat it as a string (either like - // "KILL" or like "SIGKILL") - syscallSig, ok := signal.SignalMap[strings.TrimPrefix(sigStr, "SIG")] - if !ok { - return fmt.Errorf("Invalid signal: %s", sigStr) - } - sig = uint64(syscallSig) - } else { - sig = sigN - } - - if sig == 0 { - return fmt.Errorf("Invalid signal: %s", sigStr) + var err error + if sig, err = signal.ParseSignal(sigStr); err != nil { + return err } } - if err := s.daemon.ContainerKill(name, sig); err != nil { + if err := s.daemon.ContainerKill(name, uint64(sig)); err != nil { _, isStopped := err.(daemon.ErrContainerNotRunning) // Return error that's not caused because the container is stopped. // Return error if the container is not running and the api is >= 1.20 diff --git a/components/engine/contrib/completion/bash/docker b/components/engine/contrib/completion/bash/docker index 4b8d9bd209..9d63416c4b 100644 --- a/components/engine/contrib/completion/bash/docker +++ b/components/engine/contrib/completion/bash/docker @@ -1149,6 +1149,7 @@ _docker_run() { --publish -p --restart --security-opt + --stop-signal --ulimit --user -u --uts diff --git a/components/engine/contrib/completion/fish/docker.fish b/components/engine/contrib/completion/fish/docker.fish index 9a32f8c64e..7dcc554139 100644 --- a/components/engine/contrib/completion/fish/docker.fish +++ b/components/engine/contrib/completion/fish/docker.fish @@ -335,6 +335,7 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l restart -d 'Res complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l rm -d 'Automatically remove the container when it exits (incompatible with -d)' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l security-opt -d 'Security Options' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l sig-proxy -d 'Proxy received signals to the process (non-TTY mode only). SIGCHLD, SIGSTOP, and SIGKILL are not proxied.' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l stop-signal 'Signal to kill a container' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s t -l tty -d 'Allocate a pseudo-TTY' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s u -l user -d 'Username or UID' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s v -l volume -d 'Bind mount a volume (e.g., from the host: -v /host:/container, from Docker: -v /container)' diff --git a/components/engine/contrib/completion/zsh/_docker b/components/engine/contrib/completion/zsh/_docker index fc894c84b5..448dac9d59 100644 --- a/components/engine/contrib/completion/zsh/_docker +++ b/components/engine/contrib/completion/zsh/_docker @@ -502,6 +502,7 @@ __docker_subcommand() { "($help -d --detach)"{-d,--detach}"[Detached mode: leave the container running in the background]" \ "($help)--rm[Remove intermediate containers when it exits]" \ "($help)--sig-proxy[Proxy all received signals to the process (non-TTY mode only)]" \ + "($help)--stop-signal[Signal to kill a container]" \ "($help -): :__docker_images" \ "($help -):command: _command_names -e" \ "($help -)*::arguments: _normal" && ret=0 diff --git a/components/engine/daemon/container.go b/components/engine/daemon/container.go index bd622b8780..52465be590 100644 --- a/components/engine/daemon/container.go +++ b/components/engine/daemon/container.go @@ -27,6 +27,7 @@ import ( "github.com/docker/docker/pkg/mount" "github.com/docker/docker/pkg/nat" "github.com/docker/docker/pkg/promise" + "github.com/docker/docker/pkg/signal" "github.com/docker/docker/pkg/symlink" "github.com/docker/docker/runconfig" "github.com/docker/docker/volume" @@ -495,10 +496,10 @@ func (container *Container) Kill() error { return nil } -// Stop halts a container by sending SIGTERM, waiting for the given +// Stop halts a container by sending a stop signal, waiting for the given // duration in seconds, and then calling SIGKILL and waiting for the // process to exit. If a negative duration is given, Stop will wait -// for SIGTERM forever. If the container is not running Stop returns +// for the initial signal forever. If the container is not running Stop returns // immediately. func (container *Container) Stop(seconds int) error { if !container.IsRunning() { @@ -506,9 +507,9 @@ func (container *Container) Stop(seconds int) error { } // 1. Send a SIGTERM - if err := container.killPossiblyDeadProcess(int(syscall.SIGTERM)); err != nil { + if err := container.killPossiblyDeadProcess(container.stopSignal()); err != nil { logrus.Infof("Failed to send SIGTERM to the process, force killing") - if err := container.killPossiblyDeadProcess(int(syscall.SIGKILL)); err != nil { + if err := container.killPossiblyDeadProcess(9); err != nil { return err } } @@ -523,7 +524,7 @@ func (container *Container) Stop(seconds int) error { } } - container.logEvent("stop") + container.LogEvent("stop") return nil } @@ -1140,3 +1141,15 @@ func (container *Container) copyImagePathContent(v volume.Volume, destination st return v.Unmount() } + +func (container *Container) stopSignal() int { + var stopSignal syscall.Signal + if container.Config.StopSignal != "" { + stopSignal, _ = signal.ParseSignal(container.Config.StopSignal) + } + + if int(stopSignal) == 0 { + stopSignal, _ = signal.ParseSignal(signal.DefaultStopSignal) + } + return int(stopSignal) +} diff --git a/components/engine/daemon/container_unit_test.go b/components/engine/daemon/container_unit_test.go index ab30a8e373..ff45f71276 100644 --- a/components/engine/daemon/container_unit_test.go +++ b/components/engine/daemon/container_unit_test.go @@ -31,3 +31,31 @@ func TestValidContainerNames(t *testing.T) { } } } + +func TestContainerStopSignal(t *testing.T) { + c := &Container{ + CommonContainer: CommonContainer{ + Config: &runconfig.Config{}, + }, + } + + def, err := signal.ParseSignal(signal.DefaultStopSignal) + if err != nil { + t.Fatal(err) + } + + s := c.stopSignal() + if s != int(def) { + t.Fatalf("Expected %v, got %v", def, s) + } + + c = &Container{ + CommonContainer: CommonContainer{ + Config: &runconfig.Config{StopSignal: "SIGKILL"}, + }, + } + s = c.stopSignal() + if s != 9 { + t.Fatalf("Expected 9, got %v", s) + } +} diff --git a/components/engine/daemon/daemon.go b/components/engine/daemon/daemon.go index f5a0c035f3..bf4727cba1 100644 --- a/components/engine/daemon/daemon.go +++ b/components/engine/daemon/daemon.go @@ -1095,6 +1095,11 @@ func (daemon *Daemon) verifyContainerSettings(hostConfig *runconfig.HostConfig, } } + _, err := signal.ParseSignal(config.StopSignal) + if err != nil { + return nil, err + } + // Now do platform-specific verification return verifyPlatformContainerSettings(daemon, hostConfig, config) } diff --git a/components/engine/docs/reference/api/docker_remote_api.md b/components/engine/docs/reference/api/docker_remote_api.md index 1e1ff2d5c8..0d09f5c4ef 100644 --- a/components/engine/docs/reference/api/docker_remote_api.md +++ b/components/engine/docs/reference/api/docker_remote_api.md @@ -82,6 +82,7 @@ This section lists each version from latest to oldest. Each listing includes a * `DELETE /volumes/(name)`remove a volume with the specified name. * `VolumeDriver` has been moved from config to hostConfig to make the configuration portable. * `GET /images/(name)/json` now returns information about tags of the image. +* The `config` option now accepts the field `StopSignal`, which specifies the signal to use to kill a container. ### v1.20 API changes diff --git a/components/engine/integration-cli/docker_cli_run_unix_test.go b/components/engine/integration-cli/docker_cli_run_unix_test.go index 9ca05145e8..d3ddace8db 100644 --- a/components/engine/integration-cli/docker_cli_run_unix_test.go +++ b/components/engine/integration-cli/docker_cli_run_unix_test.go @@ -315,3 +315,19 @@ func (s *DockerSuite) TestRunWithSwappinessInvalid(c *check.C) { c.Fatalf("failed. test was able to set invalid value, output: %q", out) } } + +func (s *DockerSuite) TestStopContainerSignal(c *check.C) { + out, _ := dockerCmd(c, "run", "--stop-signal", "SIGUSR1", "-d", "busybox", "/bin/sh", "-c", `trap 'echo "exit trapped"; exit 0' USR1; while true; do sleep 1; done`) + containerID := strings.TrimSpace(out) + + if err := waitRun(containerID); err != nil { + c.Fatal(err) + } + + dockerCmd(c, "stop", containerID) + out, _ = dockerCmd(c, "logs", containerID) + + if !strings.Contains(out, "exit trapped") { + c.Fatalf("Expected `exit trapped` in the log, got %v", out) + } +} diff --git a/components/engine/man/docker-create.1.md b/components/engine/man/docker-create.1.md index 9385c882be..d040e62b7b 100644 --- a/components/engine/man/docker-create.1.md +++ b/components/engine/man/docker-create.1.md @@ -51,6 +51,7 @@ docker-create - Create a new container [**--read-only**[=*false*]] [**--restart**[=*RESTART*]] [**--security-opt**[=*[]*]] +[**--stop-signal**[=*SIGNAL*]] [**-t**|**--tty**[=*false*]] [**-u**|**--user**[=*USER*]] [**--ulimit**[=*[]*]] @@ -239,6 +240,9 @@ This value should always larger than **-m**, so you should always use this with **--security-opt**=[] Security Options +**--stop-signal**=SIGTERM + Signal to stop a container. Default is SIGTERM. + **-t**, **--tty**=*true*|*false* Allocate a pseudo-TTY. The default is *false*. diff --git a/components/engine/man/docker-inspect.1.md b/components/engine/man/docker-inspect.1.md index a1bbb317a9..e289899bbb 100644 --- a/components/engine/man/docker-inspect.1.md +++ b/components/engine/man/docker-inspect.1.md @@ -180,7 +180,8 @@ To get information on a container use its ID or instance name: "Memory": 0, "MemorySwap": 0, "CpuShares": 0, - "Cpuset": "" + "Cpuset": "", + "StopSignal": 15, } } ] diff --git a/components/engine/man/docker-run.1.md b/components/engine/man/docker-run.1.md index 0bb339d34e..b37d07b0e9 100644 --- a/components/engine/man/docker-run.1.md +++ b/components/engine/man/docker-run.1.md @@ -53,6 +53,7 @@ docker-run - Run a command in a new container [**--restart**[=*RESTART*]] [**--rm**[=*false*]] [**--security-opt**[=*[]*]] +[**--stop-signal**[=*SIGNAL*]] [**--sig-proxy**[=*true*]] [**-t**|**--tty**[=*false*]] [**-u**|**--user**[=*USER*]] @@ -371,7 +372,7 @@ its root filesystem mounted as read only prohibiting any writes. **--restart**="no" Restart policy to apply when a container exits (no, on-failure[:max-retry], always, unless-stopped). - + **--rm**=*true*|*false* Automatically remove the container when it exits (incompatible with -d). The default is *false*. @@ -384,6 +385,9 @@ its root filesystem mounted as read only prohibiting any writes. "label:level:LEVEL" : Set the label level for the container "label:disable" : Turn off label confinement for the container +**--stop-signal**=SIGTERM + Signal to stop a container. Default is SIGTERM. + **--sig-proxy**=*true*|*false* Proxy received signals to the process (non-TTY mode only). SIGCHLD, SIGSTOP, and SIGKILL are not proxied. The default is *true*. diff --git a/components/engine/man/docker-stop.1.md b/components/engine/man/docker-stop.1.md index 9b882db49d..4939070d97 100644 --- a/components/engine/man/docker-stop.1.md +++ b/components/engine/man/docker-stop.1.md @@ -19,7 +19,7 @@ Stop a running container (Send SIGTERM, and then SIGKILL after Print usage statement **-t**, **--time**=10 - Number of seconds to wait for the container to stop before killing it. Default is 10 seconds. + Number of seconds to wait for the container to stop before killing it. Default is 10 seconds. #See also **docker-start(1)** to restart a stopped container. diff --git a/components/engine/pkg/signal/signal.go b/components/engine/pkg/signal/signal.go index db60bf2e59..106fe20d74 100644 --- a/components/engine/pkg/signal/signal.go +++ b/components/engine/pkg/signal/signal.go @@ -3,8 +3,12 @@ package signal import ( + "fmt" "os" "os/signal" + "strconv" + "strings" + "syscall" ) // CatchAll catches all signals and relays them to the specified channel. @@ -21,3 +25,20 @@ func StopCatch(sigc chan os.Signal) { signal.Stop(sigc) close(sigc) } + +// ParseSignal translates a string to a valid syscall signal. +// It returns an error if the signal map doesn't include the given signal. +func ParseSignal(rawSignal string) (syscall.Signal, error) { + s, err := strconv.Atoi(rawSignal) + if err == nil { + if s == 0 { + return -1, fmt.Errorf("Invalid signal: %s", rawSignal) + } + return syscall.Signal(s), nil + } + signal, ok := SignalMap[strings.TrimPrefix(strings.ToUpper(rawSignal), "SIG")] + if !ok { + return -1, fmt.Errorf("Invalid signal: %s", rawSignal) + } + return signal, nil +} diff --git a/components/engine/pkg/signal/signal_unix.go b/components/engine/pkg/signal/signal_unix.go index 5c1ad5f722..d4fea931d1 100644 --- a/components/engine/pkg/signal/signal_unix.go +++ b/components/engine/pkg/signal/signal_unix.go @@ -9,8 +9,11 @@ import ( // Signals used in api/client (no windows equivalent, use // invalid signals so they don't get handled) -// SIGCHLD is a signal sent to a process when a child process terminates, is interrupted, or resumes after being interrupted. -const SIGCHLD = syscall.SIGCHLD - -// SIGWINCH is a signal sent to a process when its controlling terminal changes its size -const SIGWINCH = syscall.SIGWINCH +const ( + // SIGCHLD is a signal sent to a process when a child process terminates, is interrupted, or resumes after being interrupted. + SIGCHLD = syscall.SIGCHLD + // SIGWINCH is a signal sent to a process when its controlling terminal changes its size + SIGWINCH = syscall.SIGWINCH + // DefaultStopSignal is the syscall signal used to stop a container in unix systems. + DefaultStopSignal = "SIGTERM" +) diff --git a/components/engine/pkg/signal/signal_windows.go b/components/engine/pkg/signal/signal_windows.go index 1f1a6edbfc..b0585b0ed9 100644 --- a/components/engine/pkg/signal/signal_windows.go +++ b/components/engine/pkg/signal/signal_windows.go @@ -11,4 +11,6 @@ import ( const ( SIGCHLD = syscall.Signal(0xff) SIGWINCH = syscall.Signal(0xff) + // DefaultStopSignal is the syscall signal used to stop a container in windows systems. + DefaultStopSignal = "15" ) diff --git a/components/engine/runconfig/config.go b/components/engine/runconfig/config.go index 04010954be..16a2f95b26 100644 --- a/components/engine/runconfig/config.go +++ b/components/engine/runconfig/config.go @@ -34,6 +34,7 @@ type Config struct { MacAddress string // Mac Address of the container OnBuild []string // ONBUILD metadata that were defined on the image Dockerfile Labels map[string]string // List of labels set to this container + StopSignal string // Signal to stop a container } // DecodeContainerConfig decodes a json encoded config into a ContainerConfigWrapper diff --git a/components/engine/runconfig/parse.go b/components/engine/runconfig/parse.go index 8850b9f46c..079dff728e 100644 --- a/components/engine/runconfig/parse.go +++ b/components/engine/runconfig/parse.go @@ -9,6 +9,7 @@ import ( flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/nat" "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/pkg/signal" "github.com/docker/docker/pkg/stringutils" "github.com/docker/docker/pkg/units" ) @@ -93,6 +94,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe flLoggingDriver = cmd.String([]string{"-log-driver"}, "", "Logging driver for container") flCgroupParent = cmd.String([]string{"-cgroup-parent"}, "", "Optional parent cgroup for the container") flVolumeDriver = cmd.String([]string{"-volume-driver"}, "", "Optional volume driver for the container") + flStopSignal = cmd.String([]string{"-stop-signal"}, signal.DefaultStopSignal, fmt.Sprintf("Signal to stop a container, %v by default", signal.DefaultStopSignal)) ) cmd.Var(&flAttach, []string{"a", "-attach"}, "Attach to STDIN, STDOUT or STDERR") @@ -322,6 +324,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe Entrypoint: entrypoint, WorkingDir: *flWorkingDir, Labels: convertKVStringsToMap(labels), + StopSignal: *flStopSignal, } hostConfig := &HostConfig{