packages feed

hwm-0.4.0: src/HWM/Runtime/Process.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE NoImplicitPrelude #-}

module HWM.Runtime.Process
  ( inheritRun,
    exec,
    Exec (..),
    ExecOptions (..),
    EnvVars,
    execInBackground,
    mkExec,
  )
where

import Control.Concurrent.Async
import Control.Monad.Error.Class (MonadError (..))
import qualified Data.Text as T
import HWM.Core.Common (Name)
import HWM.Core.Formatting (Status (..))
import HWM.Core.Result (Issue (..), IssueDetails (..), Severity (..))
import HWM.Runtime.Files (prepareDir)
import HWM.Runtime.Logging (genLogId, logCommandEnd, logCommandStart, logPath, logRoot)
import HWM.Runtime.UI
import Relude
import System.Environment (getEnvironment)
import qualified System.IO as TIO
import System.Process (readProcessWithExitCode)
import System.Process.Typed
  ( ExitCode (..),
    Process,
    proc,
    runProcess_,
    setEnv,
    setStderr,
    setStdout,
    shell,
    useHandleOpen,
    waitExitCode,
    withProcessWait,
  )

mkExec :: (Applicative m) => Text -> [Text] -> m (Exec m)
mkExec name args = pure $ Exec name args [] Nothing

exec :: (MonadIO m) => Text -> [Text] -> m (Bool, String)
exec name args = do
  (code, _, out) <- liftIO (readProcessWithExitCode (toString name) (map toString args) "")
  case code of
    ExitSuccess {} -> pure (True, out)
    ExitFailure {} -> pure (False, out)

data Exec m = Exec
  { execCmd :: Text,
    execArgs :: [Text],
    execEnv :: [(String, String)],
    postCommand :: Maybe (m ())
  }

type EnvVars = [(String, String)]

data ExecOptions = ExecOptions
  { envName :: Name,
    formatFX :: Text -> Text -> Text,
    fxEnabled :: Bool
  }

processHandle :: IO a -> Bool -> Handle -> Process stdin stdout stderr -> IO ExitCode
processHandle f True logHandle p = do
  spinner <- async f
  status <- waitExitCode p
  cancel spinner
  logCommandEnd logHandle status
  pure status
processHandle _ False logHandle p = do
  status <- waitExitCode p
  logCommandEnd logHandle status
  pure status

execInBackground :: (MonadUI m, MonadError Issue m, MonadIO m) => Exec m -> ExecOptions -> m ()
execInBackground Exec {..} ExecOptions {..} = do
  logId <- genLogId envName
  let fx = formatFX (toText $ logPath logId)
  let processLogPath = logPath logId
  prepareDir logRoot
  let cmd = execCmd <> " " <> T.unwords execArgs
  currentEnv <- liftIO getEnvironment
  let targetEnv = execEnv <> currentEnv
  status <- liftIO $ TIO.withFile processLogPath TIO.WriteMode $ \logHandle -> do
    unless fxEnabled (uiIndicator fx fxEnabled Nothing)
    logCommandStart logHandle cmd
    let config = setEnv targetEnv $ setStdout (useHandleOpen logHandle) $ setStderr (useHandleOpen logHandle) $ shell (toString cmd)
    withProcessWait config $ processHandle (uiIndicator fx fxEnabled Nothing) fxEnabled logHandle
  case status of
    ExitSuccess -> do
      liftIO $ uiIndicator fx fxEnabled (Just Checked)
      sequence_ postCommand
    _ -> do
      liftIO $ uiIndicator fx fxEnabled (Just Invalid)
      throwError
        Issue
          { issueTopic = logId,
            issueMessage = "Command failed",
            issueSeverity = SeverityError,
            issueDetails = Just CommandIssue {issueCommand = cmd, issueLogFile = processLogPath}
          }

inheritRun :: (MonadIO m, MonadUI m) => Exec m -> m ()
inheritRun Exec {..} = do
  currentEnv <- liftIO getEnvironment
  let targetEnv = execEnv <> currentEnv
  let processConfig = setEnv targetEnv $ proc "/bin/sh" (["-c", toString execCmd] <> map toString execArgs)
  liftIO (runProcess_ processConfig)
  sequence_ postCommand