GUACAMOLE-567: Merge warn if network connection appears unstable.

This commit is contained in:
Nick Couchman
2018-05-27 23:07:12 -04:00
9 changed files with 280 additions and 60 deletions

View File

@@ -232,3 +232,87 @@ Guacamole.Status.Code = {
"CLIENT_TOO_MANY": 0x031D
};
/**
* Returns the Guacamole protocol status code which most closely
* represents the given HTTP status code.
*
* @param {Number} status
* The HTTP status code to translate into a Guacamole protocol status
* code.
*
* @returns {Number}
* The Guacamole protocol status code which most closely represents the
* given HTTP status code.
*/
Guacamole.Status.Code.fromHTTPCode = function fromHTTPCode(status) {
// Translate status codes with known equivalents
switch (status) {
// HTTP 400 - Bad request
case 400:
return Guacamole.Status.Code.CLIENT_BAD_REQUEST;
// HTTP 403 - Forbidden
case 403:
return Guacamole.Status.Code.CLIENT_FORBIDDEN;
// HTTP 404 - Resource not found
case 404:
return Guacamole.Status.Code.RESOURCE_NOT_FOUND;
// HTTP 429 - Too many requests
case 429:
return Guacamole.Status.Code.CLIENT_TOO_MANY;
// HTTP 503 - Server unavailable
case 503:
return Guacamole.Status.Code.SERVER_BUSY;
}
// Default all other codes to generic internal error
return Guacamole.Status.Code.SERVER_ERROR;
};
/**
* Returns the Guacamole protocol status code which most closely
* represents the given WebSocket status code.
*
* @param {Number} code
* The WebSocket status code to translate into a Guacamole protocol
* status code.
*
* @returns {Number}
* The Guacamole protocol status code which most closely represents the
* given WebSocket status code.
*/
Guacamole.Status.Code.fromWebSocketCode = function fromWebSocketCode(code) {
// Translate status codes with known equivalents
switch (code) {
// Successful disconnect (no error)
case 1000: // Normal Closure
return Guacamole.Status.Code.SUCCESS;
// Codes which indicate the server is not reachable
case 1006: // Abnormal Closure (also signalled by JavaScript when the connection cannot be opened in the first place)
case 1015: // TLS Handshake
return Guacamole.Status.Code.UPSTREAM_NOT_FOUND;
// Codes which indicate the server is reachable but busy/unavailable
case 1001: // Going Away
case 1012: // Service Restart
case 1013: // Try Again Later
case 1014: // Bad Gateway
return Guacamole.Status.Code.UPSTREAM_UNAVAILABLE;
}
// Default all other codes to generic internal error
return Guacamole.Status.Code.SERVER_ERROR;
};

View File

