Merge pull request #8571 from ncdc/3631-stdout-premature-eof

Fix stdout premature EOF
This commit is contained in:
Jessie Frazelle 2014-10-29 11:36:32 -07:00
commit f936a10d80
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)
}
}