packages feed

grapesy-1.2.0: test-disconnect/Test/Disconnect/Util/Process.hs

module Test.Disconnect.Util.Process (
    -- * Process lifecycle
    ChildProcess(..)
  , forkChildProcess
  , killThisProcess
    -- * Simple IPC
  , IPC(..)
  , withIPC
  ) where

import Control.Concurrent
import Control.Exception
import Control.Monad
import Data.Proxy
import System.Exit
import System.IO
import System.IO.Temp
import System.Posix

{-------------------------------------------------------------------------------
  Process lifecycle
-------------------------------------------------------------------------------}

data ChildProcess = ChildProcess{
      waitForChildProcess :: IO (Maybe ProcessStatus)
    , killChildProcess    :: IO ()
    }

-- | Fork child process
forkChildProcess :: IO () -> IO ChildProcess
forkChildProcess k = do
    child <- forkProcess k
    return $ ChildProcess{
        waitForChildProcess = getProcessStatus True False child
      , killChildProcess    = signalProcess sigKILL child
      }

-- | Unclean process termination
--
-- We need to use this to properly simulate the execution environment crashing
-- in an unrecoverable way. In particular, we don't want to give the program a
-- chance to do any of its normal exception handling/cleanup behavior.
killThisProcess :: IO ()
killThisProcess = exitImmediately $ ExitFailure 1

{-------------------------------------------------------------------------------
  Simple IPC
-------------------------------------------------------------------------------}

data IPC a = IPC{
      ipcWrite :: a -> IO ()
    , ipcRead  :: IO a
    }

-- | Setup interprocess communicaton
--
-- We use a temporary file as a very rudimentary means of inter-process
-- communication so the server (which runs in a separate process) can make the
-- client aware of the port it is assigned by the OS.
withIPC :: forall a r. (Show a, Read a) => Proxy a -> (IPC a -> IO r) -> IO r
withIPC _ k =
    withTemporaryFile $ \ipcFile -> do
      let ipcWrite :: a -> IO ()
          ipcWrite x = writeFile ipcFile (show x)

          ipcRead :: IO a
          ipcRead = do
              threadDelay 10_000
              ma <- withFile ipcFile ReadWriteMode $ \h -> do
                sz <- hFileSize h
                if sz == 0 then
                  return Nothing
                else do
                  contents <- replicateM (fromIntegral sz) $ hGetChar h
                  value    <- evaluate $ read contents
                  -- Clear the contents, so we can send the next value
                  hSetFileSize h 0
                  return $ Just value

              -- If necessary, retry (outside the scope of 'withFile')
              maybe ipcRead return ma

      k IPC{ipcWrite, ipcRead}

{-------------------------------------------------------------------------------
  Internal auxiliary
-------------------------------------------------------------------------------}

withTemporaryFile :: (FilePath -> IO a) -> IO a
withTemporaryFile k =
    withSystemTempFile "grapesy-test-disconnect.txt" $ \fp h -> do
      hClose h
      k fp