container/run: Fix stdout/err truncation after container exit

Fix a regression introduced by 30c4637f03
which made the `docker run` command produce potentially truncated
stdout/stderr output.

Previous implementation stopped the content streaming as soon as the
container exited which would potentially truncate a long outputs.

This change fixes the issue by only canceling the IO stream immediately
if neither stdout nor stderr is attached.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski 2025-03-24 15:27:28 +01:00
parent ff5fdfae35
commit c27751fcfe
No known key found for this signature in database
GPG Key ID: B85EFCFE26DEF92A
2 changed files with 66 additions and 4 deletions

View File

@ -238,10 +238,16 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption
return cli.StatusError{StatusCode: status}
}
case status := <-statusChan:
// notify hijackedIOStreamer that we're exiting and wait
// so that the terminal can be restored.
cancelFun()
<-errCh
// If container exits, output stream processing may not be finished yet,
// we need to keep the streamer running until all output is read.
// However, if stdout or stderr is not attached, we can just exit.
if !config.AttachStdout && !config.AttachStderr {
// Notify hijackedIOStreamer that we're exiting and wait
// so that the terminal can be restored.
cancelFun()
}
<-errCh // Drain channel but don't care about result
if status != 0 {
return cli.StatusError{StatusCode: status}
}

View File

@ -3,6 +3,8 @@ package container
import (
"bytes"
"fmt"
"io"
"math/rand"
"os/exec"
"strings"
"syscall"
@ -280,3 +282,57 @@ func TestProcessTermination(t *testing.T) {
ExitCode: 0,
})
}
// Adapted from https://github.com/docker/for-mac/issues/7632#issue-2932169772
// Thanks [@almet](https://github.com/almet)!
func TestRunReadAfterContainerExit(t *testing.T) {
skip.If(t, environment.RemoteDaemon())
r := rand.New(rand.NewSource(0x123456))
const size = 18933764
cmd := exec.Command("docker", "run",
"--rm", "-i",
"alpine",
"sh", "-c", "cat -",
)
cmd.Stdin = io.LimitReader(r, size)
var stderr bytes.Buffer
cmd.Stderr = &stderr
stdout, err := cmd.StdoutPipe()
assert.NilError(t, err)
err = cmd.Start()
assert.NilError(t, err)
buffer := make([]byte, 1000)
counter := 0
totalRead := 0
for {
n, err := stdout.Read(buffer)
if n > 0 {
totalRead += n
}
// Wait 0.1s every megabyte (approx.)
if counter%1000 == 0 {
time.Sleep(100 * time.Millisecond)
}
if err != nil || n == 0 {
break
}
counter++
}
err = cmd.Wait()
t.Logf("Error: %v", err)
t.Logf("Stderr: %s", stderr.String())
assert.Check(t, err == nil)
assert.Check(t, is.Equal(totalRead, size))
}