summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorGregory P. Smith <greg@krypto.org>2019-02-16 20:57:40 (GMT)
committerGitHub <noreply@github.com>2019-02-16 20:57:40 (GMT)
commit38f11cc3f62db11a4a24354bd06273322ac91afa (patch)
treec41a7fd4bc4b3923cbfdd04664afc281947f56f8 /Lib
parent43766f82ddec84fad7a321eeec2e1cbff6ee44d2 (diff)
downloadcpython-38f11cc3f62db11a4a24354bd06273322ac91afa.zip
cpython-38f11cc3f62db11a4a24354bd06273322ac91afa.tar.gz
cpython-38f11cc3f62db11a4a24354bd06273322ac91afa.tar.bz2
bpo-1054041: Exit properly after an uncaught ^C. (#11862)
* bpo-1054041: Exit properly by a signal after a ^C. An uncaught KeyboardInterrupt exception means the user pressed ^C and our code did not handle it. Programs that install SIGINT handlers are supposed to reraise the SIGINT signal to the SIG_DFL handler in order to exit in a manner that their calling process can detect that they died due to a Ctrl-C. https://www.cons.org/cracauer/sigint.html After this change on POSIX systems while true; do python -c 'import time; time.sleep(23)'; done can be stopped via a simple Ctrl-C instead of the shell infinitely restarting a new python process. What to do on Windows, or if anything needs to be done there has not yet been determined. That belongs in its own PR. TODO(gpshead): A unittest for this behavior is still needed. * Do the unhandled ^C check after pymain_free. * Return STATUS_CONTROL_C_EXIT on Windows. * Fix ifdef around unistd.h include. * 📜🤖 Added by blurb_it. * Add STATUS_CTRL_C_EXIT to the os module on Windows * Add unittests. * Don't send CTRL_C_EVENT in the Windows test. It was causing CI systems to bail out of the entire test suite. See https://dev.azure.com/Python/cpython/_build/results?buildId=37980 for example. * Correct posix test (fail on macOS?) check. * STATUS_CONTROL_C_EXIT must be unsigned. * Improve the error message. * test typo :) * Skip if the bash version is too old. ...and rename the windows test to reflect what it does. * min bash version is 4.4, detect no bash. * restore a blank line i didn't mean to delete. * PyErr_Occurred() before the Py_DECREF(co); * Don't add os.STATUS_CONTROL_C_EXIT as a constant. * Update the Windows test comment. * Refactor common logic into a run_eval_code_obj fn.
Diffstat (limited to 'Lib')
-rw-r--r--Lib/test/test_signal.py61
1 files changed, 57 insertions, 4 deletions
diff --git a/Lib/test/test_signal.py b/Lib/test/test_signal.py
index 2a6217e..80c0ff4 100644
--- a/Lib/test/test_signal.py
+++ b/Lib/test/test_signal.py
@@ -78,6 +78,48 @@ class PosixTests(unittest.TestCase):
self.assertNotIn(signal.NSIG, s)
self.assertLess(len(s), signal.NSIG)
+ @unittest.skipUnless(sys.executable, "sys.executable required.")
+ def test_keyboard_interrupt_exit_code(self):
+ """KeyboardInterrupt triggers exit via SIGINT."""
+ process = subprocess.run(
+ [sys.executable, "-c",
+ "import os,signal; os.kill(os.getpid(), signal.SIGINT)"],
+ stderr=subprocess.PIPE)
+ self.assertIn(b"KeyboardInterrupt", process.stderr)
+ self.assertEqual(process.returncode, -signal.SIGINT)
+
+ @unittest.skipUnless(sys.executable, "sys.executable required.")
+ def test_keyboard_interrupt_communicated_to_shell(self):
+ """KeyboardInterrupt exits such that shells detect a ^C."""
+ try:
+ bash_proc = subprocess.run(
+ ["bash", "-c", 'echo "${BASH_VERSION}"'],
+ stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
+ except OSError:
+ raise unittest.SkipTest("bash required.")
+ if bash_proc.returncode:
+ raise unittest.SkipTest("could not determine bash version.")
+ bash_ver = bash_proc.stdout.decode("ascii").strip()
+ bash_major_minor = [int(n) for n in bash_ver.split(".", 2)[:2]]
+ if bash_major_minor < [4, 4]:
+ # In older versions of bash, -i does not work as needed
+ # _for this automated test_. Older shells do behave as
+ # expected in manual interactive use.
+ raise unittest.SkipTest(f"bash version {bash_ver} is too old.")
+ # The motivation for https://bugs.python.org/issue1054041.
+ # An _interactive_ shell (bash -i simulates that here) detects
+ # when a command exits via ^C and stops executing further
+ # commands.
+ process = subprocess.run(
+ ["bash", "-ic",
+ f"{sys.executable} -c 'import os,signal; os.kill(os.getpid(), signal.SIGINT)'; "
+ "echo TESTFAIL using bash \"${BASH_VERSION}\""],
+ stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+ self.assertIn(b"KeyboardInterrupt", process.stderr)
+ # An interactive shell will abort if python exits properly to
+ # indicate that a KeyboardInterrupt occurred.
+ self.assertNotIn(b"TESTFAIL", process.stdout)
+
@unittest.skipUnless(sys.platform == "win32", "Windows specific")
class WindowsSignalTests(unittest.TestCase):
@@ -112,6 +154,20 @@ class WindowsSignalTests(unittest.TestCase):
with self.assertRaises(ValueError):
signal.signal(7, handler)
+ @unittest.skipUnless(sys.executable, "sys.executable required.")
+ def test_keyboard_interrupt_exit_code(self):
+ """KeyboardInterrupt triggers an exit using STATUS_CONTROL_C_EXIT."""
+ # We don't test via os.kill(os.getpid(), signal.CTRL_C_EVENT) here
+ # as that requires setting up a console control handler in a child
+ # in its own process group. Doable, but quite complicated. (see
+ # @eryksun on https://github.com/python/cpython/pull/11862)
+ process = subprocess.run(
+ [sys.executable, "-c", "raise KeyboardInterrupt"],
+ stderr=subprocess.PIPE)
+ self.assertIn(b"KeyboardInterrupt", process.stderr)
+ STATUS_CONTROL_C_EXIT = 0xC000013A
+ self.assertEqual(process.returncode, STATUS_CONTROL_C_EXIT)
+
class WakeupFDTests(unittest.TestCase):
@@ -1217,11 +1273,8 @@ class StressTest(unittest.TestCase):
class RaiseSignalTest(unittest.TestCase):
def test_sigint(self):
- try:
+ with self.assertRaises(KeyboardInterrupt):
signal.raise_signal(signal.SIGINT)
- self.fail("Expected KeyInterrupt")
- except KeyboardInterrupt:
- pass
@unittest.skipIf(sys.platform != "win32", "Windows specific test")
def test_invalid_argument(self):