From 26b3b5249ffe35a6990fc2021a9cec8c222d0209 Mon Sep 17 00:00:00 2001 From: Vitor Pereira Date: Sat, 1 Jul 2017 21:20:29 +0100 Subject: [PATCH 1/6] bpo-30794: added kill() method to multiprocessing.Process --- Lib/multiprocessing/popen_fork.py | 10 ++++- Lib/multiprocessing/popen_spawn_win32.py | 2 + Lib/multiprocessing/process.py | 7 ++++ Lib/test/_test_multiprocessing.py | 53 ++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/Lib/multiprocessing/popen_fork.py b/Lib/multiprocessing/popen_fork.py index 5af9d919e4987e..44ce9a9e79b050 100644 --- a/Lib/multiprocessing/popen_fork.py +++ b/Lib/multiprocessing/popen_fork.py @@ -49,16 +49,22 @@ def wait(self, timeout=None): return self.poll(os.WNOHANG if timeout == 0.0 else 0) return self.returncode - def terminate(self): + def _send_signal(self, sig): if self.returncode is None: try: - os.kill(self.pid, signal.SIGTERM) + os.kill(self.pid, sig) except ProcessLookupError: pass except OSError: if self.wait(timeout=0.1) is None: raise + def terminate(self): + self._send_signal(signal.SIGTERM) + + def kill(self): + self._send_signal(signal.SIGKILL) + def _launch(self, process_obj): code = 1 parent_r, child_w = os.pipe() diff --git a/Lib/multiprocessing/popen_spawn_win32.py b/Lib/multiprocessing/popen_spawn_win32.py index ecb86e96ba4364..3e42e9c3387413 100644 --- a/Lib/multiprocessing/popen_spawn_win32.py +++ b/Lib/multiprocessing/popen_spawn_win32.py @@ -97,5 +97,7 @@ def terminate(self): if self.wait(timeout=1.0) is None: raise + kill = terminate + def close(self): self.finalizer() diff --git a/Lib/multiprocessing/process.py b/Lib/multiprocessing/process.py index fde97b7a67e4c8..ce4ce43cfca6fb 100644 --- a/Lib/multiprocessing/process.py +++ b/Lib/multiprocessing/process.py @@ -122,6 +122,13 @@ def terminate(self): self._check_closed() self._popen.terminate() + def kill(self): + ''' + Terminate process; sends SIGKILL signal or uses TerminateProcess() + ''' + self._check_closed() + self._popen.kill() + def join(self, timeout=None): ''' Wait until child process terminates diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index d0a5446cfea2c6..630843ee39c6df 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -337,6 +337,59 @@ def handler(*args): if os.name != 'nt': self.assertEqual(p.exitcode, -signal.SIGTERM) + def test_kill(self): + if self.TYPE == 'threads': + self.skipTest('test not appropriate for {}'.format(self.TYPE)) + + p = self.Process(target=self._test_terminate) + p.daemon = True + p.start() + + self.assertEqual(p.is_alive(), True) + self.assertIn(p, self.active_children()) + self.assertEqual(p.exitcode, None) + + join = TimingWrapper(p.join) + + self.assertEqual(join(0), None) + self.assertTimingAlmostEqual(join.elapsed, 0.0) + self.assertEqual(p.is_alive(), True) + + self.assertEqual(join(-1), None) + self.assertTimingAlmostEqual(join.elapsed, 0.0) + self.assertEqual(p.is_alive(), True) + + # XXX maybe terminating too soon causes the problems on Gentoo... + time.sleep(1) + + p.kill() + + if hasattr(signal, 'alarm'): + # On the Gentoo buildbot waitpid() often seems to block forever. + # We use alarm() to interrupt it if it blocks for too long. + def handler(*args): + raise RuntimeError('join took too long: %s' % p) + old_handler = signal.signal(signal.SIGALRM, handler) + try: + signal.alarm(10) + self.assertEqual(join(), None) + finally: + signal.alarm(0) + signal.signal(signal.SIGALRM, old_handler) + else: + self.assertEqual(join(), None) + + self.assertTimingAlmostEqual(join.elapsed, 0.0) + + self.assertEqual(p.is_alive(), False) + self.assertNotIn(p, self.active_children()) + + p.join() + + # sometimes get p.exitcode == 0 on Windows ... + if os.name != 'nt': + self.assertEqual(p.exitcode, -signal.SIGKILL) + def test_cpu_count(self): try: cpus = multiprocessing.cpu_count() From e8cdbc351e1c4c72d223915a9f018afb3318b475 Mon Sep 17 00:00:00 2001 From: Vitor Pereira Date: Tue, 4 Jul 2017 22:00:32 +0100 Subject: [PATCH 2/6] Added entries to documentation and NEWS --- Doc/library/multiprocessing.rst | 4 ++++ .../next/Library/2017-07-04-22-00-20.bpo-30794.qFwozm.rst | 2 ++ 2 files changed, 6 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2017-07-04-22-00-20.bpo-30794.qFwozm.rst diff --git a/Doc/library/multiprocessing.rst b/Doc/library/multiprocessing.rst index 5265639edb975d..f475b9d7a8cc0f 100644 --- a/Doc/library/multiprocessing.rst +++ b/Doc/library/multiprocessing.rst @@ -598,6 +598,10 @@ The :mod:`multiprocessing` package mostly replicates the API of the acquired a lock or semaphore etc. then terminating it is liable to cause other processes to deadlock. + .. method:: kill() + + Same as :meth:`terminate()` but using the ``SIGKILL`` signal on Unix. + .. method:: close() Close the :class:`Process` object, releasing all resources associated diff --git a/Misc/NEWS.d/next/Library/2017-07-04-22-00-20.bpo-30794.qFwozm.rst b/Misc/NEWS.d/next/Library/2017-07-04-22-00-20.bpo-30794.qFwozm.rst new file mode 100644 index 00000000000000..d960bd3a306dd2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2017-07-04-22-00-20.bpo-30794.qFwozm.rst @@ -0,0 +1,2 @@ +Added multiprocessing.Process.kill method to terminate using the SIGKILL +signal on Unix. From 7f6d00e0b6354e76e9ce4bd4d5e86b7d2096f629 Mon Sep 17 00:00:00 2001 From: Vitor Pereira Date: Tue, 4 Jul 2017 22:14:58 +0100 Subject: [PATCH 3/6] Refactored test_terminate and test_kill --- Lib/test/_test_multiprocessing.py | 67 +++++-------------------------- 1 file changed, 10 insertions(+), 57 deletions(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 630843ee39c6df..26997c862f966d 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -277,18 +277,18 @@ def test_process(self): self.assertNotIn(p, self.active_children()) @classmethod - def _test_terminate(cls): + def _test_sleep_some(cls): time.sleep(100) @classmethod def _test_sleep(cls, delay): time.sleep(delay) - def test_terminate(self): + def _test_signal(self, meth, sig): if self.TYPE == 'threads': self.skipTest('test not appropriate for {}'.format(self.TYPE)) - p = self.Process(target=self._test_terminate) + p = self.Process(target=self._test_sleep_some) p.daemon = True p.start() @@ -309,7 +309,7 @@ def test_terminate(self): # XXX maybe terminating too soon causes the problems on Gentoo... time.sleep(1) - p.terminate() + meth(p) if hasattr(signal, 'alarm'): # On the Gentoo buildbot waitpid() often seems to block forever. @@ -335,60 +335,13 @@ def handler(*args): # sometimes get p.exitcode == 0 on Windows ... if os.name != 'nt': - self.assertEqual(p.exitcode, -signal.SIGTERM) - - def test_kill(self): - if self.TYPE == 'threads': - self.skipTest('test not appropriate for {}'.format(self.TYPE)) - - p = self.Process(target=self._test_terminate) - p.daemon = True - p.start() - - self.assertEqual(p.is_alive(), True) - self.assertIn(p, self.active_children()) - self.assertEqual(p.exitcode, None) - - join = TimingWrapper(p.join) - - self.assertEqual(join(0), None) - self.assertTimingAlmostEqual(join.elapsed, 0.0) - self.assertEqual(p.is_alive(), True) - - self.assertEqual(join(-1), None) - self.assertTimingAlmostEqual(join.elapsed, 0.0) - self.assertEqual(p.is_alive(), True) - - # XXX maybe terminating too soon causes the problems on Gentoo... - time.sleep(1) + self.assertEqual(p.exitcode, -sig) - p.kill() - - if hasattr(signal, 'alarm'): - # On the Gentoo buildbot waitpid() often seems to block forever. - # We use alarm() to interrupt it if it blocks for too long. - def handler(*args): - raise RuntimeError('join took too long: %s' % p) - old_handler = signal.signal(signal.SIGALRM, handler) - try: - signal.alarm(10) - self.assertEqual(join(), None) - finally: - signal.alarm(0) - signal.signal(signal.SIGALRM, old_handler) - else: - self.assertEqual(join(), None) - - self.assertTimingAlmostEqual(join.elapsed, 0.0) - - self.assertEqual(p.is_alive(), False) - self.assertNotIn(p, self.active_children()) - - p.join() + def test_terminate(self): + self._test_signal(multiprocessing.Process.terminate, signal.SIGTERM) - # sometimes get p.exitcode == 0 on Windows ... - if os.name != 'nt': - self.assertEqual(p.exitcode, -signal.SIGKILL) + def test_kill(self): + self._test_signal(multiprocessing.Process.kill, signal.SIGKILL) def test_cpu_count(self): try: @@ -515,7 +468,7 @@ def test_many_processes(self): for p in procs: self.assertEqual(p.exitcode, 0) - procs = [self.Process(target=self._test_terminate) + procs = [self.Process(target=self._test_sleep_some) for i in range(N)] for p in procs: p.start() From dfd4faf75b083423cc80abecb53c904247337e76 Mon Sep 17 00:00:00 2001 From: Vitor Pereira Date: Tue, 4 Jul 2017 22:50:46 +0100 Subject: [PATCH 4/6] Fix SIGTERM and SIGKILL being used on Windows for the tests --- Lib/test/_test_multiprocessing.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 26997c862f966d..c76b1f5ca2bb05 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -277,18 +277,18 @@ def test_process(self): self.assertNotIn(p, self.active_children()) @classmethod - def _test_sleep_some(cls): + def _sleep_some(cls): time.sleep(100) @classmethod def _test_sleep(cls, delay): time.sleep(delay) - def _test_signal(self, meth, sig): + def _kill_process(self, meth): if self.TYPE == 'threads': self.skipTest('test not appropriate for {}'.format(self.TYPE)) - p = self.Process(target=self._test_sleep_some) + p = self.Process(target=self._sleep_some) p.daemon = True p.start() @@ -333,15 +333,17 @@ def handler(*args): p.join() - # sometimes get p.exitcode == 0 on Windows ... - if os.name != 'nt': - self.assertEqual(p.exitcode, -sig) + return p.exitcode def test_terminate(self): - self._test_signal(multiprocessing.Process.terminate, signal.SIGTERM) + exitcode = self._kill_process(multiprocessing.Process.terminate) + if os.name != 'nt': + self.assertEqual(exitcode, -signal.SIGTERM) def test_kill(self): - self._test_signal(multiprocessing.Process.kill, signal.SIGKILL) + exitcode = self._kill_process(multiprocessing.Process.kill) + if os.name != 'nt': + self.assertEqual(exitcode, -signal.SIGKILL) def test_cpu_count(self): try: @@ -468,7 +470,7 @@ def test_many_processes(self): for p in procs: self.assertEqual(p.exitcode, 0) - procs = [self.Process(target=self._test_sleep_some) + procs = [self.Process(target=self._sleep_some) for i in range(N)] for p in procs: p.start() From 7c25c66b5f0911871a64f64aad866ae132173786 Mon Sep 17 00:00:00 2001 From: Vitor Pereira Date: Tue, 18 Jul 2017 16:24:49 +0100 Subject: [PATCH 5/6] Added "versionadded" marker to the documentation --- Doc/library/multiprocessing.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Doc/library/multiprocessing.rst b/Doc/library/multiprocessing.rst index f475b9d7a8cc0f..9f4a6657d2c980 100644 --- a/Doc/library/multiprocessing.rst +++ b/Doc/library/multiprocessing.rst @@ -601,6 +601,8 @@ The :mod:`multiprocessing` package mostly replicates the API of the .. method:: kill() Same as :meth:`terminate()` but using the ``SIGKILL`` signal on Unix. + + .. versionadded:: 3.7 .. method:: close() From 84e0e7a063215809b81e59ab7f59c8bf44492aa8 Mon Sep 17 00:00:00 2001 From: Antoine Pitrou Date: Tue, 18 Jul 2017 17:28:36 +0200 Subject: [PATCH 6/6] Fix trailing whitespace in doc --- Doc/library/multiprocessing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/multiprocessing.rst b/Doc/library/multiprocessing.rst index 9f4a6657d2c980..9318a750919b67 100644 --- a/Doc/library/multiprocessing.rst +++ b/Doc/library/multiprocessing.rst @@ -601,7 +601,7 @@ The :mod:`multiprocessing` package mostly replicates the API of the .. method:: kill() Same as :meth:`terminate()` but using the ``SIGKILL`` signal on Unix. - + .. versionadded:: 3.7 .. method:: close()