{-# LANGUAGE CPP #-}
module System.Process.PID1
( RunOptions
, defaultRunOptions
, getRunEnv
, getRunExitTimeoutSec
, getRunGroup
, getRunUser
, getRunWorkDir
, run
, runWithOptions
, setRunEnv
, setRunExitTimeoutSec
, setRunGroup
, setRunUser
, setRunWorkDir
) where
import Control.Concurrent (forkIO, newEmptyMVar, takeMVar,
threadDelay, tryPutMVar)
import Control.Exception (assert, catch, throwIO)
import Control.Monad (forever, void)
import Data.Foldable (for_)
import System.Directory (setCurrentDirectory)
import System.Exit (ExitCode (ExitFailure), exitWith)
import System.IO.Error (isDoesNotExistError)
import System.Posix.Process (ProcessStatus (..), executeFile,
exitImmediately, getAnyProcessStatus,
getProcessID)
import System.Posix.Signals (Handler (Catch), Signal,
installHandler, sigINT, sigKILL,
sigTERM, signalProcess)
import System.Posix.Types (CPid)
import System.Posix.User (getGroupEntryForName,
getUserEntryForName,
groupID, setGroupID,
setUserID, userID)
import System.Process (createProcess, proc, env)
import System.Process.Internals (ProcessHandle__ (..),
modifyProcessHandle)
data RunOptions = RunOptions
{
RunOptions -> Maybe [(String, String)]
runEnv :: Maybe [(String, String)]
, RunOptions -> Maybe String
runUser :: Maybe String
, RunOptions -> Maybe String
runGroup :: Maybe String
, RunOptions -> Maybe String
runWorkDir :: Maybe FilePath
, RunOptions -> Int
runExitTimeoutSec :: Int
} deriving Int -> RunOptions -> ShowS
[RunOptions] -> ShowS
RunOptions -> String
(Int -> RunOptions -> ShowS)
-> (RunOptions -> String)
-> ([RunOptions] -> ShowS)
-> Show RunOptions
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> RunOptions -> ShowS
showsPrec :: Int -> RunOptions -> ShowS
$cshow :: RunOptions -> String
show :: RunOptions -> String
$cshowList :: [RunOptions] -> ShowS
showList :: [RunOptions] -> ShowS
Show
defaultRunOptions :: RunOptions
defaultRunOptions :: RunOptions
defaultRunOptions = RunOptions
{ runEnv :: Maybe [(String, String)]
runEnv = Maybe [(String, String)]
forall a. Maybe a
Nothing
, runUser :: Maybe String
runUser = Maybe String
forall a. Maybe a
Nothing
, runGroup :: Maybe String
runGroup = Maybe String
forall a. Maybe a
Nothing
, runWorkDir :: Maybe String
runWorkDir = Maybe String
forall a. Maybe a
Nothing
, runExitTimeoutSec :: Int
runExitTimeoutSec = Int
5 }
getRunEnv :: RunOptions -> Maybe [(String, String)]
getRunEnv :: RunOptions -> Maybe [(String, String)]
getRunEnv = RunOptions -> Maybe [(String, String)]
runEnv
setRunEnv :: [(String, String)] -> RunOptions -> RunOptions
setRunEnv :: [(String, String)] -> RunOptions -> RunOptions
setRunEnv [(String, String)]
env' RunOptions
opts = RunOptions
opts { runEnv = Just env' }
getRunUser :: RunOptions -> Maybe String
getRunUser :: RunOptions -> Maybe String
getRunUser = RunOptions -> Maybe String
runUser
setRunUser :: String -> RunOptions -> RunOptions
setRunUser :: String -> RunOptions -> RunOptions
setRunUser String
user RunOptions
opts = RunOptions
opts { runUser = Just user }
getRunGroup :: RunOptions -> Maybe String
getRunGroup :: RunOptions -> Maybe String
getRunGroup = RunOptions -> Maybe String
runGroup
setRunGroup :: String -> RunOptions -> RunOptions
setRunGroup :: String -> RunOptions -> RunOptions
setRunGroup String
group RunOptions
opts = RunOptions
opts { runGroup = Just group }
getRunWorkDir :: RunOptions -> Maybe FilePath
getRunWorkDir :: RunOptions -> Maybe String
getRunWorkDir = RunOptions -> Maybe String
runWorkDir
setRunWorkDir :: FilePath -> RunOptions -> RunOptions
setRunWorkDir :: String -> RunOptions -> RunOptions
setRunWorkDir String
dir RunOptions
opts = RunOptions
opts { runWorkDir = Just dir }
getRunExitTimeoutSec :: RunOptions -> Int
getRunExitTimeoutSec :: RunOptions -> Int
getRunExitTimeoutSec = RunOptions -> Int
runExitTimeoutSec
setRunExitTimeoutSec :: Int -> RunOptions -> RunOptions
setRunExitTimeoutSec :: Int -> RunOptions -> RunOptions
setRunExitTimeoutSec Int
sec RunOptions
opts = RunOptions
opts { runExitTimeoutSec = sec }
run :: FilePath
-> [String]
-> Maybe [(String, String)]
-> IO a
run :: forall a. String -> [String] -> Maybe [(String, String)] -> IO a
run String
cmd [String]
args Maybe [(String, String)]
env' = RunOptions -> String -> [String] -> IO a
forall a. RunOptions -> String -> [String] -> IO a
runWithOptions (RunOptions
defaultRunOptions {runEnv = env'}) String
cmd [String]
args
runWithOptions :: RunOptions
-> FilePath
-> [String]
-> IO a
runWithOptions :: forall a. RunOptions -> String -> [String] -> IO a
runWithOptions RunOptions
opts String
cmd [String]
args = do
Maybe String -> (String -> IO ()) -> IO ()
forall (t :: * -> *) (f :: * -> *) a b.
(Foldable t, Applicative f) =>
t a -> (a -> f b) -> f ()
for_ (RunOptions -> Maybe String
runGroup RunOptions
opts) ((String -> IO ()) -> IO ()) -> (String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
name -> do
entry <- String -> IO GroupEntry
getGroupEntryForName String
name
setGroupID $ groupID entry
Maybe String -> (String -> IO ()) -> IO ()
forall (t :: * -> *) (f :: * -> *) a b.
(Foldable t, Applicative f) =>
t a -> (a -> f b) -> f ()
for_ (RunOptions -> Maybe String
runUser RunOptions
opts) ((String -> IO ()) -> IO ()) -> (String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
name -> do
entry <- String -> IO UserEntry
getUserEntryForName String
name
setUserID $ userID entry
Maybe String -> (String -> IO ()) -> IO ()
forall (t :: * -> *) (f :: * -> *) a b.
(Foldable t, Applicative f) =>
t a -> (a -> f b) -> f ()
for_ (RunOptions -> Maybe String
runWorkDir RunOptions
opts) String -> IO ()
setCurrentDirectory
let env' :: Maybe [(String, String)]
env' = RunOptions -> Maybe [(String, String)]
runEnv RunOptions
opts
timeout :: Int
timeout = RunOptions -> Int
runExitTimeoutSec RunOptions
opts
myID <- IO ProcessID
getProcessID
if myID == 1
then runAsPID1 cmd args env' timeout
else executeFile cmd True args env'
runAsPID1 :: FilePath -> [String] -> Maybe [(String, String)] -> Int -> IO a
runAsPID1 :: forall a.
String -> [String] -> Maybe [(String, String)] -> Int -> IO a
runAsPID1 String
cmd [String]
args Maybe [(String, String)]
env' Int
timeout = do
killChildrenVar <- IO (MVar ())
forall a. IO (MVar a)
newEmptyMVar
let startKilling = IO Bool -> IO ()
forall (f :: * -> *) a. Functor f => f a -> f ()
void (IO Bool -> IO ()) -> IO Bool -> IO ()
forall a b. (a -> b) -> a -> b
$ MVar () -> () -> IO Bool
forall a. MVar a -> a -> IO Bool
tryPutMVar MVar ()
killChildrenVar ()
void $ installHandler sigTERM (Catch startKilling) Nothing
void $ installHandler sigINT (Catch startKilling) Nothing
(Nothing, Nothing, Nothing, ph) <- createProcess (proc cmd args)
{ env = env'
}
p_ <- modifyProcessHandle ph $ \ProcessHandle__
p_ -> (ProcessHandle__, ProcessHandle__)
-> IO (ProcessHandle__, ProcessHandle__)
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (ProcessHandle__
p_, ProcessHandle__
p_)
child <-
case p_ of
ClosedHandle ExitCode
e -> Bool -> IO ProcessID -> IO ProcessID
forall a. HasCallStack => Bool -> a -> a
assert Bool
False (ExitCode -> IO ProcessID
forall a. ExitCode -> IO a
exitWith ExitCode
e)
OpenHandle ProcessID
pid -> ProcessID -> IO ProcessID
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ProcessID
pid
OpenExtHandle ProcessID
pid ProcessID
_ -> ProcessID -> IO ProcessID
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ProcessID
pid
_ <- forkIO $ do
takeMVar killChildrenVar
killAllChildren child timeout
reap startKilling child
reap :: IO () -> CPid -> IO a
reap :: forall a. IO () -> ProcessID -> IO a
reap IO ()
startKilling ProcessID
child = do
childStatus <- IO (MVar ProcessStatus)
forall a. IO (MVar a)
newEmptyMVar
forever (reapOne childStatus) `catch` \IOError
e ->
if IOError -> Bool
isDoesNotExistError IOError
e
then do
MVar ProcessStatus -> IO ProcessStatus
forall a. MVar a -> IO a
takeMVar MVar ProcessStatus
childStatus IO ProcessStatus
-> (ProcessStatus -> IO (ZonkAny 0)) -> IO (ZonkAny 0)
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= ExitCode -> IO (ZonkAny 0)
forall a. ExitCode -> IO a
exitImmediately (ExitCode -> IO (ZonkAny 0))
-> (ProcessStatus -> ExitCode) -> ProcessStatus -> IO (ZonkAny 0)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ProcessStatus -> ExitCode
toExitCode
String -> IO a
forall a. HasCallStack => String -> a
error String
"This can never be reached"
else IOError -> IO a
forall e a. (HasCallStack, Exception e) => e -> IO a
throwIO IOError
e
where
reapOne :: MVar ProcessStatus -> IO ()
reapOne MVar ProcessStatus
childStatus = do
mres <- Bool -> Bool -> IO (Maybe (ProcessID, ProcessStatus))
getAnyProcessStatus Bool
True Bool
False
case mres of
Maybe (ProcessID, ProcessStatus)
Nothing -> Bool -> IO () -> IO ()
forall a. HasCallStack => Bool -> a -> a
assert Bool
False (() -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ())
Just (ProcessID
pid, ProcessStatus
status)
| ProcessID
pid ProcessID -> ProcessID -> Bool
forall a. Eq a => a -> a -> Bool
== ProcessID
child -> do
IO Bool -> IO ()
forall (f :: * -> *) a. Functor f => f a -> f ()
void (IO Bool -> IO ()) -> IO Bool -> IO ()
forall a b. (a -> b) -> a -> b
$ MVar ProcessStatus -> ProcessStatus -> IO Bool
forall a. MVar a -> a -> IO Bool
tryPutMVar MVar ProcessStatus
childStatus ProcessStatus
status
IO ()
startKilling
| Bool
otherwise -> () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()
killAllChildren :: CPid -> Int -> IO ()
killAllChildren :: ProcessID -> Int -> IO ()
killAllChildren ProcessID
cid Int
timeout = do
Signal -> ProcessID -> IO ()
signalProcess Signal
sigTERM ProcessID
cid IO () -> (IOError -> IO ()) -> IO ()
forall e a. Exception e => IO a -> (e -> IO a) -> IO a
`catch` \IOError
e ->
if IOError -> Bool
isDoesNotExistError IOError
e
then () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()
else IOError -> IO ()
forall e a. (HasCallStack, Exception e) => e -> IO a
throwIO IOError
e
Int -> IO ()
threadDelay (Int -> IO ()) -> Int -> IO ()
forall a b. (a -> b) -> a -> b
$ Int
timeout Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
1000 Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
1000
Signal -> ProcessID -> IO ()
signalProcess Signal
sigTERM (-ProcessID
1) IO () -> (IOError -> IO ()) -> IO ()
forall e a. Exception e => IO a -> (e -> IO a) -> IO a
`catch` \IOError
e ->
if IOError -> Bool
isDoesNotExistError IOError
e
then () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()
else IOError -> IO ()
forall e a. (HasCallStack, Exception e) => e -> IO a
throwIO IOError
e
Int -> IO ()
threadDelay (Int -> IO ()) -> Int -> IO ()
forall a b. (a -> b) -> a -> b
$ Int
timeout Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
1000 Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
1000
Signal -> ProcessID -> IO ()
signalProcess Signal
sigKILL (-ProcessID
1) IO () -> (IOError -> IO ()) -> IO ()
forall e a. Exception e => IO a -> (e -> IO a) -> IO a
`catch` \IOError
e ->
if IOError -> Bool
isDoesNotExistError IOError
e
then () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()
else IOError -> IO ()
forall e a. (HasCallStack, Exception e) => e -> IO a
throwIO IOError
e
toExitCode :: ProcessStatus -> ExitCode
toExitCode :: ProcessStatus -> ExitCode
toExitCode (Exited ExitCode
ec) = ExitCode
ec
#if MIN_VERSION_unix(2, 7, 0)
toExitCode (Terminated Signal
sig Bool
_) = Signal -> ExitCode
signalToEC Signal
sig
#else
toExitCode (Terminated sig) = signalToEC sig
#endif
toExitCode (Stopped Signal
sig) = Signal -> ExitCode
signalToEC Signal
sig
signalToEC :: Signal -> ExitCode
signalToEC :: Signal -> ExitCode
signalToEC Signal
sig = Int -> ExitCode
ExitFailure (Signal -> Int
forall a b. (Integral a, Num b) => a -> b
fromIntegral Signal
sig Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
128)