mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
6380e3d939
`copyEscapable` is a copy/paste of io.Copy with some added handling for checking for the attach escape sequence. This removes the copy/paste and uses `io.Copy` directly. To be able to do this, it now implements an `io.Reader` which proxies to the main reader but looks for the escape sequence. Signed-off-by: Brian Goff <cpuguy83@gmail.com>
217 lines
4.8 KiB
Go
217 lines
4.8 KiB
Go
package stream
|
|
|
|
import (
|
|
"io"
|
|
"sync"
|
|
|
|
"golang.org/x/net/context"
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
"github.com/docker/docker/pkg/promise"
|
|
)
|
|
|
|
var defaultEscapeSequence = []byte{16, 17} // ctrl-p, ctrl-q
|
|
|
|
// DetachError is special error which returned in case of container detach.
|
|
type DetachError struct{}
|
|
|
|
func (DetachError) Error() string {
|
|
return "detached from container"
|
|
}
|
|
|
|
// AttachConfig is the config struct used to attach a client to a stream's stdio
|
|
type AttachConfig struct {
|
|
// Tells the attach copier that the stream's stdin is a TTY and to look for
|
|
// escape sequences in stdin to detach from the stream.
|
|
// When true the escape sequence is not passed to the underlying stream
|
|
TTY bool
|
|
// Specifies the detach keys the client will be using
|
|
// Only useful when `TTY` is true
|
|
DetachKeys []byte
|
|
|
|
// CloseStdin signals that once done, stdin for the attached stream should be closed
|
|
// For example, this would close the attached container's stdin.
|
|
CloseStdin bool
|
|
|
|
// Provide client streams to wire up to
|
|
Stdin io.ReadCloser
|
|
Stdout, Stderr io.Writer
|
|
}
|
|
|
|
// Attach attaches the stream config to the streams specified in
|
|
// the AttachOptions
|
|
func (c *Config) Attach(ctx context.Context, cfg *AttachConfig) chan error {
|
|
var (
|
|
cStdout, cStderr io.ReadCloser
|
|
cStdin io.WriteCloser
|
|
wg sync.WaitGroup
|
|
errors = make(chan error, 3)
|
|
)
|
|
|
|
if cfg.Stdin != nil {
|
|
cStdin = c.StdinPipe()
|
|
wg.Add(1)
|
|
}
|
|
|
|
if cfg.Stdout != nil {
|
|
cStdout = c.StdoutPipe()
|
|
wg.Add(1)
|
|
}
|
|
|
|
if cfg.Stderr != nil {
|
|
cStderr = c.StderrPipe()
|
|
wg.Add(1)
|
|
}
|
|
|
|
// Connect stdin of container to the attach stdin stream.
|
|
go func() {
|
|
if cfg.Stdin == nil {
|
|
return
|
|
}
|
|
logrus.Debug("attach: stdin: begin")
|
|
|
|
var err error
|
|
if cfg.TTY {
|
|
_, err = copyEscapable(cStdin, cfg.Stdin, cfg.DetachKeys)
|
|
} else {
|
|
_, err = io.Copy(cStdin, cfg.Stdin)
|
|
}
|
|
if err == io.ErrClosedPipe {
|
|
err = nil
|
|
}
|
|
if err != nil {
|
|
logrus.Errorf("attach: stdin: %s", err)
|
|
errors <- err
|
|
}
|
|
if cfg.CloseStdin && !cfg.TTY {
|
|
cStdin.Close()
|
|
} else {
|
|
// No matter what, when stdin is closed (io.Copy unblock), close stdout and stderr
|
|
if cStdout != nil {
|
|
cStdout.Close()
|
|
}
|
|
if cStderr != nil {
|
|
cStderr.Close()
|
|
}
|
|
}
|
|
logrus.Debug("attach: stdin: end")
|
|
wg.Done()
|
|
}()
|
|
|
|
attachStream := func(name string, stream io.Writer, streamPipe io.ReadCloser) {
|
|
if stream == nil {
|
|
return
|
|
}
|
|
|
|
logrus.Debugf("attach: %s: begin", name)
|
|
_, err := io.Copy(stream, streamPipe)
|
|
if err == io.ErrClosedPipe {
|
|
err = nil
|
|
}
|
|
if err != nil {
|
|
logrus.Errorf("attach: %s: %v", name, err)
|
|
errors <- err
|
|
}
|
|
// Make sure stdin gets closed
|
|
if cfg.Stdin != nil {
|
|
cfg.Stdin.Close()
|
|
}
|
|
streamPipe.Close()
|
|
logrus.Debugf("attach: %s: end", name)
|
|
wg.Done()
|
|
}
|
|
|
|
go attachStream("stdout", cfg.Stdout, cStdout)
|
|
go attachStream("stderr", cfg.Stderr, cStderr)
|
|
|
|
return promise.Go(func() error {
|
|
done := make(chan struct{})
|
|
go func() {
|
|
wg.Wait()
|
|
close(done)
|
|
}()
|
|
select {
|
|
case <-done:
|
|
case <-ctx.Done():
|
|
// close all pipes
|
|
if cStdin != nil {
|
|
cStdin.Close()
|
|
}
|
|
if cStdout != nil {
|
|
cStdout.Close()
|
|
}
|
|
if cStderr != nil {
|
|
cStderr.Close()
|
|
}
|
|
<-done
|
|
}
|
|
close(errors)
|
|
for err := range errors {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// ttyProxy is used only for attaches with a TTY. It is used to proxy
|
|
// stdin keypresses from the underlying reader and look for the passed in
|
|
// escape key sequence to signal a detach.
|
|
type ttyProxy struct {
|
|
escapeKeys []byte
|
|
escapeKeyPos int
|
|
r io.Reader
|
|
}
|
|
|
|
func (r *ttyProxy) Read(buf []byte) (int, error) {
|
|
nr, err := r.r.Read(buf)
|
|
|
|
preserve := func() {
|
|
// this preserves the original key presses in the passed in buffer
|
|
nr += r.escapeKeyPos
|
|
preserve := make([]byte, 0, r.escapeKeyPos+len(buf))
|
|
preserve = append(preserve, r.escapeKeys[:r.escapeKeyPos]...)
|
|
preserve = append(preserve, buf...)
|
|
r.escapeKeyPos = 0
|
|
copy(buf[0:nr], preserve)
|
|
}
|
|
|
|
if nr != 1 || err != nil {
|
|
if r.escapeKeyPos > 0 {
|
|
preserve()
|
|
}
|
|
return nr, err
|
|
}
|
|
|
|
if buf[0] != r.escapeKeys[r.escapeKeyPos] {
|
|
if r.escapeKeyPos > 0 {
|
|
preserve()
|
|
}
|
|
return nr, nil
|
|
}
|
|
|
|
if r.escapeKeyPos == len(r.escapeKeys)-1 {
|
|
return 0, DetachError{}
|
|
}
|
|
|
|
// Looks like we've got an escape key, but we need to match again on the next
|
|
// read.
|
|
// Store the current escape key we found so we can look for the next one on
|
|
// the next read.
|
|
// Since this is an escape key, make sure we don't let the caller read it
|
|
// If later on we find that this is not the escape sequence, we'll add the
|
|
// keys back
|
|
r.escapeKeyPos++
|
|
return nr - r.escapeKeyPos, nil
|
|
}
|
|
|
|
func copyEscapable(dst io.Writer, src io.ReadCloser, keys []byte) (written int64, err error) {
|
|
if len(keys) == 0 {
|
|
keys = defaultEscapeSequence
|
|
}
|
|
pr := &ttyProxy{escapeKeys: keys, r: src}
|
|
defer src.Close()
|
|
|
|
return io.Copy(dst, pr)
|
|
}
|