mirror of https://github.com/docker/cli.git
187 lines
5.8 KiB
Go
187 lines
5.8 KiB
Go
package container
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
|
|
"github.com/docker/cli/cli"
|
|
"github.com/docker/cli/cli/command"
|
|
"github.com/docker/cli/cli/command/completion"
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/client"
|
|
"github.com/moby/sys/signal"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// AttachOptions group options for `attach` command
|
|
type AttachOptions struct {
|
|
NoStdin bool
|
|
Proxy bool
|
|
DetachKeys string
|
|
}
|
|
|
|
func inspectContainerAndCheckState(ctx context.Context, apiClient client.APIClient, args string) (*container.InspectResponse, error) {
|
|
c, err := apiClient.ContainerInspect(ctx, args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !c.State.Running {
|
|
return nil, errors.New("You cannot attach to a stopped container, start it first")
|
|
}
|
|
if c.State.Paused {
|
|
return nil, errors.New("You cannot attach to a paused container, unpause it first")
|
|
}
|
|
if c.State.Restarting {
|
|
return nil, errors.New("You cannot attach to a restarting container, wait until it is running")
|
|
}
|
|
|
|
return &c, nil
|
|
}
|
|
|
|
// NewAttachCommand creates a new cobra.Command for `docker attach`
|
|
func NewAttachCommand(dockerCLI command.Cli) *cobra.Command {
|
|
var opts AttachOptions
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "attach [OPTIONS] CONTAINER",
|
|
Short: "Attach local standard input, output, and error streams to a running container",
|
|
Args: cli.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
containerID := args[0]
|
|
return RunAttach(cmd.Context(), dockerCLI, containerID, &opts)
|
|
},
|
|
Annotations: map[string]string{
|
|
"aliases": "docker container attach, docker attach",
|
|
},
|
|
ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr container.Summary) bool {
|
|
return ctr.State != "paused"
|
|
}),
|
|
}
|
|
|
|
flags := cmd.Flags()
|
|
flags.BoolVar(&opts.NoStdin, "no-stdin", false, "Do not attach STDIN")
|
|
flags.BoolVar(&opts.Proxy, "sig-proxy", true, "Proxy all received signals to the process")
|
|
flags.StringVar(&opts.DetachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
|
|
return cmd
|
|
}
|
|
|
|
// RunAttach executes an `attach` command
|
|
func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, opts *AttachOptions) error {
|
|
apiClient := dockerCLI.Client()
|
|
|
|
// request channel to wait for client
|
|
waitCtx := context.WithoutCancel(ctx)
|
|
resultC, errC := apiClient.ContainerWait(waitCtx, containerID, "")
|
|
|
|
c, err := inspectContainerAndCheckState(ctx, apiClient, containerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := dockerCLI.In().CheckTty(!opts.NoStdin, c.Config.Tty); err != nil {
|
|
return err
|
|
}
|
|
|
|
detachKeys := dockerCLI.ConfigFile().DetachKeys
|
|
if opts.DetachKeys != "" {
|
|
detachKeys = opts.DetachKeys
|
|
}
|
|
|
|
options := container.AttachOptions{
|
|
Stream: true,
|
|
Stdin: !opts.NoStdin && c.Config.OpenStdin,
|
|
Stdout: true,
|
|
Stderr: true,
|
|
DetachKeys: detachKeys,
|
|
}
|
|
|
|
var in io.ReadCloser
|
|
if options.Stdin {
|
|
in = dockerCLI.In()
|
|
}
|
|
|
|
if opts.Proxy && !c.Config.Tty {
|
|
sigc := notifyAllSignals()
|
|
// since we're explicitly setting up signal handling here, and the daemon will
|
|
// get notified independently of the clients ctx cancellation, we use this context
|
|
// but without cancellation to avoid ForwardAllSignals from returning
|
|
// before all signals are forwarded.
|
|
bgCtx := context.WithoutCancel(ctx)
|
|
go ForwardAllSignals(bgCtx, apiClient, containerID, sigc)
|
|
defer signal.StopCatch(sigc)
|
|
}
|
|
|
|
resp, errAttach := apiClient.ContainerAttach(ctx, containerID, options)
|
|
if errAttach != nil {
|
|
return errAttach
|
|
}
|
|
defer resp.Close()
|
|
|
|
// If use docker attach command to attach to a stop container, it will return
|
|
// "You cannot attach to a stopped container" error, it's ok, but when
|
|
// attach to a running container, it(docker attach) use inspect to check
|
|
// the container's state, if it pass the state check on the client side,
|
|
// and then the container is stopped, docker attach command still attach to
|
|
// the container and not exit.
|
|
//
|
|
// Recheck the container's state to avoid attach block.
|
|
_, err = inspectContainerAndCheckState(ctx, apiClient, containerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.Config.Tty && dockerCLI.Out().IsTerminal() {
|
|
resizeTTY(ctx, dockerCLI, containerID)
|
|
}
|
|
|
|
streamer := hijackedIOStreamer{
|
|
streams: dockerCLI,
|
|
inputStream: in,
|
|
outputStream: dockerCLI.Out(),
|
|
errorStream: dockerCLI.Err(),
|
|
resp: resp,
|
|
tty: c.Config.Tty,
|
|
detachKeys: options.DetachKeys,
|
|
}
|
|
|
|
// if the context was canceled, this was likely intentional and we shouldn't return an error
|
|
if err := streamer.stream(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
|
return err
|
|
}
|
|
|
|
return getExitStatus(errC, resultC)
|
|
}
|
|
|
|
func getExitStatus(errC <-chan error, resultC <-chan container.WaitResponse) error {
|
|
select {
|
|
case result := <-resultC:
|
|
if result.Error != nil {
|
|
return errors.New(result.Error.Message)
|
|
}
|
|
if result.StatusCode != 0 {
|
|
return cli.StatusError{StatusCode: int(result.StatusCode)}
|
|
}
|
|
case err := <-errC:
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func resizeTTY(ctx context.Context, dockerCli command.Cli, containerID string) {
|
|
height, width := dockerCli.Out().GetTtySize()
|
|
// To handle the case where a user repeatedly attaches/detaches without resizing their
|
|
// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
|
|
// resize it, then go back to normal. Without this, every attach after the first will
|
|
// require the user to manually resize or hit enter.
|
|
resizeTtyTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false)
|
|
|
|
// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
|
|
// to the actual size.
|
|
if err := MonitorTtySize(ctx, dockerCli, containerID, false); err != nil {
|
|
logrus.Debugf("Error monitoring TTY size: %s", err)
|
|
}
|
|
}
|