Skip to content

Commit e8dc8d6

Browse files
Add new Python API SBCommandInterpreter::GetTranscript() (#90703)
# Motivation Currently, the user can already get the "transcript" (for "what is the transcript", see `CommandInterpreter::SaveTranscript`). However, the only way to obtain the transcript data as a user is to first destroy the debugger, then read the save directory. Note that destroy-callbacks cannot be used, because 1\ transcript data is private to the command interpreter (see `CommandInterpreter.h`), and 2\ the writing of the transcript is *after* the invocation of destory-callbacks (see `Debugger::Destroy`). So basically, there is no way to obtain the transcript: * during the lifetime of a debugger (including the destroy-callbacks, which often performs logging tasks, where the transcript can be useful) * without relying on external storage In theory, there are other ways for user to obtain transcript data during a debugger's life cycle: * Use Python API and intercept commands and results. * Use CLI and record console input/output. However, such ways rely on the client's setup and are not supported natively by LLDB. # Proposal Add a new Python API `SBCommandInterpreter::GetTranscript()`. Goals: * It can be called at any time during the debugger's life cycle, including in destroy-callbacks. * It returns data in-memory. Structured data: * To make data processing easier, the return type is `SBStructuredData`. See comments in code for how the data is organized. * In the future, `SaveTranscript` can be updated to write different formats using such data (e.g. JSON). This is probably accompanied by a new setting (e.g. `interpreter.save-session-format`). # Alternatives The return type can also be `std::vector<std::pair<std::string, SBCommandReturnObject>>`. This will make implementation easier, without having to translate it to `SBStructuredData`. On the other hand, `SBStructuredData` can convert to JSON easily, so it's more convenient for user to process. # Privacy Both user commands and output/error in the transcript can contain privacy data. However, as mentioned, the transcript is already available to the user. The addition of the new API doesn't increase the level of risk. In fact, it _lowers_ the risk of privacy data being leaked later on, by avoiding writing such data to external storage. Once the user (or their code) gets the transcript, it will be their responsibility to make sure that any required privacy policies are guaranteed. # Tests ``` bin/llvm-lit -sv ../external/llvm-project/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py ``` ``` bin/llvm-lit -sv ../external/llvm-project/lldb/test/API/commands/session/save/TestSessionSave.py ``` --------- Co-authored-by: Roy Shi <[email protected]> Co-authored-by: Med Ismail Bennani <[email protected]>
1 parent 8018e4c commit e8dc8d6

File tree

8 files changed

+270
-8
lines changed

8 files changed

+270
-8
lines changed

lldb/include/lldb/API/SBCommandInterpreter.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,14 @@ class SBCommandInterpreter {
318318

319319
SBStructuredData GetStatistics();
320320

321+
/// Returns a list of handled commands, output and error. Each element in
322+
/// the list is a dictionary with the following keys/values:
323+
/// - "command" (string): The command that was executed.
324+
/// - "output" (string): The output of the command. Empty ("") if no output.
325+
/// - "error" (string): The error of the command. Empty ("") if no error.
326+
/// - "seconds" (float): The time it took to execute the command.
327+
SBStructuredData GetTranscript();
328+
321329
protected:
322330
friend class lldb_private::CommandPluginInterfaceImplementation;
323331

lldb/include/lldb/Interpreter/CommandInterpreter.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include "lldb/Utility/Log.h"
2323
#include "lldb/Utility/StreamString.h"
2424
#include "lldb/Utility/StringList.h"
25+
#include "lldb/Utility/StructuredData.h"
2526
#include "lldb/lldb-forward.h"
2627
#include "lldb/lldb-private.h"
2728

@@ -560,6 +561,9 @@ class CommandInterpreter : public Broadcaster,
560561
bool GetPromptOnQuit() const;
561562
void SetPromptOnQuit(bool enable);
562563

564+
bool GetSaveTranscript() const;
565+
void SetSaveTranscript(bool enable);
566+
563567
bool GetSaveSessionOnQuit() const;
564568
void SetSaveSessionOnQuit(bool enable);
565569

@@ -647,6 +651,7 @@ class CommandInterpreter : public Broadcaster,
647651
}
648652

649653
llvm::json::Value GetStatistics();
654+
const StructuredData::Array &GetTranscript() const;
650655

651656
protected:
652657
friend class Debugger;
@@ -765,7 +770,20 @@ class CommandInterpreter : public Broadcaster,
765770
typedef llvm::StringMap<uint64_t> CommandUsageMap;
766771
CommandUsageMap m_command_usages;
767772

773+
/// Turn on settings `interpreter.save-transcript` for LLDB to populate
774+
/// this stream. Otherwise this stream is empty.
768775
StreamString m_transcript_stream;
776+
777+
/// Contains a list of handled commands and their details. Each element in
778+
/// the list is a dictionary with the following keys/values:
779+
/// - "command" (string): The command that was executed.
780+
/// - "output" (string): The output of the command. Empty ("") if no output.
781+
/// - "error" (string): The error of the command. Empty ("") if no error.
782+
/// - "seconds" (float): The time it took to execute the command.
783+
///
784+
/// Turn on settings `interpreter.save-transcript` for LLDB to populate
785+
/// this list. Otherwise this list is empty.
786+
StructuredData::Array m_transcript;
769787
};
770788

