├── spiceproxy ├── .npmignore ├── .gitignore ├── filelist.js ├── package.json ├── spicechannel.js ├── socket.js ├── concatenator.js └── globalpool.js ├── unittest ├── reassemblerfactory.test.js ├── packetfactory.test.js ├── some.html ├── keymap.test.js ├── displayrouter.test.js ├── graphictestfiles │ └── uris.js ├── packetprocess.test.js ├── collisiondetector.test.js ├── connectioncontrol.test.js ├── busprocess.test.js ├── timelapsedetector.test.js ├── syncasynchandler.test.js ├── clusternodechooser.test.js ├── eventobject.test.js ├── socket.test.js ├── graphictest.test.js ├── displayprocess.test.js ├── packetlinkfactory.test.js ├── viewqueue.test.js ├── queue.test.js ├── spiceconnection.test.js ├── socketqueue.test.js ├── packetreassembler.test.js ├── packetcontroller.test.js ├── sizedefiner.test.js ├── packetextractor.test.js ├── runqueue.test.js ├── busconnection.test.js ├── tests.js ├── graphic.test.js └── application.test.js ├── flexvdi ├── inactivity.js └── extwin.js ├── lib ├── sha1.js ├── jquery.nok.min.css ├── jquery.nok.min.js ├── prng4.js ├── CollisionDetector.js ├── rng.js ├── flipper.js ├── images │ └── jsquic_family.js ├── SyncAsyncHandler.js ├── AsyncWorker.js ├── jquery-mousewheel.js ├── timelapsedetector.js ├── IntegrationBenchmark.js ├── runqueue.js ├── encrypt.js ├── stuckkeyshandler.js ├── GenericObjectPool.js ├── displayRouter.js ├── GlobalPool.js ├── PacketWorkerIdentifier.js └── ImageUncompressor.js ├── eyeos128.png ├── swcanvas ├── cat.jpg ├── benchmark.html ├── swcanvas.js └── test.html ├── resources ├── mouse.png ├── magnifier.png ├── button-close.png ├── flexvdi-logo.png ├── mouse_cursor.gif ├── mouse_cursor.png └── button-close-highlight.png ├── package.json ├── sonar.properties ├── commit-stage.sh ├── application ├── stream.js ├── imagecache.js ├── virtualmouse.js ├── packetprocess.js ├── inputmanager.js └── packetfilter.js ├── extwin.html ├── network ├── reassemblerfactory.js ├── clusternodechooser.js ├── packetcontroller.js ├── packetextractor.js ├── connectioncontrol.js ├── websocketwrapper.js ├── packetlinkfactory.js ├── socketqueue.js ├── packetreassembler.js ├── sizedefiner.js └── socket.js ├── process ├── mainprocess.js ├── cursorprocess.js └── displaypreprocess.js ├── translation.js ├── README.md └── benchmark.html /spiceproxy/.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /unittest/reassemblerfactory.test.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spiceproxy/.gitignore: -------------------------------------------------------------------------------- 1 | /spiceproxy.js 2 | /*.tgz 3 | -------------------------------------------------------------------------------- /spiceproxy/filelist.js: -------------------------------------------------------------------------------- 1 | :q 2 | 3 | 4 | var list = [ 5 | 6 | ]; 7 | -------------------------------------------------------------------------------- /flexvdi/inactivity.js: -------------------------------------------------------------------------------- 1 | var inactivityTimeout = 0; 2 | var inactivityGrace = 30; 3 | -------------------------------------------------------------------------------- /lib/sha1.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexVDI/spice-web-client/HEAD/lib/sha1.js -------------------------------------------------------------------------------- /eyeos128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexVDI/spice-web-client/HEAD/eyeos128.png -------------------------------------------------------------------------------- /swcanvas/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexVDI/spice-web-client/HEAD/swcanvas/cat.jpg -------------------------------------------------------------------------------- /resources/mouse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexVDI/spice-web-client/HEAD/resources/mouse.png -------------------------------------------------------------------------------- /resources/magnifier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexVDI/spice-web-client/HEAD/resources/magnifier.png -------------------------------------------------------------------------------- /resources/button-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexVDI/spice-web-client/HEAD/resources/button-close.png -------------------------------------------------------------------------------- /resources/flexvdi-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexVDI/spice-web-client/HEAD/resources/flexvdi-logo.png -------------------------------------------------------------------------------- /resources/mouse_cursor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexVDI/spice-web-client/HEAD/resources/mouse_cursor.gif -------------------------------------------------------------------------------- /resources/mouse_cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexVDI/spice-web-client/HEAD/resources/mouse_cursor.png -------------------------------------------------------------------------------- /flexvdi/extwin.js: -------------------------------------------------------------------------------- 1 | var exturl = ""; 2 | var extwidth = "600px"; 3 | var extheight = "600px"; 4 | var extmargin = "100px auto"; 5 | -------------------------------------------------------------------------------- /resources/button-close-highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexVDI/spice-web-client/HEAD/resources/button-close-highlight.png -------------------------------------------------------------------------------- /unittest/packetfactory.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this template, choose Tools | Templates 3 | * and open the template in the editor. 4 | */ 5 | 6 | 7 | -------------------------------------------------------------------------------- /unittest/some.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Testing WDI Objects 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /spiceproxy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eyeos-spiceproxy", 3 | "version": "0.0.76", 4 | "description": "spice client network stack library", 5 | "main": "spiceproxy.js", 6 | "author": "eyeOS", 7 | "homepage": "http://www.eyeos.com" 8 | } 9 | -------------------------------------------------------------------------------- /lib/jquery.nok.min.css: -------------------------------------------------------------------------------- 1 | #nok{position:fixed;z-index:999;top:10px;right:0}#nok .nok{display:block;position:relative;overflow:hidden;margin-top:10px;margin-right:10px;padding:20px;width:240px;border-radius:2px;color:white;right:-300px}#nok .nok_info{background:#3c3b3b}#nok .nok_success{background:#68C300}#nok .nok_error{background:#c33c3c} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eyeos-spiceproxy", 3 | "version": "0.0.156", 4 | "description": "spice client network stack library", 5 | "main": "src/spiceproxy.js", 6 | "author": "eyeOS", 7 | "homepage": "http://www.eyeos.com", 8 | "dependencies": { 9 | }, 10 | "devDependencies": { 11 | "eyeos-gruntfile": "*", 12 | "grunt": "0.4.5", 13 | "grunt-init": "0.3.2", 14 | "mocha": "2.4.5" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sonar.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=spiceproxy 2 | sonar.projectName=spiceproxy 3 | sonar.projectVersion=1.0.0 4 | sonar.sources=src 5 | sonar.language=js 6 | sonar.dynamicAnalysis=reuseReports 7 | sonar.javascript.jstest.reportsPath=build/reports 8 | sonar.javascript.lcov.reportPath=build/reports/lcov.info 9 | sonar.exclusions=**/node_modules/**/*, \ 10 | **/test/**/*, \ 11 | **/component-test/**/*, \ 12 | **/build/**/*, \ 13 | **/doc/**/* 14 | -------------------------------------------------------------------------------- /unittest/keymap.test.js: -------------------------------------------------------------------------------- 1 | suite("Keymap", function() { 2 | var sut; 3 | 4 | setup(function () { 5 | sut = wdi.Keymap; 6 | }); 7 | 8 | suite('#handledByCharmap', function () { 9 | test('return true when type is inputmanager', function () { 10 | assert.isTrue(sut.handledByCharmap('inputmanager')); 11 | }); 12 | 13 | test('return false when type is not inputmanager', function () { 14 | assert.isFalse(sut.handledByCharmap('fakeEvent')); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /commit-stage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -u 4 | set -x 5 | 6 | # it would be good to not run the build if: 7 | # 8 | # * there are no changes in the source files used in the concatenator.js file 9 | # * there haven't been any changes in this repo 10 | # 11 | # since the last build done by jenkins. To achieve this you might get the date 12 | # of the last jenkins commit in this repo, and do git log --since 'that date' 13 | # and check if there have been any commit or not. 14 | 15 | cd "$(dirname "$0")" 16 | 17 | echo "Generating spiceproxy.js" 18 | 19 | spiceproxy/concatenator.js spice-web-client 20 | -------------------------------------------------------------------------------- /lib/jquery.nok.min.js: -------------------------------------------------------------------------------- 1 | !function(a){a.nok=function(n){(n=a.extend({message:"",type:"info",stay:4},n)).sticky&&(n.stay=0);var t="nok";if(a.trim(n.message)){0==a("#"+t).length&&a("body").append(a('
'));var e=a('
'+n.message+"
");a("#"+t).append(a(e)),a(e).animate({right:"10px"},"fast"),a.isNumeric(n.stay)&&0 2 | 3 | 4 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /unittest/packetprocess.test.js: -------------------------------------------------------------------------------- 1 | suite('PacketProcess', function() { 2 | setup(function(){ 3 | wdi.Debug.debug = false; //disable debugging, it slows tests 4 | }); 5 | 6 | suite('#process()', function() { 7 | setup(function() { 8 | this.toRestore = []; 9 | this.packetProcess = new wdi.PacketProcess({ 10 | mainProcess: true, 11 | displayProcess: true, 12 | cursorProcess: true, 13 | inputsProcess: true, 14 | playbackProcess: true 15 | }); 16 | }); 17 | 18 | test('Should throw an exception for invalid channels', function() { 19 | var failed = false; 20 | try { 21 | this.packetProcess.process({channel:99}); 22 | } catch (e) { 23 | failed = true; 24 | } 25 | assert(failed, 'Exception expected for invalid channel'); 26 | }); 27 | 28 | test('Should throw an exception for null messages', function() { 29 | var failed = false; 30 | try { 31 | this.packetProcess.process(); 32 | } catch (e) { 33 | failed = true; 34 | } 35 | assert(failed, 'Exception expected for null messages'); 36 | }); 37 | 38 | teardown(function() { 39 | this.toRestore.forEach(function(item) { 40 | item.restore(); 41 | }); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /lib/prng4.js: -------------------------------------------------------------------------------- 1 | // prng4.js - uses Arcfour as a PRNG 2 | 3 | function Arcfour() { 4 | this.i = 0; 5 | this.j = 0; 6 | this.S = new Array(); 7 | } 8 | 9 | // Initialize arcfour context from key, an array of ints, each from [0..255] 10 | function ARC4init(key) { 11 | var i, j, t; 12 | for(i = 0; i < 256; ++i) 13 | this.S[i] = i; 14 | j = 0; 15 | for(i = 0; i < 256; ++i) { 16 | j = (j + this.S[i] + key[i % key.length]) & 255; 17 | t = this.S[i]; 18 | this.S[i] = this.S[j]; 19 | this.S[j] = t; 20 | } 21 | this.i = 0; 22 | this.j = 0; 23 | } 24 | 25 | function ARC4next() { 26 | var t; 27 | this.i = (this.i + 1) & 255; 28 | this.j = (this.j + this.S[this.i]) & 255; 29 | t = this.S[this.i]; 30 | this.S[this.i] = this.S[this.j]; 31 | this.S[this.j] = t; 32 | return this.S[(t + this.S[this.i]) & 255]; 33 | } 34 | 35 | Arcfour.prototype.init = ARC4init; 36 | Arcfour.prototype.next = ARC4next; 37 | 38 | // Plug in your RNG constructor here 39 | function prng_newstate() { 40 | return new Arcfour(); 41 | } 42 | 43 | // Pool size must be a multiple of 4 and greater than 32. 44 | // An array of bytes the size of the pool will be passed to init() 45 | var rng_psize = 256; 46 | -------------------------------------------------------------------------------- /spiceproxy/socket.js: -------------------------------------------------------------------------------- 1 | var net = require('net'); 2 | 3 | wdi.socketStatus = { 4 | 'idle':0, 5 | 'prepared':1, 6 | 'connected':2, 7 | 'disconnected':3, 8 | 'failed':4 9 | }; 10 | 11 | //Works only with arrays of bytes (this means each value is a number in 0 to 255) 12 | wdi.Socket = $.spcExtend(wdi.EventObject.prototype, { 13 | netSocket: null, 14 | status: wdi.socketStatus.idle, 15 | binary: false, 16 | 17 | connect: function (uri) { 18 | var self = this; 19 | 20 | var uriParts = uri.split(':'); 21 | var port = uriParts.pop(); 22 | var host = uriParts.pop(); 23 | 24 | this.netSocket = new net.Socket(); 25 | this.netSocket.connect(port, host); 26 | 27 | this.status = wdi.socketStatus.prepared; 28 | 29 | this.netSocket.on('spiceMessage', function (data) { 30 | self.fire('message', new Uint8Array(data)); 31 | }); 32 | }, 33 | 34 | send: function (message) { 35 | this.netSocket.write(message); 36 | }, 37 | 38 | disconnect: function () { 39 | this.netSocket.removeAllListeners(); 40 | this.netSocket.end(); 41 | }, 42 | 43 | setStatus: function (status) { 44 | this.status = status; 45 | }, 46 | 47 | getStatus: function () { 48 | return this.status; 49 | }, 50 | 51 | getSocket: function () { 52 | return this.netSocket; 53 | } 54 | 55 | }); 56 | -------------------------------------------------------------------------------- /unittest/collisiondetector.test.js: -------------------------------------------------------------------------------- 1 | suite('CollisionDetector', function() { 2 | 3 | var testData = [ 4 | { 5 | base:{ 6 | top:0, 7 | left:0, 8 | right:3, 9 | bottom:3 10 | }, 11 | queue:{ 12 | top:0, 13 | left:2, 14 | right:4, 15 | bottom:2 16 | }, 17 | collides:true 18 | }, 19 | { 20 | base:{ 21 | top:0, 22 | left:0, 23 | right:5, 24 | bottom:5 25 | }, 26 | queue:{ 27 | top:3, 28 | left:-3, 29 | right:3, 30 | bottom:6 31 | }, 32 | collides:true 33 | }, 34 | { 35 | base:{ 36 | top:0, 37 | left:0, 38 | right:5, 39 | bottom:5 40 | }, 41 | queue:{ 42 | top:3, 43 | left:6, 44 | right:8, 45 | bottom:4 46 | }, 47 | collides:false 48 | } 49 | ]; 50 | var thereIsBoxCollision = function(){ 51 | var data = thereIsBoxCollision.data; 52 | var actual = wdi.CollisionDetector.thereIsBoxCollision(data.base, data.queue); 53 | assert.equal(data.collides, actual); 54 | }; 55 | thereIsBoxCollision.data = null; 56 | var setCounter=0; 57 | for(var i=0; i < testData.length; i++) { 58 | thereIsBoxCollision.data = testData[i]; 59 | test('thereIsBoxCollision will return correctData with set '+setCounter, thereIsBoxCollision); 60 | setCounter++; 61 | } 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /spiceproxy/concatenator.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | 6 | // change to parent directory so all paths are relative to the top dir 7 | process.chdir(path.dirname(__dirname)); 8 | 9 | var targetFile = path.join(__dirname, 'spiceproxy.js'); 10 | 11 | var files = [ 12 | 'lib/utils.js', 13 | 'spiceobjects/spiceobjects.js', 14 | 'spiceobjects/generated/protocol.js', 15 | 'lib/GlobalPool.js', 16 | 'lib/GenericObjectPool.js', 17 | 'spiceproxy/socket.js', 18 | 'spiceproxy/globalpool.js', 19 | 'lib/queue.js', 20 | 'network/socketqueue.js', 21 | 'network/packetextractor.js', 22 | 'network/packetcontroller.js', 23 | 'network/sizedefiner.js', 24 | 'network/packetreassembler.js', 25 | 'network/reassemblerfactory.js', 26 | 'lib/biginteger.js', 27 | 'spiceproxy/spicechannel.js' 28 | ]; 29 | 30 | var exportString = "\nmodule.exports = wdi; \n"; 31 | 32 | console.log("Will generate %s", targetFile); 33 | if (fs.existsSync(targetFile)) { 34 | fs.unlinkSync(targetFile); 35 | } 36 | 37 | files.forEach(function (file) { 38 | var data = fs.readFileSync(file); 39 | console.log('... appending %s', file); 40 | fs.appendFileSync(targetFile, data); 41 | }); 42 | 43 | console.log("Done! Appending module.exports line..."); 44 | fs.appendFileSync(targetFile, exportString); 45 | 46 | console.log("Finish... Everything is stored in %s", targetFile); 47 | -------------------------------------------------------------------------------- /unittest/connectioncontrol.test.js: -------------------------------------------------------------------------------- 1 | suite('ConnectionControl', function() { 2 | var sut, socket, config; 3 | 4 | setup(function() { 5 | config = { 6 | 'heartbeatTimeout': 4000, 7 | 'protocol': 'ws', 8 | 'host': 'localhost', 9 | 'port': 8000, 10 | 'busHost': 'localhost', 11 | 'heartbeatToken': 'heartbeat' 12 | }; 13 | socket = { 14 | connect: function() {}, 15 | setOnMessageCallback: function() {}, 16 | disconnect: function() {} 17 | }; 18 | sut = new wdi.ConnectionControl({socket: socket}); 19 | }); 20 | 21 | test('connect should call socket connect with uri', function() { 22 | var expectedString = config['protocol'] + '://' + config['host'] + ':' + config['port'] + 23 | '/websockify/destInfoToken/' + config['heartbeatToken']+'/type/raw'; 24 | var mock = sinon.mock(socket); 25 | var expectation = mock.expects('connect').once().withArgs(expectedString); 26 | sut.connect(config); 27 | expectation.verify(); 28 | }); 29 | 30 | test('connect should call socket setOnMessageCallback with callback', function() { 31 | var mock = sinon.mock(socket); 32 | var expectation = mock.expects('setOnMessageCallback').once().withArgs(sinon.match.func); 33 | sut.connect(config); 34 | expectation.verify(); 35 | }); 36 | 37 | test('disconnect should call socket disconnect', function() { 38 | var mock = sinon.mock(socket); 39 | var expectation = mock.expects('disconnect').once().withArgs(); 40 | sut.disconnect(); 41 | expectation.verify(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /unittest/busprocess.test.js: -------------------------------------------------------------------------------- 1 | suite("BusProcess:", function() { 2 | var sut; 3 | var clientGui, busConnection; 4 | 5 | setup(function () { 6 | clientGui = {}; 7 | busConnection = {}; 8 | sut = new wdi.BusProcess({ clientGui: clientGui, busConnection: busConnection }); 9 | }); 10 | 11 | suite('#parseMessage', function () { 12 | test('Should fire "wrongPathError" when the type is "launchApplication" and the event is "applicationLauncherWrongAppPathError"', sinon.test(function () { 13 | var body = { 14 | type: wdi.BUS_TYPES.launchApplication, 15 | event: "applicationLauncherWrongAppPathError" 16 | }; 17 | this.mock(sut) 18 | .expects('fire') 19 | .once() 20 | .withExactArgs('wrongPathError', body); 21 | sut.parseMessage(body); 22 | })); 23 | test('Should not fire "wrongPathError" when the type is "launchApplication" and the event is not "applicationLauncherWrongAppPathError"', sinon.test(function () { 24 | var body = { 25 | type: wdi.BUS_TYPES.launchApplication, 26 | event: "fakeEvent" 27 | }; 28 | this.mock(sut) 29 | .expects('fire') 30 | .never(); 31 | sut.parseMessage(body); 32 | })); 33 | 34 | test('Should fire "applicationLaunchedSuccessfully" when the type is "launchApplication" and the event is "applicationLaunchedSuccessfully"', sinon.test(function () { 35 | var body = { 36 | type: wdi.BUS_TYPES.launchApplication, 37 | event: "applicationLaunchedSuccessfully" 38 | }; 39 | this.mock(sut) 40 | .expects('fire') 41 | .once() 42 | .withExactArgs('applicationLaunchedSuccessfully', body); 43 | sut.parseMessage(body); 44 | })); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /unittest/timelapsedetector.test.js: -------------------------------------------------------------------------------- 1 | suite('TimeLapseDetector:', function () { 2 | var sut; 3 | var clock; 4 | var now; 5 | 6 | setup(function () { 7 | wdi.Debug.debug = false; //disable debugging, it slows tests 8 | now = Date.now(); 9 | clock = sinon.useFakeTimers(now); 10 | 11 | sut = new wdi.TimeLapseDetector(); 12 | }); 13 | 14 | teardown(function () { 15 | clock.restore(); 16 | }); 17 | 18 | test('when the timer is running normally, lastTime is updated', function () { 19 | sut.startTimer(); 20 | 21 | clock.tick(wdi.TimeLapseDetector.defaultInterval); 22 | var expected = now + wdi.TimeLapseDetector.defaultInterval; 23 | assert.equal(expected, sut.getLastTime()); 24 | }); 25 | 26 | test('when the timer is running late an event is fired', function () { 27 | sut.startTimer(); 28 | sut.setLastTime(now - (wdi.TimeLapseDetector.maxIntervalAllowed)); 29 | 30 | var expected = wdi.TimeLapseDetector.maxIntervalAllowed + wdi.TimeLapseDetector.defaultInterval; 31 | 32 | var mock = sinon.mock(sut); 33 | var expectation = mock.expects('fire') 34 | .once() 35 | .withExactArgs('timeLapseDetected', expected); 36 | 37 | clock.tick(wdi.TimeLapseDetector.defaultInterval); 38 | 39 | expectation.verify(); 40 | }); 41 | 42 | test('when the timer is running late, lastTime is updated', function () { 43 | sut.startTimer(); 44 | var passedTime = wdi.TimeLapseDetector.maxIntervalAllowed + 123; 45 | var expected = 0; 46 | while (expected + wdi.TimeLapseDetector.defaultInterval <= passedTime) { 47 | expected += wdi.TimeLapseDetector.defaultInterval; 48 | } 49 | expected += now; 50 | 51 | clock.tick(passedTime); 52 | assert.equal(expected, sut.getLastTime()); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /unittest/syncasynchandler.test.js: -------------------------------------------------------------------------------- 1 | suite('syncasynchandler', function () { 2 | var sut; 3 | var callbackWrapper; 4 | var asyncWorker; 5 | var scope; 6 | 7 | setup(function() { 8 | asyncWorker = { 9 | run: function () {} 10 | }; 11 | 12 | var isAsync = true; 13 | 14 | asyncSut = new wdi.SyncAsyncHandler({ 15 | asyncWorker: asyncWorker, 16 | isAsync: isAsync 17 | }); 18 | 19 | 20 | syncSut = new wdi.SyncAsyncHandler({ 21 | isAsync: !isAsync 22 | }); 23 | 24 | callbackWrapper = { 25 | callback: function () {} 26 | }; 27 | 28 | scope = {}; 29 | }); 30 | 31 | teardown(function () { 32 | 33 | }); 34 | 35 | test('dispatch calls workerProcess dispatch when sync', sinon.test(function() { 36 | var stub = this.stub(window, 'workerDispatch'); 37 | var buffer = 'one buffer'; 38 | syncSut.dispatch(buffer, callbackWrapper.callback, scope); 39 | 40 | var isAsync = false; 41 | 42 | sinon.assert.calledWithExactly(stub, buffer, isAsync); 43 | })); 44 | 45 | test('dispatch calls callback with dispatch result when sync', sinon.test(function() { 46 | var resultFromDispatch = 'some result'; 47 | var stub = this.stub(window, 'workerDispatch').returns(resultFromDispatch); 48 | var buffer = 'one buffer'; 49 | var callbackStub = sinon.stub(); 50 | syncSut.dispatch(buffer, callbackStub, scope); 51 | 52 | sinon.assert.calledWithExactly(callbackStub, resultFromDispatch); 53 | })); 54 | 55 | test('dispatch calls AsyncWorker dispatch when async', sinon.test(function() { 56 | var stub = this.stub(asyncWorker, 'run'); 57 | var buffer = 'one buffer'; 58 | asyncSut.dispatch(buffer, callbackWrapper.callback, scope); 59 | 60 | sinon.assert.calledWithExactly(stub, buffer, callbackWrapper.callback, scope); 61 | })); 62 | }); 63 | -------------------------------------------------------------------------------- /spiceproxy/globalpool.js: -------------------------------------------------------------------------------- 1 | wdi.GlobalPool = { 2 | pools: {}, 3 | retained: null, 4 | init: function() { 5 | this.retained = {}; 6 | var self = this; 7 | this.pools['ViewQueue'] = new wdi.GenericObjectPool([function() { 8 | //factory 9 | return new wdi.ViewQueue(); 10 | }, function(obj, index) { 11 | //reset 12 | obj.poolIndex = index; //update index at pool 13 | obj.setData([]); //reset the object 14 | }]); 15 | 16 | this.pools['RawSpiceMessage'] = new wdi.GenericObjectPool([function() { 17 | //factory 18 | return new wdi.RawSpiceMessage(); 19 | }, function(obj, index) { 20 | //reset 21 | obj.poolIndex = index; //update index at pool 22 | obj.set(null, null, null); //reset the object 23 | }]); 24 | }, 25 | 26 | create: function(objectType) { 27 | return this.pools[objectType].create(); 28 | }, 29 | 30 | discard: function(objectType, obj) { 31 | //check if its an autorelease pool 32 | if(this.retained.hasOwnProperty(objectType)) { 33 | delete this.retained[objectType][obj.poolIndex]; 34 | } 35 | return this.pools[objectType].discard(obj.poolIndex); 36 | }, 37 | 38 | cleanPool: function(objectType) { 39 | 40 | if(this.retained.hasOwnProperty(objectType)) { 41 | var pool = this.pools[objectType]; 42 | 43 | for(var i in this.retained[objectType]) { 44 | pool.discard(this.retained[objectType][i].poolIndex); 45 | } 46 | this.retained[objectType] = []; 47 | } else { 48 | wdi.Debug.error("GlobalPool: cleanPool called with invalid objectType: ",objectType); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/CollisionDetector.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.CollisionDetector = { 34 | thereIsBoxCollision: function(baseBox, queueBox) { 35 | if(baseBox.bottom < queueBox.top) return false; 36 | if(baseBox.top > queueBox.bottom) return false; 37 | if(baseBox.right < queueBox.left) return false; 38 | return baseBox.left < queueBox.right; 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /application/stream.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.Stream = { 34 | streams: {}, 35 | 36 | addStream: function(id, stream) { 37 | this.streams[id] = stream; 38 | }, 39 | 40 | deleteStream: function(id) { 41 | this.streams[id] = undefined; 42 | }, 43 | 44 | getStream: function(id) { 45 | return this.streams[id]; 46 | }, 47 | 48 | clip: function(id, clip) { 49 | this.streams[id].clip = clip; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /swcanvas/swcanvas.js: -------------------------------------------------------------------------------- 1 | var SWCanvas = function(canvas) { 2 | this.arr = new ArrayBuffer(canvas.width*canvas.height*4); 3 | this.arr32 = new Uint32Array(this.arr); 4 | this.arr8 = new Uint8ClampedArray(this.arr); 5 | this.canvas = canvas; 6 | this.width = this.canvas.width; 7 | this.height = this.canvas.height; 8 | }; 9 | 10 | SWCanvas.prototype.putImageData = function(imgData, x, y) { 11 | var srcArr = imgData.data.buffer; 12 | var w = imgData.width; 13 | var pos = imgData.height; 14 | var tmp; 15 | var canvasWidth = this.width; 16 | var tmpArr; 17 | while(pos--) { 18 | tmp = (y+pos)*canvasWidth+x; 19 | tmpArr = new Uint32Array(srcArr, w*pos*4, w); 20 | this.arr32.set(tmpArr, tmp); 21 | } 22 | }; 23 | 24 | SWCanvas.prototype.getImageData = function(x, y, width, height) { 25 | var result = new Uint32Array(width*height); 26 | var arr = this.arr32; 27 | var pos=height; 28 | var tmp; 29 | var canvasWidth = this.width; 30 | while(pos--) { 31 | tmp = (y+pos)*canvasWidth+x; 32 | var line = arr.subarray(tmp,tmp+width); 33 | result.set(line, width*pos); 34 | } 35 | return new ImageData(new Uint8ClampedArray(result.buffer), width, height); 36 | }; 37 | 38 | SWCanvas.prototype.fillRect = function(r, g, b, x, y, width, height) { 39 | var line = new Uint32Array(width); 40 | var color32 = 4278190080 | r | g << 8 | b << 16; 41 | var canvasWidth = this.width; 42 | var w = width; 43 | while(w--) { 44 | line[w] = color32; 45 | } 46 | var tmp; 47 | while(height--) { 48 | tmp = (y+height)*canvasWidth+x; 49 | this.arr32.set(line, tmp); 50 | } 51 | } 52 | 53 | //copy from canvas to arr 54 | SWCanvas.prototype.flushBack = function() { 55 | var arr8 = this.canvas.getContext('2d').getImageData(0, 0, this.width, this.height).data; 56 | this.arr8.set(arr8); 57 | }; 58 | 59 | //copy from arr to canvas 60 | SWCanvas.prototype.flush = function() { 61 | var imgData = new ImageData(this.arr8, this.width, this.height); 62 | this.canvas.getContext('2d').putImageData(imgData,0, 0); 63 | }; -------------------------------------------------------------------------------- /unittest/clusternodechooser.test.js: -------------------------------------------------------------------------------- 1 | suite('ClusterNodeChooser', function () { 2 | var sut; 3 | var shuffleStub; 4 | 5 | setup(sinon.test(function () { 6 | sut = new wdi.ClusterNodeChooser(); 7 | })); 8 | 9 | function addListWithNumberOfNodesToSut(sut, n) { 10 | var i; 11 | var list = []; 12 | for (i = 1; i <= n; i++) { 13 | list.push({ 14 | host: 'somehost' + i, 15 | port: 10000 + i 16 | }); 17 | } 18 | sut.setNodeList(list); 19 | } 20 | 21 | test('setNodeList should shuffle the received list', sinon.test(function () { 22 | var list = 'fake list'; 23 | this.mock(sut) 24 | .expects('_shuffle') 25 | .once() 26 | .withExactArgs(list) 27 | .returns(list); 28 | 29 | sut.setNodeList(list); 30 | })); 31 | 32 | test('2 consecutive calls to getAnother should return different nodes when there are more than 1', sinon.test(function () { 33 | addListWithNumberOfNodesToSut(sut, 2); 34 | var first; 35 | var second; 36 | first = sut.getAnother(); 37 | second = sut.getAnother(); 38 | assert.notDeepEqual(first, second, "returned nodes on 2 consecutive calls should not be equal if there are more than one node"); 39 | })); 40 | 41 | test('consecutive calls to getAnother should return always the same node when there is only one', sinon.test(function () { 42 | addListWithNumberOfNodesToSut(sut, 1); 43 | var first; 44 | var second; 45 | first = sut.getAnother(); 46 | second = sut.getAnother(); 47 | assert.deepEqual(first, second, "returned nodes on 2 consecutive calls should be equal if there is only one node"); 48 | })); 49 | 50 | test('n+1 consecutive calls to getAnother return the same node on first call and on n+1 call', sinon.test(function () { 51 | var n = 5; 52 | addListWithNumberOfNodesToSut(sut, n); 53 | 54 | var i; 55 | var first = sut.getAnother(); 56 | var ignored; 57 | for (i = 1; i < n; i++) { 58 | // do n-1 calls so at the end of the loop we've done n calls 59 | ignored = sut.getAnother(); 60 | } 61 | var n_plus_one = sut.getAnother(); 62 | 63 | assert.deepEqual(first, n_plus_one, "returned node on call 1 and on call n+1 should be the same"); 64 | })); 65 | 66 | }); 67 | -------------------------------------------------------------------------------- /lib/rng.js: -------------------------------------------------------------------------------- 1 | // Random number generator - requires a PRNG backend, e.g. prng4.js 2 | 3 | // For best results, put code like 4 | // 5 | // in your main HTML document. 6 | 7 | var rng_state; 8 | var rng_pool; 9 | var rng_pptr; 10 | 11 | // Mix in a 32-bit integer into the pool 12 | function rng_seed_int(x) { 13 | rng_pool[rng_pptr++] ^= x & 255; 14 | rng_pool[rng_pptr++] ^= (x >> 8) & 255; 15 | rng_pool[rng_pptr++] ^= (x >> 16) & 255; 16 | rng_pool[rng_pptr++] ^= (x >> 24) & 255; 17 | if(rng_pptr >= rng_psize) rng_pptr -= rng_psize; 18 | } 19 | 20 | // Mix in the current time (w/milliseconds) into the pool 21 | function rng_seed_time() { 22 | rng_seed_int(new Date().getTime()); 23 | } 24 | 25 | // Initialize the pool with junk if needed. 26 | if(rng_pool == null) { 27 | rng_pool = new Array(); 28 | rng_pptr = 0; 29 | var t; 30 | if(navigator.appName == "Netscape" && navigator.appVersion < "5" && window.crypto) { 31 | // Extract entropy (256 bits) from NS4 RNG if available 32 | var z = window.crypto.random(32); 33 | for(t = 0; t < z.length; ++t) 34 | rng_pool[rng_pptr++] = z.charCodeAt(t) & 255; 35 | } 36 | while(rng_pptr < rng_psize) { // extract some randomness from Math.random() 37 | t = Math.floor(65536 * Math.random()); 38 | rng_pool[rng_pptr++] = t >>> 8; 39 | rng_pool[rng_pptr++] = t & 255; 40 | } 41 | rng_pptr = 0; 42 | rng_seed_time(); 43 | //rng_seed_int(window.screenX); 44 | //rng_seed_int(window.screenY); 45 | } 46 | 47 | function rng_get_byte() { 48 | if(rng_state == null) { 49 | rng_seed_time(); 50 | rng_state = prng_newstate(); 51 | rng_state.init(rng_pool); 52 | for(rng_pptr = 0; rng_pptr < rng_pool.length; ++rng_pptr) 53 | rng_pool[rng_pptr] = 0; 54 | rng_pptr = 0; 55 | //rng_pool = null; 56 | } 57 | // TODO: allow reseeding after first request 58 | return rng_state.next(); 59 | } 60 | 61 | function rng_get_bytes(ba) { 62 | var i; 63 | for(i = 0; i < ba.length; ++i) ba[i] = rng_get_byte(); 64 | } 65 | 66 | function SecureRandom() {} 67 | 68 | SecureRandom.prototype.nextBytes = rng_get_bytes; 69 | -------------------------------------------------------------------------------- /unittest/eventobject.test.js: -------------------------------------------------------------------------------- 1 | suite('EventObject', function() { 2 | setup(function() { 3 | wdi.Debug.debug = false; //disable debugging, it slows tests 4 | }); 5 | 6 | suite('#addListener()', function() { 7 | setup(function () { 8 | this.eo = new wdi.EventObject(); 9 | }); 10 | 11 | test('Should add event to list', function() { 12 | this.eo.addListener('test', function(){}); 13 | assert.strictEqual(this.eo.getListenersLength("test"), 1); 14 | }); 15 | 16 | test('Should add two event to list', function() { 17 | this.eo.addListener('test', function(){}); 18 | this.eo.addListener('test', function(){}); 19 | assert.strictEqual(this.eo.getListenersLength("test"), 2); 20 | }); 21 | }); 22 | 23 | suite('#removeEvent()', function() { 24 | setup(function() { 25 | this.eo = new wdi.EventObject(); 26 | this.eo.addListener('test', function() {}); 27 | this.eo.addListener('test', function() {}); 28 | this.eo.addListener('test2', function() {}); 29 | }); 30 | 31 | test('Should remove correct event', function() { 32 | this.eo.removeEvent('test'); 33 | assert.notProperty(this.eo.eyeEvents, 'test'); 34 | }); 35 | }); 36 | 37 | suite('#clearEvents()', function() { 38 | setup(function() { 39 | this.eo = new wdi.EventObject(); 40 | this.eo.addListener('test', function(){}); 41 | this.eo.addListener('test', function(){}); 42 | this.eo.addListener('test', function(){}); 43 | }); 44 | 45 | test('Should remove all events and listeners', function() { 46 | this.eo.clearEvents(); 47 | assert.strictEqual(this.eo.getListenersLength('test'), 0); 48 | }); 49 | }); 50 | 51 | suite('#fire()', function() { 52 | setup(function() { 53 | this.eo = new wdi.EventObject(); 54 | this.callback = sinon.spy(); 55 | this.eo.addListener('test', this.callback, this); 56 | this.eo.addListener('test', this.callback, this); 57 | }); 58 | 59 | test('Should trigger selected event', function() { 60 | this.eo.fire('test'); 61 | assert(this.callback.calledTwice); 62 | }); 63 | 64 | test('Should keep scope', function() { 65 | this.eo.fire('test'); 66 | assert(this.callback.calledOn(this)); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /extwin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | flexVDI WebPortal 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 | 13 | 67 | 68 | -------------------------------------------------------------------------------- /lib/flipper.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.Flipper = { 34 | 35 | flip: function(sourceImg) { 36 | return this._handMadeFlip(sourceImg); 37 | }, 38 | 39 | _handMadeFlip: function(sourceImg) { 40 | var newCanvas = document.createElement('canvas'); 41 | newCanvas.width = sourceImg.width; 42 | newCanvas.height = sourceImg.height; 43 | var ctx = newCanvas.getContext('2d'); 44 | ctx.save(); 45 | // Multiply the y value by -1 to flip vertically 46 | ctx.scale(1, -1); 47 | // Start at (0, -height), which is now the bottom-left corner 48 | ctx.drawImage(sourceImg, 0, -sourceImg.height); 49 | ctx.restore(); 50 | return newCanvas; 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /lib/images/jsquic_family.js: -------------------------------------------------------------------------------- 1 | function golomb_code_len(n, l) { 2 | if (n < wdi.nGRcodewords[l]) { 3 | return (n >>> l) + 1 + l; 4 | } else { 5 | return wdi.notGRcwlen[l]; 6 | } 7 | } 8 | 9 | function golomb_decoding(l, bits, bppmask) { 10 | var cwlen; 11 | var result; 12 | if (bits > wdi.notGRprefixmask[l]) { 13 | var zeroprefix = cnt_l_zeroes(bits); 14 | cwlen = zeroprefix + 1 + l; 15 | result = ( (zeroprefix << l) >>> 0) | ((bits >>> (32 - cwlen)) & bppmask[l]); 16 | } else { 17 | cwlen = wdi.notGRcwlen[l]; 18 | result = wdi.nGRcodewords[l] + ((bits) >>> (32 - cwlen) & bppmask[wdi.notGRsuffixlen[l]]); 19 | } 20 | return [result,cwlen]; 21 | } 22 | 23 | /* update the bucket using just encoded curval */ 24 | function real_update_model(state, bucket, curval, bpp) { 25 | var i; 26 | var bestcode; 27 | var bestcodelen; 28 | var ithcodelen; 29 | 30 | var pcounters = bucket.pcounters; 31 | bestcode = bpp - 1; 32 | bestcodelen = (pcounters[bestcode] += golomb_code_len(curval, bestcode)); 33 | 34 | for (i = bpp - 2; i >= 0; i--) { 35 | ithcodelen = (pcounters[i] += golomb_code_len(curval, i)); 36 | 37 | if (ithcodelen < bestcodelen) { 38 | bestcode = i; 39 | bestcodelen = ithcodelen; 40 | } 41 | } 42 | 43 | bucket.bestcode = bestcode; 44 | if (bestcodelen > state.wm_trigger) { 45 | for (i = 0; i < bpp; i++) { 46 | pcounters[i] >>>= 1; 47 | } 48 | } 49 | } 50 | 51 | function UPDATE_MODEL(index, encoder, bpp, correlate_row_r, correlate_row_g, correlate_row_b) { 52 | real_update_model(encoder.rgb_state, find_bucket(encoder.channels[0], 53 | correlate_row_r[index - 1]), correlate_row_r[index], bpp); 54 | real_update_model(encoder.rgb_state, find_bucket(encoder.channels[1], 55 | correlate_row_g[index - 1]), correlate_row_g[index], bpp); 56 | real_update_model(encoder.rgb_state, find_bucket(encoder.channels[2], 57 | correlate_row_b[index - 1]), correlate_row_b[index], bpp); 58 | } 59 | 60 | function find_bucket(channel, val) { 61 | if(val===undefined) { 62 | val=channel.oldFirst; 63 | } 64 | return channel._buckets_ptrs[val]; 65 | } -------------------------------------------------------------------------------- /network/reassemblerfactory.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.ReassemblerFactory = { 34 | getPacketReassembler: function(socketQ) { 35 | var pE = this.getPacketExtractor(socketQ); 36 | var sD = this.getSizeDefiner(); 37 | var pC = this.getPacketController(pE, sD); 38 | return new wdi.PacketReassembler({packetController: pC}); 39 | }, 40 | 41 | getPacketExtractor: function(socketQ) { 42 | return new wdi.PacketExtractor({socketQ: socketQ}); 43 | }, 44 | 45 | getSizeDefiner: function() { 46 | return new wdi.SizeDefiner(); 47 | }, 48 | 49 | getPacketController: function(packetExtractor, sizeDefiner) { 50 | return new wdi.PacketController({packetExtractor: packetExtractor, sizeDefiner: sizeDefiner}); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /lib/SyncAsyncHandler.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.SyncAsyncHandler = $.spcExtend(wdi.EventObject.prototype, { 34 | init: function (c) { 35 | this.isAsync = !!c.isAsync; 36 | if (this.isAsync) { 37 | this.asyncWorker = c.asyncWorker || new wdi.AsyncWorker({script:'application/WorkerProcess.js'}); 38 | } 39 | }, 40 | 41 | isAsync: null, 42 | 43 | dispatch: function(buffer, callback, scope) { 44 | if (this.isAsync) { 45 | this.asyncWorker.run(buffer, callback, scope); 46 | } else { 47 | var result = window['workerDispatch'](buffer, this.isAsync); 48 | callback.call(scope, result); 49 | } 50 | }, 51 | 52 | dispose: function () { 53 | if (this.isAsync) { 54 | this.asyncWorker.dispose(); 55 | } 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /unittest/socket.test.js: -------------------------------------------------------------------------------- 1 | suite('Socket', function() {//TODO: this test is incomplete, must be revised 2 | setup(function(){ 3 | wdi.Debug.debug = false; //disable debugging, it slows tests 4 | }); 5 | 6 | suite('#getStatus()', function() { 7 | setup(function() { 8 | this.s = new wdi.Socket(); 9 | }); 10 | 11 | test('Should return idle before connecting', function() { 12 | assert.equal(this.s.getStatus(), wdi.socketStatus.idle); 13 | }); 14 | 15 | 16 | }); 17 | 18 | suite('#connect()', function() { 19 | setup(function() { 20 | this.s = new wdi.Socket(); 21 | }); 22 | 23 | test('Should set the status to prepared', function() { 24 | this.s.connect('ws://localhost:8000'); 25 | assert.strictEqual(this.s.getStatus(), wdi.socketStatus.prepared); 26 | }); 27 | }); 28 | 29 | suite('#send', function () { 30 | var sut, message; 31 | 32 | function execute () { 33 | sut.send(message); 34 | } 35 | 36 | setup(function() { 37 | message = "a fake message"; 38 | sut = new wdi.Socket(); 39 | sut.connect('ws://localhost:8000'); 40 | sut.websocket.send = function () {}; 41 | }); 42 | 43 | test('Should send the message when the websocket is ok', function () { 44 | var exp = sinon.mock(sut.websocket) 45 | .expects('send') 46 | .once() 47 | .withExactArgs(message); 48 | sinon.stub(sut, 'encode_message').returns(message); 49 | execute(); 50 | exp.verify(); 51 | }); 52 | 53 | test('Should encode the message sent', function () { 54 | var exp = sinon.mock(sut) 55 | .expects('encode_message') 56 | .once() 57 | .withExactArgs(message); 58 | sinon.stub(sut.websocket, 'send'); 59 | execute(); 60 | exp.verify(); 61 | }); 62 | 63 | test('Should set status = websocket.failed when error', function () { 64 | sinon.stub(sut, 'encode_message').throws(new Error()); 65 | execute(); 66 | assert.strictEqual(sut.getStatus(), wdi.socketStatus.failed); 67 | }); 68 | 69 | test('Should fire event error when error', function () { 70 | var err = new Error(), 71 | exp = sinon.mock(sut) 72 | .expects('fire') 73 | .once() 74 | .withExactArgs('error', err); 75 | sinon.stub(sut, 'encode_message').throws(err); 76 | execute(); 77 | exp.verify(); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /unittest/graphictest.test.js: -------------------------------------------------------------------------------- 1 | suite('GraphicTest', function() { 2 | var clientGui, displayPreProcess; 3 | 4 | this.timeout(2000); 5 | 6 | setup(function() { 7 | wdi.Debug.debug = false; 8 | clientGui = new wdi.ClientGui(); 9 | displayPreProcess = new wdi.DisplayPreProcess({ 10 | clientGui: clientGui 11 | }); 12 | }); 13 | 14 | wdi.graphicTestUris.forEach(function (item) { 15 | test(item.split('/').pop(), function(done) { 16 | $.get(item).done(function(data) { 17 | data = JSON.parse(data); 18 | var testFunction = function() { 19 | clientGui.getCanvas = function() { 20 | return ctxOrigin.canvas; 21 | }; 22 | clientGui.getContext = function() { 23 | return ctxOrigin; 24 | }; 25 | displayPreProcess.displayProcess.postProcess = function() { 26 | assert.equal(ctxOrigin.canvas.toDataURL('image/png'), ctxExpected.canvas.toDataURL('image/png'), 'The image is not the same as expected'); 27 | done(); 28 | }; 29 | var rawSpiceMessage = JSON.parse(data.object); 30 | // Depending on the browser version JSON.stringify stores typed arrays attributes or not 31 | // We are interested on length, so if it is not there we create it. 32 | if (typeof rawSpiceMessage.body.q.length === "undefined") { 33 | rawSpiceMessage.body.q.length = Object.keys(rawSpiceMessage.body.q).length; 34 | } 35 | var queue = new wdi.ViewQueue(); 36 | queue.setData(rawSpiceMessage.body.q); 37 | rawSpiceMessage.body = queue; 38 | displayPreProcess.process(wdi.PacketFactory.extract(rawSpiceMessage)); 39 | }; 40 | var aux = false; 41 | var ctxOrigin = $('')[0].getContext('2d'); 42 | var imgOrigin = new Image(); 43 | imgOrigin.onload = function() { 44 | ctxOrigin.drawImage(imgOrigin, 0, 0); 45 | if (aux) 46 | testFunction(); 47 | else 48 | aux = true; 49 | }; 50 | imgOrigin.src = data.origin.replace(/\s/g, '+'); 51 | var ctxExpected = $('')[0].getContext('2d'); 52 | var imgExpected = new Image(); 53 | imgExpected.onload = function() { 54 | ctxExpected.drawImage(imgExpected, 0, 0); 55 | if (aux) 56 | testFunction(); 57 | else 58 | aux = true; 59 | }; 60 | imgExpected.src = data.expected.replace(/\s/g, '+'); 61 | }); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /process/mainprocess.js: -------------------------------------------------------------------------------- 1 | wdi.MainProcess = $.spcExtend(wdi.EventObject.prototype, { 2 | init: function(c) { 3 | this.superInit(); 4 | this.app = c.app; 5 | this.spiceConnection = c.app.spiceConnection; 6 | this.agent = c.app.agent; 7 | }, 8 | 9 | process: function(spiceMessage) { 10 | var channel = this.spiceConnection.channels[wdi.SpiceVars.SPICE_CHANNEL_MAIN]; 11 | 12 | switch(spiceMessage.messageType) { 13 | case wdi.SpiceVars.SPICE_MSG_MAIN_INIT: 14 | channel.connectionId = spiceMessage.args.session_id; 15 | channel.fire('connectionId', channel.connectionId); 16 | if(spiceMessage.args.agent_connected == 1) { 17 | channel.fire('initAgent', spiceMessage.args.agent_tokens); 18 | } 19 | channel.fire('mouseMode', spiceMessage.args.current_mouse_mode); 20 | // the mouse mode must be change both if we have agent or not 21 | if (spiceMessage.args.supported_mouse_modes & wdi.SpiceMouseModeTypes.SPICE_MOUSE_MODE_CLIENT) 22 | this.changeMouseMode(); 23 | break; 24 | case wdi.SpiceVars.SPICE_MSG_MAIN_AGENT_DATA: 25 | var packet = spiceMessage.args; 26 | this.agent.onAgentData(packet); 27 | break; 28 | case wdi.SpiceVars.SPICE_MSG_MAIN_AGENT_CONNECTED: 29 | channel.fire('initAgent', spiceMessage.args.agent_tokens); 30 | this.changeMouseMode(); 31 | break; 32 | case wdi.SpiceVars.SPICE_MSG_MAIN_MULTI_MEDIA_TIME: 33 | this.app.multimediaTime = spiceMessage.args.multimedia_time; 34 | break; 35 | case wdi.SpiceVars.SPICE_MSG_MAIN_CHANNELS_LIST: 36 | channel.fire('channelListAvailable', spiceMessage.args.channels); 37 | break; 38 | case wdi.SpiceVars.SPICE_MSG_MAIN_MOUSE_MODE: 39 | channel.fire('mouseMode', spiceMessage.args.current_mode); 40 | if (spiceMessage.args.current_mode != wdi.SpiceMouseModeTypes.SPICE_MOUSE_MODE_CLIENT && 41 | spiceMessage.args.supported_modes & wdi.SpiceMouseModeTypes.SPICE_MOUSE_MODE_CLIENT) 42 | this.changeMouseMode(); 43 | break; 44 | } 45 | }, 46 | 47 | changeMouseMode: function() { 48 | var packet = new wdi.SpiceMessage({ 49 | messageType: wdi.SpiceVars.SPICE_MSGC_MAIN_MOUSE_MODE_REQUEST, 50 | channel: wdi.SpiceVars.SPICE_CHANNEL_MAIN, 51 | args: new wdi.SpiceMouseModeRequest({ 52 | request_mode: wdi.SpiceMouseModeTypes.SPICE_MOUSE_MODE_CLIENT 53 | }) 54 | }); 55 | this.spiceConnection.send(packet); 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /lib/AsyncWorker.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.AsyncWorker = $.spcExtend(wdi.EventObject.prototype, { 34 | worker: null, 35 | fn: null, 36 | scope: null, 37 | params: null, 38 | 39 | init: function(c) { 40 | this.superInit(); 41 | this.worker = new Worker(c.script); 42 | var self = this; 43 | this.worker.addEventListener("message", function (oEvent) { 44 | self.fn.call(self.scope, oEvent.data, self.params); 45 | }); 46 | }, 47 | 48 | run: function(data, fn, params, scope) { 49 | this.fn = fn; 50 | this.scope = scope; 51 | this.params = params; 52 | 53 | if (wdi.postMessageW3CCompilant) { 54 | this.worker.postMessage(data, [data]); 55 | } else { 56 | this.worker.postMessage(data); 57 | } 58 | }, 59 | 60 | dispose: function () { 61 | this.worker.terminate(); 62 | } 63 | }); 64 | -------------------------------------------------------------------------------- /network/clusternodechooser.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.ClusterNodeChooser = $.spcExtend(wdi.EventObject.prototype, { 34 | init: function (c) { 35 | }, 36 | 37 | setNodeList: function (nodeList) { 38 | this._nodeList = this._shuffle(nodeList); 39 | this._nodeListLength = this._nodeList.length; 40 | this._currentIndex = 0; 41 | }, 42 | 43 | getAnother: function () { 44 | var toReturn = this._nodeList[this._currentIndex++ % this._nodeListLength]; 45 | return toReturn; 46 | }, 47 | 48 | // recipe from: http://stackoverflow.com/a/6274398 49 | _shuffle: function (list) { 50 | var counter = list.length, 51 | temp, 52 | index; 53 | while (counter > 0) { 54 | index = Math.floor(Math.random() * counter); 55 | counter--; 56 | temp = list[counter]; 57 | list[counter] = list[index]; 58 | list[index] = temp; 59 | } 60 | return list; 61 | } 62 | }); 63 | -------------------------------------------------------------------------------- /network/packetcontroller.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.PacketController = $.spcExtend(wdi.EventObject.prototype, { 34 | sizeDefiner: null, 35 | packetExtractor: null, 36 | 37 | init: function(c) { 38 | this.superInit(); 39 | this.sizeDefiner = c.sizeDefiner; 40 | this.packetExtractor = c.packetExtractor; 41 | }, 42 | 43 | getNextPacket: function(data) { 44 | var self = this; 45 | if (wdi.logOperations) { 46 | wdi.DataLogger.setNetworkTimeStart(); 47 | } 48 | var size = this.sizeDefiner.getSize(data); 49 | this.packetExtractor.getBytes(size, function(bytes) { 50 | var status = this.sizeDefiner.getStatus(); 51 | 52 | this.execute(new wdi.RawMessage({status: status, data: bytes})); 53 | 54 | self.getNextPacket(bytes); 55 | 56 | 57 | }, this); 58 | }, 59 | 60 | execute: function(message) { 61 | try { 62 | this.fire('chunkComplete', message); 63 | } catch (e) { 64 | console.error('PacketTroller: ', e); 65 | } 66 | } 67 | }); 68 | -------------------------------------------------------------------------------- /unittest/displayprocess.test.js: -------------------------------------------------------------------------------- 1 | suite('DisplayProcess', function() { 2 | var sut; 3 | var runQ; 4 | var fakePacketFilter; 5 | var fakeClientGui; 6 | var displayRouter; 7 | var packetWorkerIdentifier; 8 | 9 | setup(function(){ 10 | wdi.Debug.debug = false; //disable debugging, it slows tests 11 | }); 12 | 13 | suite('#process()', function() { 14 | setup(function() { 15 | runQ = new wdi.RunQueue(); 16 | 17 | fakePacketFilter = { 18 | notifyEnd: function() { 19 | 20 | }, 21 | 22 | filter: function(o, fn, scope) { 23 | fn.call(scope, o); 24 | } 25 | }; 26 | 27 | fakeClientGui = {}; 28 | 29 | displayRouter = new wdi.DisplayRouter(); 30 | 31 | packetWorkerIdentifier = { 32 | getImageProperties: function() { 33 | return false; 34 | } 35 | }; 36 | 37 | displayRouter.packetProcess = function(spiceMessage) {}; //replace packetProcess, because of partial mocking 38 | sut = new wdi.DisplayProcess({ 39 | runQ: runQ, 40 | packetFilter: fakePacketFilter, 41 | clientGui: fakeClientGui, 42 | displayRouter: displayRouter, 43 | packetWorkerIdentifier: packetWorkerIdentifier 44 | }); 45 | 46 | 47 | wdi.ExecutionControl.sync = true; 48 | }); 49 | 50 | test('displayProcess process should call packetFilfer process', sinon.test(function() { 51 | var fakeProxy = { 52 | end:function() { 53 | 54 | } 55 | }; 56 | runQ.add = function(fnStart, scope, fnEnd) { 57 | fnStart.call(scope, fakeProxy); 58 | } 59 | this.mock(fakePacketFilter) 60 | .expects('filter') 61 | .once(); 62 | sut._process(false); 63 | })); 64 | 65 | test('displayProcess process should call packetFilfer notifyEnd', sinon.test(function() { 66 | runQ.add = function(fn, scope, endFn) { 67 | fn.call(scope,{end:function(){}}); 68 | endFn.call(scope); 69 | }; 70 | 71 | this.mock(fakePacketFilter) 72 | .expects('notifyEnd') 73 | .once(); 74 | sut._process(false); 75 | })); 76 | 77 | test('displayProcess process should call runq add', sinon.test(function() { 78 | this.mock(runQ) 79 | .expects('add') 80 | .once(); 81 | sut._process(false); 82 | })); 83 | 84 | 85 | test('displayProcess process should call runq process', sinon.test(function() { 86 | this.mock(runQ) 87 | .expects('process') 88 | .once(); 89 | sut._process(false); 90 | })); 91 | 92 | test('displayProcess process should call runq process', sinon.test(function() { 93 | this.mock(runQ) 94 | .expects('process') 95 | .once(); 96 | sut._process(false); 97 | })); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /unittest/packetlinkfactory.test.js: -------------------------------------------------------------------------------- 1 | suite('PacketLinkFactory', function() { 2 | setup(function() { 3 | wdi.Debug.debug = false; //disable debugging, it slows tests 4 | }); 5 | 6 | suite('#extract()', function() { 7 | test('Should extract generation from a RedSetAck', function() { 8 | var arr = [0x01, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00]; 9 | var myHeader = new wdi.SpiceDataHeader({type:wdi.SpiceVars.SPICE_MSG_SET_ACK, size: 8}); 10 | var queue = new wdi.ViewQueue(); 11 | queue.push(arr); 12 | var spiceSetAck = wdi.PacketLinkFactory.extract(myHeader, queue); 13 | assert.strictEqual(spiceSetAck.generation, 1); 14 | }); 15 | 16 | test('Should extract id from a RedPing', function() { 17 | var arr = [0x01, 0x00, 0x00, 0x00, 0xea, 0x00, 0x72, 0xc3, 0x00, 0x00, 0x00, 0x00]; 18 | var myHeader = new wdi.SpiceDataHeader({type:wdi.SpiceVars.SPICE_MSG_PING, size: 12}); 19 | var queue = new wdi.ViewQueue(); 20 | queue.push(arr); 21 | var spicePing = wdi.PacketLinkFactory.extract(myHeader, queue); 22 | assert.strictEqual(spicePing.id, 1); 23 | }); 24 | 25 | test('Should extract time from a RedPing and it will be an instance of BigInteger', function() { 26 | var arr = [0x01, 0x00, 0x00, 0x00, 0xea, 0x00, 0x72, 0xc3, 0x00, 0x00, 0x00, 0x00]; 27 | var myHeader = new wdi.SpiceDataHeader({type:wdi.SpiceVars.SPICE_MSG_PING, size: 12}); 28 | var queue = new wdi.ViewQueue(); 29 | queue.push(arr); 30 | var spicePing = wdi.PacketLinkFactory.extract(myHeader, queue); 31 | assert.instanceOf(spicePing.time, BigInteger); 32 | }); 33 | 34 | test('Should extract a Migrate message', function() { 35 | var arr = [0x0a, 0x00, 0x00, 0x00]; 36 | var myHeader = new wdi.SpiceDataHeader({type:wdi.SpiceVars.SPICE_MSG_MIGRATE, size: 4}); 37 | var queue = new wdi.ViewQueue(); 38 | queue.push(arr); 39 | var spiceMigrate = wdi.PacketLinkFactory.extract(myHeader, queue); 40 | assert.strictEqual(spiceMigrate.flags, 10); 41 | }); 42 | 43 | test('Should extract a Migrate Data message that is a vector', function() { 44 | var arr = [0x0a, 0x03, 0x02, 0x05]; 45 | var myHeader = new wdi.SpiceDataHeader({type:wdi.SpiceVars.SPICE_MSG_MIGRATE_DATA, size: 4}); 46 | var queue = new wdi.Queue(); 47 | queue.push(arr); 48 | var spiceMigrateData = wdi.PacketLinkFactory.extract(myHeader, queue); 49 | assert.deepEqual(spiceMigrateData.vector, [10, 3, 2, 5]); 50 | }); 51 | }); 52 | }); 53 | 54 | suite('PacketLinkProcess', function() { 55 | setup(function() { 56 | wdi.Debug.debug = false; //disable debugging, it slows tests 57 | }); 58 | 59 | suite('#process()', function() { 60 | test('Should process spice main init', function() { 61 | 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /lib/jquery-mousewheel.js: -------------------------------------------------------------------------------- 1 | /*! Copyright (c) 2011 Brandon Aaron (http://brandonaaron.net) 2 | * Licensed under the MIT License (LICENSE.txt). 3 | * 4 | * Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers. 5 | * Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix. 6 | * Thanks to: Seamus Leahy for adding deltaX and deltaY 7 | * 8 | * Version: 3.0.6 9 | * 10 | * Requires: 1.2.2+ 11 | */ 12 | 13 | (function($) { 14 | 15 | var types = ['DOMMouseScroll', 'mousewheel']; 16 | 17 | if ($.event.fixHooks) { 18 | for ( var i=types.length; i; ) { 19 | $.event.fixHooks[ types[--i] ] = $.event.mouseHooks; 20 | } 21 | } 22 | 23 | $.event.special.mousewheel = { 24 | setup: function() { 25 | if ( this.addEventListener ) { 26 | for ( var i=types.length; i; ) { 27 | this.addEventListener( types[--i], handler, false ); 28 | } 29 | } else { 30 | this.onmousewheel = handler; 31 | } 32 | }, 33 | 34 | teardown: function() { 35 | if ( this.removeEventListener ) { 36 | for ( var i=types.length; i; ) { 37 | this.removeEventListener( types[--i], handler, false ); 38 | } 39 | } else { 40 | this.onmousewheel = null; 41 | } 42 | } 43 | }; 44 | 45 | $.fn.extend({ 46 | mousewheel: function(fn) { 47 | return fn ? this.bind("mousewheel", fn) : this.trigger("mousewheel"); 48 | }, 49 | 50 | unmousewheel: function(fn) { 51 | return this.unbind("mousewheel", fn); 52 | } 53 | }); 54 | 55 | 56 | function handler(event) { 57 | var orgEvent = event || window.event, args = [].slice.call( arguments, 1 ), delta = 0, returnValue = true, deltaX = 0, deltaY = 0; 58 | event = $.event.fix(orgEvent); 59 | event.type = "mousewheel"; 60 | 61 | // Old school scrollwheel delta 62 | if ( orgEvent.wheelDelta ) { delta = orgEvent.wheelDelta/120; } 63 | if ( orgEvent.detail ) { delta = -orgEvent.detail/3; } 64 | 65 | // New school multidimensional scroll (touchpads) deltas 66 | deltaY = delta; 67 | 68 | // Gecko 69 | if ( orgEvent.axis !== undefined && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) { 70 | deltaY = 0; 71 | deltaX = -1*delta; 72 | } 73 | 74 | // Webkit 75 | if ( orgEvent.wheelDeltaY !== undefined ) { deltaY = orgEvent.wheelDeltaY/120; } 76 | if ( orgEvent.wheelDeltaX !== undefined ) { deltaX = -1*orgEvent.wheelDeltaX/120; } 77 | 78 | // Add event and delta to the front of the arguments 79 | args.unshift(event, delta, deltaX, deltaY); 80 | 81 | return ($.event.dispatch || $.event.handle).apply(this, args); 82 | } 83 | 84 | })(jQuery); -------------------------------------------------------------------------------- /network/packetextractor.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.PacketExtractor = $.spcExtend(wdi.EventObject.prototype, { 34 | socketQ: null, 35 | numBytes: null, 36 | callback: null, 37 | scope: null, 38 | 39 | init: function(c) { 40 | this.superInit(); 41 | this.socketQ = c.socketQ; 42 | this.setListener(); 43 | }, 44 | 45 | setListener: function() { 46 | this.socketQ.addListener('message', function() { 47 | if (wdi.logOperations) { 48 | wdi.DataLogger.setNetworkTimeStart(); 49 | } 50 | this.getBytes(this.numBytes, this.callback, this.scope); 51 | }, this); 52 | }, 53 | 54 | getBytes: function(numBytes, callback, scope) { 55 | var retLength = this.socketQ.rQ.getLength(); 56 | this.numBytes = numBytes; 57 | this.callback = callback; 58 | this.scope = scope; 59 | 60 | if (numBytes !== null && retLength >= numBytes) { 61 | var ret; 62 | if (numBytes) { 63 | ret = this.socketQ.rQ.shift(numBytes); 64 | } else { 65 | ret = new Uint8Array(0); 66 | } 67 | this.numBytes = null; 68 | this.callback = null; 69 | this.scope = null; 70 | callback.call(scope, ret); 71 | } else { 72 | if (wdi.logOperations) { 73 | wdi.DataLogger.logNetworkTime(); 74 | } 75 | } 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /network/connectioncontrol.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.ConnectionControl = $.spcExtend(wdi.EventObject.prototype, { 34 | socket: null, 35 | pendingTimeToConnectionLost: null, 36 | previousTimeOut: null, 37 | 38 | init: function(c) { 39 | this.superInit(); 40 | this.socket = c.socket || new wdi.Socket(); 41 | }, 42 | 43 | connect: function(c) { 44 | var url = wdi.Utils.generateWebSocketUrl(c.protocol, c.host, c.port, null, null,'raw', c.heartbeatToken); 45 | this.socket.connect(url); 46 | this.pendingTimeToConnectionLost = c.heartbeatTimeout; 47 | wdi.Debug.log('ConnectionControl: connected'); 48 | this.setListeners(); 49 | }, 50 | 51 | disconnect: function() { 52 | if(this.previousTimeOut){ 53 | clearTimeout(this.previousTimeOut); 54 | } 55 | this.socket.disconnect(); 56 | }, 57 | 58 | setListeners: function() { 59 | var self = this; 60 | this.socket.setOnMessageCallback(function(e) { 61 | wdi.Debug.log('ConectionControl: beat'); 62 | clearTimeout(self.previousTimeOut); 63 | self.previousTimeOut = setTimeout(function() { 64 | wdi.Debug.log('ConnectionControl: firing connectionLost event'); 65 | self.fire('connectionLost', e); 66 | }, self.pendingTimeToConnectionLost); 67 | }); 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /unittest/viewqueue.test.js: -------------------------------------------------------------------------------- 1 | suite('ViewQueue', function() { 2 | setup(function(){ 3 | wdi.Debug.debug = false; //disable debugging, it slows tests 4 | }); 5 | 6 | suite('#getLength()', function() { 7 | 8 | test('Should return 0 for empty queue', function() { 9 | var q = new wdi.ViewQueue(); 10 | assert.strictEqual(q.getLength(), 0); 11 | }); 12 | }); 13 | 14 | suite('#push()', function() { 15 | setup(function() { 16 | this.q = new wdi.ViewQueue(); 17 | }); 18 | 19 | test('Should be able to add elements as string', function() { 20 | this.q.push('hello'); 21 | assert.strictEqual(this.q.getLength(), 5); 22 | }); 23 | 24 | test('Should be able to add arrays', function() { 25 | this.q.push([1,2,3,4,5]); 26 | assert.strictEqual(this.q.getLength(), 5); 27 | }); 28 | 29 | test('Should be able to push multiple arrays', function() { 30 | this.q.push([1,2,3,4,5]); 31 | this.q.push([1,2,3,4,5]); 32 | assert.strictEqual(this.q.getLength(), 10); 33 | }); 34 | }); 35 | 36 | suite('#shift()', function() { 37 | setup(function() { 38 | this.q = new wdi.ViewQueue(); 39 | this.q.push([1,2,3,4,5]); 40 | }); 41 | 42 | test('Should allways return array', function() { 43 | var element = this.q.shift(1); 44 | assert.isArray(element); 45 | }); 46 | 47 | test('Should read parts of the queue', function() { 48 | var elements = this.q.shift(2); 49 | assert.deepEqual(elements, [1,2]); 50 | }); 51 | 52 | test('Should read all the queue', function() { 53 | var elements = this.q.shift(5); 54 | assert.deepEqual(elements, [1,2,3,4,5]); 55 | }); 56 | 57 | test('Should empty all the queue', function() { 58 | var elements = this.q.shift(5); 59 | assert.strictEqual(this.q.getLength(), 0); 60 | }); 61 | 62 | test('Should empty parts of the queue', function() { 63 | var elements = this.q.shift(2); 64 | assert.strictEqual(this.q.getLength(), 3); 65 | }); 66 | }); 67 | 68 | suite('#peek()', function() { 69 | setup(function() { 70 | this.q = new wdi.ViewQueue(); 71 | this.q.push([1,2,3,4,5]); 72 | }); 73 | 74 | test('Should read a single element', function() { 75 | var element = this.q.peek(0, 1); 76 | assert.deepEqual(element, [1]); 77 | }); 78 | 79 | test('Should read 3 elements of the queue', function() { 80 | var elements = this.q.peek(1, 4); 81 | assert.deepEqual(elements, [2,3,4]); 82 | }); 83 | 84 | test('Should read all the elements of the queue', function() { 85 | var elements = this.q.peek(0); 86 | assert.deepEqual(elements, [1,2,3,4,5]); 87 | }); 88 | 89 | test('Should be immutable', function() { 90 | this.q.peek(1, 4); 91 | assert.strictEqual(this.q.getLength(), 5); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /lib/timelapsedetector.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | 34 | wdi.TimeLapseDetector = $.spcExtend(wdi.EventObject.prototype, { 35 | lastTime: null, 36 | 37 | init: function timeLapseDetector_Init (c) { 38 | this.superInit(); 39 | }, 40 | 41 | startTimer: function timeLapseDetector_startTimer () { 42 | var self = this; 43 | this.lastTime = Date.now(); 44 | 45 | window.setInterval( 46 | function timeLapseDetectorInterval () { 47 | var now = Date.now(); 48 | // this.constructor == access to the class itself, so you 49 | // can access to static properties without writing/knowing 50 | // the class name 51 | var elapsed = now - self.lastTime; 52 | if (elapsed >= self.constructor.maxIntervalAllowed) { 53 | self.fire('timeLapseDetected', elapsed); 54 | } 55 | self.lastTime = now; 56 | }, 57 | wdi.TimeLapseDetector.defaultInterval 58 | ); 59 | }, 60 | 61 | getLastTime: function timeLapseDetector_getLastTime () { 62 | return this.lastTime; 63 | }, 64 | 65 | setLastTime: function timeLapseDetector_setLastTime (lastTime) { 66 | this.lastTime = lastTime; 67 | return this; 68 | } 69 | }); 70 | 71 | wdi.TimeLapseDetector.defaultInterval = 5000; 72 | wdi.TimeLapseDetector.maxIntervalAllowed = wdi.TimeLapseDetector.defaultInterval * 3; 73 | -------------------------------------------------------------------------------- /network/websocketwrapper.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.WebSocketWrapper = $.spcExtend({}, { 34 | ws: {}, 35 | onopen: null, 36 | onmessage: null, 37 | onclose: null, 38 | onerror: null, 39 | 40 | init: function() { 41 | 42 | }, 43 | 44 | connect: function(url, protocol) { 45 | this.ws = new WebSocket(url, protocol); 46 | }, 47 | 48 | onOpen: function(callback) { 49 | this.ws.onopen = callback; 50 | }, 51 | 52 | onMessage: function(callback) { 53 | this.ws.onmessage = callback; 54 | }, 55 | 56 | onClose: function(callback) { 57 | this.ws.onclose = callback; 58 | }, 59 | 60 | onError: function(callback) { 61 | this.ws.onerror = callback; 62 | }, 63 | 64 | setBinaryType: function(type) { 65 | this.ws.binaryType = type; 66 | }, 67 | 68 | close: function() { 69 | if (!this.ws || !this.ws.close) { 70 | return; 71 | } 72 | 73 | this.ws.close(); 74 | this.ws.onopen = function () {}; 75 | this.ws.onmessage = function () {}; 76 | this.ws.onclose = function () {}; 77 | this.ws.onerror = function () {}; 78 | this.onopen = function() {}; 79 | this.onmessage = function() {}; 80 | this.onclose = function() {}; 81 | this.onerror = function() {}; 82 | 83 | }, 84 | 85 | send: function(message) { 86 | this.ws.send(message); 87 | } 88 | }); 89 | -------------------------------------------------------------------------------- /unittest/queue.test.js: -------------------------------------------------------------------------------- 1 | suite('Queue', function() { 2 | setup(function(){ 3 | wdi.Debug.debug = false; //disable debugging, it slows tests 4 | }); 5 | 6 | suite('#getLength()', function() { 7 | 8 | test('Should return 0 for empty queue', function() { 9 | var q = new wdi.Queue(); 10 | assert.strictEqual(q.getLength(), 0); 11 | }); 12 | }); 13 | 14 | suite('#push()', function() { 15 | setup(function() { 16 | this.q = new wdi.Queue(); 17 | }); 18 | 19 | test('Should be able to add elements as string', function() { 20 | this.q.push('hello'); 21 | assert.strictEqual(this.q.getLength(), 5); 22 | }); 23 | 24 | test('Should be able to add arrays', function() { 25 | this.q.push(['h','e', 'l', 'l', 'o']); 26 | assert.strictEqual(this.q.getLength(), 5); 27 | }); 28 | 29 | test('Should be able to push multiple arrays', function() { 30 | this.q.push(['h','e', 'l', 'l', 'o']); 31 | this.q.push(['f','e', 'l', 'l', 'o']); 32 | assert.strictEqual(this.q.getLength(), 10); 33 | }); 34 | }); 35 | 36 | suite('#shift()', function() { 37 | setup(function() { 38 | this.q = new wdi.Queue(); 39 | this.q.push([1,2, 3, 4, 5]); 40 | }); 41 | 42 | test('Should allways return array', function() { 43 | var element = this.q.shift(1); 44 | assert.isArray(element); 45 | }); 46 | 47 | test('Should read parts of the queue', function() { 48 | var elements = this.q.shift(2); 49 | assert.deepEqual(elements, [1, 2]); 50 | }); 51 | 52 | test('Should read all the queue', function() { 53 | var elements = this.q.shift(5); 54 | assert.deepEqual(elements, [1,2, 3, 4, 5]); 55 | }); 56 | 57 | test('Should empty all the queue', function() { 58 | var elements = this.q.shift(5); 59 | assert.strictEqual(this.q.getLength(), 0); 60 | }); 61 | 62 | test('Should empty parts of the queue', function() { 63 | var elements = this.q.shift(2); 64 | assert.strictEqual(this.q.getLength(), 3); 65 | }); 66 | }); 67 | 68 | suite('#peek()', function() { 69 | setup(function() { 70 | this.q = new wdi.Queue(); 71 | this.q.push([1,2, 3, 4, 5]); 72 | }); 73 | 74 | test('Should read a single element', function() { 75 | var element = this.q.peek(0, 1); 76 | assert.deepEqual(element, [1]); 77 | }); 78 | 79 | test('Should read 3 elements of the queue', function() { 80 | var elements = this.q.peek(1, 4); 81 | assert.deepEqual(elements, [2, 3, 4]); 82 | }); 83 | 84 | test('Should read all the elements of the queue', function() { 85 | var elements = this.q.peek(0); 86 | assert.deepEqual(elements, [1,2, 3, 4, 5]); 87 | }); 88 | 89 | test('Should be immutable', function() { 90 | this.q.peek(1, 4); 91 | assert.strictEqual(this.q.getLength(), 5); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /unittest/spiceconnection.test.js: -------------------------------------------------------------------------------- 1 | suite('SpiceConnection', function() { 2 | setup(function(){ 3 | wdi.Debug.debug = false; //disable debugging, it slows tests 4 | }); 5 | 6 | suite('#connect()', function() { 7 | setup(function() { 8 | this.mainChannel = new wdi.SpiceChannel(); 9 | this.mock = sinon.mock(this.mainChannel); 10 | this.sut = this.spcConnect = new wdi.SpiceConnection({ 11 | mainChannel:this.mainChannel, 12 | connectionControl: { 13 | connect: function() {}, 14 | addListener: function() {} 15 | } 16 | }); 17 | 18 | }); 19 | 20 | test('Should call connect on the main channel', function() { 21 | this.expectation = this.mock.expects('connect').once(); 22 | this.spcConnect.connect('localhost', 8000); 23 | this.mock.verify(); 24 | }); 25 | 26 | test('Should call connect on the main channel with the correct arguments', function() { 27 | this.expectation = this.mock.expects('connect').once().withArgs({host:'localhost', port:8000}, wdi.SpiceVars.SPICE_CHANNEL_MAIN); 28 | this.spcConnect.connect({host:'localhost', port:8000}); 29 | this.mock.verify(); 30 | }); 31 | 32 | test('Should call connect on the connectionControl with the correct arguments', function() { 33 | var connectionInfo = { 34 | connectionControl: true 35 | }; 36 | this.expectation = this.mock.expects('connect').once().withArgs(connectionInfo); 37 | this.spcConnect.connect(connectionInfo); 38 | this.mock.verify(); 39 | }); 40 | 41 | test.skip('When a channel fire a channelConnected message should fire channelConnected message with channel', function() { 42 | var channel; 43 | this.sut.addListener('channelConnected', function (e) { 44 | channel = e[1]; 45 | }, this); 46 | 47 | this.mainChannel.fire('channelConnected'); 48 | 49 | assert.equal(channel, wdi.SpiceVars.SPICE_CHANNEL_MAIN); 50 | }); 51 | 52 | }); 53 | 54 | suite('#connectionId()', function() { 55 | setup(function() { 56 | this.mainChannel = new wdi.SpiceChannel(); 57 | this.stub = sinon.stub(this.mainChannel, "connect", function() { 58 | this.fire("connectionId", "12345"); 59 | this.fire("channelListAvailable", [1,2]); 60 | }); 61 | 62 | this.displayChannel = new wdi.SpiceChannel(); 63 | this.mock = sinon.mock(this.displayChannel); 64 | 65 | this.spcConnect = new wdi.SpiceConnection({ 66 | mainChannel:this.mainChannel, 67 | displayChannel:this.displayChannel, 68 | connectionControl: { 69 | connect: function() {}, 70 | addListener: function() {} 71 | } 72 | }); 73 | }); 74 | 75 | test('Should call connect on display channel when connectionId is available', function() { 76 | this.expectation = this.mock.expects('connect').once(); 77 | this.spcConnect.connect('localhost', 8000); 78 | this.mock.verify(); 79 | }); 80 | }); 81 | 82 | }); 83 | 84 | -------------------------------------------------------------------------------- /unittest/socketqueue.test.js: -------------------------------------------------------------------------------- 1 | suite('SocketQueue', function() { 2 | setup(function(){ 3 | wdi.Debug.debug = false; //disable debugging, it slows tests 4 | }); 5 | 6 | suite('#connect()', function() { 7 | setup(function() { 8 | this.socket = new wdi.Socket(); 9 | this.mock = sinon.mock(this.socket); 10 | this.expectation = this.mock.expects('connect').once(); 11 | this.socketQ = new wdi.SocketQueue({socket: this.socket}); 12 | }); 13 | 14 | test('Should call method connect from socket', function() { 15 | this.socketQ.connect('ws://localhost'); 16 | this.expectation.verify(); 17 | }); 18 | 19 | teardown(function() { 20 | this.mock.restore(); 21 | }); 22 | }); 23 | 24 | suite('#disconnect()', function() { 25 | setup(function() { 26 | this.socket = new wdi.Socket(); 27 | this.mock = sinon.mock(this.socket); 28 | this.expectation = this.mock.expects('disconnect').once(); 29 | this.socketQ = new wdi.SocketQueue({socket: this.socket}); 30 | }); 31 | 32 | test('Should call method disconnect from socket', function() { 33 | this.socketQ.disconnect(); 34 | this.expectation.verify(); 35 | }); 36 | 37 | teardown(function() { 38 | this.mock.restore(); 39 | }); 40 | }); 41 | 42 | suite('#getStatus()', function() { 43 | setup(function() { 44 | this.socket = new wdi.Socket(); 45 | this.mock = sinon.mock(this.socket); 46 | this.expectation = this.mock.expects('getStatus').once(); 47 | this.socketQ = new wdi.SocketQueue({socket:this.socket}); 48 | }); 49 | 50 | test('Should call method getStatus from socket', function() { 51 | this.socketQ.getStatus(); 52 | this.expectation.verify(); 53 | }); 54 | 55 | teardown(function() { 56 | this.mock.restore(); 57 | }); 58 | }); 59 | 60 | suite('#send()', function() { 61 | setup(function() { 62 | this.queue = new wdi.Queue(); 63 | this.mock = sinon.mock(this.queue); 64 | this.expectation = this.mock.expects('push').once(); 65 | this.socketQ = new wdi.SocketQueue({sQ:this.queue}); 66 | this.socketQ.connect('ws://localhost'); 67 | }); 68 | 69 | test('Should call send queue push on send(data, false)', function() { 70 | this.socketQ.send([0x23], false); 71 | this.expectation.verify(); 72 | }); 73 | 74 | teardown(function() { 75 | this.mock.restore(); 76 | }); 77 | }); 78 | 79 | suite('#flush()', function() { 80 | setup(function() { 81 | this.socket = new wdi.Socket(); 82 | this.mock = sinon.mock(this.socket); 83 | this.expectation = this.mock.expects('send').once(); 84 | this.socketQ = new wdi.SocketQueue({socket: this.socket}); 85 | this.socketQ.connect('ws://localhost'); 86 | }); 87 | 88 | test('Should call socket send', function() { 89 | this.socketQ.flush(); 90 | this.expectation.verify(); 91 | }); 92 | 93 | teardown(function() { 94 | this.mock.restore(); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /swcanvas/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Test SWCanvas 4 | 5 | 6 | 54 | 55 | 56 | cat: 57 | 58 | original: 59 | 60 | sw: 61 | 62 | original putimagedata: 63 | 64 | sw putimagedata: 65 | 66 |



