From 2443f7a3c5038eed2095ca26b455ce0fe9654b6c Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Sat, 29 Oct 2016 11:11:02 +0000 Subject: [PATCH] Implement resize control for docker TTYs --- common/xfer/controls.go | 5 +-- common/xfer/pipes.go | 16 --------- probe/docker/controls.go | 73 +++++++++++++++++++++++++++++++++------- probe/docker/registry.go | 2 ++ 4 files changed, 66 insertions(+), 30 deletions(-) diff --git a/common/xfer/controls.go b/common/xfer/controls.go index 7d77e418f..3a1ece97b 100644 --- a/common/xfer/controls.go +++ b/common/xfer/controls.go @@ -23,8 +23,9 @@ type Response struct { Error string `json:"error,omitempty"` // Pipe specific fields - Pipe string `json:"pipe,omitempty"` - RawTTY bool `json:"raw_tty,omitempty"` + Pipe string `json:"pipe,omitempty"` + RawTTY bool `json:"raw_tty,omitempty"` + ResizeTTYControl string `json:"resize_tty_control,omitempty"` // Remove specific fields RemovedNode string `json:"removedNode,omitempty"` // Set if node was removed diff --git a/common/xfer/pipes.go b/common/xfer/pipes.go index 605f988d8..0a60af507 100644 --- a/common/xfer/pipes.go +++ b/common/xfer/pipes.go @@ -7,20 +7,12 @@ import ( "github.com/gorilla/websocket" ) -// A TTY is a terminal with an associated height and width -type TTY interface { - SetSize(height, width uint) error -} - // Pipe is a bi-directional channel from something in the probe // to the UI. type Pipe interface { Ends() (io.ReadWriter, io.ReadWriter) CopyToWebsocket(io.ReadWriter, Websocket) error - SetTTY(TTY) - GetTTY() TTY - Close() error Closed() bool OnClose(func()) @@ -34,7 +26,6 @@ type pipe struct { quit chan struct{} closed bool onClose func() - tty TTY } // NewPipeFromEnds makes a new pipe specifying its ends @@ -74,13 +65,6 @@ func (p *pipe) Ends() (io.ReadWriter, io.ReadWriter) { return p.port, p.starboard } -func (p *pipe) SetTTY(tty TTY) { - p.tty = tty -} -func (p *pipe) GetTTY() TTY { - return p.tty -} - func (p *pipe) Close() error { p.mtx.Lock() var onClose func() diff --git a/probe/docker/controls.go b/probe/docker/controls.go index cabfa8b00..c31f2baee 100644 --- a/probe/docker/controls.go +++ b/probe/docker/controls.go @@ -1,6 +1,8 @@ package docker import ( + "strconv" + docker_client "github.com/fsouza/go-dockerclient" log "github.com/Sirupsen/logrus" @@ -20,6 +22,7 @@ const ( RemoveContainer = "docker_remove_container" AttachContainer = "docker_attach_container" ExecContainer = "docker_exec_container" + ResizeExecTTY = "docker_resize_exec_tty" waitTime = 10 ) @@ -105,15 +108,6 @@ func (r *registry) attachContainer(containerID string, req xfer.Request) xfer.Re } } -type execTTY struct { - client Client - execID string -} - -func (e execTTY) SetSize(height, width uint) error { - return e.client.ResizeExecTTY(e.execID, int(height), int(width)) -} - func (r *registry) execContainer(containerID string, req xfer.Request) xfer.Response { exec, err := r.client.CreateExec(docker_client.CreateExecOptions{ AttachStdin: true, @@ -131,7 +125,6 @@ func (r *registry) execContainer(containerID string, req xfer.Request) xfer.Resp if err != nil { return xfer.ResponseError(err) } - pipe.SetTTY(execTTY{r.client, exec.ID}) local, _ := pipe.Ends() cw, err := r.client.StartExecNonBlocking(exec.ID, docker_client.StartExecOptions{ @@ -144,11 +137,19 @@ func (r *registry) execContainer(containerID string, req xfer.Request) xfer.Resp if err != nil { return xfer.ResponseError(err) } + + r.Lock() + r.pipeIDToexecID[id] = exec.ID + r.Unlock() + pipe.OnClose(func() { if err := cw.Close(); err != nil { log.Errorf("Error closing exec in container %s: %v", containerID, err) return } + r.Lock() + delete(r.pipeIDToexecID, id) + r.Unlock() }) go func() { if err := cw.Wait(); err != nil { @@ -157,11 +158,57 @@ func (r *registry) execContainer(containerID string, req xfer.Request) xfer.Resp pipe.Close() }() return xfer.Response{ - Pipe: id, - RawTTY: true, + Pipe: id, + RawTTY: true, + ResizeTTYControl: ResizeExecTTY, } } +func (r *registry) resizeExecTTY(containerID string, req xfer.Request) xfer.Response { + var ( + height, width int + err error + ) + + pipeID, ok := req.ControlArgs["pipeID"] + if !ok { + return xfer.ResponseErrorf("Missing argument: pipeID") + } + heightS, ok := req.ControlArgs["height"] + if !ok { + return xfer.ResponseErrorf("Missing argument: height") + } + widthS, ok := req.ControlArgs["width"] + if !ok { + return xfer.ResponseErrorf("Missing argument: width") + } + + height, err = strconv.Atoi(heightS) + if err != nil { + return xfer.ResponseErrorf("Bad parameter: height (%q): %v", heightS, err) + } + width, err = strconv.Atoi(widthS) + if err != nil { + return xfer.ResponseErrorf("Bad parameter: width (%q): %v", widthS, err) + } + + r.Lock() + execID, ok := r.pipeIDToexecID[pipeID] + r.Unlock() + + if !ok { + return xfer.ResponseErrorf("Unknown pipeID (%q)", pipeID) + } + + if r.client.ResizeExecTTY(execID, int(height), int(width)); err != nil { + return xfer.ResponseErrorf( + "Error setting terminal size (%d, %d) of pipe %s: %v", + height, width, pipeID, err) + } + + return xfer.Response{} +} + func captureContainerID(f func(string, xfer.Request) xfer.Response) func(xfer.Request) xfer.Response { return func(req xfer.Request) xfer.Response { containerID, ok := report.ParseContainerNodeID(req.NodeID) @@ -182,6 +229,7 @@ func (r *registry) registerControls() { RemoveContainer: captureContainerID(r.removeContainer), AttachContainer: captureContainerID(r.attachContainer), ExecContainer: captureContainerID(r.execContainer), + ResizeExecTTY: captureContainerID(r.resizeExecTTY), } r.handlerRegistry.Batch(nil, controls) } @@ -196,6 +244,7 @@ func (r *registry) deregisterControls() { RemoveContainer, AttachContainer, ExecContainer, + ResizeExecTTY, } r.handlerRegistry.Batch(controls, nil) } diff --git a/probe/docker/registry.go b/probe/docker/registry.go index 85cb01337..796ba7cf7 100644 --- a/probe/docker/registry.go +++ b/probe/docker/registry.go @@ -65,6 +65,7 @@ type registry struct { containersByPID map[int]Container images map[string]docker_client.APIImages networks []docker_client.Network + pipeIDToexecID map[string]string } // Client interface for mocking. @@ -104,6 +105,7 @@ func NewRegistry(interval time.Duration, pipes controls.PipeClient, collectStats containers: radix.New(), containersByPID: map[int]Container{}, images: map[string]docker_client.APIImages{}, + pipeIDToexecID: map[string]string{}, client: client, pipes: pipes,