771789
} // namespace lldb_private

lldb/source/API/SBCommandInterpreter.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77
//===----------------------------------------------------------------------===//
88

9+
#include "lldb/Utility/StructuredData.h"
910
#include "lldb/lldb-types.h"
1011

1112
#include "lldb/Interpreter/CommandInterpreter.h"
@@ -571,6 +572,21 @@ SBStructuredData SBCommandInterpreter::GetStatistics() {
571572
return data;
572573
}
573574

575+
SBStructuredData SBCommandInterpreter::GetTranscript() {
576+
LLDB_INSTRUMENT_VA(this);
577+
578+
SBStructuredData data;
579+
if (IsValid())
580+
// A deep copy is performed by `std::make_shared` on the
581+
// `StructuredData::Array`, via its implicitly-declared copy constructor.
582+
// This ensures thread-safety between the user changing the returned
583+
// `SBStructuredData` and the `CommandInterpreter` changing its internal
584+
// `m_transcript`.
585+
data.m_impl_up->SetObjectSP(
586+
std::make_shared<StructuredData::Array>(m_opaque_ptr->GetTranscript()));
587+
return data;
588+
}
589+
574590
lldb::SBCommand SBCommandInterpreter::AddMultiwordCommand(const char *name,
575591
const char *help) {
576592
LLDB_INSTRUMENT_VA(this, name, help);

lldb/source/Interpreter/CommandInterpreter.cpp

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
#include "lldb/Utility/Log.h"
5252
#include "lldb/Utility/State.h"
5353
#include "lldb/Utility/Stream.h"
54+
#include "lldb/Utility/StructuredData.h"
5455
#include "lldb/Utility/Timer.h"
5556

5657
#include "lldb/Host/Config.h"
@@ -161,6 +162,17 @@ void CommandInterpreter::SetPromptOnQuit(bool enable) {
161162
SetPropertyAtIndex(idx, enable);
162163
}
163164

165+
bool CommandInterpreter::GetSaveTranscript() const {
166+
const uint32_t idx = ePropertySaveTranscript;
167+
return GetPropertyAtIndexAs<bool>(
168+
idx, g_interpreter_properties[idx].default_uint_value != 0);
169+
}
170+
171+
void CommandInterpreter::SetSaveTranscript(bool enable) {
172+
const uint32_t idx = ePropertySaveTranscript;
173+
SetPropertyAtIndex(idx, enable);
174+
}
175+
164176
bool CommandInterpreter::GetSaveSessionOnQuit() const {
165177
const uint32_t idx = ePropertySaveSessionOnQuit;
166178
return GetPropertyAtIndexAs<bool>(
@@ -1889,7 +1901,16 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
18891901
else
18901902
add_to_history = (lazy_add_to_history == eLazyBoolYes);
18911903

1892-
m_transcript_stream << "(lldb) " << command_line << '\n';
1904+
// The same `transcript_item` will be used below to add output and error of
1905+
// the command.
1906+
StructuredData::DictionarySP transcript_item;
1907+
if (GetSaveTranscript()) {
1908+
m_transcript_stream << "(lldb) " << command_line << '\n';
1909+
1910+
transcript_item = std::make_shared<StructuredData::Dictionary>();
1911+
transcript_item->AddStringItem("command", command_line);
1912+
m_transcript.AddItem(transcript_item);
1913+
}
18931914

18941915
bool empty_command = false;
18951916
bool comment_command = false;
@@ -1994,7 +2015,7 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
19942015
// Take care of things like setting up the history command & calling the
19952016
// appropriate Execute method on the CommandObject, with the appropriate
19962017
// arguments.
1997-
2018+
StatsDuration execute_time;
19982019
if (cmd_obj != nullptr) {
19992020
bool generate_repeat_command = add_to_history;
20002021
// If we got here when empty_command was true, then this command is a
@@ -2035,14 +2056,24 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
20352056
log, "HandleCommand, command line after removing command name(s): '%s'",
20362057
remainder.c_str());
20372058

2059+
ElapsedTime elapsed(execute_time);
20382060
cmd_obj->Execute(remainder.c_str(), result);
20392061
}
20402062

20412063
LLDB_LOGF(log, "HandleCommand, command %s",
20422064
(result.Succeeded() ? "succeeded" : "did not succeed"));
20432065

2044-
m_transcript_stream << result.GetOutputData();
2045-
m_transcript_stream << result.GetErrorData();
2066+
// To test whether or not transcript should be saved, `transcript_item` is
2067+
// used instead of `GetSaveTrasncript()`. This is because the latter will
2068+
// fail when the command is "settings set interpreter.save-transcript true".
2069+
if (transcript_item) {
2070+
m_transcript_stream << result.GetOutputData();
2071+
m_transcript_stream << result.GetErrorData();
2072+
2073+
transcript_item->AddStringItem("output", result.GetOutputData());
2074+
transcript_item->AddStringItem("error", result.GetErrorData());
2075+
transcript_item->AddFloatItem("seconds", execute_time.get().count());
2076+
}
20462077

20472078
return result.Succeeded();
20482079
}
@@ -3554,3 +3585,7 @@ llvm::json::Value CommandInterpreter::GetStatistics() {
35543585
stats.try_emplace(command_usage.getKey(), command_usage.getValue());
35553586
return stats;
35563587
}
3588+
3589+
const StructuredData::Array &CommandInterpreter::GetTranscript() const {
3590+
return m_transcript;
3591+
}

lldb/source/Interpreter/InterpreterProperties.td

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ let Definition = "interpreter" in {
99
Global,
1010
DefaultTrue,
1111
Desc<"If true, LLDB will prompt you before quitting if there are any live processes being debugged. If false, LLDB will quit without asking in any case.">;
12+
def SaveTranscript: Property<"save-transcript", "Boolean">,
13+
Global,
14+
DefaultFalse,
15+
Desc<"If true, commands will be saved into a transcript buffer for user access.">;
1216
def SaveSessionOnQuit: Property<"save-session-on-quit", "Boolean">,
1317
Global,
1418
DefaultFalse,

lldb/test/API/commands/session/save/TestSessionSave.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ def test_session_save(self):
2525
raw = ""
2626
interpreter = self.dbg.GetCommandInterpreter()
2727

28+
# Make sure "save-transcript" is on, so that all the following setings
29+
# and commands are saved into the trasncript. Note that this cannot be
30+
# a part of the `settings`, because this command itself won't be saved
31+
# into the transcript.
32+
self.runCmd("settings set interpreter.save-transcript true")
33+
2834
settings = [
2935
"settings set interpreter.echo-commands true",
3036
"settings set interpreter.echo-comment-commands true",
@@ -95,6 +101,12 @@ def test_session_save_on_quit(self):
95101
raw = ""
96102
interpreter = self.dbg.GetCommandInterpreter()
97103

104+
# Make sure "save-transcript" is on, so that all the following setings
105+
# and commands are saved into the trasncript. Note that this cannot be
106+
# a part of the `settings`, because this command itself won't be saved
107+
# into the transcript.
108+
self.runCmd("settings set interpreter.save-transcript true")
109+
98110
td = tempfile.TemporaryDirectory()
99111

100112
settings = [

0 commit comments

Comments
 (0)