2022-05-21 20:54:18 +00:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
{-# LANGUAGE FlexibleContexts #-}
|
|
|
|
{-# LANGUAGE DataKinds #-}
|
|
|
|
|
|
|
|
{-|
|
|
|
|
Module : GHCup.Utils.Process.Windows
|
|
|
|
Description : Process handling for windows
|
|
|
|
Copyright : (c) Julian Ospald, 2020
|
|
|
|
License : LGPL-3.0
|
|
|
|
Maintainer : hasufell@hasufell.de
|
|
|
|
Stability : experimental
|
|
|
|
Portability : Windows
|
|
|
|
-}
|
|
|
|
module GHCup.Prelude.Process.Windows where
|
|
|
|
|
|
|
|
import GHCup.Utils.Dirs
|
|
|
|
import GHCup.Prelude.File.Search
|
|
|
|
import GHCup.Prelude.Logger.Internal
|
|
|
|
import GHCup.Types
|
|
|
|
import GHCup.Types.Optics
|
|
|
|
|
|
|
|
import Control.Concurrent
|
|
|
|
import Control.DeepSeq
|
|
|
|
import Control.Exception.Safe
|
|
|
|
import Control.Monad
|
|
|
|
import Control.Monad.Reader
|
|
|
|
import Data.List
|
|
|
|
import Foreign.C.Error
|
|
|
|
import GHC.IO.Exception
|
|
|
|
import GHC.IO.Handle
|
|
|
|
import System.Environment
|
|
|
|
import System.FilePath
|
|
|
|
import System.IO
|
|
|
|
import System.Process
|
|
|
|
|
|
|
|
import qualified Control.Exception as EX
|
|
|
|
import qualified Data.ByteString as BS
|
|
|
|
import qualified Data.ByteString.Lazy as BL
|
|
|
|
import qualified Data.Map.Strict as Map
|
|
|
|
import qualified Data.Text as T
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
toProcessError :: FilePath
|
|
|
|
-> [FilePath]
|
|
|
|
-> ExitCode
|
|
|
|
-> Either ProcessError ()
|
|
|
|
toProcessError exe args exitcode = case exitcode of
|
|
|
|
(ExitFailure xi) -> Left $ NonZeroExit xi exe args
|
|
|
|
ExitSuccess -> Right ()
|
|
|
|
|
|
|
|
|
|
|
|
-- | @readCreateProcessWithExitCode@ works exactly like 'readProcessWithExitCode' except that it
|
|
|
|
-- lets you pass 'CreateProcess' giving better flexibility.
|
|
|
|
--
|
|
|
|
-- Note that @Handle@s provided for @std_in@, @std_out@, or @std_err@ via the CreateProcess
|
|
|
|
-- record will be ignored.
|
|
|
|
--
|
|
|
|
-- @since 1.2.3.0
|
|
|
|
readCreateProcessWithExitCodeBS
|
|
|
|
:: CreateProcess
|
|
|
|
-> BL.ByteString
|
|
|
|
-> IO (ExitCode, BL.ByteString, BL.ByteString) -- ^ exitcode, stdout, stderr
|
|
|
|
readCreateProcessWithExitCodeBS cp input = do
|
|
|
|
let cp_opts = cp {
|
|
|
|
std_in = CreatePipe,
|
|
|
|
std_out = CreatePipe,
|
|
|
|
std_err = CreatePipe
|
|
|
|
}
|
|
|
|
withCreateProcess_ "readCreateProcessWithExitCodeBS" cp_opts $
|
|
|
|
\mb_inh mb_outh mb_errh ph ->
|
|
|
|
case (mb_inh, mb_outh, mb_errh) of
|
|
|
|
(Just inh, Just outh, Just errh) -> do
|
|
|
|
|
|
|
|
out <- BS.hGetContents outh
|
|
|
|
err <- BS.hGetContents errh
|
|
|
|
|
|
|
|
-- fork off threads to start consuming stdout & stderr
|
|
|
|
withForkWait (EX.evaluate $ rnf out) $ \waitOut ->
|
|
|
|
withForkWait (EX.evaluate $ rnf err) $ \waitErr -> do
|
|
|
|
|
|
|
|
-- now write any input
|
|
|
|
unless (BL.null input) $
|
|
|
|
ignoreSigPipe $ BL.hPut inh input
|
|
|
|
-- hClose performs implicit hFlush, and thus may trigger a SIGPIPE
|
|
|
|
ignoreSigPipe $ hClose inh
|
|
|
|
|
|
|
|
-- wait on the output
|
|
|
|
waitOut
|
|
|
|
waitErr
|
|
|
|
|
|
|
|
hClose outh
|
|
|
|
hClose errh
|
|
|
|
|
|
|
|
-- wait on the process
|
|
|
|
ex <- waitForProcess ph
|
|
|
|
return (ex, BL.fromStrict out, BL.fromStrict err)
|
|
|
|
|
|
|
|
(Nothing,_,_) -> error "readCreateProcessWithExitCodeBS: Failed to get a stdin handle."
|
|
|
|
(_,Nothing,_) -> error "readCreateProcessWithExitCodeBS: Failed to get a stdout handle."
|
|
|
|
(_,_,Nothing) -> error "readCreateProcessWithExitCodeBS: Failed to get a stderr handle."
|
|
|
|
where
|
|
|
|
ignoreSigPipe :: IO () -> IO ()
|
|
|
|
ignoreSigPipe = EX.handle $ \e -> case e of
|
|
|
|
IOError { ioe_type = ResourceVanished
|
|
|
|
, ioe_errno = Just ioe }
|
|
|
|
| Errno ioe == ePIPE -> return ()
|
|
|
|
_ -> throwIO e
|
|
|
|
-- wrapper so we can get exceptions with the appropriate function name.
|
|
|
|
withCreateProcess_
|
|
|
|
:: String
|
|
|
|
-> CreateProcess
|
|
|
|
-> (Maybe Handle -> Maybe Handle -> Maybe Handle -> ProcessHandle -> IO a)
|
|
|
|
-> IO a
|
|
|
|
withCreateProcess_ fun c action =
|
|
|
|
EX.bracketOnError (createProcess_ fun c) cleanupProcess
|
|
|
|
(\(m_in, m_out, m_err, ph) -> action m_in m_out m_err ph)
|
|
|
|
|
|
|
|
-- | Fork a thread while doing something else, but kill it if there's an
|
|
|
|
-- exception.
|
|
|
|
--
|
|
|
|
-- This is important in the cases above because we want to kill the thread
|
|
|
|
-- that is holding the Handle lock, because when we clean up the process we
|
|
|
|
-- try to close that handle, which could otherwise deadlock.
|
|
|
|
--
|
|
|
|
withForkWait :: IO () -> (IO () -> IO a) -> IO a
|
|
|
|
withForkWait async' body = do
|
|
|
|
waitVar <- newEmptyMVar :: IO (MVar (Either SomeException ()))
|
|
|
|
mask $ \restore -> do
|
|
|
|
tid <- forkIO $ try (restore async') >>= putMVar waitVar
|
|
|
|
let wait' = takeMVar waitVar >>= either throwIO return
|
|
|
|
restore (body wait') `EX.onException` killThread tid
|
|
|
|
|
|
|
|
|
|
|
|
-- | Execute the given command and collect the stdout, stderr and the exit code.
|
|
|
|
-- The command is run in a subprocess.
|
|
|
|
executeOut :: MonadIO m
|
|
|
|
=> FilePath -- ^ command as filename, e.g. 'ls'
|
|
|
|
-> [String] -- ^ arguments to the command
|
|
|
|
-> Maybe FilePath -- ^ chdir to this path
|
|
|
|
-> m CapturedProcess
|
|
|
|
executeOut path args chdir = do
|
|
|
|
cp <- createProcessWithMingwPath ((proc path args){ cwd = chdir })
|
|
|
|
(exit, out, err) <- liftIO $ readCreateProcessWithExitCodeBS cp ""
|
|
|
|
pure $ CapturedProcess exit out err
|
|
|
|
|
|
|
|
|
|
|
|
execLogged :: ( MonadReader env m
|
|
|
|
, HasDirs env
|
|
|
|
, HasLog env
|
|
|
|
, HasSettings env
|
|
|
|
, MonadIO m
|
|
|
|
, MonadThrow m)
|
|
|
|
=> FilePath -- ^ thing to execute
|
|
|
|
-> [String] -- ^ args for the thing
|
|
|
|
-> Maybe FilePath -- ^ optionally chdir into this
|
|
|
|
-> FilePath -- ^ log filename (opened in append mode)
|
|
|
|
-> Maybe [(String, String)] -- ^ optional environment
|
|
|
|
-> m (Either ProcessError ())
|
|
|
|
execLogged exe args chdir lfile env = do
|
|
|
|
Dirs {..} <- getDirs
|
|
|
|
logDebug $ T.pack $ "Running " <> exe <> " with arguments " <> show args
|
|
|
|
let stdoutLogfile = fromGHCupPath logsDir </> lfile <> ".stdout.log"
|
|
|
|
stderrLogfile = fromGHCupPath logsDir </> lfile <> ".stderr.log"
|
|
|
|
cp <- createProcessWithMingwPath ((proc exe args)
|
|
|
|
{ cwd = chdir
|
|
|
|
, env = env
|
|
|
|
, std_in = CreatePipe
|
|
|
|
, std_out = CreatePipe
|
|
|
|
, std_err = CreatePipe
|
|
|
|
})
|
|
|
|
fmap (toProcessError exe args)
|
|
|
|
$ liftIO
|
|
|
|
$ withCreateProcess cp
|
|
|
|
$ \_ mout merr ph ->
|
|
|
|
case (mout, merr) of
|
|
|
|
(Just cStdout, Just cStderr) -> do
|
|
|
|
withForkWait (tee stdoutLogfile cStdout) $ \waitOut ->
|
|
|
|
withForkWait (tee stderrLogfile cStderr) $ \waitErr -> do
|
|
|
|
waitOut
|
|
|
|
waitErr
|
|
|
|
waitForProcess ph
|
|
|
|
_ -> fail "Could not acquire out/err handle"
|
|
|
|
|
|
|
|
where
|
|
|
|
tee :: FilePath -> Handle -> IO ()
|
|
|
|
tee logFile handle' = go
|
|
|
|
where
|
|
|
|
go = do
|
|
|
|
some <- BS.hGetSome handle' 512
|
|
|
|
if BS.null some
|
|
|
|
then pure ()
|
|
|
|
else do
|
|
|
|
void $ BS.appendFile logFile some
|
|
|
|
-- subprocess stdout also goes to stderr for logging
|
|
|
|
void $ BS.hPut stderr some
|
|
|
|
go
|
|
|
|
|
|
|
|
|
|
|
|
-- | Thin wrapper around `executeFile`.
|
|
|
|
exec :: MonadIO m
|
|
|
|
=> FilePath -- ^ thing to execute
|
|
|
|
-> [FilePath] -- ^ args for the thing
|
|
|
|
-> Maybe FilePath -- ^ optionally chdir into this
|
|
|
|
-> Maybe [(String, String)] -- ^ optional environment
|
|
|
|
-> m (Either ProcessError ())
|
|
|
|
exec exe args chdir env = do
|
2022-06-06 18:18:10 +00:00
|
|
|
-- https://gitlab.haskell.org/haskell/ghcup-hs/-/issues/375
|
|
|
|
forM_ (Map.fromList <$> env) $ \cEnv -> do
|
|
|
|
let paths = ["PATH", "Path"]
|
|
|
|
curPaths = (\x -> maybe [] splitSearchPath (Map.lookup x cEnv)) =<< paths
|
|
|
|
newPath = intercalate [searchPathSeparator] curPaths
|
|
|
|
setEnv "PATH" ""
|
|
|
|
setEnv "Path" newPath
|
2022-05-21 20:54:18 +00:00
|
|
|
cp <- createProcessWithMingwPath ((proc exe args) { cwd = chdir, env = env })
|
|
|
|
exit_code <- liftIO $ withCreateProcess cp $ \_ _ _ p -> waitForProcess p
|
|
|
|
pure $ toProcessError exe args exit_code
|
|
|
|
|
2022-06-06 21:02:21 +00:00
|
|
|
-- | Like 'exec', except doesn't add msys2 stuff to PATH.
|
|
|
|
execNoMinGW :: MonadIO m
|
|
|
|
=> FilePath -- ^ thing to execute
|
|
|
|
-> [FilePath] -- ^ args for the thing
|
|
|
|
-> Maybe FilePath -- ^ optionally chdir into this
|
|
|
|
-> Maybe [(String, String)] -- ^ optional environment
|
|
|
|
-> m (Either ProcessError ())
|
|
|
|
execNoMinGW exe args chdir env = do
|
|
|
|
-- https://gitlab.haskell.org/haskell/ghcup-hs/-/issues/375
|
|
|
|
forM_ (Map.fromList <$> env) $ \cEnv -> do
|
|
|
|
let paths = ["PATH", "Path"]
|
|
|
|
curPaths = (\x -> maybe [] splitSearchPath (Map.lookup x cEnv)) =<< paths
|
|
|
|
newPath = intercalate [searchPathSeparator] curPaths
|
|
|
|
setEnv "PATH" ""
|
|
|
|
setEnv "Path" newPath
|
|
|
|
let cp = (proc exe args) { cwd = chdir, env = env }
|
|
|
|
exit_code <- liftIO $ withCreateProcess cp $ \_ _ _ p -> waitForProcess p
|
|
|
|
pure $ toProcessError exe args exit_code
|
|
|
|
|
2022-05-21 20:54:18 +00:00
|
|
|
|
|
|
|
-- | Thin wrapper around `executeFile`.
|
|
|
|
execShell :: MonadIO m
|
|
|
|
=> FilePath -- ^ thing to execute
|
|
|
|
-> [FilePath] -- ^ args for the thing
|
|
|
|
-> Maybe FilePath -- ^ optionally chdir into this
|
|
|
|
-> Maybe [(String, String)] -- ^ optional environment
|
|
|
|
-> m (Either ProcessError ())
|
|
|
|
execShell exe args chdir env = do
|
|
|
|
let cmd = exe <> " " <> concatMap (' ':) args
|
|
|
|
cp <- createProcessWithMingwPath ((shell cmd) { cwd = chdir, env = env })
|
|
|
|
exit_code <- liftIO $ withCreateProcess cp $ \_ _ _ p -> waitForProcess p
|
|
|
|
pure $ toProcessError cmd [] exit_code
|
|
|
|
|
|
|
|
|
|
|
|
createProcessWithMingwPath :: MonadIO m
|
|
|
|
=> CreateProcess
|
|
|
|
-> m CreateProcess
|
|
|
|
createProcessWithMingwPath cp = do
|
|
|
|
msys2Dir <- liftIO ghcupMsys2Dir
|
|
|
|
cEnv <- Map.fromList <$> maybe (liftIO getEnvironment) pure (env cp)
|
|
|
|
let mingWPaths = [msys2Dir </> "usr" </> "bin"
|
|
|
|
,msys2Dir </> "mingw64" </> "bin"]
|
|
|
|
paths = ["PATH", "Path"]
|
|
|
|
curPaths = (\x -> maybe [] splitSearchPath (Map.lookup x cEnv)) =<< paths
|
|
|
|
newPath = intercalate [searchPathSeparator] (mingWPaths ++ curPaths)
|
|
|
|
envWithoutPath = foldr (\x y -> Map.delete x y) cEnv paths
|
|
|
|
envWithNewPath = Map.insert "Path" newPath envWithoutPath
|
|
|
|
liftIO $ setEnv "Path" newPath
|
|
|
|
pure $ cp { env = Just $ Map.toList envWithNewPath }
|
|
|
|
|
|
|
|
ghcupMsys2Dir :: IO FilePath
|
|
|
|
ghcupMsys2Dir =
|
|
|
|
lookupEnv "GHCUP_MSYS2" >>= \case
|
|
|
|
Just fp -> pure fp
|
|
|
|
Nothing -> do
|
|
|
|
baseDir <- liftIO ghcupBaseDir
|
|
|
|
pure (fromGHCupPath baseDir </> "msys64")
|
|
|
|
|