github.com/xeptore/docker-cli@v20.10.14+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 := notfiyAllSignals()
   101  		go ForwardAllSignals(ctx, dockerCli, opts.container, sigc)
   102  		defer signal.StopCatch(sigc)
   103  	}
   104  
   105  	resp, errAttach := client.ContainerAttach(ctx, opts.container, options)
   106  	if errAttach != nil {
   107  		return errAttach
   108  	}
   109  	defer resp.Close()
   110  
   111  	// If use docker attach command to attach to a stop container, it will return
   112  	// "You cannot attach to a stopped container" error, it's ok, but when
   113  	// attach to a running container, it(docker attach) use inspect to check
   114  	// the container's state, if it pass the state check on the client side,
   115  	// and then the container is stopped, docker attach command still attach to
   116  	// the container and not exit.
   117  	//
   118  	// Recheck the container's state to avoid attach block.
   119  	_, err = inspectContainerAndCheckState(ctx, client, opts.container)
   120  	if err != nil {
   121  		return err
   122  	}
   123  
   124  	if c.Config.Tty && dockerCli.Out().IsTerminal() {
   125  		resizeTTY(ctx, dockerCli, opts.container)
   126  	}
   127  
   128  	streamer := hijackedIOStreamer{
   129  		streams:      dockerCli,
   130  		inputStream:  in,
   131  		outputStream: dockerCli.Out(),
   132  		errorStream:  dockerCli.Err(),
   133  		resp:         resp,
   134  		tty:          c.Config.Tty,
   135  		detachKeys:   options.DetachKeys,
   136  	}
   137  
   138  	if err := streamer.stream(ctx); err != nil {
   139  		return err
   140  	}
   141  
   142  	return getExitStatus(errC, resultC)
   143  }
   144  
   145  func getExitStatus(errC <-chan error, resultC <-chan container.ContainerWaitOKBody) error {
   146  	select {
   147  	case result := <-resultC:
   148  		if result.Error != nil {
   149  			return fmt.Errorf(result.Error.Message)
   150  		}
   151  		if result.StatusCode != 0 {
   152  			return cli.StatusError{StatusCode: int(result.StatusCode)}
   153  		}
   154  	case err := <-errC:
   155  		return err
   156  	}
   157  
   158  	return nil
   159  }
   160  
   161  func resizeTTY(ctx context.Context, dockerCli command.Cli, containerID string) {
   162  	height, width := dockerCli.Out().GetTtySize()
   163  	// To handle the case where a user repeatedly attaches/detaches without resizing their
   164  	// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
   165  	// resize it, then go back to normal. Without this, every attach after the first will
   166  	// require the user to manually resize or hit enter.
   167  	resizeTtyTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false)
   168  
   169  	// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
   170  	// to the actual size.
   171  	if err := MonitorTtySize(ctx, dockerCli, containerID, false); err != nil {
   172  		logrus.Debugf("Error monitoring TTY size: %s", err)
   173  	}
   174  }