Skip to content

Commit 22bad35

Browse files
committed
Switch to checked exit code functions
1 parent 7ef0877 commit 22bad35

File tree

2 files changed

+115
-69
lines changed

2 files changed

+115
-69
lines changed

src/System/Process/Typed.hs

Lines changed: 113 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ module System.Process.Typed
3636
, setChildGroup
3737
, setChildUser
3838
#endif
39-
, setCheckExitCode
4039

4140
-- * Stream specs
4241
, mkStreamSpec
@@ -54,7 +53,9 @@ module System.Process.Typed
5453
, startProcess
5554
, stopProcess
5655
, withProcess
56+
, withProcess_
5757
, readProcess
58+
, readProcess_
5859
, runProcess
5960
, runProcess_
6061

@@ -63,6 +64,8 @@ module System.Process.Typed
6364
-- ** Process exit code
6465
, waitExitCode
6566
, waitExitCodeSTM
67+
, getExitCode
68+
, getExitCodeSTM
6669
, checkExitCode
6770
, checkExitCodeSTM
6871

@@ -149,9 +152,30 @@ data ProcessConfig stdin stdout stderr = ProcessConfig
149152
, pcChildGroup :: !(Maybe GroupID)
150153
, pcChildUser :: !(Maybe UserID)
151154
#endif
152-
153-
, pcCheckExitCode :: !Bool
154155
}
156+
instance Show (ProcessConfig stdin stdout stderr) where
157+
show pc = concat
158+
[ case pcCmdSpec pc of
159+
P.ShellCommand s -> "Shell command: " ++ s
160+
P.RawCommand x xs -> "Raw command: " ++ unwords (map escape (x:xs))
161+
, "\n"
162+
, case pcWorkingDir pc of
163+
Nothing -> ""
164+
Just wd -> concat
165+
[ "Run from: "
166+
, wd
167+
, "\n"
168+
]
169+
, case pcEnv pc of
170+
Nothing -> ""
171+
Just e -> unlines
172+
$ "Modified environment:"
173+
: map (\(k, v) -> concat [k, "=", v]) e
174+
]
175+
where
176+
escape x
177+
| any (`elem` " \\\"'") x = show x
178+
| otherwise = x
155179
instance (stdin ~ (), stdout ~ (), stderr ~ ())
156180
=> IsString (ProcessConfig stdin stdout stderr) where
157181
fromString s
@@ -172,7 +196,7 @@ data StreamType = STInput | STOutput
172196
-- @since 0.1.0.0
173197
data StreamSpec (streamType :: StreamType) a = StreamSpec
174198
{ ssStream :: !P.StdStream
175-
, ssCreate :: !(Maybe Handle -> Cleanup a)
199+
, ssCreate :: !(ProcessConfig () () () -> Maybe Handle -> Cleanup a)
176200
}
177201
deriving Functor
178202

@@ -202,13 +226,16 @@ instance Applicative Cleanup where
202226
--
203227
-- @since 0.1.0.0
204228
data Process stdin stdout stderr = Process
205-
{ pCleanup :: !(IO ())
229+
{ pConfig :: !(ProcessConfig () () ())
230+
, pCleanup :: !(IO ())
206231
, pStdin :: !stdin
207232
, pStdout :: !stdout
208233
, pStderr :: !stderr
209234
, pHandle :: !P.ProcessHandle
210235
, pExitCode :: !(TMVar ExitCode)
211236
}
237+
instance Show (Process stdin stdout stderr) where
238+
show p = "Running process: " ++ show (pConfig p)
212239

213240
-- | Internal helper
214241
defaultProcessConfig :: ProcessConfig () () ()
@@ -233,8 +260,6 @@ defaultProcessConfig = ProcessConfig
233260
, pcChildGroup = Nothing
234261
, pcChildUser = Nothing
235262
#endif
236-
237-
, pcCheckExitCode = False
238263
}
239264

240265
-- | Create a 'ProcessConfig' from the given command and arguments.
@@ -409,27 +434,6 @@ setChildUser
409434
setChildUser x pc = pc { pcChildUser = Just x }
410435
#endif
411436

