Skip to content

Commit dcbcd06

Browse files
committed
[lldb] Fix typed commands not shown on the screen
The issue is that in python3.14, `fcntl.ioctl` now throws a buffer overflow error when the buffer is too small (see python/cpython#132919). This caused the Python interpreter to fail terminal detection and not properly echo user commands back to the screen. Fix by dropping the custom terminal size check entirely and using the built-in `sys.stdin.isatty()` instead. Fixes llvm#173302
1 parent 52b4470 commit dcbcd06

3 files changed

Lines changed: 72 additions & 53 deletions

File tree

lldb/packages/Python/lldbsuite/test/lldbpexpect.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111

1212
@skipIfRemote
13+
@skipIfWindows
1314
@add_test_categories(["pexpect"])
1415
class PExpectTest(TestBase):
1516
NO_DEBUG_INFO_TESTCASE = True

lldb/source/Interpreter/embedded_interpreter.py

Lines changed: 9 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import sys
22
import builtins
33
import code
4-
import lldb
5-
import traceback
64

75
try:
86
import readline
@@ -31,19 +29,6 @@ def is_libedit():
3129

3230
g_run_one_line_str = None
3331

34-
35-
def get_terminal_size(fd):
36-
try:
37-
import fcntl
38-
import termios
39-
import struct
40-
41-
hw = struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234"))
42-
except:
43-
hw = (0, 0)
44-
return hw
45-
46-
4732
class LLDBExit(SystemExit):
4833
pass
4934

@@ -74,50 +59,21 @@ def readfunc_stdio(prompt):
7459
def run_python_interpreter(local_dict):
7560
# Pass in the dictionary, for continuity from one session to the next.
7661
try:
77-
fd = sys.stdin.fileno()
78-
interacted = False
79-
if get_terminal_size(fd)[1] == 0:
80-
try:
81-
import termios
82-
83-
old = termios.tcgetattr(fd)
84-
if old[3] & termios.ECHO:
85-
# Need to turn off echoing and restore
86-
new = termios.tcgetattr(fd)
87-
new[3] = new[3] & ~termios.ECHO
88-
try:
89-
termios.tcsetattr(fd, termios.TCSADRAIN, new)
90-
interacted = True
91-
code.interact(
92-
banner="Python Interactive Interpreter. To exit, type 'quit()', 'exit()'.",
93-
readfunc=readfunc_stdio,
94-
local=local_dict,
95-
)
96-
finally:
97-
termios.tcsetattr(fd, termios.TCSADRAIN, old)
98-
except:
99-
pass
100-
# Don't need to turn off echoing
101-
if not interacted:
102-
code.interact(
103-
banner="Python Interactive Interpreter. To exit, type 'quit()', 'exit()' or Ctrl-D.",
104-
readfunc=readfunc_stdio,
105-
local=local_dict,
106-
)
107-
else:
108-
# We have a real interactive terminal
109-
code.interact(
110-
banner="Python Interactive Interpreter. To exit, type 'quit()', 'exit()' or Ctrl-D.",
111-
readfunc=readfunc,
112-
local=local_dict,
113-
)
62+
banner = "Python Interactive Interpreter. To exit, type 'quit()', 'exit()'."
63+
input_func = readfunc_stdio
64+
65+
is_atty = sys.stdin.isatty()
66+
if is_atty:
67+
banner = "Python Interactive Interpreter. To exit, type 'quit()', 'exit()' or Ctrl-D."
68+
input_func = readfunc
69+
70+
code.interact(banner=banner, readfunc=input_func, local=local_dict)
11471
except LLDBExit:
11572
pass
11673
except SystemExit as e:
11774
if e.code:
11875
print("Script exited with code %s" % e.code)
11976

120-
12177
def run_one_line(local_dict, input_string):
12278
global g_run_one_line_str
12379
try:
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""
2+
Test that typing python expression in the terminal is echoed back to stdout.
3+
"""
4+
5+
from lldbsuite.test.decorators import skipIfAsan
6+
from lldbsuite.test.lldbpexpect import PExpectTest
7+
8+
9+
@skipIfAsan
10+
class PythonInterpreterEchoTest(PExpectTest):
11+
PYTHON_PROMPT = ">>> "
12+
13+
def verify_command_echo(
14+
self, command: str, expected_output: str = "", is_regex: bool = False
15+
):
16+
assert self.child != None
17+
child = self.child
18+
self.assertIsNotNone(self.child, "expected a running lldb process.")
19+
20+
child.sendline(command)
21+
22+
# Build pattern list: match whichever comes first (output or prompt)
23+
# This prevents waiting for a timeout if there's no match
24+
pattern = []
25+
match_expected = expected_output and len(expected_output) > 0
26+
27+
if match_expected:
28+
pattern.append(expected_output)
29+
pattern.append(self.PYTHON_PROMPT)
30+
31+
expect_func = child.expect if is_regex else child.expect_exact
32+
match_idx = expect_func(pattern)
33+
if match_expected:
34+
self.assertEqual(
35+
match_idx, 0, "Expected output `{expected_output}` in stdout."
36+
)
37+
38+
self.assertIsNotNone(self.child.before, "Expected output before prompt")
39+
self.assertIsInstance(self.child.before, bytes)
40+
echoed_text: str = self.child.before.decode("ascii").strip()
41+
self.assertEqual(
42+
command, echoed_text, f"Command '{command}' should be echoed to stdout."
43+
)
44+
45+
if match_expected:
46+
child.expect_exact(self.PYTHON_PROMPT)
47+
48+
def test_python_interpreter_echo(self):
49+
"""Test that that the user typed commands is echoed to stdout"""
50+
51+
self.launch(use_colors=False, dimensions=(100, 100))
52+
53+
# enter the python interpreter
54+
self.verify_command_echo(
55+
"script --language python --", expected_output="Python.*\\.", is_regex=True
56+
)
57+
self.child_in_script_interpreter = True
58+
59+
self.verify_command_echo("val = 300")
60+
self.verify_command_echo(
61+
"print('result =', 300)", expected_output="result = 300"
62+
)

0 commit comments

Comments
 (0)