Skip to content

Conversation

@da-viper
Copy link
Contributor

@da-viper da-viper commented Jan 2, 2026

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

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
@da-viper da-viper requested a review from medismailben January 2, 2026 14:59
@da-viper da-viper requested a review from JDevlieghere as a code owner January 2, 2026 14:59
@llvmbot llvmbot added the lldb label Jan 2, 2026
@llvmbot
Copy link
Member

llvmbot commented Jan 2, 2026

@llvm/pr-subscribers-lldb

Author: Ebuka Ezike (da-viper)

Changes

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


Full diff: https://github.com/llvm/llvm-project/pull/174216.diff

3 Files Affected:

  • (modified) lldb/packages/Python/lldbsuite/test/lldbpexpect.py (+1)
  • (modified) lldb/source/Interpreter/embedded_interpreter.py (+9-53)
  • (added) lldb/test/API/terminal/TestPythonInterpreterEcho.py (+62)
diff --git a/lldb/packages/Python/lldbsuite/test/lldbpexpect.py b/lldb/packages/Python/lldbsuite/test/lldbpexpect.py
index 3279e1fd39f8c..03b2500fbda52 100644
--- a/lldb/packages/Python/lldbsuite/test/lldbpexpect.py
+++ b/lldb/packages/Python/lldbsuite/test/lldbpexpect.py
@@ -10,6 +10,7 @@
 
 
 @skipIfRemote
+@skipIfWindows
 @add_test_categories(["pexpect"])
 class PExpectTest(TestBase):
     NO_DEBUG_INFO_TESTCASE = True
diff --git a/lldb/source/Interpreter/embedded_interpreter.py b/lldb/source/Interpreter/embedded_interpreter.py
index 42a9ab5fc367a..5eb582e131871 100644
--- a/lldb/source/Interpreter/embedded_interpreter.py
+++ b/lldb/source/Interpreter/embedded_interpreter.py
@@ -1,8 +1,6 @@
 import sys
 import builtins
 import code
-import lldb
-import traceback
 
 try:
     import readline
@@ -31,19 +29,6 @@ def is_libedit():
 
 g_run_one_line_str = None
 
-
-def get_terminal_size(fd):
-    try:
-        import fcntl
-        import termios
-        import struct
-
-        hw = struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234"))
-    except:
-        hw = (0, 0)
-    return hw
-
-
 class LLDBExit(SystemExit):
     pass
 
@@ -74,50 +59,21 @@ def readfunc_stdio(prompt):
 def run_python_interpreter(local_dict):
     # Pass in the dictionary, for continuity from one session to the next.
     try:
-        fd = sys.stdin.fileno()
-        interacted = False
-        if get_terminal_size(fd)[1] == 0:
-            try:
-                import termios
-
-                old = termios.tcgetattr(fd)
-                if old[3] & termios.ECHO:
-                    # Need to turn off echoing and restore
-                    new = termios.tcgetattr(fd)
-                    new[3] = new[3] & ~termios.ECHO
-                    try:
-                        termios.tcsetattr(fd, termios.TCSADRAIN, new)
-                        interacted = True
-                        code.interact(
-                            banner="Python Interactive Interpreter. To exit, type 'quit()', 'exit()'.",
-                            readfunc=readfunc_stdio,
-                            local=local_dict,
-                        )
-                    finally:
-                        termios.tcsetattr(fd, termios.TCSADRAIN, old)
-            except:
-                pass
-            # Don't need to turn off echoing
-            if not interacted:
-                code.interact(
-                    banner="Python Interactive Interpreter. To exit, type 'quit()', 'exit()' or Ctrl-D.",
-                    readfunc=readfunc_stdio,
-                    local=local_dict,
-                )
-        else:
-            # We have a real interactive terminal
-            code.interact(
-                banner="Python Interactive Interpreter. To exit, type 'quit()', 'exit()' or Ctrl-D.",
-                readfunc=readfunc,
-                local=local_dict,
-            )
+        banner = "Python Interactive Interpreter. To exit, type 'quit()', 'exit()'."
+        input_func = readfunc_stdio
+
+        is_atty = sys.stdin.isatty()
+        if is_atty:
+            banner = "Python Interactive Interpreter. To exit, type 'quit()', 'exit()' or Ctrl-D."
+            input_func = readfunc
+
+        code.interact(banner=banner, readfunc=input_func, local=local_dict)
     except LLDBExit:
         pass
     except SystemExit as e:
         if e.code:
             print("Script exited with code %s" % e.code)
 
