Control a desktop app through web with WebSockets

François Voron
Dec 13, 2018 · 4 min read
Image for post
Image for post

Most services we are using nowadays are web-based, and it’s easy to understand why: for the developers, it’s easy to develop, maintain and update. The users have nothing to install and can start to use the product in just a few clicks.

Thanks to the evolution of web technologies these last few years, we can indeed have interactive interfaces that have nothing to envy to good-old desktop applications.

However, in some use cases, a desktop app is still needed. Think for example about services that need to access your local files to synchronize them, like Dropbox or Wuha. It could also be an app that needs to access the local devices, like your GPS in order to update the map.

For this to work, we need them to develop an app with a desktop interface, different and separated from our web one. Painful, hard to maintain, very different from web technologies (we’ll probably need a dedicated developer for that).

What if we could just have a daemon process giving us access to the local computer that we could control from our web service?

Hello, WebSockets!

Image for post
Image for post
Desktop making HTTP requests to server

However, how can the server send requests to the desktop ? It can’t. Unless we also make the desktop app a server itself. It comes with loads of issues: firewall rules? Proxying? Security? We need a better alternative.

Here comes WebSockets: it’s a web standard protocol aiming at opening a full-duplex communication channel between a client and a server. In other words, it opens a tunnel in which correspondents can both send and receive messages.

Image for post
Image for post
WebSocket between server and desktop

So, now, what about the web interface ? Well, we can just open a WebSocket between the browser and the server ; then, the server will forward the messages from the browser to the desktop app and conversely.

Image for post
Image for post
Server making the bridge between the WebSockets of web and desktop

Enough talking, now, let’s code!

Thank you, NodeJS, but we’ll take it from here

Image for post
Image for post

While asynchronous paradigm has been the prerogative of NodeJS for a while, many other languages have now implemented an asynchronous API ; like Python, a real language we all love (if you don’t, well, you should love it).

Why I talk about asynchronous programming? Because we’ll use the Python Starlette framework (from the creator of Django REST framework) which is an API framework leveraging the power of asyncio. It also implements an API to create a WebSocket server.

Prototype

  • The server will match a web client and a desktop client through a client_id, and broadcast the messages between them.
  • The desktop will report the computer CPU usage each second and make a beep when it receives the appropriate message.
  • The web will display the CPU usage it receives and will propose a button to send the beep message.

We’ll go through some parts of the code. The entire implementation is available on GitHub. Try it yourself!

Server

@app.websocket_route('/ws')
async def websocket_endpoint(websocket):
await websocket.accept()

# "Authentication" message
message = await receive_json(websocket)
client_mode = message['client_mode']
client_id = message['client_id']
websockets[client_mode][client_id] = websocket

As you can see, serving a WebSocket is a piece of cake with Starlette. After having established the connection, we expect the client to send us an “authentication” message so that we can match it with the other client.

Obviously, in a real-world application, we would have a proper authentication with a token.

We keep the websocket in memory so that we can broadcast messages into it:

try:
# Broadcast it to the mirror client
await websockets[mirror_mode][client_id].send_text(
json.dumps(message)
)
except KeyError:
logger.debug(
f'Client {client_id}[{mirror_mode}] not connected'
)

Desktop

async def handler(uri, client_id):
async with websockets.connect(uri) as websocket:
message = {
'event': 'authentication',
'client_id': client_id,
'client_mode': 'desktop'
}
await websocket.send(json_to_payload(message))

consumer_task = asyncio.ensure_future(
consumer_handler(websocket))
producer_task = asyncio.ensure_future(
cpu_usage_reporter(websocket))
done, pending = await asyncio.wait(
[consumer_task, producer_task],
return_when=asyncio.FIRST_COMPLETED,
)
for task in pending:
task.cancel()

Web

Let’s see it in action

Left: Web | Middle: Server | Right: Desktop

One interface to rule them all

Approaches like this allow us to access the local resources of the computer while keeping the user in the same, unique interface. Less confusion, less code to maintain.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store