github.1git.de/docker/cli@v26.1.3+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/cli/cli/command/completion"
    11  	"github.com/docker/docker/api/types"
    12  	"github.com/docker/docker/api/types/container"
    13  	"github.com/docker/docker/client"
    14  	"github.com/moby/sys/signal"
    15  	"github.com/pkg/errors"
    16  	"github.com/sirupsen/logrus"
    17  	"github.com/spf13/cobra"
    18  )
    19  
    20  // AttachOptions group options for `attach` command
    21  type AttachOptions struct {
    22  	NoStdin    bool
    23  	Proxy      bool
    24  	DetachKeys string
    25  }
    26  
    27  func inspectContainerAndCheckState(ctx context.Context, apiClient client.APIClient, args string) (*types.ContainerJSON, error) {
    28  	c, err := apiClient.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  			containerID := args[0]
    55  			return RunAttach(cmd.Context(), dockerCLI, containerID, &opts)
    56  		},
    57  		Annotations: map[string]string{
    58  			"aliases": "docker container attach, docker attach",
    59  		},
    60  		ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr types.Container) bool {
    61  			return ctr.State != "paused"
    62  		}),
    63  	}
    64  
    65  	flags := cmd.Flags()
    66  	flags.BoolVar(&opts.NoStdin, "no-stdin", false, "Do not attach STDIN")
    67  	flags.BoolVar(&opts.Proxy, "sig-proxy", true, "Proxy all received signals to the process")
    68  	flags.StringVar(&opts.DetachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
    69  	return cmd
    70  }
    71  
    72  // RunAttach executes an `attach` command
    73  func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, opts *AttachOptions) error {
    74  	apiClient := dockerCLI.Client()
    75  
    76  	// request channel to wait for client
    77  	resultC, errC := apiClient.ContainerWait(ctx, containerID, "")
    78  
    79  	c, err := inspectContainerAndCheckState(ctx, apiClient, containerID)
    80  	if err != nil {
    81  		return err
    82  	}
    83  
    84  	if err := dockerCLI.In().CheckTty(!opts.NoStdin, c.Config.Tty); err != nil {
    85  		return err
    86  	}
    87  
    88  	detachKeys := dockerCLI.ConfigFile().DetachKeys
    89  	if opts.DetachKeys != "" {
    90  		detachKeys = opts.DetachKeys
    91  	}
    92  
    93  	options := container.AttachOptions{
    94  		Stream:     true,
    95  		Stdin:      !opts.NoStdin && c.Config.OpenStdin,
    96  		Stdout:     true,
    97  		Stderr:     true,
    98  		DetachKeys: detachKeys,
    99  	}
   100  
   101  	var in io.ReadCloser
   102  	if options.Stdin {
   103  		in = dockerCLI.In()
   104  	}
   105  
   106  	if opts.Proxy && !c.Config.Tty {
   107  		sigc := notifyAllSignals()
   108  		go ForwardAllSignals(ctx, apiClient, containerID, sigc)
   109  		defer signal.StopCatch(sigc)
   110  	}
   111  
   112  	resp, errAttach := apiClient.ContainerAttach(ctx, containerID, options)
   113  	if errAttach != nil {
   114  		return errAttach
   115  	}
   116  	defer resp.Close()
   117  
   118  	// If use docker attach command to attach to a stop container, it will return
   119  	// "You cannot attach to a stopped container" error, it's ok, but when
   120  	// attach to a running container, it(docker attach) use inspect to check
   121  	// the container's state, if it pass the state check on the client side,
   122  	// and then the container is stopped, docker attach command still attach to
   123  	// the container and not exit.
   124  	//
   125  	// Recheck the container's state to avoid attach block.
   126  	_, err = inspectContainerAndCheckState(ctx, apiClient, containerID)
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	if c.Config.Tty && dockerCLI.Out().IsTerminal() {
   132  		resizeTTY(ctx, dockerCLI, containerID)
   133  	}
   134  
   135  	streamer := hijackedIOStreamer{
   136  		streams:      dockerCLI,
   137  		inputStream:  in,
   138  		outputStream: dockerCLI.Out(),
   139  		errorStream:  dockerCLI.Err(),
   140  		resp:         resp,
   141  		tty:          c.Config.Tty,
   142  		detachKeys:   options.DetachKeys,
   143  	}
   144  
   145  	if err := streamer.stream(ctx); err != nil {
   146  		return err
   147  	}
   148  
   149  	return getExitStatus(errC, resultC)
   150  }
   151  
   152  func getExitStatus(errC <-chan error, resultC <-chan container.WaitResponse) error {
   153  	select {
   154  	case result := <-resultC:
   155  		if result.Error != nil {
   156  			return fmt.Errorf(result.Error.Message)
   157  		}
   158  		if result.StatusCode != 0 {
   159  			return cli.StatusError{StatusCode: int(result.StatusCode)}
   160  		}
   161  	case err := <-errC:
   162  		return err
   163  	}
   164  
   165  	return nil
   166  }
   167  
   168  func resizeTTY(ctx context.Context, dockerCli command.Cli, containerID string) {
   169  	height, width := dockerCli.Out().GetTtySize()
   170  	// To handle the case where a user repeatedly attaches/detaches without resizing their
   171  	// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
   172  	// resize it, then go back to normal. Without this, every attach after the first will
   173  	// require the user to manually resize or hit enter.
   174  	resizeTtyTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false)
   175  
   176  	// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
   177  	// to the actual size.
   178  	if err := MonitorTtySize(ctx, dockerCli, containerID, false); err != nil {
   179  		logrus.Debugf("Error monitoring TTY size: %s", err)
   180  	}
   181  }