From b6552cd93505d8b70fceed507c624618bc240da3 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Tue, 29 Jun 2021 10:16:30 +0200 Subject: [PATCH] add support for detach keys on compose run|exec Signed-off-by: Nicolas De Loof --- .golangci.yml | 1 - pkg/compose/attach.go | 27 ++++++++++++++++++--------- pkg/compose/exec.go | 27 ++++++++++++++++++++++++--- pkg/compose/run.go | 23 ++++++++++++++++++++++- 4 files changed, 64 insertions(+), 14 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 7685e8c6c..48ca9cf91 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -15,7 +15,6 @@ linters: - gosimple - govet - ineffassign - - interfacer - lll - misspell - nakedret diff --git a/pkg/compose/attach.go b/pkg/compose/attach.go index 7226ad0d6..d13ce79a1 100644 --- a/pkg/compose/attach.go +++ b/pkg/compose/attach.go @@ -27,6 +27,7 @@ import ( "github.com/docker/compose-cli/pkg/api" moby "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/stdcopy" + "github.com/moby/term" "github.com/docker/compose-cli/pkg/utils" ) @@ -77,23 +78,23 @@ func (s *composeService) attachContainer(ctx context.Context, container moby.Con Line: line, }) }) - _, err = s.attachContainerStreams(ctx, container.ID, service.Tty, nil, w) + _, _, err = s.attachContainerStreams(ctx, container.ID, service.Tty, nil, w) return err } -func (s *composeService) attachContainerStreams(ctx context.Context, container string, tty bool, r io.ReadCloser, w io.Writer) (func(), error) { +func (s *composeService) attachContainerStreams(ctx context.Context, container string, tty bool, r io.ReadCloser, w io.Writer) (func(), chan bool, error) { + detached := make(chan bool) var ( in *streams.In restore = func() { /* noop */ } ) if r != nil { in = streams.NewIn(r) - restore = in.RestoreTerminal } stdin, stdout, err := s.getContainerStreams(ctx, container) if err != nil { - return restore, err + return restore, detached, err } go func() { @@ -105,12 +106,20 @@ func (s *composeService) attachContainerStreams(ctx context.Context, container s }() if in != nil && stdin != nil { - err := in.SetRawTerminal() - if err != nil { - return restore, err + if in.IsTerminal() { + state, err := term.SetRawTerminal(in.FD()) + if err != nil { + return restore, detached, err + } + restore = func() { + term.RestoreTerminal(in.FD(), state) //nolint:errcheck + } } go func() { - io.Copy(stdin, in) //nolint:errcheck + _, err := io.Copy(stdin, r) + if _, ok := err.(term.EscapeError); ok { + close(detached) + } }() } @@ -123,7 +132,7 @@ func (s *composeService) attachContainerStreams(ctx context.Context, container s } }() } - return restore, nil + return restore, detached, nil } func (s *composeService) getContainerStreams(ctx context.Context, container string) (io.WriteCloser, io.ReadCloser, error) { diff --git a/pkg/compose/exec.go b/pkg/compose/exec.go index 0e45aebe0..5cb79e18d 100644 --- a/pkg/compose/exec.go +++ b/pkg/compose/exec.go @@ -22,9 +22,11 @@ import ( "io" "github.com/compose-spec/compose-go/types" + "github.com/docker/cli/cli/streams" moby "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/pkg/stdcopy" + "github.com/moby/term" "github.com/docker/compose-cli/pkg/api" ) @@ -92,18 +94,34 @@ func (s *composeService) interactiveExec(ctx context.Context, opts api.RunOption outputDone := make(chan error) inputDone := make(chan error) + stdout := ContainerStdout{HijackedResponse: resp} + stdin := ContainerStdin{HijackedResponse: resp} + r, err := s.getEscapeKeyProxy(opts.Reader) + if err != nil { + return err + } + + in := streams.NewIn(opts.Reader) + if in.IsTerminal() { + state, err := term.SetRawTerminal(in.FD()) + if err != nil { + return err + } + defer term.RestoreTerminal(in.FD(), state) //nolint:errcheck + } + go func() { if opts.Tty { - _, err := io.Copy(opts.Writer, resp.Reader) + _, err := io.Copy(opts.Writer, stdout) outputDone <- err } else { - _, err := stdcopy.StdCopy(opts.Writer, opts.Writer, resp.Reader) + _, err := stdcopy.StdCopy(opts.Writer, opts.Writer, stdout) outputDone <- err } }() go func() { - _, err := io.Copy(resp.Conn, opts.Reader) + _, err := io.Copy(stdin, r) inputDone <- err }() @@ -112,6 +130,9 @@ func (s *composeService) interactiveExec(ctx context.Context, opts api.RunOption case err := <-outputDone: return err case err := <-inputDone: + if _, ok := err.(term.EscapeError); ok { + return nil + } if err != nil { return err } diff --git a/pkg/compose/run.go b/pkg/compose/run.go index be78e55c8..251951388 100644 --- a/pkg/compose/run.go +++ b/pkg/compose/run.go @@ -19,13 +19,16 @@ package compose import ( "context" "fmt" + "io" "github.com/docker/compose-cli/pkg/api" "github.com/compose-spec/compose-go/types" moby "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/stringid" + "github.com/moby/term" ) func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts api.RunOptions) (int, error) { @@ -75,7 +78,11 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types. return 0, nil } - restore, err := s.attachContainerStreams(ctx, containerID, service.Tty, opts.Reader, opts.Writer) + r, err := s.getEscapeKeyProxy(opts.Reader) + if err != nil { + return 0, err + } + restore, detachC, err := s.attachContainerStreams(ctx, containerID, service.Tty, r, opts.Writer) if err != nil { return 0, err } @@ -93,12 +100,26 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types. select { case status := <-statusC: return int(status.StatusCode), nil + case <-detachC: + return 0, nil case err := <-errC: return 0, err } } +func (s *composeService) getEscapeKeyProxy(r io.ReadCloser) (io.ReadCloser, error) { + var escapeKeys = []byte{16, 17} + if s.configFile.DetachKeys != "" { + customEscapeKeys, err := term.ToBytes(s.configFile.DetachKeys) + if err != nil { + return nil, err + } + escapeKeys = customEscapeKeys + } + return ioutils.NewReadCloserWrapper(term.NewEscapeProxy(r, escapeKeys), r.Close), nil +} + func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts api.RunOptions) { service.Tty = opts.Tty service.ContainerName = opts.Name