Skip to content

Commit 43cb463

Browse files
authored
[lldb] Fix typed commands not shown on the screen (#174216)
The cause is that in `python3.14`, `fcntl.ioctl` now throws a buffer overflow error when the buffer is too small or too large (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 #173302
1 parent f0275bd commit 43cb463

5 files changed

Lines changed: 131 additions & 51 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 & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,6 @@ def is_libedit():
3232
g_run_one_line_str = None
3333

3434

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-
4735
class LLDBExit(SystemExit):
4836
pass
4937

@@ -74,50 +62,21 @@ def readfunc_stdio(prompt):
7462
def run_python_interpreter(local_dict):
7563
# Pass in the dictionary, for continuity from one session to the next.
7664
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-
)
65+
banner = "Python Interactive Interpreter. To exit, type 'quit()', 'exit()'."
66+
input_func = readfunc_stdio
67+
68+
is_atty = sys.stdin.isatty()
69+
if is_atty:
70+
banner = "Python Interactive Interpreter. To exit, type 'quit()', 'exit()' or Ctrl-D."
71+
input_func = readfunc
72+
73+
code.interact(banner=banner, readfunc=input_func, local=local_dict)
11474
except LLDBExit:
11575
pass
11676
except SystemExit as e:
11777
if e.code:
11878
print("Script exited with code %s" % e.code)
11979

120-
12180
def run_one_line(local_dict, input_string):
12281
global g_run_one_line_str
12382
try:

lldb/test/API/python_api/file_handle/TestFileHandle.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,11 @@ def setUp(self):
111111
super(FileHandleTestCase, self).setUp()
112112
self.out_filename = self.getBuildArtifact("output")
113113
self.in_filename = self.getBuildArtifact("input")
114+
self.err_filename = self.getBuildArtifact("error")
114115

115116
def tearDown(self):
116117
super(FileHandleTestCase, self).tearDown()
117-
for name in (self.out_filename, self.in_filename):
118+
for name in (self.out_filename, self.in_filename, self.err_filename):
118119
if os.path.exists(name):
119120
os.unlink(name)
120121

@@ -679,6 +680,51 @@ def test_stdout_file(self):
679680
lines = [x for x in f.read().strip().split() if x != "7"]
680681
self.assertEqual(lines, ["foobar"])
681682

683+
def test_stdout_file_interactive(self):
684+
"""Ensure when we read stdin from a file, outputs from python goes to the right I/O stream."""
685+
with open(self.in_filename, "w") as f:
686+
f.write(
687+
"script --language python --\nvalue = 250 + 5\nprint(value)\nprint(vel)"
688+
)
689+
690+
with open(self.out_filename, "w") as outf, open(
691+
self.in_filename, "r"
692+
) as inf, open(self.err_filename, "w") as errf:
693+
status = self.dbg.SetOutputFile(lldb.SBFile(outf))
694+
self.assertSuccess(status)
695+
status = self.dbg.SetErrorFile(lldb.SBFile(errf))
696+
self.assertSuccess(status)
697+
status = self.dbg.SetInputFile(lldb.SBFile(inf))
698+
self.assertSuccess(status)
699+
auto_handle_events = True
700+
spawn_thread = False
701+
num_errs = 0
702+
quit_requested = False
703+
stopped_for_crash = False
704+
opts = lldb.SBCommandInterpreterRunOptions()
705+
self.dbg.RunCommandInterpreter(
706+
auto_handle_events,
707+
spawn_thread,
708+
opts,
709+
num_errs,
710+
quit_requested,
711+
stopped_for_crash,
712+
)
713+
self.dbg.GetOutputFile().Flush()
714+
expected_out_text = "255"
715+
expected_err_text = "NameError"
716+
# check stdout
717+
with open(self.out_filename, "r") as f:
718+
out_text = f.read()
719+
self.assertIn(expected_out_text, out_text)
720+
self.assertNotIn(expected_err_text, out_text)
721+
722+
# check stderr
723+
with open(self.err_filename, "r") as f:
724+
err_text = f.read()
725+
self.assertIn(expected_err_text, err_text)
726+
self.assertNotIn(expected_out_text, err_text)
727+
682728
def test_identity(self):
683729
f = io.StringIO()
684730
sbf = lldb.SBFile(f)
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+
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# RUN: rm -rf %t.stdout %t.stderr
2+
# RUN: cat %s | %lldb --script-language python > %t.stdout 2> %t.stderr
3+
# RUN: cat %t.stdout | FileCheck %s --check-prefix STDOUT
4+
# RUN: cat %t.stderr | FileCheck %s --check-prefix STDERR
5+
script
6+
variable = 300
7+
print(variable)
8+
print(not_value)
9+
quit
10+
11+
# STDOUT: 300
12+
# STDERR: NameError{{.*}}is not defined

0 commit comments

Comments
 (0)