Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion cli/command/container/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,18 @@ func runAttach(dockerCli *command.DockerCli, opts *attachOptions) error {
logrus.Debugf("Error monitoring TTY size: %s", err)
}
}
if err := holdHijackedConnection(ctx, dockerCli, c.Config.Tty, in, dockerCli.Out(), dockerCli.Err(), resp); err != nil {

streamer := hijackedIOStreamer{
streams: dockerCli,
inputStream: in,
outputStream: dockerCli.Out(),
errorStream: dockerCli.Err(),
resp: resp,
tty: c.Config.Tty,
detachKeys: options.DetachKeys,
}

if err := streamer.stream(ctx); err != nil {
return err
}

Expand Down
12 changes: 11 additions & 1 deletion cli/command/container/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,17 @@ func runExec(dockerCli *command.DockerCli, options *execOptions, container strin
}
defer resp.Close()
errCh = promise.Go(func() error {
return holdHijackedConnection(ctx, dockerCli, execConfig.Tty, in, out, stderr, resp)
streamer := hijackedIOStreamer{
streams: dockerCli,
inputStream: in,
outputStream: out,
errorStream: stderr,
resp: resp,
tty: execConfig.Tty,
detachKeys: execConfig.DetachKeys,
}

return streamer.stream(ctx)
})

if execConfig.Tty && dockerCli.In().IsTerminal() {
Expand Down
214 changes: 148 additions & 66 deletions cli/command/container/hijack.go
Original file line number Diff line number Diff line change
@@ -1,99 +1,181 @@
package container

import (
"fmt"
"io"
"runtime"
"sync"

"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/docker/pkg/term"
"golang.org/x/net/context"
)

// holdHijackedConnection handles copying input to and output from streams to the
// connection
// nolint: gocyclo
func holdHijackedConnection(ctx context.Context, streams command.Streams, tty bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error {
var (
err error
restoreOnce sync.Once
)
if inputStream != nil && tty {
if err := setRawTerminal(streams); err != nil {
return err
}
defer func() {
restoreOnce.Do(func() {
restoreTerminal(streams, inputStream)
})
}()
// The default escape key sequence: ctrl-p, ctrl-q
// TODO: This could be moved to `pkg/term`.
var defaultEscapeKeys = []byte{16, 17}

// A hijackedIOStreamer handles copying input to and output from streams to the
// connection.
type hijackedIOStreamer struct {
streams command.Streams
inputStream io.ReadCloser
outputStream io.Writer
errorStream io.Writer

resp types.HijackedResponse

tty bool
detachKeys string
}

// stream handles setting up the IO and then begins streaming stdin/stdout
// to/from the hijacked connection, blocking until it is either done reading
// output, the user inputs the detach key sequence when in TTY mode, or when
// the given context is cancelled.
func (h *hijackedIOStreamer) stream(ctx context.Context) error {
restoreInput, err := h.setupInput()
if err != nil {
return fmt.Errorf("unable to setup input stream: %s", err)
}

receiveStdout := make(chan error, 1)
if outputStream != nil || errorStream != nil {
go func() {
// When TTY is ON, use regular copy
if tty && outputStream != nil {
_, err = io.Copy(outputStream, resp.Reader)
// we should restore the terminal as soon as possible once connection end
// so any following print messages will be in normal type.
if inputStream != nil {
restoreOnce.Do(func() {
restoreTerminal(streams, inputStream)
})
}
} else {
_, err = stdcopy.StdCopy(outputStream, errorStream, resp.Reader)
defer restoreInput()

outputDone := h.beginOutputStream(restoreInput)
inputDone, detached := h.beginInputStream(restoreInput)

select {
case err := <-outputDone:
return err
case <-inputDone:
// Input stream has closed.
if h.outputStream != nil || h.errorStream != nil {
// Wait for output to complete streaming.
select {
case err := <-outputDone:
return err
case <-ctx.Done():
return ctx.Err()
}
}
return nil
case err := <-detached:
// Got a detach key sequence.
return err
case <-ctx.Done():
return ctx.Err()
}
}

logrus.Debug("[hijack] End of stdout")
receiveStdout <- err
}()
func (h *hijackedIOStreamer) setupInput() (restore func(), err error) {
if h.inputStream == nil || !h.tty {
// No need to setup input TTY.
// The restore func is a nop.
return func() {}, nil
}

stdinDone := make(chan struct{})
go func() {
if inputStream != nil {
io.Copy(resp.Conn, inputStream)
// we should restore the terminal as soon as possible once connection end
// so any following print messages will be in normal type.
if tty {
restoreOnce.Do(func() {
restoreTerminal(streams, inputStream)
})
}
logrus.Debug("[hijack] End of stdin")
if err := setRawTerminal(h.streams); err != nil {
return nil, fmt.Errorf("unable to set IO streams as raw terminal: %s", err)
}

// Use sync.Once so we may call restore multiple times but ensure we
// only restore the terminal once.
var restoreOnce sync.Once
restore = func() {
restoreOnce.Do(func() {
restoreTerminal(h.streams, h.inputStream)
})
}

// Wrap the input to detect detach escape sequence.
// Use default escape keys if an invalid sequence is given.
escapeKeys := defaultEscapeKeys
if h.detachKeys != "" {
customEscapeKeys, err := term.ToBytes(h.detachKeys)
if err != nil {
logrus.Warnf("invalid detach escape keys, using default: %s", err)
} else {
escapeKeys = customEscapeKeys
}
}

if err := resp.CloseWrite(); err != nil {
logrus.Debugf("Couldn't send EOF: %s", err)
h.inputStream = ioutils.NewReadCloserWrapper(term.NewEscapeProxy(h.inputStream, escapeKeys), h.inputStream.Close)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By using the term proxy here, doesn't this mean the daemon will never receive the escape sequence?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, the daemon will not receive the escape sequence.


return restore, nil
}

func (h *hijackedIOStreamer) beginOutputStream(restoreInput func()) <-chan error {
if h.outputStream == nil && h.errorStream == nil {
// Ther is no need to copy output.
return nil
}

outputDone := make(chan error)
go func() {
var err error

// When TTY is ON, use regular copy
if h.outputStream != nil && h.tty {
_, err = io.Copy(h.outputStream, h.resp.Reader)
// We should restore the terminal as soon as possible
// once the connection ends so any following print
// messages will be in normal type.
restoreInput()
} else {
_, err = stdcopy.StdCopy(h.outputStream, h.errorStream, h.resp.Reader)
}
close(stdinDone)
}()

select {
case err := <-receiveStdout:
logrus.Debug("[hijack] End of stdout")

if err != nil {
logrus.Debugf("Error receiveStdout: %s", err)
return err
}
case <-stdinDone:
if outputStream != nil || errorStream != nil {
select {
case err := <-receiveStdout:
if err != nil {
logrus.Debugf("Error receiveStdout: %s", err)
return err
}
case <-ctx.Done():

outputDone <- err
}()

return outputDone
}

func (h *hijackedIOStreamer) beginInputStream(restoreInput func()) (doneC <-chan struct{}, detachedC <-chan error) {
inputDone := make(chan struct{})
detached := make(chan error)

go func() {
if h.inputStream != nil {
_, err := io.Copy(h.resp.Conn, h.inputStream)
// We should restore the terminal as soon as possible
// once the connection ends so any following print
// messages will be in normal type.
restoreInput()

logrus.Debug("[hijack] End of stdin")

if _, ok := err.(term.EscapeError); ok {
detached <- err
return
}

if err != nil {
// This error will also occur on the receive
// side (from stdout) where it will be
// propogated back to the caller.
logrus.Debugf("Error sendStdin: %s", err)
}
}
case <-ctx.Done():
}

return nil
if err := h.resp.CloseWrite(); err != nil {
logrus.Debugf("Couldn't send EOF: %s", err)
}

close(inputDone)
}()

return inputDone, detached
}

func setRawTerminal(streams command.Streams) error {
Expand Down
22 changes: 19 additions & 3 deletions cli/command/container/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/promise"
"github.com/docker/docker/pkg/signal"
"github.com/docker/docker/pkg/term"
"github.com/docker/libnetwork/resolvconf/dns"
"github.com/pkg/errors"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -175,8 +176,8 @@ func runContainer(dockerCli *command.DockerCli, opts *runOptions, copts *contain

//start the container
if err := client.ContainerStart(ctx, createResponse.ID, types.ContainerStartOptions{}); err != nil {
// If we have holdHijackedConnection, we should notify
// holdHijackedConnection we are going to exit and wait
// If we have hijackedIOStreamer, we should notify
// hijackedIOStreamer we are going to exit and wait
// to avoid the terminal are not restored.
if attach {
cancelFun()
Expand All @@ -199,6 +200,11 @@ func runContainer(dockerCli *command.DockerCli, opts *runOptions, copts *contain

if errCh != nil {
if err := <-errCh; err != nil {
if _, ok := err.(term.EscapeError); ok {
// The user entered the detach escape sequence.
return nil
}

logrus.Debugf("Error hijack: %s", err)
return err
}
Expand Down Expand Up @@ -261,7 +267,17 @@ func attachContainer(
}

*errCh = promise.Go(func() error {
if errHijack := holdHijackedConnection(ctx, dockerCli, config.Tty, in, out, cerr, resp); errHijack != nil {
streamer := hijackedIOStreamer{
streams: dockerCli,
inputStream: in,
outputStream: out,
errorStream: cerr,
resp: resp,
tty: config.Tty,
detachKeys: options.DetachKeys,
}

if errHijack := streamer.stream(ctx); errHijack != nil {
return errHijack
}
return errAttach
Expand Down
17 changes: 16 additions & 1 deletion cli/command/container/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/promise"
"github.com/docker/docker/pkg/signal"
"github.com/docker/docker/pkg/term"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
Expand Down Expand Up @@ -103,7 +104,17 @@ func runStart(dockerCli *command.DockerCli, opts *startOptions) error {
}
defer resp.Close()
cErr := promise.Go(func() error {
errHijack := holdHijackedConnection(ctx, dockerCli, c.Config.Tty, in, dockerCli.Out(), dockerCli.Err(), resp)
streamer := hijackedIOStreamer{
streams: dockerCli,
inputStream: in,
outputStream: dockerCli.Out(),
errorStream: dockerCli.Err(),
resp: resp,
tty: c.Config.Tty,
detachKeys: options.DetachKeys,
}

errHijack := streamer.stream(ctx)
if errHijack == nil {
return errAttach
}
Expand Down Expand Up @@ -136,6 +147,10 @@ func runStart(dockerCli *command.DockerCli, opts *startOptions) error {
}
}
if attchErr := <-cErr; attchErr != nil {
if _, ok := err.(term.EscapeError); ok {
// The user entered the detach escape sequence.
return nil
}
return attchErr
}

Expand Down
Loading