diff --git a/guacamole-common-js/src/main/webapp/modules/Client.js b/guacamole-common-js/src/main/webapp/modules/Client.js index a62b5246d..56fe4341b 100644 --- a/guacamole-common-js/src/main/webapp/modules/Client.js +++ b/guacamole-common-js/src/main/webapp/modules/Client.js @@ -42,7 +42,36 @@ Guacamole.Client = function(tunnel) { var currentState = STATE_IDLE; 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. @@ -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) { var handler = instructionHandlers[opcode]; if (handler) 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); - // Stop ping - if (pingInterval) - window.clearInterval(pingInterval); + // Stop sending keep-alive messages + stopKeepAlive(); // Send disconnect message and disconnect tunnel.sendMessage("disconnect"); @@ -1793,10 +1872,9 @@ Guacamole.Client = function(tunnel) { throw status; } - // Ping every 5 seconds (ensure connection alive) - pingInterval = window.setInterval(function() { - tunnel.sendMessage("nop"); - }, 5000); + // Regularly send keep-alive ping to ensure the server knows we're + // still here, even if not active + scheduleKeepAlive(); setState(STATE_WAITING); }; diff --git a/guacamole-common-js/src/main/webapp/modules/Tunnel.js b/guacamole-common-js/src/main/webapp/modules/Tunnel.js index 23657cddc..ec2d8259a 100644 --- a/guacamole-common-js/src/main/webapp/modules/Tunnel.js +++ b/guacamole-common-js/src/main/webapp/modules/Tunnel.js @@ -844,13 +844,13 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { 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. * * @private * @type {number} */ - var pingInterval = null; + var pingTimeout = null; /** * The WebSocket protocol corresponding to the protocol used for the current @@ -874,6 +874,16 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { */ 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 // If not already a websocket URL @@ -907,6 +917,20 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { } + /** + * Sends an internal "ping" instruction to the Guacamole WebSocket + * endpoint, verifying network connection stability. If the network is + * stable, the Guacamole server will receive this instruction and respond + * with an identical ping. + * + * @private + */ + var sendPing = function sendPing() { + var currentTime = new Date().getTime(); + tunnel.sendMessage(Guacamole.Tunnel.INTERNAL_DATA_OPCODE, 'ping', currentTime); + lastSentPing = currentTime; + }; + /** * Initiates a timeout which, if data is not received, causes the tunnel * to close with an error. @@ -918,6 +942,7 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { // Get rid of old timeouts (if any) window.clearTimeout(receive_timeout); window.clearTimeout(unstableTimeout); + window.clearTimeout(pingTimeout); // Clear unstable status if (tunnel.state === Guacamole.Tunnel.State.UNSTABLE) @@ -933,6 +958,16 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { tunnel.setState(Guacamole.Tunnel.State.UNSTABLE); }, 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(); + } /** @@ -949,9 +984,7 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { // Get rid of old timeouts (if any) window.clearTimeout(receive_timeout); window.clearTimeout(unstableTimeout); - - // Cease connection test pings - window.clearInterval(pingInterval); + window.clearTimeout(pingTimeout); // Ignore if already closed if (tunnel.state === Guacamole.Tunnel.State.CLOSED) @@ -1020,13 +1053,6 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { socket.onopen = function(event) { reset_timeout(); - - // 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) {