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.
helps to manage processes in Haskell, and
MVars will help to maintain these
processes across threads.
System.Process.spawnProcess takes a string – which is evaluated in a
shell – and returns an
IO ProcessHandle. It’s how we’ll spawn builds
In this example,
buildCommand is some string passed into our program
from the command line.
currentBuild is a
represents the current server process, and can be passed around to
functions as need be.
System.Process.terminateProcess takes a
ProcessHandle and sends a
SIGTERM signal to it in Unix. In Windows it sends a
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
ExitSuccess and we’ll know to restart the server. If the
process is terminated, it’ll return an
ExitFailure and we’ll know to
waitForProcess in an interesting way below.
Managing processes across threads
MVars to manage processes across concurrent callbacks. An
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
MVars, and they’re generally used to synchronize and send data between
is only one way of managing shared memory in Haskell. We’ll probably be
to listen to file events, which has a hard dependency on
there’s no need to bring in another mechanism.
Control.Concurrent.MVar.newEmptyMVar creates a new, empty
need to create empty
MVars to prepare for storing references to the
build and server processes.
Putting a value into an
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
serverProcess is some
ProcessHandle of an already running server,
currentServer in the above code now shares the
Taking a value out of an
Control.Concurrent.MVar.tryTakeMVar is one way of taking a value out
MVar. It takes an
MVar and returns
a will always
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
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
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 takes a
CommandLine and the
MVars 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 takes the build command and the
spawns the build process, puts it into the
returns the build
ProcessHandle wrapped in
IO ProcessHandle gives us the concision of calling
waitForProcess in a pointfree
That is equivalent to calling
waitForProcess build, where
spawnCommand buildCommand in
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
IO in this case – is the
We reap the benefits of the pointfree style again when
applied to the exit code that
waitForProcess is applied to the
build returned and
restartServer is applied to
serverProcess, and the
ExitCode returned and wrapped
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
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.
waiter SERVER_COMMAND BUILD_COMMAND [-f|--file-name-regex REGEX] [-d|--dir DIR]
See the Github repo for more information.Tweet