-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathCore.hs
128 lines (111 loc) · 4.64 KB
/
Core.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
-- | Defines 'Cmd', the core API of Procex.
module Procex.Core (Cmd, makeCmd', passArg, unIOCmd, postCmd, run', runReplace, passFd, passArgFd, passNoFd) where
import Control.Concurrent.Async
import Control.Exception.Base
import Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString.Lazy.UTF8 as B
import Data.Foldable (toList)
import qualified Data.Sequence as S
import Foreign.C.Error (throwErrno)
import Procex.Execve
import System.Posix.ByteString
data Arg = ArgStr ByteString | ArgFd Fd deriving stock (Show)
data Args = Args
{ args :: [Arg]
, fds :: [(Fd, Maybe Fd)]
, executor :: Execve
}
emptyArgs :: Args
emptyArgs = Args {args = [], fds = [], executor = forkexecve}
fdPrepend :: (Fd, Maybe Fd) -> Args -> Args
fdPrepend (x, y) args = args {fds = (x, y) : fds args}
argPrepend :: ByteString -> Args -> Args
argPrepend arg Args {..} = Args {args = ArgStr arg : args, ..}
argFdPrepend :: Fd -> Args -> Args
argFdPrepend arg Args {..} = Args {args = ArgFd arg : args, ..}
-- | A command. You can execute this with 'run'' or 'Procex.Process.run'.
newtype Cmd = Cmd {unCmd :: Args -> IO (Async ProcessStatus)}
{- | Make a 'Cmd' from the path to an executable. Does not take PATH into account.
See 'Procex.Process.makeCmd' for a version that provides
some sensible defaults, like forwarding stdin, stdout, stderr.
-}
makeCmd' :: ByteString -> Cmd
makeCmd' path = Cmd $ \Args {args, fds, executor} -> do
let sequentialize_fds :: [(Fd, Maybe Fd)] -> S.Seq Fd -> S.Seq Fd
sequentialize_fds [] out = out
sequentialize_fds ((new, Just old) : fds) out =
sequentialize_fds fds $ S.update (fromIntegral new) old $ out <> S.replicate (max 0 $ fromIntegral new - S.length out + 1) (-1)
sequentialize_fds ((new, Nothing) : fds) out =
sequentialize_fds fds $ S.update (fromIntegral new) (-1) $ out <> S.replicate (max 0 $ fromIntegral new - S.length out + 1) (-1)
let fds_seq = sequentialize_fds fds []
let (all_fds, args') =
foldr
( flip $ \(all_fds, args') -> \case
ArgStr str -> (all_fds, str : args')
ArgFd old_fd -> let new_fd = S.length all_fds in (all_fds S.|> old_fd, ("/proc/self/fd/" <> B.fromString (show new_fd)) : args')
)
(fds_seq, [] :: [ByteString])
args
pid <- executor path args' Nothing (toList all_fds) -- FIXME there could be an asynchronous exception here
pid <- case pid of
Just x -> pure x
Nothing -> throwErrno $ "Couldn't execute " <> show path <> " with args " <> show args' <> " with the following fds: " <> show all_fds
async $ do
-- `onException` is for asynchronous exceptions too.
status <- getProcessStatus True True pid `onException` signalProcess sigTERM pid
case status of
Just status -> pure status
Nothing -> throwErrno "getProcessStatus returned Nothing"
{- | Embeds the IO action inside the command, such that the IO action
is executed when the command is executed.
-}
unIOCmd :: IO Cmd -> Cmd
unIOCmd cmd = Cmd $ \args -> do
cmd <- cmd
unCmd cmd args
{- | Executes some code after launching the process. If launching the process
fails, it will be provided with the exception it failed with.
-}
postCmd :: (Either SomeException (Async ProcessStatus) -> IO ()) -> Cmd -> Cmd
postCmd f cmd = Cmd $ \args -> do
r <- try (unCmd cmd args)
f r
case r of
Left e -> throwIO e
Right p -> pure p
{- | Runs the specified command asynchronously and returns
the process status.
-}
run' :: Cmd -> IO (Async ProcessStatus)
run' cmd = unCmd cmd emptyArgs
{- | Runs the specified commands and replaces the current process with it.
This will not return unless an error occurs while executing the process.
-}
runReplace :: Cmd -> IO ()
runReplace cmd = const () <$> unCmd cmd emptyArgs {executor = execve}
-- | Pass an argument to the command.
passArg :: ByteString -> Cmd -> Cmd
passArg str cmd = Cmd $ \args -> unCmd cmd $ argPrepend str args
{- | Bind a fd in the new process to a fd available now.
If you try to bind an fd already bound, it will simply replace the older binding.
-}
passFd ::
-- | (new, old)
(Fd, Fd) ->
Cmd ->
Cmd
passFd (new, old) cmd = Cmd $ \args -> unCmd cmd $ fdPrepend (new, Just old) args
{- | Don't open a fd in the new process if it was going to be opened by 'passFd'.
Does not affect fds opened by 'passArgFd'.
-}
passNoFd ::
-- | new
Fd ->
Cmd ->
Cmd
passNoFd new cmd = Cmd $ \args -> unCmd cmd $ fdPrepend (new, Nothing) args
{- | Pass an argument of the form @\/proc\/self\/fd\/\<n\>@ to the process,
where `n` is an fd which is a duplicate of the fd provided here.
-}
passArgFd :: Fd -> Cmd -> Cmd
passArgFd fd cmd = Cmd $ \args -> unCmd cmd $ argFdPrepend fd args