This replaces the visitAll recursive function with a test that verifies that the option is set for all commands and subcommands, so that it doesn't have to be modified at runtime. We currently still have to loop over all functions for the setValidateArgs call, but that can be looked at separately. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
188 lines
5.8 KiB
Go
188 lines
5.8 KiB
Go
package container
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
|
|
"github.com/docker/cli/cli"
|
|
"github.com/docker/cli/cli/command"
|
|
"github.com/docker/cli/cli/command/completion"
|
|
"github.com/moby/moby/api/types/container"
|
|
"github.com/moby/moby/client"
|
|
"github.com/moby/sys/signal"
|
|
"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("cannot attach to a stopped container, start it first")
|
|
}
|
|
if c.State.Paused {
|
|
return nil, errors.New("cannot attach to a paused container, unpause it first")
|
|
}
|
|
if c.State.Restarting {
|
|
return nil, errors.New("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 != container.StatePaused
|
|
}),
|
|
DisableFlagsInUseLine: true,
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|