ghcup-hs/lib/GHCup/File.hs

183 lines
6.3 KiB
Haskell
Raw Normal View History

2020-01-17 22:29:16 +00:00
{-# LANGUAGE TemplateHaskell #-}
2020-01-14 21:55:34 +00:00
module GHCup.File where
2020-01-16 22:27:38 +00:00
import Data.ByteString
2020-01-14 21:55:34 +00:00
import qualified Data.ByteString.Lazy as L
import Data.Char
import Data.Maybe
import HPath
import HPath.IO
2020-01-17 22:29:16 +00:00
import Optics
2020-02-18 08:40:01 +00:00
import Streamly.External.ByteString
import Streamly.External.ByteString.Lazy
2020-01-14 21:55:34 +00:00
import Streamly
import System.Posix.FilePath hiding ( (</>) )
import Data.Foldable
import Control.Monad
import Control.Exception.Safe
import Data.Functor
import System.Posix.Files.ByteString
2020-02-16 21:06:07 +00:00
import System.Posix.Foreign ( oExcl )
2020-01-16 22:27:38 +00:00
import System.IO
2020-01-17 22:29:16 +00:00
import "unix" System.Posix.IO.ByteString
2020-01-16 22:27:38 +00:00
hiding ( openFd )
2020-01-14 21:55:34 +00:00
import qualified System.Posix.Process.ByteString
as SPPB
2020-02-18 08:40:01 +00:00
import System.Posix.Directory.ByteString
2020-02-19 12:30:18 +00:00
import System.Posix.Process ( ProcessStatus(..) )
2020-01-14 21:55:34 +00:00
import System.Posix.Types
import qualified Streamly.Internal.Memory.ArrayStream
as AS
import qualified Streamly.FileSystem.Handle as FH
import qualified Streamly.Internal.Data.Unfold as SU
import qualified Streamly.Prelude as S
import System.Exit
import qualified Streamly.Data.Fold as FL
import Data.ByteString.Builder
import Foreign.C.Error
import GHCup.Prelude
2020-01-17 22:29:16 +00:00
import Control.Concurrent.Async
import Control.Concurrent
import System.Posix.FD as FD
2020-02-19 12:30:18 +00:00
data ProcessError = NonZeroExit Int
| PTerminated
| PStopped
| NoSuchPid
deriving Show
2020-01-17 22:29:16 +00:00
data CapturedProcess = CapturedProcess {
_exitCode :: ExitCode
2020-02-19 12:30:18 +00:00
, _stdOut :: ByteString
, _stdErr :: ByteString
2020-01-17 22:29:16 +00:00
} deriving (Eq, Show)
makeLenses ''CapturedProcess
2020-01-14 21:55:34 +00:00
readFd :: Fd -> IO L.ByteString
readFd fd = do
handle' <- fdToHandle fd
2020-02-18 08:40:01 +00:00
fromChunksIO $ (S.unfold (SU.finallyIO hClose FH.readChunks) handle')
2020-01-14 21:55:34 +00:00
-- | Read the lines of a file into a stream. The stream holds
-- a file handle as a resource and will close it once the stream
-- terminates (either through exception or because it's drained).
readFileLines :: Path b -> IO (SerialT IO ByteString)
readFileLines p = do
2020-01-16 22:27:38 +00:00
stream <- readFileStream p
2020-01-14 21:55:34 +00:00
pure
2020-02-18 08:40:01 +00:00
. (fmap fromArray)
2020-01-14 21:55:34 +00:00
. AS.splitOn (fromIntegral $ ord '\n')
2020-02-18 08:40:01 +00:00
. (fmap toArray)
2020-01-14 21:55:34 +00:00
$ stream
-- | Find the given executable by searching all *absolute* PATH components.
-- Relative paths in PATH are ignored.
--
-- This shouldn't throw IO exceptions, unless getting the environment variable
-- PATH does.
2020-01-24 22:43:11 +00:00
findExecutable :: Path Rel -> IO (Maybe (Path Abs))
2020-01-14 21:55:34 +00:00
findExecutable ex = do
sPaths <- fmap catMaybes . (fmap . fmap) parseAbs $ getSearchPath
-- We don't want exceptions to mess up our result. If we can't
-- figure out if a file exists, then treat it as a negative result.
2020-01-16 22:27:38 +00:00
asum $ fmap (handleIO (\_ -> pure Nothing)) $ fmap
2020-01-14 21:55:34 +00:00
-- asum for short-circuiting behavior
(\s -> (isExecutable (s </> ex) >>= guard) $> (Just (s </> ex)))
sPaths
2020-01-17 22:29:16 +00:00
-- | Execute the given command and collect the stdout, stderr and the exit code.
-- The command is run in a subprocess.
2020-02-18 08:40:01 +00:00
executeOut :: Path Rel -- ^ command as filename, e.g. 'ls'
-> [ByteString] -- ^ arguments to the command
-> Maybe (Path Abs) -- ^ chdir to this path
2020-01-17 22:29:16 +00:00
-> IO (Maybe CapturedProcess)
2020-02-19 12:30:18 +00:00
executeOut path args chdir = withRelPath path $ \fp -> captureOutStreams $ do
maybe (pure ()) (changeWorkingDirectory . toFilePath) chdir
SPPB.executeFile fp True args Nothing
2020-01-17 22:29:16 +00:00
-- | Capture the stdout and stderr of the given action, which
-- is run in a subprocess. Stdin is closed. You might want to
-- 'race' this to make sure it terminates.
captureOutStreams :: IO a
-- ^ the action to execute in a subprocess
2020-02-18 08:40:01 +00:00
-> IO (Maybe CapturedProcess) -- TODO: shouldn't be maybe
2020-01-17 22:29:16 +00:00
captureOutStreams action =
actionWithPipes $ \(parentStdoutRead, childStdoutWrite) ->
actionWithPipes $ \(parentStderrRead, childStderrWrite) -> do
pid <- SPPB.forkProcess $ do
-- dup stdout
void $ dupTo childStdoutWrite stdOutput
closeFd childStdoutWrite
closeFd parentStdoutRead
-- dup stderr
void $ dupTo childStderrWrite stdError
closeFd childStderrWrite
closeFd parentStderrRead
-- execute the action
void $ action
-- close everything we don't need
closeFd childStdoutWrite
closeFd childStderrWrite
SPPB.getProcessStatus True True pid >>= \case
-- readFd will take care of closing the fd
Just (SPPB.Exited es) -> do
2020-02-19 12:30:18 +00:00
stdout' <- L.toStrict <$> readFd parentStdoutRead
stderr' <- L.toStrict <$> readFd parentStderrRead
2020-01-17 22:29:16 +00:00
pure $ Just $ CapturedProcess { _exitCode = es
, _stdOut = stdout'
, _stdErr = stderr'
}
_ -> do
closeFd parentStdoutRead
closeFd parentStderrRead
pure $ Nothing
where
actionWithPipes a =
createPipe >>= \(p1, p2) -> (flip finally) (cleanup [p1, p2]) $ a (p1, p2)
cleanup fds = for_ fds $ \fd -> handleIO (\_ -> pure ()) $ closeFd fd
-- | Create a new regular file in write-only mode. The file must not exist.
createRegularFileFd :: FileMode -> Path b -> IO Fd
2020-02-16 21:06:07 +00:00
createRegularFileFd fm dest =
FD.openFd (toFilePath dest) WriteOnly [oExcl] (Just fm)
2020-02-19 12:30:18 +00:00
exe :: ByteString
-> [ByteString]
-> Bool
-> Maybe (Path Abs)
-> IO (Either ProcessError ())
exe exe' args spath chdir = do
pid <- SPPB.forkProcess $ do
maybe (pure ()) (changeWorkingDirectory . toFilePath) chdir
SPPB.executeFile exe' spath args Nothing
fmap toProcessError $ SPPB.getProcessStatus True True pid
toProcessError :: Maybe ProcessStatus -> Either ProcessError ()
toProcessError mps = case mps of
Just (SPPB.Exited (ExitFailure i)) -> Left $ NonZeroExit i
Just (SPPB.Exited ExitSuccess ) -> Right ()
Just (Terminated _ _ ) -> Left $ PTerminated
Just (Stopped _ ) -> Left $ PStopped
Nothing -> Left $ NoSuchPid