412-
-- | Should we throw an exception when the process exits with a
413-
-- non-success code?
414-
--
415-
-- If set to 'True', then when 'stopProcess' is called - either
416-
-- directly or via 'withProcess' or other wrappers - the processes
417-
-- exit code will be checked. Any exit code besides 'ExitSuccess' will
418-
-- result in an 'ExitCodeException' being thrown.
419-
--
420-
-- Default: 'False'
421-
--
422-
-- @since 0.1.0.0
423-
setCheckExitCode :: Bool
424-
-> ProcessConfig stdin stdout stderr
425-
-> ProcessConfig stdin stdout stderr
426-
setCheckExitCode x p = p { pcCheckExitCode = x }
427-
428-
-- TODO: Instead of having this setting, we could consider just having
429-
-- alternatives to readProcess, runProcess, etc, that check the exit
430-
-- code. This could actually be a really nice convention: readProcess
431-
-- does not check, readProcess_ or readProcessCheck does.
432-
433437
-- | Create a new 'StreamSpec' from the given 'P.StdStream' and a
434438
-- helper function. This function:
435439
--
@@ -442,25 +446,25 @@ setCheckExitCode x p = p { pcCheckExitCode = x }
442446
--
443447
-- @since 0.1.0.0
444448
mkStreamSpec :: P.StdStream
445-
-> (Maybe Handle -> IO (a, IO ()))
449+
-> (ProcessConfig () () () -> Maybe Handle -> IO (a, IO ()))
446450
-> StreamSpec streamType a
447-
mkStreamSpec ss f = StreamSpec ss (Cleanup . f)
451+
mkStreamSpec ss f = StreamSpec ss (\pc mh -> Cleanup (f pc mh))
448452

449453
-- | A stream spec which simply inherits the stream of the parent
450454
-- process.
451455
--
452456
-- @since 0.1.0.0
453457
inherit :: StreamSpec anyStreamType ()
454-
inherit = mkStreamSpec P.Inherit (\Nothing -> pure ((), return ()))
458+
inherit = mkStreamSpec P.Inherit (\_ Nothing -> pure ((), return ()))
455459

456460
-- | A stream spec which will close the stream for the child process.
457461
--
458462
-- @since 0.1.0.0
459463
closed :: StreamSpec anyStreamType ()
460464
#if MIN_VERSION_process(1, 4, 0)
461-
closed = mkStreamSpec P.NoStream (\Nothing -> pure ((), return ()))
465+
closed = mkStreamSpec P.NoStream (\_ Nothing -> pure ((), return ()))
462466
#else
463-
closed = mkStreamSpec P.CreatePipe (\(Just h) -> (((), return ()) <$ hClose h))
467+
closed = mkStreamSpec P.CreatePipe (\_ (Just h) -> (((), return ()) <$ hClose h))
464468
#endif
465469

466470
-- | An input stream spec which sets the input to the given
@@ -469,7 +473,7 @@ closed = mkStreamSpec P.CreatePipe (\(Just h) -> (((), return ()) <$ hClose h))
469473
--
470474
-- @since 0.1.0.0
471475
byteStringInput :: L.ByteString -> StreamSpec 'STInput ()
472-
byteStringInput lbs = StreamSpec P.CreatePipe $ \(Just h) -> Cleanup $ do
476+
byteStringInput lbs = mkStreamSpec P.CreatePipe $ \_ (Just h) -> do
473477
void $ async $ do
474478
L.hPut h lbs
475479
hClose h
@@ -489,7 +493,7 @@ byteStringInput lbs = StreamSpec P.CreatePipe $ \(Just h) -> Cleanup $ do
489493
--
490494
-- @since 0.1.0.0
491495
byteStringOutput :: StreamSpec 'STOutput (STM (Either ByteStringOutputException L.ByteString))
492-
byteStringOutput = StreamSpec P.CreatePipe $ \(Just h) -> Cleanup $ do
496+
byteStringOutput = mkStreamSpec P.CreatePipe $ \pc (Just h) -> do
493497
mvar <- newEmptyTMVarIO
494498

