A Functional Implementation of an Abstract Development Server
Jun 19, 2015
Let's implement an abstract development server in Haskell.
A previous post determined a loose design for an abstract development server:
- compile the server executable
- serve that executable
- start a file watcher
When a file event happens, the file watcher should run a callback that:
- starts and maintains a build process,
- terminates that process if another file event occurs, and
- restarts the server after the build process finishes, but only if it we didn’t terminate it.
Why do we terminate the current build process if a file event happens?
Because concurrent callbacks are possible.
Compiling takes time. If a compilation takes five seconds and a file event occurs in the third, that means we now have two callbacks running at the same time. This also means that we want to cancel first compilation and start a new one; we don’t care about old code when developing our server.
The specification for the above callback is unneccesarily complex. It listens to file events even though it was triggered by a file event itself. A simpler version doesn’t need to know about file events:
- Terminate the current build process if one exists.
- Start and maintain a new build.
- Restart the server after that build ends, but only if it we didn’t terminate it in some other thread.
This version requires maintaining the current build and server processes across concurrent callbacks; the file watcher will run the callback in a new thread for each file event.
System.Process
helps to manage processes in Haskell, and MVar
s will help to maintain these
processes across threads.
Managing processes
Spawning processes
System.Process.spawnProcess
takes a string – which is evaluated in a
shell – and returns an IO ProcessHandle
. It’s how we’ll spawn builds
and servers.
In this example, buildCommand
is some string passed into our program
from the command line. currentBuild
is a ProcessHandle
that
represents the current server process, and can be passed around to
functions as need be.
Terminating processes
System.Process.terminateProcess
takes a ProcessHandle
and sends a
SIGTERM
signal to it in Unix. In Windows it sends a TerminateProcess
signal. We’ll use this to terminate the build and server processes in
the callback to the file watcher.
Waiting for processes
System.Process.waitForProcess
takes a ProcessHandle
and returns an
ExitCode
after waiting for the process to finish. In the callback to
the file watcher, we need to know whether we’ve terminated the build
process we initially spawned. If the process exits successfully, it
returns an ExitSuccess
and we’ll know to restart the server. If the
process is terminated, it’ll return an ExitFailure
and we’ll know to
do nothing.
We’ll use waitForProcess
in an interesting way below.
Managing processes across threads
We’ll use MVar
s to manage processes across concurrent callbacks. An MVar
is a wrapper for a value. The wrapper stores the value in a block of
memory that is shared across threads. Many actions can be performed on
MVar
s, and they’re generally used to synchronize and send data between
threads.
Control.Concurrent.MVar
is only one way of managing shared memory in Haskell. We’ll probably be
using
System.FSNotify
to listen to file events, which has a hard dependency on MVar
. So
there’s no need to bring in another mechanism.
Creating empty MVar
s
Control.Concurrent.MVar.newEmptyMVar
creates a new, empty MVar
. We
need to create empty MVar
s to prepare for storing references to the
build and server processes.
Putting a value into an MVar
Control.Concurrent.MVar.putMVar
takes an MVar
and a value and puts
the value in the MVar
. We’ll use it to store a reference to the
server process to terminate it later. We’ll also use it to put build
processes into currentBuild
.
If serverProcess
is some ProcessHandle
of an already running server,
currentServer
in the above code now shares the serverProcess
across
threads.
Taking a value out of an MVar
Control.Concurrent.MVar.tryTakeMVar
is one way of taking a value out
of an MVar
. It takes an MVar
and returns Maybe a
. a
will always
be a ProcessHandle
in our case. If a ProcessHandle
is wrapped in the
MVar
it returns Just ProcessHandle
. If nothing is wrapped in the
MVar
it returns Nothing
.
We’ll use tryTakeMVar
to see whether there’s a build current running.
If a build process is in currentBuild
, we’ll end it with
terminateProcess
. Otherwise, we won’t do anything.
We can create a reusable function for that called
maybeTerminateProcessFromMVar
.
Putting it all together
The callback to the file watcher is the meat and potatoes of our abstract development server. So we’ll concern ourselves with that only.
We’ll call it buildAndServe
.
buildAndServe
takes a CommandLine
and the MVar
s that may contain
our build and server processes. The CommandLine
contains options
passed from the user.
First, we terminate the currently running build with
maybeTerminateProcessFromMVar
. Here it is again:
Then we start a new build with startBuild
.
startBuild
takes the build command and the currentBuild
MVar
. It
spawns the build process, puts it into the currentBuild
MVar
, and
returns the build ProcessHandle
wrapped in IO
.
Returning IO ProcessHandle
gives us the concision of calling
waitForProcess
in a pointfree
manner.
That is equivalent to calling waitForProcess build
, where build
is
the ProcessHandle
from spawnCommand buildCommand
in startBuild
.
The second argument to bind (>>=
) must be a function. That function is
applied to the value wrapped in the monadic constructor returned from
the first argument of >>=
. Our value wrapped in that monadic
constructor – IO
in this case – is the build
. So waitForProcess
is
applied to build
.
We reap the benefits of the pointfree style again when restartServer
is
applied to the exit code that waitForProcess
returns.
To reiterate: waitForProcess
is applied to the build
returned and
wrapped in IO
from startBuild
. Then restartServer
is applied to
commandLine
, serverProcess
, and the ExitCode
returned and wrapped
in IO
from waitForProcess
.
restartServer
is as follows:
Here, the pointfree style lends itself to easy pattern-matching on the
ExitCode
value constructors. Remember: we don’t want to restart the
server if the build process was terminated in some other thread. So we
won’t do anything if the ExitCode
is not ExitSuccess
:
Waiter
This program exists as a command line tool called waiter
. Check it
out on Github. The end result
differs from the description here, but contains the main idea.
To install:
Usage: waiter SERVER_COMMAND BUILD_COMMAND [-f|--file-name-regex REGEX] [-d|--dir DIR]
See the Github repo for more information.
Tweet