From 1a1823d95d498a4d462fed1a5394a80b32e3fbc1 Mon Sep 17 00:00:00 2001 From: Artem Pulkin Date: Sat, 12 Apr 2025 21:33:31 +0200 Subject: [PATCH 01/17] multiprocessing: interrupt --- Doc/library/multiprocessing.rst | 9 +++++++++ Lib/multiprocessing/popen_fork.py | 3 +++ Lib/multiprocessing/process.py | 7 +++++++ Lib/test/test_subprocess.py | 6 ++++++ 4 files changed, 25 insertions(+) diff --git a/Doc/library/multiprocessing.rst b/Doc/library/multiprocessing.rst index 6ccc0d4aa59555..dd1b9b8a7f48b6 100644 --- a/Doc/library/multiprocessing.rst +++ b/Doc/library/multiprocessing.rst @@ -670,6 +670,15 @@ The :mod:`multiprocessing` package mostly replicates the API of the .. versionadded:: 3.3 + .. method:: interrupt() + + Terminate the process. Works on POSIX using the :py:const:`~signal.SIGINT` signal. + Behavior on Windows is undefined. + + By default, will terminate the child process by raising :exc:``KeyboardInterrupt``. + This behavior can be altered by setting the respective signal handler in the child + process :func:`signal.signal` for :py:const:`~signal.SIGINT`. + .. method:: terminate() Terminate the process. On POSIX this is done using the :py:const:`~signal.SIGTERM` signal; diff --git a/Lib/multiprocessing/popen_fork.py b/Lib/multiprocessing/popen_fork.py index a57ef6bdad5ccc..7affa1b985f091 100644 --- a/Lib/multiprocessing/popen_fork.py +++ b/Lib/multiprocessing/popen_fork.py @@ -54,6 +54,9 @@ def _send_signal(self, sig): if self.wait(timeout=0.1) is None: raise + def interrupt(self): + self._send_signal(signal.SIGINT) + def terminate(self): self._send_signal(signal.SIGTERM) diff --git a/Lib/multiprocessing/process.py b/Lib/multiprocessing/process.py index b45f7df476f7d8..7c05f38a2330ec 100644 --- a/Lib/multiprocessing/process.py +++ b/Lib/multiprocessing/process.py @@ -125,6 +125,13 @@ def start(self): del self._target, self._args, self._kwargs _children.add(self) + def interrupt(self): + ''' + Terminate process; sends SIGINT signal or uses TerminateProcess() + ''' + self._check_closed() + self._popen.interrupt() + def terminate(self): ''' Terminate process; sends SIGTERM signal or uses TerminateProcess() diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py index 81d97a88f07bdd..c6f1e565524f2c 100644 --- a/Lib/test/test_subprocess.py +++ b/Lib/test/test_subprocess.py @@ -2440,6 +2440,12 @@ def test_send_signal(self): self.assertIn(b'KeyboardInterrupt', stderr) self.assertNotEqual(p.wait(), 0) + def test_interrupt(self): + p = self._kill_process('interrupt') + _, stderr = p.communicate() + self.assertIn(b'KeyboardInterrupt', stderr) + self.assertNotEqual(p.wait(), 0) + def test_kill(self): p = self._kill_process('kill') _, stderr = p.communicate() From 8bb96242110ca5f367eb146cd90f7152f1305dab Mon Sep 17 00:00:00 2001 From: Artem Pulkin Date: Sat, 12 Apr 2025 21:41:22 +0200 Subject: [PATCH 02/17] docs: fix ticks --- 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 dd1b9b8a7f48b6..8a7eb29e2d48c3 100644 --- a/Doc/library/multiprocessing.rst +++ b/Doc/library/multiprocessing.rst @@ -675,7 +675,7 @@ The :mod:`multiprocessing` package mostly replicates the API of the Terminate the process. Works on POSIX using the :py:const:`~signal.SIGINT` signal. Behavior on Windows is undefined. - By default, will terminate the child process by raising :exc:``KeyboardInterrupt``. + By default, will terminate the child process by raising :exc:`KeyboardInterrupt`. This behavior can be altered by setting the respective signal handler in the child process :func:`signal.signal` for :py:const:`~signal.SIGINT`. From 5387fe7af86b9cc8c3cc8b8b5d41830ef38a2bc1 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 12 Apr 2025 19:42:54 +0000 Subject: [PATCH 03/17] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst diff --git a/Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst b/Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst new file mode 100644 index 00000000000000..4f6613094ff027 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst @@ -0,0 +1 @@ +add `Process.interrupt` From 6b031b596437b8983975b2e49bcdc0d6c8837263 Mon Sep 17 00:00:00 2001 From: Artem Pulkin Date: Sat, 12 Apr 2025 21:55:27 +0200 Subject: [PATCH 04/17] CHANGELOG: fix --- .../next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst b/Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst index 4f6613094ff027..1c0725a60410bf 100644 --- a/Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst +++ b/Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst @@ -1 +1 @@ -add `Process.interrupt` +add :func:`Process.interrupt` From 20a56a043865e5e922a7f13d7615d68d0b833733 Mon Sep 17 00:00:00 2001 From: Artem Pulkin Date: Sat, 12 Apr 2025 22:01:48 +0200 Subject: [PATCH 05/17] CHANGELOG: fix --- .../next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst b/Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst index 1c0725a60410bf..635a950169f946 100644 --- a/Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst +++ b/Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst @@ -1 +1 @@ -add :func:`Process.interrupt` +add :func:`multiprocessing.Process.interrupt` From a3359bce2fcac07cd010468655361db240a7b2bc Mon Sep 17 00:00:00 2001 From: Artem Pulkin Date: Sat, 12 Apr 2025 22:24:04 +0200 Subject: [PATCH 06/17] tests: fix --- Lib/test/_test_multiprocessing.py | 5 +++++ Lib/test/test_subprocess.py | 6 ------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 1cd5704905f95c..2e00bc012b8e9f 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -567,6 +567,11 @@ def handler(*args): return p.exitcode + def test_interrupt(self): + if os.name != 'nt': + exitcode = self._kill_process(multiprocessing.Process.interrupt) + self.assertEqual(exitcode, -signal.SIGINT) + def test_terminate(self): exitcode = self._kill_process(multiprocessing.Process.terminate) self.assertEqual(exitcode, -signal.SIGTERM) diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py index c6f1e565524f2c..81d97a88f07bdd 100644 --- a/Lib/test/test_subprocess.py +++ b/Lib/test/test_subprocess.py @@ -2440,12 +2440,6 @@ def test_send_signal(self): self.assertIn(b'KeyboardInterrupt', stderr) self.assertNotEqual(p.wait(), 0) - def test_interrupt(self): - p = self._kill_process('interrupt') - _, stderr = p.communicate() - self.assertIn(b'KeyboardInterrupt', stderr) - self.assertNotEqual(p.wait(), 0) - def test_kill(self): p = self._kill_process('kill') _, stderr = p.communicate() From 43a2a5c8feb4a62e2c31d50b24732720465443a0 Mon Sep 17 00:00:00 2001 From: Artem Pulkin Date: Sat, 12 Apr 2025 22:48:30 +0200 Subject: [PATCH 07/17] tests: fix --- Lib/test/_test_multiprocessing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 2e00bc012b8e9f..5230ca4669b94c 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -570,7 +570,7 @@ def handler(*args): def test_interrupt(self): if os.name != 'nt': exitcode = self._kill_process(multiprocessing.Process.interrupt) - self.assertEqual(exitcode, -signal.SIGINT) + self.assertEqual(exitcode, 1) def test_terminate(self): exitcode = self._kill_process(multiprocessing.Process.terminate) From 361e06899d1aced6dcdade9e781c042a7568ce7d Mon Sep 17 00:00:00 2001 From: pulkin Date: Sun, 13 Apr 2025 12:06:21 +0200 Subject: [PATCH 08/17] docs: update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Doc/library/multiprocessing.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Doc/library/multiprocessing.rst b/Doc/library/multiprocessing.rst index 8a7eb29e2d48c3..2b9602604caa6b 100644 --- a/Doc/library/multiprocessing.rst +++ b/Doc/library/multiprocessing.rst @@ -675,10 +675,12 @@ The :mod:`multiprocessing` package mostly replicates the API of the Terminate the process. Works on POSIX using the :py:const:`~signal.SIGINT` signal. Behavior on Windows is undefined. - By default, will terminate the child process by raising :exc:`KeyboardInterrupt`. + By default, this terminates the child process by raising :exc:`KeyboardInterrupt`. This behavior can be altered by setting the respective signal handler in the child process :func:`signal.signal` for :py:const:`~signal.SIGINT`. + .. versionadded:: next + .. method:: terminate() Terminate the process. On POSIX this is done using the :py:const:`~signal.SIGTERM` signal; From 80820590c049ca70b7719a892634a2df243e2f7f Mon Sep 17 00:00:00 2001 From: Artem Pulkin Date: Sun, 13 Apr 2025 12:08:41 +0200 Subject: [PATCH 09/17] multiprocessing: fix docstring --- Lib/multiprocessing/process.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/multiprocessing/process.py b/Lib/multiprocessing/process.py index 7c05f38a2330ec..9db322be1aa6d6 100644 --- a/Lib/multiprocessing/process.py +++ b/Lib/multiprocessing/process.py @@ -127,7 +127,7 @@ def start(self): def interrupt(self): ''' - Terminate process; sends SIGINT signal or uses TerminateProcess() + Terminate process; sends SIGINT signal ''' self._check_closed() self._popen.interrupt() From 67a951e70e71928f88d3bc0028b5554b0d2b3f05 Mon Sep 17 00:00:00 2001 From: Artem Pulkin Date: Sun, 13 Apr 2025 12:14:51 +0200 Subject: [PATCH 10/17] news: update --- .../next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst b/Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst index 635a950169f946..be036524bc3434 100644 --- a/Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst +++ b/Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst @@ -1 +1 @@ -add :func:`multiprocessing.Process.interrupt` +Add a shortcut function :func:`multiprocessing.Process.interrupt` alongside the existing :func:`multiprocessing.Process.terminate` and :func:`multiprocessing.Process.kill` for an improved control over child process termination. From afeaee74c9e8379fc723d66347739f0ee42b7d25 Mon Sep 17 00:00:00 2001 From: Artem Pulkin Date: Sun, 13 Apr 2025 12:34:15 +0200 Subject: [PATCH 11/17] whatsnew: add the change --- Doc/whatsnew/3.14.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 762d53eeb2df1a..9dec35b2c1dcb5 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -921,6 +921,10 @@ multiprocessing The :func:`set` in :func:`multiprocessing.Manager` method is now available. (Contributed by Mingyu Park in :gh:`129949`.) +* Add :func:`multiprocessing.Process.interrupt` which terminates the child + process by sending :py:const:`~signal.SIGINT`. This enables "finally" clauses + and printing stack trace for the terminated process. + (Contributed by Artem Pulkin in :gh:`131913`.) operator -------- From ce60363380177031f7e9f6be0a83ec988f6cd37b Mon Sep 17 00:00:00 2001 From: pulkin Date: Sun, 13 Apr 2025 12:57:59 +0200 Subject: [PATCH 12/17] tests: decorate interrupt test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/_test_multiprocessing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 5230ca4669b94c..76d55b97e22a9a 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -567,10 +567,10 @@ def handler(*args): return p.exitcode + @unittest.skipIf(os.name == 'nt', "POSIX only") def test_interrupt(self): - if os.name != 'nt': - exitcode = self._kill_process(multiprocessing.Process.interrupt) - self.assertEqual(exitcode, 1) + exitcode = self._kill_process(multiprocessing.Process.interrupt) + self.assertEqual(exitcode, 1) def test_terminate(self): exitcode = self._kill_process(multiprocessing.Process.terminate) From 8a9cf33d99e6871e9f6216375d8a78cd6de0b76b Mon Sep 17 00:00:00 2001 From: Artem Pulkin Date: Sun, 13 Apr 2025 13:07:53 +0200 Subject: [PATCH 13/17] tests: add another interrupt test --- Lib/test/_test_multiprocessing.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 76d55b97e22a9a..ccef247020ed19 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -512,15 +512,20 @@ def _test_process_mainthread_native_id(cls, q): def _sleep_some(cls): time.sleep(100) + @classmethod + def _sleep_no_int_handler(cls): + signal.signal(signal.SIGINT, signal.SIG_IGN) + cls._sleep_some() + @classmethod def _test_sleep(cls, delay): time.sleep(delay) - def _kill_process(self, meth): + def _kill_process(self, meth, target=None): if self.TYPE == 'threads': self.skipTest('test not appropriate for {}'.format(self.TYPE)) - p = self.Process(target=self._sleep_some) + p = self.Process(target=target or self._sleep_some) p.daemon = True p.start() @@ -571,6 +576,14 @@ def handler(*args): def test_interrupt(self): exitcode = self._kill_process(multiprocessing.Process.interrupt) self.assertEqual(exitcode, 1) + # exit code 1 is hard-coded for uncaught exceptions + # (KeyboardInterrupt in this case) + # in multiprocessing.BaseProcess._bootstrap + + @unittest.skipIf(os.name == 'nt', "POSIX only") + def test_interrupt_no_handler(self): + exitcode = self._kill_process(multiprocessing.Process.interrupt, target=self._sleep_no_int_handler) + self.assertEqual(exitcode, -signal.SIGINT) def test_terminate(self): exitcode = self._kill_process(multiprocessing.Process.terminate) From 5018031b5d1d14c417d6fd00681d572a26eb72f3 Mon Sep 17 00:00:00 2001 From: Artem Pulkin Date: Sun, 13 Apr 2025 13:24:49 +0200 Subject: [PATCH 14/17] docs: more --- Doc/library/multiprocessing.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Doc/library/multiprocessing.rst b/Doc/library/multiprocessing.rst index 2b9602604caa6b..5a15b02947340d 100644 --- a/Doc/library/multiprocessing.rst +++ b/Doc/library/multiprocessing.rst @@ -679,6 +679,14 @@ The :mod:`multiprocessing` package mostly replicates the API of the This behavior can be altered by setting the respective signal handler in the child process :func:`signal.signal` for :py:const:`~signal.SIGINT`. + Note: if the child process catches and discards :exc:`KeyboardInterrupt`, the + process will not be terminated. + + Note: the default behavior will also set :attr:`exitcode` to `1` as if an + uncaught exception was raised in the child process. To have a different + :attr:`exitcode` you may simply catch :exc:`KeyboardInterrupt` and call + `exit(your_code)`. + .. versionadded:: next .. method:: terminate() From e278f8f6579eb232356ddda2abf9fc2f97a9ff35 Mon Sep 17 00:00:00 2001 From: Artem Pulkin Date: Sun, 13 Apr 2025 13:29:15 +0200 Subject: [PATCH 15/17] docs: ticks --- 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 5a15b02947340d..2e4b1d35fb387c 100644 --- a/Doc/library/multiprocessing.rst +++ b/Doc/library/multiprocessing.rst @@ -685,7 +685,7 @@ The :mod:`multiprocessing` package mostly replicates the API of the Note: the default behavior will also set :attr:`exitcode` to `1` as if an uncaught exception was raised in the child process. To have a different :attr:`exitcode` you may simply catch :exc:`KeyboardInterrupt` and call - `exit(your_code)`. + ``exit(your_code)``. .. versionadded:: next From cc57ab5b445ebfd0af3955ee417052666cd91460 Mon Sep 17 00:00:00 2001 From: Artem Pulkin Date: Sun, 13 Apr 2025 13:33:37 +0200 Subject: [PATCH 16/17] docs: backticks --- 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 2e4b1d35fb387c..e44142a8ed3106 100644 --- a/Doc/library/multiprocessing.rst +++ b/Doc/library/multiprocessing.rst @@ -682,7 +682,7 @@ The :mod:`multiprocessing` package mostly replicates the API of the Note: if the child process catches and discards :exc:`KeyboardInterrupt`, the process will not be terminated. - Note: the default behavior will also set :attr:`exitcode` to `1` as if an + Note: the default behavior will also set :attr:`exitcode` to ``1`` as if an uncaught exception was raised in the child process. To have a different :attr:`exitcode` you may simply catch :exc:`KeyboardInterrupt` and call ``exit(your_code)``. From 8109864c004cbf200054c25a31643c0204beda6e Mon Sep 17 00:00:00 2001 From: Artem Pulkin Date: Sun, 13 Apr 2025 13:46:01 +0200 Subject: [PATCH 17/17] tests: fix the new test --- Lib/test/_test_multiprocessing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index ccef247020ed19..93c6c04dc558b4 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -514,7 +514,7 @@ def _sleep_some(cls): @classmethod def _sleep_no_int_handler(cls): - signal.signal(signal.SIGINT, signal.SIG_IGN) + signal.signal(signal.SIGINT, signal.SIG_DFL) cls._sleep_some() @classmethod