Skip to content

Commit

Permalink
[lldb/interpreter] Add ability to save lldb session to a file
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
medismailben committed Jul 22, 2020
1 parent a4bbc3b commit 5bb742b
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 3 deletions.
19 changes: 18 additions & 1 deletion lldb/include/lldb/Interpreter/CommandInterpreter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -485,9 +486,11 @@ class CommandInterpreter : public Broadcaster,
bool GetExpandRegexAliases() const;

bool GetPromptOnQuit() const;

void SetPromptOnQuit(bool enable);

bool GetSaveSessionOnQuit() const;
void SetSaveSessionOnQuit(bool enable);

bool GetEchoCommands() const;
void SetEchoCommands(bool enable);

Expand Down Expand Up @@ -526,6 +529,18 @@ class CommandInterpreter : public Broadcaster,

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;

Expand Down Expand Up @@ -621,6 +636,8 @@ class CommandInterpreter : public Broadcaster,
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
Expand Down
3 changes: 2 additions & 1 deletion lldb/source/Commands/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ add_lldb_library(lldbCommands
CommandObjectFrame.cpp
CommandObjectGUI.cpp
CommandObjectHelp.cpp
CommandObjectLanguage.cpp
CommandObjectLog.cpp
CommandObjectMemory.cpp
CommandObjectMultiword.cpp
Expand All @@ -22,6 +23,7 @@ add_lldb_library(lldbCommands
CommandObjectQuit.cpp
CommandObjectRegister.cpp
CommandObjectReproducer.cpp
CommandObjectSession.cpp
CommandObjectSettings.cpp
CommandObjectSource.cpp
CommandObjectStats.cpp
Expand All @@ -31,7 +33,6 @@ add_lldb_library(lldbCommands
CommandObjectVersion.cpp
CommandObjectWatchpoint.cpp
CommandObjectWatchpointCommand.cpp
CommandObjectLanguage.cpp

LINK_LIBS
lldbBase
Expand Down
5 changes: 5 additions & 0 deletions lldb/source/Commands/CommandObjectQuit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
53 changes: 53 additions & 0 deletions lldb/source/Commands/CommandObjectSession.cpp
Original file line number Diff line number Diff line change
@@ -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.
}
23 changes: 23 additions & 0 deletions lldb/source/Commands/CommandObjectSession.h
Original file line number Diff line number Diff line change
@@ -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
68 changes: 67 additions & 1 deletion lldb/source/Interpreter/CommandInterpreter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//
//===----------------------------------------------------------------------===//

#include <limits>
#include <memory>
#include <stdlib.h>
#include <string>
Expand All @@ -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"
Expand All @@ -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"

Expand All @@ -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;
Expand Down Expand Up @@ -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");
Expand All @@ -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(
Expand Down Expand Up @@ -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"] =
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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();
Expand Down
4 changes: 4 additions & 0 deletions lldb/source/Interpreter/InterpreterProperties.td
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
74 changes: 74 additions & 0 deletions lldb/test/API/commands/session/save/TestSessionSave.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 5bb742b

Please sign in to comment.