PE Haskell after part 2

posted on 2014-08-24

This is part 2 of a series describing how a Haskell program by my hand evolved.

The program is called after, for an introduction see part 1.

Previously

Previously I tried to use inotify on /proc, which does not work. Now we have to use a fork-join kind of model with polling threads to see if the process is still alive.

Fork-join polling threads

Fork-join is as simple as it sounds: you start multiple threads and at one point, wait for them all to finish.

We start with a simple Thread.sleep (if you speak Java) approach:

module After where

import Control.Concurrent
import System.Directory

halfASecondInMicroseconds = 500000

afterPid :: String -> IO ()
afterPid pid = do
    fileExists <- doesDirectoryExist ("/proc/" ++ pid)
    if fileExists
    then do
        threadDelay halfASecondInMicroseconds
        afterPid pid
    else return ()

As you can see, the function only returns if the /proc/pid directory nolonger exists.

Using this in a single threaded environment would just work by blocking on each pid. We could start the function in a thread and use MVar to block on each of the threads. That would also mean that we would have to do some error handling and management, which is why I opted to use the parallel-io package next.

The main function now parses the arguments and uses the global thread pool of parallel-io to implement a simple fork-join:

main :: IO ()
main = runCommand $ \opts args -> do
    if optQuiet opts
        then return ()
        else do
            parallel_ (map afterPid args)
            stopGlobalPool

The parallel_ function implements the complete fork/join and map creates a list of [IO()] actions to run. stopGlobalPool is only there for clean-up.

Configure continuous integration

Now that we have some basic functionality, we need to introduce a build-server to make sure we keep sharing a working version with the world. The easiest way I know is using Travis CI

After signing up for an account and connecting it with github, you can start using it by adding a configuration (.travis.yml) file to your repository:

ghc: 7.6
install:
  - cabal install --only-dependencies

Add some tests

There are multiple ways to add tests, starting with different test executors for Cabal.

The most basic is exitcode-stdio-1.0 and I just used github:scvalex/ltc as an example.

The only test I could come up with is making sure afterPid does not wait for the process itself:

main :: IO ()
main = defaultMain [
        testCase "neverSelf" testSelf
    ]

testSelf = do
    self <- getProcessID
    afterPid (show self)

To compile and run the test, I added the tets-suite to cabal:

test-suite afterTests
    hs-source-dirs:    test src
    main-is:           AfterTest.hs
    type:              exitcode-stdio-1.0
    build-depends:     base >=4.7 && <4.8,
                       unix ==2.7.*,
                       directory ==1.2.*,
                       filepath ==1.3.*,
                       test-framework,
                       test-framework-hunit,
                       HUnit
    default-language:  Haskell2010

As you can see, most of the dependencies of the main program are repeated. The only best way around repeating dependencies for a test is splitting of the functionality of the executable into a library. This I will do later on in the series.