dhrun-1.0.1: src/Dhrun/Run.hs
{-# language FlexibleContexts #-}
{-# language TypeOperators #-}
{-|
Module : Dhrun.Run
Description : runner
Copyright : (c) Valentin Reis, 2018
License : MIT
Maintainer : fre@freux.fr
-}
module Dhrun.Run
( runDhrun
)
where
import Dhrun.Types.Cfg
import Dhrun.Pure
import Dhrun.Conduit
import Protolude
import System.Exit
( ExitCode(..) )
import Data.Conduit
( ConduitT )
import qualified System.Posix.Signals as SPS
( installHandler
, keyboardSignal
, Handler(..)
)
import qualified Data.Conduit.Process.Typed as PT
import qualified Data.Conduit.Binary as CB
import qualified Data.Text as T
( isInfixOf )
import qualified System.IO as SIO
( BufferMode(NoBuffering)
, hSetBuffering
, withBinaryFile
, IOMode(WriteMode)
, stdout
)
import Control.Monad.IO.Unlift
( MonadIO(..)
, withRunInIO
)
import Control.Monad.Writer
( MonadWriter
, tell
, runWriterT
)
import qualified System.Directory as SD
( createDirectoryIfMissing )
import qualified System.Environment as SE
( getEnvironment )
import qualified System.Timeout as ST
import System.Console.ANSI
putC :: MonadIO m => Text -> Color -> m ()
putC content color = setC color *> putT content *> setC White
where
setC c = liftIO $ setSGR [SetColor Foreground Dull c]
putT :: MonadIO m => Text -> m ()
putT = putStr
-- | runDhrun d runs a dhrun specification in the lifted IO monad.
runDhrun :: (MonadIO m) => Cfg -> m ()
runDhrun dhallExec =
runWriterT (runReaderT runAllSteps dhallExec) <&> snd >>= liftIO . \case
[] -> putC "Success. " Green
<> putText "No errors were encountered and all requirements were met."
errors ->
putText "Error log:"
<> for_ errors Protolude.putText
<> putC "Failure. " Red
<> die "exiting."
runAllSteps :: (MonadIO m, MonadReader Cfg m, MonadWriter [Text] m) => m ()
runAllSteps = do
liftIO $ SIO.hSetBuffering SIO.stdout SIO.NoBuffering
runWorkDir -- | setting up the working directory
runPre -- | preprocessing steps
runAsyncs -- | running the main async step
runChecks -- | running file checks
runPost -- | postprocessing steps
where
runPre = runMultipleV ((toS <$>) <$> pre) "pre-processing"
runPost = runMultipleV ((toS <$>) <$> post) "post-processing"
putV :: (MonadIO m, MonadReader Cfg m) => Text -> m ()
putV text =
(== Verbose) . verbosity <$> ask >>= flip when (liftIO $ putText text)
runWorkDir :: (MonadIO m, MonadReader Cfg m) => m ()
runWorkDir = do
(shouldRemove, wd) <- ask <&> \a -> (Remove == cleaning a, toS $ workdir a)
when shouldRemove
$ PT.runProcess_
$ PT.shell
$ toS
$ ("rm -rf " :: Text)
<> toS wd
liftIO $ SD.createDirectoryIfMissing False wd
runChecks :: (MonadIO m, MonadReader Cfg m, MonadWriter [Text] m) => m ()
runChecks = (cmds <$> ask) >>= \case
[] -> pu "no processes"
l -> do
pu "start"
for_ l $ \Cmd {..} -> do
pu $ "running post-mortem checks for " <> toS name <> " " <> mconcat
(intersperse " " (toS <$> args))
for_ postchecks $ \FileCheck {..} -> do
contents <- liftIO $ readFile (toS filename)
forM_ (wants filecheck) $ \(Pattern x) -> unless
(T.isInfixOf x contents)
(tell
[ "post-mortem check fail: "
<> x
<> " not found in file "
<> toS filename
]
)
forM_ (avoids filecheck) $ \(Pattern x) -> when
(T.isInfixOf x contents)
(tell
[ "post-mortem check fail: "
<> x
<> " found in file "
<> toS filename
]
)
pu " done"
where pu t = putV $ "check step:" <> t
runAsyncs :: (MonadIO m, MonadReader Cfg m, MonadWriter [Text] m) => m ()
runAsyncs = (cmds <$> ask) >>= \case
[] -> pu "async step: no processes."
l -> do
pu "async step: start"
wd <- workdir <$> ask
externEnv <- liftIO SE.getEnvironment <&> (mapTuple toS <$>)
asyncs <- liftIO $ mkAsyncs l externEnv wd
_ <- kbInstallHandler $ for_ asyncs cancel
pu "async step: started processes"
liftIO (waitAnyCancel asyncs)
<&> snd
<&> concludeCmd (any noWants l)
>>= either tell putText
pu "async step: done"
where
pu t = putV $ "async step:" <> t
mkAsyncs :: [Cmd] -> [(Text, Text)] -> WorkDir -> IO [Async CmdResult]
mkAsyncs l externEnv wd = for l (async . runCmd externEnv wd)
kbInstallHandler :: (MonadIO m) => IO () -> m SPS.Handler
kbInstallHandler h =
liftIO $ SPS.installHandler SPS.keyboardSignal (SPS.Catch h) Nothing
runMultipleV
:: (MonadIO m, MonadReader Cfg m) => (Cfg -> [Text]) -> Text -> m ()
runMultipleV getter desc = getter <$> ask >>= \case
[] -> pu $ "no " <> desc <> " commands to run."
l -> do
pu $ desc <> ": start"
wdirFP <- ask <&> workdir <&> \(WorkDir wdt) -> wdt
for_ l $ runOne wdirFP
pu $ desc <> ": done"
where
pu t = putV $ desc <> " step:" <> t
runOne wdirFP x =
liftIO (PT.runProcess $ PT.setWorkingDir (toS wdirFP) (PT.shell $ toS x))
>>= \case
ExitSuccess -> pu ("ran one " <> desc <> " command.")
ExitFailure _ ->
liftIO
$ die
$ "failed to execute one "
<> desc
<> " command."
<> x
-- | Runs a single command. should be the only 'complex' function in this codebaseaside from runAsyncs. TODO: add the exitcode check functionality.
runCmd :: [(Text, Text)] -> WorkDir -> Cmd -> IO CmdResult
runCmd fullExternEnv (WorkDir wd) c@Cmd {..} = do
for_ otherwd $ \(WorkDir swd) -> SD.createDirectoryIfMissing True (toS swd)
fromMaybe (Timeout c) <$> maybeTimeout go timeout
where
realwd = maybe (toS wd) toS otherwd
-- | output files
outFile :: FilePath
outFile = getWdFilename realwd out
errFile :: FilePath
errFile = getWdFilename realwd err
-- | process configuration
pc :: PC
pc =
PT.proc (toS name) (toS <$> args)
& PT.setWorkingDir (toS realwd)
& PT.setEnv (mapTuple toS <$> envVars vars passvars fullExternEnv)
& PT.setStdout PT.createSource
& PT.setStderr PT.createSource
& PT.setStdin PT.closed
-- | lambda-scoped resource acquisition: stdin/out conduits and starting the process
go :: IO CmdResult
go = with3 (withSinkFileNoBuffering outFile)
(withSinkFileNoBuffering errFile)
(withWrappedSafeP c pc)
logic
-- | the main logic, that uses two sinks and the started process to
-- do the streaming monitoring.
logic :: Sink -> Sink -> P -> IO CmdResult
logic sout serr p =
finalizeCmd c
<$> with2 (withAsync $ monitor (filecheck out) (PT.getStdout p) sout)
(withAsync $ monitor (filecheck err) (PT.getStderr p) serr)
waitEitherCatchCancel
<*> (PT.getExitCode p >>= \case
Nothing -> Killed <$ PT.stopProcess p
Just ec -> return $ Died ec
)
-- | May timeout an IO command.
maybeTimeout :: IO a -> Maybe Int -> IO (Maybe a)
maybeTimeout io = maybe (Just <$> io) (\t -> ST.timeout (1000000 * t) io)
-- | runs an IO action with a preconfigured process (PC) in lambda scope (P)
withWrappedSafeP :: Cmd -> PC -> (P -> IO CmdResult) -> IO CmdResult
withWrappedSafeP c pc f = catchIOE $ PT.withProcess pc f
where
catchIOE :: IO CmdResult -> IO CmdResult
catchIOE ioOp =
catch ioOp $ \(e :: IOException) -> return $ ThrewException c (show e)
-- | runs an IO action with a file sink in lambda scope
withSinkFileNoBuffering
:: FilePath -> (ConduitT ByteString o IO () -> IO b) -> IO b
withSinkFileNoBuffering filepath lambdaIO =
withRunInIO $ \run -> SIO.withBinaryFile filepath SIO.WriteMode $ \h -> do
SIO.hSetBuffering h SIO.NoBuffering
run $ lambdaIO $ CB.sinkHandle h