[Lldb-commits] [lldb] 5bb742b - [lldb/interpreter] Add ability to save lldb session to a file

Med Ismail Bennani via lldb-commits lldb-commits at lists.llvm.org
Wed Jul 22 02:43:33 PDT 2020


Author: Med Ismail Bennani
Date: 2020-07-22T11:43:16+02:00
New Revision: 5bb742b10dafd595223172ae985687765934ebe9

URL: https://github.com/llvm/llvm-project/commit/5bb742b10dafd595223172ae985687765934ebe9
DIFF: https://github.com/llvm/llvm-project/commit/5bb742b10dafd595223172ae985687765934ebe9.diff

LOG: [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 at gmail.com>

Added: 
    lldb/source/Commands/CommandObjectSession.cpp
    lldb/source/Commands/CommandObjectSession.h
    lldb/test/API/commands/session/save/TestSessionSave.py

Modified: 
    lldb/include/lldb/Interpreter/CommandInterpreter.h
    lldb/source/Commands/CMakeLists.txt
    lldb/source/Commands/CommandObjectQuit.cpp
    lldb/source/Interpreter/CommandInterpreter.cpp
    lldb/source/Interpreter/InterpreterProperties.td

Removed: 
    


################################################################################
diff  --git a/lldb/include/lldb/Interpreter/CommandInterpreter.h b/lldb/include/lldb/Interpreter/CommandInterpreter.h
index 8a9dce7a19bc..72638f3d807f 100644
--- 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 @@ 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);
 
@@ -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;
 
@@ -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

diff  --git a/lldb/source/Commands/CMakeLists.txt b/lldb/source/Commands/CMakeLists.txt
index 28cbb2cd6cf6..28bcfacdf3e8 100644
--- a/lldb/source/Commands/CMakeLists.txt
+++ b/lldb/source/Commands/CMakeLists.txt
@@ -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

diff  --git a/lldb/source/Commands/CommandObjectQuit.cpp b/lldb/source/Commands/CommandObjectQuit.cpp
index d0c7bbd3abf8..c9fb59acc83f 100644
--- a/lldb/source/Commands/CommandObjectQuit.cpp
+++ b/lldb/source/Commands/CommandObjectQuit.cpp
@@ -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;
 }

diff  --git a/lldb/source/Commands/CommandObjectSession.cpp b/lldb/source/Commands/CommandObjectSession.cpp
new file mode 100644
index 000000000000..54c877137d55
--- /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 <subcommand> [<command-options>]") {
+  LoadSubCommand("save",
+                 CommandObjectSP(new CommandObjectSessionSave(interpreter)));
+  //  TODO: Move 'history' subcommand from CommandObjectCommands.
+}

diff  --git a/lldb/source/Commands/CommandObjectSession.h b/lldb/source/Commands/CommandObjectSession.h
new file mode 100644
index 000000000000..0af0e279f47e
--- /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/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp
index baceeac7fbae..1eddf0b5fa12 100644
--- a/lldb/source/Interpreter/CommandInterpreter.cpp
+++ b/lldb/source/Interpreter/CommandInterpreter.cpp
@@ -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();

diff  --git a/lldb/source/Interpreter/InterpreterProperties.td b/lldb/source/Interpreter/InterpreterProperties.td
index 600c1e3edb9b..e5346d14a6e1 100644
--- a/lldb/source/Interpreter/InterpreterProperties.td
+++ b/lldb/source/Interpreter/InterpreterProperties.td
@@ -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,

diff  --git a/lldb/test/API/commands/session/save/TestSessionSave.py b/lldb/test/API/commands/session/save/TestSessionSave.py
new file mode 100644
index 000000000000..ec244a42efca
--- /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)


        


More information about the lldb-commits mailing list