diff --git a/commands.go b/commands.go index 68929cf6b..6e4340331 100644 --- a/commands.go +++ b/commands.go @@ -799,7 +799,8 @@ func (srv *Server) CmdAttach(stdin io.ReadCloser, stdout io.Writer, args ...stri if container == nil { return fmt.Errorf("No such container: %s", name) } - return <-container.Attach(stdin, stdout, stdout) + + return <-container.Attach(stdin, nil, stdout, stdout) } // Ports type - Used to parse multiple -p flags @@ -901,11 +902,17 @@ func (srv *Server) CmdRun(stdin io.ReadCloser, stdout io.Writer, args ...string) } } var ( - cStdin io.Reader + cStdin io.ReadCloser cStdout, cStderr io.Writer ) if config.AttachStdin { - cStdin = stdin + r, w := io.Pipe() + go func() { + defer w.Close() + defer Debugf("Closing buffered stdin pipe") + io.Copy(w, stdin) + }() + cStdin = r } if config.AttachStdout { cStdout = stdout @@ -913,7 +920,8 @@ func (srv *Server) CmdRun(stdin io.ReadCloser, stdout io.Writer, args ...string) if config.AttachStderr { cStderr = stdout // FIXME: rcli can't differentiate stdout from stderr } - attachErr := container.Attach(cStdin, cStdout, cStderr) + + attachErr := container.Attach(cStdin, stdin, cStdout, cStderr) Debugf("Starting\n") if err := container.Start(); err != nil { return err @@ -922,7 +930,9 @@ func (srv *Server) CmdRun(stdin io.ReadCloser, stdout io.Writer, args ...string) fmt.Fprintln(stdout, container.ShortId()) } Debugf("Waiting for attach to return\n") - return <-attachErr + <-attachErr + // Expecting I/O pipe error, discarding + return nil } func NewServer() (*Server, error) { diff --git a/commands_test.go b/commands_test.go index aa772db74..a68aea7a4 100644 --- a/commands_test.go +++ b/commands_test.go @@ -80,6 +80,57 @@ func TestRunHostname(t *testing.T) { } } +func TestRunExit(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + stdin, stdinPipe := io.Pipe() + stdout, stdoutPipe := io.Pipe() + c1 := make(chan struct{}) + go func() { + srv.CmdRun(stdin, stdoutPipe, "-i", GetTestImage(runtime).Id, "/bin/cat") + close(c1) + }() + + setTimeout(t, "Read/Write assertion timed out", 2*time.Second, func() { + if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 15); err != nil { + t.Fatal(err) + } + }) + + container := runtime.List()[0] + + // Closing /bin/cat stdin, expect it to exit + p, err := container.StdinPipe() + if err != nil { + t.Fatal(err) + } + if err := p.Close(); err != nil { + t.Fatal(err) + } + + // as the process exited, CmdRun must finish and unblock. Wait for it + setTimeout(t, "Waiting for CmdRun timed out", 2*time.Second, func() { + <-c1 + }) + + // Make sure that the client has been disconnected + setTimeout(t, "The client should have been disconnected once the remote process exited.", 2*time.Second, func() { + // Expecting pipe i/o error, just check that read does not block + stdin.Read([]byte{}) + }) + + // Cleanup pipes + if err := closeWrap(stdin, stdinPipe, stdout, stdoutPipe); err != nil { + t.Fatal(err) + } +} + // Expected behaviour: the process dies when the client disconnects func TestRunDisconnect(t *testing.T) { runtime, err := newTestRuntime() @@ -237,6 +288,7 @@ func TestAttachDisconnect(t *testing.T) { 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.WaitTimeout(500 * time.Millisecond) diff --git a/container.go b/container.go index adb160216..4879ec3e9 100644 --- a/container.go +++ b/container.go @@ -257,9 +257,9 @@ func (container *Container) start() error { return container.cmd.Start() } -func (container *Container) Attach(stdin io.Reader, stdout io.Writer, stderr io.Writer) chan error { - var cStdout io.ReadCloser - var cStderr io.ReadCloser +func (container *Container) Attach(stdin io.ReadCloser, stdinCloser io.Closer, stdout io.Writer, stderr io.Writer) chan error { + var cStdout, cStderr io.ReadCloser + var nJobs int errors := make(chan error, 3) if stdin != nil && container.Config.OpenStdin { @@ -269,15 +269,23 @@ func (container *Container) Attach(stdin io.Reader, stdout io.Writer, stderr io. } else { go func() { Debugf("[start] attach stdin\n") - defer Debugf("[end] attach stdin\n") + defer Debugf("[end] attach stdin\n") + // No matter what, when stdin is closed (io.Copy unblock), close stdout and stderr + if cStdout != nil { + defer cStdout.Close() + } + if cStderr != nil { + defer cStderr.Close() + } if container.Config.StdinOnce { defer cStdin.Close() } _, err := io.Copy(cStdin, stdin) if err != nil { - Debugf("[error] attach stdout: %s\n", err) + Debugf("[error] attach stdin: %s\n", err) } - errors <- err + // Discard error, expecting pipe error + errors <- nil }() } } @@ -290,6 +298,15 @@ func (container *Container) Attach(stdin io.Reader, stdout io.Writer, stderr io. go func() { Debugf("[start] attach stdout\n") defer Debugf("[end] attach stdout\n") + // If we are in StdinOnce mode, then close stdin + if container.Config.StdinOnce { + if stdin != nil { + defer stdin.Close() + } + if stdinCloser != nil { + defer stdinCloser.Close() + } + } _, err := io.Copy(stdout, cStdout) if err != nil { Debugf("[error] attach stdout: %s\n", err) @@ -307,6 +324,15 @@ func (container *Container) Attach(stdin io.Reader, stdout io.Writer, stderr io. go func() { Debugf("[start] attach stderr\n") defer Debugf("[end] attach stderr\n") + // If we are in StdinOnce mode, then close stdin + if container.Config.StdinOnce { + if stdin != nil { + defer stdin.Close() + } + if stdinCloser != nil { + defer stdinCloser.Close() + } + } _, err := io.Copy(stderr, cStderr) if err != nil { Debugf("[error] attach stderr: %s\n", err)