1
0
mirror of https://github.com/bingohuang/docker-labs.git synced 2025-10-04 01:17:49 +08:00

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.
This commit is contained in:
Jonathan Leibiusky (@xetorthio)
2016-11-10 10:42:08 -03:00
parent 9b6991f130
commit 8e4981d24f
16 changed files with 566 additions and 249 deletions

View File

@@ -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();
}
}
}]);
})();

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
});

View File

@@ -2,81 +2,91 @@
<html ng-app="DockerPlay" ng-controller="PlayController">
<head>
<title>Docker Playground</title>
<link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.css">
<link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.css">
<link rel="stylesheet" href="/assets/xterm.css" />
<link rel="stylesheet" href="/assets/style.css" />
</head>
<body>
<div layout="column" style="height:100%;" ng-cloak>
<div layout="column" style="height:100%;" ng-cloak>
<section id="sessionEnd" layout="row" flex ng-if="!isAlive">
<md-content flex layout-padding ng-if="!instances.length">
<div layout="column" layout-align="top center">
<p>
<strong>Your session has expired.</strong>
</p>
</div>
<div flex></div>
</md-content>
</section>
<section id="popupContainer" layout="row" flex ng-if="isAlive">
<md-sidenav
class="md-sidenav-left"
md-component-id="left"
md-is-locked-open="$mdMedia('gt-sm')"
md-whiteframe="4" layout="column">
<section id="popupContainer" layout="row" flex>
<md-toolbar class="md-theme-indigo">
<h1 class="md-toolbar-tools">Instances</h1>
</md-toolbar>
<md-content layout-padding>
<md-button ng-click="newInstance()" class="md-primary">
+ Add new instance
</md-button>
<md-list>
<md-list-item class="md-3-line" ng-repeat="instance in instances" ng-click="showInstance(instance)" ng-class="instance.name == selectedInstance.name ? 'selected' : false">
<div class="md-list-item-text">{{instance.name}}</div>
<md-divider ng-if="!$last"></md-divider>
</md-list-item>
</md-list>
</md-content>
<md-sidenav
class="md-sidenav-left"
md-component-id="left"
md-is-locked-open="$mdMedia('gt-sm')"
md-whiteframe="4">
</md-sidenav>
<md-toolbar class="md-theme-indigo">
<h1 class="md-toolbar-tools">Instances</h1>
</md-toolbar>
<md-content layout-padding>
<md-button ng-click="newInstance()" class="md-primary">
+ Add new instance
</md-button>
<md-list>
<md-list-item class="md-3-line" ng-repeat="instance in instances" ng-click="showInstance(instance)" ng-class="instance.name == selectedInstance.name ? 'selected' : false">
<div class="md-list-item-text">{{instance.name}}</div>
<md-divider ng-if="!$last"></md-divider>
</md-list-item>
</md-list>
</md-content>
<md-content flex layout-padding ng-if="!instances.length">
<div layout="column" layout-align="top center">
<p>
Add instances to your playground.
</p>
<p>
<strong>Sessions and all their instances are deleted after 1 hour.</strong>
</p>
</div>
</md-sidenav>
<md-content flex layout-padding ng-if="!instances.length">
<div layout="column" layout-align="top center">
<p>
Add instances to your playground.
</p>
<p>
<strong>Sessions and all their instances are deleted after 1 hour.</strong>
</p>
</div>
<div flex></div>
</md-content>
<md-content flex layout-padding ng-repeat="instance in instances" ng-show="instance.name == selectedInstance.name">
<md-card md-theme="default" md-theme-watch>
<md-card-title>
<md-card-title-text>
<span class="md-headline">{{instance.name}}</span>
<span class="md-subhead">{{instance.ip}}</span>
<md-card-title-text>
</md-card-title>
<md-card-actions layout="row">
<md-button class="md-warn md-raised" ng-click="deleteInstance(instance)">Delete</md-button>
</md-card-actions>
<md-card-content>
<div id="terminal-{{instance.name}}"></div>
</md-card-content>
</md-card>
</md-content>
<div flex></div>
</md-content>
<md-content flex layout="column" ng-repeat="instance in instances" ng-show="instance.name == selectedInstance.name">
<md-card md-theme="default" md-theme-watch>
<md-card-title>
<md-card-title-text>
<span class="md-headline">{{instance.name}}</span>
<span class="md-subhead">{{instance.ip}}</span>
<md-card-title-text>
</md-card-title>
<md-card-actions>
<md-button class="md-warn md-raised" ng-click="deleteInstance(instance)">Delete</md-button>
</md-card-actions>
</md-card>
<md-card flex md-theme="default" md-theme-watch >
<md-card-content flex id="terminal-{{instance.name}}" class="terminal">
</md-card-content>
</md-card>
</md-content>
</section>
</section>
</div>
</div>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-animate.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-aria.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-messages.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.js"></script>
<script src="/assets/app.js"></script>
<script src="/assets/main.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-animate.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-aria.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-messages.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.js"></script>
<script src="https://cdn.socket.io/socket.io-1.3.7.js"></script>
<script src="/assets/app.js"></script>
<script src="/assets/xterm.js"></script>
<script src="/assets/xterm-addons/fit.js"></script>
<script src="/assets/attach.js"></script>
<script type="text/javascript" charset="utf-8">
window.onbeforeunload = function (e) {