gonito/Handler/Runner.hs

150 lines
4.6 KiB
Haskell

module Handler.Runner where
import Import
import System.Process
import System.Exit
import System.Environment
import Control.Concurrent.STM
import Control.Concurrent.Lifted (threadDelay)
import qualified Data.ByteString as BS
import Control.Monad.IO.Class
type Channel = TChan (Maybe Text)
data RunnerStatus a = RunnerOK a | RunnerError ExitCode
newtype Runner a = Runner { runRunner :: Channel -> Handler (RunnerStatus a) }
getChannel :: Runner Channel
getChannel = Runner {
runRunner = \chan -> return $ RunnerOK chan
}
instance Functor Runner where
fmap f runner = Runner {
runRunner = \chan -> do
s <- (runRunner runner) chan
return $ case s of
RunnerOK v -> RunnerOK $ f v
RunnerError e -> RunnerError e
}
instance Applicative Runner where
pure v = Runner {
runRunner = \_ -> return $ RunnerOK v
}
liftA2 f runner1 runner2 = Runner {
runRunner = \chan -> do
s1 <- (runRunner runner1) chan
case s1 of
RunnerOK v1 -> do
s2 <- (runRunner runner2) chan
case s2 of
RunnerOK v2 -> return $ RunnerOK $ f v1 v2
RunnerError e -> return $ RunnerError e
RunnerError e -> return $ RunnerError e
}
instance Monad Runner where
runner >>= k = Runner {
runRunner = \chan -> do
s <- (runRunner runner) chan
case s of
RunnerError e -> return $ RunnerError e
RunnerOK v -> do
sn <- (runRunner (k v)) chan
return $ case sn of
RunnerError e -> RunnerError e
RunnerOK w -> RunnerOK w
}
instance MonadIO Runner where
liftIO action = Runner {
runRunner = \_ -> do
r <- liftIO action
return $ RunnerOK r
}
runWithChannel :: Channel -> Runner () -> Handler ExitCode
runWithChannel chan runner = do
s <- (runRunner runner) chan
return $ case s of
RunnerOK () -> ExitSuccess
RunnerError e -> e
runProg :: Maybe FilePath -> FilePath -> [String] -> Runner ()
runProg workingDir programPath args = Runner {
runRunner = \chan -> do
(code, _) <- runProgram workingDir programPath args chan
case code of
ExitSuccess -> return $ RunnerOK ()
_ -> return $ RunnerError code
}
runProgram :: Maybe FilePath -> FilePath -> [String] -> Channel -> Handler (ExitCode, Text)
runProgram workingDir programPath args chan = do
env <- liftIO $ getEnvironment
(_, Just hout, Just herr, pid) <-
liftIO $ createProcess (proc programPath args){
std_out = CreatePipe,
std_err = CreatePipe,
-- https://serverfault.com/questions/544156/git-clone-fail-instead-of-prompting-for-credentials
env = Just (("GIT_TERMINAL_PROMPT", "0") : env),
cwd = workingDir}
(code, out) <- gatherOutput pid hout herr chan
_ <- liftIO $ waitForProcess pid
return (code, out)
gatherOutput :: ProcessHandle -> Handle -> Handle -> Channel -> Handler (ExitCode, Text)
gatherOutput ph hout herr chan = work mempty mempty
where
work accout accerr = do
-- Read any outstanding input.
resterr <- takeABit herr accerr
restout <- takeABit hout accout
liftIO $ threadDelay 1000000
-- Check on the process.
s <- liftIO $ getProcessExitCode ph
-- Exit or loop.
case s of
Nothing -> work restout resterr
Just ec -> do
-- Get any last bit written between the read and the status
-- check.
_ <- takeFinalBit herr resterr
allTheRest <- takeFinalBit hout restout
return (ec, allTheRest)
takeABit h acc = do
bs <- liftIO $ BS.hGetNonBlocking h (64 * 1024)
let acc' = acc <> (decodeUtf8 bs)
let (fullLines, rest) = processOutput acc'
mapM_ (msg chan) fullLines
return rest
takeFinalBit h rest = do
lastPart <- liftIO $ BS.hGetContents h
let allTheRest = rest <> (decodeUtf8 lastPart)
mapM_ (msg chan) $ lines allTheRest
return allTheRest
msg :: Channel -> Text -> Handler ()
msg chan m = liftIO $ atom $ writeTChan chan $ Just (m ++ "\n")
err :: Channel -> Text -> Handler ()
err = msg
raw :: Channel -> Text -> Handler ()
raw = msg
atom = Control.Concurrent.STM.atomically
processOutput :: Text -> ([Text], Text)
processOutput = processOutput' . lines
where processOutput' [] = ([], "")
processOutput' out = (init out, last out)
init [] = []
init [x] = []
init (x:xs) = (x:(init xs))
last [x] = x
last (_:xs) = last xs