Skip to content

Commit e6fc676

Browse files
committed
Ensure that waitForProcess is never called more than once (fixes #69)
1 parent bbf07a9 commit e6fc676

File tree

4 files changed

+43
-37
lines changed

4 files changed

+43
-37
lines changed

ChangeLog.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# ChangeLog for typed-process
22

3+
## 0.2.12.0
4+
5+
* Ensure that `waitForProcess` is never called more than once
6+
[#70](https://github.com/fpco/typed-process/pull/70)
7+
38
## 0.2.11.0
49

510
* Expose more from `System.Process.Typed.Internal`

package.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: typed-process
2-
version: 0.2.11.0
2+
version: 0.2.12.0
33
synopsis: Run external processes, with strong typing of streams
44
description: Please see the tutorial at <https://github.com/fpco/typed-process#readme>
55
category: System

src/System/Process/Typed.hs

+27-36
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{-# LANGUAGE CPP #-}
2+
{-# LANGUAGE LambdaCase #-}
23
{-# LANGUAGE TypeFamilies #-}
34
{-# LANGUAGE DeriveDataTypeable #-}
45
{-# LANGUAGE RecordWildCards #-}
@@ -130,13 +131,15 @@ module System.Process.Typed
130131
) where
131132

132133
import Control.Exception hiding (bracket, finally)
134+
import Control.Monad ((>=>), guard)
133135
import Control.Monad.IO.Class
134136
import qualified System.Process as P
135137
import System.IO (hClose)
136138
import System.IO.Error (isPermissionError)
137139
import Control.Concurrent (threadDelay)
138-
import Control.Concurrent.Async (asyncWithUnmask, cancel, waitCatch)
139-
import Control.Concurrent.STM (newEmptyTMVarIO, atomically, putTMVar, TMVar, readTMVar, tryReadTMVar, STM, tryPutTMVar, throwSTM, catchSTM)
140+
import Control.Concurrent.Async (asyncWithUnmask)
141+
import qualified Control.Concurrent.Async as Async
142+
import Control.Concurrent.STM (newEmptyTMVarIO, atomically, putTMVar, TMVar, readTMVar, tryReadTMVar, STM, throwSTM, catchSTM)
140143
import System.Exit (ExitCode (ExitSuccess, ExitFailure))
141144
import System.Process.Typed.Internal
142145
import qualified Data.ByteString.Lazy as L
@@ -239,51 +242,39 @@ startProcess pConfig'@ProcessConfig {..} = liftIO $ do
239242
atomically $ putTMVar pExitCode ec
240243
return ec
241244

245+
let waitForProcess = Async.wait waitingThread :: IO ExitCode
242246
let pCleanup = pCleanup1 `finally` do
243-
-- First: stop calling waitForProcess, so that we can
244-
-- avoid race conditions where the process is removed from
245-
-- the system process table while we're trying to
246-
-- terminate it.
247-
cancel waitingThread
248-
249-
-- Now check if the process had already exited
250-
eec <- waitCatch waitingThread
251-
252-
case eec of
247+
_ :: ExitCode <- Async.poll waitingThread >>= \ case
253248
-- Process already exited, nothing to do
254-
Right _ec -> return ()
249+
Just r -> either throwIO return r
255250

256251
-- Process didn't exit yet, let's terminate it and
257252
-- then call waitForProcess ourselves
258-
Left _ -> do
253+
Nothing -> do
259254
terminateProcess pHandle
260-
ec <- P.waitForProcess pHandle
261-
success <- atomically $ tryPutTMVar pExitCode ec
262-
evaluate $ assert success ()
255+
waitForProcess
256+
return ()
263257

264258
return Process {..}
265259
where
266260
pConfig = clearStreams pConfig'
267261

268-
terminateProcess pHandle = do
269-
eres <- try $ P.terminateProcess pHandle
270-
case eres of
271-
Left e
272-
-- On Windows, with the single-threaded runtime, it
273-
-- seems that if a process has already exited, the
274-
-- call to terminateProcess will fail with a
275-
-- permission denied error. To work around this, we
276-
-- catch this exception and then immediately
277-
-- waitForProcess. There's a chance that there may be
278-
-- other reasons for this permission error to appear,
279-
-- in which case this code may allow us to wait too
280-
-- long for a child process instead of erroring out.
281-
-- Recommendation: always use the multi-threaded
282-
-- runtime!
283-
| isPermissionError e && not multiThreadedRuntime && isWindows ->
284-
pure ()
285-
| otherwise -> throwIO e
286-
Right () -> pure ()
262+
terminateProcess :: P.ProcessHandle -> IO ()
263+
terminateProcess p = do
264+
-- On Windows, with the single-threaded runtime, it seems that if a
265+
-- process has already exited, the call to terminateProcess will fail
266+
-- with a permission denied error. To work around this, we ignore this
267+
-- exception. There's a chance that there may be other reasons for this
268+
-- permission error to appear, in which case this code may allow us to
269+
-- wait too long for a child process instead of erroring out on a
270+
-- subsequent call to `waitForProcess`.
271+
-- Recommendation: always use the multi-threaded runtime!
272+
ignorePermissionErrorOnSingleThreadedWindows $ P.terminateProcess p
273+
274+
ignorePermissionErrorOnSingleThreadedWindows :: IO () -> IO ()
275+
ignorePermissionErrorOnSingleThreadedWindows = tryJust (guard . p) >=> either return return
276+
where
277+
p e = isPermissionError e && not multiThreadedRuntime && isWindows
287278

288279
foreign import ccall unsafe "rtsSupportsBoundThreads"
289280
multiThreadedRuntime :: Bool

test/System/Process/TypedSpec.hs

+10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module System.Process.TypedSpec (spec) where
55
import System.Process.Typed
66
import System.Process.Typed.Internal
77
import System.IO
8+
import Control.Exception
89
import Control.Concurrent.Async (Concurrently (..))
910
import Control.Concurrent.STM (atomically)
1011
import Test.Hspec
@@ -170,3 +171,12 @@ spec = do
170171
it "empty param are showed" $
171172
let expected = "Raw command: podman exec --detach-keys \"\" ctx bash\n"
172173
in show (proc "podman" ["exec", "--detach-keys", "", "ctx", "bash"]) `shouldBe` expected
174+
175+
describe "stopProcess" $ do
176+
it "never calls waitForProcess more than once (fix for #69)" $ do
177+
-- https://github.com/fpco/typed-process/issues/70
178+
let config = setStdout createPipe (proc "echo" ["foo"])
179+
withProcessWait config $ \p -> do
180+
_ <- S.hGetContents (getStdout p)
181+
throwIO DivideByZero
182+
`shouldThrow` (== DivideByZero)

0 commit comments

Comments
 (0)