Jelajahi Sumber

Merge pull request #15666 from vdemeester/3519-configurable-escape

Implement configurable escape key for attach/exec
Sebastiaan van Stijn 9 tahun lalu
induk
melakukan
db738dd77f

+ 6 - 0
api/client/attach.go

@@ -18,6 +18,7 @@ func (cli *DockerCli) CmdAttach(args ...string) error {
 	cmd := Cli.Subcmd("attach", []string{"CONTAINER"}, Cli.DockerCommands["attach"].Description, true)
 	noStdin := cmd.Bool([]string{"-no-stdin"}, false, "Do not attach STDIN")
 	proxy := cmd.Bool([]string{"-sig-proxy"}, true, "Proxy all received signals to the process")
+	detachKeys := cmd.String([]string{"-detach-keys"}, "", "Override the key sequence for detaching a container")
 
 	cmd.Require(flag.Exact, 1)
 
@@ -46,12 +47,17 @@ func (cli *DockerCli) CmdAttach(args ...string) error {
 		}
 	}
 
+	if *detachKeys != "" {
+		cli.configFile.DetachKeys = *detachKeys
+	}
+
 	options := types.ContainerAttachOptions{
 		ContainerID: cmd.Arg(0),
 		Stream:      true,
 		Stdin:       !*noStdin && c.Config.OpenStdin,
 		Stdout:      true,
 		Stderr:      true,
+		DetachKeys:  cli.configFile.DetachKeys,
 	}
 
 	var in io.ReadCloser

+ 8 - 0
api/client/exec.go

