[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 <medismail.bennani@gmail.com>
This commit is contained in:
Med Ismail Bennani 2020-07-21 16:29:16 +02:00
parent a4bbc3b763
commit 5bb742b10d
8 changed files with 246 additions and 3 deletions

View File

@ -20,6 +20,7 @@
#include "lldb/Utility/CompletionRequest.h"
#include "lldb/Utility/Event.h"
#include "lldb/Utility/Log.h"
#include "lldb/Utility/StreamString.h"
#include "lldb/Utility/StringList.h"
#include "lldb/lldb-forward.h"
#include "lldb/lldb-private.h"
@ -485,9 +486,11 @@ public:
bool GetExpandRegexAliases() const;
bool GetPromptOnQuit() const;
void SetPromptOnQuit(bool enable);
bool GetSaveSessionOnQuit() const;
void SetSaveSessionOnQuit(bool enable);
bool GetEchoCommands() const;
void SetEchoCommands(bool enable);
@ -526,6 +529,18 @@ public:
bool GetSpaceReplPrompts() const;
/// Save the current debugger session transcript to a file on disk.
/// \param output_file
/// The file path to which the session transcript will be written. Since
/// the argument is optional, an arbitrary temporary file will be create
/// when no argument is passed.
/// \param result
/// This is used to pass function output and error messages.
/// \return \b true if the session transcript was successfully written to
/// disk, \b false otherwise.
bool SaveTranscript(CommandReturnObject &result,
llvm::Optional<std::string> output_file = llvm::None);
protected:
friend class Debugger;
@ -621,6 +636,8 @@ private:
llvm::Optional<int> m_quit_exit_code;
// If the driver is accepts custom exit codes for the 'quit' command.
bool m_allow_exit_code = false;
StreamString m_transcript_stream;
};
} // namespace lldb_private

View File

