@@ -9,17 +9,24 @@
9-- | An extension of `System.Process` that integrates with logging (`Obelisk.CLI.Logging`)
9-- | An extension of `System.Process` that integrates with logging (`Obelisk.CLI.Logging`)
10-- and is thus spinner friendly.
10-- and is thus spinner friendly.
11module Obelisk.CliApp.Process
11module Obelisk.CliApp.Process
12+ ( AsProcessFailure (..)
12− ( ProcessFailure (..)
13+ , ProcessFailure (..)
13− , AsProcessFailure (..)
14+ , ProcessSpec (..)
14− , readProcessAndLogStderr
15+ , callCommand
15− , readProcessAndLogOutput
16+ , callProcess
16− , readCreateProcessWithExitCode
17 , callProcessAndLogOutput
17 , callProcessAndLogOutput
18 , createProcess
18 , createProcess
19 , createProcess_
19 , createProcess_
20− , callProcess
20+ , overCreateProcess
21− , callCommand
21+ , proc
22+ , readCreateProcessWithExitCode
23+ , readProcessAndLogOutput
24+ , readProcessAndLogStderr
22 , reconstructCommand
25 , reconstructCommand
25import Control.Monad ((<=<), join, void)
32import Control.Monad ((<=<), join, void)
@@ -30,24 +37,50 @@ import Control.Lens (Prism', review)
30import qualified Data.ByteString as BS
37import qualified Data.ByteString as BS
31import qualified Data.ByteString.Char8 as BSC
38import qualified Data.ByteString.Char8 as BSC
32import Data.Function (fix)
39import Data.Function (fix)
41+import qualified Data.Map as Map
33import Data.Monoid ((<>))
42import Data.Monoid ((<>))
34import Data.Text (Text)
43import Data.Text (Text)
35import qualified Data.Text as T
44import qualified Data.Text as T
36import qualified Data.Text.Encoding as T
45import qualified Data.Text.Encoding as T
37import Data.Text.Encoding.Error (lenientDecode)
46import Data.Text.Encoding.Error (lenientDecode)
47+import System.Environment (getEnvironment)
38import System.Exit (ExitCode (..))
48import System.Exit (ExitCode (..))
39import System.IO (Handle)
49import System.IO (Handle)
40import System.IO.Streams (InputStream, handleToInputStream)
50import System.IO.Streams (InputStream, handleToInputStream)
41import qualified System.IO.Streams as Streams
51import qualified System.IO.Streams as Streams
42import System.IO.Streams.Concurrent (concurrentMerge)
52import System.IO.Streams.Concurrent (concurrentMerge)
43−import System.Process (CreateProcess, ProcessHandle, StdStream (CreatePipe), cmdspec, std_err, std_out,
53+import System.Process (CreateProcess, ProcessHandle, StdStream (CreatePipe), std_err, std_out, waitForProcess)
45import qualified System.Process as Process
54import qualified System.Process as Process
47import Control.Monad.Log (Severity (..))
56import Control.Monad.Log (Severity (..))
48import Obelisk.CliApp.Logging (putLog, putLogRaw)
57import Obelisk.CliApp.Logging (putLog, putLogRaw)
49import Obelisk.CliApp.Types (CliLog, CliThrow)
58import Obelisk.CliApp.Types (CliLog, CliThrow)
60+data ProcessSpec = ProcessSpec
61+ { _processSpec_createProcess :: !CreateProcess
62+ , _processSpec_overrideEnv :: !(Maybe (Map String String -> Map String String))
65+proc :: FilePath -> [String] -> ProcessSpec
66+proc cmd args = ProcessSpec (Process.proc cmd args) Nothing
68+shell :: String -> ProcessSpec
69+shell cmd = ProcessSpec (Process.shell cmd) Nothing
71+setEnvOverride :: (Map String String -> Map String String) -> ProcessSpec -> ProcessSpec
72+setEnvOverride f p = p { _processSpec_overrideEnv = Just f }
74+overCreateProcess :: (CreateProcess -> CreateProcess) -> ProcessSpec -> ProcessSpec
75+overCreateProcess f (ProcessSpec p x) = ProcessSpec (f p) x
77+setDelegateCtlc :: Bool -> ProcessSpec -> ProcessSpec
78+setDelegateCtlc b = overCreateProcess (\p -> p { Process.delegate_ctlc = b })
80+setCwd :: Maybe FilePath -> ProcessSpec -> ProcessSpec
81+setCwd fp = overCreateProcess (\p -> p { Process.cwd = fp })
51-- TODO put back in `Obelisk.CliApp.Process` and use prisms for extensible exceptions
84-- TODO put back in `Obelisk.CliApp.Process` and use prisms for extensible exceptions
52data ProcessFailure = ProcessFailure Process.CmdSpec Int -- exit code
85data ProcessFailure = ProcessFailure Process.CmdSpec Int -- exit code
53 deriving Show
86 deriving Show
@@ -62,7 +95,7 @@ instance AsProcessFailure ProcessFailure where
63readProcessAndLogStderr
96readProcessAndLogStderr
64 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
97 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
65− => Severity -> CreateProcess -> m Text
98+ => Severity -> ProcessSpec -> m Text
66readProcessAndLogStderr sev process = do
99readProcessAndLogStderr sev process = do
67 (out, _err) <- withProcess process $ \_out err -> do
100 (out, _err) <- withProcess process $ \_out err -> do
68 streamToLog =<< liftIO (streamHandle sev err)
101 streamToLog =<< liftIO (streamHandle sev err)
@@ -70,9 +103,10 @@ readProcessAndLogStderr sev process = do
71readCreateProcessWithExitCode
104readCreateProcessWithExitCode
72 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
105 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
73− => CreateProcess -> m (ExitCode, String, String)
106+ => ProcessSpec -> m (ExitCode, String, String)
74−readCreateProcessWithExitCode process = do
107+readCreateProcessWithExitCode procSpec = do
108+ process <- mkCreateProcess procSpec
75− putLog Debug $ "Creating process: " <> reconstructCommand (cmdspec process)
109+ putLog Debug $ "Creating process: " <> reconstructProcSpec procSpec
76 liftIO $ Process.readCreateProcessWithExitCode process ""
110 liftIO $ Process.readCreateProcessWithExitCode process ""
78-- | Like `System.Process.readProcess` but logs the combined output (stdout and stderr)
112-- | Like `System.Process.readProcess` but logs the combined output (stdout and stderr)
@@ -84,10 +118,10 @@ readCreateProcessWithExitCode process = do
84-- `callProcessAndLogOutput (Debug, Debug)`.
118-- `callProcessAndLogOutput (Debug, Debug)`.
85readProcessAndLogOutput
119readProcessAndLogOutput
86 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
120 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
87− => (Severity, Severity) -> CreateProcess -> m Text
121+ => (Severity, Severity) -> ProcessSpec -> m Text
88readProcessAndLogOutput (sev_out, sev_err) process = do
122readProcessAndLogOutput (sev_out, sev_err) process = do
89− (_, Just out, Just err, p) <- createProcess $ process
123+ (_, Just out, Just err, p) <- createProcess $ overCreateProcess
90− { std_out = CreatePipe , std_err = CreatePipe }
124+ (\p -> p { std_out = CreatePipe , std_err = CreatePipe }) process
92 -- TODO interleave stdout and stderr in log correctly
126 -- TODO interleave stdout and stderr in log correctly
93 streamToLog =<< liftIO (streamHandle sev_err err)
127 streamToLog =<< liftIO (streamHandle sev_err err)
@@ -96,9 +130,9 @@ readProcessAndLogOutput (sev_out, sev_err) process = do
97 liftIO (waitForProcess p) >>= \case
131 liftIO (waitForProcess p) >>= \case
98 ExitSuccess -> pure outText
132 ExitSuccess -> pure outText
99− ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (cmdspec process) code
133+ ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (Process.cmdspec $ _processSpec_createProcess process) code
101−-- | Like `System.Process.callProcess` but logs the combined output (stdout and stderr)
135+-- | Like 'System.Process.callProcess' but logs the combined output (stdout and stderr)
102-- with the corresponding severity.
136-- with the corresponding severity.
104-- Usually this function is called as `callProcessAndLogOutput (Debug, Error)`. However
138-- Usually this function is called as `callProcessAndLogOutput (Debug, Error)`. However
@@ -106,9 +140,8 @@ readProcessAndLogOutput (sev_out, sev_err) process = do
106-- which case it is advisable to call it with a non-Error severity for stderr, like
140-- which case it is advisable to call it with a non-Error severity for stderr, like
107-- `callProcessAndLogOutput (Debug, Debug)`.
141-- `callProcessAndLogOutput (Debug, Debug)`.
108callProcessAndLogOutput
142callProcessAndLogOutput
110 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
143 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
111− => (Severity, Severity) -> CreateProcess -> m ()
144+ => (Severity, Severity) -> ProcessSpec -> m ()
112callProcessAndLogOutput (sev_out, sev_err) process =
145callProcessAndLogOutput (sev_out, sev_err) process =
113 void $ withProcess process $ \out err -> do
146 void $ withProcess process $ \out err -> do
114 stream <- liftIO $ join $ combineStream
147 stream <- liftIO $ join $ combineStream
@@ -118,21 +151,31 @@ callProcessAndLogOutput (sev_out, sev_err) process =
119 combineStream s1 s2 = concurrentMerge [s1, s2]
152 combineStream s1 s2 = concurrentMerge [s1, s2]
121−-- | Like `System.Process.createProcess` but also logs (debug) the process being run
154+-- | Like 'System.Process.createProcess' but also logs (debug) the process being run
122createProcess
155createProcess
123 :: (MonadIO m, CliLog m)
156 :: (MonadIO m, CliLog m)
124− => CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
157+ => ProcessSpec -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
125−createProcess p = do
158+createProcess procSpec = do
159+ p <- mkCreateProcess procSpec
126− putLog Debug $ "Creating process: " <> reconstructCommand (cmdspec p)
160+ putLog Debug $ "Creating process: " <> reconstructProcSpec procSpec
127 liftIO $ Process.createProcess p
161 liftIO $ Process.createProcess p
129-- | Like `System.Process.createProcess_` but also logs (debug) the process being run
163-- | Like `System.Process.createProcess_` but also logs (debug) the process being run
130createProcess_
164createProcess_
131 :: (MonadIO m, CliLog m)
165 :: (MonadIO m, CliLog m)
132− => String -> CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
166+ => String -> ProcessSpec -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
133−createProcess_ name p = do
167+createProcess_ name procSpec = do
168+ p <- mkCreateProcess procSpec
134− putLog Debug $ "Creating process " <> T.pack name <> ": " <> reconstructCommand (cmdspec p)
169+ putLog Debug $ "Creating process " <> T.pack name <> ": " <> reconstructProcSpec procSpec
135− liftIO $ Process.createProcess p
170+ liftIO $ Process.createProcess_ name p
172+mkCreateProcess :: MonadIO m => ProcessSpec -> m Process.CreateProcess
173+mkCreateProcess (ProcessSpec p override') = do
177+ procEnv <- Map.fromList <$> maybe (liftIO getEnvironment) pure (Process.env p)
178+ pure $ p { Process.env = Just $ Map.toAscList (override procEnv) }
137-- | Like `System.Process.callProcess` but also logs (debug) the process being run
180-- | Like `System.Process.callProcess` but also logs (debug) the process being run
138callProcess
181callProcess
@@ -152,17 +195,17 @@ callCommand cmd = do
153withProcess
196withProcess
154 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
197 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
155− => CreateProcess -> (Handle -> Handle -> m ()) -> m (Handle, Handle)
198+ => ProcessSpec -> (Handle -> Handle -> m ()) -> m (Handle, Handle)
156withProcess process f = do -- TODO: Use bracket.
199withProcess process f = do -- TODO: Use bracket.
157 -- FIXME: Using `withCreateProcess` here leads to something operating illegally on closed handles.
200 -- FIXME: Using `withCreateProcess` here leads to something operating illegally on closed handles.
158− (_, Just out, Just err, p) <- createProcess $ process
201+ (_, Just out, Just err, p) <- createProcess $ overCreateProcess
159− { std_out = CreatePipe , std_err = CreatePipe }
202+ (\x -> x { std_out = CreatePipe , std_err = CreatePipe }) process
161 f out err -- Pass the handles to the passed function
204 f out err -- Pass the handles to the passed function
163 liftIO (waitForProcess p) >>= \case
206 liftIO (waitForProcess p) >>= \case
164 ExitSuccess -> return (out, err)
207 ExitSuccess -> return (out, err)
165− ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (cmdspec process) code
208+ ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (Process.cmdspec $ _processSpec_createProcess process) code
167-- Create an input stream from the file handle, associating each item with the given severity.
210-- Create an input stream from the file handle, associating each item with the given severity.
168streamHandle :: Severity -> Handle -> IO (InputStream (Severity, BSC.ByteString))
211streamHandle :: Severity -> Handle -> IO (InputStream (Severity, BSC.ByteString))
@@ -179,8 +222,12 @@ streamToLog stream = fix $ \loop -> do
180-- | Pretty print a 'CmdSpec'
223-- | Pretty print a 'CmdSpec'
181reconstructCommand :: Process.CmdSpec -> Text
224reconstructCommand :: Process.CmdSpec -> Text
225+reconstructCommand p = case p of
182−reconstructCommand (Process.ShellCommand str) = T.pack str
226+ Process.ShellCommand str -> T.pack str
183−reconstructCommand (Process.RawCommand c as) = processToShellString c as
227+ Process.RawCommand c as -> processToShellString c as
185 processToShellString cmd args = T.unwords $ map quoteAndEscape (cmd : args)
229 processToShellString cmd args = T.unwords $ map quoteAndEscape (cmd : args)
186 quoteAndEscape x = "'" <> T.replace "'" "'\''" (T.pack x) <> "'"
230 quoteAndEscape x = "'" <> T.replace "'" "'\''" (T.pack x) <> "'"
232+reconstructProcSpec :: ProcessSpec -> Text
233+reconstructProcSpec = reconstructCommand . Process.cmdspec . _processSpec_createProcess