@@ -84,11 +84,22 @@ Guacamole.Tunnel = function() {
* The maximum amount of time to wait for data to be received, in
* milliseconds. If data is not received within this amount of time,
* the tunnel is closed with an error. The default value is 15000.
*
*
* @type {Number}
*/
this.receiveTimeout = 15000;
/**
* The amount of time to wait for data to be received before considering
* the connection to be unstable, in milliseconds. If data is not received
* within this amount of time, the tunnel status is updated to warn that
* the connection appears unresponsive and may close. The default value is
* 1500.
*
* @type {Number}
*/
this.unstableThreshold = 1500;
/**
* The UUID uniquely identifying this tunnel. If not yet known, this will
* be null.
@@ -165,7 +176,15 @@ Guacamole.Tunnel.State = {
*
* @type {Number}
*/
"CLOSED": 2
"CLOSED": 2,
/**
* The connection is open, but communication through the tunnel appears to
* be disrupted, and the connection may close as a result.
*
* @type {Number}
*/
"UNSTABLE" : 3
};
@@ -219,6 +238,14 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
*/
var receive_timeout = null;
/**
* The current connection stability timeout ID, if any.
*
* @private
* @type {Number}
*/
var unstableTimeout = null;
/**
* Additional headers to be sent in tunnel requests. This dictionary can be
* populated with key/value header pairs to pass information such as authentication
@@ -253,14 +280,24 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
*/
function reset_timeout() {
// Get rid of old timeout (if any)
// Get rid of old timeouts (if any)
window.clearTimeout(receive_timeout);
window.clearTimeout(unstableTimeout);
// Set new timeout
// Clear unstable status
if (tunnel.state === Guacamole.Tunnel.State.UNSTABLE)
tunnel.setState(Guacamole.Tunnel.State.OPEN);
// Set new timeout for tracking overall connection timeout
receive_timeout = window.setTimeout(function () {
close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout."));
}, tunnel.receiveTimeout);
// Set new timeout for tracking suspected connection instability
unstableTimeout = window.setTimeout(function() {
tunnel.setState(Guacamole.Tunnel.State.UNSTABLE);
}, tunnel.unstableThreshold);
}
/**
@@ -274,6 +311,10 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
*/
function close_tunnel(status) {
// Get rid of old timeouts (if any)
window.clearTimeout(receive_timeout);
window.clearTimeout(unstableTimeout);
// Ignore if already closed
if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
return;
@@ -382,10 +423,23 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
function handleHTTPTunnelError(xmlhttprequest) {
// Pull status code directly from headers provided by Guacamole
var code = parseInt(xmlhttprequest.getResponseHeader("Guacamole-Status-Code"));
var message = xmlhttprequest.getResponseHeader("Guacamole-Error-Message");
if (code) {
var message = xmlhttprequest.getResponseHeader("Guacamole-Error-Message");
close_tunnel(new Guacamole.Status(code, message));
}
close_tunnel(new Guacamole.Status(code, message));
// Failing that, derive a Guacamole status code from the HTTP status
// code provided by the browser
else if (xmlhttprequest.status)
close_tunnel(new Guacamole.Status(
Guacamole.Status.Code.fromHTTPCode(xmlhttprequest.status),
xmlhttprequest.statusText));
// Otherwise, assume server is unreachable
else
close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND));
}
@@ -669,6 +723,14 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
*/
var receive_timeout = null;
/**
* The current connection stability timeout ID, if any.
*
* @private
* @type {Number}
*/
var unstableTimeout = null;
/**
* The WebSocket protocol corresponding to the protocol used for the current
* location.
@@ -720,14 +782,24 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
*/
function reset_timeout() {
// Get rid of old timeout (if any)
// Get rid of old timeouts (if any)
window.clearTimeout(receive_timeout);
window.clearTimeout(unstableTimeout);
// Set new timeout
// Clear unstable status
if (tunnel.state === Guacamole.Tunnel.State.UNSTABLE)
tunnel.setState(Guacamole.Tunnel.State.OPEN);
// Set new timeout for tracking overall connection timeout
receive_timeout = window.setTimeout(function () {
close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout."));
}, tunnel.receiveTimeout);
// Set new timeout for tracking suspected connection instability
unstableTimeout = window.setTimeout(function() {
tunnel.setState(Guacamole.Tunnel.State.UNSTABLE);
}, tunnel.unstableThreshold);
}
/**
@@ -741,6 +813,10 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
*/
function close_tunnel(status) {
// Get rid of old timeouts (if any)
window.clearTimeout(receive_timeout);
window.clearTimeout(unstableTimeout);
// Ignore if already closed
if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
return;
@@ -808,13 +884,22 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
};
socket.onclose = function(event) {
close_tunnel(new Guacamole.Status(parseInt(event.reason), event.reason));
// Pull status code directly from closure reason provided by Guacamole
if (event.reason)
close_tunnel(new Guacamole.Status(parseInt(event.reason), event.reason));
// Failing that, derive a Guacamole status code from the WebSocket
// status code provided by the browser
else if (event.code)
close_tunnel(new Guacamole.Status(Guacamole.Status.Code.fromWebSocketCode(event.code)));
// Otherwise, assume server is unreachable
else
close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND));
};
socket.onerror = function(event) {
close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, event.data));
};
socket.onmessage = function(event) {
reset_timeout();
@@ -1141,51 +1226,6 @@ Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTu
}
}
/**
* Returns the Guacamole protocol status code which most closely
* represents the given HTTP status code.
*
* @private
* @param {Number} httpStatus
* The HTTP status code to translate into a Guacamole protocol status
* code.
*
* @returns {Number}
* The Guacamole protocol status code which most closely represents the
* given HTTP status code.
*/
var getGuacamoleStatusCode = function getGuacamoleStatusCode(httpStatus) {
// Translate status codes with known equivalents
switch (httpStatus) {
// HTTP 400 - Bad request
case 400:
return Guacamole.Status.Code.CLIENT_BAD_REQUEST;
// HTTP 403 - Forbidden
case 403:
return Guacamole.Status.Code.CLIENT_FORBIDDEN;
// HTTP 404 - Resource not found
case 404:
return Guacamole.Status.Code.RESOURCE_NOT_FOUND;
// HTTP 429 - Too many requests
case 429:
return Guacamole.Status.Code.CLIENT_TOO_MANY;
// HTTP 503 - Server unavailable
case 503:
return Guacamole.Status.Code.SERVER_BUSY;
}
// Default all other codes to generic internal error
return Guacamole.Status.Code.SERVER_ERROR;
};
this.sendMessage = function sendMessage(elements) {
// Do nothing
};
@@ -1248,7 +1288,8 @@ Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTu
// Fail if file could not be downloaded via HTTP
if (tunnel.onerror)
tunnel.onerror(new Guacamole.Status(getGuacamoleStatusCode(xhr.status), xhr.statusText));
tunnel.onerror(new Guacamole.Status(
Guacamole.Status.Code.fromHTTPCode(xhr.status), xhr.statusText));
tunnel.disconnect();
};

