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

Reply via email to