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

[lldb/Interpreter] Propagate script output back to command return object #109440

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

medismailben
Copy link
Member

When running a oneliner script expression, if the script interpreter returned a value, that value would be printed to the debugger standard output straight from the interpreter instead of being propagated back to the command return object, which would then forward it to its output stream.

This implies that when evaluating a oneliner script expression (with SBCommandInterpreter::HandleCommand), the return value would get printed to stdout, but we would not be able to fetch it from the command return object.

This patch solves this issue by extending the default Python InteractiveConsole class to keep track of the return value, before include it to the command return object.

rdar://132420488

…bject

When running a oneliner script expression, if the script interpreter
returned a value, that value would be printed to the debugger standard
output straight from the interpreter instead of being propagated back to
the command return object, which would then forward it to its output stream.

This implies that when evaluating a oneliner script expression (with
`SBCommandInterpreter::HandleCommand`), the return value would get
printed to stdout, but we would not be able to fetch it from the command
return object.

This patch solves this issue by extending the default Python
`InteractiveConsole` class to keep track of the return value, before
include it to the command return object.

rdar://132420488

Signed-off-by: Med Ismail Bennani <ismail@bennani.ma>
@llvmbot
Copy link
Collaborator

llvmbot commented Sep 20, 2024

@llvm/pr-subscribers-lldb

Author: Med Ismail Bennani (medismailben)

Changes

When running a oneliner script expression, if the script interpreter returned a value, that value would be printed to the debugger standard output straight from the interpreter instead of being propagated back to the command return object, which would then forward it to its output stream.

This implies that when evaluating a oneliner script expression (with SBCommandInterpreter::HandleCommand), the return value would get printed to stdout, but we would not be able to fetch it from the command return object.

This patch solves this issue by extending the default Python InteractiveConsole class to keep track of the return value, before include it to the command return object.

rdar://132420488


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

2 Files Affected:

  • (modified) lldb/source/Interpreter/embedded_interpreter.py (+36-3)
  • (modified) lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp (+5-2)
diff --git a/lldb/source/Interpreter/embedded_interpreter.py b/lldb/source/Interpreter/embedded_interpreter.py
index a487592ef1aee5..fd5c44d0121fbd 100644
--- a/lldb/source/Interpreter/embedded_interpreter.py
+++ b/lldb/source/Interpreter/embedded_interpreter.py
@@ -8,6 +8,8 @@
 import lldb
 import traceback
 
+from io import StringIO
+
 try:
     import readline
     import rlcompleter
@@ -116,19 +118,50 @@ def run_python_interpreter(local_dict):
             print("Script exited with code %s" % e.code)
 
 
+class LLDBInteractiveConsole(code.InteractiveConsole):
+    def __init__(self, locals=None):
+        super().__init__(locals)
+        self.result_output = None
+
+    ### Implementation detail:
+    ### https://docs.python.org/3/library/code.html#code.InteractiveInterpreter.runsource
+    def runsource(self, source, filename="<input>", symbol="single"):
+        # Redirect stdout to capture print statements
+        old_stdout = sys.stdout
+        sys.stdout = result_output = StringIO()
+
+        try:
+            compiled_code = self.compile(source, filename, symbol)
+            if compiled_code is None:
+                return False
+
+            exec(compiled_code, self.locals)
+            return True
+        except Exception as e:
+            self.showsyntaxerror(filename)
+            return False
+        finally:
+            self.result_output = result_output
+            sys.stdout = old_stdout
+
+    def get_last_result(self):
+        return self.result_output.getvalue()
+
 def run_one_line(local_dict, input_string):
     global g_run_one_line_str
     try:
         input_string = strip_and_check_exit(input_string)
-        repl = code.InteractiveConsole(local_dict)
+        repl = LLDBInteractiveConsole(local_dict)
         if input_string:
             # A newline is appended to support one-line statements containing
             # control flow. For example "if True: print(1)" silently does
             # nothing, but works with a newline: "if True: print(1)\n".
             input_string += "\n"
-            repl.runsource(input_string)
+            if repl.runsource(input_string):
+                return repl.get_last_result()
         elif g_run_one_line_str:
-            repl.runsource(g_run_one_line_str)
+            if repl.runsource(g_run_one_line_str):
+                return repl.get_last_result()
     except LLDBExit:
         pass
     except SystemExit as e:
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
index 63691d24f0dadb..30b67ce48a4be9 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
@@ -884,9 +884,12 @@ bool ScriptInterpreterPythonImpl::ExecuteOneLine(
                   PyRefType::Owned,
                   PyObject_CallObject(m_run_one_line_function.get(),
                                       pargs.get()));
-              if (return_value.IsValid())
+              if (return_value.IsValid()) {
                 success = true;
-              else if (options.GetMaskoutErrors() && PyErr_Occurred()) {
+                PythonString repr = return_value.Repr();
+                if (repr && repr.GetSize())
+                  result->AppendMessage(repr.GetString());
+              } else if (options.GetMaskoutErrors() && PyErr_Occurred()) {
                 PyErr_Print();
                 PyErr_Clear();
               }

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.

I assume you have tested this both with Python 3.9 and 3.11/3.12?

Comment on lines +126 to +127
### Implementation detail:
### https://docs.python.org/3/library/code.html#code.InteractiveInterpreter.runsource
Copy link
Member

Choose a reason for hiding this comment

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

Nit: the link isn't the implementation. You can probably just omit line 126.

### Implementation detail:
### https://docs.python.org/3/library/code.html#code.InteractiveInterpreter.runsource
def runsource(self, source, filename="<input>", symbol="single"):
# Redirect stdout to capture print statements
Copy link
Member

Choose a reason for hiding this comment

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

Nit: missing period.


### Implementation detail:
### https://docs.python.org/3/library/code.html#code.InteractiveInterpreter.runsource
def runsource(self, source, filename="<input>", symbol="single"):
Copy link
Member

Choose a reason for hiding this comment

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

If you don't need any of the arguments, can you use **kwargs and forward everything so that we don't need to maintain our own default values?

@jimingham
Copy link
Collaborator

Now that successful results show up in the command return object, it's a little strange that if the expression fails - a parse error or whatever - that is not available in the command result. It would be more consistent if we did SetError on the command result object with the error. It looks like you could capture that and forward it to where the result is made up.
I think we should do that, but if you want to do it as a follow-up that's also fine.
I don't have any comments on the code beyond what Jonas added, but there should be a test for this.

@medismailben medismailben marked this pull request as draft September 20, 2024 21:36
Comment on lines +130 to +131
old_stdout = sys.stdout
sys.stdout = result_output = StringIO()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Apparently, there's even a standard utility to do this, but it comes with a big warning (which I fully agree with):

Note that the global side effect on [sys.stdout](https://docs.python.org/3/library/sys.html#sys.stdout) means that this context manager is not suitable for use in library code and most threaded applications.

We tick both of those boxes. Is this redirection really necessary? Does anything prevent two instances of this function from running in parallel (e.g. on two debugger objects). If not, we're just setting ourself for a nasty race where the stdout just suddenly goes dead (because it's been permanently redirected to StringIO). (Note that even if we can prevent races between in our own code, we can't prevent racing with anyone else attempting to do the same thing, nor we can stop inadvertently capturing the output of other threads).

Capturing the result of the command makes sense to me, but capturing stdout, seems like its going too far. Maybe we could provide the StringIO object to the executed code as some sort of an argument, so that if the user really wants print there it can do something like print(..., file=cmd_output) ?

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.

5 participants