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 }