Skip to content

Commit a6c76a8

Browse files
committed
[lldb/interpreter] Add ability to save lldb session to a file
This patch introduce a new feature that allows the users to save their debugging session's transcript (commands + outputs) to a file. It differs from the reproducers since it doesn't require to capture a session preemptively and replay the reproducer file in lldb. The user can choose the save its session manually using the session save command or automatically by setting the interpreter.save-session-on-quit on their init file. To do so, the patch adds a Stream object to the CommandInterpreter that will hold the input command from the IOHandler and the CommandReturnObject output and error. This way, that stream object accumulates passively all the interactions throughout the session and will save them to disk on demand. The user can specify a file path where the session's transcript will be saved. However, it is optional, and when it is not provided, lldb will create a temporary file name according to the session date and time. rdar://63347792 Differential Revision: https://reviews.llvm.org/D82155 Signed-off-by: Med Ismail Bennani <[email protected]>
1 parent a4fc6c2 commit a6c76a8

File tree

8 files changed

+246
-3
lines changed

8 files changed

+246
-3
lines changed

lldb/include/lldb/Interpreter/CommandInterpreter.h

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "lldb/Utility/CompletionRequest.h"
2121
#include "lldb/Utility/Event.h"
2222
#include "lldb/Utility/Log.h"
23+
#include "lldb/Utility/StreamString.h"
2324
#include "lldb/Utility/StringList.h"
2425
#include "lldb/lldb-forward.h"
2526
#include "lldb/lldb-private.h"
@@ -485,9 +486,11 @@ class CommandInterpreter : public Broadcaster,
485486
bool GetExpandRegexAliases() const;
486487

487488
bool GetPromptOnQuit() const;
488-
489489
void SetPromptOnQuit(bool enable);
490490

491+
bool GetSaveSessionOnQuit() const;
492+
void SetSaveSessionOnQuit(bool enable);
493+
491494
bool GetEchoCommands() const;
492495
void SetEchoCommands(bool enable);
493496

@@ -526,6 +529,18 @@ class CommandInterpreter : public Broadcaster,
526529

527530
bool GetSpaceReplPrompts() const;
528531

532+
/// Save the current debugger session transcript to a file on disk.
533+
/// \param output_file
534+
/// The file path to which the session transcript will be written. Since
535+
/// the argument is optional, an arbitrary temporary file will be create
536+
/// when no argument is passed.
537+
/// \param result
538+
/// This is used to pass function output and error messages.
539+
/// \return \b true if the session transcript was successfully written to
540+
/// disk, \b false otherwise.
541+
bool SaveTranscript(CommandReturnObject &result,
542+
llvm::Optional<std::string> output_file = llvm::None);
543+
529544
protected:
530545
friend class Debugger;
531546

@@ -621,6 +636,8 @@ class CommandInterpreter : public Broadcaster,
621636
llvm::Optional<int> m_quit_exit_code;
622637
// If the driver is accepts custom exit codes for the 'quit' command.
623638
bool m_allow_exit_code = false;
639+
640+
StreamString m_transcript_stream;
624641
};
625642

626643
} // namespace lldb_private

