github.com/cspotcode/docker-cli@v20.10.0-rc1.0.20201201121459-3faad7acc5b8+incompatible/cli/command/container/attach.go (about)

     1  package container
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  
     8  	"github.com/docker/cli/cli"
     9  	"github.com/docker/cli/cli/command"
    10  	"github.com/docker/docker/api/types"
    11  	"github.com/docker/docker/api/types/container"
    12  	"github.com/docker/docker/client"
    13  	"github.com/docker/docker/pkg/signal"
    14  	"github.com/pkg/errors"
    15  	"github.com/sirupsen/logrus"
    16  	"github.com/spf13/cobra"
    17  )
    18  
    19  type attachOptions struct {
    20  	noStdin    bool
    21  	proxy      bool
    22  	detachKeys string
    23  
    24  	container string
    25  }
    26  
    27  func inspectContainerAndCheckState(ctx context.Context, cli client.APIClient, args string) (*types.ContainerJSON, error) {
    28  	c, err := cli.ContainerInspect(ctx, args)
    29  	if err != nil {
    30  		return nil, err
    31  	}
    32  	if !c.State.Running {
    33  		return nil, errors.New("You cannot attach to a stopped container, start it first")
    34  	}
    35  	if c.State.Paused {
    36  		return nil, errors.New("You cannot attach to a paused container, unpause it first")
    37  	}
    38  	if c.State.Restarting {
    39  		return nil, errors.New("You cannot attach to a restarting container, wait until it is running")
    40  	}
    41  
    42  	return &c, nil
    43  }
    44  
    45  // NewAttachCommand creates a new cobra.Command for `docker attach`
    46  func NewAttachCommand(dockerCli command.Cli) *cobra.Command {
    47  	var opts attachOptions
    48  
    49  	cmd := &cobra.Command{
    50  		Use:   "attach [OPTIONS] CONTAINER",
    51  		Short: "Attach local standard input, output, and error streams to a running container",
    52  		Args:  cli.ExactArgs(1),
    53  		RunE: func(cmd *cobra.Command, args []string) error {
    54  			opts.container = args[0]
    55  			return runAttach(dockerCli, &opts)
    56  		},
    57  	}
    58  
    59  	flags := cmd.Flags()
    60  	flags.BoolVar(&opts.noStdin, "no-stdin", false, "Do not attach STDIN")
    61  	flags.BoolVar(&opts.proxy, "sig-proxy", true, "Proxy all received signals to the process")
    62  	flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
    63  	return cmd
    64  }
    65  
    66  func runAttach(dockerCli command.Cli, opts *attachOptions) error {
    67  	ctx := context.Background()
    68  	client := dockerCli.Client()
    69  
    70  	// request channel to wait for client
    71  	resultC, errC := client.ContainerWait(ctx, opts.container, "")
    72  
    73  	c, err := inspectContainerAndCheckState(ctx, client, opts.container)
    74  	if err != nil {
    75  		return err
    76  	}
    77  
    78  	if err := dockerCli.In().CheckTty(!opts.noStdin, c.Config.Tty); err != nil {
    79  		return err
    80  	}
    81  
    82  	if opts.detachKeys != "" {
    83  		dockerCli.ConfigFile().DetachKeys = opts.detachKeys
    84  	}
    85  
    86  	options := types.ContainerAttachOptions{
    87  		Stream:     true,
    88  		Stdin:      !opts.noStdin && c.Config.OpenStdin,
    89  		Stdout:     true,
    90  		Stderr:     true,
    91  		DetachKeys: dockerCli.ConfigFile().DetachKeys,
    92  	}
    93  
    94  	var in io.ReadCloser
    95  	if options.Stdin {
    96  		in = dockerCli.In()
    97  	}
    98  
    99  	if opts.proxy && !c.Config.Tty {
   100  		sigc := ForwardAllSignals(ctx, dockerCli, opts.container)
   101  		defer signal.StopCatch(sigc)
   102  	}
   103  
   104  	resp, errAttach := client.ContainerAttach(ctx, opts.container, options)
   105  	if errAttach != nil {
   106  		return errAttach
   107  	}
   108  	defer resp.Close()
   109  
   110  	// If use docker attach command to attach to a stop container, it will return
   111  	// "You cannot attach to a stopped container" error, it's ok, but when
   112  	// attach to a running container, it(docker attach) use inspect to check
   113  	// the container's state, if it pass the state check on the client side,
   114  	// and then the container is stopped, docker attach command still attach to
   115  	// the container and not exit.
   116  	//
   117  	// Recheck the container's state to avoid attach block.
   118  	_, err = inspectContainerAndCheckState(ctx, client, opts.container)
   119  	if err != nil {
   120  		return err
   121  	}
   122  
   123  	if c.Config.Tty && dockerCli.Out().IsTerminal() {
   124  		resizeTTY(ctx, dockerCli, opts.container)
   125  	}
   126  
   127  	streamer := hijackedIOStreamer{
   128  		streams:      dockerCli,
   129  		inputStream:  in,
   130  		outputStream: dockerCli.Out(),
   131  		errorStream:  dockerCli.Err(),
   132  		resp:         resp,
   133  		tty:          c.Config.Tty,
   134  		detachKeys:   options.DetachKeys,
   135  	}
   136  
   137  	if err := streamer.stream(ctx); err != nil {
   138  		return err
   139  	}
   140  
   141  	return getExitStatus(errC, resultC)
   142  }
   143  
   144  func getExitStatus(errC <-chan error, resultC <-chan container.ContainerWaitOKBody) error {
   145  	select {
   146  	case result := <-resultC:
   147  		if result.Error != nil {
   148  			return fmt.Errorf(result.Error.Message)
   149  		}
   150  		if result.StatusCode != 0 {
   151  			return cli.StatusError{StatusCode: int(result.StatusCode)}
   152  		}
   153  	case err := <-errC:
   154  		return err
   155  	}
   156  
   157  	return nil
   158  }
   159  
   160  func resizeTTY(ctx context.Context, dockerCli command.Cli, containerID string) {
   161  	height, width := dockerCli.Out().GetTtySize()
   162  	// To handle the case where a user repeatedly attaches/detaches without resizing their
   163  	// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
   164  	// resize it, then go back to normal. Without this, every attach after the first will
   165  	// require the user to manually resize or hit enter.
   166  	resizeTtyTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false)
   167  
   168  	// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
   169  	// to the actual size.
   170  	if err := MonitorTtySize(ctx, dockerCli, containerID, false); err != nil {
   171  		logrus.Debugf("Error monitoring TTY size: %s", err)
   172  	}
   173  }