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 }