diff --git a/mitogen/parent.py b/mitogen/parent.py index 45c8e2f2..a57ca20b 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -93,6 +93,12 @@ SYS_EXECUTABLE_MSG = ( ) _sys_executable_warning_logged = False +SIGNAL_BY_NUM = dict( + (getattr(signal, name), name) + for name in sorted(vars(signal), reverse=True) + if name.startswith('SIG') and not name.startswith('SIG_') +) + def get_log_level(): return (LOG.level or logging.getLogger().level or logging.INFO) @@ -584,6 +590,21 @@ def _proxy_connect(name, method_name, kwargs, econtext): } +def wstatus_to_str(status): + """ + Parse and format a :func:`os.waitpid` exit status. + """ + if os.WIFEXITED(status): + return 'exited with return code %d' % (os.WEXITSTATUS(status),) + if os.WIFSIGNALED(status): + n = os.WTERMSIG(status) + return 'exited due to signal %d (%s)' % (n, SIGNAL_BY_NUM.get(n)) + if os.WIFSTOPPED(status): + n = os.WSTOPSIG(status) + return 'stopped due to signal %d (%s)' % (n, SIGNAL_BY_NUM.get(n)) + return 'unknown wait status (%d)' % (status,) + + class Argv(object): """ Wrapper to defer argv formatting when debug logging is disabled. @@ -961,7 +982,7 @@ class Stream(mitogen.core.Stream): self._reaped = True if pid: - LOG.debug('%r: child process exit status was %d', self, status) + LOG.debug('%r: PID %d %s', self, pid, wstatus_to_str(status)) return # For processes like sudo we cannot actually send sudo a signal, diff --git a/tests/parent_test.py b/tests/parent_test.py index 53b66c1d..c9ccaf3f 100644 --- a/tests/parent_test.py +++ b/tests/parent_test.py @@ -1,5 +1,6 @@ import errno import os +import signal import subprocess import sys import tempfile @@ -44,6 +45,41 @@ class GetDefaultRemoteNameTest(testlib.TestCase): self.assertEquals("ECORP_Administrator@box:123", self.func()) +class WstatusToStrTest(testlib.TestCase): + func = staticmethod(mitogen.parent.wstatus_to_str) + + def test_return_zero(self): + pid = os.fork() + if not pid: + os._exit(0) + (pid, status), _ = mitogen.core.io_op(os.waitpid, pid, 0) + self.assertEquals(self.func(status), + 'exited with return code 0') + + def test_return_one(self): + pid = os.fork() + if not pid: + os._exit(1) + (pid, status), _ = mitogen.core.io_op(os.waitpid, pid, 0) + self.assertEquals( + self.func(status), + 'exited with return code 1' + ) + + def test_sigkill(self): + pid = os.fork() + if not pid: + time.sleep(600) + os.kill(pid, signal.SIGKILL) + (pid, status), _ = mitogen.core.io_op(os.waitpid, pid, 0) + self.assertEquals( + self.func(status), + 'exited due to signal %s (SIGKILL)' % (signal.SIGKILL,) + ) + + # can't test SIGSTOP without POSIX sessions rabbithole + + class ReapChildTest(testlib.RouterMixin, testlib.TestCase): def test_connect_timeout(self): # Ensure the child process is reaped if the connection times out.