sandwich-0.3.0.0: src/Test/Sandwich/Logging.hs
{-# LANGUAGE CPP #-}
-- | Logging functions.
module Test.Sandwich.Logging (
debug
, info
, warn
, Test.Sandwich.Logging.logError
, Test.Sandwich.Logging.logOther
-- * Process functions with logging
, createProcessWithLogging
, readCreateProcessWithLogging
, createProcessWithLoggingAndStdin
, callCommandWithLogging
, createProcessWithLogging'
, readCreateProcessWithLogging'
, createProcessWithLoggingAndStdin'
, callCommandWithLogging'
) where
import Control.Concurrent
import Control.DeepSeq (rnf)
import qualified Control.Exception as C
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.IO.Unlift
import Control.Monad.Logger hiding (logOther)
import Data.String.Interpolate
import Data.Text (Text)
import Foreign.C.Error
import GHC.IO.Exception
import GHC.Stack
import System.IO
import System.IO.Error (mkIOError)
import System.Process
import UnliftIO.Async hiding (wait)
import UnliftIO.Exception
#if !MIN_VERSION_base(4,13,0)
import Control.Monad.Fail
#endif
-- * Basic logging functions
-- | Log a message at level 'LevelDebug'.
debug :: (HasCallStack, MonadLogger m) => Text -> m ()
debug = logDebugCS callStack
-- | Log a message at level 'LevelInfo'.
info :: (HasCallStack, MonadLogger m) => Text -> m ()
info = logInfoCS callStack
-- | Log a message at level 'LevelWarn'.
warn :: (HasCallStack, MonadLogger m) => Text -> m ()
warn = logWarnCS callStack
-- | Log a message at level 'LevelError'.
logError :: (HasCallStack, MonadLogger m) => Text -> m ()
logError = logErrorCS callStack
-- | Log with a custom 'LogLevel'.
logOther :: (HasCallStack, MonadLogger m) => LogLevel -> Text -> m ()
logOther = logOtherCS callStack
-- * System.Process helpers
--
-- | Functions for launching processes while capturing their output in the logs.
-- | Spawn a process with its stdout and stderr connected to the logging system.
-- Every line output by the process will be fed to a 'debug' call.
createProcessWithLogging :: (HasCallStack, MonadUnliftIO m, MonadLogger m) => CreateProcess -> m ProcessHandle
createProcessWithLogging = withFrozenCallStack (createProcessWithLogging' LevelDebug)
-- | Spawn a process with its stdout and stderr connected to the logging system.
createProcessWithLogging' :: (HasCallStack, MonadUnliftIO m, MonadLogger m) => LogLevel -> CreateProcess -> m ProcessHandle
createProcessWithLogging' logLevel cp = do
(hRead, hWrite) <- liftIO createPipe
let name = case cmdspec cp of
ShellCommand {} -> "shell"
RawCommand path _ -> path
_ <- async $ forever $ do
line <- liftIO $ hGetLine hRead
logOtherCS callStack logLevel [i|#{name}: #{line}|]
(_, _, _, p) <- liftIO $ createProcess (cp { std_out = UseHandle hWrite, std_err = UseHandle hWrite })
return p
-- | Like 'readCreateProcess', but capture the stderr output in the logs.
-- Every line output by the process will be fed to a 'debug' call.
readCreateProcessWithLogging :: (HasCallStack, MonadUnliftIO m, MonadLogger m) => CreateProcess -> String -> m String
readCreateProcessWithLogging = withFrozenCallStack (readCreateProcessWithLogging' LevelDebug)
-- | Like 'readCreateProcess', but capture the stderr output in the logs.
readCreateProcessWithLogging' :: (HasCallStack, MonadUnliftIO m, MonadLogger m) => LogLevel -> CreateProcess -> String -> m String
readCreateProcessWithLogging' logLevel cp input = do
(hReadErr, hWriteErr) <- liftIO createPipe
let name = case cmdspec cp of
ShellCommand {} -> "shell"
RawCommand path _ -> path
_ <- async $ forever $ do
line <- liftIO $ hGetLine hReadErr
logOtherCS callStack logLevel [i|#{name}: #{line}|]
-- Do this just like 'readCreateProcess'
-- https://hackage.haskell.org/package/process-1.6.17.0/docs/src/System.Process.html#readCreateProcess
(ex, output) <- liftIO $ withCreateProcess (cp { std_in = CreatePipe, std_out = CreatePipe, std_err = UseHandle hWriteErr }) $ \sin' sout _ p -> do
case (sin', sout) of
(Just hIn, Just hOut) -> do
output <- hGetContents hOut
withForkWait (C.evaluate $ rnf output) $ \waitOut -> do
-- now write any input
unless (Prelude.null input) $
ignoreSigPipe $ hPutStr hIn input
-- hClose performs implicit hFlush, and thus may trigger a SIGPIPE
ignoreSigPipe $ hClose hIn
-- wait on the output
waitOut
hClose hOut
-- wait on the process
ex <- waitForProcess p
return (ex, output)
(Nothing, _) -> liftIO $ throwIO $ userError "readCreateProcessWithStderrLogging: Failed to get a stdin handle."
(_, Nothing) -> liftIO $ throwIO $ userError "readCreateProcessWithStderrLogging: Failed to get a stdout handle."
case ex of
ExitSuccess -> return output
ExitFailure r -> liftIO $ processFailedException "readCreateProcessWithLogging" cmd args r
where
cmd = case cp of
CreateProcess { cmdspec = ShellCommand sc } -> sc
CreateProcess { cmdspec = RawCommand fp _ } -> fp
args = case cp of
CreateProcess { cmdspec = ShellCommand _ } -> []
CreateProcess { cmdspec = RawCommand _ args' } -> args'
-- | Spawn a process with its stdout and stderr connected to the logging system.
-- Every line output by the process will be fed to a 'debug' call.
createProcessWithLoggingAndStdin :: (HasCallStack, MonadUnliftIO m, MonadFail m, MonadLogger m) => CreateProcess -> String -> m ProcessHandle
createProcessWithLoggingAndStdin = withFrozenCallStack (createProcessWithLoggingAndStdin' LevelDebug)
-- | Spawn a process with its stdout and stderr connected to the logging system.
createProcessWithLoggingAndStdin' :: (HasCallStack, MonadUnliftIO m, MonadFail m, MonadLogger m) => LogLevel -> CreateProcess -> String -> m ProcessHandle
createProcessWithLoggingAndStdin' logLevel cp input = do
(hRead, hWrite) <- liftIO createPipe
let name = case cmdspec cp of
ShellCommand {} -> "shell"
RawCommand path _ -> path
_ <- async $ forever $ do
line <- liftIO $ hGetLine hRead
logOtherCS callStack logLevel [i|#{name}: #{line}|]
(Just inh, _, _, p) <- liftIO $ createProcess (
cp { std_out = UseHandle hWrite
, std_err = UseHandle hWrite
, std_in = CreatePipe }
)
unless (Prelude.null input) $
liftIO $ ignoreSigPipe $ hPutStr inh input
-- hClose performs implicit hFlush, and thus may trigger a SIGPIPE
liftIO $ ignoreSigPipe $ hClose inh
return p
-- | Higher level version of 'createProcessWithLogging', accepting a shell command.
callCommandWithLogging :: (HasCallStack, MonadUnliftIO m, MonadLogger m) => String -> m ()
callCommandWithLogging = withFrozenCallStack (callCommandWithLogging' LevelDebug)
-- | Higher level version of 'createProcessWithLogging'', accepting a shell command.
callCommandWithLogging' :: (HasCallStack, MonadUnliftIO m, MonadLogger m) => LogLevel -> String -> m ()
callCommandWithLogging' logLevel cmd = do
(hRead, hWrite) <- liftIO createPipe
(_, _, _, p) <- liftIO $ createProcess (shell cmd) {
delegate_ctlc = True
, std_out = UseHandle hWrite
, std_err = UseHandle hWrite
}
_ <- async $ forever $ do
line <- liftIO $ hGetLine hRead
logOtherCS callStack logLevel [i|#{cmd}: #{line}|]
liftIO (waitForProcess p) >>= \case
ExitSuccess -> return ()
ExitFailure r -> liftIO $ throwIO $ userError [i|callCommandWithLogging failed for '#{cmd}': '#{r}'|]
-- * Util
-- Copied from System.Process
withForkWait :: IO () -> (IO () -> IO a) -> IO a
withForkWait asy body = do
waitVar <- newEmptyMVar :: IO (MVar (Either SomeException ()))
mask $ \restore -> do
tid <- forkIO $ try (restore asy) >>= putMVar waitVar
let wait = takeMVar waitVar >>= either throwIO return
restore (body wait) `C.onException` killThread tid
-- Copied from System.Process
ignoreSigPipe :: IO () -> IO ()
ignoreSigPipe = C.handle $ \case
IOError { ioe_type = ResourceVanished, ioe_errno = Just ioe } | Errno ioe == ePIPE -> return ()
e -> throwIO e
-- Copied from System.Process
processFailedException :: String -> String -> [String] -> Int -> IO a
processFailedException fun cmd args exit_code =
ioError (mkIOError OtherError (fun ++ ": " ++ cmd ++
Prelude.concatMap ((' ':) . show) args ++
" (exit " ++ show exit_code ++ ")")
Nothing Nothing)