diff --git a/cli/command/container/attach.go b/cli/command/container/attach.go index f31a13f27b86..bf9e5ebfe05d 100644 --- a/cli/command/container/attach.go +++ b/cli/command/container/attach.go @@ -103,7 +103,7 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o var in io.ReadCloser if options.Stdin { - in = dockerCLI.In() + in = stdinForAttach(dockerCLI.In()) } if opts.Proxy && !c.Config.Tty { diff --git a/cli/command/container/background.go b/cli/command/container/background.go new file mode 100644 index 000000000000..9e506725bcd3 --- /dev/null +++ b/cli/command/container/background.go @@ -0,0 +1,16 @@ +package container + +import ( + "io" + + "github.com/docker/cli/cli/streams" +) + +// stdinForAttach returns stdin for hijacking, or nil when copying it would +// spin (a background job whose stdin is still the controlling TTY). +func stdinForAttach(in *streams.In) io.ReadCloser { + if stdinFromBackgroundJob(in) { + return nil + } + return in +} diff --git a/cli/command/container/background_test.go b/cli/command/container/background_test.go new file mode 100644 index 000000000000..de37359b42d9 --- /dev/null +++ b/cli/command/container/background_test.go @@ -0,0 +1,22 @@ +package container + +import ( + "os" + "testing" + + "github.com/docker/cli/cli/streams" + "gotest.tools/v3/assert" +) + +func TestStdinForAttachNonTTY(t *testing.T) { + r, w, err := os.Pipe() + assert.NilError(t, err) + t.Cleanup(func() { + _ = r.Close() + _ = w.Close() + }) + + in := streams.NewIn(r) + got := stdinForAttach(in) + assert.Equal(t, got, in) +} diff --git a/cli/command/container/background_unix.go b/cli/command/container/background_unix.go new file mode 100644 index 000000000000..13138f11c8e8 --- /dev/null +++ b/cli/command/container/background_unix.go @@ -0,0 +1,25 @@ +//go:build !windows + +package container + +import ( + "golang.org/x/sys/unix" + + "github.com/docker/cli/cli/streams" +) + +// stdinFromBackgroundJob reports whether stdin is a TTY whose foreground +// process group is not us. Reading that TTY from a background job (for +// example `docker run -i ... &`) produces SIGTTIN; the CLI catches every +// signal for forwarding, so the default stop-in-background does not fire +// and the read retries in a tight loop. +func stdinFromBackgroundJob(in *streams.In) bool { + if in == nil || !in.IsTerminal() { + return false + } + fg, err := unix.IoctlGetInt(int(in.FD()), unix.TIOCGPGRP) + if err != nil { + return false + } + return unix.Getpgrp() != fg +} diff --git a/cli/command/container/background_windows.go b/cli/command/container/background_windows.go new file mode 100644 index 000000000000..121967d47de6 --- /dev/null +++ b/cli/command/container/background_windows.go @@ -0,0 +1,7 @@ +package container + +import "github.com/docker/cli/cli/streams" + +func stdinFromBackgroundJob(_ *streams.In) bool { + return false +} diff --git a/cli/command/container/exec.go b/cli/command/container/exec.go index bd4f9bc6242c..0148213103e1 100644 --- a/cli/command/container/exec.go +++ b/cli/command/container/exec.go @@ -144,7 +144,7 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *cl ) if execOptions.AttachStdin { - in = dockerCli.In() + in = stdinForAttach(dockerCli.In()) } if execOptions.AttachStdout { out = dockerCli.Out() diff --git a/cli/command/container/run.go b/cli/command/container/run.go index 3c25630d6b78..b6b40ec7f11c 100644 --- a/cli/command/container/run.go +++ b/cli/command/container/run.go @@ -277,7 +277,7 @@ func attachContainer(ctx context.Context, dockerCli command.Cli, containerID str in io.ReadCloser ) if options.Stdin { - in = dockerCli.In() + in = stdinForAttach(dockerCli.In()) } if options.Stdout { out = dockerCli.Out() diff --git a/cli/command/container/signals.go b/cli/command/container/signals.go index 3a98c1962364..ca05dd8a1f33 100644 --- a/cli/command/container/signals.go +++ b/cli/command/container/signals.go @@ -44,7 +44,7 @@ func ForwardAllSignals(ctx context.Context, apiClient client.ContainerAPIClient, break } } - if sig == "" { + if sig == "" || sig == "TTIN" || sig == "TTOU" { continue } diff --git a/cli/command/container/start.go b/cli/command/container/start.go index 916f5808d717..d496facb6e93 100644 --- a/cli/command/container/start.go +++ b/cli/command/container/start.go @@ -112,7 +112,7 @@ func RunStart(ctx context.Context, dockerCli command.Cli, opts *StartOptions) er var in io.ReadCloser if options.Stdin { - in = dockerCli.In() + in = stdinForAttach(dockerCli.In()) } resp, errAttach := dockerCli.Client().ContainerAttach(ctx, c.Container.ID, options)