From 8e4981d24f0cbd322ab07581f531dae92b7ab03d Mon Sep 17 00:00:00 2001 From: "Jonathan Leibiusky (@xetorthio)" Date: Thu, 10 Nov 2016 10:42:08 -0300 Subject: [PATCH] Huge refactor to have everything working with socket.io It fixes lots of bugs, can fallback to long polling, resize viewport of terminals and share clients state of the session, so they all see the same thing. --- README.md | 3 +- api.go | 31 +++--- handlers/delete_instance.go | 7 +- handlers/get_session.go | 7 +- handlers/new_instance.go | 6 +- handlers/{exec.go => ws.go} | 46 +++++--- services/client.go | 60 +++++++++++ services/docker.go | 20 ++-- services/instance.go | 74 +++++++++++-- services/session.go | 56 ++++++++-- types/session.go | 21 ---- www/assets/app.js | 186 ++++++++++++++++++++++++++------- www/assets/main.js | 61 ----------- www/assets/style.css | 17 +++ www/assets/xterm-addons/fit.js | 86 +++++++++++++++ www/index.html | 134 +++++++++++++----------- 16 files changed, 566 insertions(+), 249 deletions(-) rename handlers/{exec.go => ws.go} (63%) create mode 100644 services/client.go delete mode 100644 types/session.go delete mode 100644 www/assets/main.js create mode 100644 www/assets/xterm-addons/fit.js diff --git a/README.md b/README.md index 124a369..40fa564 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,7 @@ A live version is available at: http://play-with-docker.com/ ## Requirements -Docker 1.13-dev or higher is required as **attachable** overlay networks feature is required. -Here's a boot2docker 1.13-dev ISO you can use to spawn a VM with this version: https://dl.dropboxusercontent.com/u/29887388/boot2docker.iso +Docker 1.12.1 or higher is required. ## Installation diff --git a/api.go b/api.go index 6b6fc52..d867ca5 100644 --- a/api.go +++ b/api.go @@ -4,32 +4,37 @@ import ( "log" "net/http" - "golang.org/x/net/websocket" - "github.com/franela/play-with-docker/handlers" - "github.com/go-zoo/bone" + "github.com/franela/play-with-docker/services" + "github.com/gorilla/mux" "github.com/urfave/negroni" ) func main() { - mux := bone.New() + server := services.CreateWSServer() - mux.Get("/ping", http.HandlerFunc(handlers.Ping)) - mux.Get("/", http.HandlerFunc(handlers.NewSession)) - mux.Get("/sessions/:sessionId", http.HandlerFunc(handlers.GetSession)) - mux.Post("/sessions/:sessionId/instances", http.HandlerFunc(handlers.NewInstance)) - mux.Delete("/sessions/:sessionId/instances/:instanceName", http.HandlerFunc(handlers.DeleteInstance)) + server.On("connection", handlers.WS) + server.On("error", handlers.WSError) + + r := mux.NewRouter() + r.StrictSlash(false) + + r.HandleFunc("/ping", http.HandlerFunc(handlers.Ping)).Methods("GET") + r.HandleFunc("/", http.HandlerFunc(handlers.NewSession)).Methods("GET") + r.HandleFunc("/sessions/{sessionId}", http.HandlerFunc(handlers.GetSession)).Methods("GET") + r.HandleFunc("/sessions/{sessionId}/instances", http.HandlerFunc(handlers.NewInstance)).Methods("POST") + r.HandleFunc("/sessions/{sessionId}/instances/{instanceName}", http.HandlerFunc(handlers.DeleteInstance)).Methods("DELETE") h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./www/index.html") }) - mux.Get("/p/:sessionId", h) - mux.Get("/assets/*", http.FileServer(http.Dir("./www"))) + r.HandleFunc("/p/{sessionId}", h).Methods("GET") + r.PathPrefix("/assets").Handler(http.FileServer(http.Dir("./www"))) - mux.Get("/sessions/:sessionId/instances/:instanceName/attach", websocket.Handler(handlers.Exec)) + r.Handle("/sessions/{sessionId}/ws/", server) n := negroni.Classic() - n.UseHandler(mux) + n.UseHandler(r) log.Fatal(http.ListenAndServe("0.0.0.0:3000", n)) diff --git a/handlers/delete_instance.go b/handlers/delete_instance.go index 393fcce..8731b68 100644 --- a/handlers/delete_instance.go +++ b/handlers/delete_instance.go @@ -4,12 +4,13 @@ import ( "net/http" "github.com/franela/play-with-docker/services" - "github.com/go-zoo/bone" + "github.com/gorilla/mux" ) func DeleteInstance(rw http.ResponseWriter, req *http.Request) { - sessionId := bone.GetValue(req, "sessionId") - instanceName := bone.GetValue(req, "instanceName") + vars := mux.Vars(req) + sessionId := vars["sessionId"] + instanceName := vars["instanceName"] s := services.GetSession(sessionId) i := services.GetInstance(s, instanceName) diff --git a/handlers/get_session.go b/handlers/get_session.go index 1df39cc..f302ec6 100644 --- a/handlers/get_session.go +++ b/handlers/get_session.go @@ -2,14 +2,17 @@ package handlers import ( "encoding/json" + "log" "net/http" "github.com/franela/play-with-docker/services" - "github.com/go-zoo/bone" + "github.com/gorilla/mux" ) func GetSession(rw http.ResponseWriter, req *http.Request) { - sessionId := bone.GetValue(req, "sessionId") + vars := mux.Vars(req) + sessionId := vars["sessionId"] + log.Println(sessionId) session := services.GetSession(sessionId) diff --git a/handlers/new_instance.go b/handlers/new_instance.go index e346a27..be44521 100644 --- a/handlers/new_instance.go +++ b/handlers/new_instance.go @@ -6,16 +6,18 @@ import ( "net/http" "github.com/franela/play-with-docker/services" - "github.com/go-zoo/bone" + "github.com/gorilla/mux" ) func NewInstance(rw http.ResponseWriter, req *http.Request) { - sessionId := bone.GetValue(req, "sessionId") + vars := mux.Vars(req) + sessionId := vars["sessionId"] s := services.GetSession(sessionId) s.Lock() if len(s.Instances) >= 5 { + s.Unlock() rw.WriteHeader(http.StatusConflict) return } diff --git a/handlers/exec.go b/handlers/ws.go similarity index 63% rename from handlers/exec.go rename to handlers/ws.go index 3927ec1..59f8365 100644 --- a/handlers/exec.go +++ b/handlers/ws.go @@ -1,22 +1,42 @@ package handlers import ( - "io" + "log" - "golang.org/x/net/context" - "golang.org/x/net/websocket" - "golang.org/x/text/encoding" - - "github.com/franela/play-with-docker/cookoo" "github.com/franela/play-with-docker/services" - "github.com/go-zoo/bone" - "github.com/twinj/uuid" + "github.com/googollee/go-socket.io" + "github.com/gorilla/mux" ) -// Echo the data received on the WebSocket. -func Exec(ws *websocket.Conn) { - sessionId := bone.GetValue(ws.Request(), "sessionId") - instanceName := bone.GetValue(ws.Request(), "instanceName") +func WS(so socketio.Socket) { + vars := mux.Vars(so.Request()) + + sessionId := vars["sessionId"] + + session := services.GetSession(sessionId) + if session == nil { + log.Printf("Session with id [%s] does not exist!\n", sessionId) + return + } + + session.AddNewClient(services.NewClient(so, session)) +} +func WSError(so socketio.Socket) { + log.Println("error ws") +} + +/* + so.Join(sessionId) + + // TODO: Reset terminal geometry + + so.On("resize", func(cols, rows int) { + // TODO: Reset terminal geometry + }) + + so.On("disconnection", func() { + //TODO: reset the best terminal geometry + }) ctx := context.Background() @@ -62,5 +82,5 @@ func Exec(ws *websocket.Conn) { case <-ctx.Done(): } } - } +*/ diff --git a/services/client.go b/services/client.go new file mode 100644 index 0000000..b5275df --- /dev/null +++ b/services/client.go @@ -0,0 +1,60 @@ +package services + +import "github.com/googollee/go-socket.io" + +type ViewPort struct { + Rows uint + Cols uint +} + +type Client struct { + SO socketio.Socket + ViewPort ViewPort +} + +func (c *Client) ResizeViewPort(cols, rows uint) { + c.ViewPort.Rows = rows + c.ViewPort.Cols = cols +} + +func NewClient(so socketio.Socket, session *Session) *Client { + so.Join(session.Id) + + c := &Client{SO: so} + + so.On("terminal in", func(name, data string) { + // User wrote something on the terminal. Need to write it to the instance terminal + instance := GetInstance(session, name) + if len(data) > 0 { + instance.Conn.Conn.Write([]byte(data)) + } + }) + + so.On("viewport resize", func(cols, rows uint) { + // User resized his viewport + c.ResizeViewPort(cols, rows) + vp := session.GetSmallestViewPort() + // Resize all terminals in the session + wsServer.BroadcastTo(session.Id, "viewport resize", vp.Cols, vp.Rows) + for _, instance := range session.Instances { + instance.ResizeTerminal(vp.Cols, vp.Rows) + } + }) + so.On("disconnection", func() { + // Client has disconnected. Remove from session and recheck terminal sizes. + for i, cl := range session.Clients { + if cl.SO.Id() == c.SO.Id() { + session.Clients = append(session.Clients[:i], session.Clients[i+1:]...) + break + } + } + vp := session.GetSmallestViewPort() + // Resize all terminals in the session + wsServer.BroadcastTo(session.Id, "viewport resize", vp.Cols, vp.Rows) + for _, instance := range session.Instances { + instance.ResizeTerminal(vp.Cols, vp.Rows) + } + }) + + return c +} diff --git a/services/docker.go b/services/docker.go index 9ee8894..74934e7 100644 --- a/services/docker.go +++ b/services/docker.go @@ -4,8 +4,6 @@ import ( "log" "strings" - ptypes "github.com/franela/play-with-docker/types" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" @@ -29,9 +27,7 @@ func GetContainerInfo(id string) (types.ContainerJSON, error) { } func CreateNetwork(name string) error { - // TODO: This line appears to give an error when running on localhost:3000 - // when driver is specified a name must be given. - opts := types.NetworkCreate{Attachable: true, Driver: "overlay"} + opts := types.NetworkCreate{} _, err := c.NetworkCreate(context.Background(), name, opts) if err != nil { @@ -71,16 +67,14 @@ func AttachExecConnection(execId string, ctx context.Context) (*types.HijackedRe return nil, err } - err = c.ContainerExecResize(ctx, execId, types.ResizeOptions{Height: 24, Width: 80}) - - if err != nil { - return nil, err - } - return &conn, nil } -func CreateInstance(net string, dindImage string) (*ptypes.Instance, error) { +func ResizeExecConnection(execId string, ctx context.Context, cols, rows uint) error { + return c.ContainerExecResize(ctx, execId, types.ResizeOptions{Height: rows, Width: cols}) +} + +func CreateInstance(net string, dindImage string) (*Instance, error) { var maximumPidLimit int64 maximumPidLimit = 150 // Set a ulimit value to prevent misuse @@ -104,7 +98,7 @@ func CreateInstance(net string, dindImage string) (*ptypes.Instance, error) { return nil, err } - return &ptypes.Instance{Name: strings.Replace(cinfo.Name, "/", "", 1), IP: cinfo.NetworkSettings.Networks[net].IPAddress}, nil + return &Instance{Name: strings.Replace(cinfo.Name, "/", "", 1), IP: cinfo.NetworkSettings.Networks[net].IPAddress}, nil } func DeleteContainer(id string) error { diff --git a/services/instance.go b/services/instance.go index 8635c87..c9f5c17 100644 --- a/services/instance.go +++ b/services/instance.go @@ -1,12 +1,25 @@ package services import ( + "context" + "io" "log" "os" - "github.com/franela/play-with-docker/types" + "golang.org/x/text/encoding" + + "github.com/docker/docker/api/types" ) +type Instance struct { + Session *Session `json:"-"` + Name string `json:"name"` + IP string `json:"ip"` + Conn *types.HijackedResponse `json:"-"` + ExecId string `json:"-"` + Ctx context.Context `json:"-"` +} + var dindImage string var defaultDindImageName string @@ -23,31 +36,78 @@ func getDindImageName() string { return dindImage } -func NewInstance(session *types.Session) (*types.Instance, error) { +func NewInstance(session *Session) (*Instance, error) { //TODO: Validate that a session can only have 5 instances - //TODO: Create in redis log.Printf("NewInstance - using image: [%s]\n", dindImage) instance, err := CreateInstance(session.Id, dindImage) + instance.Session = session if err != nil { return nil, err } if session.Instances == nil { - session.Instances = make(map[string]*types.Instance) + session.Instances = make(map[string]*Instance) } session.Instances[instance.Name] = instance + go instance.Exec() + + wsServer.BroadcastTo(session.Id, "new instance", instance.Name, instance.IP) + return instance, nil } -func GetInstance(session *types.Session, name string) *types.Instance { +type sessionWriter struct { + instance *Instance +} + +func (s *sessionWriter) Write(p []byte) (n int, err error) { + wsServer.BroadcastTo(s.instance.Session.Id, "terminal out", s.instance.Name, string(p)) + return len(p), nil +} + +func (i *Instance) ResizeTerminal(cols, rows uint) { + ResizeExecConnection(i.ExecId, i.Ctx, cols, rows) +} + +func (i *Instance) Exec() { + i.Ctx = context.Background() + + id, err := CreateExecConnection(i.Name, i.Ctx) + if err != nil { + return + } + i.ExecId = id + conn, err := AttachExecConnection(id, i.Ctx) + if err != nil { + return + } + + i.Conn = conn + + go func() { + encoder := encoding.Replacement.NewEncoder() + sw := &sessionWriter{instance: i} + io.Copy(encoder.Writer(sw), conn.Reader) + }() + + select { + case <-i.Ctx.Done(): + } +} + +func GetInstance(session *Session, name string) *Instance { //TODO: Use redis return session.Instances[name] } -func DeleteInstance(session *types.Session, instance *types.Instance) error { +func DeleteInstance(session *Session, instance *Instance) error { //TODO: Use redis delete(session.Instances, instance.Name) - return DeleteContainer(instance.Name) + err := DeleteContainer(instance.Name) + + wsServer.BroadcastTo(session.Id, "delete instance", instance.Name) + + return err } diff --git a/services/session.go b/services/session.go index 7eca586..52fa88b 100644 --- a/services/session.go +++ b/services/session.go @@ -2,30 +2,66 @@ package services import ( "log" + "math" + "sync" "time" - "github.com/franela/play-with-docker/types" + "github.com/googollee/go-socket.io" "github.com/twinj/uuid" ) -var sessions map[string]*types.Session +var wsServer *socketio.Server -func init() { - sessions = make(map[string]*types.Session) +type Session struct { + sync.Mutex + Id string `json:"id"` + Instances map[string]*Instance `json:"instances"` + Clients []*Client `json:"-"` } -func NewSession() (*types.Session, error) { - s := &types.Session{} +func (s *Session) GetSmallestViewPort() ViewPort { + minRows := s.Clients[0].ViewPort.Rows + minCols := s.Clients[0].ViewPort.Cols + + for _, c := range s.Clients { + minRows = uint(math.Min(float64(minRows), float64(c.ViewPort.Rows))) + minCols = uint(math.Min(float64(minCols), float64(c.ViewPort.Cols))) + } + + return ViewPort{Rows: minRows, Cols: minCols} +} + +func (s *Session) AddNewClient(c *Client) { + s.Clients = append(s.Clients, c) +} + +var sessions map[string]*Session + +func init() { + sessions = make(map[string]*Session) +} + +func CreateWSServer() *socketio.Server { + server, err := socketio.NewServer(nil) + if err != nil { + log.Fatal(err) + } + wsServer = server + return server +} + +func NewSession() (*Session, error) { + s := &Session{} s.Id = uuid.NewV4().String() - s.Instances = map[string]*types.Instance{} + s.Instances = map[string]*Instance{} log.Printf("NewSession id=[%s]\n", s.Id) - //TODO: Store in something like redis sessions[s.Id] = s // Schedule cleanup of the session time.AfterFunc(1*time.Hour, func() { s = GetSession(s.Id) + wsServer.BroadcastTo(s.Id, "session end") log.Printf("Starting clean up of session [%s]\n", s.Id) for _, i := range s.Instances { i.Conn.Close() @@ -41,13 +77,13 @@ func NewSession() (*types.Session, error) { }) if err := CreateNetwork(s.Id); err != nil { + log.Println("ERROR NETWORKING") return nil, err } return s, nil } -func GetSession(sessionId string) *types.Session { - //TODO: Use redis +func GetSession(sessionId string) *Session { return sessions[sessionId] } diff --git a/types/session.go b/types/session.go deleted file mode 100644 index 38af7fc..0000000 --- a/types/session.go +++ /dev/null @@ -1,21 +0,0 @@ -package types - -import ( - "sync" - - "github.com/docker/docker/api/types" - "github.com/franela/play-with-docker/cookoo" -) - -type Session struct { - sync.Mutex - Id string `json:"id"` - Instances map[string]*Instance `json:"instances"` -} - -type Instance struct { - Name string `json:"name"` - IP string `json:"ip"` - Stdout *cookoo.MultiWriter `json:"-"` - Conn *types.HijackedResponse `json:"-"` -} diff --git a/www/assets/app.js b/www/assets/app.js index 9a6ec6f..444fb31 100644 --- a/www/assets/app.js +++ b/www/assets/app.js @@ -3,34 +3,59 @@ var app = angular.module('DockerPlay', ['ngMaterial']); - app.controller('PlayController', ['$scope', '$log', '$http', '$location', '$timeout', '$mdDialog', function($scope, $log, $http, $location, $timeout, $mdDialog) { + app.controller('PlayController', ['$scope', '$log', '$http', '$location', '$timeout', '$mdDialog', '$window', function($scope, $log, $http, $location, $timeout, $mdDialog, $window) { $scope.sessionId = window.location.pathname.replace('/p/', ''); $scope.instances = []; + $scope.idx = {}; $scope.selectedInstance = null; + $scope.isAlive = true; - $scope.showAlert = function(title, content) { + angular.element($window).bind('resize', function(){ + if ($scope.selectedInstance) { + $scope.resize($scope.selectedInstance.term.proposeGeometry()); + } + }); + + + $scope.showAlert = function(title, content, parent) { $mdDialog.show( $mdDialog.alert() - .parent(angular.element(document.querySelector('#popupContainer'))) + .parent(angular.element(document.querySelector(parent || '#popupContainer'))) .clickOutsideToClose(true) .title(title) .textContent(content) .ok('Got it!') - ); - } + ); + } + + $scope.resize = function(geometry) { + $scope.socket.emit('viewport resize', geometry.cols, geometry.rows); + } + + $scope.upsertInstance = function(info) { + var i = info; + if (!$scope.idx[i.name]) { + $scope.instances.push(i); + i.buffer = ''; + $scope.idx[i.name] = i; + } else { + $scope.idx[i.name].ip = i.ip; + } + + return $scope.idx[i.name]; + } $scope.newInstance = function() { $http({ method: 'POST', url: '/sessions/' + $scope.sessionId + '/instances', }).then(function(response) { - var i = response.data; - $scope.instances.push(i); - $scope.showInstance(i); + var i = $scope.upsertInstance(response.data); + $scope.showInstance(i); }, function(response) { - if (response.status == 409) { - $scope.showAlert('Max instances reached', 'Maximum number of instances reached') - } + if (response.status == 409) { + $scope.showAlert('Max instances reached', 'Maximum number of instances reached') + } }); } @@ -39,18 +64,64 @@ method: 'GET', url: '/sessions/' + $scope.sessionId, }).then(function(response) { + var socket = io({path: '/sessions/' + sessionId + '/ws'}); + + socket.on('terminal out', function(name, data) { + var instance = $scope.idx[name]; + + if (!instance) { + // instance is new and was created from another client, we should add it + $scope.upsertInstance({name: name}); + instance = $scope.idx[name]; + } + if (!instance.term) { + instance.buffer += data; + } else { + instance.term.write(data); + } + }); + + socket.on('session end', function() { + $scope.showAlert('Session timedout!', 'Your session has expire and all your instances has been deleted.', '#sessionEnd') + $scope.isAlive = false; + }); + + socket.on('viewport', function(rows, cols) { + }); + + socket.on('new instance', function(name, ip) { + $scope.upsertInstance({name: name, ip: ip}); + $scope.$apply(function() { + if ($scope.instances.length == 1) { + $scope.showInstance($scope.instances[0]); + } + }); + }); + + socket.on('delete instance', function(name) { + $scope.removeInstance(name); + $scope.$apply(); + }); + + socket.on('viewport resize', function(cols, rows) { + // viewport has changed, we need to resize all terminals + + $scope.instances.forEach(function(instance) { + instance.term.resize(cols, rows); + }); + }); + + $scope.socket = socket; + var i = response.data; for (var k in i.instances) { var instance = i.instances[k]; - $scope.instances.push(instance); + $scope.idx[instance.name] = instance; } if ($scope.instances.length) { - $scope.showInstance($scope.instances[0]); + $scope.showInstance($scope.instances[0]); } - - // Since session exists, we check it still exists every 10 seconds - $scope.checkHandler = setInterval(checkSession, 10*1000); }, function(response) { if (response.status == 404) { document.write('session not found'); @@ -59,25 +130,41 @@ }); } - $scope.showInstance = function(instance) { - $scope.selectedInstance = instance; - if (!instance.isAttached) { - $timeout(function() {instance.term = createTerminal(instance.name);}); - instance.isAttached = true; - } else { - $timeout(function() {instance.term.focus()}); - } - } + $scope.showInstance = function(instance) { + $scope.selectedInstance = instance; + if (!instance.creatingTerminal) { + instance.creatingTerminal = true; + if (!instance.term) { + $timeout(function() { + createTerminal(instance); + instance.term.focus(); + }, 0, false); + } else { + $timeout(function() { + instance.term.focus(); + }, 0, false); + } + } + } + + $scope.removeInstance = function(name) { + if ($scope.idx[name]) { + delete $scope.idx[name]; + $scope.instances = $scope.instances.filter(function(i) { + return i.name != name; + }); + if ($scope.instances.length) { + $scope.showInstance($scope.instances[0]); + } + } + } $scope.deleteInstance = function(instance) { $http({ method: 'DELETE', url: '/sessions/' + $scope.sessionId + '/instances/' + instance.name, }).then(function(response) { - $scope.instances = $scope.instances.filter(function(i) { return i.name != instance.name }); - if ($scope.instances.length) { - $scope.showInstance($scope.instances[0]); - } + $scope.removeInstance(instance.name); }, function(response) { console.log('error', response); }); @@ -85,16 +172,35 @@ $scope.getSession($scope.sessionId); - function checkSession() { - $http({ - method: 'GET', - url: '/sessions/' + $scope.sessionId, - }).then(function(response) {}, function(response) { - if (response.status == 404) { - clearInterval($scope.checkHandler); - $scope.showAlert('Session timedout!', 'Your session has expire and all your instances has been deleted.') - } - }); - } + function createTerminal(instance, cb) { + if (instance.term) { + return instance.term; + } + + var terminalContainer = document.getElementById('terminal-'+ instance.name); + + var term = new Terminal({ + cursorBlink: false + }); + + term.open(terminalContainer); + + $scope.resize(term.proposeGeometry()); + + term.on('data', function(d) { + $scope.socket.emit('terminal in', instance.name, d); + }); + + instance.term = term; + + if (instance.buffer) { + term.write(instance.buffer); + instance.buffer = ''; + } + + if (cb) { + cb(); + } + } }]); })(); diff --git a/www/assets/main.js b/www/assets/main.js deleted file mode 100644 index 056c429..0000000 --- a/www/assets/main.js +++ /dev/null @@ -1,61 +0,0 @@ -//var term, - //protocol, - //socketURL, - //socket, - //pid, - //charWidth, - //charHeight; - -//var terminalContainer = document.getElementById('terminal-container'), - //optionElements = { - //cursorBlink: document.querySelector('#option-cursor-blink') - //}, - //colsElement = document.getElementById('cols'), - //rowsElement = document.getElementById('rows'); - -//function setTerminalSize () { - //var cols = parseInt(colsElement.value), - //rows = parseInt(rowsElement.value), - //width = (cols * charWidth).toString() + 'px', - //height = (rows * charHeight).toString() + 'px'; - - //terminalContainer.style.width = width; - //terminalContainer.style.height = height; - //term.resize(cols, rows); -//} - -//colsElement.addEventListener('change', setTerminalSize); -//rowsElement.addEventListener('change', setTerminalSize); - -//optionElements.cursorBlink.addEventListener('change', createTerminal); - -//createTerminal(); -// -// -function createTerminal(name) { - var terminalContainer = document.getElementById('terminal-' + name); - // Clean terminal - while (terminalContainer.children.length) { - terminalContainer.removeChild(terminalContainer.children[0]); - } - var term = new Terminal({ - cursorBlink: false - }); - var sessionId = location.pathname.substr(location.pathname.lastIndexOf("/")+1); - protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'; - socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/sessions/' + sessionId + '/instances/' + name + '/attach'; - - term.open(terminalContainer); - - socket = new WebSocket(socketURL); - socket.onopen = runRealTerminal(term); - - return term; - -} - - -function runRealTerminal(term) { - term.attach(socket); - term._initialized = true; -} diff --git a/www/assets/style.css b/www/assets/style.css index 92b2589..47f6d22 100644 --- a/www/assets/style.css +++ b/www/assets/style.css @@ -1,3 +1,20 @@ .selected button { background-color: rgba(158,158,158,0.2); } + +md-card-content.terminal { + background-color: rgb(113, 113, 113); + padding: 0; +} + +.terminal { + background-color: rgb(113, 113, 113); +} + +.terminal .xterm-viewport { + background-color: rgb(113, 113, 113); +} + +.terminal .xterm-rows { + background-color: #000; +} diff --git a/www/assets/xterm-addons/fit.js b/www/assets/xterm-addons/fit.js new file mode 100644 index 0000000..7657c9c --- /dev/null +++ b/www/assets/xterm-addons/fit.js @@ -0,0 +1,86 @@ +/* + * Fit terminal columns and rows to the dimensions of its + * DOM element. + * + * Approach: + * - Rows: Truncate the division of the terminal parent element height + * by the terminal row height + * + * - Columns: Truncate the division of the terminal parent element width by + * the terminal character width (apply display: inline at the + * terminal row and truncate its width with the current number + * of columns) + */ +(function (fit) { + if (typeof exports === 'object' && typeof module === 'object') { + /* + * CommonJS environment + */ + module.exports = fit(require('../../dist/xterm')); + } else if (typeof define == 'function') { + /* + * Require.js is available + */ + define(['../../dist/xterm'], fit); + } else { + /* + * Plain browser environment + */ + fit(window.Terminal); + } +})(function (Xterm) { + /** + * This module provides methods for fitting a terminal's size to a parent container. + * + * @module xterm/addons/fit/fit + */ + var exports = {}; + + exports.proposeGeometry = function (term) { + var parentElementStyle = window.getComputedStyle(term.element.parentElement), + parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')), + parentElementWidth = parseInt(parentElementStyle.getPropertyValue('width')), + elementStyle = window.getComputedStyle(term.element), + elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom')), + elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left')), + availableHeight = parentElementHeight - elementPaddingVer, + availableWidth = parentElementWidth - elementPaddingHor, + container = term.rowContainer, + subjectRow = term.rowContainer.firstElementChild, + contentBuffer = subjectRow.innerHTML, + characterHeight, + rows, + characterWidth, + cols, + geometry; + + subjectRow.style.display = 'inline'; + subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace + characterWidth = subjectRow.getBoundingClientRect().width; + subjectRow.style.display = ''; // Revert style before calculating height, since they differ. + characterHeight = parseInt(subjectRow.offsetHeight); + subjectRow.innerHTML = contentBuffer; + + rows = parseInt(availableHeight / characterHeight); + cols = parseInt(availableWidth / characterWidth) - 1; + + geometry = {cols: cols, rows: rows}; + return geometry; + }; + + exports.fit = function (term) { + var geometry = exports.proposeGeometry(term); + + term.resize(geometry.cols, geometry.rows); + }; + + Xterm.prototype.proposeGeometry = function () { + return exports.proposeGeometry(this); + }; + + Xterm.prototype.fit = function () { + return exports.fit(this); + }; + + return exports; +}); diff --git a/www/index.html b/www/index.html index f0c3081..3620491 100644 --- a/www/index.html +++ b/www/index.html @@ -2,81 +2,91 @@ Docker Playground - + -
+
+
+ +
+

+ Your session has expired. +

+
+
+
+
+
+ -
+ +

Instances

+
+ + + + Add new instance + + + + +
{{instance.name}}
+ +
+
+
- + - -

Instances

-
- - - + Add new instance - - - - -
{{instance.name}}
- -
-
-
+ +
+

+ Add instances to your playground. +

+

+ Sessions and all their instances are deleted after 1 hour. +

+
- - - -
-

- Add instances to your playground. -

-

- Sessions and all their instances are deleted after 1 hour. -

-
- -
-
- - - - - {{instance.name}} - {{instance.ip}} - - - - Delete - - -
-
-
-
+
+
+ + + + + {{instance.name}} + {{instance.ip}} + + + + Delete + + + + + + + -
+
-
+
- - - - - - - + + + + + + + +