diff --git a/daemon/exec.go b/daemon/exec.go index 464dd5db66..4dba7c9d47 100644 --- a/daemon/exec.go +++ b/daemon/exec.go @@ -135,6 +135,11 @@ func (d *Daemon) ContainerExecStart(name string, stdin io.ReadCloser, stdout io. } ec.Lock() + if ec.ExitCode != nil { + ec.Unlock() + return derr.ErrorCodeExecExited.WithArgs(ec.ID) + } + if ec.Running { ec.Unlock() return derr.ErrorCodeExecRunning.WithArgs(ec.ID) @@ -214,7 +219,7 @@ func (d *Daemon) Exec(c *container.Container, execConfig *exec.Config, pipes *ex exitStatus = 128 } - execConfig.ExitCode = exitStatus + execConfig.ExitCode = &exitStatus execConfig.Running = false return exitStatus, err diff --git a/daemon/exec/exec.go b/daemon/exec/exec.go index 5504ed3575..5536623191 100644 --- a/daemon/exec/exec.go +++ b/daemon/exec/exec.go @@ -18,7 +18,7 @@ type Config struct { *runconfig.StreamConfig ID string Running bool - ExitCode int + ExitCode *int ProcessConfig *execdriver.ProcessConfig OpenStdin bool OpenStderr bool diff --git a/errors/daemon.go b/errors/daemon.go index 278e712275..644dcdbe68 100644 --- a/errors/daemon.go +++ b/errors/daemon.go @@ -742,6 +742,15 @@ var ( HTTPStatusCode: http.StatusInternalServerError, }) + // ErrorCodeExecExited is generated when we try to start an exec + // but its already running. + ErrorCodeExecExited = errcode.Register(errGroup, errcode.ErrorDescriptor{ + Value: "EXECEXITED", + Message: "Error: Exec command %s has already run", + Description: "An attempt to start an 'exec' was made, but 'exec' was already run", + HTTPStatusCode: http.StatusConflict, + }) + // ErrorCodeExecCantRun is generated when we try to start an exec // but it failed for some reason. ErrorCodeExecCantRun = errcode.Register(errGroup, errcode.ErrorDescriptor{ diff --git a/integration-cli/docker_api_exec_test.go b/integration-cli/docker_api_exec_test.go index de504751de..c6ed847e24 100644 --- a/integration-cli/docker_api_exec_test.go +++ b/integration-cli/docker_api_exec_test.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" "strings" + "time" "github.com/docker/docker/pkg/integration/checker" "github.com/go-check/check" @@ -66,33 +67,23 @@ func (s *DockerSuite) TestExecAPIStart(c *check.C) { testRequires(c, DaemonIsLinux) // Uses pause/unpause but bits may be salvagable to Windows to Windows CI dockerCmd(c, "run", "-d", "--name", "test", "busybox", "top") - startExec := func(id string, code int) { - resp, body, err := sockRequestRaw("POST", fmt.Sprintf("/exec/%s/start", id), strings.NewReader(`{"Detach": true}`), "application/json") - c.Assert(err, checker.IsNil) - - b, err := readBody(body) - comment := check.Commentf("response body: %s", b) - c.Assert(err, checker.IsNil, comment) - c.Assert(resp.StatusCode, checker.Equals, code, comment) - } - id := createExec(c, "test") - startExec(id, http.StatusOK) + startExec(c, id, http.StatusOK) id = createExec(c, "test") dockerCmd(c, "stop", "test") - startExec(id, http.StatusNotFound) + startExec(c, id, http.StatusNotFound) dockerCmd(c, "start", "test") - startExec(id, http.StatusNotFound) + startExec(c, id, http.StatusNotFound) // make sure exec is created before pausing id = createExec(c, "test") dockerCmd(c, "pause", "test") - startExec(id, http.StatusConflict) + startExec(c, id, http.StatusConflict) dockerCmd(c, "unpause", "test") - startExec(id, http.StatusOK) + startExec(c, id, http.StatusOK) } func (s *DockerSuite) TestExecAPIStartBackwardsCompatible(c *check.C) { @@ -108,6 +99,30 @@ func (s *DockerSuite) TestExecAPIStartBackwardsCompatible(c *check.C) { c.Assert(resp.StatusCode, checker.Equals, http.StatusOK, comment) } +// #19362 +func (s *DockerSuite) TestExecAPIStartMultipleTimesError(c *check.C) { + dockerCmd(c, "run", "-d", "--name", "test", "busybox", "top") + execID := createExec(c, "test") + startExec(c, execID, http.StatusOK) + + timeout := time.After(10 * time.Second) + var execJSON struct{ Running bool } + for { + select { + case <-timeout: + c.Fatal("timeout waiting for exec to start") + default: + } + + inspectExec(c, execID, &execJSON) + if !execJSON.Running { + break + } + } + + startExec(c, execID, http.StatusConflict) +} + func createExec(c *check.C, name string) string { _, b, err := sockRequest("POST", fmt.Sprintf("/containers/%s/exec", name), map[string]interface{}{"Cmd": []string{"true"}}) c.Assert(err, checker.IsNil, check.Commentf(string(b))) @@ -118,3 +133,22 @@ func createExec(c *check.C, name string) string { c.Assert(json.Unmarshal(b, &createResp), checker.IsNil, check.Commentf(string(b))) return createResp.ID } + +func startExec(c *check.C, id string, code int) { + resp, body, err := sockRequestRaw("POST", fmt.Sprintf("/exec/%s/start", id), strings.NewReader(`{"Detach": true}`), "application/json") + c.Assert(err, checker.IsNil) + + b, err := readBody(body) + comment := check.Commentf("response body: %s", b) + c.Assert(err, checker.IsNil, comment) + c.Assert(resp.StatusCode, checker.Equals, code, comment) +} + +func inspectExec(c *check.C, id string, out interface{}) { + resp, body, err := sockRequestRaw("GET", fmt.Sprintf("/exec/%s/json", id), nil, "") + c.Assert(err, checker.IsNil) + defer body.Close() + c.Assert(resp.StatusCode, checker.Equals, http.StatusOK) + err = json.NewDecoder(body).Decode(out) + c.Assert(err, checker.IsNil) +}