@@ -16,6 +16,7 @@ import (
 // Usage: docker exec [OPTIONS] CONTAINER COMMAND [ARG...]
 func (cli *DockerCli) CmdExec(args ...string) error {
 	cmd := Cli.Subcmd("exec", []string{"CONTAINER COMMAND [ARG...]"}, Cli.DockerCommands["exec"].Description, true)
+	detachKeys := cmd.String([]string{"-detach-keys"}, "", "Override the key sequence for detaching a container")
 
 	execConfig, err := runconfig.ParseExec(cmd, args)
 	// just in case the ParseExec does not exit
@@ -23,6 +24,13 @@ func (cli *DockerCli) CmdExec(args ...string) error {
 		return Cli.StatusError{StatusCode: 1}
 	}
 
+	if *detachKeys != "" {
+		cli.configFile.DetachKeys = *detachKeys
+	}
+
+	// Send client escape keys
+	execConfig.DetachKeys = cli.configFile.DetachKeys
+
 	response, err := cli.client.ContainerExecCreate(*execConfig)
 	if err != nil {
 		return err

+ 3 - 0
api/client/lib/container_attach.go

@@ -24,6 +24,9 @@ func (cli *Client) ContainerAttach(options types.ContainerAttachOptions) (types.
 	if options.Stderr {
 		query.Set("stderr", "1")
 	}
+	if options.DetachKeys != "" {
+		query.Set("detachKeys", options.DetachKeys)
+	}
 
 	headers := map[string][]string{"Content-Type": {"text/plain"}}
 	return cli.postHijacked("/containers/"+options.ContainerID+"/attach", query, nil, headers)

+ 6 - 0
api/client/run.go

@@ -74,6 +74,7 @@ func (cli *DockerCli) CmdRun(args ...string) error {
 		flDetach     = cmd.Bool([]string{"d", "-detach"}, false, "Run container in background and print container ID")
 		flSigProxy   = cmd.Bool([]string{"-sig-proxy"}, true, "Proxy received signals to the process")
 		flName       = cmd.String([]string{"-name"}, "", "Assign a name to the container")
+		flDetachKeys = cmd.String([]string{"-detach-keys"}, "", "Override the key sequence for detaching a container")
 		flAttach     *opts.ListOpts
 
 		ErrConflictAttachDetach               = fmt.Errorf("Conflicting options: -a and -d")
@@ -188,12 +189,17 @@ func (cli *DockerCli) CmdRun(args ...string) error {
 			}
 		}
 
+		if *flDetachKeys != "" {
+			cli.configFile.DetachKeys = *flDetachKeys
+		}
+
 		options := types.ContainerAttachOptions{
 			ContainerID: createResponse.ID,
 			Stream:      true,
 			Stdin:       config.AttachStdin,
 			Stdout:      config.AttachStdout,
 			Stderr:      config.AttachStderr,
+			DetachKeys:  cli.configFile.DetachKeys,
 		}
 
 		resp, err := cli.client.ContainerAttach(options)

+ 6 - 0
api/client/start.go

@@ -49,6 +49,7 @@ func (cli *DockerCli) CmdStart(args ...string) error {
 	cmd := Cli.Subcmd("start", []string{"CONTAINER [CONTAINER...]"}, Cli.DockerCommands["start"].Description, true)
 	attach := cmd.Bool([]string{"a", "-attach"}, false, "Attach STDOUT/STDERR and forward signals")
 	openStdin := cmd.Bool([]string{"i", "-interactive"}, false, "Attach container's STDIN")
+	detachKeys := cmd.String([]string{"-detach-keys"}, "", "Override the key sequence for detaching a container")
 	cmd.Require(flag.Min, 1)
 
 	cmd.ParseFlags(args, true)
@@ -72,12 +73,17 @@ func (cli *DockerCli) CmdStart(args ...string) error {
 			defer signal.StopCatch(sigc)
 		}
 
+		if *detachKeys != "" {
+			cli.configFile.DetachKeys = *detachKeys
+		}
+
 		options := types.ContainerAttachOptions{
 			ContainerID: containerID,
 			Stream:      true,
 			Stdin:       *openStdin && c.Config.OpenStdin,
 			Stdout:      true,
 			Stderr:      true,
+			DetachKeys:  cli.configFile.DetachKeys,
 		}
 
 		var in io.ReadCloser

+ 36 - 13
api/server/router/container/container_routes.go

@@ -19,6 +19,7 @@ import (
 	derr "github.com/docker/docker/errors"
 	"github.com/docker/docker/pkg/ioutils"
 	"github.com/docker/docker/pkg/signal"
+	"github.com/docker/docker/pkg/term"
 	"github.com/docker/docker/runconfig"
 	"github.com/docker/docker/utils"
 	"golang.org/x/net/context"
@@ -420,21 +421,32 @@ func (s *containerRouter) postContainersResize(ctx context.Context, w http.Respo
 }
 
 func (s *containerRouter) postContainersAttach(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
-	if err := httputils.ParseForm(r); err != nil {
+	err := httputils.ParseForm(r)
+	if err != nil {
 		return err
 	}
 	containerName := vars["name"]
 
 	_, upgrade := r.Header["Upgrade"]
 
+	keys := []byte{}
+	detachKeys := r.FormValue("detachKeys")
+	if detachKeys != "" {
+		keys, err = term.ToBytes(detachKeys)
+		if err != nil {
+			logrus.Warnf("Invalid escape keys provided (%s) using default : ctrl-p ctrl-q", detachKeys)
+		}
+	}
+
 	attachWithLogsConfig := &daemon.ContainerAttachWithLogsConfig{
-		Hijacker:  w.(http.Hijacker),
-		Upgrade:   upgrade,
-		UseStdin:  httputils.BoolValue(r, "stdin"),
-		UseStdout: httputils.BoolValue(r, "stdout"),
-		UseStderr: httputils.BoolValue(r, "stderr"),
-		Logs:      httputils.BoolValue(r, "logs"),
-		Stream:    httputils.BoolValue(r, "stream"),
+		Hijacker:   w.(http.Hijacker),
+		Upgrade:    upgrade,
+		UseStdin:   httputils.BoolValue(r, "stdin"),
+		UseStdout:  httputils.BoolValue(r, "stdout"),
+		UseStderr:  httputils.BoolValue(r, "stderr"),
+		Logs:       httputils.BoolValue(r, "logs"),
+		Stream:     httputils.BoolValue(r, "stream"),
+		DetachKeys: keys,
 	}
 
 	return s.backend.ContainerAttachWithLogs(containerName, attachWithLogsConfig)
@@ -450,15 +462,26 @@ func (s *containerRouter) wsContainersAttach(ctx context.Context, w http.Respons
 		return derr.ErrorCodeNoSuchContainer.WithArgs(containerName)
 	}
 
+	var keys []byte
+	var err error
+	detachKeys := r.FormValue("detachKeys")
+	if detachKeys != "" {
+		keys, err = term.ToBytes(detachKeys)
+		if err != nil {
+			logrus.Warnf("Invalid escape keys provided (%s) using default : ctrl-p ctrl-q", detachKeys)
+		}
+	}
+
 	h := websocket.Handler(func(ws *websocket.Conn) {
 		defer ws.Close()
 
 		wsAttachWithLogsConfig := &daemon.ContainerWsAttachWithLogsConfig{
-			InStream:  ws,
-			OutStream: ws,
-			ErrStream: ws,
-			Logs:      httputils.BoolValue(r, "logs"),
-			Stream:    httputils.BoolValue(r, "stream"),
+			InStream:   ws,
+			OutStream:  ws,
+			ErrStream:  ws,
+			Logs:       httputils.BoolValue(r, "logs"),
+			Stream:     httputils.BoolValue(r, "stream"),
+			DetachKeys: keys,
 		}
 
 		if err := s.backend.ContainerWsAttachWithLogs(containerName, wsAttachWithLogsConfig); err != nil {

+ 1 - 0
api/types/client.go

@@ -17,6 +17,7 @@ type ContainerAttachOptions struct {
 	Stdin       bool
 	Stdout      bool
 	Stderr      bool
+	DetachKeys  string
 }
 
 // ContainerCommitOptions holds parameters to commit changes into a container.

+ 1 - 0
api/types/configs.go

@@ -45,5 +45,6 @@ type ExecConfig struct {
 	AttachStderr bool     // Attach the standard output
 	AttachStdout bool     // Attach the standard error
 	Detach       bool     // Execute in detach mode
+	DetachKeys   string   // Escape keys for detach
 	Cmd          []string // Execution commands and args
 }

+ 1 - 0
cliconfig/config.go

@@ -51,6 +51,7 @@ type ConfigFile struct {
 	HTTPHeaders  map[string]string           `json:"HttpHeaders,omitempty"`
 	PsFormat     string                      `json:"psFormat,omitempty"`
 	ImagesFormat string                      `json:"imagesFormat,omitempty"`
+	DetachKeys   string                      `json:"detachKeys,omitempty"`
 	filename     string                      // Note: not serialized - for internal use only
 }
 

+ 15 - 10
container/container.go

@@ -329,13 +329,13 @@ func (container *Container) GetExecIDs() []string {
 
 // Attach connects to the container's TTY, delegating to standard
 // streams or websockets depending on the configuration.
-func (container *Container) Attach(stdin io.ReadCloser, stdout io.Writer, stderr io.Writer) chan error {
-	return AttachStreams(container.StreamConfig, container.Config.OpenStdin, container.Config.StdinOnce, container.Config.Tty, stdin, stdout, stderr)
+func (container *Container) Attach(stdin io.ReadCloser, stdout io.Writer, stderr io.Writer, keys []byte) chan error {
+	return AttachStreams(container.StreamConfig, container.Config.OpenStdin, container.Config.StdinOnce, container.Config.Tty, stdin, stdout, stderr, keys)
 }
 
 // AttachStreams connects streams to a TTY.
 // Used by exec too. Should this move somewhere else?
-func AttachStreams(streamConfig *runconfig.StreamConfig, openStdin, stdinOnce, tty bool, stdin io.ReadCloser, stdout io.Writer, stderr io.Writer) chan error {
+func AttachStreams(streamConfig *runconfig.StreamConfig, openStdin, stdinOnce, tty bool, stdin io.ReadCloser, stdout io.Writer, stderr io.Writer, keys []byte) chan error {
 	var (
 		cStdout, cStderr io.ReadCloser
 		cStdin           io.WriteCloser
@@ -382,7 +382,7 @@ func AttachStreams(streamConfig *runconfig.StreamConfig, openStdin, stdinOnce, t
 
 		var err error
 		if tty {
-			_, err = copyEscapable(cStdin, stdin)
+			_, err = copyEscapable(cStdin, stdin, keys)
 		} else {
 			_, err = io.Copy(cStdin, stdin)
 
@@ -438,22 +438,27 @@ func AttachStreams(streamConfig *runconfig.StreamConfig, openStdin, stdinOnce, t
 }
 
 // Code c/c from io.Copy() modified to handle escape sequence
-func copyEscapable(dst io.Writer, src io.ReadCloser) (written int64, err error) {
+func copyEscapable(dst io.Writer, src io.ReadCloser, keys []byte) (written int64, err error) {
+	if len(keys) == 0 {
+		// Default keys : ctrl-p ctrl-q
+		keys = []byte{16, 17}
+	}
 	buf := make([]byte, 32*1024)
 	for {
 		nr, er := src.Read(buf)
 		if nr > 0 {
 			// ---- Docker addition
-			// char 16 is C-p
-			if nr == 1 && buf[0] == 16 {
-				nr, er = src.Read(buf)
-				// char 17 is C-q
-				if nr == 1 && buf[0] == 17 {
+			for i, key := range keys {
+				if nr != 1 || buf[0] != key {
+					break
+				}
+				if i == len(keys)-1 {
 					if err := src.Close(); err != nil {
 						return 0, err
 					}
 					return 0, nil
 				}
+				nr, er = src.Read(buf)
 			}
 			// ---- End of docker
 			nw, ew := dst.Write(buf[0:nr])

+ 13 - 11
daemon/attach.go

@@ -15,13 +15,14 @@ import (
 
 // ContainerAttachWithLogsConfig holds the streams to use when connecting to a container to view logs.
 type ContainerAttachWithLogsConfig struct {
-	Hijacker  http.Hijacker
-	Upgrade   bool
-	UseStdin  bool
-	UseStdout bool
-	UseStderr bool
-	Logs      bool
-	Stream    bool
+	Hijacker   http.Hijacker
+	Upgrade    bool
+	UseStdin   bool
+	UseStdout  bool
+	UseStderr  bool
+	Logs       bool
+	Stream     bool
+	DetachKeys []byte
 }
 
 // ContainerAttachWithLogs attaches to logs according to the config passed in. See ContainerAttachWithLogsConfig.
@@ -75,7 +76,7 @@ func (daemon *Daemon) ContainerAttachWithLogs(prefixOrName string, c *ContainerA
 		stderr = errStream
 	}
 
-	if err := daemon.attachWithLogs(container, stdin, stdout, stderr, c.Logs, c.Stream); err != nil {
+	if err := daemon.attachWithLogs(container, stdin, stdout, stderr, c.Logs, c.Stream, c.DetachKeys); err != nil {
 		fmt.Fprintf(outStream, "Error attaching: %s\n", err)
 	}
 	return nil
@@ -87,6 +88,7 @@ type ContainerWsAttachWithLogsConfig struct {
 	InStream             io.ReadCloser
 	OutStream, ErrStream io.Writer
 	Logs, Stream         bool
+	DetachKeys           []byte
 }
 
 // ContainerWsAttachWithLogs websocket connection
@@ -95,10 +97,10 @@ func (daemon *Daemon) ContainerWsAttachWithLogs(prefixOrName string, c *Containe
 	if err != nil {
 		return err
 	}
-	return daemon.attachWithLogs(container, c.InStream, c.OutStream, c.ErrStream, c.Logs, c.Stream)
+	return daemon.attachWithLogs(container, c.InStream, c.OutStream, c.ErrStream, c.Logs, c.Stream, c.DetachKeys)
 }
 
-func (daemon *Daemon) attachWithLogs(container *container.Container, stdin io.ReadCloser, stdout, stderr io.Writer, logs, stream bool) error {
+func (daemon *Daemon) attachWithLogs(container *container.Container, stdin io.ReadCloser, stdout, stderr io.Writer, logs, stream bool, keys []byte) error {
 	if logs {
 		logDriver, err := daemon.getLogger(container)
 		if err != nil {
@@ -144,7 +146,7 @@ func (daemon *Daemon) attachWithLogs(container *container.Container, stdin io.Re
 			}()
 			stdinPipe = r
 		}
-		<-container.Attach(stdinPipe, stdout, stderr)
+		<-container.Attach(stdinPipe, stdout, stderr, keys)
 		// If we are in stdinonce mode, wait for the process to end
 		// otherwise, simply return
 		if container.Config.StdinOnce && !container.Config.Tty {

+ 12 - 1
daemon/exec.go

@@ -14,6 +14,7 @@ import (
 	derr "github.com/docker/docker/errors"
 	"github.com/docker/docker/pkg/pools"
 	"github.com/docker/docker/pkg/promise"
+	"github.com/docker/docker/pkg/term"
 )
 
 func (d *Daemon) registerExecCommand(container *container.Container, config *exec.Config) {
@@ -88,6 +89,14 @@ func (d *Daemon) ContainerExecCreate(config *types.ExecConfig) (string, error) {
 	cmd := strslice.New(config.Cmd...)
 	entrypoint, args := d.getEntrypointAndArgs(strslice.New(), cmd)
 
+	keys := []byte{}
+	if config.DetachKeys != "" {
+		keys, err = term.ToBytes(config.DetachKeys)
+		if err != nil {
+			logrus.Warnf("Wrong escape keys provided (%s, error: %s) using default : ctrl-p ctrl-q", config.DetachKeys, err.Error())
+		}
+	}
+
 	processConfig := &execdriver.ProcessConfig{
 		CommonProcessConfig: execdriver.CommonProcessConfig{
 			Tty:        config.Tty,
@@ -103,6 +112,7 @@ func (d *Daemon) ContainerExecCreate(config *types.ExecConfig) (string, error) {
 	execConfig.OpenStderr = config.AttachStderr
 	execConfig.ProcessConfig = processConfig
 	execConfig.ContainerID = container.ID
+	execConfig.DetachKeys = keys
 
 	d.registerExecCommand(container, execConfig)
 
@@ -158,7 +168,8 @@ func (d *Daemon) ContainerExecStart(name string, stdin io.ReadCloser, stdout io.
 		ec.NewNopInputPipe()
 	}
 
-	attachErr := container.AttachStreams(ec.StreamConfig, ec.OpenStdin, true, ec.ProcessConfig.Tty, cStdin, cStdout, cStderr)
+	attachErr := container.AttachStreams(ec.StreamConfig, ec.OpenStdin, true, ec.ProcessConfig.Tty, cStdin, cStdout, cStderr, ec.DetachKeys)
+
 	execErr := make(chan error)
 
 	// Note, the ExecConfig data will be removed when the container

+ 1 - 0
daemon/exec/exec.go

@@ -25,6 +25,7 @@ type Config struct {
 	OpenStdout    bool
 	CanRemove     bool
 	ContainerID   string
+	DetachKeys    []byte
 
 	// waitStart will be closed immediately after the exec is really started.
 	waitStart chan struct{}

+ 18 - 3
docs/reference/api/docker_remote_api_v1.22.md

@@ -862,10 +862,9 @@ This endpoint returns a live stream of a container's resource usage statistics.
                "total_usage" : 36488948,
                "usage_in_kernelmode" : 20000000
             },
-            "system_cpu_usage" : 20091722000000000,
+            "system_cpu_usage" : 20091722000000000,
             "throttling_data" : {}
-         }
-      }
+         }      }
 
 Query Parameters:
 
@@ -922,6 +921,12 @@ Start the container `id`
 
     HTTP/1.1 204 No Content
 
+Query Parameters:
+
+-   **detacheys** – Override the key sequence for detaching a
+        container. Format is a single character `[a-Z]` or `ctrl-<value>`
+        where `<value>` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`.
+
 Status Codes:
 
 -   **204** – no error
@@ -1133,6 +1138,9 @@ Attach to the container `id`
 
 Query Parameters:
 
+-   **detacheys** – Override the key sequence for detaching a
+        container. Format is a single character `[a-Z]` or `ctrl-<value>`
+        where `<value>` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`.
 -   **logs** – 1/True/true or 0/False/false, return logs. Default `false`.
 -   **stream** – 1/True/true or 0/False/false, return stream.
         Default `false`.
@@ -1213,6 +1221,9 @@ Implements websocket protocol handshake according to [RFC 6455](http://tools.iet
 
 Query Parameters:
 
+-   **detacheys** – Override the key sequence for detaching a
+        container. Format is a single character `[a-Z]` or `ctrl-<value>`
+        where `<value>` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`.
 -   **logs** – 1/True/true or 0/False/false, return logs. Default `false`.
 -   **stream** – 1/True/true or 0/False/false, return stream.
         Default `false`.
@@ -2420,6 +2431,7 @@ Sets up an exec instance in a running container `id`
        "AttachStdin": false,
        "AttachStdout": true,
        "AttachStderr": true,
+       "DetachKeys": "ctrl-p,ctrl-q",
        "Tty": false,
        "Cmd": [
                      "date"
@@ -2441,6 +2453,9 @@ Json Parameters:
 -   **AttachStdin** - Boolean value, attaches to `stdin` of the `exec` command.
 -   **AttachStdout** - Boolean value, attaches to `stdout` of the `exec` command.
 -   **AttachStderr** - Boolean value, attaches to `stderr` of the `exec` command.
+-   **Detacheys** – Override the key sequence for detaching a
+        container. Format is a single character `[a-Z]` or `ctrl-<value>`
+        where `<value>` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`.
 -   **Tty** - Boolean value to allocate a pseudo-TTY.
 -   **Cmd** - Command to run specified as a string or an array of strings.
 

+ 33 - 8
docs/reference/commandline/attach.md

@@ -14,9 +14,10 @@ parent = "smn_cli"
 
     Attach to a running container
 
-      --help              Print usage
-      --no-stdin          Do not attach STDIN
-      --sig-proxy=true    Proxy all received signals to the process
+      --detach-keys="<sequence>"       Set up escape key sequence
+      --help                           Print usage
+      --no-stdin                       Do not attach STDIN
+      --sig-proxy=true                 Proxy all received signals to the process
 
 The `docker attach` command allows you to attach to a running container using
 the container's ID or name, either to view its ongoing output or to control it
@@ -24,11 +25,10 @@ interactively. You can attach to the same contained process multiple times
 simultaneously, screen sharing style, or quickly view the progress of your
 detached  process.
 
-You can detach from the container and leave it running with `CTRL-p CTRL-q`
-(for a quiet exit) or with `CTRL-c` if `--sig-proxy` is false.
-
-If `--sig-proxy` is true (the default),`CTRL-c` sends a `SIGINT` to the
-container.
+To stop a container, use `CTRL-c`. This key sequence sends `SIGKILL` to the
+container. If `--sig-proxy` is true (the default),`CTRL-c` sends a `SIGINT` to
+the container. You can detach from a container and leave it running using the
+using `CTRL-p CTRL-q` key sequence.
 
 > **Note:**
 > A process running as PID 1 inside a container is treated specially by
@@ -39,6 +39,31 @@ container.
 It is forbidden to redirect the standard input of a `docker attach` command
 while attaching to a tty-enabled container (i.e.: launched with `-t`).
 
+
+## Override the detach sequence
+
+If you want, you can configure a override the Docker key sequence for detach.
+This is is useful if the Docker default sequence conflicts with key squence you
+use for other applications. There are two ways to defines a your own detach key
+sequence, as a per-container override or as a configuration property on  your
+entire configuration.
+
+To override the sequence for an individual container, use the
+`--detach-keys="<sequence>"` flag with the `docker attach` command. The format of
+the `<sequence>` is either a letter [a-Z], or the `ctrl-` combined with any of
+the following:
+
+* `a-z` (a single lowercase alpha character )
+* `@` (ampersand)
+* `[` (left bracket)
+* `\\` (two backward slashes)
+*  `_` (underscore)
+* `^` (caret)
+
+These `a`, `ctrl-a`, `X`, or `ctrl-\\` values are all examples of valid key
+sequences. To configure a different configuration default key sequence for all
+containers, see [**Configuration file** section](cli.md#configuration-files).
+
 #### Examples
 
     $ docker run -d --name topdemo ubuntu /usr/bin/top -b

+ 23 - 3
docs/reference/commandline/cli.md

@@ -101,7 +101,26 @@ The property `psFormat` specifies the default format for `docker ps` output.
 When the `--format` flag is not provided with the `docker ps` command,
 Docker's client uses this property. If this property is not set, the client
 falls back to the default table format. For a list of supported formatting
-directives, see the [**Formatting** section in the `docker ps` documentation](ps.md)
+directives, see the
+[**Formatting** section in the `docker ps` documentation](ps.md)
+
+Once attached to a container, users detach from it and leave it running using
+the using `CTRL-p CTRL-q` key sequence. This detach key sequence is customizable
+using the `detachKeys` property. Specify a `<sequence>` value for the
+property. The format of the `<sequence>` is either a letter [a-Z], or the `ctrl-`
+combined with any of the following:
+
+* `a-z` (a single lowercase alpha character )
+* `@` (ampersand)
+* `[` (left bracket)
+* `\\` (two backward slashes)
+*  `_` (underscore)
+* `^` (caret)
+
+Your customization applies to all containers started in with your Docker client.
+Users can override your custom or the default key sequence on a per-container
+basis. To do this, the user specifies the `--detach-keys` flag with the `docker
+attach`, `docker exec`, `docker run` or `docker start` command.
 
 The property `imagesFormat` specifies the default format for `docker images` output.
 When the `--format` flag is not provided with the `docker images` command,
@@ -115,8 +134,9 @@ Following is a sample `config.json` file:
       "HttpHeaders": {
         "MyHeader": "MyValue"
       },
-      "psFormat": "table {{.ID}}\\t{{.Image}}\\t{{.Command}}\\t{{.Labels}}"
-      "imagesFormat": "table {{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}"
+      "psFormat": "table {{.ID}}\\t{{.Image}}\\t{{.Command}}\\t{{.Labels}}",
+      "imagesFormat": "table {{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}",
+      "detachKeys": "ctrl-e,e"
     }
 
 ### Notary

+ 1 - 0
docs/reference/commandline/exec.md

@@ -15,6 +15,7 @@ parent = "smn_cli"
     Run a command in a running container
 
       -d, --detach               Detached mode: run command in the background
+      --detach-keys              Specify the escape key sequence used to detach a container
       --help                     Print usage
       -i, --interactive          Keep STDIN open even if not attached
       --privileged               Give extended Linux capabilities to the command

+ 1 - 0
docs/reference/commandline/run.md

@@ -28,6 +28,7 @@ parent = "smn_cli"
       --cpuset-cpus=""              CPUs in which to allow execution (0-3, 0,1)
       --cpuset-mems=""              Memory nodes (MEMs) in which to allow execution (0-3, 0,1)
       -d, --detach                  Run container in background and print container ID
+      --detach-keys                 Specify the escape key sequence used to detach a container
       --device=[]                   Add a host device to the container
       --device-read-bps=[]          Limit read rate (bytes per second) from a device (e.g., --device-read-bps=/dev/sda:1mb)
       --device-read-iops=[]         Limit read rate (IO per second) from a device (e.g., --device-read-iops=/dev/sda:1000)

+ 1 - 0
docs/reference/commandline/start.md

@@ -15,5 +15,6 @@ parent = "smn_cli"
     Start one or more containers
 
       -a, --attach               Attach STDOUT/STDERR and forward signals
+      --detach-keys              Specify the escape key sequence used to detach a container
       --help                     Print usage
       -i, --interactive          Attach container's STDIN

+ 235 - 4
integration-cli/docker_cli_run_unix_test.go

@@ -14,6 +14,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/docker/docker/pkg/homedir"
 	"github.com/docker/docker/pkg/integration/checker"
 	"github.com/docker/docker/pkg/mount"
 	"github.com/docker/docker/pkg/parsers"
@@ -87,10 +88,13 @@ func (s *DockerSuite) TestRunDeviceDirectory(c *check.C) {
 	c.Assert(strings.Trim(out, "\r\n"), checker.Contains, "seq", check.Commentf("expected output /dev/othersnd/seq"))
 }
 
-// TestRunDetach checks attaching and detaching with the escape sequence.
+// TestRunDetach checks attaching and detaching with the default escape sequence.
 func (s *DockerSuite) TestRunAttachDetach(c *check.C) {
 	name := "attach-detach"
-	cmd := exec.Command(dockerBinary, "run", "--name", name, "-it", "busybox", "cat")
+
+	dockerCmd(c, "run", "--name", name, "-itd", "busybox", "cat")
+
+	cmd := exec.Command(dockerBinary, "attach", name)
 	stdout, err := cmd.StdoutPipe()
 	c.Assert(err, checker.IsNil)
 	cpty, tty, err := pty.Open()
@@ -120,21 +124,248 @@ func (s *DockerSuite) TestRunAttachDetach(c *check.C) {
 		ch <- struct{}{}
 	}()
 
+	select {
+	case <-ch:
+	case <-time.After(10 * time.Second):
+		c.Fatal("timed out waiting for container to exit")
+	}
+
 	running, err := inspectField(name, "State.Running")
 	c.Assert(err, checker.IsNil)
 	c.Assert(running, checker.Equals, "true", check.Commentf("expected container to still be running"))
+}
+
+// TestRunDetach checks attaching and detaching with the escape sequence specified via flags.
+func (s *DockerSuite) TestRunAttachDetachFromFlag(c *check.C) {
+	name := "attach-detach"
+	keyCtrlA := []byte{1}
+	keyA := []byte{97}
+
+	dockerCmd(c, "run", "--name", name, "-itd", "busybox", "cat")
+
+	cmd := exec.Command(dockerBinary, "attach", "--detach-keys='ctrl-a,a'", name)
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		c.Fatal(err)
+	}
+	cpty, tty, err := pty.Open()
+	if err != nil {
+		c.Fatal(err)
+	}
+	defer cpty.Close()
+	cmd.Stdin = tty
+	if err := cmd.Start(); err != nil {
+		c.Fatal(err)
+	}
+	c.Assert(waitRun(name), check.IsNil)
+
+	if _, err := cpty.Write([]byte("hello\n")); err != nil {
+		c.Fatal(err)
+	}
+
+	out, err := bufio.NewReader(stdout).ReadString('\n')
+	if err != nil {
+		c.Fatal(err)
+	}
+	if strings.TrimSpace(out) != "hello" {
+		c.Fatalf("expected 'hello', got %q", out)
+	}
+
+	// escape sequence
+	if _, err := cpty.Write(keyCtrlA); err != nil {
+		c.Fatal(err)
+	}
+	time.Sleep(100 * time.Millisecond)
+	if _, err := cpty.Write(keyA); err != nil {
+		c.Fatal(err)
+	}
 
+	ch := make(chan struct{})
 	go func() {
-		exec.Command(dockerBinary, "kill", name).Run()
+		cmd.Wait()
+		ch <- struct{}{}
 	}()
 
 	select {
 	case <-ch:
-	case <-time.After(10 * time.Millisecond):
+	case <-time.After(10 * time.Second):
 		c.Fatal("timed out waiting for container to exit")
 	}
+
+	running, err := inspectField(name, "State.Running")
+	c.Assert(err, checker.IsNil)
+	c.Assert(running, checker.Equals, "true", check.Commentf("expected container to still be running"))
 }
 
+// TestRunDetach checks attaching and detaching with the escape sequence specified via config file.
+func (s *DockerSuite) TestRunAttachDetachFromConfig(c *check.C) {
+	keyCtrlA := []byte{1}
+	keyA := []byte{97}
+
+	// Setup config
+	homeKey := homedir.Key()
+	homeVal := homedir.Get()
+	tmpDir, err := ioutil.TempDir("", "fake-home")
+	c.Assert(err, checker.IsNil)
+	defer os.RemoveAll(tmpDir)
+
+	dotDocker := filepath.Join(tmpDir, ".docker")
+	os.Mkdir(dotDocker, 0600)
+	tmpCfg := filepath.Join(dotDocker, "config.json")
+
+	defer func() { os.Setenv(homeKey, homeVal) }()
+	os.Setenv(homeKey, tmpDir)
+
+	data := `{
+		"detachKeys": "ctrl-a,a"
+	}`
+
+	err = ioutil.WriteFile(tmpCfg, []byte(data), 0600)
+	c.Assert(err, checker.IsNil)
+
+	// Then do the work
+	name := "attach-detach"
+	dockerCmd(c, "run", "--name", name, "-itd", "busybox", "cat")
+
+	cmd := exec.Command(dockerBinary, "attach", name)
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		c.Fatal(err)
+	}
+	cpty, tty, err := pty.Open()
+	if err != nil {
+		c.Fatal(err)
+	}
+	defer cpty.Close()
+	cmd.Stdin = tty
+	if err := cmd.Start(); err != nil {
+		c.Fatal(err)
+	}
+	c.Assert(waitRun(name), check.IsNil)
+
+	if _, err := cpty.Write([]byte("hello\n")); err != nil {
+		c.Fatal(err)
+	}
+
+	out, err := bufio.NewReader(stdout).ReadString('\n')
+	if err != nil {
+		c.Fatal(err)
+	}
+	if strings.TrimSpace(out) != "hello" {
+		c.Fatalf("expected 'hello', got %q", out)
+	}
+
+	// escape sequence
+	if _, err := cpty.Write(keyCtrlA); err != nil {
+		c.Fatal(err)
+	}
+	time.Sleep(100 * time.Millisecond)
+	if _, err := cpty.Write(keyA); err != nil {
+		c.Fatal(err)
+	}
+
+	ch := make(chan struct{})
+	go func() {
+		cmd.Wait()
+		ch <- struct{}{}
+	}()
+
+	select {
+	case <-ch:
+	case <-time.After(10 * time.Second):
+		c.Fatal("timed out waiting for container to exit")
+	}
+
+	running, err := inspectField(name, "State.Running")
+	c.Assert(err, checker.IsNil)
+	c.Assert(running, checker.Equals, "true", check.Commentf("expected container to still be running"))
+}
+
+// TestRunDetach checks attaching and detaching with the detach flags, making sure it overrides config file
+func (s *DockerSuite) TestRunAttachDetachKeysOverrideConfig(c *check.C) {
+	keyCtrlA := []byte{1}
+	keyA := []byte{97}
+
+	// Setup config
+	homeKey := homedir.Key()
+	homeVal := homedir.Get()
+	tmpDir, err := ioutil.TempDir("", "fake-home")
+	c.Assert(err, checker.IsNil)
+	defer os.RemoveAll(tmpDir)
+
+	dotDocker := filepath.Join(tmpDir, ".docker")
+	os.Mkdir(dotDocker, 0600)
+	tmpCfg := filepath.Join(dotDocker, "config.json")
+
+	defer func() { os.Setenv(homeKey, homeVal) }()
+	os.Setenv(homeKey, tmpDir)
+
+	data := `{
+		"detachKeys": "ctrl-e,e"
+	}`
+
+	err = ioutil.WriteFile(tmpCfg, []byte(data), 0600)
+	c.Assert(err, checker.IsNil)
+
+	// Then do the work
+	name := "attach-detach"
+	dockerCmd(c, "run", "--name", name, "-itd", "busybox", "cat")
+
+	cmd := exec.Command(dockerBinary, "attach", "--detach-keys='ctrl-a,a'", name)
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		c.Fatal(err)
+	}
+	cpty, tty, err := pty.Open()
+	if err != nil {
+		c.Fatal(err)
+	}
+	defer cpty.Close()
+	cmd.Stdin = tty
+	if err := cmd.Start(); err != nil {
+		c.Fatal(err)
+	}
+	c.Assert(waitRun(name), check.IsNil)
+
+	if _, err := cpty.Write([]byte("hello\n")); err != nil {
+		c.Fatal(err)
+	}
+
+	out, err := bufio.NewReader(stdout).ReadString('\n')
+	if err != nil {
+		c.Fatal(err)
+	}
+	if strings.TrimSpace(out) != "hello" {
+		c.Fatalf("expected 'hello', got %q", out)
+	}
+
+	// escape sequence
+	if _, err := cpty.Write(keyCtrlA); err != nil {
+		c.Fatal(err)
+	}
+	time.Sleep(100 * time.Millisecond)
+	if _, err := cpty.Write(keyA); err != nil {
+		c.Fatal(err)
+	}
+
+	ch := make(chan struct{})
+	go func() {
+		cmd.Wait()
+		ch <- struct{}{}
+	}()
+
+	select {
+	case <-ch:
+	case <-time.After(10 * time.Second):
+		c.Fatal("timed out waiting for container to exit")
+	}
+
+	running, err := inspectField(name, "State.Running")
+	c.Assert(err, checker.IsNil)
+	c.Assert(running, checker.Equals, "true", check.Commentf("expected container to still be running"))
+}
+
+// "test" should be printed
 func (s *DockerSuite) TestRunWithCPUQuota(c *check.C) {
 	testRequires(c, cpuCfsQuota)
 

+ 33 - 4
man/docker-attach.1.md

@@ -6,6 +6,7 @@ docker-attach - Attach to a running container
 
 # SYNOPSIS
 **docker attach**
+[**--detach-keys**[=*[]*]]
 [**--help**]
 [**--no-stdin**]
 [**--sig-proxy**[=*true*]]
@@ -18,15 +19,19 @@ interactively.  You can attach to the same contained process multiple times
 simultaneously, screen sharing style, or quickly view the progress of your
 detached process.
 
-You can detach from the container (and leave it running) with `CTRL-p CTRL-q`
-(for a quiet exit) or `CTRL-c` which will send a `SIGKILL` to the container.
-When you are attached to a container, and exit its main process, the process's
-exit code will be returned to the client.
+To stop a container, use `CTRL-c`. This key sequence sends `SIGKILL` to the
+container. You can detach from the container (and leave it running) using a
+configurable key sequence. The default sequence is `CTRL-p CTRL-q`. You
+configure the key sequence using the **--detach-keys** option or a configuration
+file. See **config-json(5)** for documentation on using a configuration file.
 
 It is forbidden to redirect the standard input of a `docker attach` command while
 attaching to a tty-enabled container (i.e.: launched with `-t`).
 
 # OPTIONS
+**--detach-keys**=""
+    Override the key sequence for detaching a container. Format is a single character `[a-Z]` or `ctrl-<value>` where `<value>` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`.
+
 **--help**
   Print usage statement
 
@@ -36,6 +41,30 @@ attaching to a tty-enabled container (i.e.: launched with `-t`).
 **--sig-proxy**=*true*|*false*
    Proxy all received signals to the process (non-TTY mode only). SIGCHLD, SIGKILL, and SIGSTOP are not proxied. The default is *true*.
 
+# Override the detach sequence
+
+If you want, you can configure a override the Docker key sequence for detach.
+This is is useful if the Docker default sequence conflicts with key squence you
+use for other applications. There are two ways to defines a your own detach key
+sequence, as a per-container override or as a configuration property on  your
+entire configuration.
+
+To override the sequence for an individual container, use the
+`--detach-keys="<sequence>"` flag with the `docker attach` command. The format of
+the `<sequence>` is either a letter [a-Z], or the `ctrl-` combined with any of
+the following:
+
+* `a-z` (a single lowercase alpha character )
+* `@` (ampersand)
+* `[` (left bracket)
+* `\\` (two backward slashes)
+*  `_` (underscore)
+* `^` (caret)
+
+These `a`, `ctrl-a`, `X`, or `ctrl-\\` values are all examples of valid key
+sequences. To configure a different configuration default key sequence for all
+containers, see **docker(1)**.
+
 # EXAMPLES
 
 ## Attaching to a container

+ 5 - 1
man/docker-exec.1.md

@@ -7,6 +7,7 @@ docker-exec - Run a command in a running container
 # SYNOPSIS
 **docker exec**
 [**-d**|**--detach**]
+[**--detach-keys**[=*[]*]]
 [**--help**]
 [**-i**|**--interactive**]
 [**--privileged**]
@@ -26,7 +27,10 @@ container is unpaused, and then run
 
 # OPTIONS
 **-d**, **--detach**=*true*|*false*
-   Detached mode: run command in the background. The default is *false*.
+    Override the key sequence for detaching a container. Format is a single character `[a-Z]` or `ctrl-<value>` where `<value>` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`.
+
+**--detach-keys**=""
+  Define the key sequence which detaches the container.
 
 **--help**
   Print usage statement

+ 8 - 2
man/docker-run.1.md

@@ -20,6 +20,7 @@ docker-run - Run a command in a new container
 [**--cpuset-cpus**[=*CPUSET-CPUS*]]
 [**--cpuset-mems**[=*CPUSET-MEMS*]]
 [**-d**|**--detach**]
+[**--detach-keys**[=*[]*]]
 [**--device**[=*[]*]]
 [**--device-read-bps**[=*[]*]]
 [**--device-read-iops**[=*[]*]]
@@ -190,8 +191,13 @@ the other shell to view a list of the running containers. You can reattach to a
 detached container with **docker attach**. If you choose to run a container in
 the detached mode, then you cannot use the **-rm** option.
 
-   When attached in the tty mode, you can detach from a running container without
-stopping the process by pressing the keys CTRL-P CTRL-Q.
+   When attached in the tty mode, you can detach from the container (and leave it
+running) using a configurable key sequence. The default sequence is `CTRL-p CTRL-q`.
+You configure the key sequence using the **--detach-keys** option or a configuration file.
+See **config-json(5)** for documentation on using a configuration file.
+
+**--detach-keys**=""
+   Override the key sequence for detaching a container. Format is a single character `[a-Z]` or `ctrl-<value>` where `<value>` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`.
 
 **--device**=[]
    Add a host device to the container (e.g. --device=/dev/sdc:/dev/xvdc:rwm)

+ 6 - 1
man/docker-start.1.md

@@ -7,6 +7,7 @@ docker-start - Start one or more containers
 # SYNOPSIS
 **docker start**
 [**-a**|**--attach**]
+[**--detach-keys**[=*[]*]]
 [**--help**]
 [**-i**|**--interactive**]
 CONTAINER [CONTAINER...]
@@ -17,7 +18,11 @@ Start one or more containers.
 
 # OPTIONS
 **-a**, **--attach**=*true*|*false*
-   Attach container's STDOUT and STDERR and forward all signals to the process. The default is *false*.
+   Attach container's STDOUT and STDERR and forward all signals to the
+   process. The default is *false*.
+
+**--detach-keys**=""
+   Override the key sequence for detaching a container. Format is a single character `[a-Z]` or `ctrl-<value>` where `<value>` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`.
 
 **--help**
   Print usage statement

+ 1 - 0
man/docker.1.md

@@ -223,6 +223,7 @@ inside it)
   Block until a container stops, then print its exit code
   See **docker-wait(1)** for full documentation on the **wait** command.
 
+
 # EXEC DRIVER OPTIONS
 
 Use the **--exec-opt** flags to specify options to the execution driver. The only

+ 66 - 0
pkg/term/ascii.go

@@ -0,0 +1,66 @@
+package term
+
+import (
+	"fmt"
+	"strings"
+)
+
+// ASCII list the possible supported ASCII key sequence
+var ASCII = []string{
+	"ctrl-@",
+	"ctrl-a",
+	"ctrl-b",
+	"ctrl-c",
+	"ctrl-d",
+	"ctrl-e",
+	"ctrl-f",
+	"ctrl-g",
+	"ctrl-h",
+	"ctrl-i",
+	"ctrl-j",
+	"ctrl-k",
+	"ctrl-l",
+	"ctrl-m",
+	"ctrl-n",
+	"ctrl-o",
+	"ctrl-p",
+	"ctrl-q",
+	"ctrl-r",
+	"ctrl-s",
+	"ctrl-t",
+	"ctrl-u",
+	"ctrl-v",
+	"ctrl-w",
+	"ctrl-x",
+	"ctrl-y",
+	"ctrl-z",
+	"ctrl-[",
+	"ctrl-\\",
+	"ctrl-]",
+	"ctrl-^",
+	"ctrl-_",
+}
+
+// ToBytes converts a string representing a suite of key-sequence to the corresponding ASCII code.
+func ToBytes(keys string) ([]byte, error) {
+	codes := []byte{}
+next:
+	for _, key := range strings.Split(keys, ",") {
+		if len(key) != 1 {
+			for code, ctrl := range ASCII {
+				if ctrl == key {
+					codes = append(codes, byte(code))
+					continue next
+				}
+			}
+			if key == "DEL" {
+				codes = append(codes, 127)
+			} else {
+				return nil, fmt.Errorf("Unknown character: '%s'", key)
+			}
+		} else {
+			codes = append(codes, byte(key[0]))
+		}
+	}
+	return codes, nil
+}

+ 43 - 0
pkg/term/ascii_test.go

@@ -0,0 +1,43 @@
+package term
+
+import "testing"
+
+func TestToBytes(t *testing.T) {
+	codes, err := ToBytes("ctrl-a,a")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(codes) != 2 {
+		t.Fatalf("Expected 2 codes, got %d", len(codes))
+	}
+	if codes[0] != 1 || codes[1] != 97 {
+		t.Fatalf("Expected '1' '97', got '%d' '%d'", codes[0], codes[1])
+	}
+
+	codes, err = ToBytes("shift-z")
+	if err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+
+	codes, err = ToBytes("ctrl-@,ctrl-[,~,ctrl-o")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(codes) != 4 {
+		t.Fatalf("Expected 4 codes, got %d", len(codes))
+	}
+	if codes[0] != 0 || codes[1] != 27 || codes[2] != 126 || codes[3] != 15 {
+		t.Fatalf("Expected '0' '27' '126', '15', got '%d' '%d' '%d' '%d'", codes[0], codes[1], codes[2], codes[3])
+	}
+
+	codes, err = ToBytes("DEL,+")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(codes) != 2 {
+		t.Fatalf("Expected 2 codes, got %d", len(codes))
+	}
+	if codes[0] != 127 || codes[1] != 43 {
+		t.Fatalf("Expected '127 '43'', got '%d' '%d'", codes[0], codes[1])
+	}
+}