{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-| Definition of the B9 monad. It encapsulates logging, very basic command execution profiling, a reader for the "B9.B9Config" and access to the current build id, the current build directory and the artifact to build. This module is used by the _effectful_ functions in this library. -} module B9.B9Monad ( B9 , run , traceL , dbgL , infoL , errorL , getConfigParser , getConfig , getBuildId , getBuildDate , getBuildDir , getExecEnvType , getSelectedRemoteRepo , getRemoteRepos , getRepoCache , cmd ) where import B9.B9Config import B9.ConfigUtils import B9.Repository import Control.Applicative import Control.Exception ( bracket ) import Control.Monad import Control.Monad.IO.Class import Control.Monad.State import qualified Data.ByteString.Char8 as B import Data.Functor () import Data.Maybe import Data.Time.Clock import Data.Time.Format import Data.Word ( Word32 ) import System.Directory import System.Exit import System.FilePath import System.Locale ( defaultTimeLocale ) import System.Random ( randomIO ) import Text.Printf import Control.Concurrent.Async (Concurrently (..)) import Data.Conduit (($$)) import qualified Data.Conduit.List as CL import Data.Conduit.Process data BuildState = BuildState { bsBuildId :: String , bsBuildDate :: String , bsCfgParser :: ConfigParser , bsCfg :: B9Config , bsBuildDir :: FilePath , bsSelectedRemoteRepo :: Maybe RemoteRepo , bsRemoteRepos :: [RemoteRepo] , bsRepoCache :: RepoCache , bsProf :: [ProfilingEntry] , bsStartTime :: UTCTime , bsInheritStdIn :: Bool } data ProfilingEntry = IoActionDuration NominalDiffTime | LogEvent LogLevel String deriving (Eq, Show) run :: ConfigParser -> B9Config -> B9 a -> IO a run cfgParser cfg action = do buildId <- generateBuildId now <- getCurrentTime bracket (createBuildDir buildId) removeBuildDir (run' buildId now) where run' buildId now buildDir = do -- Check repositories repoCache <- initRepoCache (repositoryCache cfg) let remoteRepos = getConfiguredRemoteRepos cfgParser remoteRepos' <- mapM (initRemoteRepo repoCache) remoteRepos let ctx = BuildState buildId buildDate cfgParser cfg buildDir selectedRemoteRepo remoteRepos' repoCache [] now True buildDate = formatTime undefined "%F-%T" now selectedRemoteRepo = do sel <- repository cfg (lookupRemoteRepo remoteRepos sel <|> error (printf "selected remote repo '%s' not configured,\ \ valid remote repos are: '%s'" sel (show remoteRepos))) (r, ctxOut) <- runStateT (runB9 wrappedAction) ctx -- Write a profiling report when (isJust (profileFile cfg)) $ writeFile (fromJust (profileFile cfg)) (unlines $ show <$> (reverse $ bsProf ctxOut)) return r createBuildDir buildId = do if uniqueBuildDirs cfg then do let subDir = "BUILD-" ++ buildId buildDir <- resolveBuildDir subDir createDirectory buildDir canonicalizePath buildDir else do let subDir = "BUILD-" ++ buildId buildDir <- resolveBuildDir subDir createDirectoryIfMissing True buildDir canonicalizePath buildDir where resolveBuildDir f = do case buildDirRoot cfg of Nothing -> return f Just root' -> do createDirectoryIfMissing True root' root <- canonicalizePath root' return $ root f removeBuildDir buildDir = when (uniqueBuildDirs cfg && not (keepTempDirs cfg)) $ removeDirectoryRecursive buildDir generateBuildId = printf "%08X" <$> (randomIO :: IO Word32) -- Run the action build action wrappedAction = do startTime <- gets bsStartTime r <- action now <- liftIO getCurrentTime let duration = show (now `diffUTCTime` startTime) infoL (printf "DURATION: %s" duration) return r getBuildId :: B9 FilePath getBuildId = gets bsBuildId getBuildDate :: B9 String getBuildDate = gets bsBuildDate getBuildDir :: B9 FilePath getBuildDir = gets bsBuildDir getConfigParser :: B9 ConfigParser getConfigParser = gets bsCfgParser getConfig :: B9 B9Config getConfig = gets bsCfg getExecEnvType :: B9 ExecEnvType getExecEnvType = gets (execEnvType . bsCfg) getSelectedRemoteRepo :: B9 (Maybe RemoteRepo) getSelectedRemoteRepo = gets bsSelectedRemoteRepo getRemoteRepos :: B9 [RemoteRepo] getRemoteRepos = gets bsRemoteRepos getRepoCache :: B9 RepoCache getRepoCache = gets bsRepoCache cmd :: String -> B9 () cmd str = do inheritStdIn <- gets bsInheritStdIn if inheritStdIn then interactive str else nonInteractive str interactive :: String -> B9 () interactive str = void (cmdWithStdIn str :: B9 Inherited) nonInteractive :: String -> B9 () nonInteractive str = void (cmdWithStdIn str :: B9 ClosedStream) cmdWithStdIn :: (InputSource stdin) => String -> B9 stdin cmdWithStdIn cmdStr = do traceL $ "COMMAND: " ++ cmdStr (cpIn, cpOut, cpErr, cph) <- streamingProcess (shell cmdStr) cmdLogger <- getCmdLogger e <- liftIO $ runConcurrently $ Concurrently (cpOut $$ cmdLogger LogTrace) *> Concurrently (cpErr $$ cmdLogger LogInfo) *> Concurrently (waitForStreamingProcess cph) checkExitCode e return cpIn where getCmdLogger = do lv <- gets $ verbosity . bsCfg lf <- gets $ logFile . bsCfg return $ \ level -> (CL.mapM_ (logImpl lv lf level . B.unpack)) checkExitCode ExitSuccess = traceL $ "COMMAND SUCCESS" checkExitCode ec@(ExitFailure e) = do errorL $ printf "COMMAND '%s' FAILED: %i!" cmdStr e liftIO $ exitWith ec traceL :: String -> B9 () traceL = b9Log LogTrace dbgL :: String -> B9 () dbgL = b9Log LogDebug infoL :: String -> B9 () infoL = b9Log LogInfo errorL :: String -> B9 () errorL = b9Log LogError b9Log :: LogLevel -> String -> B9 () b9Log level msg = do lv <- gets $ verbosity . bsCfg lf <- gets $ logFile . bsCfg modify $ \ ctx -> ctx { bsProf = LogEvent level msg : bsProf ctx } B9 $ liftIO $ logImpl lv lf level msg logImpl :: Maybe LogLevel -> Maybe FilePath -> LogLevel -> String -> IO () logImpl minLevel mf level msg = do lm <- formatLogMsg level msg when (isJust minLevel && level >= fromJust minLevel) (putStr lm) when (isJust mf) (appendFile (fromJust mf) lm) formatLogMsg :: LogLevel -> String -> IO String formatLogMsg l msg = do utct <- getCurrentTime let time = formatTime defaultTimeLocale "%H:%M:%S" utct return $ unlines $ printf "[%s] %s - %s" (printLevel l) time <$> lines msg printLevel :: LogLevel -> String printLevel l = case l of LogNothing -> "NOTHING" LogError -> " ERROR " LogInfo -> " INFO " LogDebug -> " DEBUG " LogTrace -> " TRACE " newtype B9 a = B9 { runB9 :: StateT BuildState IO a } deriving ( Functor , Applicative , Monad , MonadState BuildState ) instance MonadIO B9 where liftIO m = do start <- B9 $ liftIO getCurrentTime res <- B9 $ liftIO m stop <- B9 $ liftIO getCurrentTime let durMS = IoActionDuration (stop `diffUTCTime` start) modify $ \ ctx -> ctx { bsProf = durMS : bsProf ctx } return res