|
| 1 | +{-# LANGUAGE OverloadedStrings #-} |
| 2 | + |
1 | 3 | -- | Defines 'Cmd', the core API of Procex.
|
2 |
| -module Procex.Core (Cmd, makeCmd', passArg, unIOCmd, postCmd, run', runReplace, passFd, passArgFd, passNoFd) where |
| 4 | +module Procex.Core (run, findInPATH, CloseAfterRun (..)) where |
3 | 5 |
|
4 |
| -import Control.Exception.Base |
| 6 | +import Control.Concurrent (forkIO) |
| 7 | +import Control.Exception (Exception, displayException, mask_, onException, throwIO, uninterruptibleMask_) |
| 8 | +import Control.Monad (forM_) |
5 | 9 | import Data.ByteString.Lazy (ByteString)
|
6 |
| -import Data.Foldable (toList) |
7 |
| -import Foreign.C.Error (throwErrno) |
8 |
| -import Foreign.C.Types |
9 |
| -import Procex.Execve |
10 |
| - |
11 |
| -type Fd = CInt |
12 |
| - |
13 |
| -data Arg = ArgStr ByteString | ArgFd Fd deriving (Show) |
14 |
| - |
15 |
| -data Args = Args |
16 |
| - { args :: [Arg] |
17 |
| - , fds :: [(Fd, Maybe Fd)] |
18 |
| - , executor :: Execve |
19 |
| - } |
20 |
| - |
21 |
| -emptyArgs :: Args |
22 |
| -emptyArgs = Args {args = [], fds = [], executor = forkexecve} |
23 |
| - |
24 |
| -fdPrepend :: (Fd, Maybe Fd) -> Args -> Args |
25 |
| -fdPrepend (x, y) args = args {fds = (x, y) : fds args} |
26 |
| - |
27 |
| -argPrepend :: ByteString -> Args -> Args |
28 |
| -argPrepend arg Args {..} = Args {args = ArgStr arg : args, ..} |
29 |
| - |
30 |
| -argFdPrepend :: Fd -> Args -> Args |
31 |
| -argFdPrepend arg Args {..} = Args {args = ArgFd arg : args, ..} |
32 |
| - |
33 |
| --- | A command. You can execute this with 'run'' or 'Procex.Process.run'. |
34 |
| -newtype Cmd = Cmd {unCmd :: Args -> IO (Async ProcessStatus)} |
35 |
| - |
36 |
| -{- | Make a 'Cmd' from the path to an executable. Does not take PATH into account. |
37 |
| - See 'Procex.Process.makeCmd' for a version that provides |
38 |
| - some sensible defaults, like forwarding stdin, stdout, stderr. |
39 |
| --} |
40 |
| -makeCmd' :: ByteString -> Cmd |
41 |
| -makeCmd' path = Cmd $ \Args {args, fds, executor} -> do |
42 |
| - let sequentialize_fds :: [(Fd, Maybe Fd)] -> S.Seq Fd -> S.Seq Fd |
43 |
| - sequentialize_fds [] out = out |
44 |
| - sequentialize_fds ((new, Just old) : fds) out = |
45 |
| - sequentialize_fds fds $ S.update (fromIntegral new) old $ out <> S.replicate (max 0 $ fromIntegral new - S.length out + 1) (-1) |
46 |
| - sequentialize_fds ((new, Nothing) : fds) out = |
47 |
| - sequentialize_fds fds $ S.update (fromIntegral new) (-1) $ out <> S.replicate (max 0 $ fromIntegral new - S.length out + 1) (-1) |
48 |
| - let fds_seq = sequentialize_fds fds [] |
49 |
| - let (all_fds, args') = |
50 |
| - foldr |
51 |
| - ( flip $ \(all_fds, args') -> \x -> case x of |
52 |
| - ArgStr str -> (all_fds, str : args') |
53 |
| - 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') |
54 |
| - ) |
55 |
| - (fds_seq, [] :: [ByteString]) |
56 |
| - args |
57 |
| - pid <- executor path args' Nothing (toList all_fds) -- FIXME there could be an asynchronous exception here |
58 |
| - pid <- case pid of |
59 |
| - Just x -> pure x |
60 |
| - Nothing -> throwErrno $ "Couldn't execute " <> show path <> " with args " <> show args' <> " with the following fds: " <> show all_fds |
61 |
| - async $ do |
62 |
| - -- `onException` is for asynchronous exceptions too. |
63 |
| - status <- getProcessStatus True True pid `onException` signalProcess sigTERM pid |
| 10 | +import Data.ByteString.Lazy qualified as B |
| 11 | +import Data.Char (ord) |
| 12 | +import Procex.Execve (forkExec) |
| 13 | +import System.Exit (ExitCode (ExitSuccess)) |
| 14 | +import System.IO (Handle, hClose) |
| 15 | +import System.IO.Error (doesNotExistErrorType, mkIOError) |
| 16 | +import System.Posix.Env.ByteString (getEnvDefault) |
| 17 | +import System.Posix.Files.ByteString (fileExist) |
| 18 | +import System.Posix.IO (handleToFd) |
| 19 | +import System.Posix.Process (ProcessStatus (Exited), getProcessStatus) |
| 20 | +import System.Posix.Signals (sigTERM, signalProcess) |
| 21 | + |
| 22 | +-- FIXME: kill children on exit automatically using atexit(3) |
| 23 | + |
| 24 | +newtype ProcessFailed = ProcessFailed ProcessStatus deriving (Show) |
| 25 | + |
| 26 | +instance Exception ProcessFailed where |
| 27 | + displayException (ProcessFailed status) = "Process failed: " <> show status |
| 28 | + |
| 29 | +data CloseAfterRun = CloseAfterRun | DontCloseAfterRun |
| 30 | + |
| 31 | +-- simple wrapper over forkExec, |
| 32 | +-- if you throw an exception to the thread running this, |
| 33 | +-- the process will be killed, albeit its descendants won't |
| 34 | +-- automatically unfortunately |
| 35 | +run :: |
| 36 | + -- | The absolute path to the executable. |
| 37 | + ByteString -> |
| 38 | + -- | The args. |
| 39 | + [ByteString] -> |
| 40 | + -- | The environment. If Nothing, it will be inherited. |
| 41 | + Maybe [ByteString] -> |
| 42 | + -- | The handles to pass in as file descriptors. |
| 43 | + -- The first one will be stdin, then stdout, then stderr, then fd 3, and so on. |
| 44 | + -- If set to Nothing, the file descriptor will be closed instead. |
| 45 | + -- If CloseAfterRun, the handle will be closed on the Haskell-side |
| 46 | + -- after the process has been run. |
| 47 | + -- This is useful to clean up pipes. |
| 48 | + [Maybe (CloseAfterRun, Handle)] -> |
| 49 | + IO () |
| 50 | +run fullpath args env handles = do |
| 51 | + fds <- traverse (traverse (handleToFd . snd)) handles |
| 52 | + mask_ $ do |
| 53 | + pid <- forkExec fullpath args env fds |
| 54 | + _ <- |
| 55 | + uninterruptibleMask_ $ |
| 56 | + forkIO $ |
| 57 | + forM_ handles (\x -> case x of Just (CloseAfterRun, h) -> hClose h; _ -> pure ()) |
| 58 | + status <- |
| 59 | + -- this is interruptible |
| 60 | + getProcessStatus True False pid |
| 61 | + -- we kill the child if we get cancelled |
| 62 | + `onException` ( uninterruptibleMask_ $ |
| 63 | + forkIO $ |
| 64 | + signalProcess sigTERM pid |
| 65 | + >> getProcessStatus True False pid |
| 66 | + >> pure () |
| 67 | + ) |
64 | 68 | case status of
|
65 |
| - Just status -> pure status |
66 |
| - Nothing -> throwErrno "getProcessStatus returned Nothing" |
67 |
| - |
68 |
| -{- | Embeds the IO action inside the command, such that the IO action |
69 |
| - is executed when the command is executed. |
70 |
| --} |
71 |
| -unIOCmd :: IO Cmd -> Cmd |
72 |
| -unIOCmd cmd = Cmd $ \args -> do |
73 |
| - cmd <- cmd |
74 |
| - unCmd cmd args |
75 |
| - |
76 |
| -{- | Executes some code after launching the process. If launching the process |
77 |
| - fails, it will be provided with the exception it failed with. |
78 |
| --} |
79 |
| -postCmd :: (Either SomeException (Async ProcessStatus) -> IO ()) -> Cmd -> Cmd |
80 |
| -postCmd f cmd = Cmd $ \args -> do |
81 |
| - r <- try (unCmd cmd args) |
82 |
| - f r |
83 |
| - case r of |
84 |
| - Left e -> throwIO e |
85 |
| - Right p -> pure p |
86 |
| - |
87 |
| -{- | Runs the specified command asynchronously and returns |
88 |
| - the process status. |
89 |
| --} |
90 |
| -run' :: Cmd -> IO (Async ProcessStatus) |
91 |
| -run' cmd = unCmd cmd emptyArgs |
92 |
| - |
93 |
| -{- | Runs the specified commands and replaces the current process with it. |
94 |
| - This will not return unless an error occurs while executing the process. |
95 |
| --} |
96 |
| -runReplace :: Cmd -> IO () |
97 |
| -runReplace cmd = const () <$> unCmd cmd emptyArgs {executor = execve} |
98 |
| - |
99 |
| --- | Pass an argument to the command. |
100 |
| -passArg :: ByteString -> Cmd -> Cmd |
101 |
| -passArg str cmd = Cmd $ \args -> unCmd cmd $ argPrepend str args |
102 |
| - |
103 |
| -{- | Bind a fd in the new process to a fd available now. |
104 |
| - If you try to bind an fd already bound, it will simply replace the older binding. |
105 |
| --} |
106 |
| -passFd :: |
107 |
| - -- | (new, old) |
108 |
| - (Fd, Fd) -> |
109 |
| - Cmd -> |
110 |
| - Cmd |
111 |
| -passFd (new, old) cmd = Cmd $ \args -> unCmd cmd $ fdPrepend (new, Just old) args |
112 |
| - |
113 |
| -{- | Don't open a fd in the new process if it was going to be opened by 'passFd'. |
114 |
| - Does not affect fds opened by 'passArgFd'. |
115 |
| --} |
116 |
| -passNoFd :: |
117 |
| - -- | new |
118 |
| - Fd -> |
119 |
| - Cmd -> |
120 |
| - Cmd |
121 |
| -passNoFd new cmd = Cmd $ \args -> unCmd cmd $ fdPrepend (new, Nothing) args |
122 |
| - |
123 |
| -{- | Pass an argument of the form @\/proc\/self\/fd\/\<n\>@ to the process, |
124 |
| - where `n` is an fd which is a duplicate of the fd provided here. |
125 |
| --} |
126 |
| -passArgFd :: Fd -> Cmd -> Cmd |
127 |
| -passArgFd fd cmd = Cmd $ \args -> unCmd cmd $ argFdPrepend fd args |
| 69 | + Nothing -> throwIO $ userError "getProcessStatus returned Nothing" |
| 70 | + Just (Exited ExitSuccess) -> pure () |
| 71 | + Just status -> throwIO (ProcessFailed status) |
| 72 | + |
| 73 | +findM :: (Monad m) => (a -> m Bool) -> [a] -> m (Maybe a) |
| 74 | +findM f (x : xs) = |
| 75 | + f x >>= \b -> case b of |
| 76 | + True -> pure $ Just x |
| 77 | + False -> findM f xs |
| 78 | +findM _ [] = pure Nothing |
| 79 | + |
| 80 | +findInPATH :: ByteString -> IO ByteString |
| 81 | +findInPATH path = do |
| 82 | + pathvar <- B.fromStrict <$> getEnvDefault "PATH" "" |
| 83 | + fullpath <- |
| 84 | + findM fileExist |
| 85 | + . fmap (\x -> B.toStrict $ x <> "/" <> path) |
| 86 | + . (<> ["/", "."]) |
| 87 | + . B.split (fromIntegral $ ord ':') |
| 88 | + $ pathvar |
| 89 | + case fullpath of |
| 90 | + Just p -> pure . B.fromStrict $ p |
| 91 | + Nothing -> |
| 92 | + let p = show path |
| 93 | + in throwIO $ mkIOError doesNotExistErrorType (p <> " does not exist") Nothing (Just p) |
0 commit comments