Skip to content

Commit 97f6e53

Browse files
authored
[lldb] Support CommandInterpreter print callbacks (llvm#125006)
Xcode uses a pseudoterminal for the debugger console. - The upside of this apporach is that it means that it can rely on LLDB's IOHandlers for multiline and script input. - The downside of this approach is that the command output is printed to the PTY and you don't get a SBCommandReturnObject. Adrian added support for inline diagnostics (llvm#110901) and we'd like to access those from the IDE. This patch adds support for registering a callback in the command interpreter that gives access to the `(SB)CommandReturnObject` right before it will be printed. The callback implementation can choose whether it likes to handle printing the result or defer to lldb. If the callback indicated it handled the result, the command interpreter will skip printing the result. We considered a few other alternatives to solve this problem: - The most obvious one is using `HandleCommand`, which returns a `SBCommandReturnObject`. The problem with this approach is the multiline input mentioned above. We would need a way to tell the IDE that it should expect multiline input, which isn't known until LLDB starts handling the command. - To address the multiline issue,we considered exposing (some of the) IOHandler machinery through the SB API. To solve this particular issue, that would require reimplementing a ton of logic that already exists today in the CommandInterpeter. Furthermore that seems like overkill compared to the proposed solution. rdar://141254310
1 parent 21560fe commit 97f6e53

File tree

13 files changed

+207
-37
lines changed

13 files changed

+207
-37
lines changed

lldb/bindings/python/python-swigsafecast.swig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ PythonObject SWIGBridge::ToSWIGWrapper(std::unique_ptr<lldb::SBValue> value_sb)
99
return ToSWIGHelper(value_sb.release(), SWIGTYPE_p_lldb__SBValue);
1010
}
1111

12+
PythonObject SWIGBridge::ToSWIGWrapper(std::unique_ptr<lldb::SBCommandReturnObject> result_up) {
13+
return ToSWIGHelper(result_up.release(), SWIGTYPE_p_lldb__SBCommandReturnObject);
14+
}
15+
1216
PythonObject SWIGBridge::ToSWIGWrapper(lldb::ValueObjectSP value_sp) {
1317
return ToSWIGWrapper(std::unique_ptr<lldb::SBValue>(new lldb::SBValue(value_sp)));
1418
}

lldb/bindings/python/python-typemaps.swig

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,25 @@ template <> bool SetNumberFromPyObject<double>(double &number, PyObject *obj) {
476476
$1 = $1 || PyCallable_Check(reinterpret_cast<PyObject *>($input));
477477
}
478478

479+
// For lldb::SBCommandPrintCallback
480+
%typemap(in) (lldb::SBCommandPrintCallback callback, void *baton) {
481+
if (!($input == Py_None ||
482+
PyCallable_Check(reinterpret_cast<PyObject *>($input)))) {
483+
PyErr_SetString(PyExc_TypeError, "Need a callable object or None!");
484+
SWIG_fail;
485+
}
486+
487+
// Don't lose the callback reference.
488+
Py_INCREF($input);
489+
$1 = LLDBSwigPythonCallPythonCommandPrintCallback;
490+
$2 = $input;
491+
}
492+
493+
%typemap(typecheck) (lldb::SBCommandPrintCallback callback, void *baton) {
494+
$1 = $input == Py_None;
495+
$1 = $1 || PyCallable_Check(reinterpret_cast<PyObject *>($input));
496+
}
497+
479498
%typemap(in) (lldb::CommandOverrideCallback callback, void *baton) {
480499
if (!($input == Py_None ||
481500
PyCallable_Check(reinterpret_cast<PyObject *>($input)))) {

lldb/bindings/python/python-wrapper.swig

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -727,7 +727,7 @@ lldb_private::python::SWIGBridge::LLDBSwigPythonHandleOptionArgumentCompletionFo
727727
dict_sp->AddBooleanItem("no-completion", true);
728728
return dict_sp;
729729
}
730-
730+
731731

732732
// Convert the return dictionary to a DictionarySP.
733733
StructuredData::ObjectSP result_obj_sp = result.CreateStructuredObject();
@@ -753,7 +753,7 @@ bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallParsedCommandObject(
753753
auto pfunc = self.ResolveName<PythonCallable>("__call__");
754754

755755
if (!pfunc.IsAllocated()) {
756-
cmd_retobj.AppendError("Could not find '__call__' method in implementation class");
756+
cmd_retobj.AppendError("Could not find '__call__' method in implementation class");
757757
return false;
758758
}
759759

@@ -1012,6 +1012,26 @@ static void LLDBSwigPythonCallPythonLogOutputCallback(const char *str,
10121012
}
10131013
}
10141014

1015+
// For CommandPrintCallback functions
1016+
static CommandReturnObjectCallbackResult LLDBSwigPythonCallPythonCommandPrintCallback(SBCommandReturnObject& result, void *callback_baton) {
1017+
SWIG_Python_Thread_Block swig_thread_block;
1018+
1019+
PyErr_Cleaner py_err_cleaner(true);
1020+
1021+
PythonObject result_arg = SWIGBridge::ToSWIGWrapper(
1022+
std::make_unique<SBCommandReturnObject>(result));
1023+
PythonCallable callable =
1024+
Retain<PythonCallable>(reinterpret_cast<PyObject *>(callback_baton));
1025+
1026+
if (!callable.IsValid())
1027+
return eCommandReturnObjectPrintCallbackSkipped;
1028+
1029+
PythonObject callback_result = callable(result_arg);
1030+
1031+
long long ret_val = unwrapOrSetPythonException(As<long long>(callback_result));
1032+
return (CommandReturnObjectCallbackResult)ret_val;
1033+
}
1034+
10151035
// For DebuggerTerminateCallback functions
10161036
static void LLDBSwigPythonCallPythonSBDebuggerTerminateCallback(lldb::user_id_t debugger_id,
10171037
void *baton) {

lldb/include/lldb/API/SBCommandInterpreter.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,13 +247,13 @@ class SBCommandInterpreter {
247247
lldb::SBStringList &matches,
248248
lldb::SBStringList &descriptions);
249249

250-
/// Returns whether an interrupt flag was raised either by the SBDebugger -
250+
/// Returns whether an interrupt flag was raised either by the SBDebugger -
251251
/// when the function is not running on the RunCommandInterpreter thread, or
252252
/// by SBCommandInterpreter::InterruptCommand if it is. If your code is doing
253-
/// interruptible work, check this API periodically, and interrupt if it
253+
/// interruptible work, check this API periodically, and interrupt if it
254254
/// returns true.
255255
bool WasInterrupted() const;
256-
256+
257257
/// Interrupts the command currently executing in the RunCommandInterpreter
258258
/// thread.
259259
///
@@ -331,6 +331,8 @@ class SBCommandInterpreter {
331331
/// this list. Otherwise this list is empty.
332332
SBStructuredData GetTranscript();
333333

334+
void SetPrintCallback(lldb::SBCommandPrintCallback callback, void *baton);
335+
334336
protected:
335337
friend class lldb_private::CommandPluginInterfaceImplementation;
336338

lldb/include/lldb/API/SBDefines.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ typedef bool (*SBBreakpointHitCallback)(void *baton, lldb::SBProcess &process,
144144
typedef void (*SBDebuggerDestroyCallback)(lldb::user_id_t debugger_id,
145145
void *baton);
146146

147+
typedef CommandReturnObjectCallbackResult (*SBCommandPrintCallback)(
148+
lldb::SBCommandReturnObject &result, void *baton);
149+
147150
typedef lldb::SBError (*SBPlatformLocateModuleCallback)(
148151
void *baton, const lldb::SBModuleSpec &module_spec,
149152
lldb::SBFileSpec &module_file_spec, lldb::SBFileSpec &symbol_file_spec);

lldb/include/lldb/Interpreter/CommandInterpreter.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "lldb/Interpreter/CommandObject.h"
1717
#include "lldb/Interpreter/ScriptInterpreter.h"
1818
#include "lldb/Utility/Args.h"
19+
#include "lldb/Utility/Baton.h"
1920
#include "lldb/Utility/Broadcaster.h"
2021
#include "lldb/Utility/CompletionRequest.h"
2122
#include "lldb/Utility/Event.h"
@@ -253,6 +254,10 @@ class CommandInterpreter : public Broadcaster,
253254
eCommandTypesAllThem = 0xFFFF //< all commands
254255
};
255256

257+
using CommandReturnObjectCallback =
258+
std::function<lldb::CommandReturnObjectCallbackResult(
259+
CommandReturnObject &)>;
260+
256261
// The CommandAlias and CommandInterpreter both have a hand in
257262
// substituting for alias commands. They work by writing special tokens
258263
// in the template form of the Alias command, and then detecting them when the
@@ -664,6 +669,8 @@ class CommandInterpreter : public Broadcaster,
664669
++m_command_usages[cmd_obj.GetCommandName()];
665670
}
666671

672+
void SetPrintCallback(CommandReturnObjectCallback callback);
673+
667674
llvm::json::Value GetStatistics();
668675
const StructuredData::Array &GetTranscript() const;
669676

@@ -774,6 +781,9 @@ class CommandInterpreter : public Broadcaster,
774781
std::vector<uint32_t> m_command_source_flags;
775782
CommandInterpreterRunResult m_result;
776783

784+
/// An optional callback to handle printing the CommandReturnObject.
785+
CommandReturnObjectCallback m_print_callback;
786+
777787
// The exit code the user has requested when calling the 'quit' command.
778788
// No value means the user hasn't set a custom exit code so far.
779789
std::optional<int> m_quit_exit_code;

lldb/include/lldb/lldb-enumerations.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,6 +1368,15 @@ enum Severity {
13681368
eSeverityInfo, // Equivalent to Remark used in clang.
13691369
};
13701370

1371+
/// Callback return value, indicating whether it handled printing the
1372+
/// CommandReturnObject or deferred doing so to the CommandInterpreter.
1373+
enum CommandReturnObjectCallbackResult {
1374+
/// The callback deferred printing the command return object.
1375+
eCommandReturnObjectPrintCallbackSkipped = 0,
1376+
/// The callback handled printing the command return object.
1377+
eCommandReturnObjectPrintCallbackHandled = 1,
1378+
};
1379+
13711380
} // namespace lldb
13721381

13731382
#endif // LLDB_LLDB_ENUMERATIONS_H

lldb/source/API/SBCommandInterpreter.cpp

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ SBCommandInterpreter::SBCommandInterpreter(const SBCommandInterpreter &rhs)
9898

9999
SBCommandInterpreter::~SBCommandInterpreter() = default;
100100

101-
const SBCommandInterpreter &SBCommandInterpreter::
102-
operator=(const SBCommandInterpreter &rhs) {
101+
const SBCommandInterpreter &
102+
SBCommandInterpreter::operator=(const SBCommandInterpreter &rhs) {
103103
LLDB_INSTRUMENT_VA(this, rhs);
104104

105105
m_opaque_ptr = rhs.m_opaque_ptr;
@@ -151,7 +151,7 @@ bool SBCommandInterpreter::WasInterrupted() const {
151151

152152
bool SBCommandInterpreter::InterruptCommand() {
153153
LLDB_INSTRUMENT_VA(this);
154-
154+
155155
return (IsValid() ? m_opaque_ptr->InterruptCommand() : false);
156156
}
157157

@@ -222,8 +222,7 @@ void SBCommandInterpreter::HandleCommandsFromFile(
222222
if (override_context.get())
223223
m_opaque_ptr->HandleCommandsFromFile(tmp_spec,
224224
override_context.get()->Lock(true),
225-
options.ref(),
226-
result.ref());
225+
options.ref(), result.ref());
227226

228227
else
229228
m_opaque_ptr->HandleCommandsFromFile(tmp_spec, options.ref(), result.ref());
@@ -649,7 +648,8 @@ SBCommand::operator bool() const {
649648
const char *SBCommand::GetName() {
650649
LLDB_INSTRUMENT_VA(this);
651650

652-
return (IsValid() ? ConstString(m_opaque_sp->GetCommandName()).AsCString() : nullptr);
651+
return (IsValid() ? ConstString(m_opaque_sp->GetCommandName()).AsCString()
652+
: nullptr);
653653
}
654654

655655
const char *SBCommand::GetHelp() {
@@ -743,3 +743,15 @@ void SBCommand::SetFlags(uint32_t flags) {
743743
if (IsValid())
744744
m_opaque_sp->GetFlags().Set(flags);
745745
}
746+
747+
void SBCommandInterpreter::SetPrintCallback(
748+
lldb::SBCommandPrintCallback callback, void *baton) {
749+
LLDB_INSTRUMENT_VA(this, callback, baton);
750+
751+
if (m_opaque_ptr)
752+
m_opaque_ptr->SetPrintCallback(
753+
[callback, baton](lldb_private::CommandReturnObject &result) {
754+
SBCommandReturnObject sb_result(result);
755+
return callback(sb_result, baton);
756+
});
757+
}

lldb/source/Interpreter/CommandInterpreter.cpp

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3186,30 +3186,40 @@ void CommandInterpreter::IOHandlerInputComplete(IOHandler &io_handler,
31863186
if ((result.Succeeded() &&
31873187
io_handler.GetFlags().Test(eHandleCommandFlagPrintResult)) ||
31883188
io_handler.GetFlags().Test(eHandleCommandFlagPrintErrors)) {
3189-
// Display any inline diagnostics first.
3190-
const bool inline_diagnostics = !result.GetImmediateErrorStream() &&
3191-
GetDebugger().GetShowInlineDiagnostics();
3192-
if (inline_diagnostics) {
3193-
unsigned prompt_len = m_debugger.GetPrompt().size();
3194-
if (auto indent = result.GetDiagnosticIndent()) {
3195-
std::string diags =
3196-
result.GetInlineDiagnosticString(prompt_len + *indent);
3197-
PrintCommandOutput(io_handler, diags, true);
3189+
auto DefaultPrintCallback = [&](const CommandReturnObject &result) {
3190+
// Display any inline diagnostics first.
3191+
const bool inline_diagnostics = !result.GetImmediateErrorStream() &&
3192+
GetDebugger().GetShowInlineDiagnostics();
3193+
if (inline_diagnostics) {
3194+
unsigned prompt_len = m_debugger.GetPrompt().size();
3195+
if (auto indent = result.GetDiagnosticIndent()) {
3196+
std::string diags =
3197+
result.GetInlineDiagnosticString(prompt_len + *indent);
3198+
PrintCommandOutput(io_handler, diags, true);
3199+
}
31983200
}
3199-
}
32003201

3201-
// Display any STDOUT/STDERR _prior_ to emitting the command result text.
3202-
GetProcessOutput();
3202+
// Display any STDOUT/STDERR _prior_ to emitting the command result text.
3203+
GetProcessOutput();
32033204

3204-
if (!result.GetImmediateOutputStream()) {
3205-
llvm::StringRef output = result.GetOutputString();
3206-
PrintCommandOutput(io_handler, output, true);
3207-
}
3205+
if (!result.GetImmediateOutputStream()) {
3206+
llvm::StringRef output = result.GetOutputString();
3207+
PrintCommandOutput(io_handler, output, true);
3208+
}
32083209

3209-
// Now emit the command error text from the command we just executed.
3210-
if (!result.GetImmediateErrorStream()) {
3211-
std::string error = result.GetErrorString(!inline_diagnostics);
3212-
PrintCommandOutput(io_handler, error, false);
3210+
// Now emit the command error text from the command we just executed.
3211+
if (!result.GetImmediateErrorStream()) {
3212+
std::string error = result.GetErrorString(!inline_diagnostics);
3213+
PrintCommandOutput(io_handler, error, false);
3214+
}
3215+
};
3216+
3217+
if (m_print_callback) {
3218+
const auto callback_result = m_print_callback(result);
3219+
if (callback_result == eCommandReturnObjectPrintCallbackSkipped)
3220+
DefaultPrintCallback(result);
3221+
} else {
3222+
DefaultPrintCallback(result);
32133223
}
32143224
}
32153225

@@ -3660,3 +3670,8 @@ llvm::json::Value CommandInterpreter::GetStatistics() {
36603670
const StructuredData::Array &CommandInterpreter::GetTranscript() const {
36613671
return m_transcript;
36623672
}
3673+
3674+
void CommandInterpreter::SetPrintCallback(
3675+
CommandReturnObjectCallback callback) {
3676+
m_print_callback = callback;
3677+
}

lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ template <typename T> class ScopedPythonObject : PythonObject {
8181
class SWIGBridge {
8282
public:
8383
static PythonObject ToSWIGWrapper(std::unique_ptr<lldb::SBValue> value_sb);
84+
static PythonObject
85+
ToSWIGWrapper(std::unique_ptr<lldb::SBCommandReturnObject> result_up);
8486
static PythonObject ToSWIGWrapper(lldb::ValueObjectSP value_sp);
8587
static PythonObject ToSWIGWrapper(lldb::TargetSP target_sp);
8688
static PythonObject ToSWIGWrapper(lldb::ProcessSP process_sp);
@@ -190,12 +192,11 @@ class SWIGBridge {
190192
lldb::DebuggerSP debugger, const char *args,
191193
lldb_private::CommandReturnObject &cmd_retobj,
192194
lldb::ExecutionContextRefSP exe_ctx_ref_sp);
193-
static bool
194-
LLDBSwigPythonCallParsedCommandObject(PyObject *implementor,
195-
lldb::DebuggerSP debugger,
196-
StructuredDataImpl &args_impl,
197-
lldb_private::CommandReturnObject &cmd_retobj,
198-
lldb::ExecutionContextRefSP exe_ctx_ref_sp);
195+
static bool LLDBSwigPythonCallParsedCommandObject(
196+
PyObject *implementor, lldb::DebuggerSP debugger,
197+
StructuredDataImpl &args_impl,
198+
lldb_private::CommandReturnObject &cmd_retobj,
199+
lldb::ExecutionContextRefSP exe_ctx_ref_sp);
199200

200201
static std::optional<std::string>
201202
LLDBSwigPythonGetRepeatCommandForScriptedCommand(PyObject *implementor,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
C_SOURCES := main.c
2+
3+
include Makefile.rules
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import lldb
2+
from lldbsuite.test.decorators import *
3+
from lldbsuite.test.lldbtest import *
4+
from lldbsuite.test import lldbutil
5+
6+
7+
class CommandInterepterPrintCallbackTest(TestBase):
8+
NO_DEBUG_INFO_TESTCASE = True
9+
10+
def run_command_interpreter_with_output_file(self, out_filename, input_str):
11+
with open(out_filename, "w") as f:
12+
self.dbg.SetOutputFileHandle(f, False)
13+
self.dbg.SetInputString(input_str)
14+
opts = lldb.SBCommandInterpreterRunOptions()
15+
self.dbg.RunCommandInterpreter(True, False, opts, 0, False, False)
16+
17+
def test_command_interpreter_print_callback(self):
18+
"""Test the command interpreter print callback."""
19+
self.build()
20+
exe = self.getBuildArtifact("a.out")
21+
22+
target = self.dbg.CreateTarget(exe)
23+
self.assertTrue(target, VALID_TARGET)
24+
25+
lldbutil.run_to_source_breakpoint(
26+
self, "// Break here", lldb.SBFileSpec("main.c")
27+
)
28+
29+
out_filename = self.getBuildArtifact("output")
30+
ci = self.dbg.GetCommandInterpreter()
31+
called = False
32+
33+
# The string we'll be looking for in the command output.
34+
needle = "Show a list of all debugger commands"
35+
36+
# Test registering a callback that handles the printing. Make sure the
37+
# result is passed to the callback and that we don't print the result.
38+
def handling_callback(return_object):
39+
nonlocal called
40+
called = True
41+
self.assertIn(needle, return_object.GetOutput())
42+
return lldb.eCommandReturnObjectPrintCallbackHandled
43+
44+
ci.SetPrintCallback(handling_callback)
45+
self.assertFalse(called)
46+
self.run_command_interpreter_with_output_file(out_filename, "help help\n")
47+
with open(out_filename, "r") as f:
48+
self.assertNotIn(needle, f.read())
49+
50+
# Test registering a callback that defers the printing to lldb. Make
51+
# sure the result is passed to the callback and that the result is
52+
# printed by lldb.
53+
def non_handling_callback(return_object):
54+
nonlocal called
55+
called = True
56+
self.assertIn(needle, return_object.GetOutput())
57+
return lldb.eCommandReturnObjectPrintCallbackSkipped
58+
59+
called = False
60+
ci.SetPrintCallback(non_handling_callback)
61+
self.assertFalse(called)
62+
self.run_command_interpreter_with_output_file(out_filename, "help help\n")
63+
self.assertTrue(called)
64+
65+
with open(out_filename, "r") as f:
66+
self.assertIn(needle, f.read())

0 commit comments

Comments
 (0)