@ -13,6 +13,7 @@ add_lldb_library(lldbCommands
CommandObjectFrame.cpp
CommandObjectGUI.cpp
CommandObjectHelp.cpp
CommandObjectLanguage.cpp
CommandObjectLog.cpp
CommandObjectMemory.cpp
CommandObjectMultiword.cpp
@ -22,6 +23,7 @@ add_lldb_library(lldbCommands
CommandObjectQuit.cpp
CommandObjectRegister.cpp
CommandObjectReproducer.cpp
CommandObjectSession.cpp
CommandObjectSettings.cpp
CommandObjectSource.cpp
CommandObjectStats.cpp
@ -31,7 +33,6 @@ add_lldb_library(lldbCommands
CommandObjectVersion.cpp
CommandObjectWatchpoint.cpp
CommandObjectWatchpointCommand.cpp
CommandObjectLanguage.cpp
LINK_LIBS
lldbBase

View File

@ -103,5 +103,10 @@ bool CommandObjectQuit::DoExecute(Args &command, CommandReturnObject &result) {
CommandInterpreter::eBroadcastBitQuitCommandReceived;
m_interpreter.BroadcastEvent(event_type);
result.SetStatus(eReturnStatusQuit);
if (m_interpreter.GetSaveSessionOnQuit())
m_interpreter.SaveTranscript(result);
return true;
}

View File

@ -0,0 +1,53 @@
#include "CommandObjectSession.h"
#include "lldb/Interpreter/CommandInterpreter.h"
#include "lldb/Interpreter/CommandReturnObject.h"
using namespace lldb;
using namespace lldb_private;
class CommandObjectSessionSave : public CommandObjectParsed {
public:
CommandObjectSessionSave(CommandInterpreter &interpreter)
: CommandObjectParsed(interpreter, "session save",
"Save the current session transcripts to a file.\n"
"If no file if specified, transcripts will be "
"saved to a temporary file.",
"session save [file]") {
CommandArgumentEntry arg1;
arg1.emplace_back(eArgTypePath, eArgRepeatOptional);
m_arguments.push_back(arg1);
}
~CommandObjectSessionSave() override = default;
void
HandleArgumentCompletion(CompletionRequest &request,
OptionElementVector &opt_element_vector) override {
CommandCompletions::InvokeCommonCompletionCallbacks(
GetCommandInterpreter(), CommandCompletions::eDiskFileCompletion,
request, nullptr);
}
protected:
bool DoExecute(Args &args, CommandReturnObject &result) override {
llvm::StringRef file_path;
if (!args.empty())
file_path = args[0].ref();
if (m_interpreter.SaveTranscript(result, file_path.str()))
result.SetStatus(eReturnStatusSuccessFinishNoResult);
else
result.SetStatus(eReturnStatusFailed);
return result.Succeeded();
}
};
CommandObjectSession::CommandObjectSession(CommandInterpreter &interpreter)
: CommandObjectMultiword(interpreter, "session",
"Commands controlling LLDB session.",
"session <subcommand> [<command-options>]") {
LoadSubCommand("save",
CommandObjectSP(new CommandObjectSessionSave(interpreter)));
// TODO: Move 'history' subcommand from CommandObjectCommands.
}

View File

@ -0,0 +1,23 @@
//===-- CommandObjectSession.h ----------------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#ifndef LLDB_SOURCE_COMMANDS_COMMANDOBJECTSESSION_H
#define LLDB_SOURCE_COMMANDS_COMMANDOBJECTSESSION_H
#include "lldb/Interpreter/CommandObjectMultiword.h"
namespace lldb_private {
class CommandObjectSession : public CommandObjectMultiword {
public:
CommandObjectSession(CommandInterpreter &interpreter);
};
} // namespace lldb_private
#endif // LLDB_SOURCE_COMMANDS_COMMANDOBJECTSESSION_H

View File

@ -6,6 +6,7 @@
//
//===----------------------------------------------------------------------===//
#include <limits>
#include <memory>
#include <stdlib.h>
#include <string>
@ -31,6 +32,7 @@
#include "Commands/CommandObjectQuit.h"
#include "Commands/CommandObjectRegister.h"
#include "Commands/CommandObjectReproducer.h"
#include "Commands/CommandObjectSession.h"
#include "Commands/CommandObjectSettings.h"
#include "Commands/CommandObjectSource.h"
#include "Commands/CommandObjectStats.h"
@ -52,6 +54,8 @@
#if LLDB_ENABLE_LIBEDIT
#include "lldb/Host/Editline.h"
#endif
#include "lldb/Host/File.h"
#include "lldb/Host/FileCache.h"
#include "lldb/Host/Host.h"
#include "lldb/Host/HostInfo.h"
@ -74,6 +78,7 @@
#include "llvm/Support/FormatAdapters.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/PrettyStackTrace.h"
#include "llvm/Support/ScopedPrinter.h"
using namespace lldb;
using namespace lldb_private;
@ -116,7 +121,7 @@ CommandInterpreter::CommandInterpreter(Debugger &debugger,
m_skip_lldbinit_files(false), m_skip_app_init_files(false),
m_command_io_handler_sp(), m_comment_char('#'),
m_batch_command_mode(false), m_truncation_warning(eNoTruncation),
m_command_source_depth(0), m_result() {
m_command_source_depth(0), m_result(), m_transcript_stream() {
SetEventName(eBroadcastBitThreadShouldExit, "thread-should-exit");
SetEventName(eBroadcastBitResetPrompt, "reset-prompt");
SetEventName(eBroadcastBitQuitCommandReceived, "quit");
@ -142,6 +147,17 @@ void CommandInterpreter::SetPromptOnQuit(bool enable) {
m_collection_sp->SetPropertyAtIndexAsBoolean(nullptr, idx, enable);
}
bool CommandInterpreter::GetSaveSessionOnQuit() const {
const uint32_t idx = ePropertySaveSessionOnQuit;
return m_collection_sp->GetPropertyAtIndexAsBoolean(
nullptr, idx, g_interpreter_properties[idx].default_uint_value != 0);
}
void CommandInterpreter::SetSaveSessionOnQuit(bool enable) {
const uint32_t idx = ePropertySaveSessionOnQuit;
m_collection_sp->SetPropertyAtIndexAsBoolean(nullptr, idx, enable);
}
bool CommandInterpreter::GetEchoCommands() const {
const uint32_t idx = ePropertyEchoCommands;
return m_collection_sp->GetPropertyAtIndexAsBoolean(
@ -493,6 +509,7 @@ void CommandInterpreter::LoadCommandDictionary() {
CommandObjectSP(new CommandObjectReproducer(*this));
m_command_dict["script"] =
CommandObjectSP(new CommandObjectScript(*this, script_language));
m_command_dict["session"] = std::make_shared<CommandObjectSession>(*this);
m_command_dict["settings"] =
CommandObjectSP(new CommandObjectMultiwordSettings(*this));
m_command_dict["source"] =
@ -1667,6 +1684,8 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
else
add_to_history = (lazy_add_to_history == eLazyBoolYes);
m_transcript_stream << "(lldb) " << command_line << '\n';
bool empty_command = false;
bool comment_command = false;
if (command_string.empty())
@ -1799,6 +1818,9 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
LLDB_LOGF(log, "HandleCommand, command %s",
(result.Succeeded() ? "succeeded" : "did not succeed"));
m_transcript_stream << result.GetOutputData();
m_transcript_stream << result.GetErrorData();
return result.Succeeded();
}
@ -2877,6 +2899,50 @@ bool CommandInterpreter::IOHandlerInterrupt(IOHandler &io_handler) {
return false;
}
bool CommandInterpreter::SaveTranscript(
CommandReturnObject &result, llvm::Optional<std::string> output_file) {
if (output_file == llvm::None || output_file->empty()) {
std::string now = llvm::to_string(std::chrono::system_clock::now());
std::replace(now.begin(), now.end(), ' ', '_');
const std::string file_name = "lldb_session_" + now + ".log";
FileSpec tmp = HostInfo::GetGlobalTempDir();
tmp.AppendPathComponent(file_name);
output_file = tmp.GetPath();
}
auto error_out = [&](llvm::StringRef error_message, std::string description) {
LLDB_LOG(GetLogIfAllCategoriesSet(LIBLLDB_LOG_COMMANDS), "{0} ({1}:{2})",
error_message, output_file, description);
result.AppendErrorWithFormatv(
"Failed to save session's transcripts to {0}!", *output_file);
return false;
};
File::OpenOptions flags = File::eOpenOptionWrite |
File::eOpenOptionCanCreate |
File::eOpenOptionTruncate;
auto opened_file = FileSystem::Instance().Open(FileSpec(*output_file), flags);
if (!opened_file)
return error_out("Unable to create file",
llvm::toString(opened_file.takeError()));
FileUP file = std::move(opened_file.get());
size_t byte_size = m_transcript_stream.GetSize();
Status error = file->Write(m_transcript_stream.GetData(), byte_size);
if (error.Fail() || byte_size != m_transcript_stream.GetSize())
return error_out("Unable to write to destination file",
"Bytes written do not match transcript size.");
result.AppendMessageWithFormat("Session's transcripts saved to %s\n", output_file->c_str());
return true;
}
void CommandInterpreter::GetLLDBCommandsFromIOHandler(
const char *prompt, IOHandlerDelegate &delegate, void *baton) {
Debugger &debugger = GetDebugger();

View File

@ -9,6 +9,10 @@ let Definition = "interpreter" in {
Global,
DefaultTrue,
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.">;
def SaveSessionOnQuit: Property<"save-session-on-quit", "Boolean">,
Global,
DefaultFalse,
Desc<"If true, LLDB will save the session's transcripts before quitting.">;
def StopCmdSourceOnError: Property<"stop-command-source-on-error", "Boolean">,
Global,
DefaultTrue,

View File

@ -0,0 +1,74 @@
"""
Test the session save feature
"""
import lldb
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil
class SessionSaveTestCase(TestBase):
mydir = TestBase.compute_mydir(__file__)
def raw_transcript_builder(self, cmd, res):
raw = "(lldb) " + cmd + "\n"
if res.GetOutputSize():
raw += res.GetOutput()
if res.GetErrorSize():
raw += res.GetError()
return raw
@skipIfWindows
@skipIfReproducer
@no_debug_info_test
def test_session_save(self):
raw = ""
interpreter = self.dbg.GetCommandInterpreter()
settings = [
'settings set interpreter.echo-commands true',
'settings set interpreter.echo-comment-commands true',
'settings set interpreter.stop-command-source-on-error false'
]
for setting in settings:
interpreter.HandleCommand(setting, lldb.SBCommandReturnObject())
inputs = [
'# This is a comment', # Comment
'help session', # Valid command
'Lorem ipsum' # Invalid command
]
for cmd in inputs:
res = lldb.SBCommandReturnObject()
interpreter.HandleCommand(cmd, res)
raw += self.raw_transcript_builder(cmd, res)
self.assertTrue(interpreter.HasCommands())
self.assertTrue(len(raw) != 0)
# Check for error
cmd = 'session save /root/file'
interpreter.HandleCommand(cmd, res)
self.assertFalse(res.Succeeded())
raw += self.raw_transcript_builder(cmd, res)
import tempfile
tf = tempfile.NamedTemporaryFile()
output_file = tf.name
res = lldb.SBCommandReturnObject()
interpreter.HandleCommand('session save ' + output_file, res)
self.assertTrue(res.Succeeded())
raw += self.raw_transcript_builder(cmd, res)
with open(output_file, "r") as file:
content = file.read()
# Exclude last line, since session won't record it's own output
lines = raw.splitlines()[:-1]
for line in lines:
self.assertIn(line, content)