Skip to content
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

bpo-30696: Fix the REPL looping endlessly when no memory #4160

Merged
merged 8 commits into from
Nov 12, 2017
Merged

bpo-30696: Fix the REPL looping endlessly when no memory #4160

merged 8 commits into from
Nov 12, 2017

Conversation

xdegaye
Copy link
Contributor

@xdegaye xdegaye commented Oct 28, 2017

This also fixes partly PyRun_InteractiveOneFlags() that was returning -1 with no exception set.

https://bugs.python.org/issue30696

This also fixes partly ``PyRun_InteractiveOneFlags()`` that was returning -1
with no exception set.
@xdegaye
Copy link
Contributor Author

xdegaye commented Oct 28, 2017

Could not find a better place than test_readline.py for the test case. The run_pty() function of this module may be moved to test.support but where to put the test case, this is the first test case for the REPL.

Copy link
Member

@vstinner vstinner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dislike retrying on MemoryError. I would prefer to exit immediately.

I also dislike calling Py_FatalError() which we can return an error code.

You may ignore my comments on run_pty(), I didn't notice that you only moved code.

cleanup.enter_context(proc)
def terminate(proc):
try:
proc.terminate()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use kill() instead, no need to be nice if something goes wrong.

Moreover, you should always call proc.wait() after, to prevent zombie process.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI exiting the with statement (after the enter_context call) should already guarantee one call to wait. See the bottom of https://docs.python.org/3/library/subprocess.html#subprocess.Popen: “Popen objects are supported as context managers . . . the process is waited for.”

# to use with select().
sel = cleanup.enter_context(selectors.SelectSelector())
sel.register(master, selectors.EVENT_READ | selectors.EVENT_WRITE)
os.set_blocking(master, False)
Copy link
Member

Choose a reason for hiding this comment

The 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 :-)

output.extend(chunk)
if events & selectors.EVENT_WRITE:
try:
input = input[os.write(master, input):]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please call os.write() on a separated line:

written = os.write(...)
input = input[written:]

You might move the "input = ..." in an else block. It's up to you.

nomem_count = 0;
}
PyErr_Print();
flush_io();
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

* MemoryError. */
if (PyErr_ExceptionMatches(PyExc_MemoryError)) {
if (++nomem_count > 16) {
Py_FatalError("Cannot recover from MemoryErrors.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't call Py_FatalError(), but return with an error code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When returning -1 and exiting at the first MemoryError and there is indeed no memory left upon returning -1, the error message does not indicate that there has been a MemoryError:

$ ./python -q
>>> from _testcapi import set_nomemory
>>> set_nomemory(0)
Error in atexit._run_exitfuncs:
Error in atexit._run_exitfuncs:
$

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In real life Py_FatalError() would also fail to print the error msg since printf() may rely on malloc(). And returning -1 is consistent with the behavior of the other PyRun_*() functions.

I will change PyRun_InteractiveLoopFlags() to return -1 as you have suggested :-)

@bedevere-bot
Copy link

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

@xdegaye
Copy link
Contributor Author

xdegaye commented Nov 5, 2017

I have made the requested changes; please review again.

@bedevere-bot
Copy link

Thanks for making the requested changes!

@Haypo: please review the changes made to this pull request.

@vadmium
Copy link
Member

vadmium commented Nov 6, 2017

Perhaps you can test with “python -i”, and use pipes rather than a terminal. That should be more portable, avoid platform quirks, give better error handling, be better understood, etc. It looks like there are a few tests in test_cmd_line.py that do this.

Copy link
Member

@vstinner vstinner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for removing the Py_FatalError() call! It's now much better ;-) Second round of review.

@@ -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 *);
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not understand your comment, this new function is static.

Copy link
Member

Choose a reason for hiding this comment

The 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 :-)

@@ -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;
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not need to be static, I will correct that.

if (PyErr_ExceptionMatches(PyExc_MemoryError)) {
if (++nomem_count > 16) {
PyErr_Clear();
err = -1;
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you :-)

@@ -2756,6 +2760,60 @@ def fd_count():

return count

def run_pty(script, input=b"dummy input\r", env=None):
Copy link
Member

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.
"""

@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The 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.

with SuppressCrashReport():
rc, output = run_pty(None, input=user_input)
self.assertIn(b'After the exception.\r\n>>>', output)
self.assertIn(rc, (1, 120))
Copy link
Member

Choose a reason for hiding this comment

The 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:

# Exit code 120: Py_FinalizeEx() failed to flush stdout and stderr.


class TestInteractiveInterpreter(unittest.TestCase):

def test_no_memory(self):
Copy link
Member

Choose a reason for hiding this comment

The 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.

@xdegaye
Copy link
Contributor Author

xdegaye commented Nov 6, 2017

@vadmium wrote:

Perhaps you can test with “python -i”, and use pipes rather than a terminal.

In PyRun_AnyFileExFlags(), PyRun_InteractiveLoopFlags() is only called if Py_FdIsInteractive() returns true, so a terminal is required to test that function.

@vadmium also wrote (the following text received from notifications@github.com seems to be lost by github and cannot be found in the PR so I reproduce it here in extenso):

In Lib/test/support/init.py:

+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.terminate()
    

FYI exiting the with statement (after the enter_context call) should already guarantee one call to wait. See the bottom of https://docs.python.org/3/library/subprocess.html#subprocess.Popen: “Popen objects are supported as context managers . . . the process is waited for.”

Thanks, I will remove the call to proc.wait().

@vadmium
Copy link
Member

vadmium commented Nov 6, 2017

Py_FdIsInteractive also checks Py_InteractiveFlag for special values of filename, and that flag is enabled by “python -i”. Sorry for being persistent, but did you actually try that option out?

@xdegaye
Copy link
Contributor Author

xdegaye commented Nov 6, 2017

@Haypo and @vadmium
@vadmium wrote:

Py_FdIsInteractive also checks Py_InteractiveFlag for special values of filename, and that flag is enabled by “python -i”. Sorry for being persistent, but did you actually try that option out?

No problem.
I did not try hard enough :-)
You are right and with those last changes, the test runs now using pipes instead of a terminal. So I have reverted the changes made previously to test.support and test_readline and they both remain unchanged by the PR.

@xdegaye
Copy link
Contributor Author

xdegaye commented Nov 6, 2017

I have made the requested changes; please review again.

@bedevere-bot
Copy link

Thanks for making the requested changes!

@Haypo: please review the changes made to this pull request.

Copy link
Member

@vstinner vstinner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thank you! The PR now looks much better than the first iteration ;-)

I proposed a last change, but I'm not sure that it's worth it/needed.

int res;

res = PyRun_InteractiveOneObjectEx(fp, filename, flags);
if (res == -1) {
Copy link
Member

Choose a reason for hiding this comment

The 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())" ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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.

@xdegaye xdegaye merged commit e0582a3 into python:master Nov 12, 2017
@miss-islington
Copy link
Contributor

Thanks @xdegaye for the PR 🌮🎉.. I'm working now to backport this PR to: 3.6.
🐍🍒⛏🤖 I'm not a witch! I'm not a witch!

@miss-islington
Copy link
Contributor

Sorry, @xdegaye, I could not cleanly backport this to 3.6 due to a conflict.
Please backport using cherry_picker on command line.
cherry_picker e0582a37c8d1776a2fd4968e9216f3a05f780276 3.6

@bedevere-bot
Copy link

GH-4379 is a backport of this pull request to the 3.6 branch.

xdegaye added a commit that referenced this pull request Nov 12, 2017
@xdegaye xdegaye deleted the bpo-30696 branch January 19, 2018 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants