Skip to content

Commit 2ae7fb4

Browse files
committed
Adds ability to only SIGTERM the immediate process
This is useful in certain situations (with multiprocessing apps such as `gunicorn` or `celery`) where we want to signal to the "main" process only and let *it* handle gracefully terminating the "worker" processes. In case the "main" process does not gracefully shutdown the "worker" processes in the given "timeout" we still make sure to send SIGKILL to all "(-1)" remaining processes.
1 parent 0ebece9 commit 2ae7fb4

File tree

4 files changed

+54
-10
lines changed

4 files changed

+54
-10
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ repo](https://github.com/snoyberg/docker-testing#readme).
2222

2323
### Usage
2424

25-
> pid1 [-e|--env ENV] [-u|--user USER] [-g|--group GROUP] [-w|--workdir DIR] [-t|--timeout TIMEOUT] COMMAND [ARG1 ARG2 ... ARGN]
25+
> pid1 [-e|--env ENV] [-u|--user USER] [-g|--group GROUP] [-w|--workdir DIR] [-t|--timeout TIMEOUT] [-s|--single] COMMAND [ARG1 ARG2 ... ARGN]
2626
2727
Where:
2828
* `-e`, `--env` `ENV` - Override environment variable from given name=value
@@ -33,6 +33,7 @@ Where:
3333
executing COMMAND
3434
* `-w`, `--workdir` `DIR` - chdir to `DIR` before executing COMMAND
3535
* `-t`, `--timeout` `TIMEOUT` - timeout (in seconds) to wait for all child processes to exit
36+
* `-s`, `--single` - flag if we should only send SIGTERM to the immediate child process
3637

3738
The recommended use case for this executable is to embed it in a Docker image.
3839
Assuming you've placed it at `/sbin/pid1`, the two commonly recommended usages

app/Main.hs

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ options defaultEnv =
1717
, Option ['u'] ["user"] (ReqArg setRunUser "USER") "run command as user"
1818
, Option ['g'] ["group"] (ReqArg setRunGroup "GROUP") "run command as group"
1919
, Option ['w'] ["workdir"] (ReqArg setRunWorkDir "DIR") "command working directory"
20-
, Option ['t'] ["timeout"] (ReqArg (setRunExitTimeoutSec . read) "TIMEOUT") "timeout (in seconds) to wait for all child processes to exit" ]
20+
, Option ['t'] ["timeout"] (ReqArg (setRunExitTimeoutSec . read) "TIMEOUT") "timeout (in seconds) to wait for all child processes to exit"
21+
, Option ['s'] ["single"] (NoArg (setRunSignalImmediateChildOnly True)) "Flag if we should only send SIGTERM to the immediate child process" ]
2122
where optEnv env' kv =
2223
let kvp = fmap (drop 1) $ span (/= '=') kv in
2324
kvp:filter ((fst kvp /=) . fst) env'

pid1.cabal

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: pid1
2-
version: 0.1.2.0
2+
version: 0.1.3.0
33
synopsis: Do signal handling and orphan reaping for Unix PID1 init processes
44
description: Please see README.md or view Haddocks at <https://www.stackage.org/package/pid1>
55
homepage: https://github.com/fpco/pid1#readme

src/System/Process/PID1.hs

+49-7
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ module System.Process.PID1
77
, getRunGroup
88
, getRunUser
99
, getRunWorkDir
10+
, getRunSignalImmediateChildOnly
1011
, run
1112
, runWithOptions
1213
, setRunEnv
1314
, setRunExitTimeoutSec
15+
, setRunSignalImmediateChildOnly
1416
, setRunGroup
1517
, setRunUser
1618
, setRunWorkDir
@@ -52,6 +54,7 @@ data RunOptions = RunOptions
5254
-- timeout (in seconds) to wait for all child processes to exit after
5355
-- receiving SIGTERM or SIGINT signal
5456
, runExitTimeoutSec :: Int
57+
, runSignalImmediateChildOnly :: Bool
5558
} deriving Show
5659

5760
-- | return default `RunOptions`
@@ -63,7 +66,8 @@ defaultRunOptions = RunOptions
6366
, runUser = Nothing
6467
, runGroup = Nothing
6568
, runWorkDir = Nothing
66-
, runExitTimeoutSec = 5 }
69+
, runExitTimeoutSec = 5
70+
, runSignalImmediateChildOnly = False }
6771

