]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-131913: multiprocessing: add interrupt for POSIX (GH-132453)
authorpulkin <gpulkin@gmail.com>
Wed, 23 Apr 2025 06:55:24 +0000 (08:55 +0200)
committerGitHub <noreply@github.com>
Wed, 23 Apr 2025 06:55:24 +0000 (23:55 -0700)
* multiprocessing: interrupt

Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
Doc/library/multiprocessing.rst
Doc/whatsnew/3.14.rst
Lib/multiprocessing/popen_fork.py
Lib/multiprocessing/process.py
Lib/test/_test_multiprocessing.py
Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst [new file with mode: 0644]

index 6ccc0d4aa59555f4868ff4d7998cf3c5522a05fe..e44142a8ed31065e6dc11f52cb03ce374f2dd977 100644 (file)
@@ -670,6 +670,25 @@ 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, 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`.
+
+      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()
 
       Terminate the process.  On POSIX this is done using the :py:const:`~signal.SIGTERM` signal;
index 904a3ce6d597e9227b4bbfa850ed25ccd6a99407..63a5bab7fe8fea74a15403feaf8afa778ffad9c9 100644 (file)
@@ -972,6 +972,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
 --------
index a57ef6bdad5ccc667866ede2fb695d8eb55e6ac5..7affa1b985f0916982d1022e623fa1d450f5ec7c 100644 (file)
@@ -54,6 +54,9 @@ class Popen(object):
                 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)
 
index b45f7df476f7d8cb853c21c308656b3914e55c7d..9db322be1aa6d6346fde20878d60889bd9c4fd90 100644 (file)
@@ -125,6 +125,13 @@ class BaseProcess(object):
         del self._target, self._args, self._kwargs
         _children.add(self)
 
+    def interrupt(self):
+        '''
+        Terminate process; sends SIGINT signal
+        '''
+        self._check_closed()
+        self._popen.interrupt()
+
     def terminate(self):
         '''
         Terminate process; sends SIGTERM signal or uses TerminateProcess()
index 58d8a5eae8a2ce226d1c24d3869c8363a4825712..54b942e76d78c50819bf4de9849c8e3994f82812 100644 (file)
@@ -512,15 +512,20 @@ class _TestProcess(BaseTestCase):
     def _sleep_some(cls):
         time.sleep(100)
 
+    @classmethod
+    def _sleep_no_int_handler(cls):
+        signal.signal(signal.SIGINT, signal.SIG_DFL)
+        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()
 
@@ -567,6 +572,19 @@ class _TestProcess(BaseTestCase):
 
         return p.exitcode
 
+    @unittest.skipIf(os.name == 'nt', "POSIX only")
+    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)
         self.assertEqual(exitcode, -signal.SIGTERM)
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 (file)
index 0000000..be03652
--- /dev/null
@@ -0,0 +1 @@
+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.