gdritter repos bb8 / master src / BB8 / Jobs.hs
master

Tree @master (Download .tar.gz)

Jobs.hs @masterraw · history · blame

module BB8.Jobs where

import           Control.Concurrent (ThreadId, forkIO, killThread)
import qualified Control.Concurrent.STM as STM
import           Control.Exception (SomeException, catch)
import           Control.Monad (forever)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as BS
import qualified Data.ByteString.Lazy as BSL
import qualified Data.Foldable as F
import           Data.Monoid ((<>))
import qualified Data.Sequence as Seq
import qualified Data.Time.Clock as Time
import qualified GHC.IO.Handle as Sys
import qualified System.Exit as Sys
import qualified System.Process as Sys

import BB8.Types
import BB8.Common

type Trace = STM.TVar (Seq.Seq OutputLine)
type Lock = STM.TMVar ()

buildTraceOutput :: Seq.Seq OutputLine -> BSL.ByteString
buildTraceOutput = BS.toLazyByteString . F.foldMap go
  where go (OutputLine time src text) =
          let srcBS = case src of
                        SourceStdout -> "\x1estdout\x1e"
                        SourceStderr -> "\x1estderr\x1e"
          in BS.stringUtf8 (show time) <> srcBS <> BS.byteString text <> "\n"


-- | The result of a job is a trace of (timestamped) output lines as
-- well as the resulting exit code, which is 0 on success and nonzero
-- on failure
data JobResult = JobResult
  { resultTrace :: Seq.Seq OutputLine
  , resultCode  :: Int
  } deriving (Eq, Show, Read)


-- | Create a new thread which specifies the given source, reads from
-- the provided handle, and adds an explanatory line to the end of the
-- sequence contained in the provided TVar. On any error, it will
-- clean itself up and exit.
forkOutputThread ::
  Lock ->
  LineSource ->
  Sys.Handle ->
  Trace ->
  IO ThreadId
forkOutputThread lock src handle var = do
  () <- STM.atomically $ STM.takeTMVar lock
  let handler (exn :: SomeException) = do
        debugShow (src, exn)
        STM.atomically $ STM.putTMVar lock ()
  forkIO $ flip catch handler $ forever $ do
    line <- BS.hGetLine handle
    now <- Time.getCurrentTime
    STM.atomically $ do
      STM.modifyTVar var (Seq.|> OutputLine now src line)


-- | Run a command with an optional working path, returning the
-- resulting information about the command once it's done.
runTraceCommand :: String -> [String] -> Maybe FilePath -> IO JobResult
runTraceCommand cmd args cwd = do
  var <- STM.atomically (STM.newTVar Seq.empty)
  traceCommand cmd args cwd var


-- | Run a command with an optional working path, storing the
-- intermediate output in the provided 'TMVar', and returning the
-- resulting information about the command once it's done.
traceCommand ::
  String ->
  [String] ->
  Maybe FilePath ->
  Trace ->
  IO JobResult
traceCommand cmd args cwd var = do
  let dockerProcess = (Sys.proc cmd args)
        { Sys.std_out = Sys.CreatePipe
        , Sys.std_err = Sys.CreatePipe
        , Sys.cwd     = cwd
        }
  (_, Just stdout, Just stderr, handle) <- Sys.createProcess dockerProcess

  STM.atomically $ STM.writeTVar var mempty

  stdoutLock <- STM.atomically $ STM.newTMVar ()
  _ <- forkOutputThread stdoutLock SourceStdout stdout var
  stderrLock <- STM.atomically $ STM.newTMVar ()
  _ <- forkOutputThread stderrLock SourceStderr stderr var

  exitCode <- Sys.waitForProcess handle

  () <- STM.atomically $ STM.takeTMVar stdoutLock
  () <- STM.atomically $ STM.takeTMVar stderrLock

  trace <- STM.atomically $ STM.readTVar var

  return JobResult
    { resultTrace = trace
    , resultCode  = case exitCode of
        Sys.ExitSuccess   -> 0
        Sys.ExitFailure n -> n
    }