{-| @benchmark@ contains the logic to be executed on slave nodes.
-}


module FeedGipeda.Slave
  ( benchmark
  ) where


import           Control.Applicative
import           Control.Concurrent.Async (Concurrently (..))
import           Control.Exception        (bracket)
import           Control.Logging          as Logging
import           Data.Conduit             (($$))
import qualified Data.Conduit.List        as CL
import           Data.Conduit.Process     (ClosedStream (..),
                                           CreateProcess (..),
                                           interruptProcessGroupOf, proc,
                                           readCreateProcessWithExitCode, shell,
                                           showCommandForUser, streamingProcess,
                                           streamingProcessHandleRaw,
                                           waitForStreamingProcess)
import           Data.Monoid
import qualified Data.Text                as Text
import qualified Data.Text.Encoding       as Text
import           FeedGipeda.GitShell      (SHA)
import           FeedGipeda.Prelude
import           FeedGipeda.Repo          (Repo)
import qualified FeedGipeda.Repo          as Repo
import           FeedGipeda.Types         (Timeout)
import           System.Exit              (ExitCode (..))
import           System.IO.Temp           (withSystemTempDirectory)
import qualified System.Timeout


-- We have to roll our own, because the provided functions use @terminateProcess@
-- instead of @interruptProcessGroupOf@, so that in case of shelling out we only
-- kill the shell process but not its children.
readCreateProcessGroupWithExitCode :: CreateProcess -> IO (ExitCode, String, String)
readCreateProcessGroupWithExitCode cp =
  bracket
    (streamingProcess cp { create_group = True })
    cleanup
    captureStreams
  where
    cleanup (_, _, _, sph) =
      interruptProcessGroupOf (streamingProcessHandleRaw sph)

    captureStreams (ClosedStream, out, err, sph) =
      runConcurrently $ (,,)
        <$> Concurrently (waitForStreamingProcess sph)
        <*> Concurrently (out $$ stringSink)
        <*> Concurrently (err $$ stringSink)

    stringSink =
      Text.unpack . Text.decodeUtf8 <$> CL.fold (<>) mempty


procReportingError :: Repo -> SHA -> Maybe FilePath -> FilePath -> [String] -> IO String
procReportingError repo commit cwd cmd args = do
  (exitCode, stdout, stderr) <-
    readCreateProcessWithExitCode (proc cmd args) { cwd = cwd } ""
  reportError repo commit (showCommandForUser cmd args) exitCode stderr
  return stdout


shellReportingError :: Repo -> SHA -> Maybe FilePath -> FilePath -> IO String
shellReportingError repo commit cwd cmd = do
  (exitCode, stdout, stderr) <-
    readCreateProcessGroupWithExitCode (shell cmd) { cwd = cwd }
  reportError repo commit cmd exitCode stderr
  return stdout


reportError :: Repo -> SHA -> String -> ExitCode -> String -> IO ()
reportError repo commit cmd code stderr =
  case code of
    ExitSuccess -> return ()
    ExitFailure c ->
      logWarn . unlines $
        [ "Benchmark script error"
        , "At commit " ++ Repo.uri repo ++ "@" ++ commit ++ ":"
        , cmd ++ ": exit code " ++ show c
        , stderr
        ]


cloneRecursiveAndCheckout :: Repo -> SHA -> FilePath -> IO ()
cloneRecursiveAndCheckout repo commit cloneDir = do
  procReportingError repo commit Nothing "git" ["clone", "--quiet", Repo.uri repo, cloneDir]
  procReportingError repo commit (Just cloneDir) "git" ["reset", "--hard", commit]
  shellReportingError repo commit (Just cloneDir) "git submodule update --init --recursive --quiet"
  return ()


{-| Clones the given @repo@ at a specific @commit@ into a temporary directory.
    Then calls the @benchmarkScript@ within that directory and returns its output.

    Will be executed on slave nodes.
-}
benchmark :: String -> Repo -> SHA -> Timeout -> IO String
benchmark benchmarkScript repo commit timeout = do
  clone <- Repo.cloneDir repo
  logInfo ("Benchmarking " ++ Repo.uri repo ++ "@" ++ commit)
  withSystemTempDirectory "feed-gipeda" $ \cloneDir -> do
    cloneRecursiveAndCheckout repo commit cloneDir
    res <- System.Timeout.timeout (ceiling (timeout * 10^6)) $
      shellReportingError repo commit (Just cloneDir) benchmarkScript
    case res of
      Just res -> do
        logInfo "Finished. Output:"
        mapM_ logInfo (lines res)
        return res
      Nothing -> do
        logWarn . unlines $
          [ "Benchmark script timed out (--timeout is " ++ show timeout ++ ")"
          , "At commit " ++ Repo.uri repo ++ "@" ++ commit
          ]
        return "build/timeout;1.0"