sunlight-0.2.0.0: lib/Test/Sunlight/Shell.hs
{-# LANGUAGE OverloadedStrings #-}
module Test.Sunlight.Shell where
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
import Control.Exception
import Data.Monoid
import Foreign.Safe
import System.Directory
import System.Exit
import System.IO
import System.Process
import Control.Concurrent
import Data.Time
-- | Runs an IO action within the given directory, and then returns
-- to the original directory.
inDirectory
:: FilePath
-> IO a
-> IO a
inDirectory inside a = bracket get rel run
where
get = do
d <- getCurrentDirectory
putStrLn $ "current directory is: " ++ d
putStrLn $ "changing to directory: " ++ inside
setCurrentDirectory inside
return d
rel d = do
putStrLn $ "returning to directory: " ++ d
setCurrentDirectory d
run = const a
-- | Creates a process. Its stdin is closed. Returns handles to
-- stdout and stderr, and the ProcessHandle.
makeProcess
:: FilePath
-- ^ Run this command
-> [String]
-- ^ Arguments
-> IO (Handle, Handle, ProcessHandle)
makeProcess c as = do
d <- getCurrentDirectory
putStrLn $ "current directory is: " ++ d
putStrLn $ "running: " ++ c ++ " " ++ show as
(Just hin, Just out, Just err, h) <- createProcess cp
hClose hin
return (out, err, h)
where
cp = CreateProcess { cmdspec = RawCommand c as
, cwd = Nothing
, env = Nothing
, std_in = CreatePipe
, std_out = CreatePipe
, std_err = CreatePipe
, close_fds = True
, create_group = False }
bufsize :: Int
bufsize = 512
data CmdResult = CmdResult
{ crCode :: ExitCode
, crStdOut :: BS.ByteString
, crStdErr :: BS.ByteString
, crCommand :: String
, crArgs :: [String]
} deriving Show
class ShowBS a where
showBS :: a -> BS.ByteString
instance ShowBS CmdResult where
showBS r =
BS8.pack (crCommand r) <> " " <> (BS8.pack . show . crArgs $ r)
<> " "
<> case crCode r of
ExitSuccess -> "succeeded with exit code 0"
ExitFailure e -> "FAILED with exit code " <> BS8.pack (show e)
<> "\n"
<> "Standard output:\n"
<> shower (crStdOut r)
<> "\nStandard error:\n"
<> shower (crStdErr r)
<> "\n"
where
shower bs
| BS8.null bs = "<empty>\n"
| otherwise = bs
instance ShowBS UTCTime where
showBS = BS8.pack . show
resultOk :: CmdResult -> Bool
resultOk c = case crCode c of
ExitSuccess -> True
_ -> False
class CheckOk a where
isOk :: a -> Bool
instance CheckOk CmdResult where
isOk c = case crCode c of
ExitSuccess -> True
_ -> False
-- | Runs a command, sending standard output and standard error to
-- the terminal, while also returning the standard output.
tee
:: FilePath
-- ^ Run this command
-> [String]
-- ^ Arguments
-> IO CmdResult
tee cmd as = bracket (makeProcess cmd as) rel go
where
rel (out, err, _) = hClose out >> hClose err
go (out, err, hdl) = do
mvOut <- newEmptyMVar
mvErr <- newEmptyMVar
_ <- forkIO (readToEOF out stdout >>= putMVar mvOut)
_ <- forkIO (readToEOF err stderr >>= putMVar mvErr)
bsOut <- takeMVar mvOut
bsErr <- takeMVar mvErr
cd <- waitForProcess hdl
return $ CmdResult cd bsOut bsErr cmd as
-- | Reads from the given handle until EOF. Sends results to the
-- given handle and also stores them in the returned ByteString.
readToEOF
:: Handle
-- ^ Read from this handle
-> Handle
-- ^ Send results to this handle
-> IO BS.ByteString
readToEOF inp out = allocaBytes bufsize $ \ptr ->
let go bs = do
n <- hGetBufSome inp ptr bufsize
case n of
0 -> return bs
_ -> do
hPutBuf out ptr n
bs' <- BS.packCStringLen (ptr, n)
go (bs `BS.append` bs')
in go BS.empty