-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathProcess.hs
182 lines (160 loc) · 6.19 KB
/
Process.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
-- | This module wraps over the API in @Procex.Core@ in a user-friendly way.
module Procex.Process (makeCmd, CmdException (..), run, pipeArgIn, pipeArgOut, pipeHIn, pipeHOut, pipeIn, pipeOut, pipeArgHIn, pipeArgHOut, captureFdsAsHandles, waitCmd) where
import Control.Concurrent.Async
import Control.Exception.Base
import Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString.Lazy as B
import Data.Char (ord)
import Data.Function
import Data.Tuple
import Procex.Core
import System.Exit (ExitCode (..))
import System.IO (Handle)
import System.Posix.ByteString
findM :: Monad m => (a -> m Bool) -> [a] -> m (Maybe a)
findM f (x : xs) =
f x >>= \b -> case b of
True -> pure $ Just x
False -> findM f xs
findM _ [] = pure Nothing
{- | A version of 'Procex.Core.makeCmd'' that resolves the path
according to PATH and passes through stdin, stdout and stderr (unless overrided).
-}
makeCmd :: ByteString -> Cmd
makeCmd path = unIOCmd $ do
fullpath :: ByteString <-
if B.any ((fromIntegral . ord $ '/') ==) path
then pure path
else do
pathvar <- B.fromStrict <$> getEnvDefault "PATH" ""
fullpath <- findM fileExist . fmap (\x -> B.toStrict $ x <> "/" <> path) . (<> ["/", "."]) . B.split (fromIntegral $ ord ':') $ pathvar
case fullpath of
Just p -> pure . B.fromStrict $ p
Nothing -> throwIO $ userError (show path <> " does not exist")
pure $ makeCmd' fullpath & passArg path & passFd (0, 0) & passFd (1, 1) & passFd (2, 2)
-- | Thrown when the return code of a command isn't 0.
newtype CmdException = CmdException ProcessStatus deriving stock (Show)
instance Exception CmdException where
displayException (CmdException status) = "Command failed: " <> show status
-- | Wait on a process status and raise 'CmdException' if it is a non-zero exit code.
waitCmd :: Async ProcessStatus -> IO ()
waitCmd status =
wait status >>= \case
Exited ExitSuccess -> pure ()
e -> throwIO (CmdException e)
{- | Runs a command synchronously. See also 'Procex.Core.run''.
'CmdException' will be thrown if the command fails.
-}
run :: Cmd -> IO ()
run cmd =
run' cmd >>= waitCmd
pipeFd' :: Bool -> Fd -> Cmd -> (Fd -> Cmd) -> Cmd
pipeFd' dir fd1 cmd1 cmd2 = unIOCmd $ do
bracketOnError ((if dir then swap else id) <$> createPipe) (\(x, y) -> closeFd x >> closeFd y) $ \(x, y) -> do
bracketOnError (run' $ cmd1 & passFd (fd1, x)) (async . cancel) $ \status1 -> do
pure $
flip postCmd (cmd2 y) $ \status2 -> do
closeFd x
closeFd y
_ <- async $ (either throwIO pure status2 >>= wait) `finally` cancel status1
pure ()
pipeArgFd :: Bool -> Fd -> Cmd -> Cmd -> Cmd
pipeArgFd dir fd cmd1 cmd2 = pipeFd' dir fd cmd1 (\y -> passArgFd y cmd2)
pipeFd :: Bool -> Fd -> Fd -> Cmd -> Cmd -> Cmd
pipeFd dir fd1 fd2 cmd1 cmd2 = pipeFd' dir fd1 cmd1 (\y -> passFd (fd2, y) cmd2)
{- | Pass an argument of the form @\/proc\/self\/fd\/\<n\>@ to the process,
where `n` is the reader end of a pipe which the command
writes to through the specified fd.
-}
pipeArgIn ::
-- | The fd the command will write to
Fd ->
-- | The command that will write to the fd
Cmd ->
-- | The command you're modifying
Cmd ->
Cmd
pipeArgIn = pipeArgFd True
{- | Pass an argument of the form @\/proc\/self\/fd\/\<n\>@ to the process,
where `n` is the writer end of a pipe which the command
reads from through the specified fd.
-}
pipeArgOut ::
-- | The fd the command will read from
Fd ->
-- | The command that will read from the fd
Cmd ->
-- | The command you're modifying
Cmd ->
Cmd
pipeArgOut = pipeArgFd False
-- | Pipes from the first command to the second command
pipeIn ::
-- | The writing end
Fd ->
-- | The reading end
Fd ->
-- | The writer command
Cmd ->
-- | The reader command
Cmd ->
Cmd
pipeIn = pipeFd True
-- | Pipes from the second command to the first command
pipeOut ::
-- | The reading end
Fd ->
-- | The writing end
Fd ->
-- | The reader command
Cmd ->
-- | The writer command
Cmd ->
Cmd
pipeOut = pipeFd False
pipeH' :: Bool -> (Async ProcessStatus -> Handle -> IO ()) -> (Fd -> Cmd) -> Cmd
pipeH' dir handler cmd = unIOCmd $
bracketOnError ((if dir then swap else id) <$> createPipe) (\(x, y) -> closeFd x >> closeFd y) $ \(x, y) -> do
pure $
flip postCmd (cmd y) $ \status -> do
closeFd y
case status of
Right status -> do
x <- fdToHandle x
a <- async $ handler status x
link a
pure ()
Left e -> do
closeFd x
throwIO e
pipeH :: Bool -> Fd -> (Async ProcessStatus -> Handle -> IO ()) -> Cmd -> Cmd
pipeH dir fdNew handler cmd = pipeH' dir handler (\fdOld -> passFd (fdNew, fdOld) cmd)
-- | Pipes from the handle to the fd.
pipeHIn :: Fd -> (Async ProcessStatus -> Handle -> IO ()) -> Cmd -> Cmd
pipeHIn = pipeH True
-- | Pipes from the fd to the handle.
pipeHOut :: Fd -> (Async ProcessStatus -> Handle -> IO ()) -> Cmd -> Cmd
pipeHOut = pipeH False
pipeArgH :: Bool -> (Async ProcessStatus -> Handle -> IO ()) -> Cmd -> Cmd
pipeArgH dir handler cmd = pipeH' dir handler (\fd -> passArgFd fd cmd)
{- | Pass an argument of the form @\/proc\/self\/fd\/\<n\>@ to the process,
where `n` is the reader end of a pipe where the writer end is passed
to a Haskell function.
-}
pipeArgHIn :: (Async ProcessStatus -> Handle -> IO ()) -> Cmd -> Cmd
pipeArgHIn = pipeArgH True
{- | Pass an argument of the form @\/proc\/self\/fd\/\<n\>@ to the process,
where `n` is the writer end of a pipe where the reader end is passed
to a Haskell function.
-}
pipeArgHOut :: (Async ProcessStatus -> Handle -> IO ()) -> Cmd -> Cmd
pipeArgHOut = pipeArgH False
-- | Captures the outputs to the specified fds.
captureFdsAsHandles :: [Fd] -> Cmd -> IO (Async ProcessStatus, [Handle])
captureFdsAsHandles fds cmd = do
fds <- traverse (\wnew -> createPipe >>= \(r, wold) -> pure (wnew, r, wold)) fds
flip onException (traverse (\(_wnew, r, wold) -> closeFd r >> closeFd wold) fds) $ do
status <- run' $ foldr (\(wnew, _r, wold) -> passFd (wnew, wold)) cmd fds -- TODO terminate eventually?
mapM_ (\(_wnew, _r, wold) -> closeFd wold) fds
handles <- traverse (\(_wnew, r, _wold) -> fdToHandle r) fds
pure $ (status, handles)