495499
void $ async $ do
@@ -499,7 +503,7 @@ byteStringOutput = StreamSpec P.CreatePipe $ \(Just h) -> Cleanup $ do
499503
then atomically $ putTMVar mvar $ Right $ L.fromChunks $ front []
500504
else loop $ front . (bs:)
501505
loop id `catch` \e -> do
502-
atomically $ void $ tryPutTMVar mvar $ Left $ ByteStringOutputException e
506+
atomically $ void $ tryPutTMVar mvar $ Left $ ByteStringOutputException e pc
503507
throwIO e
504508

505509
return (readTMVar mvar, hClose h)
@@ -509,7 +513,7 @@ byteStringOutput = StreamSpec P.CreatePipe $ \(Just h) -> Cleanup $ do
509513
--
510514
-- @since 0.1.0.0
511515
createPipe :: StreamSpec anyStreamType Handle
512-
createPipe = StreamSpec P.CreatePipe $ \(Just h) -> Cleanup $ return (h, hClose h)
516+
createPipe = mkStreamSpec P.CreatePipe $ \_ (Just h) -> return (h, hClose h)
513517

514518
-- | Use the provided 'Handle' for the child process, and when the
515519
-- process exits, do /not/ close it. This is useful if, for example,
@@ -518,15 +522,15 @@ createPipe = StreamSpec P.CreatePipe $ \(Just h) -> Cleanup $ return (h, hClose
518522
--
519523
-- @since 0.1.0.0
520524
useHandleOpen :: Handle -> StreamSpec anyStreamType ()
521-
useHandleOpen h = StreamSpec (P.UseHandle h) $ \Nothing -> Cleanup $ return ((), return ())
525+
useHandleOpen h = mkStreamSpec (P.UseHandle h) $ \_ Nothing -> return ((), return ())
522526

523527
-- | Use the provided 'Handle' for the child process, and when the
524528
-- process exits, close it. If you have no reason to keep the 'Handle'
525529
-- open, you should use this over 'useHandleOpen'.
526530
--
527531
-- @since 0.1.0.0
528532
useHandleClose :: Handle -> StreamSpec anyStreamType ()
529-
useHandleClose h = StreamSpec (P.UseHandle h) $ \Nothing -> Cleanup $ return ((), hClose h)
533+
useHandleClose h = mkStreamSpec (P.UseHandle h) $ \_ Nothing -> return ((), hClose h)
530534

531535
-- | Provide input to a process by writing to a conduit.
532536
--
@@ -553,7 +557,7 @@ source =
553557
startProcess :: MonadIO m
554558
=> ProcessConfig stdin stdout stderr
555559
-> m (Process stdin stdout stderr)
556-
startProcess ProcessConfig {..} = liftIO $ do
560+
startProcess pConfig'@ProcessConfig {..} = liftIO $ do
557561
let cp0 =
558562
case pcCmdSpec of
559563
P.ShellCommand cmd -> P.shell cmd
@@ -584,16 +588,16 @@ startProcess ProcessConfig {..} = liftIO $ do
584588
(minH, moutH, merrH, pHandle) <- P.createProcess_ "startProcess" cp
585589

586590
((pStdin, pStdout, pStderr), pCleanup1) <- runCleanup $ (,,)
587-
<$> ssCreate pcStdin minH
588-
<*> ssCreate pcStdout moutH
589-
<*> ssCreate pcStderr merrH
591+
<$> ssCreate pcStdin pConfig minH
592+
<*> ssCreate pcStdout pConfig moutH
593+
<*> ssCreate pcStderr pConfig merrH
590594

591595
pExitCode <- newEmptyTMVarIO
592596
void $ async $ do
593597
ec <- P.waitForProcess pHandle
594598
atomically $ putTMVar pExitCode ec
595599

596-
let pCleanup2 = pCleanup1 `finally` do
600+
let pCleanup = pCleanup1 `finally` do
597601
mec <- atomically $ tryReadTMVar pExitCode
598602
case mec of
599603
Nothing -> do
@@ -602,24 +606,16 @@ startProcess ProcessConfig {..} = liftIO $ do
602606
-- a SIGKILL on Unix?
603607
void $ atomically $ readTMVar pExitCode
604608
Just _ -> return ()
605-
pCleanup
606-
| pcCheckExitCode = do
607-
eres <- try pCleanup2
608-
ec <- atomically $ readTMVar pExitCode
609-
case (ec, eres) of
610-
(ExitSuccess, Right ()) -> return ()
611-
(ExitSuccess, Left e) -> throwIO e
612-
_ -> throwIO $ ExitCodeException ec $ either Just (const Nothing) eres
613-
| otherwise = pCleanup2
614609

615610
return Process {..}
611+
where
612+
pConfig = clearStreams pConfig'
616613

617614
-- | Close a process and release any resources acquired. This will
618615
-- ensure 'P.terminateProcess' is called, wait for the process to
619616
-- actually exit, and then close out resources allocated for the
620-
-- streams. In the event of any cleanup exceptions being thrown, or if
621-
-- a non-success exit code was received and 'setCheckExitCode' was
622-
-- used, this will throw an exception.
617+
-- streams. In the event of any cleanup exceptions being thrown this
618+
-- will throw an exception.
623619
--
624620
-- @since 0.1.0.0
625621
stopProcess :: MonadIO m
@@ -637,6 +633,17 @@ withProcess :: (MonadIO m, C.MonadMask m)
637633
-> m a
638634
withProcess config = C.bracket (startProcess config) stopProcess
639635

636+
-- | Same as 'withProcess', but also calls 'checkExitCode'
637+
--
638+
-- @since 0.1.0.0
639+
withProcess_ :: (MonadIO m, C.MonadMask m)
640+
=> ProcessConfig stdin stdout stderr
641+
-> (Process stdin stdout stderr -> m a)
642+
-> m a
643+
withProcess_ config = C.bracket
644+
(startProcess config)
645+
(\p -> stopProcess p `finally` checkExitCode p)
646+
640647
-- | Run a process, capture its standard output and error as a
641648
-- 'L.ByteString', wait for it to complete, and then return its exit
642649
-- code, output, and error.
@@ -657,6 +664,22 @@ readProcess pc =
657664
pc' = setStdout byteStringOutput
658665
$ setStderr byteStringOutput pc
659666

667+
-- | Same as 'readProcess', but instead of returning the 'ExitCode',
668+
-- checks it with 'checkExitCode'.
669+
--
670+
-- @since 0.1.0.0
671+
readProcess_ :: MonadIO m
672+
=> ProcessConfig stdin stdoutIgnored stderrIgnored
673+
-> m (L.ByteString, L.ByteString)
674+
readProcess_ pc =
675+
liftIO $ withProcess pc' $ \p -> atomically $ (,)
676+
<$> (checkExitCodeSTM p
677+
*> (getStdout p >>= either throwSTM return))
678+
<*> (getStderr p >>= either throwSTM return)
679+
where
680+
pc' = setStdout byteStringOutput
681+
$ setStderr byteStringOutput pc
682+
660683
-- | Run the given process, wait for it to exit, and returns its
661684
-- 'ExitCode'.
662685
--
@@ -672,7 +695,7 @@ runProcess pc = liftIO $ withProcess pc waitExitCode
672695
runProcess_ :: MonadIO m
673696
=> ProcessConfig stdin stdout stderr
674697
-> m ()
675-
runProcess_ = liftIO . void . runProcess
698+
runProcess_ pc = liftIO $ withProcess pc checkExitCode
676699

677700
-- | Wait for the process to exit and then return its 'ExitCode'.
678701
--
@@ -689,14 +712,39 @@ waitExitCodeSTM = readTMVar . pExitCode
689712
-- | Check if a process has exited and, if so, return its 'ExitCode'.
690713
--
691714
-- @since 0.1.0.0
692-
checkExitCode :: MonadIO m => Process stdin stdout stderr -> m (Maybe ExitCode)
715+
getExitCode :: MonadIO m => Process stdin stdout stderr -> m (Maybe ExitCode)
716+
getExitCode = liftIO . atomically . getExitCodeSTM
717+
718+
-- | Same as 'getExitCode', but in 'STM'.
719+
--
720+
-- @since 0.1.0.0
721+
getExitCodeSTM :: Process stdin stdout stderr -> STM (Maybe ExitCode)
722+
getExitCodeSTM = tryReadTMVar . pExitCode
723+
724+
-- | Wait for a process to exit, and ensure that it exited
725+
-- successfully. If not, throws an 'ExitCodeException'.
726+
--
727+
-- @since 0.1.0.0
728+
checkExitCode :: MonadIO m => Process stdin stdout stderr -> m ()
693729
checkExitCode = liftIO . atomically . checkExitCodeSTM
694730

695731
-- | Same as 'checkExitCode', but in 'STM'.
696732
--
697733
-- @since 0.1.0.0
698-
checkExitCodeSTM :: Process stdin stdout stderr -> STM (Maybe ExitCode)
699-
checkExitCodeSTM = tryReadTMVar . pExitCode
734+
checkExitCodeSTM :: Process stdin stdout stderr -> STM ()
735+
checkExitCodeSTM p = do
736+
ec <- readTMVar (pExitCode p)
737+
case ec of
738+
ExitSuccess -> return ()
739+
_ -> throwSTM (ExitCodeException ec (clearStreams (pConfig p)))
740+
741+
-- | Internal
742+
clearStreams :: ProcessConfig stdin stdout stderr -> ProcessConfig () () ()
743+
clearStreams pc = pc
744+
{ pcStdin = inherit
745+
, pcStdout = inherit
746+
, pcStderr = inherit
747+
}
700748

701749
-- | Get the child's standard input stream value.
702750
--
@@ -716,20 +764,19 @@ getStdout = pStdout
716764
getStderr :: Process stdin stdout stderr -> stderr
717765
getStderr = pStderr
718766

719-
-- | Exit code generated by 'stopProcess' when 'setCheckExitCode' is
720-
-- 'True' and a process exits with a non-success code. Contains the
721-
-- non-success code, and if any other exceptions occur during cleanup,
722-
-- that exception.
767+
-- | Exception thrown by 'checkExitCode' in the event of a non-success
768+
-- exit code. Note that 'checkExitCode' is called by other functions
769+
-- as well, like 'runProcess_' or 'readProcess_'.
723770
--
724771
-- @since 0.1.0.0
725-
data ExitCodeException = ExitCodeException ExitCode (Maybe SomeException)
772+
data ExitCodeException = ExitCodeException ExitCode (ProcessConfig () () ())
726773
deriving (Show, Typeable)
727774
instance Exception ExitCodeException
728775

729776
-- | Wrapper for when an exception is thrown when reading from a child
730777
-- process, used by 'byteStringOutput'.
731778
--
732779
-- @since 0.1.0.0
733-
newtype ByteStringOutputException = ByteStringOutputException SomeException
780+
data ByteStringOutputException = ByteStringOutputException SomeException (ProcessConfig () () ())
734781
deriving (Show, Typeable)
735782
instance Exception ByteStringOutputException

test/System/Process/TypedSpec.hs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,8 @@ spec = do
7272
res <- runProcess "false"
7373
res `shouldBe` ExitFailure 1
7474

75-
it "checked exit code" $ do
76-
runProcess_ (setCheckExitCode True "false")
77-
`shouldThrow` \ExitCodeException{} -> True
75+
it "checked exit code" $
76+
runProcess_ "false" `shouldThrow` \ExitCodeException{} -> True
7877

7978
it "async" $ withSystemTempFile "httpbin" $ \fp h -> do
8079
bss <- withProcess (setStdin sink $ setStdout source "base64") $ \p ->

0 commit comments

Comments
 (0)