diff --git a/supervisor/options.py b/supervisor/options.py index 563b8a547..23f55c168 100644 --- a/supervisor/options.py +++ b/supervisor/options.py @@ -900,6 +900,15 @@ def get(section, opt, *args, **kwargs): process_name = process_or_group_name( get(section, 'process_name', '%(program_name)s', do_expand=False)) + prsetpdeathsig = get(section, 'prsetpdeathsig', None) + if prsetpdeathsig is not None: + if sys.platform.startswith("linux"): + prsetpdeathsig = signal_number(prsetpdeathsig) + else: + raise ValueError( + "Cannot set prsetpdeathsig on non-linux os" + ) + if numprocs > 1: if not '%(process_num)' in process_name: # process_name needs to include process_num when we @@ -987,7 +996,8 @@ def get(section, opt, *args, **kwargs): exitcodes=exitcodes, redirect_stderr=redirect_stderr, environment=environment, - serverurl=serverurl) + serverurl=serverurl, + prsetpdeathsig=prsetpdeathsig) programs.append(pconfig) @@ -1776,7 +1786,7 @@ class ProcessConfig(Config): 'stderr_logfile_backups', 'stderr_logfile_maxbytes', 'stderr_events_enabled', 'stderr_syslog', 'stopsignal', 'stopwaitsecs', 'stopasgroup', 'killasgroup', - 'exitcodes', 'redirect_stderr' ] + 'exitcodes', 'redirect_stderr', 'prsetpdeathsig' ] optional_param_names = [ 'environment', 'serverurl' ] def __init__(self, options, **params): diff --git a/supervisor/process.py b/supervisor/process.py index 30573b07c..cf6a2a799 100644 --- a/supervisor/process.py +++ b/supervisor/process.py @@ -1,3 +1,4 @@ +import sys import os import time import errno @@ -293,6 +294,21 @@ def _spawn_as_child(self, filename, argv): # supervisord from being sent to children. options.setpgrp() + # Send this process a kill signal if supervisor crashes. + # Uses system call prctl(PR_SET_PDEATHSIG, ). + # This will only work on Linux. + if self.config.prsetpdeathsig is not None \ + and sys.platform.startswith("linux"): + # Constant from http://linux.die.net/include/linux/prctl.h + PR_SET_PDEATHSIG = 1 + try: + import ctypes + import ctypes.util + libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('c')) + libc.prctl(PR_SET_PDEATHSIG, self.config.prsetpdeathsig) + except Exception: + options.logger.debug("Could not set parent death signal.") + self._prepare_child_fds() # sending to fd 2 will put this output in the stderr log diff --git a/supervisor/tests/base.py b/supervisor/tests/base.py index 94932f3b6..23cca42a5 100644 --- a/supervisor/tests/base.py +++ b/supervisor/tests/base.py @@ -516,7 +516,7 @@ def __init__(self, options, name, command, directory=None, umask=None, stderr_logfile_backups=0, stderr_logfile_maxbytes=0, redirect_stderr=False, stopsignal=None, stopwaitsecs=10, stopasgroup=False, killasgroup=False, - exitcodes=(0,2), environment=None, serverurl=None): + exitcodes=(0,2), environment=None, serverurl=None, prsetpdeathsig=None): self.options = options self.name = name self.command = command @@ -550,6 +550,7 @@ def __init__(self, options, name, command, directory=None, umask=None, self.umask = umask self.autochildlogs_created = False self.serverurl = serverurl + self.prsetpdeathsig = prsetpdeathsig def create_autochildlogs(self): self.autochildlogs_created = True diff --git a/supervisor/tests/test_options.py b/supervisor/tests/test_options.py index 88cb0c014..93a6757bc 100644 --- a/supervisor/tests/test_options.py +++ b/supervisor/tests/test_options.py @@ -1423,6 +1423,68 @@ def test_processes_from_section_host_node_name_expansion(self): expected = "/bin/foo --host=" + platform.node() self.assertEqual(pconfigs[0].command, expected) + def test_processes_from_section_prsetdeathsig_error(self): + instance = self._makeOne() + text = lstrip("""\ + [program:foo] + command = /bin/foo + prsetpdeathsig = SIGKILL + """) + from supervisor.options import UnhosedConfigParser + config = UnhosedConfigParser() + config.read_string(text) + + platform_mock = Mock() + platform_mock.return_value = "darwin" + @patch('sys.platform', platform_mock) + def parse_config(instance, config): + instance.processes_from_section(config, 'program:foo', 'bar') + + try: + parse_config(instance, config) + except ValueError as exc: + self.assertTrue(exc.args[0].startswith( + 'Cannot set prsetpdeathsig on non-linux os in section')) + + def test_processes_from_section_prsetdeathsig_linux(self): + instance = self._makeOne() + text = lstrip("""\ + [program:foo] + command = /bin/foo + prsetpdeathsig = SIGKILL + """) + from supervisor.options import UnhosedConfigParser + config = UnhosedConfigParser() + config.read_string(text) + + platform_mock = Mock() + platform_mock.return_value = "linux" + @patch('sys.platform', platform_mock) + def parse_config(instance, config): + return instance.processes_from_section(config, 'program:foo', 'bar') + + pconfig = parse_config(instance, config) + self.assertEqual(pconfig[0].prsetpdeathsig, signal.SIGKILL) + + def test_processes_from_section_prsetdeathsig_linux_default(self): + instance = self._makeOne() + text = lstrip("""\ + [program:foo] + command = /bin/foo + """) + from supervisor.options import UnhosedConfigParser + config = UnhosedConfigParser() + config.read_string(text) + + platform_mock = Mock() + platform_mock.return_value = "linux" + @patch('sys.platform', platform_mock) + def parse_config(instance, config): + return instance.processes_from_section(config, 'program:foo', 'bar') + + pconfig = parse_config(instance, config) + self.assertEqual(pconfig[0].prsetpdeathsig, None) + def test_processes_from_section_process_num_expansion(self): instance = self._makeOne() text = lstrip("""\ @@ -2645,7 +2707,7 @@ def _makeOne(self, *arg, **kw): 'stderr_events_enabled', 'stderr_syslog', 'stopsignal', 'stopwaitsecs', 'stopasgroup', 'killasgroup', 'exitcodes', 'redirect_stderr', - 'environment'): + 'environment', 'prsetpdeathsig'): defaults[name] = name for name in ('stdout_logfile_backups', 'stdout_logfile_maxbytes', 'stderr_logfile_backups', 'stderr_logfile_maxbytes'): @@ -2727,7 +2789,7 @@ def _makeOne(self, *arg, **kw): 'stderr_events_enabled', 'stderr_syslog', 'stopsignal', 'stopwaitsecs', 'stopasgroup', 'killasgroup', 'exitcodes', 'redirect_stderr', - 'environment'): + 'environment', 'prsetpdeathsig'): defaults[name] = name for name in ('stdout_logfile_backups', 'stdout_logfile_maxbytes', 'stderr_logfile_backups', 'stderr_logfile_maxbytes'): @@ -2775,7 +2837,7 @@ def _makeOne(self, *arg, **kw): 'stderr_events_enabled', 'stderr_syslog', 'stopsignal', 'stopwaitsecs', 'stopasgroup', 'killasgroup', 'exitcodes', 'redirect_stderr', - 'environment'): + 'environment', 'prsetpdeathsig'): defaults[name] = name for name in ('stdout_logfile_backups', 'stdout_logfile_maxbytes', 'stderr_logfile_backups', 'stderr_logfile_maxbytes'): diff --git a/supervisor/tests/test_supervisord.py b/supervisor/tests/test_supervisord.py index df1523296..4ca08b19a 100644 --- a/supervisor/tests/test_supervisord.py +++ b/supervisor/tests/test_supervisord.py @@ -332,6 +332,7 @@ def make_pconfig(name, command, **params): 'stopasgroup': False, 'killasgroup': False, 'exitcodes': (0,2), 'environment': None, 'serverurl': None, + 'prsetpdeathsig': None } result.update(params) return ProcessConfig(options, **result)