diff --git a/lldb/include/lldb/Interpreter/CommandInterpreter.h b/lldb/include/lldb/Interpreter/CommandInterpreter.h --- a/lldb/include/lldb/Interpreter/CommandInterpreter.h +++ b/lldb/include/lldb/Interpreter/CommandInterpreter.h @@ -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 @@ 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 @@ 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 output_file = llvm::None); + protected: friend class Debugger; @@ -621,6 +636,8 @@ llvm::Optional 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 diff --git a/lldb/source/Commands/CMakeLists.txt b/lldb/source/Commands/CMakeLists.txt --- a/lldb/source/Commands/CMakeLists.txt +++ b/lldb/source/Commands/CMakeLists.txt @@ -13,6 +13,7 @@ CommandObjectFrame.cpp CommandObjectGUI.cpp CommandObjectHelp.cpp + CommandObjectLanguage.cpp CommandObjectLog.cpp CommandObjectMemory.cpp CommandObjectMultiword.cpp @@ -22,6 +23,7 @@ CommandObjectQuit.cpp CommandObjectRegister.cpp CommandObjectReproducer.cpp + CommandObjectSession.cpp CommandObjectSettings.cpp CommandObjectSource.cpp CommandObjectStats.cpp @@ -31,7 +33,6 @@ CommandObjectVersion.cpp CommandObjectWatchpoint.cpp CommandObjectWatchpointCommand.cpp - CommandObjectLanguage.cpp LINK_LIBS lldbBase diff --git a/lldb/source/Commands/CommandObjectQuit.cpp b/lldb/source/Commands/CommandObjectQuit.cpp --- a/lldb/source/Commands/CommandObjectQuit.cpp +++ b/lldb/source/Commands/CommandObjectQuit.cpp @@ -103,5 +103,10 @@ CommandInterpreter::eBroadcastBitQuitCommandReceived; m_interpreter.BroadcastEvent(event_type); result.SetStatus(eReturnStatusQuit); + + + if (m_interpreter.GetSaveSessionOnQuit()) + m_interpreter.SaveTranscript(result); + return true; } diff --git a/lldb/source/Commands/CommandObjectSession.h b/lldb/source/Commands/CommandObjectSession.h new file mode 100644 --- /dev/null +++ b/lldb/source/Commands/CommandObjectSession.h @@ -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 diff --git a/lldb/source/Commands/CommandObjectSession.cpp b/lldb/source/Commands/CommandObjectSession.cpp new file mode 100644 --- /dev/null +++ b/lldb/source/Commands/CommandObjectSession.cpp @@ -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 []") { + LoadSubCommand("save", + CommandObjectSP(new CommandObjectSessionSave(interpreter))); + // TODO: Move 'history' subcommand from CommandObjectCommands. +} diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp --- a/lldb/source/Interpreter/CommandInterpreter.cpp +++ b/lldb/source/Interpreter/CommandInterpreter.cpp @@ -6,6 +6,7 @@ // //===----------------------------------------------------------------------===// +#include #include #include #include @@ -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 @@ 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 @@ 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 @@ CommandObjectSP(new CommandObjectReproducer(*this)); m_command_dict["script"] = CommandObjectSP(new CommandObjectScript(*this, script_language)); + m_command_dict["session"] = std::make_shared(*this); m_command_dict["settings"] = CommandObjectSP(new CommandObjectMultiwordSettings(*this)); m_command_dict["source"] = @@ -1667,6 +1684,8 @@ 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 @@ 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 @@ return false; } +bool CommandInterpreter::SaveTranscript( + CommandReturnObject &result, llvm::Optional 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(); diff --git a/lldb/source/Interpreter/InterpreterProperties.td b/lldb/source/Interpreter/InterpreterProperties.td --- a/lldb/source/Interpreter/InterpreterProperties.td +++ b/lldb/source/Interpreter/InterpreterProperties.td @@ -9,6 +9,10 @@ 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, diff --git a/lldb/test/API/commands/session/save/TestSessionSave.py b/lldb/test/API/commands/session/save/TestSessionSave.py new file mode 100644 --- /dev/null +++ b/lldb/test/API/commands/session/save/TestSessionSave.py @@ -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)