From 4863a8e96b3d8dd3bb3de73dc4c201054bb7728c Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 14 Nov 2014 11:46:06 -0800 Subject: [PATCH] GUAC-605: Move client and tunnel creation into factories. Use scope watch on ID to handle connect/disconnect. --- .../client/controllers/clientController.js | 9 +- .../app/client/directives/guacClient.js | 270 +++--------------- .../app/client/services/guacClientFactory.js | 169 +++++++++++ .../app/client/services/guacTunnelFactory.js | 84 ++++++ .../webapp/app/client/templates/client.html | 1 - 5 files changed, 302 insertions(+), 231 deletions(-) create mode 100644 guacamole/src/main/webapp/app/client/services/guacClientFactory.js create mode 100644 guacamole/src/main/webapp/app/client/services/guacTunnelFactory.js diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index 22d96dbf3..8257661fc 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -94,8 +94,7 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams', * Parse the type, name, and id out of the url paramteres, * as well as any extra parameters if set. */ - $scope.type = $routeParams.type; - $scope.id = $routeParams.id; + $scope.id = $routeParams.type + '/' + $routeParams.id; $scope.connectionParameters = $routeParams.params || ''; // Keep title in sync with connection state @@ -197,7 +196,7 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams', }); // Show status dialog when client errors occur - $scope.$on('guacClientError', function clientErrorListener(event, client, status, reconnect) { + $scope.$on('guacClientError', function clientErrorListener(event, client, status) { // Hide any existing status statusModal.deactivate(); @@ -216,7 +215,7 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams', }); // Show status dialog when tunnel status changes - $scope.$on('guacTunnelStateChange', function tunnelStateChangeListener(event, client, status) { + $scope.$on('guacTunnelStateChange', function tunnelStateChangeListener(event, tunnel, status) { // Hide previous status, if any statusModal.deactivate(); @@ -232,7 +231,7 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams', }); // Show status dialog when tunnel errors occur - $scope.$on('guacTunnelError', function tunnelErrorListener(event, client, status, reconnect) { + $scope.$on('guacTunnelError', function tunnelErrorListener(event, tunnel, status) { // Hide any existing status statusModal.deactivate(); diff --git a/guacamole/src/main/webapp/app/client/directives/guacClient.js b/guacamole/src/main/webapp/app/client/directives/guacClient.js index e8ed9cede..751aa06ae 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacClient.js +++ b/guacamole/src/main/webapp/app/client/directives/guacClient.js @@ -35,7 +35,6 @@ angular.module('client').directive('guacClient', [function guacClient() { // Parameters for initially connecting id : '=', - type : '=', connectionName : '=', connectionParameters : '=' }, @@ -56,10 +55,15 @@ angular.module('client').directive('guacClient', [function guacClient() { this.$apply(fn); } }; - + + $scope.guac = null; + $scope.clipboard = ""; + var $window = $injector.get('$window'), guacAudio = $injector.get('guacAudio'), guacVideo = $injector.get('guacVideo'), + guacTunnelFactory = $injector.get('guacTunnelFactory'), + guacClientFactory = $injector.get('guacClientFactory'), localStorageUtility = $injector.get('localStorageUtility'); // Get elements for DOM manipulation @@ -78,9 +82,9 @@ angular.module('client').directive('guacClient', [function guacClient() { * Updates the scale of the attached Guacamole.Client based on current window * size and "auto-fit" setting. */ - $scope.updateDisplayScale = function() { + var updateDisplayScale = function updateDisplayScale() { - var guac = $scope.attachedClient; + var guac = $scope.guac; if (!guac) return; @@ -101,163 +105,40 @@ angular.module('client').directive('guacClient', [function guacClient() { $scope.clientProperties.scale = $scope.clientProperties.maxScale; }; - - /** - * Attaches a Guacamole.Client to the client UI, such that Guacamole events - * affect the UI, and local events affect the Guacamole.Client. If a client - * is already attached, it is replaced. - * - * @param {Guacamole.Client} guac The Guacamole.Client to attach to the UI. - */ - $scope.attach = function(guac) { - // If a client is already attached, ensure it is disconnected - if ($scope.attachedClient) - $scope.attachedClient.disconnect(); + // Update active client if clipboard changes + $scope.$watch('clipboard', function clipboardChange(data) { + if ($scope.guac) + $scope.guac.setClipboard(data); + }); - // Store attached client - $scope.attachedClient = guac; + // Connect to given ID whenever ID changes + $scope.$watch('id', function(id) { - // Get display element - var guac_display = guac.getDisplay().getElement(); + // If a client is already attached, ensure it is disconnected + if ($scope.guac) + $scope.guac.disconnect(); - /* - * Update the scale of the display when the client display size changes. - */ + // Only proceed if a new client is attached + if (!id) + return; - guac.getDisplay().onresize = function() { - $scope.safeApply($scope.updateDisplayScale); - }; - - /* - * Update UI when the state of the Guacamole.Client changes. - */ - - guac.onstatechange = function(clientState) { - - switch (clientState) { - - // Idle - case 0: - - $scope.$emit('guacClientStateChange', guac, "idle"); - break; - - // Connecting - case 1: - - $scope.$emit('guacClientStateChange', guac, "connecting"); - break; - - // Connected + waiting - case 2: - - $scope.$emit('guacClientStateChange', guac, "waiting"); - break; - - // Connected - case 3: - - $scope.$emit('guacClientStateChange', guac, "connected"); - - // Update server clipboard with current data - var clipboard = localStorageUtility.get("clipboard"); - if (clipboard) - guac.setClipboard(clipboard); - - break; - - // Disconnecting / disconnected are handled by tunnel instead - case 4: - case 5: - break; - - } - }; - - // Listen for clipboard events not sent by the client - $scope.$on('guacClipboard', function onClipboardChange(event, data) { - // Update server clipboard with current data - $scope.attachedClient.setClipboard(data); - }); + // Get new client instance + var tunnel = guacTunnelFactory.getInstance(); + var guac = guacClientFactory.getInstance(tunnel); + $scope.guac = guac; /* - * Emit a name change event - */ - guac.onname = function(name) { - $scope.$emit('name', guac, name); - }; - - /* - * Disconnect and emits an error when the client receives an error - */ - guac.onerror = function(status) { - - // Disconnect, if connected - guac.disconnect(); - - $scope.$emit('guacClientError', guac, status.code); - - }; - - // Server copy handler - guac.onclipboard = function(stream, mimetype) { - - // Only text/plain is supported for now - if (mimetype !== "text/plain") { - stream.sendAck("Only text/plain supported", Guacamole.Status.Code.UNSUPPORTED); - return; - } - - var reader = new Guacamole.StringReader(stream); - var data = ""; - - // Append any received data to buffer - reader.ontext = function clipboard_text_received(text) { - data += text; - stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); - }; - - // Emit event when done - reader.onend = function clipboard_text_end() { - $scope.$emit('guacClientClipboard', guac, data); - }; - - }; - - /* - * Prompt to download file when file received. + * Update the scale of the display when the client display size changes. */ - guac.onfile = function onfile(stream, mimetype, filename) { - - // Begin file download - var guacFileStartEvent = $scope.$emit('guacFileStart', guac, stream.index, mimetype, filename); - if (!guacFileStartEvent.defaultPrevented) { - - var blob_reader = new Guacamole.BlobReader(stream, mimetype); - - // Update progress as data is received - blob_reader.onprogress = function onprogress() { - $scope.$emit('guacFileProgress', guac, stream.index, mimetype, filename); - stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); - }; - - // When complete, prompt for download - blob_reader.onend = function onend() { - $scope.$emit('guacFileEnd', guac, stream.index, mimetype, filename); - }; - - stream.sendAck("Ready", Guacamole.Status.Code.SUCCESS); - - } - - // Respond with UNSUPPORTED if download (default action) canceled within event handler - else - stream.sendAck("Download canceled", Guacamole.Status.Code.UNSUPPORTED); - + guac.getDisplay().onresize = function() { + $scope.safeApply(updateDisplayScale); }; + // Get display element + var guac_display = guac.getDisplay().getElement(); + /* * Do nothing when the display element is clicked on. */ @@ -404,35 +285,6 @@ angular.module('client').directive('guacClient', [function guacClient() { }); - }; - - - /** - * Connects to the current Guacamole connection, attaching a new Guacamole - * client to the user interface. If a Guacamole client is already attached, - * it is replaced. - */ - $scope.connect = function connect() { - - var tunnel; - - // If WebSocket available, try to use it. - if ($window.WebSocket) - tunnel = new Guacamole.ChainedTunnel( - new Guacamole.WebSocketTunnel("websocket-tunnel"), - new Guacamole.HTTPTunnel("tunnel") - ); - - // If no WebSocket, then use HTTP. - else - tunnel = new Guacamole.HTTPTunnel("tunnel"); - - // Instantiate client - var guac = new Guacamole.Client(tunnel); - - // Tie UI to client - $scope.attach(guac); - // Calculate optimal width/height for display var pixel_density = $window.devicePixelRatio || 1; var optimal_dpi = pixel_density * 96; @@ -451,15 +303,13 @@ angular.module('client').directive('guacClient', [function guacClient() { // all parameters should be preserved and passed on for // the sake of authentication. - var authToken = localStorageUtility.get('authToken'), - uniqueId = encodeURIComponent($scope.type + '/' + $scope.id); - var connectString = - "id=" + uniqueId + ($scope.connectionParameters ? '&' + $scope.connectionParameters : '') - + "&authToken="+ authToken - + "&width=" + Math.floor(optimal_width) - + "&height=" + Math.floor(optimal_height) - + "&dpi=" + Math.floor(optimal_dpi); + "id=" + encodeURIComponent($scope.id) + + "&authToken=" + encodeURIComponent(localStorageUtility.get('authToken')) + + "&width=" + Math.floor(optimal_width) + + "&height=" + Math.floor(optimal_height) + + "&dpi=" + Math.floor(optimal_dpi) + + ($scope.connectionParameters ? '&' + $scope.connectionParameters : ''); // Add audio mimetypes to connect_string guacAudio.supported.forEach(function(mimetype) { @@ -471,39 +321,11 @@ angular.module('client').directive('guacClient', [function guacClient() { connectString += "&video=" + encodeURIComponent(mimetype); }); - - // Fire events for tunnel errors - tunnel.onerror = function onerror(status) { - $scope.$emit('guacTunnelError', guac, status.code); - }; - - - // Fire events for tunnel state changes - tunnel.onstatechange = function onstatechange(state) { - - switch (state) { - - case Guacamole.Tunnel.State.CONNECTING: - $scope.$emit('guacTunnelStateChange', guac, "connecting"); - break; - - case Guacamole.Tunnel.State.OPEN: - $scope.$emit('guacTunnelStateChange', guac, "open"); - break; - - case Guacamole.Tunnel.State.CLOSED: - $scope.$emit('guacTunnelStateChange', guac, "closed"); - break; - - } - - }; - // Connect guac.connect(connectString); - }; - + }); + // Adjust scale if modified externally $scope.$watch('clientProperties.scale', function changeScale(scale) { @@ -520,8 +342,8 @@ angular.module('client').directive('guacClient', [function guacClient() { main.style.overflow = "auto"; // Apply scale if client attached - if ($scope.attachedClient) - $scope.attachedClient.getDisplay().scale(scale); + if ($scope.guac) + $scope.guac.getDisplay().scale(scale); if (scale !== $scope.clientProperties.scale) $scope.clientProperties.scale = scale; @@ -536,21 +358,21 @@ angular.module('client').directive('guacClient', [function guacClient() { // If the window is resized, attempt to resize client $window.addEventListener('resize', function onResizeWindow() { - $scope.safeApply($scope.updateDisplayScale); + $scope.safeApply(updateDisplayScale); }); var show_keyboard_gesture_possible = true; // Handle Keyboard events function __send_key(pressed, keysym) { - $scope.attachedClient.sendKeyEvent(pressed, keysym); + $scope.guac.sendKeyEvent(pressed, keysym); return false; } $scope.keydown = function keydown (keysym, keyboard) { // Only handle key events if client is attached - var guac = $scope.attachedClient; + var guac = $scope.guac; if (!guac) return true; // Handle Ctrl-shortcuts specifically @@ -584,7 +406,7 @@ angular.module('client').directive('guacClient', [function guacClient() { $scope.keyup = function keyup(keysym, keyboard) { // Only handle key events if client is attached - var guac = $scope.attachedClient; + var guac = $scope.guac; if (!guac) return true; // If lifting up on shift, toggle menu visibility if rest of gesture @@ -635,8 +457,6 @@ angular.module('client').directive('guacClient', [function guacClient() { } }); - // Connect! - $scope.connect(); }] }; }]); \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/client/services/guacClientFactory.js b/guacamole/src/main/webapp/app/client/services/guacClientFactory.js new file mode 100644 index 000000000..666563b55 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/services/guacClientFactory.js @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * 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: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * 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. + */ + +/** + * A service for creating Guacamole clients. + */ +angular.module('client').factory('guacClientFactory', ['$rootScope', + function guacClientFactory($rootScope) { + + var service = {}; + + /** + * Returns a new Guacamole client instance which connects using the + * provided tunnel. + * + * @param {Guacamole.Tunnel} tunnel The tunnel to connect through. + * @returns {Guacamole.Client} A new Guacamole client instance. + */ + service.getInstance = function getClientInstance(tunnel) { + + // Instantiate client + var guac = new Guacamole.Client(tunnel); + + /* + * Fire guacClientStateChange events when client state changes. + */ + guac.onstatechange = function onClientStateChange(clientState) { + + switch (clientState) { + + // Idle + case 0: + $rootScope.$broadcast('guacClientStateChange', guac, "idle"); + break; + + // Connecting + case 1: + $rootScope.$broadcast('guacClientStateChange', guac, "connecting"); + break; + + // Connected + waiting + case 2: + $rootScope.$broadcast('guacClientStateChange', guac, "waiting"); + break; + + // Connected + case 3: + $rootScope.$broadcast('guacClientStateChange', guac, "connected"); + + // Update server clipboard with current data + if ($rootScope.clipboard) + guac.setClipboard($rootScope.clipboard); + + break; + + // Disconnecting / disconnected are handled by tunnel instead + case 4: + case 5: + break; + + } + }; + + /* + * Fire guacClientName events when a new name is received. + */ + guac.onname = function onClientName(name) { + $rootScope.$broadcast('guacClientName', guac, name); + }; + + /* + * Disconnect and fire guacClientError when the client receives an + * error. + */ + guac.onerror = function onClientError(status) { + + // Disconnect, if connected + guac.disconnect(); + + $rootScope.$broadcast('guacClientError', guac, status.code); + + }; + + /* + * Fire guacClientClipboard events after new clipboard data is received. + */ + guac.onclipboard = function onClientClipboard(stream, mimetype) { + + // Only text/plain is supported for now + if (mimetype !== "text/plain") { + stream.sendAck("Only text/plain supported", Guacamole.Status.Code.UNSUPPORTED); + return; + } + + var reader = new Guacamole.StringReader(stream); + var data = ""; + + // Append any received data to buffer + reader.ontext = function clipboard_text_received(text) { + data += text; + stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); + }; + + // Emit event when done + reader.onend = function clipboard_text_end() { + $rootScope.$broadcast('guacClientClipboard', guac, data); + }; + + }; + + /* + * Fire guacFileStart, guacFileProgress, and guacFileEnd events during + * the receipt of files. + */ + guac.onfile = function onClientFile(stream, mimetype, filename) { + + // Begin file download + var guacFileStartEvent = $rootScope.$broadcast('guacFileStart', guac, stream.index, mimetype, filename); + if (!guacFileStartEvent.defaultPrevented) { + + var blob_reader = new Guacamole.BlobReader(stream, mimetype); + + // Update progress as data is received + blob_reader.onprogress = function onprogress() { + $rootScope.$broadcast('guacFileProgress', guac, stream.index, mimetype, filename); + stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); + }; + + // When complete, prompt for download + blob_reader.onend = function onend() { + $rootScope.$broadcast('guacFileEnd', guac, stream.index, mimetype, filename); + }; + + stream.sendAck("Ready", Guacamole.Status.Code.SUCCESS); + + } + + // Respond with UNSUPPORTED if download (default action) canceled within event handler + else + stream.sendAck("Download canceled", Guacamole.Status.Code.UNSUPPORTED); + + }; + + return guac; + + }; + + return service; + +}]); diff --git a/guacamole/src/main/webapp/app/client/services/guacTunnelFactory.js b/guacamole/src/main/webapp/app/client/services/guacTunnelFactory.js new file mode 100644 index 000000000..ead734ee1 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/services/guacTunnelFactory.js @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * 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: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * 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. + */ + +/** + * A service for creating Guacamole tunnels. + */ +angular.module('client').factory('guacTunnelFactory', ['$rootScope', '$window', + function guacTunnelFactory($rootScope, $window) { + + var service = {}; + + /** + * Returns a new Guacamole tunnel instance, using an implementation that is + * supported by the web browser. + * + * @returns {Guacamole.Tunnel} A new Guacamole tunnel instance. + */ + service.getInstance = function getTunnelInstance() { + + var tunnel; + + // If WebSocket available, try to use it. + if ($window.WebSocket) + tunnel = new Guacamole.ChainedTunnel( + new Guacamole.WebSocketTunnel('websocket-tunnel'), + new Guacamole.HTTPTunnel('tunnel') + ); + + // If no WebSocket, then use HTTP. + else + tunnel = new Guacamole.HTTPTunnel('tunnel'); + + // Fire events for tunnel errors + tunnel.onerror = function onTunnelError(status) { + $rootScope.$broadcast('guacTunnelError', tunnel, status.code); + }; + + // Fire events for tunnel state changes + tunnel.onstatechange = function onTunnelStateChange(state) { + + switch (state) { + + case Guacamole.Tunnel.State.CONNECTING: + $rootScope.$broadcast('guacTunnelStateChange', tunnel, 'connecting'); + break; + + case Guacamole.Tunnel.State.OPEN: + $rootScope.$broadcast('guacTunnelStateChange', tunnel, 'open'); + break; + + case Guacamole.Tunnel.State.CLOSED: + $rootScope.$broadcast('guacTunnelStateChange', tunnel, 'closed'); + break; + + } + + }; + + return tunnel; + + }; + + return service; + +}]); diff --git a/guacamole/src/main/webapp/app/client/templates/client.html b/guacamole/src/main/webapp/app/client/templates/client.html index c01e26873..dfa3e773f 100644 --- a/guacamole/src/main/webapp/app/client/templates/client.html +++ b/guacamole/src/main/webapp/app/client/templates/client.html @@ -25,7 +25,6 @@