GUACAMOLE-1687: Merge fix to ensure keep-alive pings are sent.

This commit is contained in:
James Muehlner
2023-05-04 16:53:30 -07:00
committed by GitHub
2 changed files with 149 additions and 37 deletions

View File

@@ -42,7 +42,36 @@ Guacamole.Client = function(tunnel) {
var currentState = STATE_IDLE; var currentState = STATE_IDLE;
var currentTimestamp = 0; var currentTimestamp = 0;
var pingInterval = null;
/**
* The rough number of milliseconds to wait between sending keep-alive
* pings. This may vary depending on how frequently the browser allows
* timers to run, as well as how frequently the client receives messages
* from the server.
*
* @private
* @constant
* @type {!number}
*/
var KEEP_ALIVE_FREQUENCY = 5000;
/**
* The current keep-alive ping timeout ID, if any. This will only be set
* upon connecting.
*
* @private
* @type {number}
*/
var keepAliveTimeout = null;
/**
* The timestamp of the point in time that the last keep-live ping was
* sent, in milliseconds elapsed since midnight of January 1, 1970 UTC.
*
* @private
* @type {!number}
*/
var lastSentKeepAlive = 0;
/** /**
* Translation from Guacamole protocol line caps to Layer line caps. * Translation from Guacamole protocol line caps to Layer line caps.
@@ -1738,12 +1767,63 @@ Guacamole.Client = function(tunnel) {
}; };
/**
* Sends a keep-alive ping to the Guacamole server, advising the server
* that the client is still connected and responding. The lastSentKeepAlive
* timestamp is automatically updated as a result of calling this function.
*
* @private
*/
var sendKeepAlive = function sendKeepAlive() {
tunnel.sendMessage('nop');
lastSentKeepAlive = new Date().getTime();
};
/**
* Schedules the next keep-alive ping based on the KEEP_ALIVE_FREQUENCY and
* the time that the last ping was sent, if ever. If enough time has
* elapsed that a ping should have already been sent, calling this function
* will send that ping immediately.
*
* @private
*/
var scheduleKeepAlive = function scheduleKeepAlive() {
window.clearTimeout(keepAliveTimeout);
var currentTime = new Date().getTime();
var keepAliveDelay = Math.max(lastSentKeepAlive + KEEP_ALIVE_FREQUENCY - currentTime, 0);
// Ping server regularly to keep connection alive, but send the ping
// immediately if enough time has elapsed that it should have already
// been sent
if (keepAliveDelay > 0)
keepAliveTimeout = window.setTimeout(sendKeepAlive, keepAliveDelay);
else
sendKeepAlive();
};
/**
* Stops sending any further keep-alive pings. If a keep-alive ping was
* scheduled to be sent, that ping is cancelled.
*
* @private
*/
var stopKeepAlive = function stopKeepAlive() {
window.clearTimeout(keepAliveTimeout);
};
tunnel.oninstruction = function(opcode, parameters) { tunnel.oninstruction = function(opcode, parameters) {
var handler = instructionHandlers[opcode]; var handler = instructionHandlers[opcode];
if (handler) if (handler)
handler(parameters); handler(parameters);
// Leverage network activity to ensure the next keep-alive ping is
// sent, even if the browser is currently throttling timers
scheduleKeepAlive();
}; };
/** /**
@@ -1757,9 +1837,8 @@ Guacamole.Client = function(tunnel) {
setState(STATE_DISCONNECTING); setState(STATE_DISCONNECTING);
// Stop ping // Stop sending keep-alive messages
if (pingInterval) stopKeepAlive();
window.clearInterval(pingInterval);
// Send disconnect message and disconnect // Send disconnect message and disconnect
tunnel.sendMessage("disconnect"); tunnel.sendMessage("disconnect");
@@ -1793,10 +1872,9 @@ Guacamole.Client = function(tunnel) {
throw status; throw status;
} }
// Ping every 5 seconds (ensure connection alive) // Regularly send keep-alive ping to ensure the server knows we're
pingInterval = window.setInterval(function() { // still here, even if not active
tunnel.sendMessage("nop"); scheduleKeepAlive();
}, 5000);
setState(STATE_WAITING); setState(STATE_WAITING);
}; };

View File

@@ -356,12 +356,16 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
} }
/** /**
* Initiates a timeout which, if data is not received, causes the tunnel * Resets the state of timers tracking network activity and stability. If
* to close with an error. * those timers are not yet started, invoking this function starts them.
* This function should be invoked when the tunnel is established and every
* time there is network activity on the tunnel, such that the timers can
* safely assume the network and/or server are not responding if this
* function has not been invoked for a significant period of time.
* *
* @private * @private
*/ */
function reset_timeout() { var resetTimers = function resetTimers() {
// Get rid of old timeouts (if any) // Get rid of old timeouts (if any)
window.clearTimeout(receive_timeout); window.clearTimeout(receive_timeout);
@@ -381,7 +385,7 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
tunnel.setState(Guacamole.Tunnel.State.UNSTABLE); tunnel.setState(Guacamole.Tunnel.State.UNSTABLE);
}, tunnel.unstableThreshold); }, tunnel.unstableThreshold);
} };
/** /**
* Closes this tunnel, signaling the given status and corresponding * Closes this tunnel, signaling the given status and corresponding
@@ -491,7 +495,7 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
message_xmlhttprequest.onreadystatechange = function() { message_xmlhttprequest.onreadystatechange = function() {
if (message_xmlhttprequest.readyState === 4) { if (message_xmlhttprequest.readyState === 4) {
reset_timeout(); resetTimers();
// 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)
@@ -581,7 +585,7 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
if (xmlhttprequest.readyState === 3 || if (xmlhttprequest.readyState === 3 ||
xmlhttprequest.readyState === 4) { xmlhttprequest.readyState === 4) {
reset_timeout(); resetTimers();
// 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) {
@@ -742,7 +746,7 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
this.connect = function(data) { this.connect = function(data) {
// Start waiting for connect // Start waiting for connect
reset_timeout(); resetTimers();
// Mark the tunnel as connecting // Mark the tunnel as connecting
tunnel.setState(Guacamole.Tunnel.State.CONNECTING); tunnel.setState(Guacamole.Tunnel.State.CONNECTING);
@@ -760,7 +764,7 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
return; return;
} }
reset_timeout(); resetTimers();
// Get UUID and HTTP-specific tunnel session token from response // Get UUID and HTTP-specific tunnel session token from response
tunnel.setUUID(connect_xmlhttprequest.responseText); tunnel.setUUID(connect_xmlhttprequest.responseText);
@@ -844,13 +848,13 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
var unstableTimeout = null; var unstableTimeout = null;
/** /**
* The current connection stability test ping interval ID, if any. This * The current connection stability test ping timeout ID, if any. This
* will only be set upon successful connection. * will only be set upon successful connection.
* *
* @private * @private
* @type {number} * @type {number}
*/ */
var pingInterval = null; var pingTimeout = null;
/** /**
* The WebSocket protocol corresponding to the protocol used for the current * The WebSocket protocol corresponding to the protocol used for the current
@@ -874,6 +878,16 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
*/ */
var PING_FREQUENCY = 500; var PING_FREQUENCY = 500;
/**
* The timestamp of the point in time that the last connection stability
* test ping was sent, in milliseconds elapsed since midnight of January 1,
* 1970 UTC.
*
* @private
* @type {!number}
*/
var lastSentPing = 0;
// Transform current URL to WebSocket URL // Transform current URL to WebSocket URL
// If not already a websocket URL // If not already a websocket URL
@@ -908,16 +922,35 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
} }
/** /**
* Initiates a timeout which, if data is not received, causes the tunnel * Sends an internal "ping" instruction to the Guacamole WebSocket
* to close with an error. * endpoint, verifying network connection stability. If the network is
* stable, the Guacamole server will receive this instruction and respond
* with an identical ping.
* *
* @private * @private
*/ */
function reset_timeout() { var sendPing = function sendPing() {
var currentTime = new Date().getTime();
tunnel.sendMessage(Guacamole.Tunnel.INTERNAL_DATA_OPCODE, 'ping', currentTime);
lastSentPing = currentTime;
};
/**
* Resets the state of timers tracking network activity and stability. If
* those timers are not yet started, invoking this function starts them.
* This function should be invoked when the tunnel is established and every
* time there is network activity on the tunnel, such that the timers can
* safely assume the network and/or server are not responding if this
* function has not been invoked for a significant period of time.
*
* @private
*/
var resetTimers = function resetTimers() {
// Get rid of old timeouts (if any) // Get rid of old timeouts (if any)
window.clearTimeout(receive_timeout); window.clearTimeout(receive_timeout);
window.clearTimeout(unstableTimeout); window.clearTimeout(unstableTimeout);
window.clearTimeout(pingTimeout);
// Clear unstable status // Clear unstable status
if (tunnel.state === Guacamole.Tunnel.State.UNSTABLE) if (tunnel.state === Guacamole.Tunnel.State.UNSTABLE)
@@ -933,7 +966,17 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
tunnel.setState(Guacamole.Tunnel.State.UNSTABLE); tunnel.setState(Guacamole.Tunnel.State.UNSTABLE);
}, tunnel.unstableThreshold); }, tunnel.unstableThreshold);
} var currentTime = new Date().getTime();
var pingDelay = Math.max(lastSentPing + PING_FREQUENCY - currentTime, 0);
// Ping tunnel endpoint regularly to test connection stability, sending
// the ping immediately if enough time has already elapsed
if (pingDelay > 0)
pingTimeout = window.setTimeout(sendPing, pingDelay);
else
sendPing();
};
/** /**
* Closes this tunnel, signaling the given status and corresponding * Closes this tunnel, signaling the given status and corresponding
@@ -949,9 +992,7 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
// Get rid of old timeouts (if any) // Get rid of old timeouts (if any)
window.clearTimeout(receive_timeout); window.clearTimeout(receive_timeout);
window.clearTimeout(unstableTimeout); window.clearTimeout(unstableTimeout);
window.clearTimeout(pingTimeout);
// Cease connection test pings
window.clearInterval(pingInterval);
// Ignore if already closed // Ignore if already closed
if (tunnel.state === Guacamole.Tunnel.State.CLOSED) if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
@@ -1010,7 +1051,7 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
this.connect = function(data) { this.connect = function(data) {
reset_timeout(); resetTimers();
// Mark the tunnel as connecting // Mark the tunnel as connecting
tunnel.setState(Guacamole.Tunnel.State.CONNECTING); tunnel.setState(Guacamole.Tunnel.State.CONNECTING);
@@ -1019,14 +1060,7 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
socket = new WebSocket(tunnelURL + "?" + data, "guacamole"); socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
socket.onopen = function(event) { socket.onopen = function(event) {
reset_timeout(); resetTimers();
// Ping tunnel endpoint regularly to test connection stability
pingInterval = setInterval(function sendPing() {
tunnel.sendMessage(Guacamole.Tunnel.INTERNAL_DATA_OPCODE,
"ping", new Date().getTime());
}, PING_FREQUENCY);
}; };
socket.onclose = function(event) { socket.onclose = function(event) {
@@ -1048,7 +1082,7 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
socket.onmessage = function(event) { socket.onmessage = function(event) {
reset_timeout(); resetTimers();
var message = event.data; var message = event.data;
var startIndex = 0; var startIndex = 0;