[lldb] Add a log dump command
authorJonas Devlieghere <jonas@devlieghere.com>
Mon, 27 Jun 2022 17:00:05 +0000 (10:00 -0700)
committerJonas Devlieghere <jonas@devlieghere.com>
Mon, 27 Jun 2022 17:02:34 +0000 (10:02 -0700)
Add a log dump command to dump logs to a file. This only works for
channels that have a log handler associated that supports dumping. For
now that's limited to the circular log handler, but more could be added
in the future.

Differential revision: https://reviews.llvm.org/D128557

lldb/include/lldb/Host/Host.h
lldb/include/lldb/Utility/Log.h
lldb/source/Commands/CommandObjectLog.cpp
lldb/source/Commands/Options.td
lldb/source/Host/common/Host.cpp
lldb/source/Utility/Log.cpp
lldb/test/API/commands/log/basic/TestLogHandlers.py [new file with mode: 0644]

index b3e0cba..4fc2bd1 100644 (file)
@@ -261,6 +261,12 @@ class SystemLogHandler : public LogHandler {
 public:
   SystemLogHandler();
   void Emit(llvm::StringRef message) override;
+
+  bool isA(const void *ClassID) const override { return ClassID == &ID; }
+  static bool classof(const LogHandler *obj) { return obj->isA(&ID); }
+
+private:
+  static char ID;
 };
 
 } // namespace lldb_private
index 4772291..404cbed 100644 (file)
@@ -49,6 +49,12 @@ class LogHandler {
 public:
   virtual ~LogHandler() = default;
   virtual void Emit(llvm::StringRef message) = 0;
+
+  virtual bool isA(const void *ClassID) const { return ClassID == &ID; }
+  static bool classof(const LogHandler *obj) { return obj->isA(&ID); }
+
+private:
+  static char ID;
 };
 
 class StreamLogHandler : public LogHandler {
@@ -59,9 +65,13 @@ public:
   void Emit(llvm::StringRef message) override;
   void Flush();
 
+  bool isA(const void *ClassID) const override { return ClassID == &ID; }
+  static bool classof(const LogHandler *obj) { return obj->isA(&ID); }
+
 private:
   std::mutex m_mutex;
   llvm::raw_fd_ostream m_stream;
+  static char ID;
 };
 
 class CallbackLogHandler : public LogHandler {
@@ -70,9 +80,13 @@ public:
 
   void Emit(llvm::StringRef message) override;
 
+  bool isA(const void *ClassID) const override { return ClassID == &ID; }
+  static bool classof(const LogHandler *obj) { return obj->isA(&ID); }
+
 private:
   lldb::LogOutputCallback m_callback;
   void *m_baton;
+  static char ID;
 };
 
 class RotatingLogHandler : public LogHandler {
@@ -82,6 +96,9 @@ public:
   void Emit(llvm::StringRef message) override;
   void Dump(llvm::raw_ostream &stream) const;
 
+  bool isA(const void *ClassID) const override { return ClassID == &ID; }
+  static bool classof(const LogHandler *obj) { return obj->isA(&ID); }
+
 private:
   size_t NormalizeIndex(size_t i) const;
   size_t GetNumMessages() const;
@@ -92,6 +109,7 @@ private:
   const size_t m_size = 0;
   size_t m_next_index = 0;
   size_t m_total_count = 0;
+  static char ID;
 };
 
 class Log final {
@@ -169,6 +187,10 @@ public:
                                 llvm::ArrayRef<const char *> categories,
                                 llvm::raw_ostream &error_stream);
 
+  static bool DumpLogChannel(llvm::StringRef channel,
+                             llvm::raw_ostream &output_stream,
+                             llvm::raw_ostream &error_stream);
+
   static bool ListChannelCategories(llvm::StringRef channel,
                                     llvm::raw_ostream &stream);
 
@@ -258,6 +280,8 @@ private:
 
   void Disable(uint32_t flags);
 
+  bool Dump(llvm::raw_ostream &stream);
+
   typedef llvm::StringMap<Log> ChannelMap;
   static llvm::ManagedStatic<ChannelMap> g_channel_map;
 
index 349af26..684cb35 100644 (file)
@@ -56,6 +56,9 @@ static constexpr OptionEnumValues LogHandlerType() {
 #define LLDB_OPTIONS_log_enable
 #include "CommandOptions.inc"
 
+#define LLDB_OPTIONS_log_dump
+#include "CommandOptions.inc"
+
 /// Common completion logic for log enable/disable.
 static void CompleteEnableDisable(CompletionRequest &request) {
   size_t arg_index = request.GetCursorIndex();
@@ -345,6 +348,114 @@ protected:
     return result.Succeeded();
   }
 };
