Giving a Web Interface to a Command Line Application
This post gives some of the implementation details surrounding the Easter egg I've hidden in this website. So, spoiler alert. There's only one hint (read boldface instruction) about actually finding the egg and I've put that way way down at the bottom.
So you wrote some interesting command line utility in C++ or something. Maybe it's even interactive. And now you want to share that utility on the web. You do some googling, but conclude there's no natural way of doing this.
This is the situation I found myself in when I wanted to add an Easter egg to my website. I had a little C++ text-based game laying around that I thought would be a fun thing to stumble across.
I am not a web developer, but I've played enough with
Flask to know
my way around. I was also pretty familiar with the
module. I figured I could serve the web interface with
subprocess to run the native binary upon request.
I was imagining a very literal translation of the game: a terminal like interface where the user provides a line of input and the result is reflected in some sort of scrolling window above the input area.
However, there were two big black-boxes in my design:
- The user shouldn't have to refresh the page to get results, but I didn't know how to change page content dynamically. In other words, I didn't have any experience with AJAX.
- I wasn't sure how to handle running the binary. As it was, the process continues to run until the player dies or wins. I had a fuzzy idea about running a process per session in the background, but that was doomed as you'll read in a bit.
AJAX Requests From Client
After some googling I found that AJAX requests are the typical
method by which dynamic content is added to a page without
refreshing. Then I found this great blog-post about integrating AJAX
requests and responses into a Flask app. So I created a simple HTML
scaffolding for rough drafts: A
<div id="terminal"> for output, an
<input id="prompt"> for input, and a link like
the AJAX request.
trigger. Look at the "AJAX From The Client" section of the
aforementioned blog post, or inspect the source of the Easter egg,
to see how I did it.
trigger is a lot like his
function, except the only arguments necessary are a source/input
element and a dest/output element.
AJAX Request From Server
Once again, see "AJAX From The Server" from the other blog post for a rough outline of how to make Flask serve up a reasonable response to an AJAX request.
For my purposes, I needed to issue whatever command just came in from the user to an instance of the text game, and respond with the result.
Communicating with the Binary
Incorrect First Step
My first approach was to construct a
Popen object with
subprocess and store it in the
sessions object. That didn't
work. Next I tried the
g object, and that didn't work. I even
server side sessions and that again failed. Clearly this
approach wasn't working, and taking more shots in the dark would be
dumb. The problem was getting the
Popen objects to persist between
requests; they don't pickle and I didn't want to do a deep dive of
Flask's internal state. On top of that, providing input to and
extracting output from a running, interactive process is kind of
clumsy at best.
A Better Re-Framing
Perhaps some readers out there have a solution to the persistence problem, but I think I found a better solution to my overall objective: record every command issued during a session and for every request spin up a new instance of the game, replay all of the commands, and return the result. This solved both the persistence and I/O problems.
Earlier I had been communicating through
write methods on
Popen.stdin, but I found
that clumsy. I figured the cleanest way to retrieve output from my
game would be with the
Popen.communicate method. The problem with
that is that
communicate() blocks until the process terminates;
in an interactive process, like a game, that will not happen after
every command, like those not resulting in death or victory. So I
had to modify the game to recognize a special string as an "exit
code" such that it would immediately flush buffers and exit, like
how pilots say "over" at the end of a transmission. Then whenever
communicate() with the game process, append that command to
the end of the command list.
Sanitization must also be done to turn your textual output into
good HTML. For instance, I needed to convert newlines into
That covers the nuts and bolts of the backend. A lot of my time went into fiddling with the CSS and Jinja template to get the terminal to act the way I wanted: centered, scroll bar on the outside, command entering with the "Enter" key, etc.
Wiring it All Up
Finally, I used this DigitalOcean post as a guide for serving Flask apps with Gunicorn on Nginx. My justification for choosing Gunicorn is twofold.
- I like the name
- I found that blog post
Okay, hover over the spoiler text if you are not clever enough to find the East egg on your own.Look at the favicon