-
-
Notifications
You must be signed in to change notification settings - Fork 32.1k
gh-132962: _pyrepl: Prevent crash on Windows when stdout is redirected #135456
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
base: main
Are you sure you want to change the base?
gh-132962: _pyrepl: Prevent crash on Windows when stdout is redirected #135456
Conversation
…irected The interactive interpreter (-i) on Windows no longer crashes when standard output is redirected. The check for `CAN_USE_PYREPL` now also checks if stdout is a TTY on Windows, falling back to the basic REPL if it is not.
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.
Please add a test case. You can refer to the devguide or tag me if you're unsure what that entails.
@ZeroIntensity Thanks for your guidance on adding a test. I've drafted a test case below that correctly fails on the main branch and passes with my fix. I haven't committed and pushed this yet, because I wanted to get your feedback first. I feel the approach is very very ugly, especially the need to use time.sleep and then kill the process. Is this an acceptable way to test this kind of hanging/crashing bug, or do you have any suggestions for a cleaner approach? My rationale for this test: diff --git a/Lib/test/test_pyrepl/test_windows_console.py b/Lib/test/test_pyrepl/test_windows_console.py
index f9607e02c60..963f4db26ba 100644
--- a/Lib/test/test_pyrepl/test_windows_console.py
+++ b/Lib/test/test_pyrepl/test_windows_console.py
@@ -576,6 +576,46 @@ def test_up_vt(self):
Event(evt='key', data='up', raw=bytearray(b'\x1b[A')))
self.assertEqual(self.mock.call_count, 3)
+import subprocess
+from tempfile import TemporaryDirectory
+import os
+import time
+
+class WindowsCommandLineTests(unittest.TestCase):
+ def test_for_crash_traceback_with_redirected_stdout(self):
+ script_command = "print('script has run')"
+ stderr_output = "" # Define in an outer scope
+
+ with TemporaryDirectory() as temp_dir:
+ stdout_path = os.path.join(temp_dir, "WindowsCommandLineTests_stdout.txt")
+
+ with open(stdout_path, "w", encoding="utf-8") as stdout_file:
+ with subprocess.Popen(
+ [sys.executable, '-i', '-c', script_command],
+ stdin=None,
+ stdout=stdout_file,
+ stderr=subprocess.PIPE,
+ text=True, encoding='utf-8', errors='replace'
+ ) as process:
+ time.sleep(3)
+ if process.poll() is None:
+ process.kill()
+
+ stderr_output = process.stderr.read()
+
+ with open(stdout_path, "r", encoding="utf-8") as f:
+ stdout_from_file = f.read()
+
+ has_crash_traceback = (
+ "OSError" in stderr_output
+ and "getheightwidth" in stderr_output
+ and len(stderr_output) > 500 # brilliant check for looping traceback
+ )
+
+ if has_crash_traceback:
+ self.fail(
+ "Detected the endless OSError traceback.\n"
+ )
if __name__ == "__main__":
unittest.main() main branch: PS F:\github\cpython001> ./python.bat -m test test_pyrepl
Running Debug|x64 interpreter...
Using random seed: 2091289539
0:00:00 Run 1 test sequentially in a single process
0:00:00 [1/1] test_pyrepl
test test_pyrepl failed -- Traceback (most recent call last):
File "F:\github\cpython001\Lib\test\test_pyrepl\test_windows_console.py", line 616, in test_for_crash_traceback_with_redirected_stdout
self.fail(
~~~~~~~~~^
"Detected the endless OSError traceback.\n"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
AssertionError: Detected the endless OSError traceback.
0:00:20 load avg: 0.32 [1/1/1] test_pyrepl failed (1 failure)
== Tests result: FAILURE ==
1 test failed:
test_pyrepl
Total duration: 20.1 sec
Total tests: run=236 failures=1 skipped=46
Total test files: run=1/1 failed=1
Result: FAILURE this branch: PS F:\github\cpython001> ./python.bat -m test -j2 test_pyrepl test_pyrepl
Running Debug|x64 interpreter...
Using random seed: 1651374118
0:00:00 Run 2 tests in parallel using 2 worker processes
0:00:19 load avg: 1.43 [1/2] test_pyrepl passed
0:00:19 load avg: 1.43 [2/2] test_pyrepl passed
== Tests result: SUCCESS ==
All 2 tests OK.
Total duration: 19.5 sec
Total tests: run=472 skipped=94
Total test files: run=2/2
Result: SUCCESS |
I think using It would be best to design a test that just exercises the correct behavior; there's no need for the extra checking of the infinite traceback. If the test hangs on the current main, that's OK--our test suite would time it out and fail. Hanging is close enough to a test failure. |
Hi @ZeroIntensity , Thank you again for your valuable feedback on the pyrepl test case. I tried to implement your suggestion of testing for correct behavior (a fast exit), but I've run into some very specific constraints and would be grateful for your advice on a better approach. My goal is to test a bug where the Python interpreter, in interactive mode (-i) with stdout redirected to a file, enters an infinite traceback loop. This test is meant to simulate a real-world scenario. Through testing, I've found several strict environmental requirements to trigger the bug:
With the My main question is: could you suggest a more robust, non-racy pattern to solve this? How can I reliably test for a process that is expected to hang (due to this specific bug), given that I cannot programmatically cause it to exit and must adhere to the strict Thanks again for your time and expertise. edit to attach: Now I am trying to use import subprocess
from tempfile import TemporaryDirectory
import os
class WindowsCommandLineTests(unittest.TestCase):
def test_for_crash_traceback_with_redirected_stdout(self):
"""python.bat -i -c "print('hlwd')" > file.txt"""
script_command = "print('script has run')"
with TemporaryDirectory() as tmp_dir:
stdout_path = os.path.join(tmp_dir, "WinCMDLineTests.txt")
with open(stdout_path, "w") as stdout_file, \
subprocess.Popen(
[sys.executable, '-i', '-c', script_command],
stdin=None,
stdout=stdout_file,
stderr=subprocess.PIPE,
text=True, errors='replace'
) as process:
stderr_output = ""
try:
process.wait(timeout=3)
self.fail("Process exited unexpectedly within the timeout.")
except subprocess.TimeoutExpired:
process.kill()
_, stderr_output = process.communicate()
has_crash_traceback = (
"OSError" in stderr_output and
len(stderr_output) > 1200
)
if has_crash_traceback:
self.fail("Detected endless OSError traceback."
f"\n--- stderr ---\n{stderr_output[:1200]}") main branch: PS F:\github\cpython001> ./python.bat -m test test_pyrepl
Running Debug|x64 interpreter...
Using random seed: 2225918861
0:00:00 Run 1 test sequentially in a single process
0:00:00 [1/1] test_pyrepl
test test_pyrepl failed -- Traceback (most recent call last):
File "F:\github\cpython001\Lib\test\test_pyrepl\test_windows_console.py", line 615, in test_for_crash_traceback_with_redirected_stdout
self.fail("Detected endless OSError traceback."
~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
f"\n--- stderr ---\n{stderr_output[:1200]}")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: Detected endless OSError traceback.
--- stderr ---
Traceback (most recent call last):
File "F:\github\cpython001\Lib\_pyrepl\readline.py", line 394, in multiline_input
return reader.readline()
~~~~~~~~~~~~~~~^^
File "F:\github\cpython001\Lib\_pyrepl\reader.py", line 742, in readline
self.prepare()
~~~~~~~~~~~~^^
File "F:\github\cpython001\Lib\_pyrepl\historical_reader.py", line 306, in prepare
super().prepare()
~~~~~~~~~~~~~~~^^
File "F:\github\cpython001\Lib\_pyrepl\reader.py", line 585, in prepare
self.console.prepare()
~~~~~~~~~~~~~~~~~~~~^^
File "F:\github\cpython001\Lib\_pyrepl\windows_console.py", line 358, in prepare
self.height, self.width = self.getheightwidth()
~~~~~~~~~~~~~~~~~~~^^
File "F:\github\cpython001\Lib\_pyrepl\windows_console.py", line 409, in getheightwidth
raise WinError(GetLastError())
OSError: [WinError 6] The handle is invalid.
Traceback (most recent call last):
File "F:\github\cpython001\Lib\_pyrepl\readline.py", line 394, in multiline_input
return reader.readline()
~~~~~~~~~~~~~~~^^
File "F:\github\cpython001\Lib\_pyrepl\reader.py", line 742, in readline
self.prepare()
~~~~~~~~~~~~^^
File "F:\github\cpyth
0:00:19 load avg: 0.18 [1/1/1] test_pyrepl failed (1 failure)
== Tests result: FAILURE ==
1 test failed:
test_pyrepl
Total duration: 20.0 sec
Total tests: run=236 failures=1 skipped=46
Total test files: run=1/1 failed=1
Result: FAILURE this branch PS F:\github\cpython001> ./python.bat -m test -j2 test_pyrepl test_pyrepl
Running Debug|x64 interpreter...
Using random seed: 1927743178
0:00:00 Run 2 tests in parallel using 2 worker processes
0:00:19 load avg: 0.02 [1/2] test_pyrepl passed
0:00:19 load avg: 0.02 [2/2] test_pyrepl passed
== Tests result: SUCCESS ==
All 2 tests OK.
Total duration: 19.7 sec
Total tests: run=472 skipped=94
Total test files: run=2/2
Result: SUCCESS |
The interactive interpreter (-i) on Windows no longer crashes when standard output is redirected. The check for
CAN_USE_PYREPL
now also checks if stdout is a TTY on Windows, falling back to the basic REPL if it is not.Main branch:
With fix: