packages feed

sensei-0.8.0: src/Run.hs

{-# LANGUAGE CPP #-}
module Run (
  run
, runWeb
#ifdef TEST
, RunArgs(..)
, runWith
, defaultRunArgs
, watchFiles
#endif
) where

import           Imports

import qualified Data.ByteString as ByteString
import           Data.IORef
import           System.IO
import qualified System.FSNotify as FSNotify

import qualified HTTP
import qualified Session

import           EventQueue
import           Trigger
import qualified Input
import           Pager (pager)
import           Util
import           Config

waitForever :: IO ()
waitForever = forever $ threadDelay 10000000

watchFiles :: FilePath -> EventQueue -> IO () -> IO ()
watchFiles dir queue action = do
  FSNotify.withManager $ \ manager -> do
    bracket (FSNotify.watchTree manager dir isInteresting dispatch) (\ stopListening -> stopListening) $ \ _ -> do
      action
  where
    dispatch :: FSNotify.Event -> IO ()
    dispatch = \ case
      FSNotify.Added file _ _ -> emit $ FileEvent FileAdded file
      FSNotify.Modified file _ _ -> emit $ FileEvent FileModified file
      FSNotify.ModifiedAttributes file _ _ -> emit $ FileEvent FileModified file
      FSNotify.Removed file _ _ -> emit $ FileEvent FileRemoved file
      FSNotify.WatchedDirectoryRemoved _file _ _ -> pass
      FSNotify.CloseWrite file _ _ -> emit $ FileEvent FileModified file
      FSNotify.Unknown file _ _ _ -> emit $ FileEvent FileModified file

    emit :: EventQueue.Event -> IO ()
    emit = emitEvent queue

    isInteresting :: FSNotify.Event -> Bool
    isInteresting = (&&) <$> isFile <*> not . isBoring . FSNotify.eventPath

    isFile :: FSNotify.Event -> Bool
    isFile = FSNotify.eventIsDirectory >>> (== FSNotify.IsFile)

data Mode = Lenient | Strict

run :: FilePath -> [String] -> IO ()
run startupFile args = do
  runArgs@RunArgs{dir, lastOutput, queue} <- defaultRunArgs startupFile
  HTTP.withServer dir (readMVar lastOutput) $ do
    watchFiles dir queue $ do
      mode <- newIORef Lenient
      Input.watch stdin (dispatch mode queue) (emitEvent queue Done)
      runWith runArgs {args}
  where
    dispatch :: IORef Mode -> EventQueue -> Char -> IO ()
    dispatch mode queue = \ case
      '\n' -> emitEvent queue TriggerAll
      'w' -> do
        modifyIORef mode toggle
        readIORef mode >>= \ case
          Lenient -> emitEvent queue (RestartWith ["-Wdefault"])
          Strict -> emitEvent queue (RestartWith ["-Wall"])
      'q' -> emitEvent queue Done
      _ -> pass

    toggle :: Mode -> Mode
    toggle = \ case
      Lenient -> Strict
      Strict -> Lenient

defaultRunArgs :: FilePath -> IO RunArgs
defaultRunArgs startupFile = do
  queue <- newQueue
  lastOutput <- newMVar (Trigger.Success, "")
  return RunArgs {
    ignoreConfig = False
  , dir = ""
  , args = []
  , lastOutput = lastOutput
  , queue = queue
  , sessionConfig = defaultSessionConfig startupFile
  , withSession = Session.withSession
  }

data RunArgs = RunArgs {
  ignoreConfig :: Bool
, dir :: FilePath
, args :: [String]
, lastOutput :: MVar (Result, String)
, queue :: EventQueue
, sessionConfig  :: Session.Config
, withSession :: forall r. Session.Config -> [String] -> (Session.Session -> IO r) -> IO r
}

runWith :: RunArgs -> IO ()
runWith RunArgs {..} = do
  config <- case ignoreConfig of
    False -> loadConfig
    True -> return defaultConfig
  cleanup <- newIORef pass
  let
    runCleanupAction :: IO ()
    runCleanupAction = join $ atomicModifyIORef' cleanup $ (,) pass

    addCleanupAction :: IO () -> IO ()
    addCleanupAction cleanupAction = atomicModifyIORef' cleanup $ \ action -> (action >> cleanupAction, ())

    saveOutput :: IO (Trigger.Result, String) -> IO ()
    saveOutput action = do
      runCleanupAction
      result <- modifyMVar lastOutput $ \ _ -> (id &&& id) <$> action
      case result of
        (HookFailed, _output) -> pass
        (Failure, output) -> config.senseiHooksOnFailure >>= \ case
          HookSuccess -> pager output >>= addCleanupAction
          HookFailure message -> hPutStrLn stderr message
        (Success, _output) -> config.senseiHooksOnSuccess >>= \ case
          HookSuccess -> pass
          HookFailure message -> hPutStrLn stderr message

    hooks :: Hooks
    hooks = Hooks {
      beforeReload = config.senseiHooksBeforeReload
    , afterReload = config.senseiHooksAfterReload
    }

    go extraArgs = do
      status <- withSession sessionConfig (extraArgs <> args) $ \ session -> do
        let
          triggerAction = saveOutput (trigger session hooks)
          triggerAllAction = saveOutput (triggerAll session hooks)
        triggerAction
        processQueue runCleanupAction (sessionConfig.configEcho . encodeUtf8) dir queue triggerAllAction triggerAction
      case status of
        Restart mExtraArgs -> go (fromMaybe extraArgs mExtraArgs)
        Terminate -> return ()
  go []

runWeb :: FilePath -> [String] -> IO ()
runWeb startupFile args = do
  Session.withSession (defaultSessionConfig startupFile) args $ \session -> do
    _ <- trigger session defaultHooks
    lock <- newMVar ()
    HTTP.withServer "" (withMVar lock $ \() -> trigger session defaultHooks) $ do
      waitForever

defaultSessionConfig :: FilePath -> Session.Config
defaultSessionConfig startupFile = Session.Config {
  configIgnoreDotGhci = False
, configStartupFile = startupFile
, configWorkingDirectory = Nothing
, configEcho = \ string -> ByteString.putStr string >> hFlush stdout
}