diff options
author | Gregory P. Smith <greg@krypto.org> | 2019-02-16 20:57:40 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-02-16 20:57:40 (GMT) |
commit | 38f11cc3f62db11a4a24354bd06273322ac91afa (patch) | |
tree | c41a7fd4bc4b3923cbfdd04664afc281947f56f8 /Lib | |
parent | 43766f82ddec84fad7a321eeec2e1cbff6ee44d2 (diff) | |
download | cpython-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.py | 61 |
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): |