packages feed

hydra-kernel-0.17.0: src/main/haskell/Hydra/Overlay/Haskell/Lib/System.hs

-- | Haskell implementations of hydra.lib.system primitives

module Hydra.Overlay.Haskell.Lib.System where

import qualified Control.Exception as E
import qualified Data.ByteString as BS
import qualified Data.Map as M
import qualified Data.Maybe as Y
import qualified Hydra.Error.System as SystemError
import qualified Hydra.File as File
import qualified Hydra.System as System
import qualified Hydra.Time as Time
import Data.Time.Clock.POSIX (getPOSIXTime)
import System.Exit (ExitCode(..), exitWith)
import qualified System.Directory as Dir
import qualified System.Environment as Env
import qualified System.IO.Error as IOE
import qualified System.Process as P


-- | Run a program to completion and capture its result.
execute :: System.Command -> IO (Either SystemError.SystemError System.ProcessResult)
execute command =
  let program = File.unFilePath (System.commandProgram command)
      args = System.commandArguments command
      cwd = fmap File.unFilePath (System.commandWorkingDirectory command)
      env = fmap toEnvList (System.commandEnvironment command)
      proc0 = (P.proc program args) {
                P.cwd = cwd,
                P.env = env,
                P.std_out = P.CreatePipe,
                P.std_err = P.CreatePipe}
  in E.catch (runProcess proc0) (pure . Left . classify (System.commandProgram command))

-- | Terminate the current process with a status code.
exit :: System.StatusCode -> IO ()
exit code =
  let n = System.unStatusCode code
  in exitWith (if n == 0 then ExitSuccess else ExitFailure (fromIntegral n))

-- | Get the full set of environment variables.
getEnvironment :: IO (M.Map System.EnvironmentVariable String)
getEnvironment = do
  pairs <- Env.getEnvironment
  pure $ M.fromList [(System.EnvironmentVariable k, v) | (k, v) <- pairs]

-- | Look up a single environment variable by name.
getEnvironmentVariable :: System.EnvironmentVariable -> IO (Maybe String)
getEnvironmentVariable name =
  Env.lookupEnv (System.unEnvironmentVariable name)

-- | Get the current wall-clock time.
getTime :: IO Time.Timespec
getTime = do
  t <- getPOSIXTime
  let picos = floor (toRational t * 1000000000000) :: Integer
      (secs, subPicos) = picos `divMod` 1000000000000
      nanos = subPicos `div` 1000
  pure Time.Timespec {
    Time.timespecSeconds = fromInteger secs,
    Time.timespecNanoseconds = fromInteger nanos}

-- | Get the current working directory.
getWorkingDirectory :: IO (Either SystemError.SystemError File.FilePath)
getWorkingDirectory =
  E.catch (Right . File.FilePath <$> Dir.getCurrentDirectory)
    (\e -> pure $ Left $ SystemError.SystemErrorOther (IOE.ioeGetErrorString (e :: IOError)))

-- Helpers (not primitives)

-- | Convert the environment map to the association list System.Process expects.
toEnvList :: M.Map System.EnvironmentVariable String -> [(String, String)]
toEnvList m = [(System.unEnvironmentVariable k, v) | (k, v) <- M.toList m]

-- | Run a prepared process, capturing stdout/stderr as bytes and the exit code.
runProcess :: P.CreateProcess -> IO (Either SystemError.SystemError System.ProcessResult)
runProcess cp = do
  (_, mout, merr, ph) <- P.createProcess cp
  out <- Y.maybe (pure BS.empty) BS.hGetContents mout
  err <- Y.maybe (pure BS.empty) BS.hGetContents merr
  ec <- P.waitForProcess ph
  let code = case ec of
        ExitSuccess -> 0
        ExitFailure n -> fromIntegral n
  pure $ Right System.ProcessResult {
    System.processResultExitCode = System.StatusCode code,
    System.processResultStdout = out,
    System.processResultStderr = err}

-- | Classify a launch IOError into a SystemError.
classify :: File.FilePath -> IOError -> SystemError.SystemError
classify program e
  | IOE.isDoesNotExistError e = SystemError.SystemErrorCommandNotFound program
  | IOE.isPermissionError e = SystemError.SystemErrorPermissionDenied program
  | otherwise = SystemError.SystemErrorOther (IOE.ioeGetErrorString e)