+class CommandObjectLogDump : public CommandObjectParsed {
+public:
+  CommandObjectLogDump(CommandInterpreter &interpreter)
+      : CommandObjectParsed(interpreter, "log dump",
+                            "dump circular buffer logs", nullptr) {
+    CommandArgumentEntry arg1;
+    CommandArgumentData channel_arg;
+
+    // Define the first (and only) variant of this arg.
+    channel_arg.arg_type = eArgTypeLogChannel;
+    channel_arg.arg_repetition = eArgRepeatPlain;
+
+    // There is only one variant this argument could be; put it into the
+    // argument entry.
+    arg1.push_back(channel_arg);
+
+    // Push the data for the first argument into the m_arguments vector.
+    m_arguments.push_back(arg1);
+  }
+
+  ~CommandObjectLogDump() override = default;
+
+  Options *GetOptions() override { return &m_options; }
+
+  class CommandOptions : public Options {
+  public:
+    CommandOptions() = default;
+
+    ~CommandOptions() override = default;
+
+    Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg,
+                          ExecutionContext *execution_context) override {
+      Status error;
+      const int short_option = m_getopt_table[option_idx].val;
+
+      switch (short_option) {
+      case 'f':
+        log_file.SetFile(option_arg, FileSpec::Style::native);
+        FileSystem::Instance().Resolve(log_file);
+        break;
+      default:
+        llvm_unreachable("Unimplemented option");
+      }
+
+      return error;
+    }
+
+    void OptionParsingStarting(ExecutionContext *execution_context) override {
+      log_file.Clear();
+    }
+
+    llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
+      return llvm::makeArrayRef(g_log_dump_options);
+    }
+
+    FileSpec log_file;
+  };
+
+  void
+  HandleArgumentCompletion(CompletionRequest &request,
+                           OptionElementVector &opt_element_vector) override {
+    CompleteEnableDisable(request);
+  }
+
+protected:
+  bool DoExecute(Args &args, CommandReturnObject &result) override {
+    if (args.empty()) {
+      result.AppendErrorWithFormat(
+          "%s takes a log channel and one or more log types.\n",
+          m_cmd_name.c_str());
+      return false;
+    }
+
+    std::unique_ptr<llvm::raw_ostream> stream_up;
+    if (m_options.log_file) {
+      const File::OpenOptions flags = File::eOpenOptionWriteOnly |
+                                      File::eOpenOptionCanCreate |
+                                      File::eOpenOptionTruncate;
+      llvm::Expected<FileUP> file = FileSystem::Instance().Open(
+          m_options.log_file, flags, lldb::eFilePermissionsFileDefault, false);
+      if (!file) {
+        result.AppendErrorWithFormat("Unable to open log file '%s': %s",
+                                     m_options.log_file.GetCString(),
+                                     llvm::toString(file.takeError()).c_str());
+        return false;
+      }
+      stream_up = std::make_unique<llvm::raw_fd_ostream>(
+          (*file)->GetDescriptor(), /*shouldClose=*/true);
+    } else {
+      stream_up = std::make_unique<llvm::raw_fd_ostream>(
+          GetDebugger().GetOutputFile().GetDescriptor(), /*shouldClose=*/false);
+    }
+
+    const std::string channel = std::string(args[0].ref());
+    std::string error;
+    llvm::raw_string_ostream error_stream(error);
+    if (Log::DumpLogChannel(channel, *stream_up, error_stream)) {
+      result.SetStatus(eReturnStatusSuccessFinishNoResult);
+    } else {
+      result.SetStatus(eReturnStatusFailed);
+      result.GetErrorStream() << error_stream.str();
+    }
+
+    return result.Succeeded();
+  }
+
+  CommandOptions m_options;
+};
 
 class CommandObjectLogTimerEnable : public CommandObjectParsed {
 public:
@@ -554,6 +665,8 @@ CommandObjectLog::CommandObjectLog(CommandInterpreter &interpreter)
                  CommandObjectSP(new CommandObjectLogDisable(interpreter)));
   LoadSubCommand("list",
                  CommandObjectSP(new CommandObjectLogList(interpreter)));
+  LoadSubCommand("dump",
+                 CommandObjectSP(new CommandObjectLogDump(interpreter)));
   LoadSubCommand("timers",
                  CommandObjectSP(new CommandObjectLogTimer(interpreter)));
 }
index 53e09cf..6e5f1ef 100644 (file)
@@ -456,6 +456,11 @@ let Command = "log enable" in {
     Desc<"Prepend the names of files and function that generate the logs.">;
 }
 
