Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

when resorting to SIGKILL send signal (optionally) to whole process group #30

Merged
merged 2 commits into from Aug 18, 2011
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGES.txt
Expand Up @@ -28,6 +28,11 @@ Next release
supervisord is run as root, otherwise the error is printed as before.
Patch by Benoit Sigoure.

- Add a boolean program option ``killasgroup``, defaulting to false,
if true when resorting to send SIGKILL to stop/terminate the process
send it to its whole process group instead to take care of possible
children as well and not leave them behind.

3.0a10 (2011-03-30)
-------------------

Expand Down
13 changes: 13 additions & 0 deletions docs/configuration.rst
Expand Up @@ -712,6 +712,19 @@ where specified.

*Introduced*: 3.0

``killasgroup``

If true, when resorting to send SIGKILL to the program to terminate
it send it to its whole process group instead, taking care of its
children as well, useful e.g with Python programs using
:mod:`multiprocessing`.

*Default*: false

*Required*: No.

*Introduced*: 3.0a11

``user``

If :program:`supervisord` runs as root, this UNIX user account will
Expand Down
5 changes: 4 additions & 1 deletion supervisor/options.py
Expand Up @@ -744,6 +744,7 @@ def processes_from_section(self, parser, section, group_name,
uid = name_to_uid(get(section, 'user', None))
stopsignal = signal_number(get(section, 'stopsignal', 'TERM'))
stopwaitsecs = integer(get(section, 'stopwaitsecs', 10))
killasgroup = boolean(get(section, 'killasgroup', 'false'))
exitcodes = list_of_exitcodes(get(section, 'exitcodes', '0,2'))
redirect_stderr = boolean(get(section, 'redirect_stderr','false'))
numprocs = integer(get(section, 'numprocs', 1))
Expand Down Expand Up @@ -837,6 +838,7 @@ def processes_from_section(self, parser, section, group_name,
stderr_logfile_maxbytes=logfiles['stderr_logfile_maxbytes'],
stopsignal=stopsignal,
stopwaitsecs=stopwaitsecs,
killasgroup=killasgroup,
exitcodes=exitcodes,
redirect_stderr=redirect_stderr,
environment=environment,
Expand Down Expand Up @@ -1545,7 +1547,8 @@ class ProcessConfig(Config):
'stderr_logfile', 'stderr_capture_maxbytes',
'stderr_logfile_backups', 'stderr_logfile_maxbytes',
'stderr_events_enabled',
'stopsignal', 'stopwaitsecs', 'exitcodes', 'redirect_stderr' ]
'stopsignal', 'stopwaitsecs', 'killasgroup',
'exitcodes', 'redirect_stderr' ]
optional_param_names = [ 'environment', 'serverurl' ]

def __init__(self, options, **params):
Expand Down
16 changes: 14 additions & 2 deletions supervisor/process.py
Expand Up @@ -351,9 +351,16 @@ def kill(self, sig):
options.logger.debug(msg)
return msg

options.logger.debug('killing %s (pid %s) with signal %s'
killasgroup = self.config.killasgroup and sig == signal.SIGKILL

as_group = ""
if killasgroup:
as_group = "process group "

options.logger.debug('killing %s (pid %s) %swith signal %s'
% (self.config.name,
self.pid,
as_group,
signame(sig))
)

Expand All @@ -366,8 +373,13 @@ def kill(self, sig):
ProcessStates.STOPPING)
self.change_state(ProcessStates.STOPPING)

pid = self.pid
if killasgroup:
# send to the whole process group instead
pid = -self.pid