67 | original fillrect: 68 | 69 | sw fillrect: 70 | 71 | 72 | -------------------------------------------------------------------------------- /process/cursorprocess.js: -------------------------------------------------------------------------------- 1 | wdi.CursorProcess = $.spcExtend(wdi.EventObject.prototype, { 2 | imageData: null, 3 | 4 | process: function(spiceMessage) { 5 | switch (spiceMessage.messageType) { 6 | case wdi.SpiceVars.SPICE_MSG_CURSOR_INIT: 7 | case wdi.SpiceVars.SPICE_MSG_CURSOR_SET: 8 | var wdiCursor = this.extractCursor(spiceMessage); 9 | if(wdiCursor) { 10 | wdi.VirtualMouse.setHotspot(0, 0); 11 | wdi.VirtualMouse.setMouse(wdiCursor.data, wdiCursor.header.hot_spot_x, wdiCursor.header.hot_spot_y); 12 | } 13 | break; 14 | } 15 | }, 16 | 17 | _toUrl:function(data) { 18 | var imageData = $('').attr({ 19 | 'width': data.width, 20 | 'height': data.height 21 | })[0]; 22 | var ctx = imageData.getContext('2d'); 23 | ctx.putImageData(data, 0, 0); 24 | return imageData.toDataURL("image/png"); 25 | }, 26 | 27 | extractCursor: function(spiceMessage) { 28 | var flags = spiceMessage.args.cursor.flags; 29 | var position = spiceMessage.args.position; 30 | var visible = spiceMessage.args.visible; 31 | 32 | //if there is no cursor, return null 33 | if(flags & 1) { 34 | return null; 35 | } 36 | 37 | var imageData = null; 38 | 39 | //cursor from cache? 40 | if(flags & wdi.SpiceCursorFlags.SPICE_CURSOR_FLAGS_FROM_CACHE) { 41 | imageData = wdi.ImageCache.getCursorFrom(spiceMessage.args.cursor); 42 | } else { 43 | //cursor from packet 44 | //any case should return url 45 | switch (spiceMessage.args.cursor.header.type) { 46 | 47 | case wdi.SpiceCursorType.SPICE_CURSOR_TYPE_ALPHA: 48 | imageData = this._toUrl(wdi.graphics.argbToImageData(spiceMessage.args.cursor.data, spiceMessage.args.cursor.header.width, spiceMessage.args.cursor.header.height)); 49 | break; 50 | case wdi.SpiceCursorType.SPICE_CURSOR_TYPE_MONO: 51 | imageData = this._toUrl(wdi.graphics.monoToImageData(spiceMessage.args.cursor.data, spiceMessage.args.cursor.header.width, spiceMessage.args.cursor.header.height)); 52 | break; 53 | case 8: 54 | imageData = wdi.SpiceObject.bytesToString(spiceMessage.args.cursor.data); 55 | break; 56 | case wdi.SpiceCursorType.SPICE_CURSOR_TYPE_COLOR4: 57 | case wdi.SpiceCursorType.SPICE_CURSOR_TYPE_COLOR8: 58 | case wdi.SpiceCursorType.SPICE_CURSOR_TYPE_COLOR16: 59 | case wdi.SpiceCursorType.SPICE_CURSOR_TYPE_COLOR24: 60 | case wdi.SpiceCursorType.SPICE_CURSOR_TYPE_COLOR32: 61 | case wdi.SpiceCursorType.SPICE_CURSOR_TYPE_ENUM_END: 62 | break; 63 | } 64 | } 65 | 66 | //got no cursor? error! 67 | if(!imageData) { 68 | return null; 69 | } 70 | 71 | //we have cursor, cache it? 72 | if(flags & wdi.SpiceCursorFlags.SPICE_CURSOR_FLAGS_CACHE_ME) { 73 | wdi.ImageCache.addCursor(spiceMessage.args.cursor, imageData); 74 | } 75 | 76 | return { 77 | data: imageData, 78 | position: position, 79 | visible: visible, 80 | header: spiceMessage.args.cursor.header 81 | }; 82 | } 83 | }); 84 | -------------------------------------------------------------------------------- /unittest/packetreassembler.test.js: -------------------------------------------------------------------------------- 1 | suite('PacketReassembler', function() { 2 | var sut, packetController, sizeDefinerConstant; 3 | 4 | setup(function() { 5 | wdi.Debug.debug = false; //disable debugging, it slows tests 6 | packetController = new wdi.PacketController(); 7 | sut = new wdi.PacketReassembler({packetController: packetController}); 8 | sizeDefinerConstant = wdi.SizeDefiner.prototype; 9 | }); 10 | 11 | suite.skip('#setListeners()', function() { 12 | 13 | test('Check it fires packetComplete event with reply', function() { 14 | var obtainedData; 15 | var message = new wdi.RawMessage({status: sizeDefinerConstant.STATUS_REPLY, data: [2]}); 16 | var message2 = new wdi.RawMessage({status: sizeDefinerConstant.STATUS_REPLY_BODY, data: [3]}); 17 | sut.addListener('packetComplete', function(e) { 18 | obtainedData = e[1]; 19 | }, this); 20 | packetController.fire('chunkComplete', message); 21 | packetController.fire('chunkComplete', message2); 22 | assert.equal(obtainedData.status, 'reply', 'The chunkComplete event with reply packet doesn\'t fire packetComplete event'); 23 | }); 24 | 25 | test('Check it fires packetComplete event with errorCode', function() { 26 | var obtainedData; 27 | var message = new wdi.RawMessage({status: sizeDefinerConstant.STATUS_ERROR_CODE, data: [2]}); 28 | sut.addListener('packetComplete', function(e) { 29 | obtainedData = e[1]; 30 | }, this); 31 | packetController.fire('chunkComplete', message); 32 | assert.equal(obtainedData.status, 'errorCode', 'The chunkComplete event with errorCode packet doesn\'t fire packetComplete event'); 33 | }); 34 | 35 | test('Check it waits to have data with header', function() { 36 | var obtainedData; 37 | var message = new wdi.RawMessage({status: sizeDefinerConstant.STATUS_HEADER, data: [2]}); 38 | var message2 = new wdi.RawMessage({status: sizeDefinerConstant.STATUS_BODY, data: [3]}); 39 | sut.addListener('packetComplete', function(e) { 40 | obtainedData = e[1]; 41 | }, this); 42 | packetController.fire('chunkComplete', message); 43 | packetController.fire('chunkComplete', message2); 44 | assert.equal(obtainedData.data.length, 2, 'The chunkComplete event with errorCode packet doesn\'t fire packetComplete event'); 45 | }); 46 | 47 | test('Check it waits to have the reply_body with header', function () { 48 | var obtainedData; 49 | var message = new wdi.RawMessage({status: sizeDefinerConstant.STATUS_REPLY, data: [2]}); 50 | var message2 = new wdi.RawMessage({status: sizeDefinerConstant.STATUS_REPLY_BODY, data: [3]}); 51 | sut.addListener('packetComplete', function(e) { 52 | obtainedData = e[1]; 53 | }, this); 54 | packetController.fire('chunkComplete', message); 55 | packetController.fire('chunkComplete', message2); 56 | assert.equal(obtainedData.data.length, 2, 'The chunkComplete event with reply_body packet doesn\'t fire packetComplete event with all data'); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /unittest/packetcontroller.test.js: -------------------------------------------------------------------------------- 1 | suite('PacketController', function() { 2 | var sut, sizeDefiner, packetExtractor, toRestore; 3 | 4 | setup(function() { 5 | wdi.Debug.debug = false; 6 | sizeDefiner = new wdi.SizeDefiner(); 7 | packetExtractor = new wdi.PacketExtractor({ 8 | socketQ: new wdi.SocketQueue() 9 | }); 10 | toRestore = []; 11 | sut = new wdi.PacketController({ 12 | sizeDefiner: sizeDefiner, 13 | packetExtractor: packetExtractor 14 | }); 15 | }); 16 | 17 | teardown(function() { 18 | toRestore.forEach(function(item) { 19 | item.restore(); 20 | }); 21 | }); 22 | 23 | suite('#getNextPacket()', function() { 24 | 25 | test('It fires chunkComplete event', function() { 26 | var called = false; 27 | var times = 0; 28 | var stub = sinon.stub(sizeDefiner, 'getSize'); 29 | toRestore.push(stub); 30 | stub = sinon.stub(packetExtractor, 'getBytes', function(numBytes, callback, scope) { 31 | if (!times++) 32 | callback.call(scope, [0, 4, 3, 3, 5, 2, 4]); 33 | }); 34 | toRestore.push(stub); 35 | sut.addListener('chunkComplete', function() { 36 | called = true; 37 | }, this); 38 | sut.getNextPacket(); 39 | assert.isTrue(called, 'The chunkComplete event never fired'); 40 | }); 41 | 42 | test('It calls getStatus from SizeDefiner', function() { 43 | var times = 0; 44 | var stub = sinon.stub(sizeDefiner, 'getSize'); 45 | toRestore.push(stub); 46 | stub = sinon.stub(packetExtractor, 'getBytes', function(numBytes, callback, scope) { 47 | if (!times++) 48 | callback.call(scope, [0, 4, 3, 3, 5, 2, 4]); 49 | }); 50 | toRestore.push(stub); 51 | var mock = sinon.mock(sizeDefiner); 52 | var expectation = mock.expects('getStatus').once(); 53 | toRestore.push(mock); 54 | sut.getNextPacket(); 55 | expectation.verify(); 56 | }); 57 | 58 | test('It calls getSize from sizeDefiner', function() { 59 | var mock = sinon.mock(sizeDefiner); 60 | var expectation = mock.expects('getSize').once(); 61 | 62 | toRestore.push(mock); 63 | var stub = sinon.stub(packetExtractor, 'getBytes'); 64 | toRestore.push(stub); 65 | sut.getNextPacket(); 66 | expectation.verify(); 67 | }); 68 | 69 | test('It calls getBytes from packetExtractor', function() { 70 | var mock = sinon.mock(packetExtractor); 71 | var expectation = mock.expects('getBytes').once(); 72 | 73 | toRestore.push(mock); 74 | var stub = sinon.stub(sizeDefiner, 'getSize'); 75 | toRestore.push(stub); 76 | sut.getNextPacket(); 77 | expectation.verify(); 78 | }); 79 | 80 | test('It calls getSize from sizeDefiner with the last data acquired', function() { 81 | var header = [4, 0, 12, 0, 0, 0]; 82 | var mock = sinon.mock(sizeDefiner); 83 | var expectation = mock.expects('getSize').once().withArgs(header); 84 | 85 | toRestore.push(mock); 86 | var stub = sinon.stub(packetExtractor, 'getBytes'); 87 | toRestore.push(stub); 88 | sut.getNextPacket(header); 89 | expectation.verify(); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /unittest/sizedefiner.test.js: -------------------------------------------------------------------------------- 1 | suite('SizeDefiner', function() { 2 | var sut; 3 | var headerArray = [4, 0, 12, 0, 0, 0]; 4 | var headerRLRArray = [4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0]; 5 | setup(function() { 6 | wdi.Debug.debug = false; 7 | sut = new wdi.SizeDefiner(); 8 | }); 9 | 10 | suite('#getSize()', function() { 11 | 12 | 13 | test('The first time it is called returns the Red Link header size', function() { 14 | var size = sut.getSize(); 15 | assert.equal(size, wdi.SpiceLinkHeader.prototype.objectSize, 'Red Link Header size doesn\'t match'); 16 | }); 17 | 18 | test('The second time it is called returns the Red Link Reply body size', function () { 19 | sut.getSize(); 20 | var size = sut.getSize(headerRLRArray); 21 | assert.equal(size, headerArray[2], 'Body syze size doesn\'t match'); 22 | }); 23 | 24 | test('The third time it is called returns the errorCode size', function() { 25 | sut.getSize(); 26 | sut.getSize(headerRLRArray); 27 | var size = sut.getSize(); 28 | assert.equal(size, 4, 'Error Code size doesn\'t match'); 29 | }); 30 | 31 | test('The fourth time it is called returns the Header size', function() { 32 | sut.getSize(); 33 | sut.getSize(headerRLRArray); 34 | sut.getSize(); 35 | var size = sut.getSize(); 36 | assert.equal(size, wdi.SpiceDataHeader.prototype.objectSize, 'Spice Header Size doesn\'t match'); 37 | }); 38 | 39 | test('From the fifth time we have it returns the size from a passed header', function() { 40 | sut.getSize(); 41 | sut.getSize(headerRLRArray); 42 | sut.getSize(); 43 | sut.getSize(); 44 | var size = sut.getSize(headerArray); 45 | assert.equal(size, 12, 'Spice Body Packet Size doesn\'t match'); 46 | }); 47 | 48 | test('The data must still be in the array after the call', function () { 49 | sut.getSize(); 50 | sut.getSize(headerRLRArray); 51 | sut.getSize(); 52 | sut.getSize(); 53 | var size = sut.getSize(headerArray); 54 | assert.equal(headerArray.length, 6, "The array doesn't have the data"); 55 | }); 56 | }); 57 | 58 | suite('#getStatus()', function() { 59 | 60 | test('Returns reply the first time', function() { 61 | sut.getSize(); 62 | assert.equal(sut.STATUS_REPLY, sut.getStatus()); 63 | }); 64 | 65 | test('Returns error code the third time', function() { 66 | sut.getSize(); 67 | sut.getSize(headerRLRArray); 68 | sut.getSize(); 69 | assert.equal(sut.STATUS_ERROR_CODE, sut.getStatus()); 70 | }); 71 | 72 | test('Returns header when header size is returned', function() { 73 | sut.getSize(); 74 | sut.getSize(headerRLRArray); 75 | sut.getSize(); 76 | sut.getSize(); 77 | assert.equal(sut.STATUS_HEADER, sut.getStatus()); 78 | }); 79 | 80 | test('Returns body when body size is returned', function() { 81 | sut.getSize(); 82 | sut.getSize(headerRLRArray); 83 | sut.getSize(); 84 | sut.getSize(); 85 | var size = sut.getSize(headerRLRArray); 86 | assert.equal(sut.STATUS_BODY, sut.getStatus()); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /unittest/packetextractor.test.js: -------------------------------------------------------------------------------- 1 | suite('PacketExtractor', function() { 2 | var sut, socketQ; 3 | 4 | setup(function() { 5 | wdi.Debug.debug= false; 6 | socketQ = new wdi.SocketQueue(); 7 | sut = new wdi.PacketExtractor({socketQ: socketQ}); 8 | }); 9 | 10 | suite('#getBytes()', function() { 11 | test('Check that callback is called once', function() { 12 | var size = 50, data = [], called = false; 13 | data.length = size; 14 | socketQ.rQ.setData(data); 15 | sut.getBytes(size, function(bytes) { 16 | called = true; 17 | }); 18 | assert.isTrue(called, 'The callback is never called'); 19 | }); 20 | 21 | test('Check queue has enough bytes and return them', function() { 22 | var size = 50, data = []; 23 | data.length = size; 24 | socketQ.rQ.setData(data); 25 | sut.getBytes(size, function(bytes) { 26 | assert.equal(size, bytes.length, 'The gathered data it is not the expected size'); 27 | }); 28 | }); 29 | 30 | test('Check callback is not called when not enough data', function() { 31 | var size = 50, data = [], called = false; 32 | data.length = 40; 33 | socketQ.rQ.setData(data); 34 | sut.getBytes(size, function(bytes) { 35 | called = true; 36 | }); 37 | assert.isFalse(called, 'The callback is unexpectedly called'); 38 | }); 39 | 40 | test('Check callback is called when enough data is received', function() { 41 | var size = 50, data = [], called = false; 42 | data.length = 40; 43 | socketQ.rQ.setData(data); 44 | sut.getBytes(size, function(bytes) { 45 | called = true; 46 | }); 47 | data.length = size; 48 | socketQ.rQ.setData(data); 49 | socketQ.fire('message'); 50 | assert.isTrue(called, 'The callback is never called'); 51 | }); 52 | 53 | test('Check that scope passed is used', function() { 54 | var size = 50, data = [], called = false, callbackScope = { 55 | scopeCheck: function() { 56 | called = true; 57 | } 58 | }; 59 | data.length = size; 60 | socketQ.rQ.setData(data); 61 | sut.getBytes(size, function(bytes) { 62 | this.scopeCheck(); 63 | }, callbackScope); 64 | assert.isTrue(called, 'The callback is never called'); 65 | }); 66 | 67 | test.skip('Check callback is called with the expected size after multiple socketQ messages', function() { 68 | var size = 50, data = [], numBytes = 0; 69 | data.length = 30; 70 | socketQ.rQ.setData(data); 71 | sut.getBytes(size, function(bytes) { 72 | numBytes = bytes.length; 73 | }); 74 | data = []; 75 | data.length = 10; 76 | socketQ.rQ.push(data); 77 | socketQ.fire('message'); 78 | data = []; 79 | data.length = 10; 80 | socketQ.rQ.push(data); 81 | socketQ.fire('message'); 82 | assert.equal(numBytes, size, 'The size of the received data is not the expected one'); 83 | }); 84 | 85 | test('When called without a callback it doesn\'t crash', function () { 86 | var size = 50, data = []; 87 | data.length = size; 88 | socketQ.rQ.setData(data); 89 | socketQ.fire('message'); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /application/imagecache.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.ImageCache = { 34 | images: {}, 35 | cursor: {}, 36 | palettes: {}, 37 | 38 | getImageFrom: function(descriptor, cb) { 39 | //see http://jsperf.com/todataurl-vs-getimagedata-to-base64/7 40 | var cnv = wdi.GlobalPool.create('Canvas'); 41 | var imgData = this.images[descriptor.id.toString()]; 42 | cnv.width = imgData.width; 43 | cnv.height = imgData.height; 44 | cnv.getContext('2d').putImageData(imgData,0,0); 45 | cb(cnv); 46 | }, 47 | 48 | isImageInCache: function(descriptor) { 49 | if(descriptor.id.toString() in this.images) { 50 | return true; 51 | } 52 | return false; 53 | }, 54 | 55 | delImage: function(id) { 56 | delete this.images[id.toString()]; 57 | }, 58 | 59 | addImage: function(descriptor, canvas) { 60 | if(canvas.getContext) { 61 | this.images[descriptor.id.toString()] = canvas.getContext('2d').getImageData(0,0,canvas.width, canvas.height); 62 | } else { 63 | this.images[descriptor.id.toString()] = canvas; 64 | } 65 | 66 | }, 67 | 68 | getCursorFrom: function(cursor) { 69 | return this.cursor[cursor.header.unique.toString()]; 70 | }, 71 | 72 | addCursor: function(cursor, imageData) { 73 | this.cursor[cursor.header.unique.toString()] = imageData; 74 | }, 75 | 76 | getPalette: function(id) { 77 | return this.palettes[id.toString()]; 78 | }, 79 | 80 | addPalette: function(id, palette) { 81 | this.palettes[id.toString()] = palette; 82 | }, 83 | 84 | clearPalettes: function() { 85 | this.palettes = {}; 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /application/virtualmouse.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.VirtualMouse = { 34 | eventLayers: [], 35 | mouseData:null, 36 | visible: null, 37 | lastLayer: null, 38 | hotspot: { 39 | x: 0, 40 | y: 0 41 | }, 42 | lastMousePosition: { 43 | x: 0, 44 | y: 0, 45 | width: 0, 46 | height: 0 47 | }, 48 | 49 | setHotspot: function(x, y) { 50 | this.hotspot.x = x; 51 | this.hotspot.y = y; 52 | }, 53 | 54 | setEventLayer: function(ev, x, y, width, height, position) { 55 | this.eventLayers.push({ 56 | layer: ev, 57 | left: x, 58 | top: y, 59 | right: x+width, 60 | bottom: y+height, 61 | position: position 62 | }); 63 | }, 64 | 65 | removeEventLayer: function(ev) { 66 | var len = this.eventLayers.length; 67 | for(var i=0;i= layer.left && x <= layer.right && y >= layer.top && y <= layer.bottom) { 80 | return layer.layer; 81 | } 82 | } 83 | }, 84 | 85 | setMouse: function(mouseData, x, y) { 86 | //if(!Modernizr.touch) { 87 | var layer = null; 88 | var len = this.eventLayers.length; 89 | for(var i=0;i. 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.PacketLinkFactory = { 34 | extract: function(header, queue) { 35 | switch (header.type) { 36 | case wdi.SpiceVars.SPICE_MSG_SET_ACK: 37 | return new wdi.RedSetAck().demarshall(queue); 38 | case wdi.SpiceVars.SPICE_MSG_PING: 39 | return new wdi.RedPing().demarshall(queue, header.size); 40 | case wdi.SpiceVars.SPICE_MSG_MIGRATE: 41 | return new wdi.RedMigrate().demarshall(queue); 42 | case wdi.SpiceVars.SPICE_MSG_MIGRATE_DATA: 43 | return new wdi.RedMigrateData().demarshall(queue, header.size); 44 | case wdi.SpiceVars.SPICE_MSG_WAIT_FOR_CHANNELS: 45 | return new wdi.RedWaitForChannels().demarshall(queue); 46 | case wdi.SpiceVars.SPICE_MSG_DISCONNECTING: 47 | return new wdi.RedDisconnect().demarshall(queue); 48 | case wdi.SpiceVars.SPICE_MSG_NOTIFY: 49 | var packet = new wdi.RedNotify().demarshall(queue); 50 | return packet; 51 | } 52 | } 53 | }; 54 | 55 | wdi.PacketLinkProcess = { 56 | process: function(header, packet, channel) { 57 | switch(header.type) { 58 | case wdi.SpiceVars.SPICE_MSG_SET_ACK: 59 | var body = wdi.SpiceObject.numberTo32(packet.generation); 60 | channel.setAckWindow(packet.window) 61 | channel.sendObject(body, wdi.SpiceVars.SPICE_MSGC_ACK_SYNC); 62 | break; 63 | case wdi.SpiceVars.SPICE_MSG_PING: 64 | var body = new wdi.RedPing({id: packet.id, time: packet.time}).marshall(); 65 | channel.sendObject(body, wdi.SpiceVars.SPICE_MSGC_PONG); 66 | break; 67 | case wdi.SpiceVars.SPICE_MSG_NOTIFY: 68 | channel.fire('notify'); 69 | break; 70 | } 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /unittest/runqueue.test.js: -------------------------------------------------------------------------------- 1 | suite('RunQueue', function() { 2 | setup(function(){ 3 | wdi.Debug.debug = false; //disable debugging, it slows tests 4 | }); 5 | 6 | suite('#getTasksLength()', function() { 7 | test('Should return 0 for empty runqueue', function() { 8 | this.rQ = new wdi.RunQueue(); 9 | assert.strictEqual(this.rQ.getTasksLength(), 0); 10 | }); 11 | }); 12 | 13 | suite('#add()', function() { 14 | setup(function() { 15 | this.rQ = new wdi.RunQueue(); 16 | }); 17 | 18 | test('Should add single tasks', function() { 19 | this.rQ.add(function(){}, this); 20 | assert.equal(this.rQ.getTasksLength(), 1); 21 | }); 22 | 23 | test('Should add two tasks', function() { 24 | this.rQ.add(function(){}, this); 25 | this.rQ.add(function(){}, this); 26 | assert.equal(this.rQ.getTasksLength(), 2); 27 | }); 28 | }); 29 | 30 | suite('#clear()', function() { 31 | setup(function() { 32 | this.rQ = new wdi.RunQueue(); 33 | this.rQ.add(function(){}, this); 34 | this.rQ.add(function(){}, this); 35 | }); 36 | 37 | test('Should clear all tasks', function() { 38 | this.rQ.clear(); 39 | assert.equal(this.rQ.getTasksLength(), 0); 40 | }); 41 | }); 42 | 43 | suite('#process()', function() { 44 | setup(function() { 45 | this.rQ = new wdi.RunQueue(); 46 | }); 47 | 48 | test('Should call single tasks', function() { 49 | var object = {method: function(proxy){proxy.end();}}; 50 | var spy = sinon.spy(object, 'method'); 51 | this.rQ.add(object.method, object); 52 | this.rQ.process(); 53 | assert(spy.calledOnce); 54 | }); 55 | 56 | test('Should keep scope', function() { 57 | var object = {method: function(proxy){proxy.end();}}; 58 | var spy = sinon.spy(object, 'method'); 59 | this.rQ.add(object.method, object); 60 | this.rQ.process(); 61 | assert(spy.calledOn(object)); 62 | }); 63 | 64 | test('Should call two syncronous tasks', function() { 65 | var object = {method: function(proxy){proxy.end()}}; 66 | var spy = sinon.spy(object, 'method'); 67 | this.rQ.add(object.method, object); 68 | this.rQ.add(object.method, object); 69 | this.rQ.process(); 70 | assert(spy.calledTwice); 71 | }); 72 | 73 | test('Should call asynchronous task', function(done) { 74 | var object = {method: function(proxy){ 75 | setTimeout(function() { 76 | proxy.end(); 77 | done(); 78 | }, 100); 79 | }}; 80 | this.rQ.add(object.method, object); 81 | this.rQ.process(); 82 | }); 83 | 84 | test('Should return nothing if there are no tasks', function() { 85 | var runqueue = this.rQ.process(); 86 | assert.isUndefined(runqueue); 87 | }); 88 | 89 | test('Should not run process if runqueue is running', function(done) { 90 | var object = {method: function(proxy){ 91 | setTimeout(function() { 92 | done(); 93 | }, 100); 94 | }}; 95 | var object2 = {method: function(proxy){proxy.end()}}; 96 | var spy = sinon.spy(object2, 'method'); 97 | this.rQ.add(object.method, object); 98 | this.rQ.add(object2.method, object2); 99 | this.rQ.process(); 100 | this.rQ.process(); 101 | assert(!spy.called); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /network/socketqueue.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.SocketQueue = $.spcExtend(wdi.EventObject.prototype, { 34 | rQ: null, 35 | sQ: null, 36 | socket: null, 37 | 38 | init: function(c) { 39 | this.superInit(); 40 | this.socket = c.socket || new wdi.Socket(); 41 | this.rQ = c.rQ || new wdi.FixedQueue(); 42 | this.sQ = c.sQ || new wdi.Queue(); 43 | this.setup(); 44 | }, 45 | 46 | setup: function() { 47 | this.socket.addListener('open', function() { 48 | this.fire('open'); 49 | }, this); 50 | this.socket.addListener('message', function(data) { 51 | this.rQ.push(new Uint8Array(data)); 52 | this.fire('message'); 53 | }, this); 54 | this.socket.addListener('close', function(e) { 55 | this.fire('close', e); 56 | }, this); 57 | this.socket.addListener('error', function(e) { 58 | this.fire('error', e); 59 | }, this); 60 | }, 61 | 62 | getStatus: function() { 63 | return this.socket.getStatus(); 64 | }, 65 | 66 | connect: function(uri) { 67 | this.socket.connect(uri); 68 | }, 69 | 70 | disconnect: function() { 71 | this.socket.disconnect(); 72 | }, 73 | 74 | send: function(data, shouldFlush) { 75 | //check for shouldFlush parameter, by default is true 76 | if (shouldFlush === undefined) { 77 | var flush = true; 78 | } else { 79 | var flush = shouldFlush; 80 | } 81 | 82 | //performance: avoid passing through the queue if there is no queue and 83 | //we have flush! 84 | if(this.sQ.getLength() == 0 && flush) { 85 | this.socket.send(data); 86 | return; 87 | } 88 | 89 | //normal operation, append to buffer and send if flush 90 | this.sQ.push(data); 91 | if (flush) this.flush(); 92 | }, 93 | 94 | flush: function() { 95 | var data = this.sQ.shift(); 96 | this.socket.send(data); 97 | } 98 | }); 99 | -------------------------------------------------------------------------------- /application/packetprocess.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.PacketProcess = $.spcExtend(wdi.DomainObject, { 34 | processors: {}, 35 | 36 | init: function(c) { 37 | this.processors[wdi.SpiceVars.SPICE_CHANNEL_MAIN] = c.mainProcess || new wdi.MainProcess({ 38 | app: c.app 39 | }); 40 | this.processors[wdi.SpiceVars.SPICE_CHANNEL_DISPLAY] = c.displayProcess || new wdi.DisplayPreProcess({ 41 | clientGui: c.clientGui 42 | }); 43 | this.processors[wdi.SpiceVars.SPICE_CHANNEL_INPUTS] = c.inputsProcess || new wdi.InputProcess({ 44 | clientGui: c.clientGui, 45 | spiceConnection: c.spiceConnection 46 | }); 47 | this.processors[wdi.SpiceVars.SPICE_CHANNEL_CURSOR] = c.cursorProcess || new wdi.CursorProcess(); 48 | this.processors[wdi.SpiceVars.SPICE_CHANNEL_PLAYBACK] = c.playbackProcess || new wdi.PlaybackProcess({ 49 | app: c.app 50 | }); 51 | }, 52 | 53 | process: function(spiceMessage) { 54 | if(wdi.exceptionHandling) { 55 | return this.processExceptionHandled(spiceMessage); 56 | } else { 57 | return this.processPacket(spiceMessage); 58 | } 59 | }, 60 | 61 | processExceptionHandled: function(spiceMessage) { 62 | try { 63 | return this.processPacket(spiceMessage); 64 | } catch(e) { 65 | wdi.Debug.error('PacketProcess: Error processing packet', e); 66 | } 67 | }, 68 | 69 | processPacket: function(spiceMessage) { 70 | if(!spiceMessage || !this.processors[spiceMessage.channel]) { 71 | throw "Invalid channel or null message"; 72 | } 73 | 74 | this.processors[spiceMessage.channel].process(spiceMessage); 75 | }, 76 | 77 | dispose: function () { 78 | this.processors[wdi.SpiceVars.SPICE_CHANNEL_DISPLAY].dispose(); 79 | } 80 | }); 81 | -------------------------------------------------------------------------------- /network/packetreassembler.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.PacketReassembler = $.spcExtend(wdi.EventObject.prototype, { 34 | packetController: null, 35 | currentHeader: null, 36 | statusToString: null, 37 | sizeDefinerConstant: null, 38 | 39 | init: function(c) { 40 | this.superInit(); 41 | this.packetController = c.packetController; 42 | this.sizeDefinerConstant = wdi.SizeDefiner.prototype; 43 | this.statusToString = []; 44 | this.statusToString[this.sizeDefinerConstant.STATUS_REPLY_BODY] = 'reply'; 45 | this.statusToString[this.sizeDefinerConstant.STATUS_ERROR_CODE] = 'errorCode'; 46 | this.statusToString[this.sizeDefinerConstant.STATUS_BODY] = 'spicePacket'; 47 | this.setListeners(); 48 | 49 | }, 50 | 51 | start: function () { 52 | this.packetController.getNextPacket(); 53 | }, 54 | 55 | setListeners: function() { 56 | this.packetController.addListener('chunkComplete', function(e) { 57 | var rawMessage = e; 58 | var status = rawMessage.status; 59 | switch(status) { 60 | case this.sizeDefinerConstant.STATUS_HEADER: 61 | case this.sizeDefinerConstant.STATUS_REPLY: 62 | this.currentHeader = rawMessage; 63 | break; 64 | case this.sizeDefinerConstant.STATUS_REPLY_BODY: 65 | case this.sizeDefinerConstant.STATUS_BODY: 66 | var tmpBuff = new Uint8Array(rawMessage.data.length + this.currentHeader.data.length); 67 | tmpBuff.set(this.currentHeader.data); 68 | tmpBuff.set(rawMessage.data, this.currentHeader.data.length); 69 | rawMessage.data = tmpBuff; 70 | rawMessage.status = this.statusToString[status]; 71 | this.fire('packetComplete', rawMessage); 72 | break; 73 | default: 74 | rawMessage.status = this.statusToString[status]; 75 | this.fire('packetComplete', rawMessage); 76 | break; 77 | } 78 | }, this); 79 | } 80 | }); 81 | -------------------------------------------------------------------------------- /lib/IntegrationBenchmark.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.IntegrationBenchmark = { 34 | benchmarking: false, 35 | startTime: 0, 36 | timeoutInterval: 3000, // in ms, amount of time after it will be considered that 37 | // we have received all packets and can stop counting 38 | timeOutId: undefined, 39 | 40 | busConnection: undefined, 41 | 42 | setEndTime: function() { 43 | var self = this; 44 | this.timeOutId = setTimeout(function() { 45 | // if 3000 ms have passed since the last packet we assume we have processed them all and can launch MS Word 46 | self.timeOutId = undefined; 47 | self.benchmarking = false; 48 | var now = new Date().getTime(); 49 | var elapsed = now - self.startTime - self.timeoutInterval; 50 | self.onEndBenchmarkCallback(elapsed); 51 | var message = { 52 | "type": wdi.BUS_TYPES.killApplicationDoNotUseInProductionEver, 53 | "application": "EXCEL.EXE" 54 | }; 55 | self.busConnection.send(message); 56 | }, this.timeoutInterval); 57 | }, 58 | 59 | setStartTime: function() { 60 | if (this.timeOutId !== undefined) { 61 | clearTimeout(this.timeOutId); 62 | } 63 | }, 64 | 65 | launchApp: function(busConnection, onEndBenchmarkCallback) { 66 | this.busConnection = busConnection; 67 | wdi.IntegrationBenchmark.benchmarking = true; 68 | wdi.IntegrationBenchmark.setStartTime(); 69 | this.onEndBenchmarkCallback = onEndBenchmarkCallback; 70 | this.startTime = new Date().getTime(); 71 | var message = { 72 | "type": wdi.BUS_TYPES.launchApplication, 73 | "file": "c:\\Users\\eyeos\\Desktop\\test.xlsx" 74 | }; 75 | this.busConnection.send(message); 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /lib/runqueue.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.RunQueue = $.spcExtend(wdi.DomainObject, { 34 | tasks: null, 35 | isRunning: false, 36 | 37 | init: function() { 38 | this.tasks = []; 39 | }, 40 | 41 | getTasksLength: function() { 42 | return this.tasks.length; 43 | }, 44 | 45 | add: function(fn, scope, endCallback, params) { 46 | this.tasks.push({ 47 | fn: fn, 48 | scope: scope, 49 | fnFinish: endCallback, 50 | params: params 51 | }); 52 | 53 | return this; 54 | }, 55 | 56 | clear: function() { 57 | this.tasks = []; 58 | 59 | return this; 60 | }, 61 | 62 | _process: function() { 63 | wdi.ExecutionControl.sync = true; 64 | var proxy, self = this; 65 | this.isRunning = true; 66 | var task = this.tasks.shift(); 67 | 68 | if (!task) { 69 | this.isRunning = false; 70 | return; 71 | } 72 | 73 | proxy = { 74 | end: function() { 75 | if(task.fnFinish) { 76 | task.fnFinish.call(task.scope); 77 | } 78 | self._process(); 79 | } 80 | }; 81 | 82 | try { 83 | task.fn.call(task.scope, proxy, task.params); 84 | } catch(e) { 85 | wdi.Debug.error(e.message); 86 | proxy.end(); 87 | } 88 | 89 | return this; 90 | }, 91 | 92 | process: function() { 93 | if (!this.isRunning) { 94 | this._process(); 95 | } else { 96 | return; 97 | } 98 | } 99 | }); 100 | 101 | //wdi.ExecutionControl = $.spcExtend(wdi.DomainObject, { 102 | // currentProxy: null, 103 | // sync: true, 104 | // runQ: null, 105 | // init: function(c) { 106 | // this.runQ = c.runQ || new wdi.RunQueue(); 107 | // } 108 | //}); 109 | 110 | //TODO: make an instance of it on each channel 111 | wdi.ExecutionControl = { 112 | currentProxy: null, 113 | sync: true, 114 | runQ: new wdi.RunQueue() 115 | }; 116 | -------------------------------------------------------------------------------- /process/displaypreprocess.js: -------------------------------------------------------------------------------- 1 | wdi.DisplayPreProcess = $.spcExtend(wdi.EventObject.prototype, { 2 | displayProcess: null, 3 | queued: [], 4 | inProcess: [], 5 | idleConsumers : [], 6 | consumers: [], 7 | 8 | init: function(c) { 9 | this.superInit(); 10 | this.displayProcess = c.displayProcess || new wdi.DisplayProcess({ 11 | clientGui: c.clientGui 12 | }); 13 | this.clientGui = c.clientGui; 14 | 15 | /** 16 | 17 | Since javascript do not provide an API to check 18 | the number of cpu cores available, the best case for average computers 19 | and devices is 4. 20 | 21 | If the computer doesn't have 4 or more available cores, there is only a little 22 | memory waste creating the threads and a bit of cpu overheat doing context 23 | switching. 24 | 25 | There is an ongoing draft in w3c to standarize a way to detect this: 26 | 27 | http://www.w3.org/2012/sysapps/device-capabilities/#cpu 28 | 29 | **/ 30 | if(c.numConsumers == null || c.numConsumers == undefined) c.numConsumers = 4; 31 | var numConsumers = c.numConsumers; 32 | 33 | for(var i = 0;i 0) { 63 | this.executeConsumer(); 64 | } 65 | }, 66 | 67 | process: function(spiceMessage) { 68 | this.addTask(spiceMessage); //first of all, queue it 69 | //it is the only item in the list? 70 | //we are the only message in the queue... process? 71 | this.executeConsumer(); 72 | }, 73 | 74 | addTask: function(spiceMessage) { 75 | this.queued.push({ 76 | message: spiceMessage, 77 | clientGui: this.clientGui 78 | }); 79 | }, 80 | 81 | getNextTask : function () { 82 | var task = this.queued.shift(); 83 | while(typeof task == 'undefined' && this.queued.length != 0) { 84 | task = this.queued.shift(); 85 | } 86 | 87 | //we found a task? 88 | if(typeof task == 'undefined') { 89 | return false; 90 | } 91 | 92 | task.state = 0; 93 | this.inProcess.push(task); //add the task to the inProcess list 94 | return task; 95 | }, 96 | 97 | executeConsumer: function() { 98 | //check if there are idle consumers 99 | if(this.idleConsumers.length > 0) { 100 | wdi.Debug.log('DisplayPreProcess: available workers: '+this.idleConsumers.length); 101 | wdi.Debug.log('DisplaypreProcess: pending tasks: '+this.queued.length); 102 | //idle consumer found 103 | var consumer = this.idleConsumers.shift(); 104 | //execute the next task in this consumer 105 | var task = this.getNextTask(); 106 | 107 | if(task) { 108 | consumer.consume(task); 109 | } 110 | 111 | } 112 | }, 113 | 114 | dispose: function () { 115 | this.consumers.forEach(function (consumer) { 116 | consumer.dispose(); 117 | }); 118 | } 119 | }); 120 | -------------------------------------------------------------------------------- /unittest/busconnection.test.js: -------------------------------------------------------------------------------- 1 | suite('BusConnection', function() { 2 | var sut, socket; 3 | var clusterNodeChooser; 4 | var clusterGetAnotherStub; 5 | 6 | setup(function() { 7 | config = { 8 | useBus: true, 9 | protocol: 'ws', 10 | host: 'localhost', 11 | port: 8000, 12 | busUser: 'test', 13 | busPass: 'kjasdhfadis', 14 | busFileServerBaseUrl: 'http://***.com', 15 | busSubscriptions: '/topic' 16 | }; 17 | 18 | clusterNodeChooser = { 19 | getAnother: function () { 20 | return { 21 | host: 'somehost1', 22 | port: 'someport1' 23 | } 24 | }, 25 | setNodeList: function () { 26 | } 27 | }; 28 | 29 | socket = new wdi.WebSocketWrapper(); 30 | 31 | sut = new wdi.BusConnection({ 32 | websocket: socket, 33 | binary: true, 34 | clusterNodeChooser: clusterNodeChooser 35 | }); 36 | }); 37 | 38 | function getConfigWithNumberOfBusClusterNodes(config, numberOfBusNodes) { 39 | var busHostList = []; 40 | var i; 41 | for (i = 1; i <= numberOfBusNodes; i++) { 42 | busHostList.push({ 43 | host: 'somehost' + i, 44 | port: 'someport' + i 45 | }); 46 | } 47 | config.busHostList = busHostList; 48 | return config; 49 | } 50 | 51 | function getConfigWithBusHostAndBusPort(config) { 52 | config.busHost = 'somehost1'; 53 | config.busPort = 'someport1'; 54 | return config; 55 | } 56 | 57 | test('connect should call socket connect with uri when using busHostList', function() { 58 | var mock = sinon.mock(socket); 59 | var expectation = mock 60 | .expects('connect') 61 | .once() 62 | .withExactArgs( 63 | 'ws://localhost:8000/websockify/host/somehost1/port/someport1/type/raw', 64 | 'binary' 65 | ); 66 | 67 | sut.connect(getConfigWithNumberOfBusClusterNodes(config, 3)); 68 | 69 | expectation.verify(); 70 | }); 71 | 72 | test('connect should call socket connect with uri when using busHost and busPort', function () { 73 | var mock = sinon.mock(socket); 74 | var expectation = mock 75 | .expects('connect') 76 | .once() 77 | .withExactArgs( 78 | 'ws://localhost:8000/websockify/host/somehost1/port/someport1/type/raw', 79 | 'binary' 80 | ); 81 | 82 | sut.connect(getConfigWithBusHostAndBusPort(config)); 83 | 84 | }); 85 | 86 | test('connect should call websocket setBinaryType on binary', function() { 87 | var mock = sinon.mock(socket); 88 | var stub = sinon.stub(socket, 'connect'); 89 | var expectation = mock.expects('setBinaryType').once().withExactArgs('arraybuffer'); 90 | sut.connect(getConfigWithNumberOfBusClusterNodes(config, 3)); 91 | expectation.verify(); 92 | }); 93 | 94 | test('disconnect should call socket close', function() { 95 | var mock = sinon.mock(socket); 96 | var expectation = mock.expects('close').once().withExactArgs(); 97 | sut.disconnect(); 98 | expectation.verify(); 99 | }); 100 | 101 | test('send should call socket send', function() { 102 | var mock = sinon.mock(socket); 103 | var expectation = mock.expects('send').once(); 104 | sut.send('message'); 105 | expectation.verify(); 106 | }); 107 | 108 | //test('setListeners: we call _connectToNextHost again when the ws closes', function () { 109 | // var setTimeoutStub = sinon.stub(window, 'setTimeout', function (fn, timeout) { 110 | // fn(); 111 | // }); 112 | // 113 | // var wsOnCloseStub = sinon.stub(socket, 'onClose', function (fn) { 114 | // fn(); 115 | // }); 116 | // 117 | // var mock = sinon.mock(sut); 118 | // var expectation = mock 119 | // .expects('_connectToNextHost') 120 | // .once() 121 | // .withExactArgs(); 122 | // 123 | // sut.setListeners(); 124 | // 125 | // expectation.verify; 126 | //}); 127 | }); 128 | -------------------------------------------------------------------------------- /network/sizedefiner.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.SizeDefiner = $.spcExtend(wdi.DomainObject, { 34 | ERROR_CODE_SIZE: 4, 35 | status: null, 36 | STATUS_READY: 0, 37 | STATUS_REPLY: 1, 38 | STATUS_REPLY_BODY: 2, 39 | STATUS_ERROR_CODE: 3, 40 | STATUS_MESSAGE: 4, 41 | STATUS_HEADER: 5, 42 | STATUS_BODY: 6, 43 | isHeader: false, 44 | 45 | init: function(c) { 46 | this.status = this.STATUS_READY; 47 | }, 48 | 49 | getSize: function(arr) { 50 | if (this.STATUS_READY === this.status) { 51 | this.status++; 52 | return wdi.SpiceLinkHeader.prototype.objectSize; 53 | } else if (this.STATUS_REPLY === this.status) { 54 | this.status++; 55 | return this.getReplyBodySize(arr); 56 | } else if (this.STATUS_REPLY_BODY === this.status) { 57 | this.status++; 58 | return this.ERROR_CODE_SIZE; 59 | } else if (this.STATUS_ERROR_CODE === this.status) { 60 | this.status++; 61 | this.isHeader = true; 62 | return 6; //wdi.SpiceDataHeader.prototype.objectSize access here is slow 63 | } else { 64 | if (this.isHeader) { 65 | this.isHeader = false; 66 | return this.getBodySizeFromArrayHeader(arr); 67 | } else { 68 | this.isHeader = true; 69 | return 6;//wdi.SpiceDataHeader.prototype.objectSize; access here is slow 70 | } 71 | } 72 | }, 73 | 74 | getReplyBodySize: function (arr) { 75 | var queue = wdi.GlobalPool.create('ViewQueue'); 76 | queue.setData(arr); 77 | var header = new wdi.SpiceLinkHeader().demarshall(queue); 78 | wdi.GlobalPool.discard('ViewQueue', queue); 79 | return header.size; 80 | }, 81 | 82 | getBodySizeFromArrayHeader: function (arr) { 83 | var queue = wdi.GlobalPool.create('ViewQueue'); 84 | queue.setData(arr); 85 | var header = new wdi.SpiceDataHeader().demarshall(queue); 86 | wdi.GlobalPool.discard('ViewQueue', queue); 87 | return header.size; 88 | }, 89 | 90 | getStatus: function() { 91 | if (this.status === this.STATUS_MESSAGE && this.isHeader) { 92 | return this.STATUS_HEADER; 93 | } else if (this.status === this.STATUS_MESSAGE && !this.isHeader) { 94 | return this.STATUS_BODY; 95 | } else { 96 | return this.status; 97 | } 98 | } 99 | }); 100 | -------------------------------------------------------------------------------- /lib/encrypt.js: -------------------------------------------------------------------------------- 1 | rng = new SecureRandom(); 2 | 3 | function pack(source) 4 | 5 | { 6 | 7 | var temp = ""; 8 | 9 | for (var i = 0; i < source.length; i+=2) 10 | 11 | { 12 | 13 | temp+= String.fromCharCode(parseInt(source.substring(i, i + 2), 16)); 14 | 15 | } 16 | 17 | return temp; 18 | 19 | } 20 | function char2hex(source) 21 | 22 | { 23 | 24 | var hex = ""; 25 | 26 | for (var i = 0; i < source.length; i+=1) 27 | 28 | { 29 | 30 | temp = source[i].toString(16); 31 | 32 | switch (temp.length) 33 | 34 | { 35 | 36 | case 1: 37 | 38 | temp = "0" + temp; 39 | 40 | break; 41 | 42 | case 0: 43 | 44 | temp = "00"; 45 | 46 | } 47 | 48 | hex+= temp; 49 | 50 | } 51 | 52 | return hex; 53 | 54 | } 55 | 56 | 57 | 58 | function xor(a, b) 59 | 60 | { 61 | 62 | length = Math.min(a.length, b.length); 63 | 64 | temp = ""; 65 | 66 | for (var i = 0; i < length; i++) 67 | 68 | { 69 | 70 | temp+= String.fromCharCode(a.charCodeAt(i) ^ b.charCodeAt(i)); 71 | 72 | } 73 | 74 | length = Math.max(a.length, b.length) - length; 75 | 76 | for (var i = 0; i < length; i++) 77 | 78 | { 79 | 80 | temp+= "\x00"; 81 | 82 | } 83 | 84 | return temp; 85 | 86 | } 87 | 88 | 89 | 90 | function mgf1(mgfSeed, maskLen) 91 | 92 | { 93 | 94 | t = ""; 95 | 96 | hLen = 20; 97 | 98 | count = Math.ceil(maskLen / hLen); 99 | 100 | for (var i = 0; i < count; i++) 101 | 102 | { 103 | 104 | c = String.fromCharCode((i >> 24) & 0xFF, (i >> 16) & 0xFF, (i >> 8) & 0xFF, i & 0xFF); 105 | 106 | t+= pack(sha1Hash(mgfSeed + c)); 107 | 108 | } 109 | 110 | 111 | 112 | return t.substring(0, maskLen); 113 | 114 | } 115 | function rsa_oaep_encrypt(message, n, e) { 116 | 117 | // precomputed values 118 | var k = 128; // length of n in bytes 119 | var hLen = 20; 120 | var mLen = message.length; 121 | var lHash = '\xda\x39\xa3\xee\x5e\x6b\x4b\x0d\x32\x55\xbf\xef\x95\x60\x18\x90\xaf\xd8\x07\x09'; // pack(sha1Hash("")) 122 | var temp = k - mLen - 2 * hLen - 2; 123 | 124 | for (var i = 0; i < temp; i++) { 125 | lHash += '\x00'; 126 | } 127 | 128 | var db = lHash + '\x01' + message; 129 | 130 | var seed = ''; 131 | for (var i = 0; i < hLen + 4; i += 4) { 132 | temp = new Array(4); 133 | rng.nextBytes(temp); 134 | seed += String.fromCharCode(temp[0], temp[1], temp[2], temp[3]); 135 | } 136 | seed = seed.substring(4 - seed.length % 4); 137 | 138 | var dbMask = mgf1(seed, k - hLen - 1); 139 | var maskedDB = xor(db, dbMask); 140 | var seedMask = mgf1(maskedDB, hLen); 141 | var maskedSeed = xor(seed, seedMask); 142 | var em = "\x00" + maskedSeed + maskedDB; 143 | 144 | m = new Array(); 145 | for (i = 0; i < em.length; i++) { 146 | m[i] = em.charCodeAt(i); 147 | } 148 | m = new encryptionBigInteger(m, 256); 149 | c = m.modPowInt(e, n); // doPublic 150 | c = c.toString(16); 151 | 152 | if (c.length & 1) 153 | c = "0" + c; 154 | 155 | return c; 156 | } 157 | 158 | 159 | function RSA_public_encrypt(password, pub_key) { 160 | 161 | var keyInChar = new Uint8Array(pub_key); 162 | var rawPubKey = new Array(129); // 00xxx 163 | 164 | for (var i = 0; i < 129; i++) 165 | rawPubKey[i] = keyInChar[28 + i]; 166 | 167 | var n = new encryptionBigInteger(rawPubKey); 168 | var e = new encryptionBigInteger('010001', 16); 169 | 170 | var hexRsa = rsa_oaep_encrypt(password + String.fromCharCode(0), n, e); 171 | return hexRsa; 172 | } 173 | -------------------------------------------------------------------------------- /translation.js: -------------------------------------------------------------------------------- 1 | var translations = { 2 | es: { 3 | uploadfile: "Subir Fichero", 4 | menubarbutton: "Ocultar Menu", 5 | menubarbutton_alt: "Fijar Menu", 6 | fullscreen: "Pantalla Completa", 7 | fullscreen_alt: "Ventana Normal", 8 | keystroke: "Combinación de teclas", 9 | showclientid: "Mostrar Id", 10 | dialog_fs_text: "La aplicación solicita permiso para acceder a su Escritorio VDI en modo Pantalla Completa.", 11 | cancel: "Cancelar", 12 | accept: "Aceptar", 13 | dialog_close_text: "Su sesión de VDI se cerrará. Por favor, asegúrese de que ha guardado sus cambios.", 14 | dialog_end_text: "Su sesión de VDI ha finalizado. Por favor, cierre esta ventana.", 15 | inactivity_close_text: "Su sesión de VDI se cerrará por inactividad en _ segundos", 16 | inactivity_end_text: "Su sesión de VDI ha sido cerrada por inactividad. Por favor, cierre esta ventana.", 17 | error_text: "Error de conexión. Por favor, cierre esta ventana.", 18 | show_id: "El identificador de este navegador es: _", 19 | no_auto_fs: "Este navegador no soporta el cambio automatico a pantalla completa. Por favor, pulse F11 para cambiar de forma manual.", 20 | msg_click_to_capture: "Haga click dentro del escritorio virtual para capturar el ratón. Podrá liberar el ratón pulsando la tecla ESC.", 21 | }, 22 | en: { 23 | uploadfile: "Upload File", 24 | menubarbutton: "Hide Menu", 25 | menubarbutton_alt: "Show Menu", 26 | fullscreen: "Fullscreen", 27 | fullscreen_alt: "Normal Window", 28 | keystroke: "Send keystroke", 29 | showclientid: "Show Id", 30 | dialog_fs_text: "The application requests to access your virtual desktop in fullscreen mode.", 31 | cancel: "Cancel", 32 | accept: "Accept", 33 | dialog_close_text: "Your VDI session will close. Please, confirm that you saved your changes.", 34 | dialog_end_text: "Your VDI session has ended. Please, close this window.", 35 | inactivity_close_text: "Your VDI session will close due to inactivity in _ seconds", 36 | inactivity_end_text: "Your VDI session was closed due to inactivity. Please, close this window.", 37 | error_text: "Connection error. Please, close this window.", 38 | show_id: "This browser's ID is: _", 39 | no_auto_fs: "This browser does not support automatic fullscreen switch. Please, press F11 to switch manually.", 40 | msg_click_to_capture: "Click inside the virtual desktop to capture the mouse. Then press ESC to release pointer.", 41 | }, 42 | ru: { 43 | uploadfile: "Загрузить файл", 44 | menubarbutton: "Скрыть меню", 45 | menubarbutton_alt: "Показать меню", 46 | fullscreen: "Во весь экран", 47 | fullscreen_alt: "Нормальное окно", 48 | keystroke: "комбинация клавиш", 49 | showclientid: "Показать Id", 50 | dialog_fs_text: "Приложение запрашивает доступ к вашему виртуальному рабочему столу в полноэкранном режиме.", 51 | cancel: "Отмена", 52 | accept: "Принять", 53 | dialog_close_text: "Ваш сеанс VDI будет закрыт. Пожалуйста, подтвердите, что вы сохранили изменения.", 54 | dialog_end_text: "Ваш сеанс VDI закончился. Пожалуйста, закройте это окно.", 55 | inactivity_close_text: "Ваш сеанс VDI будет закрыт из-за неактивности _ секунд", 56 | inactivity_end_text: "Ваш сеанс VDI был закрыт из-за неактивности. Закройте это окно.", 57 | error_text: "Ошибка подключения. Закройте это окно.", 58 | show_id: "ID этого браузера: _", 59 | no_auto_fs: "Этот браузер не поддерживает автоматическое переключение в полноэкранный режим. Нажмите F11 для переключения вручную.", 60 | msg_click_to_capture: "Нажмите на виртуальный рабочий стол, чтобы захватить мышь. Затем нажмите ESC, чтобы освободить указатель.", 61 | }, 62 | }; 63 | 64 | var tr = translations["en"] 65 | -------------------------------------------------------------------------------- /application/inputmanager.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.InputManager = $.spcExtend(wdi.EventObject.prototype, { 34 | 35 | checkFocus: false, 36 | input: null, 37 | window: null, 38 | stuckKeysHandler: null, 39 | 40 | init: function (c) { 41 | this.superInit(); 42 | this.input = c.input; 43 | this.window = c.window; 44 | this.stuckKeysHandler = c.stuckKeysHandler; 45 | this.$ = c.jQuery || $; 46 | if (!c.disableInput) { 47 | this.inputElement = this.$('
'); 48 | } 49 | this.currentWindow = null; 50 | }, 51 | 52 | setCurrentWindow: function(wnd) { 53 | wnd = this.$(wnd); 54 | if(this.currentWindow) { 55 | this.inputElement.remove(); 56 | //remove listeners 57 | this.currentWindow.unbind('blur'); 58 | } 59 | this.$(wnd[0].document.body).prepend(this.inputElement); 60 | this.input = this.$(wnd[0].document.getElementById('inputmanager')); 61 | //TODO: remove events from the other window 62 | this.addListeners(wnd); 63 | this.currentWindow = wnd; 64 | }, 65 | 66 | addListeners: function (wnd) { 67 | this._onBlur(wnd); 68 | this._onInput(); 69 | }, 70 | 71 | _onBlur: function (wnd) { 72 | var self = this; 73 | wnd.on('blur', function onBlur (e) { 74 | if (self.checkFocus) { 75 | self.input.focus(); 76 | } 77 | self.stuckKeysHandler.releaseSpecialKeysPressed(); 78 | }); 79 | }, 80 | 81 | _onInput: function () { 82 | var self = this; 83 | this.input.on('input', function input (e) { 84 | // ctrl-v issue related 85 | var aux = self.input.val(); 86 | if (aux.length > 1) { 87 | self.reset(); 88 | } 89 | }); 90 | }, 91 | 92 | enable: function () { 93 | this.checkFocus = true; 94 | this.input.select(); 95 | }, 96 | 97 | disable: function () { 98 | this.checkFocus = false; 99 | this.input.blur(); 100 | }, 101 | 102 | reset: function () { 103 | this.input.val(""); 104 | }, 105 | 106 | getValue: function () { 107 | var val = this.input.val(); 108 | if (val) { 109 | this.reset(); 110 | } 111 | return val; 112 | }, 113 | 114 | manageChar: function (val, params) { 115 | var res = [Object.create(params[0])]; 116 | res[0]['type'] = 'inputmanager'; 117 | res[0]['charCode'] = val.charCodeAt(0); 118 | return res; 119 | } 120 | 121 | }); 122 | -------------------------------------------------------------------------------- /lib/stuckkeyshandler.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.StuckKeysHandler = $.spcExtend(wdi.EventObject.prototype, { 34 | ctrlTimeoutId: null, 35 | altTimeoutId: null, 36 | shiftTimeoutId: null, 37 | shiftKeyPressed: false, 38 | ctrlKeyPressed: false, 39 | altKeyPressed: false, 40 | 41 | handleStuckKeys: function (jqueryEvent) { 42 | if (jqueryEvent) { 43 | switch (jqueryEvent.keyCode) { 44 | case 16: 45 | this._handleKey('shiftTimeoutId', jqueryEvent.type, 16); 46 | break; 47 | case 17: 48 | this._handleKey('ctrlTimeoutId', jqueryEvent.type, 17); 49 | break; 50 | case 18: 51 | this._handleKey('altTimeoutId', jqueryEvent.type, 18); 52 | break; 53 | } 54 | } 55 | }, 56 | 57 | releaseAllKeys: function releaseAllKeys () { 58 | var e; 59 | var i; 60 | for (i = 0; i < 300; i++) { 61 | this.releaseKeyPressed(i); 62 | } 63 | }, 64 | 65 | _handleKey: function (variable, type, keyCode) { 66 | if (type === 'keydown') { 67 | this[variable] = this._configureTimeout(keyCode); 68 | } else if (type === 'keyup') { 69 | clearTimeout(this[variable]); 70 | } 71 | }, 72 | 73 | _configureTimeout: function (keyCode) { 74 | var self = this; 75 | return setTimeout(function keyPressedTimeout () { 76 | // added the 'window' for the jQuery call for testing. 77 | self.releaseKeyPressed(keyCode); 78 | }, wdi.StuckKeysHandler.defaultTimeout); 79 | }, 80 | 81 | releaseKeyPressed: function (keyCode) { 82 | var e = window.jQuery.Event("keyup"); 83 | e["which"] = keyCode; 84 | e["keyCode"] = keyCode; 85 | e["charCode"] = 0; 86 | e["generated"] = true; 87 | this.fire('inputStuck', ['keyup', [e]]); 88 | }, 89 | 90 | checkSpecialKey: function (event, keyCode) { 91 | switch (keyCode) { 92 | case 16: 93 | this.shiftKeyPressed = event === 'keydown'; 94 | break; 95 | case 17: 96 | this.ctrlKeyPressed = event === 'keydown'; 97 | break; 98 | case 18: 99 | this.altKeyPressed = event === 'keydown'; 100 | break; 101 | } 102 | }, 103 | 104 | releaseSpecialKeysPressed: function () { 105 | if (this.shiftKeyPressed) { 106 | this.releaseKeyPressed(16); 107 | this.shiftKeyPressed = false; 108 | } 109 | if (this.ctrlKeyPressed) { 110 | this.releaseKeyPressed(17); 111 | this.ctrlKeyPressed = false; 112 | } 113 | if (this.altKeyPressed) { 114 | this.releaseKeyPressed(18); 115 | this.altKeyPressed = false; 116 | } 117 | } 118 | 119 | 120 | }); 121 | 122 | wdi.StuckKeysHandler.defaultTimeout = 2000; 123 | -------------------------------------------------------------------------------- /unittest/tests.js: -------------------------------------------------------------------------------- 1 | require("long-stack-traces"); 2 | var fs=require("fs") 3 | _ = require("underscore"); 4 | window = null; 5 | suite("tests suite", function () { 6 | test("define suites", function (done) { 7 | this.timeout(15000); 8 | var jsdom = require("jsdom"); 9 | jsdom.env( 10 | "some.html", 11 | [], 12 | function (errors, domWindow) { 13 | 14 | function fakeWorkerProcess() { 15 | window.self = domWindow; 16 | window.workerDispatch = function () { 17 | }; 18 | } 19 | 20 | //region fakes 21 | window = domWindow; 22 | fakeWorkerProcess(); 23 | Modernizr = {}; 24 | Modernizr['websocketsbinary'] = true; 25 | WebSocket = require("websocket").client; 26 | 27 | //endregion fakes 28 | 29 | //region test-environment 30 | sinon = require("sinon"); 31 | assert = require("chai").assert; 32 | 33 | require("../lib/base64"), 34 | Canvas = require('canvas'), 35 | Image = Canvas.Image, 36 | BigInteger = require("../lib/biginteger").BigInteger, 37 | window.$=require("../lib/jquery-2.0.3"), 38 | window.bowser = require("../lib/bowser"), 39 | require("../lib/virtualjoystick"), 40 | require("../lib/utils"), 41 | require("../lib/CollisionDetector.js"), 42 | require("../lib/GlobalPool"), 43 | require("../lib/GenericObjectPool"), 44 | require("../spiceobjects/spiceobjects"), 45 | require("../spiceobjects/generated/protocol"), 46 | require("../lib/graphicdebug"), 47 | require("../lib/images/lz"), 48 | require("../lib/images/bitmap"), 49 | require("../lib/images/png"), 50 | require("../lib/runqueue"), 51 | require("../lib/queue"), 52 | require("../lib/ImageUncompressor"), 53 | require("../lib/SyncAsyncHandler"), 54 | require("../lib/stuckkeyshandler"), 55 | require("../lib/timelapsedetector"), 56 | require("../lib/displayRouter"), 57 | require("../lib/rasterEngine"), 58 | require("../lib/DataLogger"), 59 | require("../network/socket"), 60 | require("../network/socketqueue"), 61 | require("../network/packetlinkfactory"), 62 | require("../network/packetcontroller"), 63 | require("../network/packetextractor"), 64 | require("../network/packetreassembler"), 65 | require("../network/reassemblerfactory"), 66 | require("../network/sizedefiner"), 67 | require("../network/packetlinkfactory"), 68 | require("../network/spicechannel"), 69 | require("../network/busconnection"), 70 | require("../network/clusternodechooser"), 71 | require("../network/websocketwrapper"), 72 | require("../network/connectioncontrol"), 73 | require("../application/agent"), 74 | require("../application/spiceconnection"), 75 | require("../application/spiceconnection"), 76 | require("../application/clientgui"), 77 | require("../application/packetprocess"), 78 | require("../application/packetfilter"), 79 | require("../application/packetfactory"), 80 | require("../application/application"), 81 | require("../application/virtualmouse"), 82 | require("../application/imagecache"), 83 | require("../application/rasteroperation"), 84 | require("../application/stream"), 85 | require("../application/inputmanager"), 86 | require("../process/displayprocess"), 87 | require("../process/displaypreprocess"), 88 | require("../process/inputprocess"), 89 | require("../process/cursorprocess"), 90 | require("../process/mainprocess"), 91 | require("../process/busprocess"), 92 | require("../keymaps/keymapes"), 93 | require("../keymaps/keymapus"), 94 | require("../keymaps/keymap"), 95 | require("../testlibs/fakewebsocket"), 96 | require("../node_modules/mocha/mocha"); 97 | 98 | 99 | wdi.GlobalPool.createCanvas = function () { 100 | return new Canvas(200, 200); 101 | } 102 | 103 | var files = fs.readdirSync(__dirname); 104 | _.each(files, function(item) { 105 | if (!item.match(/\.test\.js/g)||item.match(/graphic.*test/g)) { 106 | return; 107 | } 108 | require("./"+item.slice(0, -3)); 109 | }); 110 | wdi.exceptionHandling = false; 111 | wdi.GlobalPool.init(); 112 | //endregion test-environment 113 | done(); 114 | } 115 | ); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /application/packetfilter.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.PacketFilter = { 34 | restoreContext: false, 35 | start: null, 36 | filter: function(spiceMessage, fn, scope, clientGui) { 37 | if(wdi.logOperations) { 38 | this.start = Date.now(); 39 | } 40 | 41 | //TODO: design an architecture for loading 42 | //dynamic filters, instead of filtering here. 43 | //This should be just the entry point for filters. 44 | if (wdi.graphicDebug && wdi.graphicDebug.debugMode) { 45 | wdi.graphicDebug.printDebugMessageOnFilter(spiceMessage, clientGui); 46 | } 47 | //end of hardcoded filter 48 | 49 | // MS Word Benchmark startup 50 | if (wdi.IntegrationBenchmark && wdi.IntegrationBenchmark.benchmarking) { 51 | var date = new Date(); 52 | wdi.IntegrationBenchmark.setStartTime(date.getTime()); 53 | } 54 | 55 | //check clipping 56 | if(spiceMessage.args.base) { 57 | if(spiceMessage.args.base.clip.type === wdi.SpiceClipType.SPICE_CLIP_TYPE_RECTS) { 58 | var context = clientGui.getContext(spiceMessage.args.base.surface_id); 59 | context.save(); 60 | context.beginPath(); 61 | var rects = spiceMessage.args.base.clip.rects.rects; 62 | var len = rects.length; 63 | while(len--) { 64 | var box = wdi.graphics.getBoxFromSrcArea(rects[len]); 65 | context.rect(box.x, box.y, box.width, box.height); 66 | } 67 | context.clip(); 68 | this.restoreContext = spiceMessage.args.base.surface_id; 69 | } 70 | } 71 | fn.call(scope, spiceMessage); 72 | }, 73 | 74 | notifyEnd: function(spiceMessage, clientGui) { 75 | if(this.restoreContext !== false) { 76 | var context = clientGui.getContext(this.restoreContext); 77 | context.restore(); 78 | this.restoreContext = false; 79 | } 80 | 81 | if(wdi.SeamlessIntegration) { 82 | var filterPosition = null; 83 | if(spiceMessage.args.base && spiceMessage.args.base.box) { 84 | filterPosition = spiceMessage.args.base.box; 85 | } 86 | clientGui.fillSubCanvas(filterPosition); 87 | } 88 | 89 | if (wdi.graphicDebug && wdi.graphicDebug.debugMode) { 90 | wdi.graphicDebug.printDebugMessageOnNotifyEnd(spiceMessage, clientGui); 91 | } 92 | 93 | // MS Word Benchmark 94 | if (wdi.IntegrationBenchmark && wdi.IntegrationBenchmark.benchmarking) { 95 | var date = new Date(); 96 | wdi.IntegrationBenchmark.setEndTime(date.getTime()); 97 | } 98 | 99 | // clear the tmpcanvas 100 | wdi.GlobalPool.cleanPool('Canvas'); 101 | wdi.GlobalPool.cleanPool('Image'); 102 | if(wdi.logOperations) { 103 | wdi.DataLogger.log(spiceMessage, this.start); 104 | } 105 | } 106 | 107 | 108 | 109 | } 110 | 111 | -------------------------------------------------------------------------------- /unittest/graphic.test.js: -------------------------------------------------------------------------------- 1 | suite("Graphic suite", function () { 2 | var sut, clientGui; 3 | var imageData, brush, opaque; 4 | var imageDescriptor = { 5 | width: 10, 6 | height: 10 7 | }; 8 | var header = { 9 | top_down: true 10 | }; 11 | 12 | setup(function () { 13 | sut = wdi.graphics; 14 | 15 | var context = $('')[0].getContext('2d'); 16 | clientGui = { 17 | getContext: function (pos) { 18 | return context; 19 | } 20 | }; 21 | }); 22 | 23 | teardown(function () { 24 | 25 | }); 26 | 27 | function testCheckingImageUncompressorIsCalled (method, self) { 28 | var imageUncompressor = new wdi.ImageUncompressor(); 29 | var imageUncompressorStub = self.stub(imageUncompressor, 'process'); 30 | var getInstanceStub = self.stub(wdi.ImageUncompressor, 'getSyncInstance') 31 | .returns(imageUncompressor); 32 | 33 | sut[method](imageDescriptor, imageData, brush, opaque, clientGui); 34 | 35 | sinon.assert.calledWithExactly( 36 | imageUncompressorStub, imageDescriptor, imageData, 37 | brush, opaque, clientGui, sinon.match.func, sut 38 | ); 39 | } 40 | 41 | test('processQuic calls ImageUncompressor.process', sinon.test(function() { 42 | testCheckingImageUncompressorIsCalled('processQuic', this); 43 | })); 44 | 45 | test('processLz calls ImageUncompressor.process', sinon.test(function() { 46 | testCheckingImageUncompressorIsCalled('processLz', this); 47 | })); 48 | 49 | 50 | function testFunctionsReturnsImageData(method, self) { 51 | var processResult = new ArrayBuffer([1, 2, 3, 4]); 52 | var imageUncompressor = new wdi.ImageUncompressor(); 53 | var imageUncompressorStub1 = self.stub(imageUncompressor, 'process', 54 | function(imageDescriptor, imageData, brush, opaque, clientGui, callback, scope) { 55 | callback.call(scope, processResult); 56 | }); 57 | 58 | var imageUncompressorStub2 = self.stub(imageUncompressor, 'extractLzHeader').returns({ 59 | header: header, 60 | imageData: 'an image Data' 61 | }); 62 | 63 | var getInstanceStub = self.stub(wdi.ImageUncompressor, 'getSyncInstance') 64 | .returns(imageUncompressor); 65 | 66 | var u8 = new Uint8Array(processResult); 67 | var source_img = clientGui.getContext(0).createImageData(imageDescriptor.width, imageDescriptor.height); 68 | source_img.data.set(u8); 69 | 70 | var actual = sut[method](imageDescriptor, imageData, brush, opaque, clientGui); 71 | 72 | assert.deepEqual(actual, source_img); 73 | } 74 | 75 | 76 | test('processQuic returns an imageData', sinon.test(function() { 77 | testFunctionsReturnsImageData('processQuic', this); 78 | })); 79 | 80 | test('processLz returns an imageData', sinon.test(function() { 81 | testFunctionsReturnsImageData('processLz', this); 82 | })); 83 | 84 | function testFlip (self, processResult) { 85 | processResult = processResult || new ArrayBuffer([1, 2, 3, 4]); 86 | var imageUncompressor = new wdi.ImageUncompressor(); 87 | var imageUncompressorStub = self.stub(imageUncompressor, 'process', 88 | function(imageDescriptor, imageData, brush, opaque, clientGui, callback, scope) { 89 | callback.call(scope, processResult); 90 | }); 91 | 92 | var imageUncompressorStub2 = self.stub(imageUncompressor, 'extractLzHeader').returns({ 93 | header: header, 94 | imageData: 'an image Data' 95 | }); 96 | 97 | var getInstanceStub = self.stub(wdi.ImageUncompressor, 'getSyncInstance') 98 | .returns(imageUncompressor); 99 | 100 | sut.processLz(imageDescriptor, imageData, brush, opaque, clientGui); 101 | } 102 | 103 | test('processLz flips the image if topDown falsy in header', sinon.test(function () { 104 | header.top_down = false; 105 | 106 | var flipStub = this.stub(sut, 'imageFlip'); 107 | var processResult = new ArrayBuffer([1, 2, 3, 4]); 108 | 109 | testFlip(this, processResult); 110 | 111 | var u8 = new Uint8Array(processResult); 112 | var source_img = clientGui.getContext(0).createImageData(imageDescriptor.width, imageDescriptor.height); 113 | source_img.data.set(u8); 114 | 115 | sinon.assert.calledWithExactly(flipStub, source_img); 116 | 117 | })); 118 | 119 | test('processLz never flips the image if topDown truthy in header', sinon.test(function () { 120 | header.top_down = true; 121 | 122 | var flipStub = this.stub(sut, 'imageFlip'); 123 | 124 | testFlip(this); 125 | 126 | sinon.assert.notCalled(flipStub); 127 | })); 128 | }); 129 | -------------------------------------------------------------------------------- /lib/GenericObjectPool.js: -------------------------------------------------------------------------------- 1 | /* 2 | Generic Object Pooling from: 3 | https://github.com/miohtama/objectpool.js/ 4 | MIT License 5 | 6 | Copyright (C) 2013 Mikko Ohtamaa 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | 12 | Version: 65c7399c30a3f6f3593bb4bfca3d9cde65675b84 (git commit) 13 | */ 14 | 15 | 16 | wdi.GenericObjectPool = $.spcExtend(wdi.EventObject.prototype, { 17 | 18 | /** How fast we grow */ 19 | expandFactor : 0.2, 20 | 21 | /** Minimum number of items we grow */ 22 | expandMinUnits : 16, 23 | 24 | elems : null, 25 | 26 | /** List of discarded element indexes in our this.elems pool */ 27 | freeElems : null, 28 | 29 | allocator: null, 30 | resetor: null, 31 | 32 | /** 33 | * Generic object pool for Javascript. 34 | * 35 | * @param {Function} allocator return new empty elements 36 | * 37 | * @param {Function} resetor resetor(obj, index) is called on all new elements when they are (re)allocated from pool. 38 | * This is mostly useful for making object to track its own pool index. 39 | */ 40 | init : function(params) { 41 | var allocator = params[0]; 42 | var resetor = params[1]; 43 | // Start with one element 44 | this.allocator = allocator; 45 | this.resetor = resetor; 46 | // Set initial state of 1 object 47 | this.elems = [this.allocator()]; 48 | this.freeElems = [0]; 49 | }, 50 | 51 | /** 52 | * @return {[type]} [description] 53 | */ 54 | create : function() { 55 | 56 | if(!this.freeElems.length) { 57 | this.expand(); 58 | } 59 | 60 | // See if we have any allocated elements to reuse 61 | var index = this.freeElems.pop(); 62 | var elem = this.elems[index]; 63 | this.resetor(elem, index); 64 | return elem; 65 | 66 | }, 67 | 68 | /** 69 | * How many allocated units we have 70 | * 71 | * @type {Number} 72 | */ 73 | length : function() { 74 | return this.elems.length - this.freeElems.length; 75 | }, 76 | 77 | /** 78 | * Make pool bigger by the default growth parameters. 79 | * 80 | */ 81 | expand : function() { 82 | 83 | var oldSize = this.elems.length; 84 | 85 | var growth = Math.ceil(this.elems.length * this.expandFactor); 86 | 87 | if(growth < this.expandMinUnits) { 88 | growth = this.expandMinUnits; 89 | } 90 | 91 | this.elems.length = this.elems.length + growth; 92 | 93 | for(var i=oldSize; i= 0) { 109 | throw "GeneircObjectPool: Double-free for element index: "+n; 110 | } 111 | 112 | if(this.elems[n].keepAlive) { 113 | return false; 114 | } 115 | 116 | this.freeElems.push(n); 117 | return true; 118 | }, 119 | 120 | /** 121 | * Return object at pool index n 122 | */ 123 | get : function(n) { 124 | return this.elems[n]; 125 | } 126 | }); 127 | -------------------------------------------------------------------------------- /lib/displayRouter.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.DisplayRouter = $.spcExtend(wdi.EventObject.prototype, { 34 | 35 | init: function(c) { 36 | this.clientGui = c.clientGui; 37 | this.rasterEngine = c.rasterEngine || new wdi.RasterEngine({clientGui: this.clientGui}); 38 | if(c.routeList) { 39 | this.routeList = c.routeList; 40 | } else { 41 | this._initRoutes(); 42 | } 43 | 44 | }, 45 | 46 | _initRoutes: function() { 47 | this.routeList = {}; 48 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_SURFACE_CREATE] = this.rasterEngine.drawCanvas; 49 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_SURFACE_DESTROY] = this.rasterEngine.removeCanvas; 50 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_COPY] = this.rasterEngine.drawImage; 51 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_FILL] = this.rasterEngine.drawFill; 52 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_ALPHA_BLEND] = this.rasterEngine.drawAlphaBlend; 53 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_WHITENESS] = this.rasterEngine.drawWhiteness; 54 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_BLACKNESS] = this.rasterEngine.drawBlackness; 55 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_TRANSPARENT] = this.rasterEngine.drawTransparent; 56 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_COPY_BITS] = this.rasterEngine.drawCopyBits; 57 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_TEXT] = this.rasterEngine.drawText; 58 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_STROKE] = this.rasterEngine.drawStroke; 59 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_ROP3] = this.rasterEngine.drawRop3; 60 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_INVERS] = this.rasterEngine.drawInvers; 61 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_STREAM_CREATE] = this.rasterEngine.handleStreamCreate; 62 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_STREAM_DESTROY] = this.rasterEngine.handleStreamDestroy; 63 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_STREAM_DATA] = this.rasterEngine.handleStreamData; 64 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_STREAM_CLIP] = this.rasterEngine.handleStreamClip; 65 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_BLEND] = this.rasterEngine.drawBlend; 66 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_INVAL_LIST] = this.rasterEngine.invalList; 67 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_INVAL_ALL_PALETTES] = this.rasterEngine.invalPalettes; 68 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_MARK] = false; 69 | this.routeList[wdi.SpiceVars.SPICE_MSG_DISPLAY_RESET] = false; 70 | }, 71 | 72 | processPacket: function(spiceMessage) { 73 | //filter out empty messages 74 | if(!spiceMessage) { 75 | wdi.Debug.log('DisplayProcess processPacket: Skipping empty message...'); 76 | return; 77 | } 78 | 79 | var route = this.routeList[spiceMessage.messageType]; 80 | if (route) { 81 | route.call(this.rasterEngine, spiceMessage); 82 | } 83 | } 84 | }); 85 | -------------------------------------------------------------------------------- /unittest/application.test.js: -------------------------------------------------------------------------------- 1 | suite('Application', function() { 2 | var sut, spiceConnection, fakeClientGui, clientGuiMock, toRestore = []; 3 | var timeLapseDetector; 4 | var connectionControl, fakeBusProcess, fakeBusConnection; 5 | 6 | setup(function() { 7 | wdi.Debug.debug = false; //disable debugging, it slows tests 8 | connectionControl = new wdi.ConnectionControl(); 9 | spiceConnection = new wdi.SpiceConnection({ 10 | connectionControl: new wdi.ConnectionControl() 11 | }); 12 | fakeClientGui = {}; 13 | fakeClientGui['addListener'] = function() {}; 14 | fakeClientGui['releaseAllKeys'] = function() {}; 15 | fakeClientGui['setClipBoardData'] = function() {}; 16 | clientGuiMock = sinon.mock(fakeClientGui); 17 | fakeBusProcess = {addListener: function() {}}; 18 | fakeBusConnection = {disconnect: function() {}, addListener: function() {}}; 19 | timeLapseDetector = new wdi.TimeLapseDetector(); 20 | sut = new Application({ 21 | spiceConnection: spiceConnection, 22 | busConnection: fakeBusConnection, 23 | clientGui: fakeClientGui, 24 | busProcess: fakeBusProcess, 25 | timeLapseDetector: timeLapseDetector 26 | }); 27 | }); 28 | 29 | teardown(function () { 30 | toRestore.forEach(function (item) { 31 | item.restore(); 32 | }); 33 | }); 34 | 35 | test('disconnect should call spiceConnection disconnect', function() { 36 | var mock = sinon.mock(spiceConnection); 37 | var expectation = mock.expects('disconnect').once().withArgs(); 38 | sut.disconnect(); 39 | expectation.verify(); 40 | }); 41 | 42 | test('disconnect should call busConnection disconnect', function() { 43 | sinon.stub(spiceConnection); 44 | var mock = sinon.mock(fakeBusConnection); 45 | var expectation = mock.expects('disconnect').once().withArgs(); 46 | sut.disconnect(); 47 | expectation.verify(); 48 | }); 49 | 50 | test('When spiceConnection fires channelConnected with channel inputs should call clientGui releaseAllKeys ', function () { 51 | 52 | var expectation = clientGuiMock.expects('releaseAllKeys').once(); 53 | toRestore.push(clientGuiMock); 54 | 55 | spiceConnection.fire('channelConnected', wdi.SpiceVars.SPICE_CHANNEL_INPUTS); 56 | 57 | expectation.verify(); 58 | }); 59 | 60 | test('onTimeLapseDetected calls executeExternalCallback with the lapse', sinon.test(function () { 61 | var event = 'timeLapseDetected'; 62 | var elapsed = 'fakeElapsedTime'; 63 | 64 | this.mock(sut) 65 | .expects('executeExternalCallback') 66 | .once() 67 | .withExactArgs(event, elapsed); 68 | timeLapseDetector.fire(event, elapsed); 69 | })); 70 | 71 | function testObjectFiringEventCallsExternalCallbackWithError(self, object, event, params) { 72 | self.mock(sut) 73 | .expects('executeExternalCallback') 74 | .once() 75 | .withExactArgs('error', params); 76 | object.fire(event, params); 77 | } 78 | 79 | test('on spiceConnection.error event calls externalCallback with error', sinon.test(function () { 80 | testObjectFiringEventCallsExternalCallbackWithError(this, spiceConnection, 'error', 'fakeParams'); 81 | })); 82 | 83 | test.skip('onClipBoardData calls executeExternalCallback when externalClipboardHandling', sinon.test(function () { 84 | var string = 'string to paste'; 85 | sut.externalClipoardHandling = true; 86 | this.mock(sut) 87 | .expects('executeExternalCallback') 88 | .once() 89 | .withExactArgs('clipboardEvent', string); 90 | sut.onClipBoardData([0, string]); 91 | })); 92 | 93 | test.skip('onClipBoardData calls clientGui setClipBoardData when not externalClipboardHandling', sinon.test(function () { 94 | var string = 'string to paste'; 95 | sut.externalClipoardHandling = false; 96 | this.mock(fakeClientGui) 97 | .expects('setClipBoardData') 98 | .once() 99 | .withExactArgs(string); 100 | sut.onClipBoardData([0, string]); 101 | })); 102 | 103 | test('onWrongPathError calls to executeExternalCallback', sinon.test(function () { 104 | var params = "fake params"; 105 | this.mock(sut) 106 | .expects('executeExternalCallback') 107 | .once() 108 | .withExactArgs('wrongPathError', params); 109 | sut.onWrongPathError(params); 110 | })); 111 | 112 | test('onApplicationLaunchedSuccessfully calls to executeExternalCallback', sinon.test(function () { 113 | var params = "fake params"; 114 | this.mock(sut) 115 | .expects('executeExternalCallback') 116 | .once() 117 | .withExactArgs('applicationLaunchedSuccessfully', params); 118 | sut.onApplicationLaunchedSuccessfully(params); 119 | })); 120 | 121 | }); 122 | -------------------------------------------------------------------------------- /lib/GlobalPool.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.GlobalPool = { 34 | pools: {}, 35 | retained: null, 36 | init: function() { 37 | this.retained = {}; 38 | var self = this; 39 | this.pools['ViewQueue'] = new wdi.GenericObjectPool([function() { 40 | //factory 41 | return new wdi.ViewQueue(); 42 | }, function(obj, index) { 43 | //reset 44 | obj.poolIndex = index; //update index at pool 45 | obj.setData([]); //reset the object 46 | }]); 47 | 48 | this.pools['RawSpiceMessage'] = new wdi.GenericObjectPool([function() { 49 | //factory 50 | return new wdi.RawSpiceMessage(); 51 | }, function(obj, index) { 52 | //reset 53 | obj.poolIndex = index; //update index at pool 54 | obj.set(null, null, null); //reset the object 55 | }]); 56 | 57 | this.retained['Image'] = []; 58 | this.pools['Image'] = new wdi.GenericObjectPool([function() { 59 | //factory 60 | return new Image(); 61 | }, function(obj, index) { 62 | //reset 63 | obj.poolIndex = index; 64 | obj.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==';//Blank image 1x1 pixel (avoids console error GET null image) 65 | obj.onload = null; 66 | obj.keepAlive = false; 67 | self.retained['Image'][index] = obj; 68 | }]); 69 | 70 | 71 | this.retained['Canvas'] = []; 72 | this.pools['Canvas'] = new wdi.GenericObjectPool([function() { 73 | //factory 74 | return self.createCanvas(); 75 | }, function(obj, index) { 76 | //reset 77 | obj.keepAlive = false; 78 | //obj.getContext('2d').clearRect(0, 0, obj.width, obj.height); 79 | obj.poolIndex = index; 80 | self.retained['Canvas'][index] = obj; 81 | }]); 82 | }, 83 | 84 | createCanvas: function() { 85 | return $('')[0]; 86 | }, 87 | 88 | create: function(objectType) { 89 | return this.pools[objectType].create(); 90 | }, 91 | 92 | discard: function(objectType, obj) { 93 | //check if its an autorelease pool 94 | if(this.retained.hasOwnProperty(objectType)) { 95 | delete this.retained[objectType][obj.poolIndex]; 96 | } 97 | return this.pools[objectType].discard(obj.poolIndex); 98 | }, 99 | 100 | cleanPool: function(objectType) { 101 | 102 | if(this.retained.hasOwnProperty(objectType)) { 103 | var pool = this.pools[objectType]; 104 | 105 | for(var i in this.retained[objectType]) { 106 | if(pool.discard(this.retained[objectType][i].poolIndex)) { 107 | delete this.retained[objectType][i]; 108 | } 109 | } 110 | } else { 111 | wdi.Debug.error("GlobalPool: cleanPool called with invalid objectType: ",objectType); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /network/socket.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.socketStatus = { 34 | 'idle':0, 35 | 'prepared':1, 36 | 'connected':2, 37 | 'disconnected':3, 38 | 'failed':4 39 | }; 40 | //Works only with arrays of bytes (this means each value is a number in 0 to 255) 41 | wdi.Socket = $.spcExtend(wdi.EventObject.prototype, { 42 | websocket: null, 43 | status: wdi.socketStatus.idle, 44 | binary: false, 45 | 46 | connect: function(uri) { 47 | var self = this; 48 | var protocol = 'base64'; //default protocol 49 | 50 | if(Modernizr['websocketsbinary']) { 51 | protocol = 'binary'; 52 | this.binary = true; 53 | } 54 | 55 | this.websocket = new WebSocket(uri, protocol); 56 | 57 | wdi.Debug.log("Socket: using protocol: "+protocol); 58 | 59 | if(this.binary) { 60 | this.websocket.binaryType = 'arraybuffer'; 61 | } 62 | 63 | this.status = wdi.socketStatus.prepared; 64 | this.websocket.onopen = function() { 65 | self.status = wdi.socketStatus.connected; 66 | self.fire('open'); 67 | }; 68 | this.websocket.onmessage = function(e) { 69 | self.fire('message', e.data); 70 | }; 71 | this.websocket.onclose = function(e) { 72 | self.status = wdi.socketStatus.disconnected; 73 | console.warn('Spice Web Client: ', e.code, e.reason); 74 | self.disconnect(); 75 | self.fire('error', e); 76 | }; 77 | this.websocket.onerror = function(e) { 78 | if (e.isTrusted) { 79 | 80 | var getLocation = function(href) { 81 | var l = document.createElement("a"); 82 | l.href = href; 83 | return l; 84 | }; 85 | var l = getLocation(uri); 86 | console.debug(l.hostname) 87 | alert("Could not open websocket at host " + l.hostname + ". Websocket server may be down. Also your browser may not trust the certificate. If so, please instruct your browser to trust the certificate for " +l.hostname + " and try again to open console."); 88 | 89 | } 90 | self.status = wdi.socketStatus.failed; 91 | self.fire('error', e); 92 | }; 93 | }, 94 | 95 | setOnMessageCallback: function(callback) { 96 | this.websocket.onmessage = callback; 97 | }, 98 | 99 | send: function(message) { 100 | try { 101 | this.websocket.send(this.encode_message(message)); 102 | } catch (err) { 103 | this.status = wdi.socketStatus.failed; 104 | this.fire('error', err); 105 | } 106 | }, 107 | 108 | disconnect: function() { 109 | if (this.websocket) { 110 | this.websocket.onopen = function() {}; 111 | this.websocket.onmessage = function() {}; 112 | this.websocket.onclose = function() {}; 113 | this.websocket.onerror = function() {}; 114 | this.websocket.close(); 115 | this.websocket = null; 116 | } 117 | }, 118 | 119 | setStatus: function(status) { 120 | this.status = status; 121 | this.fire('status', status); 122 | }, 123 | 124 | getStatus: function() { 125 | return this.status; 126 | }, 127 | 128 | encode_message: function(mess) { 129 | if(!this.binary) { 130 | var arr = Base64.encode(mess); 131 | return arr; 132 | } 133 | 134 | var len = mess.length; 135 | 136 | var buffer = new ArrayBuffer(len); 137 | var u8 = new Uint8Array(buffer); 138 | 139 | u8.set(mess); 140 | 141 | return u8; 142 | } 143 | }); 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Complete Spice Web Client written in HTML5 and Javascript 2 | Full and complete implementation of the SPICE protocol (by Red Hat) written in HTML5 and JavaScript. It allows any standard HTML5-ready Web Browser to connect to remote 3 | virtual sessions just by accessing a single URL. 4 | 5 | The client can be deployed through a normal web server to connect to spice sessions. To use it you would need to proxy your spice session through a websockets-to-tcp 6 | proxy like Kanaka, Websockify or similar projects. 7 | 8 | NOTE: This project is NOT based on the spice-html5 prototype. 9 | 10 | ## Features 11 | 12 | - Full QXL Support of the entire spice protocol, including clipping, masking, scaling etc (accelerated mode) 13 | - Audio support, but only for raw audio samples, not for celt 14 | - Full KeyBoard support including English, Spanish and Catalan layouts 15 | - Clipboard sharing support with customizble interface 16 | - Video streaming support with excellent performance even at 60fps FHD 1080p 17 | - Extremly high performant LZ decoder with sub <10ms for a FHD 1080P image 18 | - Pure Javascript codec for quic 19 | - Configurable multi core support using webworkers (by default it uses 4 CPU Cores) 20 | - Spice Agent support 21 | - Set resolution support 22 | - Honors spice cache for images, cursors and palettes 23 | - Very low memory footprint for a javascript application like this 24 | - Spice authentication tokens support 25 | - Supports graphic live debugging the spice protocol and to replay packets to fix bugs 26 | 27 | ##Missing features 28 | 29 | There are some SPICE features still to be implemented, the most important ones are: 30 | 31 | - Celt or other audio codec 32 | - USB redirection (not possible at browser level, maybe with a plugin?) 33 | 34 | ##Client System requirements 35 | 36 | To get the best result we recommend at least 1GB of ram and at least two cores at 1,5ghz. 37 | 38 | It should work decently on 512mb of ram and 1ghz. 39 | 40 | We have made tests in raspberry pi 2 with very good results. 41 | 42 | ##Network requirements 43 | 44 | Only Binary websockets are used to send and receive server data, so you should expect similar network requirements than SPICE itself. 45 | for a normal 1080p session the performance is very good up to 150-200ms of latency and 100kb/s bandwidth. 46 | 47 | The network consumption of a spice session depends a lot on the usage patterns. 48 | 49 | ##Performance 50 | 51 | Writing a web client for a protocol like spice is challenge because of the limited access to system resources like GPU and the way the javascript VM works. 52 | 53 | We have spent almost 2 years profiling the entire project. The lz decoder has been optimized to <10Ms for full hd images. Quic codec has been hacked a lot 54 | to get acceptable performance even being executed in javascript. 55 | 56 | We have created a graphic pipeline to remove unnecesary draw operations that are going to be overdrawn at the next known packets. We have minimized the work 57 | for the javascript GC and refined all our canvas operations and all the entire stack to prevent big data structures to be copied. 58 | 59 | You should expect a near perfect experience if you meet the client requirements and the network requirements. 60 | 61 | ##Browser support 62 | 63 | We strongly recommend use the spice web client with Chromium/Chrome or Firefox, however it should work at least on: 64 | 65 | - Google Chrome 66 | - Firefox 67 | - Internet Explorer 11 68 | - Edge 69 | 70 | 71 | ##How to use it 72 | 73 | In order to work you only need to provide the IP address of the websockets proxy and the port 74 | of the websockets proxy. 75 | 76 | You can do it permanently editing run.js or through the URL using the parameters: 77 | 78 | http://example.com/spice-web-client/index.html?host=IP_ADDRESS_OF_WEBSOCKIFY&port=TCP_PORT_OF_WEBSOCKIFY 79 | 80 | By doing this you will connect to the remote spice session and the resolution will be adapted to your browser viewport area. 81 | 82 | ##Notes For linux sessions 83 | If you are planning to use this to connect to remote linux sessions you should consider disabling compositing on your desktop. The best performance is achieved with 84 | kde with compositing and visual effects disabled. 85 | 86 | Always install the spice-vdagent and xorg-qxl to get the best results and to have custom resolutions etc. 87 | 88 | ##Notes For Windows sessions 89 | 90 | Spice web client has a very good performance connecting to remote windows sessions. Always install the spice-agent package including the qxl video driver to get the best results and to have custom resolutions etc. 91 | 92 | ##More information 93 | 94 | For more information about the implementation or questions about roadmap etc contact Jose Carlos Norte (jcarlosn) at jose@eyeos.com 95 | 96 | ##License 97 | 98 | Spice Web Client is distributed under [GNU Affero GPL3 license](http://www.gnu.org/licenses/agpl-3.0.en.html). 99 | 100 | -------------------------------------------------------------------------------- /lib/PacketWorkerIdentifier.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | /* 34 | * Check if a packet should be intercepted in packetpreprocess to be executed 35 | * in parallel. 36 | */ 37 | 38 | wdi.PacketWorkerIdentifier = $.spcExtend(wdi.EventObject.prototype, { 39 | init: function(c) { 40 | //default empty constructor 41 | }, 42 | 43 | shouldUseWorker: function(message) { 44 | switch (message.messageType) { 45 | case wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_COPY: 46 | return wdi.PacketWorkerIdentifier.processingType.DECOMPRESS; 47 | case wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_FILL: 48 | var brush = message.args.brush; 49 | if(brush.type === wdi.SpiceBrushType.SPICE_BRUSH_TYPE_PATTERN) { 50 | return wdi.PacketWorkerIdentifier.processingType.DECOMPRESS; 51 | } 52 | break; 53 | case wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_ALPHA_BLEND: 54 | return wdi.PacketWorkerIdentifier.processingType.DECOMPRESS; 55 | case wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_BLEND: 56 | return wdi.PacketWorkerIdentifier.processingType.DECOMPRESS; 57 | case wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_TRANSPARENT: 58 | return wdi.PacketWorkerIdentifier.processingType.DECOMPRESS; 59 | //case wdi.SpiceVars.SPICE_MSG_DISPLAY_STREAM_DATA: 60 | // return wdi.PacketWorkerIdentifier.processingType.PROCESSVIDEO; 61 | } 62 | 63 | return 0; 64 | }, 65 | 66 | getImageProperties: function(message) { 67 | var props = { 68 | data: null, 69 | descriptor: null, 70 | opaque: true, 71 | brush: null 72 | }; 73 | 74 | //coupling here, to be cleaned when doing real code 75 | switch (message.messageType) { 76 | case wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_COPY: 77 | props.descriptor = message.args.image.imageDescriptor; 78 | props.data = message.args.image.data; 79 | break; 80 | case wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_FILL: 81 | props.brush = message.args.brush; 82 | if(props.brush.type === wdi.SpiceBrushType.SPICE_BRUSH_TYPE_PATTERN) { 83 | props.descriptor = props.brush.pattern.image; 84 | props.data = props.brush.pattern.imageData; 85 | } else { 86 | return false; 87 | } 88 | break; 89 | case wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_ALPHA_BLEND: 90 | case wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_BLEND: 91 | case wdi.SpiceVars.SPICE_MSG_DISPLAY_DRAW_TRANSPARENT: 92 | props.data = message.args.image.data; 93 | props.descriptor = message.args.image.imageDescriptor; 94 | props.opaque = false; 95 | break; 96 | default: 97 | wdi.Debug.log("PacketWorkerIdentifier: Unknown Packet in getImageProperties"); 98 | return false; 99 | } 100 | 101 | return props; 102 | }, 103 | 104 | getVideoData: function(message) { 105 | if(message.messageType !== wdi.SpiceVars.SPICE_MSG_DISPLAY_STREAM_DATA) { 106 | wdi.Debug.log('PacketWOrkerIdentifier: Invalid packet in getVideoData'); 107 | return false; 108 | } 109 | 110 | return message.args.data; 111 | } 112 | }); 113 | 114 | wdi.PacketWorkerIdentifier.processingType = {}; 115 | wdi.PacketWorkerIdentifier.processingType.DECOMPRESS = 1; 116 | wdi.PacketWorkerIdentifier.processingType.PROCESSVIDEO = 2; 117 | -------------------------------------------------------------------------------- /benchmark.html: -------------------------------------------------------------------------------- 1 | 2 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /lib/ImageUncompressor.js: -------------------------------------------------------------------------------- 1 | /* 2 | eyeOS Spice Web Client 3 | Copyright (c) 2015 eyeOS S.L. 4 | 5 | Contact Jose Carlos Norte (jose@eyeos.com) for more information about this software. 6 | 7 | This program is free software; you can redistribute it and/or modify it under 8 | the terms of the GNU Affero General Public License version 3 as published by the 9 | Free Software Foundation. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | version 3 along with this program in the file "LICENSE". If not, see 18 | . 19 | 20 | See www.eyeos.org for more details. All requests should be sent to licensing@eyeos.org 21 | 22 | The interactive user interfaces in modified source and object code versions 23 | of this program must display Appropriate Legal Notices, as required under 24 | Section 5 of the GNU Affero General Public License version 3. 25 | 26 | In accordance with Section 7(b) of the GNU Affero General Public License version 3, 27 | these Appropriate Legal Notices must retain the display of the "Powered by 28 | eyeos" logo and retain the original copyright notice. If the display of the 29 | logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices 30 | must display the words "Powered by eyeos" and retain the original copyright notice. 31 | */ 32 | 33 | wdi.ImageUncompressor = $.spcExtend(wdi.EventObject.prototype, { 34 | init: function (c) { 35 | this.syncAsyncHandler = c.syncAsyncHandler || new wdi.SyncAsyncHandler({ 36 | isAsync: c.isAsync 37 | }); 38 | }, 39 | 40 | lzHeaderSize: 32, 41 | 42 | extractLzHeader: function (imageData, brush) { 43 | var headerData, header; 44 | if (!brush) { //brushes are still js arrays 45 | if (Object.prototype.toString.call(imageData) === "[object Array]") { 46 | headerData = imageData.slice(0, this.lzHeaderSize); 47 | imageData = imageData.slice(this.lzHeaderSize); //skip the header 48 | } else { 49 | headerData = imageData.subarray(0, this.lzHeaderSize).toJSArray(); 50 | imageData = imageData.subarray(this.lzHeaderSize); //skip the header 51 | } 52 | header = wdi.LZSS.demarshall_rgb(headerData); 53 | } else { 54 | header = wdi.LZSS.demarshall_rgb(imageData); 55 | } 56 | 57 | return { 58 | header: header, 59 | imageData: imageData 60 | }; 61 | }, 62 | 63 | processLz: function (imageData, brush, opaque, clientGui, callback, scope) { 64 | var extractedData, u8, buffer, number, context; 65 | 66 | extractedData = this.extractLzHeader(imageData, brush); 67 | imageData = extractedData.imageData; 68 | number = extractedData.header.width * extractedData.header.height * 4; 69 | 70 | buffer = new ArrayBuffer(imageData.length + 16); 71 | u8 = new Uint8Array(buffer); 72 | 73 | u8[0] = 1; //LZ_RGB 74 | u8[1] = opaque; 75 | u8[2] = extractedData.header.type; 76 | u8[3] = extractedData.header.top_down; //padding 77 | 78 | for (var i = 0; i < 4; i++) { //iterations because of javascript number size 79 | u8[4 + i] = number & (255); //Get only the last byte 80 | number = number >> 8; //Remove the last byte 81 | } 82 | 83 | var view = new DataView(buffer); 84 | view.setUint32(8, extractedData.header.width); 85 | view.setUint32(12, extractedData.header.height); 86 | 87 | u8.set(imageData, 16); 88 | 89 | this.syncAsyncHandler.dispatch(buffer, callback, scope); 90 | }, 91 | 92 | processQuic: function (imageData, opaque, clientGui, callback, scope) { 93 | wdi.Debug.log('Quic decode'); 94 | buffer = new ArrayBuffer(imageData.length + 4); 95 | view = new Uint8Array(buffer); 96 | 97 | view.set(imageData, 4); 98 | view[3] = opaque ? 1 : 0; 99 | view[0] = 0; //quic 100 | 101 | this.syncAsyncHandler.dispatch(buffer, callback, scope); 102 | }, 103 | 104 | process: function (imageDescriptor, imageData, brush, opaque, clientGui, callback, scope) { 105 | switch(imageDescriptor.type) { 106 | case wdi.SpiceImageType.SPICE_IMAGE_TYPE_QUIC: 107 | this.processQuic(imageData, opaque, clientGui, callback, scope); 108 | break; 109 | case wdi.SpiceImageType.SPICE_IMAGE_TYPE_LZ_RGB: 110 | this.processLz(imageData, brush, opaque, clientGui, callback, scope); 111 | break; 112 | } 113 | }, 114 | 115 | dispose: function () { 116 | this.syncAsyncHandler.dispose(); 117 | } 118 | }); 119 | 120 | var syncInstance; 121 | var asyncInstance; 122 | 123 | wdi.ImageUncompressor.getSyncInstance = function () { 124 | if (!syncInstance) { 125 | syncInstance = new wdi.ImageUncompressor({ 126 | isAsync: false 127 | }); 128 | } 129 | 130 | return syncInstance; 131 | }; 132 | 133 | wdi.ImageUncompressor.getAsyncInstance = function () { 134 | if (!asyncInstance) { 135 | asyncInstance = new wdi.ImageUncompressor({ 136 | isAsync: true 137 | }); 138 | } 139 | 140 | return asyncInstance; 141 | }; 142 | --------------------------------------------------------------------------------