+let Command = "log dump" in {
+  def log_dump_file : Option<"file", "f">, Group<1>, Arg<"Filename">,
+    Desc<"Set the destination file to dump to.">;
+}
+
 let Command = "reproducer dump" in {
   def reproducer_provider : Option<"provider", "p">, Group<1>,
     EnumArg<"None", "ReproducerProviderType()">,
index a0834ca..f35eb47 100644 (file)
@@ -632,6 +632,8 @@ uint32_t Host::FindProcesses(const ProcessInstanceInfoMatch &match_info,
   return result;
 }
 
+char SystemLogHandler::ID;
+
 SystemLogHandler::SystemLogHandler() {}
 
 void SystemLogHandler::Emit(llvm::StringRef message) {
index 4c3e05a..67edb15 100644 (file)
@@ -13,6 +13,7 @@
 #include "llvm/ADT/Twine.h"
 #include "llvm/ADT/iterator.h"
 
+#include "llvm/Support/Casting.h"
 #include "llvm/Support/Chrono.h"
 #include "llvm/Support/ManagedStatic.h"
 #include "llvm/Support/Path.h"
 
 using namespace lldb_private;
 
+char LogHandler::ID;
+char StreamLogHandler::ID;
+char CallbackLogHandler::ID;
+char RotatingLogHandler::ID;
+
 llvm::ManagedStatic<Log::ChannelMap> Log::g_channel_map;
 
 void Log::ForEachCategory(
@@ -106,6 +112,16 @@ void Log::Disable(uint32_t flags) {
   }
 }
 
+bool Log::Dump(llvm::raw_ostream &output_stream) {
+  llvm::sys::ScopedReader lock(m_mutex);
+  if (RotatingLogHandler *handler =
+          llvm::dyn_cast_or_null<RotatingLogHandler>(m_handler.get())) {
+    handler->Dump(output_stream);
+    return true;
+  }
+  return false;
+}
+
 const Flags Log::GetOptions() const {
   return m_options.load(std::memory_order_relaxed);
 }
@@ -222,6 +238,22 @@ bool Log::DisableLogChannel(llvm::StringRef channel,
   return true;
 }
 
+bool Log::DumpLogChannel(llvm::StringRef channel,
+                         llvm::raw_ostream &output_stream,
+                         llvm::raw_ostream &error_stream) {
+  auto iter = g_channel_map->find(channel);
+  if (iter == g_channel_map->end()) {
+    error_stream << llvm::formatv("Invalid log channel '{0}'.\n", channel);
+    return false;
+  }
+  if (!iter->second.Dump(output_stream)) {
+    error_stream << llvm::formatv(
+        "log channel '{0}' does not support dumping.\n", channel);
+    return false;
+  }
+  return true;
+}
+
 bool Log::ListChannelCategories(llvm::StringRef channel,
                                 llvm::raw_ostream &stream) {
   auto ch = g_channel_map->find(channel);
diff --git a/lldb/test/API/commands/log/basic/TestLogHandlers.py b/lldb/test/API/commands/log/basic/TestLogHandlers.py
new file mode 100644 (file)
index 0000000..367813b
--- /dev/null
@@ -0,0 +1,55 @@
+"""
+Test lldb log handlers.
+"""
+
+import os
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class LogHandlerTestCase(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    def setUp(self):
+        TestBase.setUp(self)
+        self.log_file = self.getBuildArtifact("log-file.txt")
+        if (os.path.exists(self.log_file)):
+            os.remove(self.log_file)
+
+    def test_circular(self):
+        self.runCmd("log enable -b 5 -h circular lldb commands")
+        self.runCmd("bogus", check=False)
+        self.runCmd("log dump lldb -f {}".format(self.log_file))
+
+        with open(self.log_file, 'r') as f:
+            log_lines = f.readlines()
+
+        self.assertEqual(len(log_lines), 5)
+
+        found_command_log_dump = False
+        found_command_bogus = False
+
+        for line in log_lines:
+            if 'Processing command: log dump' in line:
+                found_command_log_dump = True
+            if 'Processing command: bogus' in line:
+                found_command_bogus = True
+
+        self.assertTrue(found_command_log_dump)
+        self.assertFalse(found_command_bogus)
+
+    def test_circular_no_buffer_size(self):
+        self.expect(
+            "log enable -h circular lldb commands",
+            error=True,
+            substrs=[
+                'the circular buffer handler requires a non-zero buffer size'
+            ])
+
+    def test_dump_unsupported(self):
+        self.runCmd("log enable lldb commands -f {}".format(self.log_file))
+        self.expect("log dump lldb",
+                    error=True,
+                    substrs=["log channel 'lldb' does not support dumping"])