Web-Based Remote Desktop with Guacamole and Django


As part of a recent Rescale hackday, I wanted to incorporate in-browser remote desktop functionality for the nodes that are launched on behalf of the user. Most workflows submitted to Rescale are batch processing jobs that run unattended, but the ability to log in to a machine and run a GUI to monitor the running simulation can be useful for some users.
A quick Google search turned up a library called Guacamole that has been around for a few years now. Guacamole consists of three main components: a daemon application called guacd that translates between various remote desktop protocols (VNC, RDC, SSH) and a custom guacamole protocol, a set of Javascript libraries that render the remote desktop to a HTML5 canvas element and listen for mouse and keyboard events, and finally, a thin web application that receives HTTP requests from the js libraries and communicates with the guacd daemon(*).
Unfortunately, there is a bit of a mismatch between the provided web application, a Java servlet, and our existing Django web stack. It would be much simpler to integrate Guacamole into our existing product if there were a python version of the web app. Thankfully, the docs state that this should be relatively straightforward to do. The source code is well written and it was pretty easy to get a hackday-quality version ported over to python quickly.
The guacamole js libraries start up by making a connection request to the web app. A socket is opened to guacd at this point and assigned a tunnel ID that is then returned to the client. The key point to make here is that this socket needs to be shared across multiple HTTP requests that include the same tunnel ID. In order to do this safely, a pair of locks is used to ensure that only one thread is reading from or writing to the socket at a time. The following code snippet shows how guacd instructions are returned to the client through a Django view helper function:

sockets = {}
sockets_lock = threading.RLock()
read_lock = threading.RLock()
write_lock = threading.RLock()
pending_read_request = threading.Event()
def _do_read(request, cache_key):
    def content():
        with sockets_lock:
            guac = sockets[cache_key]
        with read_lock:
            while True:
                content = guac.read()
                if content:
                    yield content
                if pending_read_request.is_set():
                    logger.info('Letting another request take over.')
            # End-of-instruction marker
            yield '0.;'
    response = StreamingHttpResponse(
        content(), content_type='application/octet-stream')
    response['Cache-Control'] = 'no-cache'
    return response

One interesting wrinkle to point out is that the guacamole js libraries will stream data from the server using an alternating pair of AJAX requests. First, an initial AJAX request is made. At some point after this first connection is open and data is flowing back from the server, a second connection attempt is made. The web app needs to be able to detect this on the server side, end the first connection, and then start sending data back on the second connection. The ReentrantLock being used in the Java code has an easy way to tell if there are other waiting threads, but the python RLock does not expose a similar method. An Event is being used as a quick and dirty signaling mechanism between the two in-flight HTTP requests.
Obviously, this was hacked up as quickly as possible and is not suitable for production use and probably contains more than one bug in it. One of the big limitations at the moment is that the sockets connected to guacd are simply stored in a shared, global dictionary right now. As such, this is only suited for an HTTP server that runs as a single process with multiple threads handling the requests.
Overall, it was a fun little bit of code to port over. Kudos to the Guacamole authors for all of their hard work. A sample Django / Guacamole project is available on github for those curious.
(*) While Guacamole does support websockets, the HTTP tunneling approach that it uses out-of-the-box is actually a benefit when trying to support older browsers. Despite the various shims that are available to emulate modern browser behaviors, the fewer dependencies you have on HTML5 features, the better the compatibility with legacy browsers.

Similar Posts