packages feed

robin-0.1.0.0: src/Main.hs

{-# LANGUAGE OverloadedStrings #-}
module Main (main) where

import Control.Concurrent
import Control.Lens
import Control.Monad
import Control.Monad.Except
import Control.Monad.Reader
import Control.Monad.Trans.Resource
import System.Environment
import System.Exit
import System.FilePath
import System.IO
import System.Posix.Daemonize
import System.Posix.Directory
import System.Posix.IO
import System.Posix.Process
import System.Posix.Signals
import System.Process

import Command
import Lock
import PidFile
import Types
import Watch
import Util

check :: Either Err a -> IO ()
check = maybe (return ()) print . leftMaybe

startDaemon :: IO ()
startDaemon = do
  wd <- getWorkingDirectory
  -- Fork before calling daemonize, because it calls exit after forking the daemon.
  void $ forkProcess $ daemonize $ do
    changeWorkingDirectory wd
    (check =<<) . runI $ do
      logFileString <- view envLogFile
      let
      { openLog = do
          (hKey, logHandle) <- allocate (openFile logFileString AppendMode) hClose
          (fdKey, logFd) <- allocate (handleToFd logHandle) closeFd
          void $ unprotect hKey
          void $ allocate (dupTo logFd stdOutput) closeFd
          release fdKey
      }
      openLog
      debug "Daemon started; log opened."
      start

usage :: IO ()
usage = hPutStrLn stderr
  "Usage: cabal-build-daemon (start | stop | build | watch | debug)"

commands :: [(String, IO ())]
commands =
  [ ("start" , startDaemon)
  , ("debug" , runI start >>= check)
  , ("stop"  , runI stop >>= check)
  , ("build" , startDaemon >> runM waitForBuild >>= check)
  , ("watch" , startDaemon >> runM watchBuilds >>= check)
  , ("help"  , usage)
  ]

errExit :: IO ()
errExit = usage >> exitFailure

command :: [String] -> IO ()
command []     = command ["build"]
command [name] = maybe errExit id $ lookup name commands
command _      = errExit

main :: IO ()
main = getArgs >>= command

exts :: [String]
exts = ["hs", "lhs", "cabal", "yaml"]

srcPred :: WatchPred
srcPred = PredDisj $ map PredExtension exts

start :: I ()
start = do
  repoRoot <- view envRepoRoot
  readPidFile >>= \pid -> case pid of
    Just pid -> do
      debugs $ "Daemon already running: pid " ++ show pid
    Nothing -> do
      debug "Creating lock..."
      void ensureLock
      debug "Starting notification manager..."
      do
        mainThread <- liftIO myThreadId
        liftIO $ do
          let
          { handler = Catch $ do
              debug "Caught signal; throwing ExitSuccess to main thread"
              throwTo mainThread $ ExitSuccess
          }
          let exitOn sig = void $ installHandler sig handler Nothing
          debug "Installing signal handlers..."
          exitOn sigTERM
          exitOn sigINT
        debug "Writing pidfile..."
        writePidFile
        debug "Starting event loop..."
        let
        { w = (emptyWatch $ repoRoot)
          { wRecurse  = True
          , wPred     = srcPred
          }
        }
        watchForever [w] build

-- TODO use ResourceT to be safer about the handle
runBuildProcess :: CreateProcess -> I ExitCode
runBuildProcess p = do
  repoRoot <- view envRepoRoot
  outHandle <- view envOutputFile >>= liftIO . flip openFile WriteMode
  liftIO $ do
    (_, _, _, ph) <- createProcess $ p
      { cwd = Just repoRoot
      , std_in = Inherit
      , std_out = Inherit
      , std_err = UseHandle outHandle
      }
    code <- waitForProcess ph
    hClose outHandle
    return code

build :: I ()
build = withLock $ view envRepoRoot >>= buildCommand >>= runBuildProcess >>= \case
  ExitSuccess -> writeCode 0
  ExitFailure n -> writeCode n

writeCode :: (MonadReader Env m, MonadIO m) => Int -> m ()
writeCode n = view envStatusFile >>= liftIO . flip writeFile (show n)

stop :: I ()
stop = do
  pid <- readPidFile
  case pid of
    Nothing -> return ()
    Just pid -> liftIO $ signalProcess sigTERM pid

waitForBuild :: M ()
waitForBuild = do
  (liftIO . exitWith =<<) . withLock $ do
    view envOutputFile >>= liftIO . readFile >>= liftIO . putStr
    statusStr <- view envStatusFile >>= liftIO . readFile
    return $ case readMaybe statusStr of
      Nothing -> ExitFailure 42
      Just 0  -> ExitSuccess
      Just n  -> ExitFailure n

watchBuilds :: M ()
watchBuilds = do
  statusFile <- view envStatusFile
  let
  { w = (emptyWatch $ takeDirectory statusFile)
    { wPred = PredConj [PredPath statusFile, PredInverse PredRemoved]
    }
  }
  watchForever [w] lessDump

lessDump :: M ()
lessDump = void $
  use stLessProcess >>= maybe (return ()) (liftIO . terminateProcess)
  >> dump >>= assign stLessProcess . Just

clearTerminal :: (MonadIO m) => m ProcessHandle
clearTerminal = liftIO $ runProcess "clear" [] Nothing Nothing Nothing Nothing Nothing

dump :: M ProcessHandle
dump = do
  clearProc <- clearTerminal
  lessFileString <- view envLessFile
  withLock
    $ view envOutputFile
      >>= liftIO . readFile
      >>= liftIO . writeFile lessFileString
  outHandle <- liftIO $ openFile lessFileString ReadMode
  void $ liftIO $ waitForProcess clearProc
  lessProc <-
    liftIO
    $ runProcess "less" [] Nothing Nothing (Just outHandle) Nothing Nothing
  liftIO $ hClose outHandle
  return lessProc