1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

Fix stdout premature EOF

Never close attached stream before both stdout and stderr have written
all their buffered contents. Remove stdinCloser because it is not needed
any more as the stream is closed anyway after attach has finished.

Fixes #3631

Signed-off-by: Andy Goldstein <agoldste@redhat.com>
This commit is contained in:
Andy Goldstein 2014-10-03 13:38:44 -04:00
parent 25c32d3167
commit 5572dbb750
3 changed files with 49 additions and 23 deletions

View file

@ -83,7 +83,6 @@ func (daemon *Daemon) ContainerAttach(job *engine.Job) engine.Status {
var ( var (
cStdin io.ReadCloser cStdin io.ReadCloser
cStdout, cStderr io.Writer cStdout, cStderr io.Writer
cStdinCloser io.Closer
) )
if stdin { if stdin {
@ -94,7 +93,6 @@ func (daemon *Daemon) ContainerAttach(job *engine.Job) engine.Status {
io.Copy(w, job.Stdin) io.Copy(w, job.Stdin)
}() }()
cStdin = r cStdin = r
cStdinCloser = job.Stdin
} }
if stdout { if stdout {
cStdout = job.Stdout cStdout = job.Stdout
@ -103,7 +101,7 @@ func (daemon *Daemon) ContainerAttach(job *engine.Job) engine.Status {
cStderr = job.Stderr cStderr = job.Stderr
} }
<-daemon.attach(&container.StreamConfig, container.Config.OpenStdin, container.Config.StdinOnce, container.Config.Tty, cStdin, cStdinCloser, cStdout, cStderr) <-daemon.attach(&container.StreamConfig, container.Config.OpenStdin, container.Config.StdinOnce, container.Config.Tty, cStdin, cStdout, cStderr)
// If we are in stdinonce mode, wait for the process to end // If we are in stdinonce mode, wait for the process to end
// otherwise, simply return // otherwise, simply return
if container.Config.StdinOnce && !container.Config.Tty { if container.Config.StdinOnce && !container.Config.Tty {
@ -113,7 +111,7 @@ func (daemon *Daemon) ContainerAttach(job *engine.Job) engine.Status {
return engine.StatusOK return engine.StatusOK
} }
func (daemon *Daemon) attach(streamConfig *StreamConfig, openStdin, stdinOnce, tty bool, stdin io.ReadCloser, stdinCloser io.Closer, stdout io.Writer, stderr io.Writer) chan error { func (daemon *Daemon) attach(streamConfig *StreamConfig, openStdin, stdinOnce, tty bool, stdin io.ReadCloser, stdout io.Writer, stderr io.Writer) chan error {
var ( var (
cStdout, cStderr io.ReadCloser cStdout, cStderr io.ReadCloser
nJobs int nJobs int
@ -130,10 +128,10 @@ func (daemon *Daemon) attach(streamConfig *StreamConfig, openStdin, stdinOnce, t
go func() { go func() {
log.Debugf("attach: stdin: begin") log.Debugf("attach: stdin: begin")
defer log.Debugf("attach: stdin: end") defer log.Debugf("attach: stdin: end")
// No matter what, when stdin is closed (io.Copy unblock), close stdout and stderr
if stdinOnce && !tty { if stdinOnce && !tty {
defer cStdin.Close() defer cStdin.Close()
} else { } else {
// No matter what, when stdin is closed (io.Copy unblock), close stdout and stderr
defer func() { defer func() {
if cStdout != nil { if cStdout != nil {
cStdout.Close() cStdout.Close()
@ -173,9 +171,6 @@ func (daemon *Daemon) attach(streamConfig *StreamConfig, openStdin, stdinOnce, t
if stdinOnce && stdin != nil { if stdinOnce && stdin != nil {
defer stdin.Close() defer stdin.Close()
} }
if stdinCloser != nil {
defer stdinCloser.Close()
}
_, err := io.Copy(stdout, cStdout) _, err := io.Copy(stdout, cStdout)
if err == io.ErrClosedPipe { if err == io.ErrClosedPipe {
err = nil err = nil
@ -189,9 +184,6 @@ func (daemon *Daemon) attach(streamConfig *StreamConfig, openStdin, stdinOnce, t
} else { } else {
// Point stdout of container to a no-op writer. // Point stdout of container to a no-op writer.
go func() { go func() {
if stdinCloser != nil {
defer stdinCloser.Close()
}
if cStdout, err := streamConfig.StdoutPipe(); err != nil { if cStdout, err := streamConfig.StdoutPipe(); err != nil {
log.Errorf("attach: stdout pipe: %s", err) log.Errorf("attach: stdout pipe: %s", err)
} else { } else {
@ -213,9 +205,6 @@ func (daemon *Daemon) attach(streamConfig *StreamConfig, openStdin, stdinOnce, t
if stdinOnce && stdin != nil { if stdinOnce && stdin != nil {
defer stdin.Close() defer stdin.Close()
} }
if stdinCloser != nil {
defer stdinCloser.Close()
}
_, err := io.Copy(stderr, cStderr) _, err := io.Copy(stderr, cStderr)
if err == io.ErrClosedPipe { if err == io.ErrClosedPipe {
err = nil err = nil
@ -229,10 +218,6 @@ func (daemon *Daemon) attach(streamConfig *StreamConfig, openStdin, stdinOnce, t
} else { } else {
// Point stderr at a no-op writer. // Point stderr at a no-op writer.
go func() { go func() {
if stdinCloser != nil {
defer stdinCloser.Close()
}
if cStderr, err := streamConfig.StderrPipe(); err != nil { if cStderr, err := streamConfig.StderrPipe(); err != nil {
log.Errorf("attach: stdout pipe: %s", err) log.Errorf("attach: stdout pipe: %s", err)
} else { } else {
@ -251,8 +236,6 @@ func (daemon *Daemon) attach(streamConfig *StreamConfig, openStdin, stdinOnce, t
} }
}() }()
// FIXME: how to clean up the stdin goroutine without the unwanted side effect
// of closing the passed stdin? Add an intermediary io.Pipe?
for i := 0; i < nJobs; i++ { for i := 0; i < nJobs; i++ {
log.Debugf("attach: waiting for job %d/%d", i+1, nJobs) log.Debugf("attach: waiting for job %d/%d", i+1, nJobs)
if err := <-errors; err != nil { if err := <-errors; err != nil {

View file

@ -155,7 +155,6 @@ func (d *Daemon) ContainerExecStart(job *engine.Job) engine.Status {
var ( var (
cStdin io.ReadCloser cStdin io.ReadCloser
cStdout, cStderr io.Writer cStdout, cStderr io.Writer
cStdinCloser io.Closer
execName = job.Args[0] execName = job.Args[0]
) )
@ -183,10 +182,10 @@ func (d *Daemon) ContainerExecStart(job *engine.Job) engine.Status {
r, w := io.Pipe() r, w := io.Pipe()
go func() { go func() {
defer w.Close() defer w.Close()
defer log.Debugf("Closing buffered stdin pipe")
io.Copy(w, job.Stdin) io.Copy(w, job.Stdin)
}() }()
cStdin = r cStdin = r
cStdinCloser = job.Stdin
} }
if execConfig.OpenStdout { if execConfig.OpenStdout {
cStdout = job.Stdout cStdout = job.Stdout
@ -204,7 +203,7 @@ func (d *Daemon) ContainerExecStart(job *engine.Job) engine.Status {
execConfig.StreamConfig.stdinPipe = ioutils.NopWriteCloser(ioutil.Discard) // Silently drop stdin execConfig.StreamConfig.stdinPipe = ioutils.NopWriteCloser(ioutil.Discard) // Silently drop stdin
} }
attachErr := d.attach(&execConfig.StreamConfig, execConfig.OpenStdin, false, execConfig.ProcessConfig.Tty, cStdin, cStdinCloser, cStdout, cStderr) attachErr := d.attach(&execConfig.StreamConfig, execConfig.OpenStdin, false, execConfig.ProcessConfig.Tty, cStdin, cStdout, cStderr)
execErr := make(chan error) execErr := make(chan error)

View file

@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net" "net"
"os" "os"
@ -2446,3 +2447,46 @@ func TestRunVolumesCleanPaths(t *testing.T) {
logDone("run - volume paths are cleaned") logDone("run - volume paths are cleaned")
} }
// Regression test for #3631
func TestRunSlowStdoutConsumer(t *testing.T) {
defer deleteAllContainers()
c := exec.Command("/bin/bash", "-c", dockerBinary+` run --rm -i busybox /bin/sh -c "dd if=/dev/zero of=/foo bs=1024 count=2000 &>/dev/null; catv /foo"`)
stdout, err := c.StdoutPipe()
if err != nil {
t.Fatal(err)
}
if err := c.Start(); err != nil {
t.Fatal(err)
}
n, err := consumeSlow(stdout, 10000, 5*time.Millisecond)
if err != nil {
t.Fatal(err)
}
expected := 2 * 1024 * 2000
if n != expected {
t.Fatalf("Expected %d, got %d", expected, n)
}
logDone("run - slow consumer")
}
func consumeSlow(reader io.Reader, chunkSize int, interval time.Duration) (n int, err error) {
buffer := make([]byte, chunkSize)
for {
var readBytes int
readBytes, err = reader.Read(buffer)
n += readBytes
if err != nil {
if err == io.EOF {
err = nil
}
return
}
time.Sleep(interval)
}
}