-
 def run_one_line(local_dict, input_string):
     global g_run_one_line_str
     try:
diff --git a/lldb/test/API/terminal/TestPythonInterpreterEcho.py b/lldb/test/API/terminal/TestPythonInterpreterEcho.py
new file mode 100644
index 0000000000000..afa8bca2c9be8
--- /dev/null
+++ b/lldb/test/API/terminal/TestPythonInterpreterEcho.py
@@ -0,0 +1,62 @@
+"""
+Test that typing python expression in the terminal is echoed back to stdout.
+"""
+
+from lldbsuite.test.decorators import skipIfAsan
+from lldbsuite.test.lldbpexpect import PExpectTest
+
+
+@skipIfAsan
+class PythonInterpreterEchoTest(PExpectTest):
+    PYTHON_PROMPT = ">>> "
+
+    def verify_command_echo(
+        self, command: str, expected_output: str = "", is_regex: bool = False
+    ):
+        assert self.child != None
+        child = self.child
+        self.assertIsNotNone(self.child, "expected a running lldb process.")
+
+        child.sendline(command)
+
+        # Build pattern list: match whichever comes first (output or prompt)
+        # This prevents waiting for a timeout if there's no match
+        pattern = []
+        match_expected = expected_output and len(expected_output) > 0
+
+        if match_expected:
+            pattern.append(expected_output)
+        pattern.append(self.PYTHON_PROMPT)
+
+        expect_func = child.expect if is_regex else child.expect_exact
+        match_idx = expect_func(pattern)
+        if match_expected:
+            self.assertEqual(
+                match_idx, 0, "Expected output `{expected_output}` in stdout."
+            )
+
+        self.assertIsNotNone(self.child.before, "Expected output before prompt")
+        self.assertIsInstance(self.child.before, bytes)
+        echoed_text: str = self.child.before.decode("ascii").strip()
+        self.assertEqual(
+            command, echoed_text, f"Command '{command}' should be echoed to stdout."
+        )
+
+        if match_expected:
+            child.expect_exact(self.PYTHON_PROMPT)
+
+    def test_python_interpreter_echo(self):
+        """Test that that the user typed commands is echoed to stdout"""
+
+        self.launch(use_colors=False, dimensions=(100, 100))
+
+        # enter the python interpreter
+        self.verify_command_echo(
+            "script --language python --", expected_output="Python.*\\.", is_regex=True
+        )
+        self.child_in_script_interpreter = True
+
+        self.verify_command_echo("val = 300")
+        self.verify_command_echo(
+            "print('result =', 300)", expected_output="result = 300"
+        )

Copy link
Member

@medismailben medismailben left a comment

Choose a reason for hiding this comment

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

This seems fine but I'm unsure of how sys.stdin.isatty behaves on older python installs. @JDevlieghere might know better.

Copy link
Member

@JDevlieghere JDevlieghere left a comment

Choose a reason for hiding this comment

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

This looks like a good improvement (I see a lot of deleted lines!) but I agree with Ismail that we need to make sure this behaves correctly with all our supported Python versions.

The new PExpect test covers the case of using a TTY, but do we have existing tests that do the same when we're redirecting input and setting the input programmatically through the SB API?

Comment on lines -4 to -5
import lldb
import traceback
Copy link
Member

Choose a reason for hiding this comment

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

This seems unrelated to this PR? Drive-by changes are very much appreciated, but should go in their own PR (and if trivial can be merged without review).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[lldb] Typing commands in the python embedded interpreter is not shown on the screen.

4 participants