View File

@@ -626,6 +626,18 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
};
/**
* Returns whether the current connection has been flagged as unstable due
* to an apparent network disruption.
*
* @returns {Boolean}
* true if the current connection has been flagged as unstable, false
* otherwise.
*/
$scope.isConnectionUnstable = function isConnectionUnstable() {
return $scope.client && $scope.client.clientState.connectionState === ManagedClientState.ConnectionState.UNSTABLE;
};
// Show status dialog when connection status changes
$scope.$watch('client.clientState.connectionState', function clientStateChanged(connectionState) {

View File

@@ -0,0 +1,56 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#connection-warning {
position: absolute;
right: 0.25em;
bottom: 0.25em;
z-index: 20;
width: 3in;
max-width: 100%;
min-height: 1em;
border-left: 2em solid #FA0;
box-shadow: 1px 1px 2px rgba(0,0,0,0.25);
background: #FFE;
padding: 0.5em 0.75em;
font-size: .8em;
}
#connection-warning::before {
content: ' ';
display: block;
position: absolute;
left: -2em;
top: 0;
width: 1.25em;
height: 100%;
margin: 0 0.375em;
background: url('images/warning.png');
background-size: contain;
background-position: center;
background-repeat: no-repeat;
}

View File

@@ -36,6 +36,11 @@
<guac-file-transfer-manager client="client"></guac-file-transfer-manager>
</div>
<!-- Connection stability warning -->
<div id="connection-warning" ng-show="isConnectionUnstable()">
{{'CLIENT.TEXT_CLIENT_STATUS_UNSTABLE' | translate}}
</div>
<!-- Menu -->
<div class="menu" ng-class="{open: menu.shown}" id="guac-menu">
<div class="menu-content" ng-if="menu.shown">

View File

@@ -346,6 +346,18 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
ManagedClientState.ConnectionState.CONNECTING);
break;
// Connection is established
case Guacamole.Tunnel.State.OPEN:
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.CONNECTED);
break;
// Connection is established but misbehaving
case Guacamole.Tunnel.State.UNSTABLE:
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.UNSTABLE);
break;
// Connection has closed
case Guacamole.Tunnel.State.CLOSED:
ManagedClientState.setConnectionState(managedClient.clientState,

View File

@@ -88,11 +88,20 @@ angular.module('client').factory('ManagedClientState', [function defineManagedCl
/**
* The Guacamole connection has been successfully established, and
* initial graphical data has been received.
*
*
* @type String
*/
CONNECTED : "CONNECTED",
/**
* The Guacamole connection has been successfully established, but the
* network connection seems unstable. The connection may perform poorly
* or disconnect.
*
* @type String
*/
UNSTABLE : "UNSTABLE",
/**
* The Guacamole connection has terminated successfully. No errors are
* indicated.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -137,6 +137,7 @@
"TEXT_CLIENT_STATUS_IDLE" : "Idle.",
"TEXT_CLIENT_STATUS_CONNECTING" : "Connecting to Guacamole...",
"TEXT_CLIENT_STATUS_DISCONNECTED" : "You have been disconnected.",
"TEXT_CLIENT_STATUS_UNSTABLE" : "The network connection to the Guacamole server appears unstable.",
"TEXT_CLIENT_STATUS_WAITING" : "Connected to Guacamole. Waiting for response...",
"TEXT_RECONNECT_COUNTDOWN" : "Reconnecting in {REMAINING} {REMAINING, plural, one{second} other{seconds}}...",
"TEXT_FILE_TRANSFER_PROGRESS" : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}",