| ... | ... |
@@ -2,7 +2,6 @@ package container |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"io" |
| 5 |
- "time" |
|
| 6 | 5 |
|
| 7 | 6 |
"golang.org/x/net/context" |
| 8 | 7 |
|
| ... | ... |
@@ -10,6 +9,7 @@ import ( |
| 10 | 10 |
"github.com/docker/docker/api/types/backend" |
| 11 | 11 |
"github.com/docker/docker/api/types/container" |
| 12 | 12 |
"github.com/docker/docker/api/types/filters" |
| 13 |
+ containerpkg "github.com/docker/docker/container" |
|
| 13 | 14 |
"github.com/docker/docker/pkg/archive" |
| 14 | 15 |
) |
| 15 | 16 |
|
| ... | ... |
@@ -44,7 +44,7 @@ type stateBackend interface {
|
| 44 | 44 |
ContainerStop(name string, seconds *int) error |
| 45 | 45 |
ContainerUnpause(name string) error |
| 46 | 46 |
ContainerUpdate(name string, hostConfig *container.HostConfig) (container.ContainerUpdateOKBody, error) |
| 47 |
- ContainerWait(name string, timeout time.Duration) (int, error) |
|
| 47 |
+ ContainerWait(ctx context.Context, name string, condition containerpkg.WaitCondition) (<-chan containerpkg.StateStatus, error) |
|
| 48 | 48 |
} |
| 49 | 49 |
|
| 50 | 50 |
// monitorBackend includes functions to implement to provide containers monitoring functionality. |
| ... | ... |
@@ -59,7 +59,7 @@ func (r *containerRouter) initRoutes() {
|
| 59 | 59 |
router.NewPostRoute("/containers/{name:.*}/restart", r.postContainersRestart),
|
| 60 | 60 |
router.NewPostRoute("/containers/{name:.*}/start", r.postContainersStart),
|
| 61 | 61 |
router.NewPostRoute("/containers/{name:.*}/stop", r.postContainersStop),
|
| 62 |
- router.NewPostRoute("/containers/{name:.*}/wait", r.postContainersWait),
|
|
| 62 |
+ router.NewPostRoute("/containers/{name:.*}/wait", r.postContainersWait, router.WithCancel),
|
|
| 63 | 63 |
router.NewPostRoute("/containers/{name:.*}/resize", r.postContainersResize),
|
| 64 | 64 |
router.NewPostRoute("/containers/{name:.*}/attach", r.postContainersAttach),
|
| 65 | 65 |
router.NewPostRoute("/containers/{name:.*}/copy", r.postContainersCopy), // Deprecated since 1.8, Errors out since 1.12
|
| ... | ... |
@@ -7,7 +7,6 @@ import ( |
| 7 | 7 |
"net/http" |
| 8 | 8 |
"strconv" |
| 9 | 9 |
"syscall" |
| 10 |
- "time" |
|
| 11 | 10 |
|
| 12 | 11 |
"github.com/Sirupsen/logrus" |
| 13 | 12 |
"github.com/docker/docker/api" |
| ... | ... |
@@ -17,6 +16,7 @@ import ( |
| 17 | 17 |
"github.com/docker/docker/api/types/container" |
| 18 | 18 |
"github.com/docker/docker/api/types/filters" |
| 19 | 19 |
"github.com/docker/docker/api/types/versions" |
| 20 |
+ containerpkg "github.com/docker/docker/container" |
|
| 20 | 21 |
"github.com/docker/docker/pkg/ioutils" |
| 21 | 22 |
"github.com/docker/docker/pkg/signal" |
| 22 | 23 |
"golang.org/x/net/context" |
| ... | ... |
@@ -284,13 +284,48 @@ func (s *containerRouter) postContainersUnpause(ctx context.Context, w http.Resp |
| 284 | 284 |
} |
| 285 | 285 |
|
| 286 | 286 |
func (s *containerRouter) postContainersWait(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
| 287 |
- status, err := s.backend.ContainerWait(vars["name"], -1*time.Second) |
|
| 287 |
+ // Behavior changed in version 1.30 to handle wait condition and to |
|
| 288 |
+ // return headers immediately. |
|
| 289 |
+ version := httputils.VersionFromContext(ctx) |
|
| 290 |
+ legacyBehavior := versions.LessThan(version, "1.30") |
|
| 291 |
+ |
|
| 292 |
+ // The wait condition defaults to "not-running". |
|
| 293 |
+ waitCondition := containerpkg.WaitConditionNotRunning |
|
| 294 |
+ if !legacyBehavior {
|
|
| 295 |
+ if err := httputils.ParseForm(r); err != nil {
|
|
| 296 |
+ return err |
|
| 297 |
+ } |
|
| 298 |
+ switch container.WaitCondition(r.Form.Get("condition")) {
|
|
| 299 |
+ case container.WaitConditionNextExit: |
|
| 300 |
+ waitCondition = containerpkg.WaitConditionNextExit |
|
| 301 |
+ case container.WaitConditionRemoved: |
|
| 302 |
+ waitCondition = containerpkg.WaitConditionRemoved |
|
| 303 |
+ } |
|
| 304 |
+ } |
|
| 305 |
+ |
|
| 306 |
+ // Note: the context should get canceled if the client closes the |
|
| 307 |
+ // connection since this handler has been wrapped by the |
|
| 308 |
+ // router.WithCancel() wrapper. |
|
| 309 |
+ waitC, err := s.backend.ContainerWait(ctx, vars["name"], waitCondition) |
|
| 288 | 310 |
if err != nil {
|
| 289 | 311 |
return err |
| 290 | 312 |
} |
| 291 | 313 |
|
| 292 |
- return httputils.WriteJSON(w, http.StatusOK, &container.ContainerWaitOKBody{
|
|
| 293 |
- StatusCode: int64(status), |
|
| 314 |
+ w.Header().Set("Content-Type", "application/json")
|
|
| 315 |
+ |
|
| 316 |
+ if !legacyBehavior {
|
|
| 317 |
+ // Write response header immediately. |
|
| 318 |
+ w.WriteHeader(http.StatusOK) |
|
| 319 |
+ if flusher, ok := w.(http.Flusher); ok {
|
|
| 320 |
+ flusher.Flush() |
|
| 321 |
+ } |
|
| 322 |
+ } |
|
| 323 |
+ |
|
| 324 |
+ // Block on the result of the wait operation. |
|
| 325 |
+ status := <-waitC |
|
| 326 |
+ |
|
| 327 |
+ return json.NewEncoder(w).Encode(&container.ContainerWaitOKBody{
|
|
| 328 |
+ StatusCode: int64(status.ExitCode()), |
|
| 294 | 329 |
}) |
| 295 | 330 |
} |
| 296 | 331 |
|
| ... | ... |
@@ -4270,6 +4270,11 @@ paths: |
| 4270 | 4270 |
required: true |
| 4271 | 4271 |
description: "ID or name of the container" |
| 4272 | 4272 |
type: "string" |
| 4273 |
+ - name: "condition" |
|
| 4274 |
+ in: "query" |
|
| 4275 |
+ description: "Wait until a container state reaches the given condition, either 'not-running' (default), 'next-exit', or 'removed'." |
|
| 4276 |
+ type: "string" |
|
| 4277 |
+ default: "not-running" |
|
| 4273 | 4278 |
tags: ["Container"] |
| 4274 | 4279 |
/containers/{id}:
|
| 4275 | 4280 |
delete: |
| 4276 | 4281 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,22 @@ |
| 0 |
+package container |
|
| 1 |
+ |
|
| 2 |
+// WaitCondition is a type used to specify a container state for which |
|
| 3 |
+// to wait. |
|
| 4 |
+type WaitCondition string |
|
| 5 |
+ |
|
| 6 |
+// Possible WaitCondition Values. |
|
| 7 |
+// |
|
| 8 |
+// WaitConditionNotRunning (default) is used to wait for any of the non-running |
|
| 9 |
+// states: "created", "exited", "dead", "removing", or "removed". |
|
| 10 |
+// |
|
| 11 |
+// WaitConditionNextExit is used to wait for the next time the state changes |
|
| 12 |
+// to a non-running state. If the state is currently "created" or "exited", |
|
| 13 |
+// this would cause Wait() to block until either the container runs and exits |
|
| 14 |
+// or is removed. |
|
| 15 |
+// |
|
| 16 |
+// WaitConditionRemoved is used to wait for the container to be removed. |
|
| 17 |
+const ( |
|
| 18 |
+ WaitConditionNotRunning WaitCondition = "not-running" |
|
| 19 |
+ WaitConditionNextExit WaitCondition = "next-exit" |
|
| 20 |
+ WaitConditionRemoved WaitCondition = "removed" |
|
| 21 |
+) |
| ... | ... |
@@ -6,12 +6,13 @@ package builder |
| 6 | 6 |
|
| 7 | 7 |
import ( |
| 8 | 8 |
"io" |
| 9 |
- "time" |
|
| 9 |
+ |
|
| 10 |
+ "golang.org/x/net/context" |
|
| 10 | 11 |
|
| 11 | 12 |
"github.com/docker/docker/api/types" |
| 12 | 13 |
"github.com/docker/docker/api/types/backend" |
| 13 | 14 |
"github.com/docker/docker/api/types/container" |
| 14 |
- "golang.org/x/net/context" |
|
| 15 |
+ containerpkg "github.com/docker/docker/container" |
|
| 15 | 16 |
) |
| 16 | 17 |
|
| 17 | 18 |
const ( |
| ... | ... |
@@ -49,7 +50,7 @@ type Backend interface {
|
| 49 | 49 |
// ContainerStart starts a new container |
| 50 | 50 |
ContainerStart(containerID string, hostConfig *container.HostConfig, checkpoint string, checkpointDir string) error |
| 51 | 51 |
// ContainerWait stops processing until the given container is stopped. |
| 52 |
- ContainerWait(containerID string, timeout time.Duration) (int, error) |
|
| 52 |
+ ContainerWait(ctx context.Context, name string, condition containerpkg.WaitCondition) (<-chan containerpkg.StateStatus, error) |
|
| 53 | 53 |
// ContainerCreateWorkdir creates the workdir |
| 54 | 54 |
ContainerCreateWorkdir(containerID string) error |
| 55 | 55 |
|
| ... | ... |
@@ -23,6 +23,7 @@ import ( |
| 23 | 23 |
"github.com/docker/docker/api/types/container" |
| 24 | 24 |
"github.com/docker/docker/builder" |
| 25 | 25 |
"github.com/docker/docker/builder/remotecontext" |
| 26 |
+ containerpkg "github.com/docker/docker/container" |
|
| 26 | 27 |
"github.com/docker/docker/pkg/httputils" |
| 27 | 28 |
"github.com/docker/docker/pkg/ioutils" |
| 28 | 29 |
"github.com/docker/docker/pkg/jsonmessage" |
| ... | ... |
@@ -596,16 +597,25 @@ func (b *Builder) run(cID string, cmd []string) (err error) {
|
| 596 | 596 |
return err |
| 597 | 597 |
} |
| 598 | 598 |
|
| 599 |
- if ret, _ := b.docker.ContainerWait(cID, -1); ret != 0 {
|
|
| 599 |
+ waitC, err := b.docker.ContainerWait(b.clientCtx, cID, containerpkg.WaitConditionNotRunning) |
|
| 600 |
+ if err != nil {
|
|
| 601 |
+ // Unable to begin waiting for container. |
|
| 602 |
+ close(finished) |
|
| 603 |
+ if cancelErr := <-cancelErrCh; cancelErr != nil {
|
|
| 604 |
+ logrus.Debugf("Build cancelled (%v) and unable to begin ContainerWait: %d", cancelErr, err)
|
|
| 605 |
+ } |
|
| 606 |
+ return err |
|
| 607 |
+ } |
|
| 608 |
+ |
|
| 609 |
+ if status := <-waitC; status.ExitCode() != 0 {
|
|
| 600 | 610 |
close(finished) |
| 601 | 611 |
if cancelErr := <-cancelErrCh; cancelErr != nil {
|
| 602 |
- logrus.Debugf("Build cancelled (%v) and got a non-zero code from ContainerWait: %d",
|
|
| 603 |
- cancelErr, ret) |
|
| 612 |
+ logrus.Debugf("Build cancelled (%v) and got a non-zero code from ContainerWait: %d", cancelErr, status.ExitCode())
|
|
| 604 | 613 |
} |
| 605 | 614 |
// TODO: change error type, because jsonmessage.JSONError assumes HTTP |
| 606 | 615 |
return &jsonmessage.JSONError{
|
| 607 |
- Message: fmt.Sprintf("The command '%s' returned a non-zero code: %d", strings.Join(cmd, " "), ret),
|
|
| 608 |
- Code: ret, |
|
| 616 |
+ Message: fmt.Sprintf("The command '%s' returned a non-zero code: %d", strings.Join(cmd, " "), status.ExitCode()),
|
|
| 617 |
+ Code: status.ExitCode(), |
|
| 609 | 618 |
} |
| 610 | 619 |
} |
| 611 | 620 |
close(finished) |
| ... | ... |
@@ -2,13 +2,13 @@ package dockerfile |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"io" |
| 5 |
- "time" |
|
| 6 | 5 |
|
| 7 | 6 |
"github.com/docker/distribution/reference" |
| 8 | 7 |
"github.com/docker/docker/api/types" |
| 9 | 8 |
"github.com/docker/docker/api/types/backend" |
| 10 | 9 |
"github.com/docker/docker/api/types/container" |
| 11 | 10 |
"github.com/docker/docker/builder" |
| 11 |
+ containerpkg "github.com/docker/docker/container" |
|
| 12 | 12 |
"github.com/docker/docker/image" |
| 13 | 13 |
"golang.org/x/net/context" |
| 14 | 14 |
) |
| ... | ... |
@@ -54,8 +54,8 @@ func (m *MockBackend) ContainerStart(containerID string, hostConfig *container.H |
| 54 | 54 |
return nil |
| 55 | 55 |
} |
| 56 | 56 |
|
| 57 |
-func (m *MockBackend) ContainerWait(containerID string, timeout time.Duration) (int, error) {
|
|
| 58 |
- return 0, nil |
|
| 57 |
+func (m *MockBackend) ContainerWait(ctx context.Context, containerID string, condition containerpkg.WaitCondition) (<-chan containerpkg.StateStatus, error) {
|
|
| 58 |
+ return nil, nil |
|
| 59 | 59 |
} |
| 60 | 60 |
|
| 61 | 61 |
func (m *MockBackend) ContainerCreateWorkdir(containerID string) error {
|
| ... | ... |
@@ -2,25 +2,83 @@ package client |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"encoding/json" |
| 5 |
+ "net/url" |
|
| 5 | 6 |
|
| 6 | 7 |
"golang.org/x/net/context" |
| 7 | 8 |
|
| 8 | 9 |
"github.com/docker/docker/api/types/container" |
| 10 |
+ "github.com/docker/docker/api/types/versions" |
|
| 9 | 11 |
) |
| 10 | 12 |
|
| 11 |
-// ContainerWait pauses execution until a container exits. |
|
| 12 |
-// It returns the API status code as response of its readiness. |
|
| 13 |
-func (cli *Client) ContainerWait(ctx context.Context, containerID string) (int64, error) {
|
|
| 14 |
- resp, err := cli.post(ctx, "/containers/"+containerID+"/wait", nil, nil, nil) |
|
| 15 |
- if err != nil {
|
|
| 16 |
- return -1, err |
|
| 13 |
+// ContainerWait waits until the specified continer is in a certain state |
|
| 14 |
+// indicated by the given condition, either "not-running" (default), |
|
| 15 |
+// "next-exit", or "removed". |
|
| 16 |
+// |
|
| 17 |
+// If this client's API version is beforer 1.30, condition is ignored and |
|
| 18 |
+// ContainerWait will return immediately with the two channels, as the server |
|
| 19 |
+// will wait as if the condition were "not-running". |
|
| 20 |
+// |
|
| 21 |
+// If this client's API version is at least 1.30, ContainerWait blocks until |
|
| 22 |
+// the request has been acknowledged by the server (with a response header), |
|
| 23 |
+// then returns two channels on which the caller can wait for the exit status |
|
| 24 |
+// of the container or an error if there was a problem either beginning the |
|
| 25 |
+// wait request or in getting the response. This allows the caller to |
|
| 26 |
+// sychronize ContainerWait with other calls, such as specifying a |
|
| 27 |
+// "next-exit" condition before issuing a ContainerStart request. |
|
| 28 |
+func (cli *Client) ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.ContainerWaitOKBody, <-chan error) {
|
|
| 29 |
+ if versions.LessThan(cli.ClientVersion(), "1.30") {
|
|
| 30 |
+ return cli.legacyContainerWait(ctx, containerID) |
|
| 17 | 31 |
} |
| 18 |
- defer ensureReaderClosed(resp) |
|
| 19 | 32 |
|
| 20 |
- var res container.ContainerWaitOKBody |
|
| 21 |
- if err := json.NewDecoder(resp.body).Decode(&res); err != nil {
|
|
| 22 |
- return -1, err |
|
| 33 |
+ resultC := make(chan container.ContainerWaitOKBody) |
|
| 34 |
+ errC := make(chan error) |
|
| 35 |
+ |
|
| 36 |
+ query := url.Values{}
|
|
| 37 |
+ query.Set("condition", string(condition))
|
|
| 38 |
+ |
|
| 39 |
+ resp, err := cli.post(ctx, "/containers/"+containerID+"/wait", query, nil, nil) |
|
| 40 |
+ if err != nil {
|
|
| 41 |
+ defer ensureReaderClosed(resp) |
|
| 42 |
+ errC <- err |
|
| 43 |
+ return resultC, errC |
|
| 23 | 44 |
} |
| 24 | 45 |
|
| 25 |
- return res.StatusCode, nil |
|
| 46 |
+ go func() {
|
|
| 47 |
+ defer ensureReaderClosed(resp) |
|
| 48 |
+ var res container.ContainerWaitOKBody |
|
| 49 |
+ if err := json.NewDecoder(resp.body).Decode(&res); err != nil {
|
|
| 50 |
+ errC <- err |
|
| 51 |
+ return |
|
| 52 |
+ } |
|
| 53 |
+ |
|
| 54 |
+ resultC <- res |
|
| 55 |
+ }() |
|
| 56 |
+ |
|
| 57 |
+ return resultC, errC |
|
| 58 |
+} |
|
| 59 |
+ |
|
| 60 |
+// legacyContainerWait returns immediately and doesn't have an option to wait |
|
| 61 |
+// until the container is removed. |
|
| 62 |
+func (cli *Client) legacyContainerWait(ctx context.Context, containerID string) (<-chan container.ContainerWaitOKBody, <-chan error) {
|
|
| 63 |
+ resultC := make(chan container.ContainerWaitOKBody) |
|
| 64 |
+ errC := make(chan error) |
|
| 65 |
+ |
|
| 66 |
+ go func() {
|
|
| 67 |
+ resp, err := cli.post(ctx, "/containers/"+containerID+"/wait", nil, nil, nil) |
|
| 68 |
+ if err != nil {
|
|
| 69 |
+ errC <- err |
|
| 70 |
+ return |
|
| 71 |
+ } |
|
| 72 |
+ defer ensureReaderClosed(resp) |
|
| 73 |
+ |
|
| 74 |
+ var res container.ContainerWaitOKBody |
|
| 75 |
+ if err := json.NewDecoder(resp.body).Decode(&res); err != nil {
|
|
| 76 |
+ errC <- err |
|
| 77 |
+ return |
|
| 78 |
+ } |
|
| 79 |
+ |
|
| 80 |
+ resultC <- res |
|
| 81 |
+ }() |
|
| 82 |
+ |
|
| 83 |
+ return resultC, errC |
|
| 26 | 84 |
} |
| ... | ... |
@@ -20,12 +20,14 @@ func TestContainerWaitError(t *testing.T) {
|
| 20 | 20 |
client := &Client{
|
| 21 | 21 |
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), |
| 22 | 22 |
} |
| 23 |
- code, err := client.ContainerWait(context.Background(), "nothing") |
|
| 24 |
- if err == nil || err.Error() != "Error response from daemon: Server error" {
|
|
| 25 |
- t.Fatalf("expected a Server Error, got %v", err)
|
|
| 26 |
- } |
|
| 27 |
- if code != -1 {
|
|
| 28 |
- t.Fatalf("expected a status code equal to '-1', got %d", code)
|
|
| 23 |
+ resultC, errC := client.ContainerWait(context.Background(), "nothing", "") |
|
| 24 |
+ select {
|
|
| 25 |
+ case result := <-resultC: |
|
| 26 |
+ t.Fatalf("expected to not get a wait result, got %d", result.StatusCode)
|
|
| 27 |
+ case err := <-errC: |
|
| 28 |
+ if err.Error() != "Error response from daemon: Server error" {
|
|
| 29 |
+ t.Fatalf("expected a Server Error, got %v", err)
|
|
| 30 |
+ } |
|
| 29 | 31 |
} |
| 30 | 32 |
} |
| 31 | 33 |
|
| ... | ... |
@@ -49,12 +51,14 @@ func TestContainerWait(t *testing.T) {
|
| 49 | 49 |
}), |
| 50 | 50 |
} |
| 51 | 51 |
|
| 52 |
- code, err := client.ContainerWait(context.Background(), "container_id") |
|
| 53 |
- if err != nil {
|
|
| 52 |
+ resultC, errC := client.ContainerWait(context.Background(), "container_id", "") |
|
| 53 |
+ select {
|
|
| 54 |
+ case err := <-errC: |
|
| 54 | 55 |
t.Fatal(err) |
| 55 |
- } |
|
| 56 |
- if code != 15 {
|
|
| 57 |
- t.Fatalf("expected a status code equal to '15', got %d", code)
|
|
| 56 |
+ case result := <-resultC: |
|
| 57 |
+ if result.StatusCode != 15 {
|
|
| 58 |
+ t.Fatalf("expected a status code equal to '15', got %d", result.StatusCode)
|
|
| 59 |
+ } |
|
| 58 | 60 |
} |
| 59 | 61 |
} |
| 60 | 62 |
|
| ... | ... |
@@ -63,8 +67,8 @@ func ExampleClient_ContainerWait_withTimeout() {
|
| 63 | 63 |
defer cancel() |
| 64 | 64 |
|
| 65 | 65 |
client, _ := NewEnvClient() |
| 66 |
- _, err := client.ContainerWait(ctx, "container_id") |
|
| 67 |
- if err != nil {
|
|
| 66 |
+ _, errC := client.ContainerWait(ctx, "container_id", "") |
|
| 67 |
+ if err := <-errC; err != nil {
|
|
| 68 | 68 |
log.Fatal(err) |
| 69 | 69 |
} |
| 70 | 70 |
} |
| ... | ... |
@@ -64,7 +64,7 @@ type ContainerAPIClient interface {
|
| 64 | 64 |
ContainerTop(ctx context.Context, container string, arguments []string) (container.ContainerTopOKBody, error) |
| 65 | 65 |
ContainerUnpause(ctx context.Context, container string) error |
| 66 | 66 |
ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) (container.ContainerUpdateOKBody, error) |
| 67 |
- ContainerWait(ctx context.Context, container string) (int64, error) |
|
| 67 |
+ ContainerWait(ctx context.Context, container string, condition container.WaitCondition) (<-chan container.ContainerWaitOKBody, <-chan error) |
|
| 68 | 68 |
CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) |
| 69 | 69 |
CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error |
| 70 | 70 |
ContainersPrune(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error) |
| ... | ... |
@@ -1,6 +1,7 @@ |
| 1 | 1 |
package container |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "errors" |
|
| 4 | 5 |
"fmt" |
| 5 | 6 |
"sync" |
| 6 | 7 |
"time" |
| ... | ... |
@@ -29,40 +30,37 @@ type State struct {
|
| 29 | 29 |
ErrorMsg string `json:"Error"` // contains last known error when starting the container |
| 30 | 30 |
StartedAt time.Time |
| 31 | 31 |
FinishedAt time.Time |
| 32 |
- waitChan chan struct{}
|
|
| 33 | 32 |
Health *Health |
| 33 |
+ |
|
| 34 |
+ waitStop chan struct{}
|
|
| 35 |
+ waitRemove chan struct{}
|
|
| 34 | 36 |
} |
| 35 | 37 |
|
| 36 |
-// StateStatus is used to return an error type implementing both |
|
| 37 |
-// exec.ExitCode and error. |
|
| 38 |
+// StateStatus is used to return container wait results. |
|
| 39 |
+// Implements exec.ExitCode interface. |
|
| 38 | 40 |
// This type is needed as State include a sync.Mutex field which make |
| 39 | 41 |
// copying it unsafe. |
| 40 | 42 |
type StateStatus struct {
|
| 41 | 43 |
exitCode int |
| 42 |
- error string |
|
| 43 |
-} |
|
| 44 |
- |
|
| 45 |
-func newStateStatus(ec int, err string) *StateStatus {
|
|
| 46 |
- return &StateStatus{
|
|
| 47 |
- exitCode: ec, |
|
| 48 |
- error: err, |
|
| 49 |
- } |
|
| 44 |
+ err error |
|
| 50 | 45 |
} |
| 51 | 46 |
|
| 52 | 47 |
// ExitCode returns current exitcode for the state. |
| 53 |
-func (ss *StateStatus) ExitCode() int {
|
|
| 54 |
- return ss.exitCode |
|
| 48 |
+func (s StateStatus) ExitCode() int {
|
|
| 49 |
+ return s.exitCode |
|
| 55 | 50 |
} |
| 56 | 51 |
|
| 57 |
-// Error returns current error for the state. |
|
| 58 |
-func (ss *StateStatus) Error() string {
|
|
| 59 |
- return ss.error |
|
| 52 |
+// Err returns current error for the state. Returns nil if the container had |
|
| 53 |
+// exited on its own. |
|
| 54 |
+func (s StateStatus) Err() error {
|
|
| 55 |
+ return s.err |
|
| 60 | 56 |
} |
| 61 | 57 |
|
| 62 | 58 |
// NewState creates a default state object with a fresh channel for state changes. |
| 63 | 59 |
func NewState() *State {
|
| 64 | 60 |
return &State{
|
| 65 |
- waitChan: make(chan struct{}),
|
|
| 61 |
+ waitStop: make(chan struct{}),
|
|
| 62 |
+ waitRemove: make(chan struct{}),
|
|
| 66 | 63 |
} |
| 67 | 64 |
} |
| 68 | 65 |
|
| ... | ... |
@@ -160,64 +158,89 @@ func IsValidStateString(s string) bool {
|
| 160 | 160 |
return true |
| 161 | 161 |
} |
| 162 | 162 |
|
| 163 |
-func wait(waitChan <-chan struct{}, timeout time.Duration) error {
|
|
| 164 |
- if timeout < 0 {
|
|
| 165 |
- <-waitChan |
|
| 166 |
- return nil |
|
| 167 |
- } |
|
| 168 |
- select {
|
|
| 169 |
- case <-time.After(timeout): |
|
| 170 |
- return fmt.Errorf("Timed out: %v", timeout)
|
|
| 171 |
- case <-waitChan: |
|
| 172 |
- return nil |
|
| 173 |
- } |
|
| 174 |
-} |
|
| 163 |
+// WaitCondition is an enum type for different states to wait for. |
|
| 164 |
+type WaitCondition int |
|
| 165 |
+ |
|
| 166 |
+// Possible WaitCondition Values. |
|
| 167 |
+// |
|
| 168 |
+// WaitConditionNotRunning (default) is used to wait for any of the non-running |
|
| 169 |
+// states: "created", "exited", "dead", "removing", or "removed". |
|
| 170 |
+// |
|
| 171 |
+// WaitConditionNextExit is used to wait for the next time the state changes |
|
| 172 |
+// to a non-running state. If the state is currently "created" or "exited", |
|
| 173 |
+// this would cause Wait() to block until either the container runs and exits |
|
| 174 |
+// or is removed. |
|
| 175 |
+// |
|
| 176 |
+// WaitConditionRemoved is used to wait for the container to be removed. |
|
| 177 |
+const ( |
|
| 178 |
+ WaitConditionNotRunning WaitCondition = iota |
|
| 179 |
+ WaitConditionNextExit |
|
| 180 |
+ WaitConditionRemoved |
|
| 181 |
+) |
|
| 175 | 182 |
|
| 176 |
-// WaitStop waits until state is stopped. If state already stopped it returns |
|
| 177 |
-// immediately. If you want wait forever you must supply negative timeout. |
|
| 178 |
-// Returns exit code, that was passed to SetStopped |
|
| 179 |
-func (s *State) WaitStop(timeout time.Duration) (int, error) {
|
|
| 180 |
- ctx := context.Background() |
|
| 181 |
- if timeout >= 0 {
|
|
| 182 |
- var cancel func() |
|
| 183 |
- ctx, cancel = context.WithTimeout(ctx, timeout) |
|
| 184 |
- defer cancel() |
|
| 185 |
- } |
|
| 186 |
- if err := s.WaitWithContext(ctx); err != nil {
|
|
| 187 |
- if status, ok := err.(*StateStatus); ok {
|
|
| 188 |
- return status.ExitCode(), nil |
|
| 183 |
+// Wait waits until the continer is in a certain state indicated by the given |
|
| 184 |
+// condition. A context must be used for cancelling the request, controlling |
|
| 185 |
+// timeouts, and avoiding goroutine leaks. Wait must be called without holding |
|
| 186 |
+// the state lock. Returns a channel from which the caller will receive the |
|
| 187 |
+// result. If the container exited on its own, the result's Err() method will |
|
| 188 |
+// be nil and its ExitCode() method will return the conatiners exit code, |
|
| 189 |
+// otherwise, the results Err() method will return an error indicating why the |
|
| 190 |
+// wait operation failed. |
|
| 191 |
+func (s *State) Wait(ctx context.Context, condition WaitCondition) <-chan StateStatus {
|
|
| 192 |
+ s.Lock() |
|
| 193 |
+ defer s.Unlock() |
|
| 194 |
+ |
|
| 195 |
+ if condition == WaitConditionNotRunning && !s.Running {
|
|
| 196 |
+ // Buffer so we can put it in the channel now. |
|
| 197 |
+ resultC := make(chan StateStatus, 1) |
|
| 198 |
+ |
|
| 199 |
+ // Send the current status. |
|
| 200 |
+ resultC <- StateStatus{
|
|
| 201 |
+ exitCode: s.ExitCode(), |
|
| 202 |
+ err: s.Err(), |
|
| 189 | 203 |
} |
| 190 |
- return -1, err |
|
| 204 |
+ |
|
| 205 |
+ return resultC |
|
| 191 | 206 |
} |
| 192 |
- return 0, nil |
|
| 193 |
-} |
|
| 194 | 207 |
|
| 195 |
-// WaitWithContext waits for the container to stop. Optional context can be |
|
| 196 |
-// passed for canceling the request. |
|
| 197 |
-func (s *State) WaitWithContext(ctx context.Context) error {
|
|
| 198 |
- s.Lock() |
|
| 199 |
- if !s.Running {
|
|
| 200 |
- state := newStateStatus(s.ExitCode(), s.Error()) |
|
| 201 |
- defer s.Unlock() |
|
| 202 |
- if state.ExitCode() == 0 {
|
|
| 203 |
- return nil |
|
| 204 |
- } |
|
| 205 |
- return state |
|
| 208 |
+ // If we are waiting only for removal, the waitStop channel should |
|
| 209 |
+ // remain nil and block forever. |
|
| 210 |
+ var waitStop chan struct{}
|
|
| 211 |
+ if condition < WaitConditionRemoved {
|
|
| 212 |
+ waitStop = s.waitStop |
|
| 206 | 213 |
} |
| 207 |
- waitChan := s.waitChan |
|
| 208 |
- s.Unlock() |
|
| 209 |
- select {
|
|
| 210 |
- case <-waitChan: |
|
| 214 |
+ |
|
| 215 |
+ // Always wait for removal, just in case the container gets removed |
|
| 216 |
+ // while it is still in a "created" state, in which case it is never |
|
| 217 |
+ // actually stopped. |
|
| 218 |
+ waitRemove := s.waitRemove |
|
| 219 |
+ |
|
| 220 |
+ resultC := make(chan StateStatus) |
|
| 221 |
+ |
|
| 222 |
+ go func() {
|
|
| 223 |
+ select {
|
|
| 224 |
+ case <-ctx.Done(): |
|
| 225 |
+ // Context timeout or cancellation. |
|
| 226 |
+ resultC <- StateStatus{
|
|
| 227 |
+ exitCode: -1, |
|
| 228 |
+ err: ctx.Err(), |
|
| 229 |
+ } |
|
| 230 |
+ return |
|
| 231 |
+ case <-waitStop: |
|
| 232 |
+ case <-waitRemove: |
|
| 233 |
+ } |
|
| 234 |
+ |
|
| 211 | 235 |
s.Lock() |
| 212 |
- state := newStateStatus(s.ExitCode(), s.Error()) |
|
| 213 |
- s.Unlock() |
|
| 214 |
- if state.ExitCode() == 0 {
|
|
| 215 |
- return nil |
|
| 236 |
+ result := StateStatus{
|
|
| 237 |
+ exitCode: s.ExitCode(), |
|
| 238 |
+ err: s.Err(), |
|
| 216 | 239 |
} |
| 217 |
- return state |
|
| 218 |
- case <-ctx.Done(): |
|
| 219 |
- return ctx.Err() |
|
| 220 |
- } |
|
| 240 |
+ s.Unlock() |
|
| 241 |
+ |
|
| 242 |
+ resultC <- result |
|
| 243 |
+ }() |
|
| 244 |
+ |
|
| 245 |
+ return resultC |
|
| 221 | 246 |
} |
| 222 | 247 |
|
| 223 | 248 |
// IsRunning returns whether the running flag is set. Used by Container to check whether a container is running. |
| ... | ... |
@@ -268,8 +291,8 @@ func (s *State) SetStopped(exitStatus *ExitStatus) {
|
| 268 | 268 |
s.Pid = 0 |
| 269 | 269 |
s.FinishedAt = time.Now().UTC() |
| 270 | 270 |
s.setFromExitStatus(exitStatus) |
| 271 |
- close(s.waitChan) // fire waiters for stop |
|
| 272 |
- s.waitChan = make(chan struct{})
|
|
| 271 |
+ close(s.waitStop) // Fire waiters for stop |
|
| 272 |
+ s.waitStop = make(chan struct{})
|
|
| 273 | 273 |
} |
| 274 | 274 |
|
| 275 | 275 |
// SetRestarting sets the container state to "restarting" without locking. |
| ... | ... |
@@ -282,8 +305,8 @@ func (s *State) SetRestarting(exitStatus *ExitStatus) {
|
| 282 | 282 |
s.Pid = 0 |
| 283 | 283 |
s.FinishedAt = time.Now().UTC() |
| 284 | 284 |
s.setFromExitStatus(exitStatus) |
| 285 |
- close(s.waitChan) // fire waiters for stop |
|
| 286 |
- s.waitChan = make(chan struct{})
|
|
| 285 |
+ close(s.waitStop) // Fire waiters for stop |
|
| 286 |
+ s.waitStop = make(chan struct{})
|
|
| 287 | 287 |
} |
| 288 | 288 |
|
| 289 | 289 |
// SetError sets the container's error state. This is useful when we want to |
| ... | ... |
@@ -335,7 +358,19 @@ func (s *State) SetDead() {
|
| 335 | 335 |
s.Unlock() |
| 336 | 336 |
} |
| 337 | 337 |
|
| 338 |
-// Error returns current error for the state. |
|
| 339 |
-func (s *State) Error() string {
|
|
| 340 |
- return s.ErrorMsg |
|
| 338 |
+// SetRemoved assumes this container is already in the "dead" state and |
|
| 339 |
+// closes the internal waitRemove channel to unblock callers waiting for a |
|
| 340 |
+// container to be removed. |
|
| 341 |
+func (s *State) SetRemoved() {
|
|
| 342 |
+ s.Lock() |
|
| 343 |
+ close(s.waitRemove) // Unblock those waiting on remove. |
|
| 344 |
+ s.Unlock() |
|
| 345 |
+} |
|
| 346 |
+ |
|
| 347 |
+// Err returns an error if there is one. |
|
| 348 |
+func (s *State) Err() error {
|
|
| 349 |
+ if s.ErrorMsg != "" {
|
|
| 350 |
+ return errors.New(s.ErrorMsg) |
|
| 351 |
+ } |
|
| 352 |
+ return nil |
|
| 341 | 353 |
} |
| ... | ... |
@@ -1,7 +1,7 @@ |
| 1 | 1 |
package container |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "sync/atomic" |
|
| 4 |
+ "context" |
|
| 5 | 5 |
"testing" |
| 6 | 6 |
"time" |
| 7 | 7 |
|
| ... | ... |
@@ -30,31 +30,63 @@ func TestIsValidHealthString(t *testing.T) {
|
| 30 | 30 |
|
| 31 | 31 |
func TestStateRunStop(t *testing.T) {
|
| 32 | 32 |
s := NewState() |
| 33 |
- for i := 1; i < 3; i++ { // full lifecycle two times
|
|
| 33 |
+ |
|
| 34 |
+ // Begin another wait with WaitConditionRemoved. It should complete |
|
| 35 |
+ // within 200 milliseconds. |
|
| 36 |
+ ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) |
|
| 37 |
+ defer cancel() |
|
| 38 |
+ removalWait := s.Wait(ctx, WaitConditionRemoved) |
|
| 39 |
+ |
|
| 40 |
+ // Full lifecycle two times. |
|
| 41 |
+ for i := 1; i <= 2; i++ {
|
|
| 42 |
+ // A wait with WaitConditionNotRunning should return |
|
| 43 |
+ // immediately since the state is now either "created" (on the |
|
| 44 |
+ // first iteration) or "exited" (on the second iteration). It |
|
| 45 |
+ // shouldn't take more than 50 milliseconds. |
|
| 46 |
+ ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) |
|
| 47 |
+ defer cancel() |
|
| 48 |
+ // Expectx exit code to be i-1 since it should be the exit |
|
| 49 |
+ // code from the previous loop or 0 for the created state. |
|
| 50 |
+ if status := <-s.Wait(ctx, WaitConditionNotRunning); status.ExitCode() != i-1 {
|
|
| 51 |
+ t.Fatalf("ExitCode %v, expected %v, err %q", status.ExitCode(), i-1, status.Err())
|
|
| 52 |
+ } |
|
| 53 |
+ |
|
| 54 |
+ // A wait with WaitConditionNextExit should block until the |
|
| 55 |
+ // container has started and exited. It shouldn't take more |
|
| 56 |
+ // than 100 milliseconds. |
|
| 57 |
+ ctx, cancel = context.WithTimeout(context.Background(), 100*time.Millisecond) |
|
| 58 |
+ defer cancel() |
|
| 59 |
+ initialWait := s.Wait(ctx, WaitConditionNextExit) |
|
| 60 |
+ |
|
| 61 |
+ // Set the state to "Running". |
|
| 34 | 62 |
s.Lock() |
| 35 |
- s.SetRunning(i+100, false) |
|
| 63 |
+ s.SetRunning(i, true) |
|
| 36 | 64 |
s.Unlock() |
| 37 | 65 |
|
| 66 |
+ // Assert desired state. |
|
| 38 | 67 |
if !s.IsRunning() {
|
| 39 | 68 |
t.Fatal("State not running")
|
| 40 | 69 |
} |
| 41 |
- if s.Pid != i+100 {
|
|
| 42 |
- t.Fatalf("Pid %v, expected %v", s.Pid, i+100)
|
|
| 70 |
+ if s.Pid != i {
|
|
| 71 |
+ t.Fatalf("Pid %v, expected %v", s.Pid, i)
|
|
| 43 | 72 |
} |
| 44 | 73 |
if s.ExitCode() != 0 {
|
| 45 | 74 |
t.Fatalf("ExitCode %v, expected 0", s.ExitCode())
|
| 46 | 75 |
} |
| 47 | 76 |
|
| 48 |
- stopped := make(chan struct{})
|
|
| 49 |
- var exit int64 |
|
| 50 |
- go func() {
|
|
| 51 |
- exitCode, _ := s.WaitStop(-1 * time.Second) |
|
| 52 |
- atomic.StoreInt64(&exit, int64(exitCode)) |
|
| 53 |
- close(stopped) |
|
| 54 |
- }() |
|
| 77 |
+ // Now that it's running, a wait with WaitConditionNotRunning |
|
| 78 |
+ // should block until we stop the container. It shouldn't take |
|
| 79 |
+ // more than 100 milliseconds. |
|
| 80 |
+ ctx, cancel = context.WithTimeout(context.Background(), 100*time.Millisecond) |
|
| 81 |
+ defer cancel() |
|
| 82 |
+ exitWait := s.Wait(ctx, WaitConditionNotRunning) |
|
| 83 |
+ |
|
| 84 |
+ // Set the state to "Exited". |
|
| 55 | 85 |
s.Lock() |
| 56 | 86 |
s.SetStopped(&ExitStatus{ExitCode: i})
|
| 57 | 87 |
s.Unlock() |
| 88 |
+ |
|
| 89 |
+ // Assert desired state. |
|
| 58 | 90 |
if s.IsRunning() {
|
| 59 | 91 |
t.Fatal("State is running")
|
| 60 | 92 |
} |
| ... | ... |
@@ -64,50 +96,73 @@ func TestStateRunStop(t *testing.T) {
|
| 64 | 64 |
if s.Pid != 0 {
|
| 65 | 65 |
t.Fatalf("Pid %v, expected 0", s.Pid)
|
| 66 | 66 |
} |
| 67 |
- select {
|
|
| 68 |
- case <-time.After(100 * time.Millisecond): |
|
| 69 |
- t.Fatal("Stop callback doesn't fire in 100 milliseconds")
|
|
| 70 |
- case <-stopped: |
|
| 71 |
- t.Log("Stop callback fired")
|
|
| 72 |
- } |
|
| 73 |
- exitCode := int(atomic.LoadInt64(&exit)) |
|
| 74 |
- if exitCode != i {
|
|
| 75 |
- t.Fatalf("ExitCode %v, expected %v", exitCode, i)
|
|
| 67 |
+ |
|
| 68 |
+ // Receive the initialWait result. |
|
| 69 |
+ if status := <-initialWait; status.ExitCode() != i {
|
|
| 70 |
+ t.Fatalf("ExitCode %v, expected %v, err %q", status.ExitCode(), i, status.Err())
|
|
| 76 | 71 |
} |
| 77 |
- if exitCode, err := s.WaitStop(-1 * time.Second); err != nil || exitCode != i {
|
|
| 78 |
- t.Fatalf("WaitStop returned exitCode: %v, err: %v, expected exitCode: %v, err: %v", exitCode, err, i, nil)
|
|
| 72 |
+ |
|
| 73 |
+ // Receive the exitWait result. |
|
| 74 |
+ if status := <-exitWait; status.ExitCode() != i {
|
|
| 75 |
+ t.Fatalf("ExitCode %v, expected %v, err %q", status.ExitCode(), i, status.Err())
|
|
| 79 | 76 |
} |
| 80 | 77 |
} |
| 78 |
+ |
|
| 79 |
+ // Set the state to dead and removed. |
|
| 80 |
+ s.SetDead() |
|
| 81 |
+ s.SetRemoved() |
|
| 82 |
+ |
|
| 83 |
+ // Wait for removed status or timeout. |
|
| 84 |
+ if status := <-removalWait; status.ExitCode() != 2 {
|
|
| 85 |
+ // Should have the final exit code from the loop. |
|
| 86 |
+ t.Fatalf("Removal wait exitCode %v, expected %v, err %q", status.ExitCode(), 2, status.Err())
|
|
| 87 |
+ } |
|
| 81 | 88 |
} |
| 82 | 89 |
|
| 83 | 90 |
func TestStateTimeoutWait(t *testing.T) {
|
| 84 | 91 |
s := NewState() |
| 85 |
- stopped := make(chan struct{})
|
|
| 86 |
- go func() {
|
|
| 87 |
- s.WaitStop(100 * time.Millisecond) |
|
| 88 |
- close(stopped) |
|
| 89 |
- }() |
|
| 92 |
+ |
|
| 93 |
+ s.Lock() |
|
| 94 |
+ s.SetRunning(0, true) |
|
| 95 |
+ s.Unlock() |
|
| 96 |
+ |
|
| 97 |
+ // Start a wait with a timeout. |
|
| 98 |
+ ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) |
|
| 99 |
+ defer cancel() |
|
| 100 |
+ waitC := s.Wait(ctx, WaitConditionNotRunning) |
|
| 101 |
+ |
|
| 102 |
+ // It should timeout *before* this 200ms timer does. |
|
| 90 | 103 |
select {
|
| 91 | 104 |
case <-time.After(200 * time.Millisecond): |
| 92 | 105 |
t.Fatal("Stop callback doesn't fire in 200 milliseconds")
|
| 93 |
- case <-stopped: |
|
| 106 |
+ case status := <-waitC: |
|
| 94 | 107 |
t.Log("Stop callback fired")
|
| 108 |
+ // Should be a timeout error. |
|
| 109 |
+ if status.Err() == nil {
|
|
| 110 |
+ t.Fatal("expected timeout error, got nil")
|
|
| 111 |
+ } |
|
| 112 |
+ if status.ExitCode() != -1 {
|
|
| 113 |
+ t.Fatalf("expected exit code %v, got %v", -1, status.ExitCode())
|
|
| 114 |
+ } |
|
| 95 | 115 |
} |
| 96 | 116 |
|
| 97 | 117 |
s.Lock() |
| 98 |
- s.SetStopped(&ExitStatus{ExitCode: 1})
|
|
| 118 |
+ s.SetStopped(&ExitStatus{ExitCode: 0})
|
|
| 99 | 119 |
s.Unlock() |
| 100 | 120 |
|
| 101 |
- stopped = make(chan struct{})
|
|
| 102 |
- go func() {
|
|
| 103 |
- s.WaitStop(100 * time.Millisecond) |
|
| 104 |
- close(stopped) |
|
| 105 |
- }() |
|
| 121 |
+ // Start another wait with a timeout. This one should return |
|
| 122 |
+ // immediately. |
|
| 123 |
+ ctx, cancel = context.WithTimeout(context.Background(), 100*time.Millisecond) |
|
| 124 |
+ defer cancel() |
|
| 125 |
+ waitC = s.Wait(ctx, WaitConditionNotRunning) |
|
| 126 |
+ |
|
| 106 | 127 |
select {
|
| 107 | 128 |
case <-time.After(200 * time.Millisecond): |
| 108 | 129 |
t.Fatal("Stop callback doesn't fire in 200 milliseconds")
|
| 109 |
- case <-stopped: |
|
| 130 |
+ case status := <-waitC: |
|
| 110 | 131 |
t.Log("Stop callback fired")
|
| 132 |
+ if status.ExitCode() != 0 {
|
|
| 133 |
+ t.Fatalf("expected exit code %v, got %v, err %q", 0, status.ExitCode(), status.Err())
|
|
| 134 |
+ } |
|
| 111 | 135 |
} |
| 112 |
- |
|
| 113 | 136 |
} |
| ... | ... |
@@ -8,17 +8,11 @@ import ( |
| 8 | 8 |
|
| 9 | 9 |
"github.com/Sirupsen/logrus" |
| 10 | 10 |
"github.com/docker/docker/pkg/promise" |
| 11 |
+ "github.com/docker/docker/pkg/term" |
|
| 11 | 12 |
) |
| 12 | 13 |
|
| 13 | 14 |
var defaultEscapeSequence = []byte{16, 17} // ctrl-p, ctrl-q
|
| 14 | 15 |
|
| 15 |
-// DetachError is special error which returned in case of container detach. |
|
| 16 |
-type DetachError struct{}
|
|
| 17 |
- |
|
| 18 |
-func (DetachError) Error() string {
|
|
| 19 |
- return "detached from container" |
|
| 20 |
-} |
|
| 21 |
- |
|
| 22 | 16 |
// AttachConfig is the config struct used to attach a client to a stream's stdio |
| 23 | 17 |
type AttachConfig struct {
|
| 24 | 18 |
// Tells the attach copier that the stream's stdin is a TTY and to look for |
| ... | ... |
@@ -173,62 +167,11 @@ func (c *Config) CopyStreams(ctx context.Context, cfg *AttachConfig) chan error |
| 173 | 173 |
}) |
| 174 | 174 |
} |
| 175 | 175 |
|
| 176 |
-// ttyProxy is used only for attaches with a TTY. It is used to proxy |
|
| 177 |
-// stdin keypresses from the underlying reader and look for the passed in |
|
| 178 |
-// escape key sequence to signal a detach. |
|
| 179 |
-type ttyProxy struct {
|
|
| 180 |
- escapeKeys []byte |
|
| 181 |
- escapeKeyPos int |
|
| 182 |
- r io.Reader |
|
| 183 |
-} |
|
| 184 |
- |
|
| 185 |
-func (r *ttyProxy) Read(buf []byte) (int, error) {
|
|
| 186 |
- nr, err := r.r.Read(buf) |
|
| 187 |
- |
|
| 188 |
- preserve := func() {
|
|
| 189 |
- // this preserves the original key presses in the passed in buffer |
|
| 190 |
- nr += r.escapeKeyPos |
|
| 191 |
- preserve := make([]byte, 0, r.escapeKeyPos+len(buf)) |
|
| 192 |
- preserve = append(preserve, r.escapeKeys[:r.escapeKeyPos]...) |
|
| 193 |
- preserve = append(preserve, buf...) |
|
| 194 |
- r.escapeKeyPos = 0 |
|
| 195 |
- copy(buf[0:nr], preserve) |
|
| 196 |
- } |
|
| 197 |
- |
|
| 198 |
- if nr != 1 || err != nil {
|
|
| 199 |
- if r.escapeKeyPos > 0 {
|
|
| 200 |
- preserve() |
|
| 201 |
- } |
|
| 202 |
- return nr, err |
|
| 203 |
- } |
|
| 204 |
- |
|
| 205 |
- if buf[0] != r.escapeKeys[r.escapeKeyPos] {
|
|
| 206 |
- if r.escapeKeyPos > 0 {
|
|
| 207 |
- preserve() |
|
| 208 |
- } |
|
| 209 |
- return nr, nil |
|
| 210 |
- } |
|
| 211 |
- |
|
| 212 |
- if r.escapeKeyPos == len(r.escapeKeys)-1 {
|
|
| 213 |
- return 0, DetachError{}
|
|
| 214 |
- } |
|
| 215 |
- |
|
| 216 |
- // Looks like we've got an escape key, but we need to match again on the next |
|
| 217 |
- // read. |
|
| 218 |
- // Store the current escape key we found so we can look for the next one on |
|
| 219 |
- // the next read. |
|
| 220 |
- // Since this is an escape key, make sure we don't let the caller read it |
|
| 221 |
- // If later on we find that this is not the escape sequence, we'll add the |
|
| 222 |
- // keys back |
|
| 223 |
- r.escapeKeyPos++ |
|
| 224 |
- return nr - r.escapeKeyPos, nil |
|
| 225 |
-} |
|
| 226 |
- |
|
| 227 | 176 |
func copyEscapable(dst io.Writer, src io.ReadCloser, keys []byte) (written int64, err error) {
|
| 228 | 177 |
if len(keys) == 0 {
|
| 229 | 178 |
keys = defaultEscapeSequence |
| 230 | 179 |
} |
| 231 |
- pr := &ttyProxy{escapeKeys: keys, r: src}
|
|
| 180 |
+ pr := term.NewEscapeProxy(src, keys) |
|
| 232 | 181 |
defer src.Close() |
| 233 | 182 |
|
| 234 | 183 |
return io.Copy(dst, pr) |
| ... | ... |
@@ -1,9 +1,9 @@ |
| 1 | 1 |
package daemon |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "context" |
|
| 4 | 5 |
"fmt" |
| 5 | 6 |
"io" |
| 6 |
- "time" |
|
| 7 | 7 |
|
| 8 | 8 |
"github.com/Sirupsen/logrus" |
| 9 | 9 |
"github.com/docker/docker/api/errors" |
| ... | ... |
@@ -160,21 +160,18 @@ func (daemon *Daemon) containerAttach(c *container.Container, cfg *stream.Attach |
| 160 | 160 |
cfg.Stdin = nil |
| 161 | 161 |
} |
| 162 | 162 |
|
| 163 |
- waitChan := make(chan struct{})
|
|
| 164 | 163 |
if c.Config.StdinOnce && !c.Config.Tty {
|
| 164 |
+ // Wait for the container to stop before returning. |
|
| 165 |
+ waitChan := c.Wait(context.Background(), container.WaitConditionNotRunning) |
|
| 165 | 166 |
defer func() {
|
| 166 |
- <-waitChan |
|
| 167 |
- }() |
|
| 168 |
- go func() {
|
|
| 169 |
- c.WaitStop(-1 * time.Second) |
|
| 170 |
- close(waitChan) |
|
| 167 |
+ _ = <-waitChan // Ignore returned exit code. |
|
| 171 | 168 |
}() |
| 172 | 169 |
} |
| 173 | 170 |
|
| 174 | 171 |
ctx := c.InitAttachContext() |
| 175 | 172 |
err := <-c.StreamConfig.CopyStreams(ctx, cfg) |
| 176 | 173 |
if err != nil {
|
| 177 |
- if _, ok := err.(stream.DetachError); ok {
|
|
| 174 |
+ if _, ok := err.(term.EscapeError); ok {
|
|
| 178 | 175 |
daemon.LogContainerEvent(c, "detach") |
| 179 | 176 |
} else {
|
| 180 | 177 |
logrus.Errorf("attach failed with error: %v", err)
|
| ... | ... |
@@ -13,6 +13,7 @@ import ( |
| 13 | 13 |
"github.com/docker/docker/api/types/filters" |
| 14 | 14 |
"github.com/docker/docker/api/types/network" |
| 15 | 15 |
swarmtypes "github.com/docker/docker/api/types/swarm" |
| 16 |
+ containerpkg "github.com/docker/docker/container" |
|
| 16 | 17 |
clustertypes "github.com/docker/docker/daemon/cluster/provider" |
| 17 | 18 |
"github.com/docker/docker/plugin" |
| 18 | 19 |
"github.com/docker/libnetwork" |
| ... | ... |
@@ -39,7 +40,7 @@ type Backend interface {
|
| 39 | 39 |
DeactivateContainerServiceBinding(containerName string) error |
| 40 | 40 |
UpdateContainerServiceConfig(containerName string, serviceConfig *clustertypes.ServiceConfig) error |
| 41 | 41 |
ContainerInspectCurrent(name string, size bool) (*types.ContainerJSON, error) |
| 42 |
- ContainerWaitWithContext(ctx context.Context, name string) error |
|
| 42 |
+ ContainerWait(ctx context.Context, name string, condition containerpkg.WaitCondition) (<-chan containerpkg.StateStatus, error) |
|
| 43 | 43 |
ContainerRm(name string, config *types.ContainerRmConfig) error |
| 44 | 44 |
ContainerKill(name string, sig uint64) error |
| 45 | 45 |
SetContainerDependencyStore(name string, store exec.DependencyGetter) error |
| ... | ... |
@@ -17,6 +17,7 @@ import ( |
| 17 | 17 |
"github.com/docker/docker/api/types/backend" |
| 18 | 18 |
containertypes "github.com/docker/docker/api/types/container" |
| 19 | 19 |
"github.com/docker/docker/api/types/events" |
| 20 |
+ containerpkg "github.com/docker/docker/container" |
|
| 20 | 21 |
"github.com/docker/docker/daemon/cluster/convert" |
| 21 | 22 |
executorpkg "github.com/docker/docker/daemon/cluster/executor" |
| 22 | 23 |
"github.com/docker/libnetwork" |
| ... | ... |
@@ -337,8 +338,8 @@ func (c *containerAdapter) events(ctx context.Context) <-chan events.Message {
|
| 337 | 337 |
return eventsq |
| 338 | 338 |
} |
| 339 | 339 |
|
| 340 |
-func (c *containerAdapter) wait(ctx context.Context) error {
|
|
| 341 |
- return c.backend.ContainerWaitWithContext(ctx, c.container.nameOrID()) |
|
| 340 |
+func (c *containerAdapter) wait(ctx context.Context) (<-chan containerpkg.StateStatus, error) {
|
|
| 341 |
+ return c.backend.ContainerWait(ctx, c.container.nameOrID(), containerpkg.WaitConditionNotRunning) |
|
| 342 | 342 |
} |
| 343 | 343 |
|
| 344 | 344 |
func (c *containerAdapter) shutdown(ctx context.Context) error {
|
| ... | ... |
@@ -279,25 +279,27 @@ func (r *controller) Wait(pctx context.Context) error {
|
| 279 | 279 |
} |
| 280 | 280 |
}() |
| 281 | 281 |
|
| 282 |
- err := r.adapter.wait(ctx) |
|
| 283 |
- if ctx.Err() != nil {
|
|
| 284 |
- return ctx.Err() |
|
| 282 |
+ waitC, err := r.adapter.wait(ctx) |
|
| 283 |
+ if err != nil {
|
|
| 284 |
+ return err |
|
| 285 | 285 |
} |
| 286 | 286 |
|
| 287 |
- if err != nil {
|
|
| 288 |
- ee := &exitError{}
|
|
| 289 |
- if ec, ok := err.(exec.ExitCoder); ok {
|
|
| 290 |
- ee.code = ec.ExitCode() |
|
| 287 |
+ if status := <-waitC; status.ExitCode() != 0 {
|
|
| 288 |
+ exitErr := &exitError{
|
|
| 289 |
+ code: status.ExitCode(), |
|
| 291 | 290 |
} |
| 291 |
+ |
|
| 292 |
+ // Set the cause if it is knowable. |
|
| 292 | 293 |
select {
|
| 293 | 294 |
case e := <-healthErr: |
| 294 |
- ee.cause = e |
|
| 295 |
+ exitErr.cause = e |
|
| 295 | 296 |
default: |
| 296 |
- if err.Error() != "" {
|
|
| 297 |
- ee.cause = err |
|
| 297 |
+ if status.Err() != nil {
|
|
| 298 |
+ exitErr.cause = status.Err() |
|
| 298 | 299 |
} |
| 299 | 300 |
} |
| 300 |
- return ee |
|
| 301 |
+ |
|
| 302 |
+ return exitErr |
|
| 301 | 303 |
} |
| 302 | 304 |
|
| 303 | 305 |
return nil |
| ... | ... |
@@ -3,6 +3,7 @@ |
| 3 | 3 |
package daemon |
| 4 | 4 |
|
| 5 | 5 |
import ( |
| 6 |
+ "context" |
|
| 6 | 7 |
"fmt" |
| 7 | 8 |
"io/ioutil" |
| 8 | 9 |
"os" |
| ... | ... |
@@ -290,11 +291,16 @@ func (daemon *Daemon) setupConfigDir(c *container.Container) (setupErr error) {
|
| 290 | 290 |
return nil |
| 291 | 291 |
} |
| 292 | 292 |
|
| 293 |
-func killProcessDirectly(container *container.Container) error {
|
|
| 294 |
- if _, err := container.WaitStop(10 * time.Second); err != nil {
|
|
| 293 |
+func killProcessDirectly(cntr *container.Container) error {
|
|
| 294 |
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) |
|
| 295 |
+ defer cancel() |
|
| 296 |
+ |
|
| 297 |
+ // Block until the container to stops or timeout. |
|
| 298 |
+ status := <-cntr.Wait(ctx, container.WaitConditionNotRunning) |
|
| 299 |
+ if status.Err() != nil {
|
|
| 295 | 300 |
// Ensure that we don't kill ourselves |
| 296 |
- if pid := container.GetPID(); pid != 0 {
|
|
| 297 |
- logrus.Infof("Container %s failed to exit within 10 seconds of kill - trying direct SIGKILL", stringid.TruncateID(container.ID))
|
|
| 301 |
+ if pid := cntr.GetPID(); pid != 0 {
|
|
| 302 |
+ logrus.Infof("Container %s failed to exit within 10 seconds of kill - trying direct SIGKILL", stringid.TruncateID(cntr.ID))
|
|
| 298 | 303 |
if err := syscall.Kill(pid, 9); err != nil {
|
| 299 | 304 |
if err != syscall.ESRCH {
|
| 300 | 305 |
return err |
| ... | ... |
@@ -6,6 +6,7 @@ |
| 6 | 6 |
package daemon |
| 7 | 7 |
|
| 8 | 8 |
import ( |
| 9 |
+ "context" |
|
| 9 | 10 |
"fmt" |
| 10 | 11 |
"io/ioutil" |
| 11 | 12 |
"net" |
| ... | ... |
@@ -773,7 +774,12 @@ func (daemon *Daemon) shutdownContainer(c *container.Container) error {
|
| 773 | 773 |
if err := daemon.containerUnpause(c); err != nil {
|
| 774 | 774 |
return fmt.Errorf("Failed to unpause container %s with error: %v", c.ID, err)
|
| 775 | 775 |
} |
| 776 |
- if _, err := c.WaitStop(time.Duration(stopTimeout) * time.Second); err != nil {
|
|
| 776 |
+ |
|
| 777 |
+ ctx, cancel := context.WithTimeout(context.Background(), time.Duration(stopTimeout)*time.Second) |
|
| 778 |
+ defer cancel() |
|
| 779 |
+ |
|
| 780 |
+ // Wait with timeout for container to exit. |
|
| 781 |
+ if status := <-c.Wait(ctx, container.WaitConditionNotRunning); status.Err() != nil {
|
|
| 777 | 782 |
logrus.Debugf("container %s failed to exit in %d second of SIGTERM, sending SIGKILL to force", c.ID, stopTimeout)
|
| 778 | 783 |
sig, ok := signal.SignalMap["KILL"] |
| 779 | 784 |
if !ok {
|
| ... | ... |
@@ -782,8 +788,10 @@ func (daemon *Daemon) shutdownContainer(c *container.Container) error {
|
| 782 | 782 |
if err := daemon.kill(c, int(sig)); err != nil {
|
| 783 | 783 |
logrus.Errorf("Failed to SIGKILL container %s", c.ID)
|
| 784 | 784 |
} |
| 785 |
- c.WaitStop(-1 * time.Second) |
|
| 786 |
- return err |
|
| 785 |
+ // Wait for exit again without a timeout. |
|
| 786 |
+ // Explicitly ignore the result. |
|
| 787 |
+ _ = <-c.Wait(context.Background(), container.WaitConditionNotRunning) |
|
| 788 |
+ return status.Err() |
|
| 787 | 789 |
} |
| 788 | 790 |
} |
| 789 | 791 |
// If container failed to exit in stopTimeout seconds of SIGTERM, then using the force |
| ... | ... |
@@ -791,7 +799,9 @@ func (daemon *Daemon) shutdownContainer(c *container.Container) error {
|
| 791 | 791 |
return fmt.Errorf("Failed to stop container %s with error: %v", c.ID, err)
|
| 792 | 792 |
} |
| 793 | 793 |
|
| 794 |
- c.WaitStop(-1 * time.Second) |
|
| 794 |
+ // Wait without timeout for the container to exit. |
|
| 795 |
+ // Ignore the result. |
|
| 796 |
+ _ = <-c.Wait(context.Background(), container.WaitConditionNotRunning) |
|
| 795 | 797 |
return nil |
| 796 | 798 |
} |
| 797 | 799 |
|
| ... | ... |
@@ -134,6 +134,7 @@ func (daemon *Daemon) cleanupContainer(container *container.Container, forceRemo |
| 134 | 134 |
if e := daemon.removeMountPoints(container, removeVolume); e != nil {
|
| 135 | 135 |
logrus.Error(e) |
| 136 | 136 |
} |
| 137 |
+ container.SetRemoved() |
|
| 137 | 138 |
stateCtr.del(container.ID) |
| 138 | 139 |
daemon.LogContainerEvent(container, "destroy") |
| 139 | 140 |
return nil |
| ... | ... |
@@ -253,7 +253,7 @@ func (d *Daemon) ContainerExecStart(ctx context.Context, name string, stdin io.R |
| 253 | 253 |
return fmt.Errorf("context cancelled")
|
| 254 | 254 |
case err := <-attachErr: |
| 255 | 255 |
if err != nil {
|
| 256 |
- if _, ok := err.(stream.DetachError); !ok {
|
|
| 256 |
+ if _, ok := err.(term.EscapeError); !ok {
|
|
| 257 | 257 |
return fmt.Errorf("exec attach failed with error: %v", err)
|
| 258 | 258 |
} |
| 259 | 259 |
d.LogContainerEvent(c, "exec_detach") |
| ... | ... |
@@ -153,7 +153,7 @@ func (daemon *Daemon) getInspectData(container *container.Container) (*types.Con |
| 153 | 153 |
Dead: container.State.Dead, |
| 154 | 154 |
Pid: container.State.Pid, |
| 155 | 155 |
ExitCode: container.State.ExitCode(), |
| 156 |
- Error: container.State.Error(), |
|
| 156 |
+ Error: container.State.ErrorMsg, |
|
| 157 | 157 |
StartedAt: container.State.StartedAt.Format(time.RFC3339Nano), |
| 158 | 158 |
FinishedAt: container.State.FinishedAt.Format(time.RFC3339Nano), |
| 159 | 159 |
Health: containerHealth, |
| ... | ... |
@@ -1,6 +1,7 @@ |
| 1 | 1 |
package daemon |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "context" |
|
| 4 | 5 |
"fmt" |
| 5 | 6 |
"runtime" |
| 6 | 7 |
"strings" |
| ... | ... |
@@ -8,7 +9,7 @@ import ( |
| 8 | 8 |
"time" |
| 9 | 9 |
|
| 10 | 10 |
"github.com/Sirupsen/logrus" |
| 11 |
- "github.com/docker/docker/container" |
|
| 11 |
+ containerpkg "github.com/docker/docker/container" |
|
| 12 | 12 |
"github.com/docker/docker/pkg/signal" |
| 13 | 13 |
) |
| 14 | 14 |
|
| ... | ... |
@@ -54,7 +55,7 @@ func (daemon *Daemon) ContainerKill(name string, sig uint64) error {
|
| 54 | 54 |
// to send the signal. An error is returned if the container is paused |
| 55 | 55 |
// or not running, or if there is a problem returned from the |
| 56 | 56 |
// underlying kill command. |
| 57 |
-func (daemon *Daemon) killWithSignal(container *container.Container, sig int) error {
|
|
| 57 |
+func (daemon *Daemon) killWithSignal(container *containerpkg.Container, sig int) error {
|
|
| 58 | 58 |
logrus.Debugf("Sending kill signal %d to container %s", sig, container.ID)
|
| 59 | 59 |
container.Lock() |
| 60 | 60 |
defer container.Unlock() |
| ... | ... |
@@ -110,7 +111,7 @@ func (daemon *Daemon) killWithSignal(container *container.Container, sig int) er |
| 110 | 110 |
} |
| 111 | 111 |
|
| 112 | 112 |
// Kill forcefully terminates a container. |
| 113 |
-func (daemon *Daemon) Kill(container *container.Container) error {
|
|
| 113 |
+func (daemon *Daemon) Kill(container *containerpkg.Container) error {
|
|
| 114 | 114 |
if !container.IsRunning() {
|
| 115 | 115 |
return errNotRunning{container.ID}
|
| 116 | 116 |
} |
| ... | ... |
@@ -131,7 +132,10 @@ func (daemon *Daemon) Kill(container *container.Container) error {
|
| 131 | 131 |
return nil |
| 132 | 132 |
} |
| 133 | 133 |
|
| 134 |
- if _, err2 := container.WaitStop(2 * time.Second); err2 != nil {
|
|
| 134 |
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) |
|
| 135 |
+ defer cancel() |
|
| 136 |
+ |
|
| 137 |
+ if status := <-container.Wait(ctx, containerpkg.WaitConditionNotRunning); status.Err() != nil {
|
|
| 135 | 138 |
return err |
| 136 | 139 |
} |
| 137 | 140 |
} |
| ... | ... |
@@ -144,12 +148,15 @@ func (daemon *Daemon) Kill(container *container.Container) error {
|
| 144 | 144 |
return err |
| 145 | 145 |
} |
| 146 | 146 |
|
| 147 |
- container.WaitStop(-1 * time.Second) |
|
| 147 |
+ // Wait for exit with no timeout. |
|
| 148 |
+ // Ignore returned status. |
|
| 149 |
+ _ = <-container.Wait(context.Background(), containerpkg.WaitConditionNotRunning) |
|
| 150 |
+ |
|
| 148 | 151 |
return nil |
| 149 | 152 |
} |
| 150 | 153 |
|
| 151 | 154 |
// killPossibleDeadProcess is a wrapper around killSig() suppressing "no such process" error. |
| 152 |
-func (daemon *Daemon) killPossiblyDeadProcess(container *container.Container, sig int) error {
|
|
| 155 |
+func (daemon *Daemon) killPossiblyDeadProcess(container *containerpkg.Container, sig int) error {
|
|
| 153 | 156 |
err := daemon.killWithSignal(container, sig) |
| 154 | 157 |
if err == syscall.ESRCH {
|
| 155 | 158 |
e := errNoSuchProcess{container.GetPID(), sig}
|
| ... | ... |
@@ -159,6 +166,6 @@ func (daemon *Daemon) killPossiblyDeadProcess(container *container.Container, si |
| 159 | 159 |
return err |
| 160 | 160 |
} |
| 161 | 161 |
|
| 162 |
-func (daemon *Daemon) kill(c *container.Container, sig int) error {
|
|
| 162 |
+func (daemon *Daemon) kill(c *containerpkg.Container, sig int) error {
|
|
| 163 | 163 |
return daemon.containerd.Signal(c.ID, sig) |
| 164 | 164 |
} |
| ... | ... |
@@ -1,13 +1,14 @@ |
| 1 | 1 |
package daemon |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "context" |
|
| 4 | 5 |
"fmt" |
| 5 | 6 |
"net/http" |
| 6 | 7 |
"time" |
| 7 | 8 |
|
| 8 | 9 |
"github.com/Sirupsen/logrus" |
| 9 | 10 |
"github.com/docker/docker/api/errors" |
| 10 |
- "github.com/docker/docker/container" |
|
| 11 |
+ containerpkg "github.com/docker/docker/container" |
|
| 11 | 12 |
) |
| 12 | 13 |
|
| 13 | 14 |
// ContainerStop looks for the given container and terminates it, |
| ... | ... |
@@ -40,7 +41,7 @@ func (daemon *Daemon) ContainerStop(name string, seconds *int) error {
|
| 40 | 40 |
// process to exit. If a negative duration is given, Stop will wait |
| 41 | 41 |
// for the initial signal forever. If the container is not running Stop returns |
| 42 | 42 |
// immediately. |
| 43 |
-func (daemon *Daemon) containerStop(container *container.Container, seconds int) error {
|
|
| 43 |
+func (daemon *Daemon) containerStop(container *containerpkg.Container, seconds int) error {
|
|
| 44 | 44 |
if !container.IsRunning() {
|
| 45 | 45 |
return nil |
| 46 | 46 |
} |
| ... | ... |
@@ -60,7 +61,10 @@ func (daemon *Daemon) containerStop(container *container.Container, seconds int) |
| 60 | 60 |
// So, instead we'll give it up to 2 more seconds to complete and if |
| 61 | 61 |
// by that time the container is still running, then the error |
| 62 | 62 |
// we got is probably valid and so we force kill it. |
| 63 |
- if _, err := container.WaitStop(2 * time.Second); err != nil {
|
|
| 63 |
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) |
|
| 64 |
+ defer cancel() |
|
| 65 |
+ |
|
| 66 |
+ if status := <-container.Wait(ctx, containerpkg.WaitConditionNotRunning); status.Err() != nil {
|
|
| 64 | 67 |
logrus.Infof("Container failed to stop after sending signal %d to the process, force killing", stopSignal)
|
| 65 | 68 |
if err := daemon.killPossiblyDeadProcess(container, 9); err != nil {
|
| 66 | 69 |
return err |
| ... | ... |
@@ -69,11 +73,15 @@ func (daemon *Daemon) containerStop(container *container.Container, seconds int) |
| 69 | 69 |
} |
| 70 | 70 |
|
| 71 | 71 |
// 2. Wait for the process to exit on its own |
| 72 |
- if _, err := container.WaitStop(time.Duration(seconds) * time.Second); err != nil {
|
|
| 72 |
+ ctx, cancel := context.WithTimeout(context.Background(), time.Duration(seconds)*time.Second) |
|
| 73 |
+ defer cancel() |
|
| 74 |
+ |
|
| 75 |
+ if status := <-container.Wait(ctx, containerpkg.WaitConditionNotRunning); status.Err() != nil {
|
|
| 73 | 76 |
logrus.Infof("Container %v failed to exit within %d seconds of signal %d - using the force", container.ID, seconds, stopSignal)
|
| 74 | 77 |
// 3. If it doesn't, then send SIGKILL |
| 75 | 78 |
if err := daemon.Kill(container); err != nil {
|
| 76 |
- container.WaitStop(-1 * time.Second) |
|
| 79 |
+ // Wait without a timeout, ignore result. |
|
| 80 |
+ _ = <-container.Wait(context.Background(), containerpkg.WaitConditionNotRunning) |
|
| 77 | 81 |
logrus.Warn(err) // Don't return error because we only care that container is stopped, not what function stopped it |
| 78 | 82 |
} |
| 79 | 83 |
} |
| ... | ... |
@@ -1,32 +1,22 @@ |
| 1 | 1 |
package daemon |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "time" |
|
| 5 |
- |
|
| 4 |
+ "github.com/docker/docker/container" |
|
| 6 | 5 |
"golang.org/x/net/context" |
| 7 | 6 |
) |
| 8 | 7 |
|
| 9 |
-// ContainerWait stops processing until the given container is |
|
| 10 |
-// stopped. If the container is not found, an error is returned. On a |
|
| 11 |
-// successful stop, the exit code of the container is returned. On a |
|
| 12 |
-// timeout, an error is returned. If you want to wait forever, supply |
|
| 13 |
-// a negative duration for the timeout. |
|
| 14 |
-func (daemon *Daemon) ContainerWait(name string, timeout time.Duration) (int, error) {
|
|
| 15 |
- container, err := daemon.GetContainer(name) |
|
| 16 |
- if err != nil {
|
|
| 17 |
- return -1, err |
|
| 18 |
- } |
|
| 19 |
- |
|
| 20 |
- return container.WaitStop(timeout) |
|
| 21 |
-} |
|
| 22 |
- |
|
| 23 |
-// ContainerWaitWithContext returns a channel where exit code is sent |
|
| 24 |
-// when container stops. Channel can be cancelled with a context. |
|
| 25 |
-func (daemon *Daemon) ContainerWaitWithContext(ctx context.Context, name string) error {
|
|
| 26 |
- container, err := daemon.GetContainer(name) |
|
| 8 |
+// ContainerWait waits until the given container is in a certain state |
|
| 9 |
+// indicated by the given condition. If the container is not found, a nil |
|
| 10 |
+// channel and non-nil error is returned immediately. If the container is |
|
| 11 |
+// found, a status result will be sent on the returned channel once the wait |
|
| 12 |
+// condition is met or if an error occurs waiting for the container (such as a |
|
| 13 |
+// context timeout or cancellation). On a successful wait, the exit code of the |
|
| 14 |
+// container is returned in the status with a non-nil Err() value. |
|
| 15 |
+func (daemon *Daemon) ContainerWait(ctx context.Context, name string, condition container.WaitCondition) (<-chan container.StateStatus, error) {
|
|
| 16 |
+ cntr, err := daemon.GetContainer(name) |
|
| 27 | 17 |
if err != nil {
|
| 28 |
- return err |
|
| 18 |
+ return nil, err |
|
| 29 | 19 |
} |
| 30 | 20 |
|
| 31 |
- return container.WaitWithContext(ctx) |
|
| 21 |
+ return cntr.Wait(ctx, condition), nil |
|
| 32 | 22 |
} |
| ... | ... |
@@ -28,6 +28,7 @@ keywords: "API, Docker, rcli, REST, documentation" |
| 28 | 28 |
the swarm, the desired CA key for the swarm (if not using an external certificate), and an optional parameter to force swarm to |
| 29 | 29 |
generate and rotate to a new CA certificate/key pair. |
| 30 | 30 |
* `POST /service/create` and `POST /services/(id or name)/update` now take the field `Platforms` as part of the service `Placement`, allowing to specify platforms supported by the service. |
| 31 |
+* `POST /containers/(name)/wait` now accepts a `condition` query parameter to indicate which state change condition to wait for. Also, response headers are now returned immediately to acknowledge that the server has registered a wait callback for the client. |
|
| 31 | 32 |
|
| 32 | 33 |
## v1.29 API changes |
| 33 | 34 |
|
| ... | ... |
@@ -21,7 +21,7 @@ Usage: docker wait CONTAINER [CONTAINER...] |
| 21 | 21 |
Block until one or more containers stop, then print their exit codes |
| 22 | 22 |
|
| 23 | 23 |
Options: |
| 24 |
- --help Print usage |
|
| 24 |
+ --help Print usage |
|
| 25 | 25 |
``` |
| 26 | 26 |
|
| 27 | 27 |
> **Note**: `docker wait` returns `0` when run against a container which had |
| ... | ... |
@@ -83,11 +83,13 @@ func privilegedTestChunkExecutor(autoRemove bool) testChunkExecutor {
|
| 83 | 83 |
} |
| 84 | 84 |
var b bytes.Buffer |
| 85 | 85 |
teeContainerStream(&b, os.Stdout, os.Stderr, stream) |
| 86 |
- rc, err := cli.ContainerWait(context.Background(), id) |
|
| 87 |
- if err != nil {
|
|
| 86 |
+ resultC, errC := cli.ContainerWait(context.Background(), id, "") |
|
| 87 |
+ select {
|
|
| 88 |
+ case err := <-errC: |
|
| 88 | 89 |
return 0, "", err |
| 90 |
+ case result := <-resultC: |
|
| 91 |
+ return result.StatusCode, b.String(), nil |
|
| 89 | 92 |
} |
| 90 |
- return rc, b.String(), nil |
|
| 91 | 93 |
} |
| 92 | 94 |
} |
| 93 | 95 |
|
| ... | ... |
@@ -188,5 +188,11 @@ func waitForContainerCompletion(cli *client.Client, stdout, stderr io.Writer, co |
| 188 | 188 |
} |
| 189 | 189 |
stdcopy.StdCopy(stdout, stderr, stream) |
| 190 | 190 |
stream.Close() |
| 191 |
- return cli.ContainerWait(context.Background(), containerID) |
|
| 191 |
+ resultC, errC := cli.ContainerWait(context.Background(), containerID, "") |
|
| 192 |
+ select {
|
|
| 193 |
+ case err := <-errC: |
|
| 194 |
+ return 1, err |
|
| 195 |
+ case result := <-resultC: |
|
| 196 |
+ return result.StatusCode, nil |
|
| 197 |
+ } |
|
| 192 | 198 |
} |
| 193 | 199 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,74 @@ |
| 0 |
+package term |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "io" |
|
| 4 |
+) |
|
| 5 |
+ |
|
| 6 |
+// EscapeError is special error which returned by a TTY proxy reader's Read() |
|
| 7 |
+// method in case its detach escape sequence is read. |
|
| 8 |
+type EscapeError struct{}
|
|
| 9 |
+ |
|
| 10 |
+func (EscapeError) Error() string {
|
|
| 11 |
+ return "read escape sequence" |
|
| 12 |
+} |
|
| 13 |
+ |
|
| 14 |
+// escapeProxy is used only for attaches with a TTY. It is used to proxy |
|
| 15 |
+// stdin keypresses from the underlying reader and look for the passed in |
|
| 16 |
+// escape key sequence to signal a detach. |
|
| 17 |
+type escapeProxy struct {
|
|
| 18 |
+ escapeKeys []byte |
|
| 19 |
+ escapeKeyPos int |
|
| 20 |
+ r io.Reader |
|
| 21 |
+} |
|
| 22 |
+ |
|
| 23 |
+// NewEscapeProxy returns a new TTY proxy reader which wraps the given reader |
|
| 24 |
+// and detects when the specified escape keys are read, in which case the Read |
|
| 25 |
+// method will return an error of type EscapeError. |
|
| 26 |
+func NewEscapeProxy(r io.Reader, escapeKeys []byte) io.Reader {
|
|
| 27 |
+ return &escapeProxy{
|
|
| 28 |
+ escapeKeys: escapeKeys, |
|
| 29 |
+ r: r, |
|
| 30 |
+ } |
|
| 31 |
+} |
|
| 32 |
+ |
|
| 33 |
+func (r *escapeProxy) Read(buf []byte) (int, error) {
|
|
| 34 |
+ nr, err := r.r.Read(buf) |
|
| 35 |
+ |
|
| 36 |
+ preserve := func() {
|
|
| 37 |
+ // this preserves the original key presses in the passed in buffer |
|
| 38 |
+ nr += r.escapeKeyPos |
|
| 39 |
+ preserve := make([]byte, 0, r.escapeKeyPos+len(buf)) |
|
| 40 |
+ preserve = append(preserve, r.escapeKeys[:r.escapeKeyPos]...) |
|
| 41 |
+ preserve = append(preserve, buf...) |
|
| 42 |
+ r.escapeKeyPos = 0 |
|
| 43 |
+ copy(buf[0:nr], preserve) |
|
| 44 |
+ } |
|
| 45 |
+ |
|
| 46 |
+ if nr != 1 || err != nil {
|
|
| 47 |
+ if r.escapeKeyPos > 0 {
|
|
| 48 |
+ preserve() |
|
| 49 |
+ } |
|
| 50 |
+ return nr, err |
|
| 51 |
+ } |
|
| 52 |
+ |
|
| 53 |
+ if buf[0] != r.escapeKeys[r.escapeKeyPos] {
|
|
| 54 |
+ if r.escapeKeyPos > 0 {
|
|
| 55 |
+ preserve() |
|
| 56 |
+ } |
|
| 57 |
+ return nr, nil |
|
| 58 |
+ } |
|
| 59 |
+ |
|
| 60 |
+ if r.escapeKeyPos == len(r.escapeKeys)-1 {
|
|
| 61 |
+ return 0, EscapeError{}
|
|
| 62 |
+ } |
|
| 63 |
+ |
|
| 64 |
+ // Looks like we've got an escape key, but we need to match again on the next |
|
| 65 |
+ // read. |
|
| 66 |
+ // Store the current escape key we found so we can look for the next one on |
|
| 67 |
+ // the next read. |
|
| 68 |
+ // Since this is an escape key, make sure we don't let the caller read it |
|
| 69 |
+ // If later on we find that this is not the escape sequence, we'll add the |
|
| 70 |
+ // keys back |
|
| 71 |
+ r.escapeKeyPos++ |
|
| 72 |
+ return nr - r.escapeKeyPos, nil |
|
| 73 |
+} |