diff --git a/guacamole/src/main/java/org/apache/guacamole/tunnel/OutputStreamInterceptingFilter.java b/guacamole/src/main/java/org/apache/guacamole/tunnel/OutputStreamInterceptingFilter.java index 7cdadba9b..f01690658 100644 --- a/guacamole/src/main/java/org/apache/guacamole/tunnel/OutputStreamInterceptingFilter.java +++ b/guacamole/src/main/java/org/apache/guacamole/tunnel/OutputStreamInterceptingFilter.java @@ -47,6 +47,14 @@ public class OutputStreamInterceptingFilter private static final Logger logger = LoggerFactory.getLogger(OutputStreamInterceptingFilter.class); + /** + * Whether this OutputStreamInterceptingFilter should respond to received + * blobs with "ack" messages on behalf of the client. If false, blobs will + * still be handled by this filter, but empty blobs will be sent to the + * client, forcing the client to respond on its own. + */ + private boolean acknowledgeBlobs = true; + /** * Creates a new OutputStreamInterceptingFilter which selectively intercepts * "blob" and "end" instructions. The required "ack" responses will @@ -129,10 +137,22 @@ public class OutputStreamInterceptingFilter return null; } - // Attempt to write data to stream try { + + // Attempt to write data to stream stream.getStream().write(blob); + + // Force client to respond with their own "ack" if we need to + // confirm that they are not falling behind with respect to the + // graphical session + if (!acknowledgeBlobs) { + acknowledgeBlobs = true; + return new GuacamoleInstruction("blob", index, ""); + } + + // Otherwise, acknowledge the blob on the client's behalf sendAck(index, "OK", GuacamoleStatus.SUCCESS); + } catch (IOException e) { sendAck(index, "FAIL", GuacamoleStatus.SERVER_ERROR); @@ -164,6 +184,17 @@ public class OutputStreamInterceptingFilter } + /** + * Handles a single "sync" instruction, updating internal tracking of + * client render state. + * + * @param instruction + * The "sync" instruction being handled. + */ + private void handleSync(GuacamoleInstruction instruction) { + acknowledgeBlobs = false; + } + @Override public GuacamoleInstruction filter(GuacamoleInstruction instruction) throws GuacamoleException { @@ -178,6 +209,13 @@ public class OutputStreamInterceptingFilter return instruction; } + // Monitor "sync" instructions to ensure the client does not starve + // from lack of graphical updates + if (instruction.getOpcode().equals("sync")) { + handleSync(instruction); + return instruction; + } + // Pass instruction through untouched return instruction; diff --git a/guacamole/src/main/webapp/app/rest/services/tunnelService.js b/guacamole/src/main/webapp/app/rest/services/tunnelService.js index 11c2e92c0..f74088d4b 100644 --- a/guacamole/src/main/webapp/app/rest/services/tunnelService.js +++ b/guacamole/src/main/webapp/app/rest/services/tunnelService.js @@ -215,6 +215,11 @@ angular.module('rest').factory('tunnelService', ['$injector', document.body.removeChild(iframe); }; + // Acknowledge (and ignore) any received blobs + stream.onblob = function acknowledgeData() { + stream.sendAck('OK', Guacamole.Status.Code.SUCCESS); + }; + // Automatically remove iframe from DOM a few seconds after the stream // ends, in the browser does NOT fire the "load" event for downloads stream.onend = function downloadComplete() {