-
Notifications
You must be signed in to change notification settings - Fork 119
/
Process.hs
executable file
·238 lines (195 loc) · 9.09 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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
-- | A wrapping of createProcess to provide a more flexible interface.
module General.Process(
Buffer, newBuffer, readBuffer,
process, ProcessOpts(..), Source(..), Destination(..)
) where
import Control.Concurrent.Extra
import Control.DeepSeq
import Control.Exception.Extra as C
import Control.Monad.Extra
import Data.List.Extra
import Data.Maybe
import Foreign.C.Error
import System.Exit
import System.IO.Extra
import System.Info.Extra
import System.Process
import System.Time.Extra
import Data.Unique
import Data.IORef.Extra
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as LBS
import General.Extra
import Development.Shake.Internal.Errors
import GHC.IO.Exception (IOErrorType(..), IOException(..))
---------------------------------------------------------------------
-- BUFFER ABSTRACTION
data Buffer a = Buffer Unique (IORef [a])
instance Eq (Buffer a) where Buffer x _ == Buffer y _ = x == y
instance Ord (Buffer a) where compare (Buffer x _) (Buffer y _) = compare x y
newBuffer :: IO (Buffer a)
newBuffer = liftM2 Buffer newUnique (newIORef [])
addBuffer :: Buffer a -> a -> IO ()
addBuffer (Buffer _ ref) x = atomicModifyIORef_ ref (x:)
readBuffer :: Buffer a -> IO [a]
readBuffer (Buffer _ ref) = reverse <$> readIORef ref
---------------------------------------------------------------------
-- OPTIONS
data Source
= SrcFile FilePath
| SrcString String
| SrcBytes LBS.ByteString
| SrcInherit
data Destination
= DestEcho
| DestFile FilePath
| DestString (Buffer String)
| DestBytes (Buffer BS.ByteString)
deriving (Eq,Ord)
isDestString DestString{} = True; isDestString _ = False
isDestBytes DestBytes{} = True; isDestBytes _ = False
data ProcessOpts = ProcessOpts
{poCommand :: CmdSpec
,poCwd :: Maybe FilePath
,poEnv :: Maybe [(String, String)]
,poTimeout :: Maybe Double
,poStdin :: [Source]
,poStdout :: [Destination]
,poStderr :: [Destination]
,poAsync :: Bool
,poCloseFds :: Bool
,poGroup :: Bool
}
---------------------------------------------------------------------
-- IMPLEMENTATION
-- | If two buffers can be replaced by one and a copy, do that (only if they start empty)
optimiseBuffers :: ProcessOpts -> IO (ProcessOpts, IO ())
optimiseBuffers po@ProcessOpts{..} = pure (po{poStdout = nubOrd poStdout, poStderr = nubOrd poStderr}, pure ())
stdStream :: (FilePath -> Handle) -> [Destination] -> [Destination] -> StdStream
stdStream _ [DestEcho] _ = Inherit
stdStream file [DestFile x] other | other == [DestFile x] || DestFile x `notElem` other = UseHandle $ file x
stdStream _ _ _ = CreatePipe
stdIn :: (FilePath -> Handle) -> [Source] -> (StdStream, Handle -> IO ())
stdIn _ [SrcInherit] = (Inherit, const $ pure ())
stdIn file [SrcFile x] = (UseHandle $ file x, const $ pure ())
stdIn file src = (,) CreatePipe $ \h -> ignoreSigPipe $ do
forM_ src $ \case
SrcString x -> hPutStr h x
SrcBytes x -> LBS.hPutStr h x
SrcFile x -> LBS.hPutStr h =<< LBS.hGetContents (file x)
SrcInherit -> pure () -- Can't both inherit and set it
hClose h
ignoreSigPipe :: IO () -> IO ()
ignoreSigPipe = handleIO $ \e -> case e of
IOError {ioe_type=ResourceVanished, ioe_errno=Just ioe} | Errno ioe == ePIPE -> pure ()
_ -> throwIO e
withExceptions :: IO () -> IO a -> IO a
withExceptions stop go = do
bar <- newBarrier
v <- mask $ \unmask -> do
forkFinally (unmask go) $ signalBarrier bar
unmask (waitBarrier bar) `onException` do
forkIO stop
waitBarrier bar
either throwIO pure v
withTimeout :: Maybe Double -> IO () -> IO a -> IO a
withTimeout Nothing _ go = go
withTimeout (Just s) stop go = bracket (forkIO $ sleep s >> stop) killThread $ const go
cmdSpec :: CmdSpec -> CreateProcess
cmdSpec (ShellCommand x) = shell x
cmdSpec (RawCommand x xs) = proc x xs
forkWait :: IO a -> IO (IO a)
forkWait a = do
res <- newEmptyMVar
_ <- mask $ \restore -> forkIO $ try_ (restore a) >>= putMVar res
pure $ takeMVar res >>= either throwIO pure
abort :: Bool -> ProcessHandle -> IO ()
abort poGroup pid = do
when poGroup $ do
interruptProcessGroupOf pid
sleep 3 -- give the process a few seconds grace period to die nicely
terminateProcess pid
withFiles :: IOMode -> [FilePath] -> ((FilePath -> Handle) -> IO a) -> IO a
withFiles mode files act = withs (map (`withFile` mode) files) $ \handles ->
act $ \x -> fromJust $ lookup x $ zipExact files handles
-- General approach taken from readProcessWithExitCode
process :: ProcessOpts -> IO (ProcessHandle, ExitCode)
process po = do
(ProcessOpts{..}, flushBuffers) <- optimiseBuffers po
let outFiles = nubOrd [x | DestFile x <- poStdout ++ poStderr]
let inFiles = nubOrd [x | SrcFile x <- poStdin]
withFiles WriteMode outFiles $ \outHandle -> withFiles ReadMode inFiles $ \inHandle -> do
let cp = (cmdSpec poCommand){cwd = poCwd, env = poEnv, create_group = poGroup, close_fds = poCloseFds
,std_in = fst $ stdIn inHandle poStdin
,std_out = stdStream outHandle poStdout poStderr, std_err = stdStream outHandle poStderr poStdout}
withCreateProcessCompat cp $ \inh outh errh pid ->
withTimeout poTimeout (abort poGroup pid) $ withExceptions (abort poGroup pid) $ do
let streams = [(outh, stdout, poStdout) | Just outh <- [outh], CreatePipe <- [std_out cp]] ++
[(errh, stderr, poStderr) | Just errh <- [errh], CreatePipe <- [std_err cp]]
wait <- forM streams $ \(h, hh, dest) -> do
-- no point tying the streams together if one is being streamed directly
let isTied = not (poStdout `disjoint` poStderr) && length streams == 2
let isBinary = any isDestBytes dest || not (any isDestString dest)
when isTied $ hSetBuffering h LineBuffering
when (DestEcho `elem` dest) $ do
buf <- hGetBuffering hh
case buf of
BlockBuffering{} -> pure ()
_ -> hSetBuffering h buf
if isBinary then do
hSetBinaryMode h True
dest<- pure $ flip map dest $ \case
DestEcho -> BS.hPut hh
DestFile x -> BS.hPut (outHandle x)
DestString x -> addBuffer x . (if isWindows then replace "\r\n" "\n" else id) . BS.unpack
DestBytes x -> addBuffer x
forkWait $ whileM $ do
src <- BS.hGetSome h 4096
mapM_ ($ src) dest
notM $ hIsEOF h
else if isTied then do
dest<- pure $ flip map dest $ \case
DestEcho -> hPutStrLn hh
DestFile x -> hPutStrLn (outHandle x)
DestString x -> addBuffer x . (++ "\n")
DestBytes{} -> throwImpure $ errorInternal "Not reachable due to isBinary condition"
forkWait $ whileM $
ifM (hIsEOF h) (pure False) $ do
src <- hGetLine h
mapM_ ($ src) dest
pure True
else do
src <- hGetContents h
wait1 <- forkWait $ C.evaluate $ rnf src
waits <- forM dest $ \case
DestEcho -> forkWait $ hPutStr hh src
DestFile x -> forkWait $ hPutStr (outHandle x) src
DestString x -> do addBuffer x src; pure $ pure ()
DestBytes{} -> throwImpure $ errorInternal "Not reachable due to isBinary condition"
pure $ sequence_ $ wait1 : waits
whenJust inh $ snd $ stdIn inHandle poStdin
if poAsync then
pure (pid, ExitSuccess)
else do
sequence_ wait
flushBuffers
res <- waitForProcess pid
whenJust outh hClose
whenJust errh hClose
pure (pid, res)
---------------------------------------------------------------------
-- COMPATIBILITY
-- available in process-1.4.3.0, GHC ??? (Nov 2015)
-- logic copied directly (apart from Ctrl-C handling magic using internal pieces)
withCreateProcessCompat :: CreateProcess -> (Maybe Handle -> Maybe Handle -> Maybe Handle -> ProcessHandle -> IO a) -> IO a
withCreateProcessCompat cp act = bracketOnError (createProcess cp) cleanup
(\(m_in, m_out, m_err, ph) -> act m_in m_out m_err ph)
where
cleanup (inh, outh, errh, pid) = do
terminateProcess pid
whenJust inh $ ignoreSigPipe . hClose
whenJust outh hClose
whenJust errh hClose
forkIO $ void $ waitForProcess pid