6872
-- | Get environment variable overrides for the given `RunOptions`
6973
--
@@ -127,6 +131,19 @@ getRunExitTimeoutSec = runExitTimeoutSec
127131
setRunExitTimeoutSec :: Int -> RunOptions -> RunOptions
128132
setRunExitTimeoutSec sec opts = opts { runExitTimeoutSec = sec }
129133

134+
-- | Return boolean flag if we should only send SIGTERM to the immediate child process
135+
--
136+
--- @since 0.1.3.0
137+
getRunSignalImmediateChildOnly :: RunOptions -> Bool
138+
getRunSignalImmediateChildOnly = runSignalImmediateChildOnly
139+
140+
-- | Set boolean flag if we should only send SIGTERM to the immediate child process
141+
--
142+
-- @since 0.1.3.0
143+
setRunSignalImmediateChildOnly :: Bool -> RunOptions -> RunOptions
144+
setRunSignalImmediateChildOnly x opts = opts { runSignalImmediateChildOnly = x }
145+
146+
130147
-- | Run the given command with specified arguments, with optional environment
131148
-- variable override (default is to use the current process's environment).
132149
--
@@ -168,22 +185,20 @@ runWithOptions opts cmd args = do
168185
for_ (runWorkDir opts) setCurrentDirectory
169186
let env' = runEnv opts
170187
timeout = runExitTimeoutSec opts
188+
single = runSignalImmediateChildOnly opts
171189
-- check if we should act as pid1 or just exec the process
172190
myID <- getProcessID
173191
if myID == 1
174-
then runAsPID1 cmd args env' timeout
192+
then runAsPID1 single cmd args env' timeout
175193
else executeFile cmd True args env'
176194

177195
-- | Run as a child with signal handling and orphan reaping.
178-
runAsPID1 :: FilePath -> [String] -> Maybe [(String, String)] -> Int -> IO a
179-
runAsPID1 cmd args env' timeout = do
196+
runAsPID1 :: Bool -> FilePath -> [String] -> Maybe [(String, String)] -> Int -> IO a
197+
runAsPID1 single cmd args env' timeout = do
180198
-- Set up an MVar to indicate we're ready to start killing all
181199
-- children processes. Then start a thread waiting for that
182200
-- variable to be filled and do the actual killing.
183201
killChildrenVar <- newEmptyMVar
184-
_ <- forkIO $ do
185-
takeMVar killChildrenVar
186-
killAllChildren timeout
187202

188203
-- Helper function to start killing, used below
189204
let startKilling = void $ tryPutMVar killChildrenVar ()
@@ -206,6 +221,13 @@ runAsPID1 cmd args env' timeout = do
206221
ClosedHandle e -> assert False (exitWith e)
207222
OpenHandle pid -> return pid
208223

224+
_ <- forkIO $ do
225+
takeMVar killChildrenVar
226+
if single then
227+
killImmediateChild child timeout
228+
else
229+
killAllChildren timeout
230+
209231
-- Loop on reaping child processes
210232
reap startKilling child
211233

@@ -267,6 +289,26 @@ killAllChildren timeout = do
267289
then return ()
268290
else throwIO e
269291

292+
killImmediateChild :: CPid -> Int -> IO ()
293+
killImmediateChild cid timeout = do
294+
-- Send immediate child processes the TERM signal
295+
signalProcess sigTERM cid `catch` \e ->
296+
if isDoesNotExistError e
297+
then return ()
298+
else throwIO e
299+
300+
-- Wait for `timeout` seconds. We don't need to put in any logic about
301+
-- whether there are still child processes; if all children have
302+
-- exited, then the reap loop will exit and our process will shut
303+
-- down.
304+
threadDelay $ timeout * 1000 * 1000
305+
306+
-- OK, some children didn't exit. Now time to get serious!
307+
signalProcess sigKILL (-1) `catch` \e ->
308+
if isDoesNotExistError e
309+
then return ()
310+
else throwIO e
311+
270312
-- | Convert a ProcessStatus to an ExitCode. In the case of a signal being the
271313
-- cause of termination, see 'signalToEC'.
272314
toExitCode :: ProcessStatus -> ExitCode

0 commit comments

Comments
 (0)