GUAC-558: Add specific handler for tunnel state change. Add state property and values. Clean up warnings.

This commit is contained in:
Michael Jumper
2014-03-21 16:46:55 -07:00
parent af4de98277
commit 627271953d
2 changed files with 196 additions and 106 deletions

View File

@@ -39,7 +39,6 @@ Guacamole.Tunnel = function() {
* up to the tunnel implementation. * up to the tunnel implementation.
* *
* @param {String} data The data to send to the tunnel when connecting. * @param {String} data The data to send to the tunnel when connecting.
* @throws {Guacamole.Status} If an error occurs during connection.
*/ */
this.connect = function(data) {}; this.connect = function(data) {};
@@ -56,7 +55,14 @@ Guacamole.Tunnel = function() {
* service on the other side of the tunnel. * service on the other side of the tunnel.
*/ */
this.sendMessage = function(elements) {}; this.sendMessage = function(elements) {};
/**
* The current state of this tunnel.
*
* @type Number
*/
this.state = Guacamole.Tunnel.State.CONNECTING;
/** /**
* Fired whenever an error is encountered by the tunnel. * Fired whenever an error is encountered by the tunnel.
* *
@@ -66,6 +72,14 @@ Guacamole.Tunnel = function() {
*/ */
this.onerror = null; this.onerror = null;
/**
* Fired whenever the state of the tunnel changes.
*
* @event
* @param {Number} state The new state of the client.
*/
this.onstatechange = null;
/** /**
* Fired once for every complete Guacamole instruction received, in order. * Fired once for every complete Guacamole instruction received, in order.
* *
@@ -78,6 +92,37 @@ Guacamole.Tunnel = function() {
}; };
/**
* All possible tunnel states.
*/
Guacamole.Tunnel.State = {
/**
* A connection is in pending. It is not yet known whether connection was
* successful.
*
* @type Number
*/
"CONNECTING": 0,
/**
* Connection was successful, and data is being received.
*
* @type Number
*/
"OPEN": 1,
/**
* The connection is closed. Connection may not have been successful, the
* tunnel may have been explicitly closed by either side, or an error may
* have occurred.
*
* @type Number
*/
"CLOSED": 2
};
/** /**
* Guacamole Tunnel implemented over HTTP via XMLHttpRequest. * Guacamole Tunnel implemented over HTTP via XMLHttpRequest.
* *
@@ -99,12 +144,6 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
var TUNNEL_READ = tunnelURL + "?read:"; var TUNNEL_READ = tunnelURL + "?read:";
var TUNNEL_WRITE = tunnelURL + "?write:"; var TUNNEL_WRITE = tunnelURL + "?write:";
var STATE_IDLE = 0;
var STATE_CONNECTED = 1;
var STATE_DISCONNECTED = 2;
var currentState = STATE_IDLE;
var POLLING_ENABLED = 1; var POLLING_ENABLED = 1;
var POLLING_DISABLED = 0; var POLLING_DISABLED = 0;
@@ -114,14 +153,42 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
var sendingMessages = false; var sendingMessages = false;
var outputMessageBuffer = ""; var outputMessageBuffer = "";
/**
* Closes this tunnel, signaling the given status and corresponding
* message, which will be sent to the onerror handler if the status is
* an error status.
*
* @private
* @param {Number} guac_code The Guacamole status code related to the
* closure.
* @param {String} message A human-readable message for debugging.
*/
function close_tunnel(guac_code, message) {
// Ignore if already closed
if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
return;
// Mark as closed
tunnel.state = Guacamole.Tunnel.State.CLOSED;
if (tunnel.onstatechange)
tunnel.onstatechange(tunnel.state);
// If connection closed abnormally, signal error.
if (guac_code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror)
tunnel.onerror(guac_code, message);
}
this.sendMessage = function() { this.sendMessage = function() {
// Do not attempt to send messages if not connected // Do not attempt to send messages if not connected
if (currentState != STATE_CONNECTED) if (tunnel.state !== Guacamole.Tunnel.State.OPEN)
return; return;
// Do not attempt to send empty messages // Do not attempt to send empty messages
if (arguments.length == 0) if (arguments.length === 0)
return; return;
/** /**
@@ -168,10 +235,10 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
// Once response received, send next queued event. // Once response received, send next queued event.
message_xmlhttprequest.onreadystatechange = function() { message_xmlhttprequest.onreadystatechange = function() {
if (message_xmlhttprequest.readyState == 4) { if (message_xmlhttprequest.readyState === 4) {
// If an error occurs during send, handle it // If an error occurs during send, handle it
if (message_xmlhttprequest.status != 200) if (message_xmlhttprequest.status !== 200)
handleHTTPTunnelError(message_xmlhttprequest); handleHTTPTunnelError(message_xmlhttprequest);
// Otherwise, continue the send loop // Otherwise, continue the send loop
@@ -179,7 +246,7 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
sendPendingMessages(); sendPendingMessages();
} }
} };
message_xmlhttprequest.send(outputMessageBuffer); message_xmlhttprequest.send(outputMessageBuffer);
outputMessageBuffer = ""; // Clear buffer outputMessageBuffer = ""; // Clear buffer
@@ -231,10 +298,10 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
function parseResponse() { function parseResponse() {
// Do not handle responses if not connected // Do not handle responses if not connected
if (currentState != STATE_CONNECTED) { if (tunnel.state !== Guacamole.Tunnel.State.OPEN) {
// Clean up interval if polling // Clean up interval if polling
if (interval != null) if (interval !== null)
clearInterval(interval); clearInterval(interval);
return; return;
@@ -251,29 +318,29 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
catch (e) { status = 200; } catch (e) { status = 200; }
// Start next request as soon as possible IF request was successful // Start next request as soon as possible IF request was successful
if (nextRequest == null && status == 200) if (!nextRequest && status === 200)
nextRequest = makeRequest(); nextRequest = makeRequest();
// Parse stream when data is received and when complete. // Parse stream when data is received and when complete.
if (xmlhttprequest.readyState == 3 || if (xmlhttprequest.readyState === 3 ||
xmlhttprequest.readyState == 4) { xmlhttprequest.readyState === 4) {
// Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data) // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
if (pollingMode == POLLING_ENABLED) { if (pollingMode === POLLING_ENABLED) {
if (xmlhttprequest.readyState == 3 && interval == null) if (xmlhttprequest.readyState === 3 && !interval)
interval = setInterval(parseResponse, 30); interval = setInterval(parseResponse, 30);
else if (xmlhttprequest.readyState == 4 && interval != null) else if (xmlhttprequest.readyState === 4 && !interval)
clearInterval(interval); clearInterval(interval);
} }
// If canceled, stop transfer // If canceled, stop transfer
if (xmlhttprequest.status == 0) { if (xmlhttprequest.status === 0) {
tunnel.disconnect(); tunnel.disconnect();
return; return;
} }
// Halt on error during request // Halt on error during request
else if (xmlhttprequest.status != 200) { else if (xmlhttprequest.status !== 200) {
handleHTTPTunnelError(xmlhttprequest); handleHTTPTunnelError(xmlhttprequest);
return; return;
} }
@@ -299,13 +366,13 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
elements.push(element); elements.push(element);
// If last element, handle instruction // If last element, handle instruction
if (terminator == ";") { if (terminator === ";") {
// Get opcode // Get opcode
var opcode = elements.shift(); var opcode = elements.shift();
// Call instruction handler. // Call instruction handler.
if (tunnel.oninstruction != null) if (tunnel.oninstruction)
tunnel.oninstruction(opcode, elements); tunnel.oninstruction(opcode, elements);
// Clear elements // Clear elements
@@ -321,16 +388,16 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
// Search for end of length // Search for end of length
var lengthEnd = current.indexOf(".", startIndex); var lengthEnd = current.indexOf(".", startIndex);
if (lengthEnd != -1) { if (lengthEnd !== -1) {
// Parse length // Parse length
var length = parseInt(current.substring(elementEnd+1, lengthEnd)); var length = parseInt(current.substring(elementEnd+1, lengthEnd));
// If we're done parsing, handle the next response. // If we're done parsing, handle the next response.
if (length == 0) { if (length === 0) {
// Clean up interval if polling // Clean up interval if polling
if (interval != null) if (!interval)
clearInterval(interval); clearInterval(interval);
// Clean up object // Clean up object
@@ -369,12 +436,12 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
// If response polling enabled, attempt to detect if still // If response polling enabled, attempt to detect if still
// necessary (via wrapping parseResponse()) // necessary (via wrapping parseResponse())
if (pollingMode == POLLING_ENABLED) { if (pollingMode === POLLING_ENABLED) {
xmlhttprequest.onreadystatechange = function() { xmlhttprequest.onreadystatechange = function() {
// If we receive two or more readyState==3 events, // If we receive two or more readyState==3 events,
// there is no need to poll. // there is no need to poll.
if (xmlhttprequest.readyState == 3) { if (xmlhttprequest.readyState === 3) {
dataUpdateEvents++; dataUpdateEvents++;
if (dataUpdateEvents >= 2) { if (dataUpdateEvents >= 2) {
pollingMode = POLLING_DISABLED; pollingMode = POLLING_DISABLED;
@@ -383,7 +450,7 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
} }
parseResponse(); parseResponse();
} };
} }
// Otherwise, just parse // Otherwise, just parse
@@ -413,29 +480,39 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
this.connect = function(data) { this.connect = function(data) {
// Start tunnel and connect synchronously // Start tunnel and connect
var connect_xmlhttprequest = new XMLHttpRequest(); var connect_xmlhttprequest = new XMLHttpRequest();
connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false); connect_xmlhttprequest.onreadystatechange = function() {
if (connect_xmlhttprequest.readyState !== 4)
return;
// If failure, throw error
if (connect_xmlhttprequest.status !== 200) {
var status = getHTTPTunnelErrorStatus(connect_xmlhttprequest);
close_tunnel(status.code, status.message);
}
// Get UUID from response
tunnel_uuid = connect_xmlhttprequest.responseText;
tunnel.state = Guacamole.Tunnel.State.OPEN;
if (tunnel.onstatechange)
tunnel.onstatechange(tunnel.state);
// Start reading data
handleResponse(makeRequest());
};
connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, true);
connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8"); connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
connect_xmlhttprequest.send(data); connect_xmlhttprequest.send(data);
// If failure, throw error
if (connect_xmlhttprequest.status != 200) {
var status = getHTTPTunnelErrorStatus(connect_xmlhttprequest);
throw status;
}
// Get UUID from response
tunnel_uuid = connect_xmlhttprequest.responseText;
// Start reading data
currentState = STATE_CONNECTED;
handleResponse(makeRequest());
}; };
this.disconnect = function() { this.disconnect = function() {
currentState = STATE_DISCONNECTED; close_tunnel(Guacamole.Status.Code.SUCCESS, "Manually closed.");
}; };
}; };
@@ -473,36 +550,16 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
"https:": "wss:" "https:": "wss:"
}; };
var status_code = {
1000: "Connection closed normally.",
1001: "Connection shut down.",
1002: "Protocol error.",
1003: "Invalid data.",
1004: "[UNKNOWN, RESERVED]",
1005: "No status code present.",
1006: "Connection closed abnormally.",
1007: "Inconsistent data type.",
1008: "Policy violation.",
1009: "Message too large.",
1010: "Extension negotiation failed."
};
var STATE_IDLE = 0;
var STATE_CONNECTED = 1;
var STATE_DISCONNECTED = 2;
var currentState = STATE_IDLE;
// Transform current URL to WebSocket URL // Transform current URL to WebSocket URL
// If not already a websocket URL // If not already a websocket URL
if ( tunnelURL.substring(0, 3) != "ws:" if ( tunnelURL.substring(0, 3) !== "ws:"
&& tunnelURL.substring(0, 4) != "wss:") { && tunnelURL.substring(0, 4) !== "wss:") {
var protocol = ws_protocol[window.location.protocol]; var protocol = ws_protocol[window.location.protocol];
// If absolute URL, convert to absolute WS URL // If absolute URL, convert to absolute WS URL
if (tunnelURL.substring(0, 1) == "/") if (tunnelURL.substring(0, 1) === "/")
tunnelURL = tunnelURL =
protocol protocol
+ "//" + window.location.host + "//" + window.location.host
@@ -526,14 +583,43 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
} }
/**
* Closes this tunnel, signaling the given status and corresponding
* message, which will be sent to the onerror handler if the status is
* an error status.
*
* @private
* @param {Number} guac_code The Guacamole status code related to the
* closure.
* @param {String} message A human-readable message for debugging.
*/
function close_tunnel(guac_code, message) {
// Ignore if already closed
if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
return;
// Mark as closed
tunnel.state = Guacamole.Tunnel.State.CLOSED;
if (tunnel.onstatechange)
tunnel.onstatechange(tunnel.state);
// If connection closed abnormally, signal error.
if (guac_code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror)
tunnel.onerror(guac_code, message);
socket.close();
}
this.sendMessage = function(elements) { this.sendMessage = function(elements) {
// Do not attempt to send messages if not connected // Do not attempt to send messages if not connected
if (currentState != STATE_CONNECTED) if (tunnel.state !== Guacamole.Tunnel.State.OPEN)
return; return;
// Do not attempt to send empty messages // Do not attempt to send empty messages
if (arguments.length == 0) if (arguments.length === 0)
return; return;
/** /**
@@ -569,25 +655,17 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
socket = new WebSocket(tunnelURL + "?" + data, "guacamole"); socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
socket.onopen = function(event) { socket.onopen = function(event) {
currentState = STATE_CONNECTED; tunnel.state = Guacamole.Tunnel.State.OPEN;
if (tunnel.onstatechange)
tunnel.onstatechange(tunnel.state);
}; };
socket.onclose = function(event) { socket.onclose = function(event) {
close_tunnel(parseInt(event.reason), event.reason);
var guac_code = parseInt(event.reason);
// If connection closed abnormally, signal error.
if (guac_code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror)
tunnel.onerror(guac_code, event.reason);
}; };
socket.onerror = function(event) { socket.onerror = function(event) {
close_tunnel(Guacamole.Status.Code.SERVER_ERROR, event.data);
// Call error handler
if (tunnel.onerror)
tunnel.onerror(Guacamole.Status.Code.SERVER_ERROR, event.data);
}; };
socket.onmessage = function(event) { socket.onmessage = function(event) {
@@ -602,7 +680,7 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
// Search for end of length // Search for end of length
var lengthEnd = message.indexOf(".", startIndex); var lengthEnd = message.indexOf(".", startIndex);
if (lengthEnd != -1) { if (lengthEnd !== -1) {
// Parse length // Parse length
var length = parseInt(message.substring(elementEnd+1, lengthEnd)); var length = parseInt(message.substring(elementEnd+1, lengthEnd));
@@ -617,7 +695,7 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
// If no period, incomplete instruction. // If no period, incomplete instruction.
else else
throw new Error("Incomplete instruction."); close_tunnel(Guacamole.Status.Code.SERVER_ERROR, "Incomplete instruction.");
// We now have enough data for the element. Parse. // We now have enough data for the element. Parse.
var element = message.substring(startIndex, elementEnd); var element = message.substring(startIndex, elementEnd);
@@ -627,13 +705,13 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
elements.push(element); elements.push(element);
// If last element, handle instruction // If last element, handle instruction
if (terminator == ";") { if (terminator === ";") {
// Get opcode // Get opcode
var opcode = elements.shift(); var opcode = elements.shift();
// Call instruction handler. // Call instruction handler.
if (tunnel.oninstruction != null) if (tunnel.oninstruction)
tunnel.oninstruction(opcode, elements); tunnel.oninstruction(opcode, elements);
// Clear elements // Clear elements
@@ -652,8 +730,7 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
}; };
this.disconnect = function() { this.disconnect = function() {
currentState = STATE_DISCONNECTED; close_tunnel(Guacamole.Status.Code.SUCCESS, "Manually closed.");
socket.close();
}; };
}; };
@@ -715,6 +792,7 @@ Guacamole.ChainedTunnel = function(tunnel_chain) {
if (current_tunnel) { if (current_tunnel) {
current_tunnel.onerror = null; current_tunnel.onerror = null;
current_tunnel.oninstruction = null; current_tunnel.oninstruction = null;
current_tunnel.onstatechange = null;
} }
// Set own functions to tunnel's functions // Set own functions to tunnel's functions
@@ -724,19 +802,30 @@ Guacamole.ChainedTunnel = function(tunnel_chain) {
// Record current tunnel // Record current tunnel
current_tunnel = tunnel; current_tunnel = tunnel;
// Wrap own onstatechange within current tunnel
current_tunnel.onstatechange = function(state) {
// Invoke handler
if (chained_tunnel.onstatechange)
chained_tunnel.onstatechange(state);
// Use handlers permanently from now on
if (state === Guacamole.Tunnel.State.OPEN) {
current_tunnel.onstatechange = chained_tunnel.onstatechange;
current_tunnel.oninstruction = chained_tunnel.oninstruction;
current_tunnel.onerror = chained_tunnel.onerror;
}
};
// Wrap own oninstruction within current tunnel // Wrap own oninstruction within current tunnel
current_tunnel.oninstruction = function(opcode, elements) { current_tunnel.oninstruction = function(opcode, elements) {
// Invoke handler // Invoke handler
chained_tunnel.oninstruction(opcode, elements); if (chained_tunnel.oninstruction)
chained_tunnel.oninstruction(opcode, elements);
// Use handler permanently from now on };
current_tunnel.oninstruction = chained_tunnel.oninstruction;
// Pass through errors (without trying other tunnels)
current_tunnel.onerror = chained_tunnel.onerror;
}
// Attach next tunnel on error // Attach next tunnel on error
current_tunnel.onerror = function(status) { current_tunnel.onerror = function(status) {

View File

@@ -89,7 +89,7 @@
// If no WebSocket, then use HTTP. // If no WebSocket, then use HTTP.
else else
tunnel = new Guacamole.HTTPTunnel("tunnel") tunnel = new Guacamole.HTTPTunnel("tunnel");
// Instantiate client // Instantiate client
var guac = new Guacamole.Client(tunnel); var guac = new Guacamole.Client(tunnel);
@@ -135,13 +135,14 @@
connect_string += "&video=" + encodeURIComponent(mimetype); connect_string += "&video=" + encodeURIComponent(mimetype);
}); });
try { // Show connection errors from tunnel
guac.connect(connect_string); tunnel.onerror = function(status) {
}
catch (status) {
var message = GuacUI.Client.errors[status.code] || GuacUI.Client.errors.DEFAULT; var message = GuacUI.Client.errors[status.code] || GuacUI.Client.errors.DEFAULT;
GuacUI.Client.showError("Cannot Connect", message); GuacUI.Client.showError("Connection Error", message);
} };
// Connect
guac.connect(connect_string);
}, 0); }, 0);
}; };