try:
options.kill(self.pid, sig)
options.kill(pid, sig)
except:
io = StringIO.StringIO()
traceback.print_exc(file=io)
Expand Down
3 changes: 2 additions & 1 deletion supervisor/tests/base.py
Expand Up @@ -480,7 +480,7 @@ def __init__(self, options, name, command, directory=None, umask=None,
stderr_events_enabled=False,
stderr_logfile_backups=0, stderr_logfile_maxbytes=0,
redirect_stderr=False,
stopsignal=None, stopwaitsecs=10,
stopsignal=None, stopwaitsecs=10, killasgroup=False,
exitcodes=(0,2), environment=None, serverurl=None):
self.options = options
self.name = name
Expand All @@ -507,6 +507,7 @@ def __init__(self, options, name, command, directory=None, umask=None,
stopsignal = signal.SIGTERM
self.stopsignal = stopsignal
self.stopwaitsecs = stopwaitsecs
self.killasgroup = killasgroup
self.exitcodes = exitcodes
self.environment = environment
self.directory = directory
Expand Down
18 changes: 13 additions & 5 deletions supervisor/tests/test_options.py
Expand Up @@ -218,6 +218,7 @@ def test_options(self):
command=/bin/cat
autorestart=true
exitcodes=0,1,127
killasgroup=true

[program:cat4]
priority=4
Expand Down Expand Up @@ -275,6 +276,7 @@ def test_options(self):
self.assertEqual(proc1.stdout_logfile, '/tmp/cat.log')
self.assertEqual(proc1.stopsignal, signal.SIGKILL)
self.assertEqual(proc1.stopwaitsecs, 5)
self.assertEqual(proc1.killasgroup, False)
self.assertEqual(proc1.stdout_logfile_maxbytes,
datatypes.byte_size('50MB'))
self.assertEqual(proc1.stdout_logfile_backups, 10)
Expand All @@ -297,6 +299,7 @@ def test_options(self):
self.assertEqual(proc2.uid, None)
self.assertEqual(proc2.stdout_logfile, '/tmp/cat2.log')
self.assertEqual(proc2.stopsignal, signal.SIGTERM)
self.assertEqual(proc2.killasgroup, False)
self.assertEqual(proc2.stdout_logfile_maxbytes, 1024)
self.assertEqual(proc2.stdout_logfile_backups, 2)
self.assertEqual(proc2.exitcodes, [0,2])
Expand All @@ -320,7 +323,8 @@ def test_options(self):
self.assertEqual(proc3.stdout_logfile_backups, 10)
self.assertEqual(proc3.exitcodes, [0,1,127])
self.assertEqual(proc3.stopsignal, signal.SIGTERM)

self.assertEqual(proc3.killasgroup, True)

cat4 = options.process_group_configs[3]
self.assertEqual(cat4.name, 'cat4')
self.assertEqual(cat4.priority, 4)
Expand All @@ -340,7 +344,8 @@ def test_options(self):
self.assertEqual(proc4_a.stdout_logfile_backups, 10)
self.assertEqual(proc4_a.exitcodes, [0,2])
self.assertEqual(proc4_a.stopsignal, signal.SIGTERM)

self.assertEqual(proc4_a.killasgroup, False)

proc4_b = cat4.process_configs[1]
self.assertEqual(proc4_b.name, 'fleeb_1')
self.assertEqual(proc4_b.command, '/bin/cat')
Expand All @@ -355,7 +360,8 @@ def test_options(self):
self.assertEqual(proc4_b.stdout_logfile_backups, 10)
self.assertEqual(proc4_b.exitcodes, [0,2])
self.assertEqual(proc4_b.stopsignal, signal.SIGTERM)

self.assertEqual(proc4_b.killasgroup, False)

here = os.path.abspath(os.getcwd())
self.assertEqual(instance.uid, 0)
self.assertEqual(instance.gid, 0)
Expand Down Expand Up @@ -597,6 +603,7 @@ def test_processes_from_section(self):
stdout_events_enabled = true
stopsignal = KILL
stopwaitsecs = 100
killasgroup = true
exitcodes = 1,4
redirect_stderr = false
environment = KEY1=val1,KEY2=val2,KEY3=%(process_num)s
Expand All @@ -621,6 +628,7 @@ def test_processes_from_section(self):
self.assertEqual(pconfig.stdout_logfile_maxbytes, 104857600)
self.assertEqual(pconfig.stdout_events_enabled, True)
self.assertEqual(pconfig.stopsignal, signal.SIGKILL)
self.assertEqual(pconfig.killasgroup, True)
self.assertEqual(pconfig.stopwaitsecs, 100)
self.assertEqual(pconfig.exitcodes, [1,4])
self.assertEqual(pconfig.redirect_stderr, False)
Expand Down Expand Up @@ -1295,7 +1303,7 @@ def _makeOne(self, *arg, **kw):
'stderr_logfile', 'stderr_capture_maxbytes',
'stderr_events_enabled',
'stderr_logfile_backups', 'stderr_logfile_maxbytes',
'stopsignal', 'stopwaitsecs', 'exitcodes',
'stopsignal', 'stopwaitsecs', 'killasgroup', 'exitcodes',
'redirect_stderr', 'environment'):
defaults[name] = name
defaults.update(kw)
Expand Down Expand Up @@ -1369,7 +1377,7 @@ def _makeOne(self, *arg, **kw):
'stderr_logfile', 'stderr_capture_maxbytes',
'stderr_events_enabled',
'stderr_logfile_backups', 'stderr_logfile_maxbytes',
'stopsignal', 'stopwaitsecs', 'exitcodes',
'stopsignal', 'stopwaitsecs', 'killasgroup', 'exitcodes',
'redirect_stderr', 'environment'):
defaults[name] = name
defaults.update(kw)
Expand Down
17 changes: 17 additions & 0 deletions supervisor/tests/test_process.py
Expand Up @@ -715,6 +715,23 @@ def test_kill_from_stopping(self):
self.assertEqual(options.kills[11], signal.SIGKILL)
self.assertEqual(L, []) # no event because we didn't change state

def test_kill_from_stopping_w_killasgroup(self):
options = DummyOptions()
config = DummyPConfig(options, 'test', '/test', killasgroup=True)
instance = self._makeOne(config)
instance.pid = 11
L = []
from supervisor.states import ProcessStates
from supervisor import events
events.subscribe(events.Event,lambda x: L.append(x))
instance.state = ProcessStates.STOPPING
instance.kill(signal.SIGKILL)
self.assertEqual(options.logger.data[0], 'killing test (pid 11) '
'process group with signal SIGKILL')
self.assertEqual(instance.killing, 1)
self.assertEqual(options.kills[-11], signal.SIGKILL)
self.assertEqual(L, []) # no event because we didn't change state

def test_finish(self):
options = DummyOptions()
config = DummyPConfig(options, 'notthere', '/notthere',
Expand Down
1 change: 1 addition & 0 deletions supervisor/tests/test_supervisord.py
Expand Up @@ -259,6 +259,7 @@ def make_pconfig(name, command, **params):
'stderr_logfile_backups': 0, 'stderr_logfile_maxbytes': 0,
'redirect_stderr': False,
'stopsignal': None, 'stopwaitsecs': 10,
'killasgroup': False,
'exitcodes': (0,2), 'environment': None, 'serverurl': None }
result.update(params)
return ProcessConfig(options, **result)
Expand Down