diff --git a/daemon/container.go b/daemon/container.go index 99fe157ad8..f89db5cfcf 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -14,7 +14,6 @@ import ( "syscall" "time" - "github.com/docker/libcontainer" "github.com/docker/libcontainer/configs" "github.com/docker/libcontainer/devices" "github.com/docker/libcontainer/label" @@ -1020,14 +1019,6 @@ func (container *Container) Exposes(p nat.Port) bool { return exists } -func (container *Container) GetPtyMaster() (libcontainer.Console, error) { - ttyConsole, ok := container.command.ProcessConfig.Terminal.(execdriver.TtyTerminal) - if !ok { - return nil, ErrNoTTY - } - return ttyConsole.Master(), nil -} - func (container *Container) HostConfig() *runconfig.HostConfig { container.Lock() res := container.hostConfig diff --git a/integration-cli/docker_cli_attach_test.go b/integration-cli/docker_cli_attach_test.go index cf21cda588..04cb593989 100644 --- a/integration-cli/docker_cli_attach_test.go +++ b/integration-cli/docker_cli_attach_test.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "io" "os/exec" "strings" @@ -134,3 +135,51 @@ func TestAttachTtyWithoutStdin(t *testing.T) { logDone("attach - forbid piped stdin to tty enabled container") } + +func TestAttachDisconnect(t *testing.T) { + defer deleteAllContainers() + out, _, _ := dockerCmd(t, "run", "-di", "busybox", "/bin/cat") + id := strings.TrimSpace(out) + + cmd := exec.Command(dockerBinary, "attach", id) + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + defer stdin.Close() + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + defer stdout.Close() + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + defer cmd.Process.Kill() + + if _, err := stdin.Write([]byte("hello\n")); err != nil { + t.Fatal(err) + } + out, err = bufio.NewReader(stdout).ReadString('\n') + if err != nil { + t.Fatal(err) + } + if strings.TrimSpace(out) != "hello" { + t.Fatalf("exepected 'hello', got %q", out) + } + + if err := stdin.Close(); err != nil { + t.Fatal(err) + } + + // Expect container to still be running after stdin is closed + running, err := inspectField(id, "State.Running") + if err != nil { + t.Fatal(err) + } + if running != "true" { + t.Fatal("exepected container to still be running") + } + + logDone("attach - disconnect") +} diff --git a/integration-cli/docker_cli_attach_unix_test.go b/integration-cli/docker_cli_attach_unix_test.go index 8d15735120..ebc3804e3e 100644 --- a/integration-cli/docker_cli_attach_unix_test.go +++ b/integration-cli/docker_cli_attach_unix_test.go @@ -3,11 +3,13 @@ package main import ( + "bufio" "os/exec" "strings" "testing" "time" + "github.com/docker/docker/pkg/stringid" "github.com/kr/pty" ) @@ -137,3 +139,150 @@ func TestAttachAfterDetach(t *testing.T) { logDone("attach - reconnect after detaching") } + +// TestAttachDetach checks that attach in tty mode can be detached using the long container ID +func TestAttachDetach(t *testing.T) { + out, _, _ := dockerCmd(t, "run", "-itd", "busybox", "cat") + id := strings.TrimSpace(out) + if err := waitRun(id); err != nil { + t.Fatal(err) + } + + cpty, tty, err := pty.Open() + if err != nil { + t.Fatal(err) + } + defer cpty.Close() + + cmd := exec.Command(dockerBinary, "attach", id) + cmd.Stdin = tty + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + defer stdout.Close() + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + if err := waitRun(id); err != nil { + t.Fatalf("error waiting for container to start: %v", err) + } + + if _, err := cpty.Write([]byte("hello\n")); err != nil { + t.Fatal(err) + } + out, err = bufio.NewReader(stdout).ReadString('\n') + if err != nil { + t.Fatal(err) + } + if strings.TrimSpace(out) != "hello" { + t.Fatalf("exepected 'hello', got %q", out) + } + + // escape sequence + if _, err := cpty.Write([]byte{16}); err != nil { + t.Fatal(err) + } + time.Sleep(100 * time.Millisecond) + if _, err := cpty.Write([]byte{17}); err != nil { + t.Fatal(err) + } + + ch := make(chan struct{}) + go func() { + cmd.Wait() + ch <- struct{}{} + }() + + running, err := inspectField(id, "State.Running") + if err != nil { + t.Fatal(err) + } + if running != "true" { + t.Fatal("exepected container to still be running") + } + + go func() { + dockerCmd(t, "kill", id) + }() + + select { + case <-ch: + case <-time.After(10 * time.Millisecond): + t.Fatal("timed out waiting for container to exit") + } + + logDone("attach - detach") +} + +// TestAttachDetachTruncatedID checks that attach in tty mode can be detached +func TestAttachDetachTruncatedID(t *testing.T) { + out, _, _ := dockerCmd(t, "run", "-itd", "busybox", "cat") + id := stringid.TruncateID(strings.TrimSpace(out)) + if err := waitRun(id); err != nil { + t.Fatal(err) + } + + cpty, tty, err := pty.Open() + if err != nil { + t.Fatal(err) + } + defer cpty.Close() + + cmd := exec.Command(dockerBinary, "attach", id) + cmd.Stdin = tty + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + defer stdout.Close() + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + + if _, err := cpty.Write([]byte("hello\n")); err != nil { + t.Fatal(err) + } + out, err = bufio.NewReader(stdout).ReadString('\n') + if err != nil { + t.Fatal(err) + } + if strings.TrimSpace(out) != "hello" { + t.Fatalf("exepected 'hello', got %q", out) + } + + // escape sequence + if _, err := cpty.Write([]byte{16}); err != nil { + t.Fatal(err) + } + time.Sleep(100 * time.Millisecond) + if _, err := cpty.Write([]byte{17}); err != nil { + t.Fatal(err) + } + + ch := make(chan struct{}) + go func() { + cmd.Wait() + ch <- struct{}{} + }() + + running, err := inspectField(id, "State.Running") + if err != nil { + t.Fatal(err) + } + if running != "true" { + t.Fatal("exepected container to still be running") + } + + go func() { + dockerCmd(t, "kill", id) + }() + + select { + case <-ch: + case <-time.After(10 * time.Millisecond): + t.Fatal("timed out waiting for container to exit") + } + + logDone("attach - detach truncated ID") +} diff --git a/integration-cli/docker_cli_run_unix_test.go b/integration-cli/docker_cli_run_unix_test.go index 026f8279ef..211e6c1f55 100644 --- a/integration-cli/docker_cli_run_unix_test.go +++ b/integration-cli/docker_cli_run_unix_test.go @@ -3,6 +3,7 @@ package main import ( + "bufio" "fmt" "io/ioutil" "os" @@ -201,3 +202,73 @@ func TestRunDeviceDirectory(t *testing.T) { logDone("run - test --device directory mounts all internal devices") } + +// TestRunDetach checks attaching and detaching with the escape sequence. +func TestRunAttachDetach(t *testing.T) { + defer deleteAllContainers() + name := "attach-detach" + cmd := exec.Command(dockerBinary, "run", "--name", name, "-it", "busybox", "cat") + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + cpty, tty, err := pty.Open() + if err != nil { + t.Fatal(err) + } + defer cpty.Close() + cmd.Stdin = tty + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + if err := waitRun(name); err != nil { + t.Fatal(err) + } + + if _, err := cpty.Write([]byte("hello\n")); err != nil { + t.Fatal(err) + } + + out, err := bufio.NewReader(stdout).ReadString('\n') + if err != nil { + t.Fatal(err) + } + if strings.TrimSpace(out) != "hello" { + t.Fatalf("exepected 'hello', got %q", out) + } + + // escape sequence + if _, err := cpty.Write([]byte{16}); err != nil { + t.Fatal(err) + } + time.Sleep(100 * time.Millisecond) + if _, err := cpty.Write([]byte{17}); err != nil { + t.Fatal(err) + } + + ch := make(chan struct{}) + go func() { + cmd.Wait() + ch <- struct{}{} + }() + + running, err := inspectField(name, "State.Running") + if err != nil { + t.Fatal(err) + } + if running != "true" { + t.Fatal("exepected container to still be running") + } + + go func() { + dockerCmd(t, "kill", name) + }() + + select { + case <-ch: + case <-time.After(10 * time.Millisecond): + t.Fatal("timed out waiting for container to exit") + } + + logDone("run - attach detach") +} diff --git a/integration-cli/utils.go b/integration-cli/utils.go index 536f6984e2..1fcf44535e 100644 --- a/integration-cli/utils.go +++ b/integration-cli/utils.go @@ -213,7 +213,16 @@ func waitInspect(name, expr, expected string, timeout int) error { cmd := exec.Command(dockerBinary, "inspect", "-f", expr, name) out, _, err := runCommandWithOutput(cmd) if err != nil { - return fmt.Errorf("error executing docker inspect: %v", err) + if !strings.Contains(out, "No such") { + return fmt.Errorf("error executing docker inspect: %v\n%s", err, out) + } + select { + case <-after: + return err + default: + time.Sleep(10 * time.Millisecond) + continue + } } out = strings.TrimSpace(out) diff --git a/integration/commands_test.go b/integration/commands_test.go deleted file mode 100644 index 97a927b8bf..0000000000 --- a/integration/commands_test.go +++ /dev/null @@ -1,436 +0,0 @@ -package docker - -import ( - "bufio" - "fmt" - "io" - "io/ioutil" - "strings" - "testing" - "time" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/api/client" - "github.com/docker/docker/daemon" - "github.com/docker/docker/pkg/stringid" - "github.com/docker/docker/pkg/term" - "github.com/kr/pty" -) - -func closeWrap(args ...io.Closer) error { - e := false - ret := fmt.Errorf("Error closing elements") - for _, c := range args { - if err := c.Close(); err != nil { - e = true - ret = fmt.Errorf("%s\n%s", ret, err) - } - } - if e { - return ret - } - return nil -} - -func setRaw(t *testing.T, c *daemon.Container) *term.State { - pty, err := c.GetPtyMaster() - if err != nil { - t.Fatal(err) - } - state, err := term.MakeRaw(pty.Fd()) - if err != nil { - t.Fatal(err) - } - return state -} - -func unsetRaw(t *testing.T, c *daemon.Container, state *term.State) { - pty, err := c.GetPtyMaster() - if err != nil { - t.Fatal(err) - } - term.RestoreTerminal(pty.Fd(), state) -} - -func waitContainerStart(t *testing.T, timeout time.Duration) *daemon.Container { - var container *daemon.Container - - setTimeout(t, "Waiting for the container to be started timed out", timeout, func() { - for { - l := globalDaemon.List() - if len(l) == 1 && l[0].IsRunning() { - container = l[0] - break - } - time.Sleep(10 * time.Millisecond) - } - }) - - if container == nil { - t.Fatal("An error occured while waiting for the container to start") - } - - return container -} - -func setTimeout(t *testing.T, msg string, d time.Duration, f func()) { - c := make(chan bool) - - // Make sure we are not too long - go func() { - time.Sleep(d) - c <- true - }() - go func() { - f() - c <- false - }() - if <-c && msg != "" { - t.Fatal(msg) - } -} - -func expectPipe(expected string, r io.Reader) error { - o, err := bufio.NewReader(r).ReadString('\n') - if err != nil { - return err - } - if strings.Trim(o, " \r\n") != expected { - return fmt.Errorf("Unexpected output. Expected [%s], received [%s]", expected, o) - } - return nil -} - -func assertPipe(input, output string, r io.Reader, w io.Writer, count int) error { - for i := 0; i < count; i++ { - if _, err := w.Write([]byte(input)); err != nil { - return err - } - if err := expectPipe(output, r); err != nil { - return err - } - } - return nil -} - -// TestRunDetach checks attaching and detaching with the escape sequence. -func TestRunDetach(t *testing.T) { - stdout, stdoutPipe := io.Pipe() - cpty, tty, err := pty.Open() - if err != nil { - t.Fatal(err) - } - - cli := client.NewDockerCli(tty, stdoutPipe, ioutil.Discard, "", testDaemonProto, testDaemonAddr, nil) - defer cleanup(globalEngine, t) - - ch := make(chan struct{}) - go func() { - defer close(ch) - cli.CmdRun("-i", "-t", unitTestImageID, "cat") - }() - - container := waitContainerStart(t, 10*time.Second) - - state := setRaw(t, container) - defer unsetRaw(t, container, state) - - setTimeout(t, "First read/write assertion timed out", 2*time.Second, func() { - if err := assertPipe("hello\n", "hello", stdout, cpty, 150); err != nil { - t.Fatal(err) - } - }) - - setTimeout(t, "Escape sequence timeout", 5*time.Second, func() { - cpty.Write([]byte{16}) - time.Sleep(100 * time.Millisecond) - cpty.Write([]byte{17}) - }) - - // wait for CmdRun to return - setTimeout(t, "Waiting for CmdRun timed out", 15*time.Second, func() { - <-ch - }) - closeWrap(cpty, stdout, stdoutPipe) - - time.Sleep(500 * time.Millisecond) - if !container.IsRunning() { - t.Fatal("The detached container should be still running") - } - - setTimeout(t, "Waiting for container to die timed out", 20*time.Second, func() { - container.Kill() - }) -} - -// TestAttachDetach checks that attach in tty mode can be detached using the long container ID -func TestAttachDetach(t *testing.T) { - stdout, stdoutPipe := io.Pipe() - cpty, tty, err := pty.Open() - if err != nil { - t.Fatal(err) - } - - cli := client.NewDockerCli(tty, stdoutPipe, ioutil.Discard, "", testDaemonProto, testDaemonAddr, nil) - defer cleanup(globalEngine, t) - - ch := make(chan struct{}) - go func() { - defer close(ch) - if err := cli.CmdRun("-i", "-t", "-d", unitTestImageID, "cat"); err != nil { - t.Fatal(err) - } - }() - - container := waitContainerStart(t, 10*time.Second) - - setTimeout(t, "Reading container's id timed out", 10*time.Second, func() { - buf := make([]byte, 1024) - n, err := stdout.Read(buf) - if err != nil { - t.Fatal(err) - } - - if strings.Trim(string(buf[:n]), " \r\n") != container.ID { - t.Fatalf("Wrong ID received. Expect %s, received %s", container.ID, buf[:n]) - } - }) - setTimeout(t, "Starting container timed out", 10*time.Second, func() { - <-ch - }) - - state := setRaw(t, container) - defer unsetRaw(t, container, state) - - stdout, stdoutPipe = io.Pipe() - cpty, tty, err = pty.Open() - if err != nil { - t.Fatal(err) - } - - cli = client.NewDockerCli(tty, stdoutPipe, ioutil.Discard, "", testDaemonProto, testDaemonAddr, nil) - - ch = make(chan struct{}) - go func() { - defer close(ch) - if err := cli.CmdAttach(container.ID); err != nil { - if err != io.ErrClosedPipe { - t.Fatal(err) - } - } - }() - - setTimeout(t, "First read/write assertion timed out", 2*time.Second, func() { - if err := assertPipe("hello\n", "hello", stdout, cpty, 150); err != nil { - if err != io.ErrClosedPipe { - t.Fatal(err) - } - } - }) - - setTimeout(t, "Escape sequence timeout", 5*time.Second, func() { - cpty.Write([]byte{16}) - time.Sleep(100 * time.Millisecond) - cpty.Write([]byte{17}) - }) - - // wait for CmdRun to return - setTimeout(t, "Waiting for CmdAttach timed out", 15*time.Second, func() { - <-ch - }) - - closeWrap(cpty, stdout, stdoutPipe) - - time.Sleep(500 * time.Millisecond) - if !container.IsRunning() { - t.Fatal("The detached container should be still running") - } - - setTimeout(t, "Waiting for container to die timedout", 5*time.Second, func() { - container.Kill() - }) -} - -// TestAttachDetachTruncatedID checks that attach in tty mode can be detached -func TestAttachDetachTruncatedID(t *testing.T) { - stdout, stdoutPipe := io.Pipe() - cpty, tty, err := pty.Open() - if err != nil { - t.Fatal(err) - } - - cli := client.NewDockerCli(tty, stdoutPipe, ioutil.Discard, "", testDaemonProto, testDaemonAddr, nil) - defer cleanup(globalEngine, t) - - // Discard the CmdRun output - go stdout.Read(make([]byte, 1024)) - setTimeout(t, "Starting container timed out", 2*time.Second, func() { - if err := cli.CmdRun("-i", "-t", "-d", unitTestImageID, "cat"); err != nil { - t.Fatal(err) - } - }) - - container := waitContainerStart(t, 10*time.Second) - - state := setRaw(t, container) - defer unsetRaw(t, container, state) - - stdout, stdoutPipe = io.Pipe() - cpty, tty, err = pty.Open() - if err != nil { - t.Fatal(err) - } - - cli = client.NewDockerCli(tty, stdoutPipe, ioutil.Discard, "", testDaemonProto, testDaemonAddr, nil) - - ch := make(chan struct{}) - go func() { - defer close(ch) - if err := cli.CmdAttach(stringid.TruncateID(container.ID)); err != nil { - if err != io.ErrClosedPipe { - t.Fatal(err) - } - } - }() - - setTimeout(t, "First read/write assertion timed out", 2*time.Second, func() { - if err := assertPipe("hello\n", "hello", stdout, cpty, 150); err != nil { - if err != io.ErrClosedPipe { - t.Fatal(err) - } - } - }) - - setTimeout(t, "Escape sequence timeout", 5*time.Second, func() { - cpty.Write([]byte{16}) - time.Sleep(100 * time.Millisecond) - cpty.Write([]byte{17}) - }) - - // wait for CmdRun to return - setTimeout(t, "Waiting for CmdAttach timed out", 15*time.Second, func() { - <-ch - }) - closeWrap(cpty, stdout, stdoutPipe) - - time.Sleep(500 * time.Millisecond) - if !container.IsRunning() { - t.Fatal("The detached container should be still running") - } - - setTimeout(t, "Waiting for container to die timedout", 5*time.Second, func() { - container.Kill() - }) -} - -// Expected behaviour, the process stays alive when the client disconnects -func TestAttachDisconnect(t *testing.T) { - stdout, stdoutPipe := io.Pipe() - cpty, tty, err := pty.Open() - if err != nil { - t.Fatal(err) - } - - cli := client.NewDockerCli(tty, stdoutPipe, ioutil.Discard, "", testDaemonProto, testDaemonAddr, nil) - defer cleanup(globalEngine, t) - - go func() { - // Start a process in daemon mode - if err := cli.CmdRun("-d", "-i", unitTestImageID, "/bin/cat"); err != nil { - logrus.Debugf("Error CmdRun: %s", err) - } - }() - - setTimeout(t, "Waiting for CmdRun timed out", 10*time.Second, func() { - if _, err := bufio.NewReader(stdout).ReadString('\n'); err != nil { - t.Fatal(err) - } - }) - - setTimeout(t, "Waiting for the container to be started timed out", 10*time.Second, func() { - for { - l := globalDaemon.List() - if len(l) == 1 && l[0].IsRunning() { - break - } - time.Sleep(10 * time.Millisecond) - } - }) - - container := globalDaemon.List()[0] - - // Attach to it - c1 := make(chan struct{}) - go func() { - // We're simulating a disconnect so the return value doesn't matter. What matters is the - // fact that CmdAttach returns. - cli.CmdAttach(container.ID) - close(c1) - }() - - setTimeout(t, "First read/write assertion timed out", 2*time.Second, func() { - if err := assertPipe("hello\n", "hello", stdout, cpty, 150); err != nil { - t.Fatal(err) - } - }) - // Close pipes (client disconnects) - if err := closeWrap(cpty, stdout, stdoutPipe); err != nil { - t.Fatal(err) - } - - // Wait for attach to finish, the client disconnected, therefore, Attach finished his job - setTimeout(t, "Waiting for CmdAttach timed out", 2*time.Second, func() { - <-c1 - }) - - // We closed stdin, expect /bin/cat to still be running - // Wait a little bit to make sure container.monitor() did his thing - _, err = container.WaitStop(500 * time.Millisecond) - if err == nil || !container.IsRunning() { - t.Fatalf("/bin/cat is not running after closing stdin") - } - - // Try to avoid the timeout in destroy. Best effort, don't check error - cStdin := container.StdinPipe() - cStdin.Close() - container.WaitStop(-1 * time.Second) -} - -// Expected behaviour: container gets deleted automatically after exit -func TestRunAutoRemove(t *testing.T) { - t.Skip("Fixme. Skipping test for now, race condition") - stdout, stdoutPipe := io.Pipe() - - cli := client.NewDockerCli(nil, stdoutPipe, ioutil.Discard, "", testDaemonProto, testDaemonAddr, nil) - defer cleanup(globalEngine, t) - - c := make(chan struct{}) - go func() { - defer close(c) - if err := cli.CmdRun("--rm", unitTestImageID, "hostname"); err != nil { - t.Fatal(err) - } - }() - - var temporaryContainerID string - setTimeout(t, "Reading command output time out", 2*time.Second, func() { - cmdOutput, err := bufio.NewReader(stdout).ReadString('\n') - if err != nil { - t.Fatal(err) - } - temporaryContainerID = cmdOutput - if err := closeWrap(stdout, stdoutPipe); err != nil { - t.Fatal(err) - } - }) - - setTimeout(t, "CmdRun timed out", 10*time.Second, func() { - <-c - }) - - time.Sleep(500 * time.Millisecond) - - if len(globalDaemon.List()) > 0 { - t.Fatalf("failed to remove container automatically: container %s still exists", temporaryContainerID) - } -} diff --git a/integration/utils.go b/integration/utils.go new file mode 100644 index 0000000000..1d27cd6e42 --- /dev/null +++ b/integration/utils.go @@ -0,0 +1,88 @@ +package docker + +import ( + "bufio" + "fmt" + "io" + "strings" + "testing" + "time" + + "github.com/docker/docker/daemon" +) + +func closeWrap(args ...io.Closer) error { + e := false + ret := fmt.Errorf("Error closing elements") + for _, c := range args { + if err := c.Close(); err != nil { + e = true + ret = fmt.Errorf("%s\n%s", ret, err) + } + } + if e { + return ret + } + return nil +} + +func waitContainerStart(t *testing.T, timeout time.Duration) *daemon.Container { + var container *daemon.Container + + setTimeout(t, "Waiting for the container to be started timed out", timeout, func() { + for { + l := globalDaemon.List() + if len(l) == 1 && l[0].IsRunning() { + container = l[0] + break + } + time.Sleep(10 * time.Millisecond) + } + }) + + if container == nil { + t.Fatal("An error occured while waiting for the container to start") + } + + return container +} + +func setTimeout(t *testing.T, msg string, d time.Duration, f func()) { + c := make(chan bool) + + // Make sure we are not too long + go func() { + time.Sleep(d) + c <- true + }() + go func() { + f() + c <- false + }() + if <-c && msg != "" { + t.Fatal(msg) + } +} + +func expectPipe(expected string, r io.Reader) error { + o, err := bufio.NewReader(r).ReadString('\n') + if err != nil { + return err + } + if strings.Trim(o, " \r\n") != expected { + return fmt.Errorf("Unexpected output. Expected [%s], received [%s]", expected, o) + } + return nil +} + +func assertPipe(input, output string, r io.Reader, w io.Writer, count int) error { + for i := 0; i < count; i++ { + if _, err := w.Write([]byte(input)); err != nil { + return err + } + if err := expectPipe(output, r); err != nil { + return err + } + } + return nil +}