packages feed

bein-0.1: Bein/Minion.hs

-- Add setting status to running in db, and checking state and finalizing status when doing cleanup
module Main where

import Database.HDBC
import System.Posix.Syslog
import System.Environment (getProgName)
import Control.Monad.Reader
import Database.HDBC.PostgreSQL
import Control.Concurrent.STM
import System.Exit (ExitCode(..))
import System.Directory
import System.Posix.Signals
import System.Posix.Process
import System.FilePath.Posix (joinPath)

import Bein.Configuration
import Bein.Minion.Types
import Bein.Minion.Arguments
import Bein.SocketHandler
import Bein.Minion.Commands
import Bein.Minion.Protocol

main :: IO ()
main = do
  getSettings >>= getState >>= \st -> do
    programName <- getProgName
    withSyslog programName [PID,PERROR] DAEMON $ runReaderT main' st
      
getState :: Settings -> IO State
getState opts = do
  conn <- connectPostgreSQL "dbname=bein"
  config <- readConfiguration conn
  configTV <- newTVarIO config
  minP <- newTVarIO Nothing
  return $ State { stDb = conn, stConfigT = configTV, stSettings = opts, stMinionPid = minP }

main' :: ReaderT State IO b
main' = do 
  execution >>= \e -> checkExecution e >>= \r -> case r of
    Left errors -> dieWith errors
    Right _ -> do installHandlers
                  scratchDir >>= createScratchDirectory
                  script <- execution >>= \_ -> scratchDir >>= \s -> writeScript e s
                  forkR $ minionSocket >>= listenWith minionProtocol
                  runScript script
                  forever $ return () -- Doing this requires using exitImmediately to kill the program when we're done.

checkExecution :: ExecutionID -> BeinM State (Either String ())
checkExecution ex = do
  [notFuture, isWaiting, nonemptyScript, noNullInputs] <- 
    liftM (map fromSql) $ receiveOneRow $ query queryString [toSql ex]
  let errors = concat $ map snd $ filter (not.fst) $
               [(notFuture, "Execution " ++ show ex ++ " is not a future.\n"),
                (isWaiting, "Execution " ++ show ex ++ " is not waiting.\n"),
                (nonemptyScript, "Execution " ++ show ex ++ 
                                 " refers to a program with an empty script.\n"),
                (noNullInputs, "Execution " ++ show ex ++ " has null inputs.\n")]
    in if (notFuture && isWaiting && nonemptyScript && noNullInputs) 
         then do verboseMsgLn "Setting status to running."
                 update "update executions set status = 'running' where id=?" [toSql ex]
                 return (Right ())
         else return (Left errors)
    where queryString = "select not_future, is_waiting, nonempty_script, " ++ 
                        "no_null_inputs from runnable where id = ?"

installHandlers :: BeinM State ()
installHandlers = ask >>= \s -> do 
  verboseMsg "Installing handlers..."
  mapM_ ignoreSignal [sigALRM, sigPIPE, sigPOLL, sigPROF, sigUSR1, sigUSR2, sigVTALRM]
  liftIO $ installHandler sigCHLD (Catch $ runReaderT sigCHLDHandler s) (Just fullSignalSet)
  liftIO $ installHandler sigHUP (Catch $ runReaderT (cleanUp (ExitFailure 1)) s) (Just fullSignalSet)
  liftIO $ installHandler sigTERM (Catch $ runReaderT (cleanUp (ExitFailure 1)) s) (Just fullSignalSet)
  verboseMsgLn "ok"

sigCHLDHandler :: BeinM State ()
sigCHLDHandler = do
  v <- liftIO $ getAnyProcessStatus False False
  case v of
    Just (pid,status) -> do 
      minionPid >>= \p -> if p /= Just pid 
        then verboseMsgLn "Received sigCHLD from something besides script." >> sigCHLDHandler
        else (verboseMsgLn $ "Child exited with status " ++ show status) >> cleanUp ExitSuccess
    Nothing -> return ()
                                                  

createScratchDirectory :: FilePath -> BeinM State ()
createScratchDirectory dir = do
  verboseMsg $ "Creating scratch directory " ++ dir ++ "..."
  (liftIO $ createDirectory dir) `catchR` 
    (\e -> dieWith $ "Could not create scratch directory.\n" ++ show e)
  (liftIO $ setCurrentDirectory dir) `catchR` 
    (\e -> dieWith $ "Could not set working directory.\n" ++ show e)
  verboseMsgLn "ok"

writeScript :: ExecutionID -> FilePath -> BeinM State FilePath
writeScript ex dir = do 
  verboseMsg $ "Writing script to " ++ dir ++ "..."
  [lang,script] <- liftM (map fromSql) $ receiveOneRow $ query queryString [toSql ex]
  let scriptName = case lang of
        "perl" -> "script.pl"
        "r" -> "script.R"
        _ -> error "Invalid value for language."
  scriptPath <- scratchDir >>= (\d -> return $ joinPath [d,scriptName])
  liftIO $ writeFile scriptPath script
  verboseMsgLn "ok"
  return scriptPath
    where queryString = "select p.language,p.script from programs " ++ 
                        "as p join executions as e on e.program=p.id where e.id=?"
                     
runScript :: FilePath -> BeinM State ()
runScript script = do
  verboseMsg $ "Running script " ++ script ++ "..."
  perlCmd <- configField perl_executable
  minionPort <- configField minion_port
  p <- liftIO $ forkProcess $ executeFile perlCmd False [script,minionPort] Nothing
  writeMinionPid p
  verboseMsgLn "ok"