shell-conduit-4.7.0: src/Data/Conduit/Shell/Process.hs
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE DeriveDataTypeable #-}
-- | Reading from the process.
module Data.Conduit.Shell.Process
(-- * Running scripts
run
-- * Conduit types
,text
,bytes
-- * General conduits
,conduit
,conduitEither
-- * Running processes
,Data.Conduit.Shell.Process.shell
,Data.Conduit.Shell.Process.proc
,($|)
,Segment
,ProcessException(..)
,ToChunk(..)
,tryS
)
where
import Control.Applicative
import Control.Concurrent.Async
import Control.Exception
import Control.Monad
import Control.Monad.IO.Class
import Data.ByteString (ByteString)
import qualified Data.ByteString as S
import Data.Conduit
import Data.Conduit.Binary
import qualified Data.Conduit.List as CL
import Data.Conduit.Text (encodeUtf8, decodeUtf8)
import Data.Text (Text)
import Data.Typeable
import System.Exit
import System.IO
import System.Posix.IO (createPipe, fdToHandle)
import System.Process hiding (createPipe)
-- | A pipeable segment. Either a conduit or a process.
data Segment r
= SegmentConduit (ConduitM ByteString (Either ByteString ByteString) IO r)
| SegmentProcess (Handles -> IO r)
instance Monad Segment where
return = SegmentConduit . return
SegmentConduit c >>= f =
SegmentProcess (conduitToProcess c) >>=
f
SegmentProcess f >>= g =
SegmentProcess
(\handles ->
do x <- f handles
case g x of
SegmentConduit c ->
conduitToProcess c handles
SegmentProcess p -> p handles)
instance Functor Segment where
fmap = liftM
instance Applicative Segment where
(<*>) = ap; pure = return
instance Alternative Segment where
this <|> that =
do ex <- tryS this
case ex of
Right x -> pure x
Left (_ :: ProcessException) -> that
empty = throw ProcessEmpty
-- | Try something in a segment.
tryS :: Exception e => Segment r -> Segment (Either e r)
tryS s =
case s of
SegmentConduit c -> SegmentConduit (tryC c)
SegmentProcess f -> SegmentProcess (\h -> try (f h))
instance MonadIO Segment where
liftIO x = SegmentProcess (const x)
-- | Process handles: @stdin@, @stdout@, @stderr@
data Handles =
Handles Handle
Handle
Handle
-- | Process running exception.
data ProcessException
= ProcessException CreateProcess
ExitCode
| ProcessEmpty
deriving (Typeable)
instance Exception ProcessException
instance Show ProcessException where
show ProcessEmpty = "empty process"
show (ProcessException cp ec) =
concat
[ "The "
, case cmdspec cp of
ShellCommand s -> "shell command " ++ show s
RawCommand f args -> "raw command: " ++ unwords (f : map show args)
, " returned a failure exit code: "
, case ec of
ExitFailure i -> show i
_ -> show ec
]
-- | Convert a process or a conduit to a segment.
class ToSegment a where
type SegmentResult a
toSegment :: a -> Segment (SegmentResult a)
instance ToSegment (Segment r) where
type SegmentResult (Segment r) = r
toSegment = id
instance (a ~ ByteString, ToChunk b, m ~ IO) =>
ToSegment (ConduitT a b m r) where
type SegmentResult (ConduitT a b m r) = r
toSegment f = SegmentConduit (f `fuseUpstream` CL.map toChunk)
instance ToSegment CreateProcess where
type SegmentResult CreateProcess = ()
toSegment = liftProcess
-- | Used to allow outputting stdout or stderr.
class ToChunk a where
toChunk :: a -> Either ByteString ByteString
instance ToChunk ByteString where
toChunk = Left
instance ToChunk (Either ByteString ByteString) where
toChunk = id
-- | Run a shell command.
shell :: String -> Segment ()
shell = liftProcess . System.Process.shell
-- | Run a process command.
proc :: String -> [String] -> Segment ()
proc name args = liftProcess (System.Process.proc name args)
-- | Run a segment.
run :: Segment r -> IO r
run (SegmentConduit c) = run (SegmentProcess (conduitToProcess c))
run (SegmentProcess p) = p (Handles stdin stdout stderr)
-- | Fuse two segments (either processes or conduits).
($|) :: Segment () -> Segment b -> Segment b
x $| y = x `fuseSegment` y
infixl 0 $|
-- | Work on the stream as 'Text' values from UTF-8.
text
:: (r ~ (), m ~ IO)
=> ConduitT Text Text m r -> Segment r
text conduit' = bytes (decodeUtf8 .| conduit' .| encodeUtf8)
-- | Lift a conduit into a segment.
bytes
:: (a ~ ByteString, m ~ IO)
=> ConduitT a ByteString m r -> Segment r
bytes f = SegmentConduit (f `fuseUpstream` CL.map toChunk)
-- | Lift a conduit into a segment.
conduit
:: (a ~ ByteString, m ~ IO)
=> ConduitT a ByteString m r -> Segment r
conduit f = SegmentConduit (f `fuseUpstream` CL.map toChunk)
-- | Lift a conduit into a segment, which can yield stderr.
conduitEither
:: (a ~ ByteString, m ~ IO)
=> ConduitT a (Either ByteString ByteString) m r -> Segment r
conduitEither f = SegmentConduit (f `fuseUpstream` CL.map toChunk)
-- | Lift a process into a segment.
liftProcess :: CreateProcess -> Segment ()
liftProcess cp =
SegmentProcess
(\(Handles inh outh errh) ->
let config =
cp
{ std_in = UseHandle inh
, std_out = UseHandle outh
, std_err = UseHandle errh
, close_fds = True
}
in do (Nothing, Nothing, Nothing, ph) <- createProcess_ "liftProcess" config
ec <- waitForProcess ph
case ec of
ExitSuccess -> return ()
_ -> throwIO (ProcessException cp ec))
-- | Convert a conduit to a process.
conduitToProcess :: ConduitT ByteString (Either ByteString ByteString) IO r
-> (Handles -> IO r)
conduitToProcess c (Handles inh outh errh) =
runConduit $ sourceHandle inh .| c `fuseUpstream` sinkHandles outh errh
-- | Sink everything into the two handles.
sinkHandles :: Handle
-> Handle
-> ConduitT (Either ByteString ByteString) Void IO ()
sinkHandles out err =
CL.mapM_
(\ebs ->
case ebs of
Left bs -> S.hPut out bs
Right bs -> S.hPut err bs)
-- | Create a pipe.
createHandles :: IO (Handle, Handle)
createHandles =
mask_
(do (inFD, outFD) <- createPipe
x <- fdToHandle inFD
y <- fdToHandle outFD
hSetBuffering x NoBuffering
hSetBuffering y NoBuffering
return (x, y))
-- | Fuse two processes.
fuseProcess :: (Handles -> IO ()) -> (Handles -> IO r) -> (Handles -> IO r)
fuseProcess left right (Handles in1 out2 err) = do
(in2, out1) <- createHandles
runConcurrently
(Concurrently (left (Handles in1 out1 err) `finally` hClose out1) *>
Concurrently (right (Handles in2 out2 err) `finally` hClose in2))
-- | Fuse two conduits.
fuseConduit
:: Monad m
=> ConduitT ByteString (Either ByteString ByteString) m ()
-> ConduitT ByteString (Either ByteString ByteString) m r
-> ConduitT ByteString (Either ByteString ByteString) m r
fuseConduit left right = left .| getZipConduit right'
where
right' =
ZipConduit (CL.filter isRight) *>
ZipConduit (CL.mapMaybe (either (const Nothing) Just) .| right)
isRight Right {} = True
isRight Left {} = False
-- | Fuse a conduit with a process.
fuseConduitProcess
:: ConduitT ByteString (Either ByteString ByteString) IO ()
-> (Handles -> IO r)
-> (Handles -> IO r)
fuseConduitProcess left right (Handles in1 out2 err) = do
(in2, out1) <- createHandles
runConcurrently
(Concurrently
((runConduit $ sourceHandle in1 .| left .| sinkHandles out1 err) `finally`
hClose out1) *>
Concurrently (right (Handles in2 out2 err) `finally` hClose in2))
-- | Fuse a process with a conduit.
fuseProcessConduit
:: (Handles -> IO ())
-> ConduitT ByteString (Either ByteString ByteString) IO r
-> (Handles -> IO r)
fuseProcessConduit left right (Handles in1 out2 err) = do
(in2, out1) <- createHandles
runConcurrently
(Concurrently (left (Handles in1 out1 err) `finally` hClose out1) *>
Concurrently
((runConduit $
sourceHandle in2 .| right `fuseUpstream` sinkHandles out2 err) `finally`
hClose in2))
-- | Fuse one segment with another.
fuseSegment :: Segment () -> Segment r -> Segment r
SegmentConduit x `fuseSegment` SegmentConduit y =
SegmentConduit (fuseConduit x y)
SegmentConduit x `fuseSegment` SegmentProcess y =
SegmentProcess (fuseConduitProcess x y)
SegmentProcess x `fuseSegment` SegmentConduit y =
SegmentProcess (fuseProcessConduit x y)
SegmentProcess x `fuseSegment` SegmentProcess y =
SegmentProcess (fuseProcess x y)