Also noticed potential hang when only stdout or stderr are used with
follow=1
Signed-off-by: Brian Goff <cpuguy83@gmail.com>
| ... | ... |
@@ -597,6 +597,11 @@ func (s *Server) getContainersLogs(version version.Version, w http.ResponseWrite |
| 597 | 597 |
since = time.Unix(s, 0) |
| 598 | 598 |
} |
| 599 | 599 |
|
| 600 |
+ var closeNotifier <-chan bool |
|
| 601 |
+ if notifier, ok := w.(http.CloseNotifier); ok {
|
|
| 602 |
+ closeNotifier = notifier.CloseNotify() |
|
| 603 |
+ } |
|
| 604 |
+ |
|
| 600 | 605 |
logsConfig := &daemon.ContainerLogsConfig{
|
| 601 | 606 |
Follow: boolValue(r, "follow"), |
| 602 | 607 |
Timestamps: boolValue(r, "timestamps"), |
| ... | ... |
@@ -605,6 +610,7 @@ func (s *Server) getContainersLogs(version version.Version, w http.ResponseWrite |
| 605 | 605 |
UseStdout: stdout, |
| 606 | 606 |
UseStderr: stderr, |
| 607 | 607 |
OutStream: ioutils.NewWriteFlusher(w), |
| 608 |
+ Stop: closeNotifier, |
|
| 608 | 609 |
} |
| 609 | 610 |
|
| 610 | 611 |
if err := s.daemon.ContainerLogs(vars["name"], logsConfig); err != nil {
|
| ... | ... |
@@ -25,6 +25,7 @@ type ContainerLogsConfig struct {
|
| 25 | 25 |
Since time.Time |
| 26 | 26 |
UseStdout, UseStderr bool |
| 27 | 27 |
OutStream io.Writer |
| 28 |
+ Stop <-chan bool |
|
| 28 | 29 |
} |
| 29 | 30 |
|
| 30 | 31 |
func (daemon *Daemon) ContainerLogs(name string, config *ContainerLogsConfig) error {
|
| ... | ... |
@@ -119,7 +120,8 @@ func (daemon *Daemon) ContainerLogs(name string, config *ContainerLogsConfig) er |
| 119 | 119 |
} |
| 120 | 120 |
|
| 121 | 121 |
if config.Follow && container.IsRunning() {
|
| 122 |
- chErr := make(chan error) |
|
| 122 |
+ chErrStderr := make(chan error) |
|
| 123 |
+ chErrStdout := make(chan error) |
|
| 123 | 124 |
var stdoutPipe, stderrPipe io.ReadCloser |
| 124 | 125 |
|
| 125 | 126 |
// write an empty chunk of data (this is to ensure that the |
| ... | ... |
@@ -131,7 +133,7 @@ func (daemon *Daemon) ContainerLogs(name string, config *ContainerLogsConfig) er |
| 131 | 131 |
stdoutPipe = container.StdoutLogPipe() |
| 132 | 132 |
go func() {
|
| 133 | 133 |
logrus.Debug("logs: stdout stream begin")
|
| 134 |
- chErr <- jsonlog.WriteLog(stdoutPipe, outStream, format, config.Since) |
|
| 134 |
+ chErrStdout <- jsonlog.WriteLog(stdoutPipe, outStream, format, config.Since) |
|
| 135 | 135 |
logrus.Debug("logs: stdout stream end")
|
| 136 | 136 |
}() |
| 137 | 137 |
} |
| ... | ... |
@@ -139,19 +141,33 @@ func (daemon *Daemon) ContainerLogs(name string, config *ContainerLogsConfig) er |
| 139 | 139 |
stderrPipe = container.StderrLogPipe() |
| 140 | 140 |
go func() {
|
| 141 | 141 |
logrus.Debug("logs: stderr stream begin")
|
| 142 |
- chErr <- jsonlog.WriteLog(stderrPipe, errStream, format, config.Since) |
|
| 142 |
+ chErrStderr <- jsonlog.WriteLog(stderrPipe, errStream, format, config.Since) |
|
| 143 | 143 |
logrus.Debug("logs: stderr stream end")
|
| 144 | 144 |
}() |
| 145 | 145 |
} |
| 146 | 146 |
|
| 147 |
- err = <-chErr |
|
| 148 |
- if stdoutPipe != nil {
|
|
| 149 |
- stdoutPipe.Close() |
|
| 150 |
- } |
|
| 151 |
- if stderrPipe != nil {
|
|
| 152 |
- stderrPipe.Close() |
|
| 147 |
+ select {
|
|
| 148 |
+ case err = <-chErrStderr: |
|
| 149 |
+ if stdoutPipe != nil {
|
|
| 150 |
+ stdoutPipe.Close() |
|
| 151 |
+ <-chErrStdout |
|
| 152 |
+ } |
|
| 153 |
+ case err = <-chErrStdout: |
|
| 154 |
+ if stderrPipe != nil {
|
|
| 155 |
+ stderrPipe.Close() |
|
| 156 |
+ <-chErrStderr |
|
| 157 |
+ } |
|
| 158 |
+ case <-config.Stop: |
|
| 159 |
+ if stdoutPipe != nil {
|
|
| 160 |
+ stdoutPipe.Close() |
|
| 161 |
+ <-chErrStdout |
|
| 162 |
+ } |
|
| 163 |
+ if stderrPipe != nil {
|
|
| 164 |
+ stderrPipe.Close() |
|
| 165 |
+ <-chErrStderr |
|
| 166 |
+ } |
|
| 167 |
+ return nil |
|
| 153 | 168 |
} |
| 154 |
- <-chErr // wait for 2nd goroutine to exit, otherwise bad things will happen |
|
| 155 | 169 |
|
| 156 | 170 |
if err != nil && err != io.EOF && err != io.ErrClosedPipe {
|
| 157 | 171 |
if e, ok := err.(*net.OpError); ok && e.Err != syscall.EPIPE {
|
| ... | ... |
@@ -425,3 +425,44 @@ func (s *DockerSuite) TestLogsFollowGoroutinesWithStdout(c *check.C) {
|
| 425 | 425 |
} |
| 426 | 426 |
} |
| 427 | 427 |
} |
| 428 |
+ |
|
| 429 |
+func (s *DockerSuite) TestLogsFollowGoroutinesNoOutput(c *check.C) {
|
|
| 430 |
+ out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "while true; do sleep 2; done") |
|
| 431 |
+ id := strings.TrimSpace(out) |
|
| 432 |
+ c.Assert(waitRun(id), check.IsNil) |
|
| 433 |
+ |
|
| 434 |
+ type info struct {
|
|
| 435 |
+ NGoroutines int |
|
| 436 |
+ } |
|
| 437 |
+ getNGoroutines := func() int {
|
|
| 438 |
+ var i info |
|
| 439 |
+ status, b, err := sockRequest("GET", "/info", nil)
|
|
| 440 |
+ c.Assert(err, check.IsNil) |
|
| 441 |
+ c.Assert(status, check.Equals, 200) |
|
| 442 |
+ c.Assert(json.Unmarshal(b, &i), check.IsNil) |
|
| 443 |
+ return i.NGoroutines |
|
| 444 |
+ } |
|
| 445 |
+ |
|
| 446 |
+ nroutines := getNGoroutines() |
|
| 447 |
+ |
|
| 448 |
+ cmd := exec.Command(dockerBinary, "logs", "-f", id) |
|
| 449 |
+ c.Assert(cmd.Start(), check.IsNil) |
|
| 450 |
+ time.Sleep(200 * time.Millisecond) |
|
| 451 |
+ c.Assert(cmd.Process.Kill(), check.IsNil) |
|
| 452 |
+ |
|
| 453 |
+ // NGoroutines is not updated right away, so we need to wait before failing |
|
| 454 |
+ t := time.After(30 * time.Second) |
|
| 455 |
+ for {
|
|
| 456 |
+ select {
|
|
| 457 |
+ case <-t: |
|
| 458 |
+ if n := getNGoroutines(); n > nroutines {
|
|
| 459 |
+ c.Fatalf("leaked goroutines: expected less than or equal to %d, got: %d", nroutines, n)
|
|
| 460 |
+ } |
|
| 461 |
+ default: |
|
| 462 |
+ if n := getNGoroutines(); n <= nroutines {
|
|
| 463 |
+ return |
|
| 464 |
+ } |
|
| 465 |
+ time.Sleep(200 * time.Millisecond) |
|
| 466 |
+ } |
|
| 467 |
+ } |
|
| 468 |
+} |