mib created this revision. mib added reviewers: labath, JDevlieghere. mib added a project: LLDB. Herald added a subscriber: mgorny. mib retitled this revision from "[lldb/interpreter] Add ability to save lldb session to a file" to "[WIP][lldb/interpreter] Add ability to save lldb session to a file". mib added a comment.
Hey folks, this is a first implementation of the patch and surely there will be several other revisions before landing it. Some functionalities are not supported yet, like: - Multi-line expression - Python/Lua? Interactive script interpreter - Outputs that aren't in the `CommandReturnObject` I didn't provide tests because it's still WIP (obviously, it will come it the next revisions) , but at the moment, I'm not sure what would be a good test plan for this feature (suggestions are welcomed). Thanks in advance for the feedback. 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 Signed-off-by: Med Ismail Bennani <medismail.benn...@gmail.com> Repository: rG LLVM Github Monorepo https://reviews.llvm.org/D82155 Files: lldb/include/lldb/Interpreter/CommandInterpreter.h lldb/source/Commands/CMakeLists.txt lldb/source/Commands/CommandObjectQuit.cpp lldb/source/Commands/CommandObjectSession.cpp lldb/source/Commands/CommandObjectSession.h lldb/source/Interpreter/CommandInterpreter.cpp lldb/source/Interpreter/InterpreterProperties.td
Index: lldb/source/Interpreter/InterpreterProperties.td =================================================================== --- lldb/source/Interpreter/InterpreterProperties.td +++ 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, Index: lldb/source/Interpreter/CommandInterpreter.cpp =================================================================== --- lldb/source/Interpreter/CommandInterpreter.cpp +++ lldb/source/Interpreter/CommandInterpreter.cpp @@ -31,6 +31,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 +53,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 +77,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 +120,8 @@ 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_session_transcripts(new StreamString()) { 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"] = CommandObjectSP(new CommandObjectSession(*this)); m_command_dict["settings"] = CommandObjectSP(new CommandObjectMultiwordSettings(*this)); m_command_dict["source"] = @@ -2795,6 +2812,21 @@ lldb_private::CommandReturnObject result(m_debugger.GetUseColor()); HandleCommand(line.c_str(), eLazyBoolCalculate, result); + if (line.find("session save") == line.npos) { + *m_session_transcripts << io_handler.GetPrompt() << ' ' << line.c_str() + << '\n'; + + llvm::StringRef result_output = result.GetOutputData(); + if (!result_output.empty()) + *m_session_transcripts << result_output; + + llvm::StringRef result_error = result.GetErrorData(); + if (!result_error.empty()) + *m_session_transcripts << result_error; + + m_session_transcripts->Flush(); + } + // Now emit the command output text from the command we just executed if ((result.Succeeded() && io_handler.GetFlags().Test(eHandleCommandFlagPrintResult)) || @@ -2877,6 +2909,53 @@ return false; } +bool CommandInterpreter::SaveTranscripts(llvm::StringRef file_path) { + std::string output_file = file_path.str(); + + if (file_path.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(); + } + + File::OpenOptions flags = File::eOpenOptionWrite | + File::eOpenOptionCanCreate | + File::eOpenOptionTruncate; + + Status error; + user_id_t fd_dst = FileCache::GetInstance().OpenFile( + FileSpec(output_file), flags, lldb::eFilePermissionsFileDefault, error); + + auto error_out = [&](llvm::StringRef error_message) { + LLDB_LOG(GetLogIfAllCategoriesSet(LIBLLDB_LOG_COMMANDS), "{0} ({1})", + error_message, output_file); + StreamSP error_stream = GetDebugger().GetErrorStreamSP(); + *error_stream << "Failed to save session's transcripts to " << output_file + << "!" << '\n'; + return false; + }; + + if (fd_dst == UINT64_MAX || error.Fail()) + return error_out("Unable to create file"); + + auto string_stream = + std::static_pointer_cast<StreamString>(m_session_transcripts); + size_t stream_size = string_stream->GetSize(); + + size_t bytes_written = FileCache::GetInstance().WriteFile( + fd_dst, 0, string_stream->GetData(), stream_size, error); + if (bytes_written != stream_size || error.Fail()) + return error_out("Unable to write to destination file"); + + GetDebugger().GetOutputStreamSP()->Printf( + "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(); Index: lldb/source/Commands/CommandObjectSession.h =================================================================== --- /dev/null +++ lldb/source/Commands/CommandObjectSession.h @@ -0,0 +1,33 @@ +//===-- 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 { + +// CommandObjectSession + +class CommandObjectSession : public CommandObjectMultiword { +public: + // Constructors and Destructors + CommandObjectSession(CommandInterpreter &interpreter); + + ~CommandObjectSession() override; + +private: + // For CommandObjectSession only + CommandObjectSession(const CommandObjectSession &) = delete; + const CommandObjectSession &operator=(const CommandObjectSession &) = delete; +}; + +} // namespace lldb_private + +#endif // LLDB_SOURCE_COMMANDS_COMMANDOBJECTSESSION_H Index: lldb/source/Commands/CommandObjectSession.cpp =================================================================== --- /dev/null +++ lldb/source/Commands/CommandObjectSession.cpp @@ -0,0 +1,67 @@ +#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: + // Constructors and Destructors + 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; + CommandArgumentData channel_arg; + + // Define the first (and only) variant of this arg. + channel_arg.arg_type = eArgTypePath; + channel_arg.arg_repetition = eArgRepeatOptional; + + // There is only one variant this argument could be; put it into the + // argument entry. + arg1.push_back(channel_arg); + + 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(); + + bool success = m_interpreter.SaveTranscripts(file_path); + + if (success) + 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 +} + +CommandObjectSession::~CommandObjectSession() = default; Index: lldb/source/Commands/CommandObjectQuit.cpp =================================================================== --- lldb/source/Commands/CommandObjectQuit.cpp +++ lldb/source/Commands/CommandObjectQuit.cpp @@ -72,6 +72,13 @@ } } + if (m_interpreter.GetSaveSessionOnQuit()) { + if (!m_interpreter.SaveTranscripts("")) { + result.SetStatus(eReturnStatusFailed); + return false; + } + } + if (command.GetArgumentCount() > 1) { result.AppendError("Too many arguments for 'quit'. Only an optional exit " "code is allowed"); Index: lldb/source/Commands/CMakeLists.txt =================================================================== --- lldb/source/Commands/CMakeLists.txt +++ 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 Index: lldb/include/lldb/Interpreter/CommandInterpreter.h =================================================================== --- lldb/include/lldb/Interpreter/CommandInterpreter.h +++ lldb/include/lldb/Interpreter/CommandInterpreter.h @@ -485,9 +485,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 +528,8 @@ bool GetSpaceReplPrompts() const; + bool SaveTranscripts(llvm::StringRef file_path); + protected: friend class Debugger; @@ -621,6 +625,8 @@ 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; + + lldb::StreamSP m_session_transcripts; }; } // namespace lldb_private
_______________________________________________ lldb-commits mailing list lldb-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits