github.com/justincormack/cli@v0.0.0-20201215022714-831ebeae9675/cli/command/container/hijack.go (about)

     1  package container
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"runtime"
     8  	"sync"
     9  
    10  	"github.com/docker/cli/cli/command"
    11  	"github.com/docker/docker/api/types"
    12  	"github.com/docker/docker/pkg/ioutils"
    13  	"github.com/docker/docker/pkg/stdcopy"
    14  	"github.com/moby/term"
    15  	"github.com/sirupsen/logrus"
    16  )
    17  
    18  // The default escape key sequence: ctrl-p, ctrl-q
    19  // TODO: This could be moved to `pkg/term`.
    20  var defaultEscapeKeys = []byte{16, 17}
    21  
    22  // A hijackedIOStreamer handles copying input to and output from streams to the
    23  // connection.
    24  type hijackedIOStreamer struct {
    25  	streams      command.Streams
    26  	inputStream  io.ReadCloser
    27  	outputStream io.Writer
    28  	errorStream  io.Writer
    29  
    30  	resp types.HijackedResponse
    31  
    32  	tty        bool
    33  	detachKeys string
    34  }
    35  
    36  // stream handles setting up the IO and then begins streaming stdin/stdout
    37  // to/from the hijacked connection, blocking until it is either done reading
    38  // output, the user inputs the detach key sequence when in TTY mode, or when
    39  // the given context is cancelled.
    40  func (h *hijackedIOStreamer) stream(ctx context.Context) error {
    41  	restoreInput, err := h.setupInput()
    42  	if err != nil {
    43  		return fmt.Errorf("unable to setup input stream: %s", err)
    44  	}
    45  
    46  	defer restoreInput()
    47  
    48  	outputDone := h.beginOutputStream(restoreInput)
    49  	inputDone, detached := h.beginInputStream(restoreInput)
    50  
    51  	select {
    52  	case err := <-outputDone:
    53  		return err
    54  	case <-inputDone:
    55  		// Input stream has closed.
    56  		if h.outputStream != nil || h.errorStream != nil {
    57  			// Wait for output to complete streaming.
    58  			select {
    59  			case err := <-outputDone:
    60  				return err
    61  			case <-ctx.Done():
    62  				return ctx.Err()
    63  			}
    64  		}
    65  		return nil
    66  	case err := <-detached:
    67  		// Got a detach key sequence.
    68  		return err
    69  	case <-ctx.Done():
    70  		return ctx.Err()
    71  	}
    72  }
    73  
    74  func (h *hijackedIOStreamer) setupInput() (restore func(), err error) {
    75  	if h.inputStream == nil || !h.tty {
    76  		// No need to setup input TTY.
    77  		// The restore func is a nop.
    78  		return func() {}, nil
    79  	}
    80  
    81  	if err := setRawTerminal(h.streams); err != nil {
    82  		return nil, fmt.Errorf("unable to set IO streams as raw terminal: %s", err)
    83  	}
    84  
    85  	// Use sync.Once so we may call restore multiple times but ensure we
    86  	// only restore the terminal once.
    87  	var restoreOnce sync.Once
    88  	restore = func() {
    89  		restoreOnce.Do(func() {
    90  			restoreTerminal(h.streams, h.inputStream)
    91  		})
    92  	}
    93  
    94  	// Wrap the input to detect detach escape sequence.
    95  	// Use default escape keys if an invalid sequence is given.
    96  	escapeKeys := defaultEscapeKeys
    97  	if h.detachKeys != "" {
    98  		customEscapeKeys, err := term.ToBytes(h.detachKeys)
    99  		if err != nil {
   100  			logrus.Warnf("invalid detach escape keys, using default: %s", err)
   101  		} else {
   102  			escapeKeys = customEscapeKeys
   103  		}
   104  	}
   105  
   106  	h.inputStream = ioutils.NewReadCloserWrapper(term.NewEscapeProxy(h.inputStream, escapeKeys), h.inputStream.Close)
   107  
   108  	return restore, nil
   109  }
   110  
   111  func (h *hijackedIOStreamer) beginOutputStream(restoreInput func()) <-chan error {
   112  	if h.outputStream == nil && h.errorStream == nil {
   113  		// There is no need to copy output.
   114  		return nil
   115  	}
   116  
   117  	outputDone := make(chan error)
   118  	go func() {
   119  		var err error
   120  
   121  		// When TTY is ON, use regular copy
   122  		if h.outputStream != nil && h.tty {
   123  			_, err = io.Copy(h.outputStream, h.resp.Reader)
   124  			// We should restore the terminal as soon as possible
   125  			// once the connection ends so any following print
   126  			// messages will be in normal type.
   127  			restoreInput()
   128  		} else {
   129  			_, err = stdcopy.StdCopy(h.outputStream, h.errorStream, h.resp.Reader)
   130  		}
   131  
   132  		logrus.Debug("[hijack] End of stdout")
   133  
   134  		if err != nil {
   135  			logrus.Debugf("Error receiveStdout: %s", err)
   136  		}
   137  
   138  		outputDone <- err
   139  	}()
   140  
   141  	return outputDone
   142  }
   143  
   144  func (h *hijackedIOStreamer) beginInputStream(restoreInput func()) (doneC <-chan struct{}, detachedC <-chan error) {
   145  	inputDone := make(chan struct{})
   146  	detached := make(chan error)
   147  
   148  	go func() {
   149  		if h.inputStream != nil {
   150  			_, err := io.Copy(h.resp.Conn, h.inputStream)
   151  			// We should restore the terminal as soon as possible
   152  			// once the connection ends so any following print
   153  			// messages will be in normal type.
   154  			restoreInput()
   155  
   156  			logrus.Debug("[hijack] End of stdin")
   157  
   158  			if _, ok := err.(term.EscapeError); ok {
   159  				detached <- err
   160  				return
   161  			}
   162  
   163  			if err != nil {
   164  				// This error will also occur on the receive
   165  				// side (from stdout) where it will be
   166  				// propagated back to the caller.
   167  				logrus.Debugf("Error sendStdin: %s", err)
   168  			}
   169  		}
   170  
   171  		if err := h.resp.CloseWrite(); err != nil {
   172  			logrus.Debugf("Couldn't send EOF: %s", err)
   173  		}
   174  
   175  		close(inputDone)
   176  	}()
   177  
   178  	return inputDone, detached
   179  }
   180  
   181  func setRawTerminal(streams command.Streams) error {
   182  	if err := streams.In().SetRawTerminal(); err != nil {
   183  		return err
   184  	}
   185  	return streams.Out().SetRawTerminal()
   186  }
   187  
   188  // nolint: unparam
   189  func restoreTerminal(streams command.Streams, in io.Closer) error {
   190  	streams.In().RestoreTerminal()
   191  	streams.Out().RestoreTerminal()
   192  	// WARNING: DO NOT REMOVE THE OS CHECKS !!!
   193  	// For some reason this Close call blocks on darwin..
   194  	// As the client exits right after, simply discard the close
   195  	// until we find a better solution.
   196  	//
   197  	// This can also cause the client on Windows to get stuck in Win32 CloseHandle()
   198  	// in some cases. See https://github.com/docker/docker/issues/28267#issuecomment-288237442
   199  	// Tracked internally at Microsoft by VSO #11352156. In the
   200  	// Windows case, you hit this if you are using the native/v2 console,
   201  	// not the "legacy" console, and you start the client in a new window. eg
   202  	// `start docker run --rm -it microsoft/nanoserver cmd /s /c echo foobar`
   203  	// will hang. Remove start, and it won't repro.
   204  	if in != nil && runtime.GOOS != "darwin" && runtime.GOOS != "windows" {
   205  		return in.Close()
   206  	}
   207  	return nil
   208  }