Skip to content

Commit

Permalink
Implement configurable detach key
Browse files Browse the repository at this point in the history
Implement configurable detach keys (for `attach`, exec`, `run` and
`start`) using the client-side configuration

- Adds a `--detach-keys` flag to `attach`, `exec`, `run` and `start`
  commands.
- Adds a new configuration field (in `~/.docker/config.json`) to
  configure the default escape keys for docker client.

Signed-off-by: Vincent Demeester <[email protected]>
  • Loading branch information
vdemeester committed Jan 3, 2016
1 parent eb551ba commit 15aa2a6
Show file tree
Hide file tree
Showing 27 changed files with 583 additions and 61 deletions.
6 changes: 6 additions & 0 deletions api/client/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions api/client/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,21 @@ 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
if execConfig.Container == "" || err != nil {
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
Expand Down
3 changes: 3 additions & 0 deletions api/client/lib/container_attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions api/client/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions api/client/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
49 changes: 36 additions & 13 deletions api/server/router/container/container_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions api/types/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type ContainerAttachOptions struct {
Stdin bool
Stdout bool
Stderr bool
DetachKeys string
}

// ContainerCommitOptions holds parameters to commit changes into a container.
Expand Down
1 change: 1 addition & 0 deletions api/types/configs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 change: 1 addition & 0 deletions cliconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
25 changes: 15 additions & 10 deletions container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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])
Expand Down
24 changes: 13 additions & 11 deletions daemon/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -87,6 +88,7 @@ type ContainerWsAttachWithLogsConfig struct {
InStream io.ReadCloser
OutStream, ErrStream io.Writer
Logs, Stream bool
DetachKeys []byte
}

// ContainerWsAttachWithLogs websocket connection
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
13 changes: 12 additions & 1 deletion daemon/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions daemon/exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
Loading

0 comments on commit 15aa2a6

Please sign in to comment.