Repository navigation
bpo-30696: Fix the REPL looping endlessly when no memory #4160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
560ccee
15230d1
ac6003a
44c335e
be1df91
477f9c5
1516d4c
1fe98b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,9 @@ | |
| import unittest | ||
| import urllib.error | ||
| import warnings | ||
| import selectors | ||
| from contextlib import ExitStack | ||
| from errno import EIO | ||
|
|
||
| try: | ||
| import multiprocessing.process | ||
|
|
@@ -105,6 +108,7 @@ | |
| "run_with_locale", "swap_item", | ||
| "swap_attr", "Matcher", "set_memlimit", "SuppressCrashReport", "sortdict", | ||
| "run_with_tz", "PGO", "missing_compiler_executable", "fd_count", | ||
| "run_pty", | ||
| ] | ||
|
|
||
| class Error(Exception): | ||
|
|
@@ -2756,6 +2760,60 @@ def fd_count(): | |
|
|
||
| return count | ||
|
|
||
| def run_pty(script, input=b"dummy input\r", env=None): | ||
| pty = import_module('pty') | ||
| output = bytearray() | ||
| [master, slave] = pty.openpty() | ||
| if script is None: | ||
| args = (sys.executable, '-q') | ||
| else: | ||
| args = (sys.executable, '-c', script) | ||
| proc = subprocess.Popen(args, stdin=slave, stdout=slave, stderr=slave, env=env) | ||
| os.close(slave) | ||
| with ExitStack() as cleanup: | ||
| cleanup.enter_context(proc) | ||
| def terminate(proc): | ||
| try: | ||
| proc.kill() | ||
| except ProcessLookupError: | ||
| # Workaround for Open/Net BSD bug (Issue 16762) | ||
| pass | ||
| cleanup.callback(terminate, proc) | ||
| cleanup.callback(os.close, master) | ||
| # Avoid using DefaultSelector and PollSelector. Kqueue() does not | ||
| # work with pseudo-terminals on OS X < 10.9 (Issue 20365) and Open | ||
| # BSD (Issue 20667). Poll() does not work with OS X 10.6 or 10.4 | ||
| # either (Issue 20472). Hopefully the file descriptor is low enough | ||
| # to use with select(). | ||
| sel = cleanup.enter_context(selectors.SelectSelector()) | ||
| os.set_blocking(master, False) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had prefer to set master to non-blocking mode before registering it into the selector. By the way, a selector must not accept a FD in blocking more :-) |
||
| sel.register(master, selectors.EVENT_READ | selectors.EVENT_WRITE) | ||
| while sel.get_map(): | ||
| for [key, events] in sel.select(): | ||
| if events & selectors.EVENT_READ: | ||
| try: | ||
| chunk = os.read(master, 0x10000) | ||
| except OSError as err: | ||
| # Linux raises EIO when slave is closed (Issue 5380) | ||
| if err.errno != EIO: | ||
| raise | ||
| chunk = b"" | ||
| if not chunk: | ||
| sel.unregister(key.fileobj) | ||
| break | ||
| output.extend(chunk) | ||
| if events & selectors.EVENT_WRITE: | ||
| try: | ||
| input = input[os.write(master, input):] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please call os.write() on a separated line: You might move the "input = ..." in an else block. It's up to you. |
||
| except OSError as err: | ||
| # Apparently EIO means the slave was closed | ||
| if err.errno != EIO: | ||
| raise | ||
| input = b"" # Stop writing | ||
| if not input: | ||
| sel.modify(master, selectors.EVENT_READ) | ||
| proc.wait() | ||
| return proc.returncode, output | ||
|
|
||
| class SaveSignals: | ||
| """ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,11 @@ | ||
| """ | ||
| Very minimal unittests for parts of the readline module. | ||
| """ | ||
| from contextlib import ExitStack | ||
| from errno import EIO | ||
| import os | ||
| import selectors | ||
| import subprocess | ||
| import sys | ||
| import tempfile | ||
| import unittest | ||
| from test.support import import_module, unlink, temp_dir, TESTFN, verbose | ||
| from test.support import (import_module, unlink, temp_dir, TESTFN, verbose, | ||
| run_pty) | ||
| from test.support.script_helper import assert_python_ok | ||
|
|
||
| # Skip tests if there is no readline module | ||
|
|
@@ -145,11 +141,11 @@ def test_init(self): | |
| """ | ||
|
|
||
| def test_auto_history_enabled(self): | ||
| output = run_pty(self.auto_history_script.format(True)) | ||
| rc, output = run_pty(self.auto_history_script.format(True)) | ||
| self.assertIn(b"History length: 1\r\n", output) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You may add self.assertEqual(rc, 0) in these tests after checking the output. |
||
|
|
||
| def test_auto_history_disabled(self): | ||
| output = run_pty(self.auto_history_script.format(False)) | ||
| rc, output = run_pty(self.auto_history_script.format(False)) | ||
| self.assertIn(b"History length: 0\r\n", output) | ||
|
|
||
| def test_nonascii(self): | ||
|
|
@@ -215,7 +211,7 @@ def display(substitution, matches, longest_match_length): | |
| input += b"\t\t" # Display possible completions | ||
| input += b"x\t" # Complete "t\xEBx" -> "t\xEBxt" | ||
| input += b"\r" | ||
| output = run_pty(script, input) | ||
| rc, output = run_pty(script, input) | ||
| self.assertIn(b"text 't\\xeb'\r\n", output) | ||
| self.assertIn(b"line '[\\xefnserted]|t\\xeb[after]'\r\n", output) | ||
| self.assertIn(b"indexes 11 13\r\n", output) | ||
|
|
@@ -271,55 +267,5 @@ def test_history_size(self): | |
| self.assertEqual(lines[-1].strip(), b"last input") | ||
|
|
||
|
|
||
| def run_pty(script, input=b"dummy input\r", env=None): | ||
| pty = import_module('pty') | ||
| output = bytearray() | ||
| [master, slave] = pty.openpty() | ||
| args = (sys.executable, '-c', script) | ||
| proc = subprocess.Popen(args, stdin=slave, stdout=slave, stderr=slave, env=env) | ||
| os.close(slave) | ||
| with ExitStack() as cleanup: | ||
| cleanup.enter_context(proc) | ||
| def terminate(proc): | ||
| try: | ||
| proc.terminate() | ||
| except ProcessLookupError: | ||
| # Workaround for Open/Net BSD bug (Issue 16762) | ||
| pass | ||
| cleanup.callback(terminate, proc) | ||
| cleanup.callback(os.close, master) | ||
| # Avoid using DefaultSelector and PollSelector. Kqueue() does not | ||
| # work with pseudo-terminals on OS X < 10.9 (Issue 20365) and Open | ||
| # BSD (Issue 20667). Poll() does not work with OS X 10.6 or 10.4 | ||
| # either (Issue 20472). Hopefully the file descriptor is low enough | ||
| # to use with select(). | ||
| sel = cleanup.enter_context(selectors.SelectSelector()) | ||
| sel.register(master, selectors.EVENT_READ | selectors.EVENT_WRITE) | ||
| os.set_blocking(master, False) | ||
| while True: | ||
| for [_, events] in sel.select(): | ||
| if events & selectors.EVENT_READ: | ||
| try: | ||
| chunk = os.read(master, 0x10000) | ||
| except OSError as err: | ||
| # Linux raises EIO when slave is closed (Issue 5380) | ||
| if err.errno != EIO: | ||
| raise | ||
| chunk = b"" | ||
| if not chunk: | ||
| return output | ||
| output.extend(chunk) | ||
| if events & selectors.EVENT_WRITE: | ||
| try: | ||
| input = input[os.write(master, input):] | ||
| except OSError as err: | ||
| # Apparently EIO means the slave was closed | ||
| if err.errno != EIO: | ||
| raise | ||
| input = b"" # Stop writing | ||
| if not input: | ||
| sel.modify(master, selectors.EVENT_READ) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| """Test the interactive interpreter.""" | ||
|
|
||
| import unittest | ||
| from textwrap import dedent | ||
| from test.support import run_pty, SuppressCrashReport | ||
|
|
||
| class TestInteractiveInterpreter(unittest.TestCase): | ||
|
|
||
| def test_no_memory(self): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please decorate the test with @support.cpython_only, or use support.import_module('_testcapi'), to skip the test on Python implementations without _testcapi. |
||
| # Issue #30696: Fix the interactive interpreter looping endlessly when | ||
| # no memory. Check also that the fix does not break the interactive | ||
| # loop when an exception is raised. | ||
| user_input = """ | ||
| import sys, _testcapi | ||
| 1/0 | ||
| print('After the exception.') | ||
| _testcapi.set_nomemory(0) | ||
| sys.exit(0) | ||
| """ | ||
| user_input = dedent(user_input) | ||
| user_input = b'\r'.join(x.encode() for x in user_input.split('\n')) | ||
| user_input += b'\r' | ||
| with SuppressCrashReport(): | ||
| rc, output = run_pty(None, input=user_input) | ||
| self.assertIn(b'After the exception.\r\n>>>', output) | ||
| self.assertIn(rc, (1, 120)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I didn't know this exit code, please add a short comment: |
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Fix the interactive interpreter looping endlessly when no memory. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,6 +65,7 @@ static PyObject *run_pyc_file(FILE *, const char *, PyObject *, PyObject *, | |
| PyCompilerFlags *); | ||
| static void err_input(perrdetail *); | ||
| static void err_free(perrdetail *); | ||
| static int PyRun_InteractiveOneObjectEx(FILE *, PyObject *, PyCompilerFlags *); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure that we need to make this new function public. I propose to make it private. Please add also a short comment to explain that it's similar to PyRun_InteractiveOneObject() but don't print the error on failure.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not understand your comment, this new function is static.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I was wrong. I understood that the function was public and exported. It's ok :-) |
||
|
|
||
| /* Parse input from a file and execute it */ | ||
| int | ||
|
|
@@ -89,6 +90,7 @@ PyRun_InteractiveLoopFlags(FILE *fp, const char *filename_str, PyCompilerFlags * | |
| PyObject *filename, *v; | ||
| int ret, err; | ||
| PyCompilerFlags local_flags; | ||
| static int nomem_count = 0; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a comment to explain why the variable must be static. Do we really have to remember the value between two calls?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It does not need to be static, I will correct that. |
||
|
|
||
| filename = PyUnicode_DecodeFSDefault(filename_str); | ||
| if (filename == NULL) { | ||
|
|
@@ -110,22 +112,32 @@ PyRun_InteractiveLoopFlags(FILE *fp, const char *filename_str, PyCompilerFlags * | |
| _PySys_SetObjectId(&PyId_ps2, v = PyUnicode_FromString("... ")); | ||
| Py_XDECREF(v); | ||
| } | ||
| err = -1; | ||
| for (;;) { | ||
| ret = PyRun_InteractiveOneObject(fp, filename, flags); | ||
| err = 0; | ||
| do { | ||
| ret = PyRun_InteractiveOneObjectEx(fp, filename, flags); | ||
| if (ret == -1 && PyErr_Occurred()) { | ||
| /* Prevent an endless loop after multiple consecutive MemoryErrors | ||
| * while still allowing an interactive command to fail with a | ||
| * MemoryError. */ | ||
| if (PyErr_ExceptionMatches(PyExc_MemoryError)) { | ||
| if (++nomem_count > 16) { | ||
| PyErr_Clear(); | ||
| err = -1; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please document in Doc/c-api/veryhigh.rst that PyRun_InteractiveLoopFlags() returns a negative number on failure.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you :-) |
||
| break; | ||
| } | ||
| } else { | ||
| nomem_count = 0; | ||
| } | ||
| PyErr_Print(); | ||
| flush_io(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer to exit at the first MemoryError. You can write your own code catching MemoryError, like the code module does, no?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After a MemoryError has been raised and when PyRun_InteractiveOneObjectEx() returns, the memory of the frames locals has been released upon unwinding the frames stack and enough memory may be available to continue running the REPL, so it is not clear why the user should not be allowed to enter another command at the next prompt in this case. BTW it seems that the only module in the standard library (excluding the tests) that catches MemoryError is the linecache module. |
||
| } else { | ||
| nomem_count = 0; | ||
| } | ||
| #ifdef Py_REF_DEBUG | ||
| if (_PyDebug_XOptionShowRefCount() == Py_True) | ||
| _PyDebug_PrintTotalRefs(); | ||
| #endif | ||
| if (ret == E_EOF) { | ||
| err = 0; | ||
| break; | ||
| } | ||
| /* | ||
| if (ret == E_NOMEM) | ||
| break; | ||
| */ | ||
| } | ||
| } while (ret != E_EOF); | ||
| Py_DECREF(filename); | ||
| return err; | ||
| } | ||
|
|
@@ -154,8 +166,9 @@ static int PARSER_FLAGS(PyCompilerFlags *flags) | |
| PyPARSE_WITH_IS_KEYWORD : 0)) : 0) | ||
| #endif | ||
|
|
||
| int | ||
| PyRun_InteractiveOneObject(FILE *fp, PyObject *filename, PyCompilerFlags *flags) | ||
| static int | ||
| PyRun_InteractiveOneObjectEx(FILE *fp, PyObject *filename, | ||
| PyCompilerFlags *flags) | ||
| { | ||
| PyObject *m, *d, *v, *w, *oenc = NULL, *mod_name; | ||
| mod_ty mod; | ||
|
|
@@ -167,7 +180,6 @@ PyRun_InteractiveOneObject(FILE *fp, PyObject *filename, PyCompilerFlags *flags) | |
|
|
||
| mod_name = _PyUnicode_FromId(&PyId___main__); /* borrowed */ | ||
| if (mod_name == NULL) { | ||
| PyErr_Print(); | ||
| return -1; | ||
| } | ||
|
|
||
|
|
@@ -227,7 +239,6 @@ PyRun_InteractiveOneObject(FILE *fp, PyObject *filename, PyCompilerFlags *flags) | |
| PyErr_Clear(); | ||
| return E_EOF; | ||
| } | ||
| PyErr_Print(); | ||
| return -1; | ||
| } | ||
| m = PyImport_AddModuleObject(mod_name); | ||
|
|
@@ -239,15 +250,26 @@ PyRun_InteractiveOneObject(FILE *fp, PyObject *filename, PyCompilerFlags *flags) | |
| v = run_mod(mod, filename, d, d, flags, arena); | ||
| PyArena_Free(arena); | ||
| if (v == NULL) { | ||
| PyErr_Print(); | ||
| flush_io(); | ||
| return -1; | ||
| } | ||
| Py_DECREF(v); | ||
| flush_io(); | ||
| return 0; | ||
| } | ||
|
|
||
| int | ||
| PyRun_InteractiveOneObject(FILE *fp, PyObject *filename, PyCompilerFlags *flags) | ||
| { | ||
| int res; | ||
|
|
||
| res = PyRun_InteractiveOneObjectEx(fp, filename, flags); | ||
| if (res == -1) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we shouldn't call PyErr_Print() + flush_io() in the worst MemoryError case which cleared the error (PyErr_Clear()), since flush_io() is likely to raise a new exception. What do you think of replacing "if (res == -1)" with "if (res == -1 && PyErr_Occurred())" ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there may be a confusion: the worst MemoryError case which cleared the error (PyErr_Clear()) occurs in PyRun_InteractiveLoopFlags(), but here we are in PyRun_InteractiveOneObject() and this function does not call PyRun_InteractiveLoopFlags(). The substitution you suggest makes sense, but the initial version of PyRun_InteractiveOneObject() did not call PyErr_Occurred() and I prefer not to change that. What is modified by the PR though, is that now when PyImport_AddModuleObject() returns NULL in PyRun_InteractiveOneObjectEx() then PyErr_Print() is called now while it is not in the initial version.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, I was confused between the "One" and "Loop" flavors. You can leave it unchanged. |
||
| PyErr_Print(); | ||
| flush_io(); | ||
| } | ||
| return res; | ||
| } | ||
|
|
||
| int | ||
| PyRun_InteractiveOneFlags(FILE *fp, const char *filename_str, PyCompilerFlags *flags) | ||
| { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I propose to add a docstring to document the side effect of import_module('pty'):
"""
Run a script in a temporary pseudoterminal created just for the script.
Raise SkipTest if the pty module cannot be imported.
"""