@@ -9,7 +9,8 @@
99-- | An extension of `System.Process` that integrates with logging (`Obelisk.CLI.Logging`)
1010-- and is thus spinner friendly.
1111module Obelisk.CliApp.Process
12− ( ProcessFailed (..)
12+ ( ProcessFailure (..)
13+ , AsProcessFailure (..)
1314 , readProcessAndLogStderr
1415 , readProcessAndLogOutput
1516 , callProcessAndLogOutput
@@ -22,6 +23,7 @@ module Obelisk.CliApp.Process
2223import Control.Monad ((<=<), join, void)
2324import Control.Monad.Except (throwError)
2425import Control.Monad.IO.Class (MonadIO, liftIO)
26+import Control.Lens (Prism', review)
2527import qualified Data.ByteString as BS
2628import qualified Data.ByteString.Char8 as BSC
2729import Data.Function (fix)
@@ -41,10 +43,22 @@ import qualified System.Process as Process
4143
4244import Control.Monad.Log (Severity (..))
4345import Obelisk.CliApp.Logging (putLog, putLogRaw)
44−import Obelisk.CliApp.Types (Cli, ProcessFailed (..), reconstructCommand)
46+import Obelisk.CliApp.Types (CliLog, CliThrow)
47+
48+-- TODO put back in `Obelisk.CliApp.Process` and use prisms for extensible exceptions
49+data ProcessFailure = ProcessFailure Process.CmdSpec Int -- exit code
50+ deriving Show
51+
52+-- | Indicates arbitrary process failures form one variant (or conceptual projection) of
53+-- the error type.
54+class AsProcessFailure e where
55+ asProcessFailure :: Prism' e ProcessFailure
56+
57+instance AsProcessFailure ProcessFailure where
58+ asProcessFailure = id
4559
4660readProcessAndLogStderr
47− :: (MonadIO m, Cli m)
61+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
4862 => Severity -> CreateProcess -> m Text
4963readProcessAndLogStderr sev process = do
5064 (out, _err) <- withProcess process $ \_out err -> do
@@ -58,9 +72,8 @@ readProcessAndLogStderr sev process = do
5872-- some processes are known to spit out diagnostic or informative messages in stderr, in
5973-- which case it is advisable to call it with a non-Error severity for stderr, like
6074-- `callProcessAndLogOutput (Debug, Debug)`.
61−
6275readProcessAndLogOutput
63− :: (MonadIO m, Cli m)
76+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
6477 => (Severity, Severity) -> CreateProcess -> m Text
6578readProcessAndLogOutput (sev_out, sev_err) process = do
6679 (_, Just out, Just err, p) <- createProcess $ process
@@ -73,7 +86,7 @@ readProcessAndLogOutput (sev_out, sev_err) process = do
7386
7487 liftIO (waitForProcess p) >>= \case
7588 ExitSuccess -> pure outText
76− ExitFailure code -> throwError $ Right $ ProcessFailed (cmdspec process) code
89+ ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (cmdspec process) code
7790
7891-- | Like `System.Process.callProcess` but logs the combined output (stdout and stderr)
7992-- with the corresponding severity.
@@ -84,7 +97,7 @@ readProcessAndLogOutput (sev_out, sev_err) process = do
8497-- `callProcessAndLogOutput (Debug, Debug)`.
8598callProcessAndLogOutput
8699
87− :: (MonadIO m, Cli m)
100+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
88101 => (Severity, Severity) -> CreateProcess -> m ()
89102callProcessAndLogOutput (sev_out, sev_err) process =
90103 void $ withProcess process $ \out err -> do
@@ -97,7 +110,7 @@ callProcessAndLogOutput (sev_out, sev_err) process =
97110
98111-- | Like `System.Process.createProcess` but also logs (debug) the process being run
99112createProcess
100− :: (MonadIO m, Cli m)
113+ :: (MonadIO m, CliLog m)
101114 => CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
102115createProcess p = do
103116 putLog Debug $ "Creating process: " <> reconstructCommand (cmdspec p)
@@ -105,7 +118,7 @@ createProcess p = do
105118
106119-- | Like `System.Process.createProcess_` but also logs (debug) the process being run
107120createProcess_
108− :: (MonadIO m, Cli m)
121+ :: (MonadIO m, CliLog m)
109122 => String -> CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
110123createProcess_ name p = do
111124 putLog Debug $ "Creating process " <> T.pack name <> ": " <> reconstructCommand (cmdspec p)
@@ -113,7 +126,7 @@ createProcess_ name p = do
113126
114127-- | Like `System.Process.callProcess` but also logs (debug) the process being run
115128callProcess
116− :: (MonadIO m, Cli m)
129+ :: (MonadIO m, CliLog m)
117130 => String -> [String] -> m ()
118131callProcess exe args = do
119132 putLog Debug $ "Calling process " <> T.pack exe <> " with args: " <> T.pack (show args)
@@ -121,14 +134,14 @@ callProcess exe args = do
121134
122135-- | Like `System.Process.callCommand` but also logs (debug) the command being run
123136callCommand
124− :: (MonadIO m, Cli m)
137+ :: (MonadIO m, CliLog m)
125138 => String -> m ()
126139callCommand cmd = do
127140 putLog Debug $ "Calling command " <> T.pack cmd
128141 liftIO $ Process.callCommand cmd
129142
130143withProcess
131− :: (MonadIO m, Cli m)
144+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
132145 => CreateProcess -> (Handle -> Handle -> m ()) -> m (Handle, Handle)
133146withProcess process f = do -- TODO: Use bracket.
134147 -- FIXME: Using `withCreateProcess` here leads to something operating illegally on closed handles.
@@ -139,7 +152,7 @@ withProcess process f = do -- TODO: Use bracket.
139152
140153 liftIO (waitForProcess p) >>= \case
141154 ExitSuccess -> return (out, err)
142− ExitFailure code -> throwError $ Right $ ProcessFailed (cmdspec process) code
155+ ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (cmdspec process) code
143156
144157-- Create an input stream from the file handle, associating each item with the given severity.
145158streamHandle :: Severity -> Handle -> IO (InputStream (Severity, BSC.ByteString))
@@ -147,9 +160,16 @@ streamHandle sev = Streams.map (sev,) <=< handleToInputStream
147160
148161-- | Read from an input stream and log its contents
149162streamToLog
150− :: (MonadIO m, Cli m)
163+ :: (MonadIO m, CliLog m)
151164 => InputStream (Severity, BSC.ByteString) -> m ()
152165streamToLog stream = fix $ \loop -> do
153166 liftIO (Streams.read stream) >>= \case
154167 Nothing -> return ()
155168 Just (sev, line) -> putLogRaw sev (T.decodeUtf8 line) >> loop
169+
170+reconstructCommand :: Process.CmdSpec -> Text
171+reconstructCommand (Process.ShellCommand str) = T.pack str
172+reconstructCommand (Process.RawCommand c as) = processToShellString c as
173+ where
174+ processToShellString cmd args = T.unwords $ map quoteAndEscape (cmd : args)
175+ quoteAndEscape x = "'" <> T.replace "'" "'\''" (T.pack x) <> "'"