diff --git a/app/pipes.go b/app/pipes.go index 791a2bb9a..4073f94d7 100644 --- a/app/pipes.go +++ b/app/pipes.go @@ -2,6 +2,7 @@ package app import ( "net/http" + "strconv" log "github.com/Sirupsen/logrus" "github.com/gorilla/mux" @@ -15,7 +16,12 @@ func RegisterPipeRoutes(router *mux.Router, pr PipeRouter) { router.Methods("GET"). Name("api_pipe_pipeid_check"). Path("/api/pipe/{pipeID}/check"). - HandlerFunc(requestContextDecorator(checkPipe(pr, UIEnd))) + HandlerFunc(requestContextDecorator(checkPipe(pr))) + + router.Methods("POST"). + Name("api_pipe_pipeid_resize_tty"). + Path("/api/pipe/{pipeID}/resize_tty/{height}/{width}"). + HandlerFunc(requestContextDecorator(resizeTTY(pr))) router.Methods("GET"). Name("api_pipe_pipeid"). @@ -33,7 +39,7 @@ func RegisterPipeRoutes(router *mux.Router, pr PipeRouter) { HandlerFunc(requestContextDecorator(deletePipe(pr))) } -func checkPipe(pr PipeRouter, end End) CtxHandlerFunc { +func checkPipe(pr PipeRouter) CtxHandlerFunc { return func(ctx context.Context, w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["pipeID"] exists, err := pr.Exists(ctx, id) @@ -47,6 +53,47 @@ func checkPipe(pr PipeRouter, end End) CtxHandlerFunc { } } +func resizeTTY(pr PipeRouter) CtxHandlerFunc { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + var ( + height, width uint64 + err error + ) + id := mux.Vars(r)["pipeID"] + height, err = strconv.ParseUint(mux.Vars(r)["height"], 10, 32) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + width, err = strconv.ParseUint(mux.Vars(r)["width"], 10, 32) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + pipe, _, err := pr.Get(ctx, id, ProbeEnd) + if err != nil { + log.Debugf("Error getting pipe %s: %v", id, err) + http.NotFound(w, r) + return + } + + tty := pipe.GetTTY() + if tty == nil { + // The pipe doesn't have a tty, nothing to do + log.Debugf("Error getting terminal for pipe %s", id) + http.NotFound(w, r) + return + } + + if err = tty.SetSize(uint(height), uint(width)); err != nil { + log.Errorf("Error setting terminal size (%d, %d) of pipe %s: %v", height, width, id, err) + respondWith(w, http.StatusInternalServerError, err) + } + + } +} + func handlePipeWs(pr PipeRouter, end End) CtxHandlerFunc { return func(ctx context.Context, w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["pipeID"] diff --git a/common/xfer/pipes.go b/common/xfer/pipes.go index be1f96a84..605f988d8 100644 --- a/common/xfer/pipes.go +++ b/common/xfer/pipes.go @@ -7,12 +7,20 @@ import ( "github.com/gorilla/websocket" ) -// Pipe is a bi-directional channel from someone thing in the probe +// 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()) @@ -26,6 +34,7 @@ type pipe struct { quit chan struct{} closed bool onClose func() + tty TTY } // NewPipeFromEnds makes a new pipe specifying its ends @@ -65,6 +74,13 @@ 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 375d86a9d..cabfa8b00 100644 --- a/probe/docker/controls.go +++ b/probe/docker/controls.go @@ -105,6 +105,15 @@ 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, @@ -122,6 +131,8 @@ 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{ Tty: true, diff --git a/probe/docker/controls_test.go b/probe/docker/controls_test.go index 61b7baf43..eeef820e2 100644 --- a/probe/docker/controls_test.go +++ b/probe/docker/controls_test.go @@ -47,6 +47,8 @@ func (mockPipe) CopyToWebsocket(io.ReadWriter, xfer.Websocket) error { return ni func (mockPipe) Close() error { return nil } func (mockPipe) Closed() bool { return false } func (mockPipe) OnClose(func()) {} +func (mockPipe) GetTTY() xfer.TTY { return nil } +func (mockPipe) SetTTY(xfer.TTY) {} func TestPipes(t *testing.T) { oldNewPipe := controls.NewPipe diff --git a/probe/docker/registry.go b/probe/docker/registry.go index 1f533806b..85cb01337 100644 --- a/probe/docker/registry.go +++ b/probe/docker/registry.go @@ -86,6 +86,7 @@ type Client interface { CreateExec(docker_client.CreateExecOptions) (*docker_client.Exec, error) StartExecNonBlocking(string, docker_client.StartExecOptions) (docker_client.CloseWaiter, error) Stats(docker_client.StatsOptions) error + ResizeExecTTY(id string, height, width int) error } func newDockerClient(endpoint string) (Client, error) { diff --git a/probe/docker/registry_test.go b/probe/docker/registry_test.go index cb5762891..71428ed08 100644 --- a/probe/docker/registry_test.go +++ b/probe/docker/registry_test.go @@ -167,6 +167,10 @@ func (m *mockDockerClient) Stats(_ client.StatsOptions) error { return fmt.Errorf("stats") } +func (m *mockDockerClient) ResizeExecTTY(id string, height, width int) error { + return fmt.Errorf("resizeExecTTY") +} + type mockCloseWaiter struct{} func (mockCloseWaiter) Close() error { return nil }