lldb/source/Commands/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ add_lldb_library(lldbCommands
1313
CommandObjectFrame.cpp
1414
CommandObjectGUI.cpp
1515
CommandObjectHelp.cpp
16+
CommandObjectLanguage.cpp
1617
CommandObjectLog.cpp
1718
CommandObjectMemory.cpp
1819
CommandObjectMultiword.cpp
@@ -22,6 +23,7 @@ add_lldb_library(lldbCommands
2223
CommandObjectQuit.cpp
2324
CommandObjectRegister.cpp
2425
CommandObjectReproducer.cpp
26+
CommandObjectSession.cpp
2527
CommandObjectSettings.cpp
2628
CommandObjectSource.cpp
2729
CommandObjectStats.cpp
@@ -31,7 +33,6 @@ add_lldb_library(lldbCommands
3133
CommandObjectVersion.cpp
3234
CommandObjectWatchpoint.cpp
3335
CommandObjectWatchpointCommand.cpp
34-
CommandObjectLanguage.cpp
3536

3637
LINK_LIBS
3738
lldbBase

lldb/source/Commands/CommandObjectQuit.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,5 +103,10 @@ bool CommandObjectQuit::DoExecute(Args &command, CommandReturnObject &result) {
103103
CommandInterpreter::eBroadcastBitQuitCommandReceived;
104104
m_interpreter.BroadcastEvent(event_type);
105105
result.SetStatus(eReturnStatusQuit);
106+
107+
108+
if (m_interpreter.GetSaveSessionOnQuit())
109+
m_interpreter.SaveTranscript(result);
110+
106111
return true;
107112
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#include "CommandObjectSession.h"
2+
#include "lldb/Interpreter/CommandInterpreter.h"
3+
#include "lldb/Interpreter/CommandReturnObject.h"
4+
5+
using namespace lldb;
6+
using namespace lldb_private;
7+
8+
class CommandObjectSessionSave : public CommandObjectParsed {
9+
public:
10+
CommandObjectSessionSave(CommandInterpreter &interpreter)
11+
: CommandObjectParsed(interpreter, "session save",
12+
"Save the current session transcripts to a file.\n"
13+
"If no file if specified, transcripts will be "
14+
"saved to a temporary file.",
15+
"session save [file]") {
16+
CommandArgumentEntry arg1;
17+
arg1.emplace_back(eArgTypePath, eArgRepeatOptional);
18+
m_arguments.push_back(arg1);
19+
}
20+
21+
~CommandObjectSessionSave() override = default;
22+
23+
void
24+
HandleArgumentCompletion(CompletionRequest &request,
25+
OptionElementVector &opt_element_vector) override {
26+
CommandCompletions::InvokeCommonCompletionCallbacks(
27+
GetCommandInterpreter(), CommandCompletions::eDiskFileCompletion,
28+
request, nullptr);
29+
}
30+
31+
protected:
32+
bool DoExecute(Args &args, CommandReturnObject &result) override {
33+
llvm::StringRef file_path;
34+
35+
if (!args.empty())
36+
file_path = args[0].ref();
37+
38+
if (m_interpreter.SaveTranscript(result, file_path.str()))
39+
result.SetStatus(eReturnStatusSuccessFinishNoResult);
40+
else
41+
result.SetStatus(eReturnStatusFailed);
42+
return result.Succeeded();
43+
}
44+
};
45+
46+
CommandObjectSession::CommandObjectSession(CommandInterpreter &interpreter)
47+
: CommandObjectMultiword(interpreter, "session",
48+
"Commands controlling LLDB session.",
49+
"session <subcommand> [<command-options>]") {
50+
LoadSubCommand("save",
51+
CommandObjectSP(new CommandObjectSessionSave(interpreter)));
52+
// TODO: Move 'history' subcommand from CommandObjectCommands.
53+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//===-- CommandObjectSession.h ----------------------------------*- C++ -*-===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#ifndef LLDB_SOURCE_COMMANDS_COMMANDOBJECTSESSION_H
10+
#define LLDB_SOURCE_COMMANDS_COMMANDOBJECTSESSION_H
11+
12+
#include "lldb/Interpreter/CommandObjectMultiword.h"
13+
14+
namespace lldb_private {
15+
16+
class CommandObjectSession : public CommandObjectMultiword {
17+
public:
18+
CommandObjectSession(CommandInterpreter &interpreter);
19+
};
20+
21+
} // namespace lldb_private
22+
23+
#endif // LLDB_SOURCE_COMMANDS_COMMANDOBJECTSESSION_H

lldb/source/Interpreter/CommandInterpreter.cpp

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77
//===----------------------------------------------------------------------===//
88

9+
#include <limits>
910
#include <memory>
1011
#include <stdlib.h>
1112
#include <string>
@@ -31,6 +32,7 @@
3132
#include "Commands/CommandObjectQuit.h"
3233
#include "Commands/CommandObjectRegister.h"
3334
#include "Commands/CommandObjectReproducer.h"
35+
#include "Commands/CommandObjectSession.h"
3436
#include "Commands/CommandObjectSettings.h"
3537
#include "Commands/CommandObjectSource.h"
3638
#include "Commands/CommandObjectStats.h"
@@ -52,6 +54,8 @@
5254
#if LLDB_ENABLE_LIBEDIT
5355
#include "lldb/Host/Editline.h"
5456
#endif
57+
#include "lldb/Host/File.h"
58+
#include "lldb/Host/FileCache.h"
5559
#include "lldb/Host/Host.h"
5660
#include "lldb/Host/HostInfo.h"
5761

@@ -74,6 +78,7 @@
7478
#include "llvm/Support/FormatAdapters.h"
7579
#include "llvm/Support/Path.h"
7680
#include "llvm/Support/PrettyStackTrace.h"
81+
#include "llvm/Support/ScopedPrinter.h"
7782

7883
using namespace lldb;
7984
using namespace lldb_private;
@@ -116,7 +121,7 @@ CommandInterpreter::CommandInterpreter(Debugger &debugger,
116121
m_skip_lldbinit_files(false), m_skip_app_init_files(false),
117122
m_command_io_handler_sp(), m_comment_char('#'),
118123
m_batch_command_mode(false), m_truncation_warning(eNoTruncation),
119-
m_command_source_depth(0), m_result() {
124+
m_command_source_depth(0), m_result(), m_transcript_stream() {
120125
SetEventName(eBroadcastBitThreadShouldExit, "thread-should-exit");
121126
SetEventName(eBroadcastBitResetPrompt, "reset-prompt");
122127
SetEventName(eBroadcastBitQuitCommandReceived, "quit");
@@ -142,6 +147,17 @@ void CommandInterpreter::SetPromptOnQuit(bool enable) {
142147
m_collection_sp->SetPropertyAtIndexAsBoolean(nullptr, idx, enable);
143148
}
144149

150+
bool CommandInterpreter::GetSaveSessionOnQuit() const {
151+
const uint32_t idx = ePropertySaveSessionOnQuit;
152+
return m_collection_sp->GetPropertyAtIndexAsBoolean(
153+
nullptr, idx, g_interpreter_properties[idx].default_uint_value != 0);
154+
}
155+
156+
void CommandInterpreter::SetSaveSessionOnQuit(bool enable) {
157+
const uint32_t idx = ePropertySaveSessionOnQuit;
158+
m_collection_sp->SetPropertyAtIndexAsBoolean(nullptr, idx, enable);
159+
}
160+
145161
bool CommandInterpreter::GetEchoCommands() const {
146162
const uint32_t idx = ePropertyEchoCommands;
147163
return m_collection_sp->GetPropertyAtIndexAsBoolean(
@@ -493,6 +509,7 @@ void CommandInterpreter::LoadCommandDictionary() {
493509
CommandObjectSP(new CommandObjectReproducer(*this));
494510
m_command_dict["script"] =
495511
CommandObjectSP(new CommandObjectScript(*this, script_language));
512+
m_command_dict["session"] = std::make_shared<CommandObjectSession>(*this);
496513
m_command_dict["settings"] =
497514
CommandObjectSP(new CommandObjectMultiwordSettings(*this));
498515
m_command_dict["source"] =
@@ -1667,6 +1684,8 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
16671684
else
16681685
add_to_history = (lazy_add_to_history == eLazyBoolYes);
16691686

1687+
m_transcript_stream << "(lldb) " << command_line << '\n';
1688+
16701689
bool empty_command = false;
16711690
bool comment_command = false;
16721691
if (command_string.empty())
@@ -1799,6 +1818,9 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
17991818
LLDB_LOGF(log, "HandleCommand, command %s",
18001819
(result.Succeeded() ? "succeeded" : "did not succeed"));
18011820

1821+
m_transcript_stream << result.GetOutputData();
1822+
m_transcript_stream << result.GetErrorData();
1823+
18021824
return result.Succeeded();
18031825
}
18041826

@@ -2877,6 +2899,50 @@ bool CommandInterpreter::IOHandlerInterrupt(IOHandler &io_handler) {
28772899
return false;
28782900
}
28792901

2902+
bool CommandInterpreter::SaveTranscript(
2903+
CommandReturnObject &result, llvm::Optional<std::string> output_file) {
2904+
if (output_file == llvm::None || output_file->empty()) {
2905+
std::string now = llvm::to_string(std::chrono::system_clock::now());
2906+
std::replace(now.begin(), now.end(), ' ', '_');
2907+
const std::string file_name = "lldb_session_" + now + ".log";
2908+
FileSpec tmp = HostInfo::GetGlobalTempDir();
2909+
tmp.AppendPathComponent(file_name);
2910+
output_file = tmp.GetPath();
2911+
}
2912+
2913+
auto error_out = [&](llvm::StringRef error_message, std::string description) {
2914+
LLDB_LOG(GetLogIfAllCategoriesSet(LIBLLDB_LOG_COMMANDS), "{0} ({1}:{2})",
2915+
error_message, output_file, description);
2916+
result.AppendErrorWithFormatv(
2917+
"Failed to save session's transcripts to {0}!", *output_file);
2918+
return false;
2919+
};
2920+
2921+
File::OpenOptions flags = File::eOpenOptionWrite |
2922+
File::eOpenOptionCanCreate |
2923+
File::eOpenOptionTruncate;
2924+
2925+
auto opened_file = FileSystem::Instance().Open(FileSpec(*output_file), flags);
2926+
2927+
if (!opened_file)
2928+
return error_out("Unable to create file",
2929+
llvm::toString(opened_file.takeError()));
2930+
2931+
FileUP file = std::move(opened_file.get());
2932+
2933+
size_t byte_size = m_transcript_stream.GetSize();
2934+
2935+
Status error = file->Write(m_transcript_stream.GetData(), byte_size);
2936+
2937+
if (error.Fail() || byte_size != m_transcript_stream.GetSize())
2938+
return error_out("Unable to write to destination file",
2939+
"Bytes written do not match transcript size.");
2940+
2941+
result.AppendMessageWithFormat("Session's transcripts saved to %s\n", output_file->c_str());
2942+
2943+
return true;
2944+
}
2945+
28802946
void CommandInterpreter::GetLLDBCommandsFromIOHandler(
28812947
const char *prompt, IOHandlerDelegate &delegate, void *baton) {
28822948
Debugger &debugger = GetDebugger();

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 SaveSessionOnQuit: Property<"save-session-on-quit", "Boolean">,
13+
Global,
14+
DefaultFalse,
15+
Desc<"If true, LLDB will save the session's transcripts before quitting.">;
1216
def StopCmdSourceOnError: Property<"stop-command-source-on-error", "Boolean">,
1317
Global,
1418
DefaultTrue,
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""
2+
Test the session save feature
3+
"""
4+
5+
import lldb
6+
from lldbsuite.test.decorators import *
7+
from lldbsuite.test.lldbtest import *
8+
from lldbsuite.test import lldbutil
9+
10+
11+
class SessionSaveTestCase(TestBase):
12+
13+
mydir = TestBase.compute_mydir(__file__)
14+
15+
def raw_transcript_builder(self, cmd, res):
16+
raw = "(lldb) " + cmd + "\n"
17+
if res.GetOutputSize():
18+
raw += res.GetOutput()
19+
if res.GetErrorSize():
20+
raw += res.GetError()
21+
return raw
22+
23+
24+
@skipIfWindows
25+
@skipIfReproducer
26+
@no_debug_info_test
27+
def test_session_save(self):
28+
raw = ""
29+
interpreter = self.dbg.GetCommandInterpreter()
30+
31+
settings = [
32+
'settings set interpreter.echo-commands true',
33+
'settings set interpreter.echo-comment-commands true',
34+
'settings set interpreter.stop-command-source-on-error false'
35+
]
36+
37+
for setting in settings:
38+
interpreter.HandleCommand(setting, lldb.SBCommandReturnObject())
39+
40+
inputs = [
41+
'# This is a comment', # Comment
42+
'help session', # Valid command
43+
'Lorem ipsum' # Invalid command
44+
]
45+
46+
for cmd in inputs:
47+
res = lldb.SBCommandReturnObject()
48+
interpreter.HandleCommand(cmd, res)
49+
raw += self.raw_transcript_builder(cmd, res)
50+
51+
self.assertTrue(interpreter.HasCommands())
52+
self.assertTrue(len(raw) != 0)
53+
54+
# Check for error
55+
cmd = 'session save /root/file'
56+
interpreter.HandleCommand(cmd, res)
57+
self.assertFalse(res.Succeeded())
58+
raw += self.raw_transcript_builder(cmd, res)
59+
60+
import tempfile
61+
tf = tempfile.NamedTemporaryFile()
62+
output_file = tf.name
63+
64+
res = lldb.SBCommandReturnObject()
65+
interpreter.HandleCommand('session save ' + output_file, res)
66+
self.assertTrue(res.Succeeded())
67+
raw += self.raw_transcript_builder(cmd, res)
68+
69+
with open(output_file, "r") as file:
70+
content = file.read()
71+
# Exclude last line, since session won't record it's own output
72+
lines = raw.splitlines()[:-1]
73+
for line in lines:
74+
self.assertIn(line, content)

0 commit comments

Comments
 (0)