packages feed

hydra-kernel-0.17.2: 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 as SIO
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)))

-- | Read the complete contents of standard input as raw bytes.
readStdin :: IO (Either SystemError.SystemError BS.ByteString)
readStdin = withStdioError BS.hGetContents SIO.stdin

-- | Write raw bytes to standard error.
writeStderr :: BS.ByteString -> IO (Either SystemError.SystemError ())
writeStderr = withStdioError' BS.hPut SIO.stderr

-- | Write raw bytes to standard output.
writeStdout :: BS.ByteString -> IO (Either SystemError.SystemError ())
writeStdout = withStdioError' BS.hPut SIO.stdout

-- Helpers (not primitives)

withStdioError :: (SIO.Handle -> IO a) -> SIO.Handle -> IO (Either SystemError.SystemError a)
withStdioError action handle =
  E.catch (Right <$> action handle)
    (\e -> pure $ Left $ SystemError.SystemErrorOther (IOE.ioeGetErrorString (e :: IOError)))

withStdioError' :: (SIO.Handle -> a -> IO ()) -> SIO.Handle -> a -> IO (Either SystemError.SystemError ())
withStdioError' action handle x = withStdioError (`action` x) handle

-- | 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)