diff --git a/lldb/include/lldb/Core/BugReporter.h b/lldb/include/lldb/Core/BugReporter.h new file mode 100644 index 0000000000000..6a0b3c936008e --- /dev/null +++ b/lldb/include/lldb/Core/BugReporter.h @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// 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_CORE_BUGREPORTER_H +#define LLDB_CORE_BUGREPORTER_H + +#include "lldb/Core/Diagnostics.h" +#include "lldb/Core/PluginInterface.h" + +#include "llvm/Support/Error.h" + +namespace lldb_private { + +/// A pluggable destination for a diagnostics bundle. +/// CreateBugReporterInstance() returns the first registered reporter, so +/// downstream registers ahead of the no-op fallback to take over. +class BugReporter : public PluginInterface { +public: + virtual llvm::Error File(const Diagnostics::Report &report) = 0; +}; + +} // namespace lldb_private + +#endif // LLDB_CORE_BUGREPORTER_H diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h index 29770ede39e53..2fbd0905d1b29 100644 --- a/lldb/include/lldb/Core/Debugger.h +++ b/lldb/include/lldb/Core/Debugger.h @@ -16,6 +16,7 @@ #include #include "lldb/Core/DebuggerEvents.h" +#include "lldb/Core/Diagnostics.h" #include "lldb/Core/FormatEntity.h" #include "lldb/Core/IOHandler.h" #include "lldb/Core/SourceManager.h" @@ -31,7 +32,6 @@ #include "lldb/Target/TargetList.h" #include "lldb/Utility/Broadcaster.h" #include "lldb/Utility/ConstString.h" -#include "lldb/Utility/Diagnostics.h" #include "lldb/Utility/FileSpec.h" #include "lldb/Utility/Status.h" #include "lldb/Utility/StructuredData.h" @@ -278,6 +278,11 @@ class Debugger : public std::enable_shared_from_this, void SetLoggingCallback(lldb::LogOutputCallback log_callback, void *baton); + /// Copy this debugger's file-backed log files into the given directory, for + /// inclusion in a diagnostics bundle. Returns the names of the files that + /// were copied. Best-effort: files that cannot be copied are skipped. + std::vector CopyLogFilesToDirectory(const FileSpec &dir); + Status SetPropertyValue(const ExecutionContext *exe_ctx, VarSetOperationType op, llvm::StringRef property_path, llvm::StringRef value) override; @@ -816,7 +821,6 @@ class Debugger : public std::enable_shared_from_this, lldb::ListenerSP m_forward_listener_sp; llvm::once_flag m_clear_once; lldb::TargetSP m_dummy_target_sp; - Diagnostics::CallbackID m_diagnostics_callback_id; /// Bookkeeping for command line progress events. /// @{ diff --git a/lldb/include/lldb/Core/Diagnostics.h b/lldb/include/lldb/Core/Diagnostics.h new file mode 100644 index 0000000000000..177150f21d32c --- /dev/null +++ b/lldb/include/lldb/Core/Diagnostics.h @@ -0,0 +1,154 @@ +//===-- Diagnostics.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_CORE_DIAGNOSTICS_H +#define LLDB_CORE_DIAGNOSTICS_H + +#include "lldb/Utility/FileSpec.h" +#include "lldb/Utility/Log.h" +#include "llvm/Support/Error.h" + +#include +#include +#include +#include +#include + +namespace llvm { +namespace json { +class Value; +} // namespace json +} // namespace llvm + +namespace lldb_private { + +class Debugger; +class ExecutionContext; + +/// Diagnostics maintain an always-on, in-memory log of recent diagnostic +/// messages that can be written out to help investigate bugs and troubleshoot +/// issues. +class Diagnostics { +public: + Diagnostics(); + ~Diagnostics(); + + /// The bundle directory and the files written into it, recorded as each one + /// is created so a file that could not be written is simply absent. + struct Attachments { + std::string directory; + std::vector files; + }; + + /// The state a triager needs to make sense of a bug report. The full payload + /// is written into the bundle directory. These scalars are carried in the + /// terminal output and the report body rather than as redundant files. More + /// fields are expected to accrue over time. + struct Report { + std::string version; + std::string os; + std::string invocation; + Attachments attachments; + }; + + /// Write the in-memory diagnostic log into the given directory. + llvm::Error Create(const FileSpec &dir); + + /// Collect a full diagnostics bundle into \p dir and return its report. + /// + /// Writes the always-on log, the debugger's file-backed logs, statistics, + /// and a snapshot of the commands a triager runs first. Collection is + /// best-effort: a failure to produce one artifact never aborts the rest, so + /// a partial bundle is always better than none. + llvm::Expected Collect(Debugger &debugger, + const ExecutionContext &exe_ctx, + const FileSpec &dir); + + /// Write the diagnostic log into a directory and print a message to the given + /// output stream. + /// @{ + bool Dump(llvm::raw_ostream &stream); + bool Dump(llvm::raw_ostream &stream, const FileSpec &dir); + /// @} + + /// Record a diagnostic message into the always-on, in-memory log. + void Record(llvm::StringRef message); + + /// Supplies an artifact's contents on demand. Subsystems register a provider + /// so Core need not depend on them. Each runs when a bundle is collected. + using ArtifactProvider = std::function; + using ArtifactProviderID = uint64_t; + + /// Register \p provider to contribute file \p name. Returns an id for + /// RemoveArtifactProvider. Thread-safe. + ArtifactProviderID AddArtifactProvider(std::string name, + ArtifactProvider provider); + + /// Unregister a provider. Thread-safe. + void RemoveArtifactProvider(ArtifactProviderID id); + + static Diagnostics &Instance(); + + static bool Enabled(); + static void Initialize(); + static void Terminate(); + + /// Create a unique diagnostic directory. + static llvm::Expected CreateUniqueDirectory(); + +private: + static std::optional &InstanceImpl(); + + llvm::Error DumpDiangosticsLog(const FileSpec &dir) const; + + /// Collect the individual parts of the bundle into \p dir, appending the name + /// of each file to \p files as it is written. + /// @{ + void CollectLogs(Debugger &debugger, const FileSpec &dir, + std::vector &files); + static void CollectStatistics(Debugger &debugger, + const ExecutionContext &exe_ctx, + const FileSpec &dir, + std::vector &files); + static void CollectCommands(Debugger &debugger, + const ExecutionContext &exe_ctx, + const FileSpec &dir, + std::vector &files); + void CollectArtifactProviders(const FileSpec &dir, + std::vector &files); + /// @} + + /// Scalars carried in the report rather than written as files. + /// @{ + static std::string GetHostDescription(const ExecutionContext &exe_ctx); + static std::string GetInvocation(); + /// @} + + RotatingLogHandler m_log_handler; + + struct ArtifactProviderEntry { + ArtifactProviderID id; + std::string name; + ArtifactProvider provider; + }; + + /// Registered artifact providers, guarded by the mutex. + /// @{ + ArtifactProviderID m_next_artifact_provider_id = 0; + std::vector m_artifact_providers; + std::mutex m_artifact_providers_mutex; + /// @} +}; + +/// Render a diagnostics report as JSON, for `diagnostics dump`'s terminal +/// output. +llvm::json::Value toJSON(const Diagnostics::Report &report); + +} // namespace lldb_private + +#endif diff --git a/lldb/include/lldb/Core/PluginManager.h b/lldb/include/lldb/Core/PluginManager.h index 3e1b723cd9902..b084d12df1cc0 100644 --- a/lldb/include/lldb/Core/PluginManager.h +++ b/lldb/include/lldb/Core/PluginManager.h @@ -136,6 +136,15 @@ class PluginManager { static std::unique_ptr CreateArchitectureInstance(const ArchSpec &arch); + // BugReporter + static void RegisterPlugin(llvm::StringRef name, llvm::StringRef description, + BugReporterCreateInstance create_callback); + + static void UnregisterPlugin(BugReporterCreateInstance create_callback); + + static std::unique_ptr + CreateBugReporterInstance(llvm::StringRef name = {}); + // Disassembler static bool RegisterPlugin(llvm::StringRef name, llvm::StringRef description, DisassemblerCreateInstance create_callback); @@ -719,6 +728,9 @@ class PluginManager { static std::vector GetArchitecturePluginInfo(); static bool SetArchitecturePluginEnabled(llvm::StringRef name, bool enable); + static std::vector GetBugReporterPluginInfo(); + static bool SetBugReporterPluginEnabled(llvm::StringRef name, bool enable); + static std::vector GetDisassemblerPluginInfo(); static bool SetDisassemblerPluginEnabled(llvm::StringRef name, bool enable); diff --git a/lldb/include/lldb/Host/Host.h b/lldb/include/lldb/Host/Host.h index 4538fd206251b..632614c17d173 100644 --- a/lldb/include/lldb/Host/Host.h +++ b/lldb/include/lldb/Host/Host.h @@ -311,8 +311,15 @@ class Host { const FileSpec &file_spec, uint32_t line_no); + /// Open a URL with the host's default handler (Launch Services on macOS, + /// xdg-open on other Unix). Returns an error if opening fails or the platform + /// has no implementation (e.g. Windows). static llvm::Error OpenURL(llvm::StringRef url); + /// Percent-encode a string for use in a URL query component, per RFC 3986 + /// (alphanumerics and "-_.~" are kept literal; everything else becomes %HH). + static std::string URLEncode(llvm::StringRef str); + /// Check if we're running in an interactive graphical session. /// /// \return diff --git a/lldb/include/lldb/Utility/Diagnostics.h b/lldb/include/lldb/Utility/Diagnostics.h deleted file mode 100644 index c2e4b44350a37..0000000000000 --- a/lldb/include/lldb/Utility/Diagnostics.h +++ /dev/null @@ -1,86 +0,0 @@ -//===-- Diagnostics.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_UTILITY_DIAGNOSTICS_H -#define LLDB_UTILITY_DIAGNOSTICS_H - -#include "lldb/Utility/FileSpec.h" -#include "lldb/Utility/Log.h" -#include "llvm/ADT/SmallVector.h" -#include "llvm/ADT/StringSet.h" -#include "llvm/Support/Error.h" - -#include -#include -#include -#include - -namespace lldb_private { - -/// Diagnostics are a collection of files to help investigate bugs and -/// troubleshoot issues. Any part of the debugger can register itself with the -/// help of a callback to emit one or more files into the diagnostic directory. -class Diagnostics { -public: - Diagnostics(); - ~Diagnostics(); - - /// Gather diagnostics in the given directory. - llvm::Error Create(const FileSpec &dir); - - /// Gather diagnostics and print a message to the given output stream. - /// @{ - bool Dump(llvm::raw_ostream &stream); - bool Dump(llvm::raw_ostream &stream, const FileSpec &dir); - /// @} - - void Report(llvm::StringRef message); - - using Callback = std::function; - using CallbackID = uint64_t; - - CallbackID AddCallback(Callback callback); - void RemoveCallback(CallbackID id); - - static Diagnostics &Instance(); - - static bool Enabled(); - static void Initialize(); - static void Terminate(); - - /// Create a unique diagnostic directory. - static llvm::Expected CreateUniqueDirectory(); - -private: - static std::optional &InstanceImpl(); - - llvm::Error DumpDiangosticsLog(const FileSpec &dir) const; - - RotatingLogHandler m_log_handler; - - struct CallbackEntry { - CallbackEntry(CallbackID id, Callback callback) - : id(id), callback(std::move(callback)) {} - CallbackID id; - Callback callback; - }; - - /// Monotonically increasing callback identifier. Unique per Diagnostic - /// instance. - CallbackID m_callback_id; - - /// List of callback entries. - llvm::SmallVector m_callbacks; - - /// Mutex to protect callback list and callback identifier. - std::mutex m_callbacks_mutex; -}; - -} // namespace lldb_private - -#endif diff --git a/lldb/include/lldb/lldb-forward.h b/lldb/include/lldb/lldb-forward.h index ccfe5efa19e1d..05e1a69168d8a 100644 --- a/lldb/include/lldb/lldb-forward.h +++ b/lldb/include/lldb/lldb-forward.h @@ -44,6 +44,7 @@ class BreakpointSite; class BroadcastEventSpec; class Broadcaster; class BroadcasterManager; +class BugReporter; class CXXSyntheticChildren; struct CacheSignature; class CallFrameInfo; diff --git a/lldb/include/lldb/lldb-private-interfaces.h b/lldb/include/lldb/lldb-private-interfaces.h index a958fbe2f986a..5ef54c20ce95d 100644 --- a/lldb/include/lldb/lldb-private-interfaces.h +++ b/lldb/include/lldb/lldb-private-interfaces.h @@ -30,6 +30,7 @@ typedef lldb::ABISP (*ABICreateInstance)(lldb::ProcessSP process_sp, const ArchSpec &arch); typedef std::unique_ptr (*ArchitectureCreateInstance)( const ArchSpec &arch); +typedef std::unique_ptr (*BugReporterCreateInstance)(); typedef lldb::DisassemblerSP (*DisassemblerCreateInstance)( const ArchSpec &arch, const char *flavor, const char *cpu, const char *features); diff --git a/lldb/source/API/SBDebugger.cpp b/lldb/source/API/SBDebugger.cpp index 6ad4507250bf5..660c0215f3406 100644 --- a/lldb/source/API/SBDebugger.cpp +++ b/lldb/source/API/SBDebugger.cpp @@ -37,6 +37,7 @@ #include "lldb/Core/Debugger.h" #include "lldb/Core/DebuggerEvents.h" +#include "lldb/Core/Diagnostics.h" #include "lldb/Core/PluginManager.h" #include "lldb/Core/Progress.h" #include "lldb/Core/StructuredDataImpl.h" @@ -51,7 +52,6 @@ #include "lldb/Target/Process.h" #include "lldb/Target/TargetList.h" #include "lldb/Utility/Args.h" -#include "lldb/Utility/Diagnostics.h" #include "lldb/Utility/State.h" #include "lldb/Version/Version.h" diff --git a/lldb/source/Commands/CommandObjectDiagnostics.cpp b/lldb/source/Commands/CommandObjectDiagnostics.cpp index b565e16e76b53..a23670519428a 100644 --- a/lldb/source/Commands/CommandObjectDiagnostics.cpp +++ b/lldb/source/Commands/CommandObjectDiagnostics.cpp @@ -7,6 +7,9 @@ //===----------------------------------------------------------------------===// #include "CommandObjectDiagnostics.h" +#include "lldb/Core/BugReporter.h" +#include "lldb/Core/Diagnostics.h" +#include "lldb/Core/PluginManager.h" #include "lldb/Host/OptionParser.h" #include "lldb/Interpreter/CommandOptionArgumentTable.h" #include "lldb/Interpreter/CommandReturnObject.h" @@ -14,7 +17,8 @@ #include "lldb/Interpreter/OptionValueEnumeration.h" #include "lldb/Interpreter/OptionValueUInt64.h" #include "lldb/Interpreter/Options.h" -#include "lldb/Utility/Diagnostics.h" + +#include "llvm/Support/JSON.h" using namespace lldb; using namespace lldb_private; @@ -22,6 +26,9 @@ using namespace lldb_private; #define LLDB_OPTIONS_diagnostics_dump #include "CommandOptions.inc" +#define LLDB_OPTIONS_diagnostics_report +#include "CommandOptions.inc" + class CommandObjectDiagnosticsDump : public CommandObjectParsed { public: // Constructors and Destructors @@ -85,16 +92,139 @@ class CommandObjectDiagnosticsDump : public CommandObjectParsed { return; } - llvm::Error error = Diagnostics::Instance().Create(*directory); - if (error) { + // Collect the diagnostics bundle into the directory. + llvm::Expected report = + Diagnostics::Instance().Collect(GetDebugger(), m_exe_ctx, *directory); + if (!report) { result.AppendErrorWithFormat("failed to write diagnostics to %s", directory->GetPath().c_str()); - result.AppendError(llvm::toString(std::move(error))); + result.AppendError(llvm::toString(report.takeError())); return; } - result.GetOutputStream() << "diagnostics written to " << *directory << '\n'; + // Print the report as JSON so the user can review what a bug report would + // carry. The bundle directory and its files are listed under "attachments". + result.GetOutputStream().Format("{0:2}\n", toJSON(*report)); + + result.SetStatus(eReturnStatusSuccessFinishResult); + } + + CommandOptions m_options; +}; + +class CommandObjectDiagnosticsReport : public CommandObjectParsed { +public: + CommandObjectDiagnosticsReport(CommandInterpreter &interpreter) + : CommandObjectParsed( + interpreter, "diagnostics report", + "Assemble a diagnostics bundle and file it as a bug report.", + nullptr) {} + + ~CommandObjectDiagnosticsReport() override = default; + + 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 'd': + directory.SetDirectory(option_arg); + break; + case 'n': + no_open = true; + break; + case 'P': + plugin = option_arg.str(); + break; + default: + llvm_unreachable("Unimplemented option"); + } + return error; + } + + void OptionParsingStarting(ExecutionContext *execution_context) override { + directory.Clear(); + no_open = false; + plugin.clear(); + } + + llvm::ArrayRef GetDefinitions() override { + return llvm::ArrayRef(g_diagnostics_report_options); + } + + FileSpec directory; + bool no_open = false; + std::string plugin; + }; + + Options *GetOptions() override { return &m_options; } + +protected: + llvm::Expected GetDirectory() { + if (m_options.directory) { + auto ec = + llvm::sys::fs::create_directories(m_options.directory.GetPath()); + if (ec) + return llvm::errorCodeToError(ec); + return m_options.directory; + } + return Diagnostics::CreateUniqueDirectory(); + } + + void DoExecute(Args &args, CommandReturnObject &result) override { + llvm::Expected directory = GetDirectory(); + if (!directory) { + result.AppendError(llvm::toString(directory.takeError())); + return; + } + + llvm::Expected report = + Diagnostics::Instance().Collect(GetDebugger(), m_exe_ctx, *directory); + if (!report) { + result.AppendError(llvm::toString(report.takeError())); + return; + } + + Stream &out = result.GetOutputStream(); + out << "Bug report written to " << directory->GetPath() << "\n"; + if (!report->attachments.files.empty()) { + out << "Attach the following files to the issue:\n"; + for (const std::string &file : report->attachments.files) + out << " [ ] " << file << "\n"; + } + result.AppendWarning("the report may contain file paths, command history " + "and program data. Review it before attaching it to a " + "public issue"); + + if (m_options.no_open) { + result.SetStatus(eReturnStatusSuccessFinishResult); + return; + } + + // No-tracker handling lives in the fallback reporter's File(), not here. + std::unique_ptr reporter = + PluginManager::CreateBugReporterInstance(m_options.plugin); + if (!reporter) { + if (!m_options.plugin.empty()) + result.AppendErrorWithFormat("no bug reporter named '%s'", + m_options.plugin.c_str()); + else + result.AppendError("no bug reporter is available"); + return; + } + if (llvm::Error error = reporter->File(*report)) { + result.AppendError(llvm::toString(std::move(error))); + return; + } + out << "Opened a pre-filled " << reporter->GetPluginName() << " report.\n"; result.SetStatus(eReturnStatusSuccessFinishResult); } @@ -108,6 +238,8 @@ CommandObjectDiagnostics::CommandObjectDiagnostics( "diagnostics []") { LoadSubCommand( "dump", CommandObjectSP(new CommandObjectDiagnosticsDump(interpreter))); + LoadSubCommand("report", CommandObjectSP(new CommandObjectDiagnosticsReport( + interpreter))); } CommandObjectDiagnostics::~CommandObjectDiagnostics() = default; diff --git a/lldb/source/Commands/Options.td b/lldb/source/Commands/Options.td index 81ba27db8a090..3361e7042497c 100644 --- a/lldb/source/Commands/Options.td +++ b/lldb/source/Commands/Options.td @@ -508,6 +508,21 @@ let Command = "diagnostics dump" in { Arg<"Path">, Desc<"Dump the diagnostics to the given directory.">; } +let Command = "diagnostics report" in { + def diagnostics_report_directory + : Option<"directory", "d">, + Group<1>, + Arg<"Path">, + Desc<"Write the bug report bundle to the given directory.">; + def diagnostics_report_no_open + : Option<"no-open", "n">, + Desc<"Write the bundle but do not open the bug tracker.">; + def diagnostics_report_plugin + : Option<"plugin", "P">, + Arg<"Plugin">, + Desc<"File the report with the named bug reporter plugin.">; +} + let Command = "expression" in { def expression_options_all_threads : Option<"all-threads", "a">, Groups<[1,2]>, Arg<"Boolean">, Desc<"Should we run all threads if the " diff --git a/lldb/source/Core/CMakeLists.txt b/lldb/source/Core/CMakeLists.txt index 053bd1598883b..af8af41b8d5f5 100644 --- a/lldb/source/Core/CMakeLists.txt +++ b/lldb/source/Core/CMakeLists.txt @@ -28,6 +28,7 @@ add_lldb_library(lldbCore NO_PLUGIN_DEPENDENCIES DebuggerEvents.cpp Declaration.cpp DemangledNameInfo.cpp + Diagnostics.cpp Disassembler.cpp DumpDataExtractor.cpp DumpRegisterValue.cpp diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp index cc7c746ad532f..6eab8fa8118d5 100644 --- a/lldb/source/Core/Debugger.cpp +++ b/lldb/source/Core/Debugger.cpp @@ -1126,22 +1126,6 @@ Debugger::Debugger(lldb::LogOutputCallback log_callback, void *baton) if (!GetOutputFileSP()->GetIsTerminalWithColors()) disable_color(); - if (Diagnostics::Enabled()) { - m_diagnostics_callback_id = Diagnostics::Instance().AddCallback( - [this](const FileSpec &dir) -> llvm::Error { - for (auto &entry : m_stream_handlers) { - llvm::StringRef log_path = entry.first(); - llvm::StringRef file_name = llvm::sys::path::filename(log_path); - FileSpec destination = dir.CopyByAppendingPathComponent(file_name); - std::error_code ec = - llvm::sys::fs::copy_file(log_path, destination.GetPath()); - if (ec) - return llvm::errorCodeToError(ec); - } - return llvm::Error::success(); - }); - } - #if defined(_WIN32) && defined(ENABLE_VIRTUAL_TERMINAL_PROCESSING) // Enabling use of ANSI color codes because LLDB is using them to highlight // text. @@ -1187,9 +1171,6 @@ void Debugger::Clear() { GetInputFile().Close(); m_command_interpreter_up->Clear(); - - if (Diagnostics::Enabled()) - Diagnostics::Instance().RemoveCallback(m_diagnostics_callback_id); }); } @@ -1741,6 +1722,20 @@ void Debugger::SetLoggingCallback(lldb::LogOutputCallback log_callback, std::make_shared(log_callback, baton); } +std::vector +Debugger::CopyLogFilesToDirectory(const FileSpec &dir) { + std::vector copied; + for (auto &entry : m_stream_handlers) { + llvm::StringRef log_path = entry.first(); + llvm::StringRef file_name = llvm::sys::path::filename(log_path); + FileSpec destination = dir.CopyByAppendingPathComponent(file_name); + // Best-effort: skip logs that can't be copied rather than aborting. + if (!llvm::sys::fs::copy_file(log_path, destination.GetPath())) + copied.push_back(file_name.str()); + } + return copied; +} + void Debugger::SetDestroyCallback( lldb_private::DebuggerDestroyCallback destroy_callback, void *baton) { std::lock_guard guard(m_destroy_callback_mutex); @@ -1855,7 +1850,7 @@ void Debugger::ReportDiagnosticImpl(Severity severity, std::string message, // The diagnostic subsystem is optional but we still want to broadcast // events when it's disabled. if (Diagnostics::Enabled()) - Diagnostics::Instance().Report(message); + Diagnostics::Instance().Record(message); // We don't broadcast info events. if (severity == lldb::eSeverityInfo) diff --git a/lldb/source/Core/Diagnostics.cpp b/lldb/source/Core/Diagnostics.cpp new file mode 100644 index 0000000000000..1d02f76ef5497 --- /dev/null +++ b/lldb/source/Core/Diagnostics.cpp @@ -0,0 +1,321 @@ +//===-- Diagnostics.cpp ---------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#include "lldb/Core/Diagnostics.h" +#include "lldb/Core/Debugger.h" +#include "lldb/Host/Host.h" +#include "lldb/Host/HostInfo.h" +#include "lldb/Interpreter/CommandInterpreter.h" +#include "lldb/Interpreter/CommandReturnObject.h" +#include "lldb/Target/ExecutionContext.h" +#include "lldb/Target/Platform.h" +#include "lldb/Target/Statistics.h" +#include "lldb/Target/Target.h" +#include "lldb/Utility/Args.h" +#include "lldb/Utility/LLDBAssert.h" +#include "lldb/Utility/ProcessInfo.h" +#include "lldb/Version/Version.h" + +#include "llvm/ADT/STLExtras.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include + +using namespace lldb_private; +using namespace lldb; +using namespace llvm; + +static constexpr size_t g_num_log_messages = 100; + +void Diagnostics::Initialize() { + lldbassert(!InstanceImpl() && "Already initialized."); + InstanceImpl().emplace(); +} + +void Diagnostics::Terminate() { + lldbassert(InstanceImpl() && "Already terminated."); + InstanceImpl().reset(); +} + +bool Diagnostics::Enabled() { return InstanceImpl().operator bool(); } + +std::optional &Diagnostics::InstanceImpl() { + static std::optional g_diagnostics; + return g_diagnostics; +} + +Diagnostics &Diagnostics::Instance() { return *InstanceImpl(); } + +Diagnostics::Diagnostics() : m_log_handler(g_num_log_messages) {} + +Diagnostics::~Diagnostics() {} + +bool Diagnostics::Dump(raw_ostream &stream) { + Expected diagnostics_dir = CreateUniqueDirectory(); + if (!diagnostics_dir) { + stream << "unable to create diagnostic dir: " + << toString(diagnostics_dir.takeError()) << '\n'; + return false; + } + + return Dump(stream, *diagnostics_dir); +} + +bool Diagnostics::Dump(raw_ostream &stream, const FileSpec &dir) { + stream << "LLDB diagnostics will be written to " << dir.GetPath() << "\n"; + stream << "Please include the directory content when filing a bug report\n"; + + if (Error error = Create(dir)) { + stream << toString(std::move(error)) << '\n'; + return false; + } + + return true; +} + +llvm::Expected Diagnostics::CreateUniqueDirectory() { + SmallString<128> diagnostics_dir; + std::error_code ec = + sys::fs::createUniqueDirectory("diagnostics", diagnostics_dir); + if (ec) + return errorCodeToError(ec); + return FileSpec(diagnostics_dir.str()); +} + +Error Diagnostics::Create(const FileSpec &dir) { + if (Error err = DumpDiangosticsLog(dir)) + return err; + + return Error::success(); +} + +llvm::Error Diagnostics::DumpDiangosticsLog(const FileSpec &dir) const { + FileSpec log_file = dir.CopyByAppendingPathComponent("diagnostics.log"); + std::error_code ec; + llvm::raw_fd_ostream stream(log_file.GetPath(), ec, llvm::sys::fs::OF_None); + if (ec) + return errorCodeToError(ec); + m_log_handler.Dump(stream); + return Error::success(); +} + +void Diagnostics::Record(llvm::StringRef message) { + m_log_handler.Emit(message); +} + +Diagnostics::ArtifactProviderID +Diagnostics::AddArtifactProvider(std::string name, ArtifactProvider provider) { + std::lock_guard guard(m_artifact_providers_mutex); + ArtifactProviderID id = m_next_artifact_provider_id++; + m_artifact_providers.push_back({id, std::move(name), std::move(provider)}); + return id; +} + +void Diagnostics::RemoveArtifactProvider(ArtifactProviderID id) { + std::lock_guard guard(m_artifact_providers_mutex); + llvm::erase_if(m_artifact_providers, + [id](const ArtifactProviderEntry &e) { return e.id == id; }); +} + +// Write a single artifact into the bundle and, on success, record its name in +// \p files. Best-effort: a write failure leaves the file out of the list, so a +// missing artifact stays visible. The file is made owner-only because the +// bundle can contain paths, argv, and command history. +static void WriteArtifact(const FileSpec &dir, llvm::StringRef name, + llvm::StringRef content, + std::vector &files) { + FileSpec file = dir.CopyByAppendingPathComponent(name); + std::error_code ec; + llvm::raw_fd_ostream os(file.GetPath(), ec, llvm::sys::fs::OF_Text); + if (ec) + return; + os << content; + os.flush(); + llvm::sys::fs::setPermissions(file.GetPath(), llvm::sys::fs::owner_read | + llvm::sys::fs::owner_write); + files.push_back(name.str()); +} + +// Run a command through the interpreter and return its combined output and +// error text, for inclusion as a snapshot in the bundle. +static std::string CaptureCommand(Debugger &debugger, llvm::StringRef command) { + CommandReturnObject result(/*colors=*/false); + debugger.GetCommandInterpreter().HandleCommand(command.str().c_str(), + eLazyBoolNo, result); + return (result.GetOutputString() + result.GetErrorString()).str(); +} + +namespace { +/// The execution context a triage command needs before it is worth running. +enum class Requires { Always, Target, Process, Frame }; + +struct TriageCommand { + llvm::StringRef command; + Requires requirement; +}; +} // namespace + +// The commands a triager runs first, captured into the bundle. Add a row to +// extend the snapshot. The requirement keeps a command from emitting a spurious +// "no process" error when its context is absent. +static constexpr TriageCommand g_triage_commands[] = { + {"target list", Requires::Always}, + {"image list", Requires::Target}, + {"process status", Requires::Process}, + {"thread list", Requires::Process}, + {"thread backtrace all", Requires::Process}, + {"image lookup -va $pc", Requires::Frame}, + {"register read", Requires::Frame}, + {"frame variable", Requires::Frame}, +}; + +static bool Available(Requires requirement, const ExecutionContext &exe_ctx) { + switch (requirement) { + case Requires::Always: + return true; + case Requires::Target: + return exe_ctx.GetTargetPtr() != nullptr; + case Requires::Process: + return exe_ctx.GetProcessPtr() != nullptr; + case Requires::Frame: + return exe_ctx.GetFramePtr() != nullptr; + } + llvm_unreachable("unhandled Requires"); +} + +llvm::Expected +Diagnostics::Collect(Debugger &debugger, const ExecutionContext &exe_ctx, + const FileSpec &dir) { + // The bundle holds potentially sensitive data (paths, argv, command history), + // so restrict the directory to the owner before writing anything into it. + llvm::sys::fs::setPermissions(dir.GetPath(), llvm::sys::fs::owner_read | + llvm::sys::fs::owner_write | + llvm::sys::fs::owner_exe); + + Report report; + report.attachments.directory = dir.GetPath(); + CollectLogs(debugger, dir, report.attachments.files); + CollectStatistics(debugger, exe_ctx, dir, report.attachments.files); + CollectCommands(debugger, exe_ctx, dir, report.attachments.files); + CollectArtifactProviders(dir, report.attachments.files); + + report.version = lldb_private::GetVersion(); + report.os = GetHostDescription(exe_ctx); + report.invocation = GetInvocation(); + return report; +} + +void Diagnostics::CollectLogs(Debugger &debugger, const FileSpec &dir, + std::vector &files) { + // The always-on diagnostic log. + if (Error error = Create(dir)) + consumeError(std::move(error)); + else + files.push_back("diagnostics.log"); + + // This debugger's file-backed logs. + for (std::string &name : debugger.CopyLogFilesToDirectory(dir)) + files.push_back(std::move(name)); +} + +void Diagnostics::CollectStatistics(Debugger &debugger, + const ExecutionContext &exe_ctx, + const FileSpec &dir, + std::vector &files) { + StatisticsOptions options; + json::Value stats = DebuggerStats::ReportStatistics( + debugger, exe_ctx.GetTargetPtr(), options); + std::string str; + raw_string_ostream os(str); + os << formatv("{0:2}", stats); + WriteArtifact(dir, "statistics.json", str, files); +} + +void Diagnostics::CollectCommands(Debugger &debugger, + const ExecutionContext &exe_ctx, + const FileSpec &dir, + std::vector &files) { + std::string snapshot; + for (const TriageCommand &tc : g_triage_commands) { + if (!Available(tc.requirement, exe_ctx)) + continue; + snapshot += formatv("=== {0} ===\n", tc.command).str(); + snapshot += CaptureCommand(debugger, tc.command); + snapshot += "\n\n"; + } + WriteArtifact(dir, "commands.txt", snapshot, files); +} + +void Diagnostics::CollectArtifactProviders(const FileSpec &dir, + std::vector &files) { + // Snapshot under the lock, then run providers without it: a provider can be + // slow and must not block registration or removal. + std::vector providers; + { + std::lock_guard guard(m_artifact_providers_mutex); + providers = m_artifact_providers; + } + for (const ArtifactProviderEntry &entry : providers) + WriteArtifact(dir, entry.name, entry.provider(), files); +} + +std::string Diagnostics::GetHostDescription(const ExecutionContext &exe_ctx) { + std::string os = HostInfo::GetTargetTriple().str(); + Target *target = exe_ctx.GetTargetPtr(); + if (!target) + return os; + PlatformSP platform_sp = target->GetPlatform(); + if (!platform_sp) + return os; + + os += formatv(" platform={0}", platform_sp->GetName()).str(); + VersionTuple version = platform_sp->GetOSVersion(); + if (!version.empty()) + os += " os=" + version.getAsString(); + if (std::optional build = platform_sp->GetOSBuildString()) + os += " build=" + *build; + return os; +} + +std::string Diagnostics::GetInvocation() { + // libLLDB does not store its own argv, so read the invocation from the host + // process. + ProcessInstanceInfo info; + if (!Host::GetProcessInfo(Host::GetCurrentProcessID(), info)) + return {}; + + const Args &args = info.GetArguments(); + std::string invocation; + for (size_t i = 0; i < args.GetArgumentCount(); ++i) { + if (i) + invocation += ' '; + invocation += args.GetArgumentAtIndex(i); + } + return invocation; +} + +llvm::json::Value lldb_private::toJSON(const Diagnostics::Report &report) { + json::Object obj{ + {"version", report.version}, + {"os", report.os}, + }; + if (!report.invocation.empty()) + obj["invocation"] = report.invocation; + obj["attachments"] = json::Object{ + {"directory", report.attachments.directory}, + {"files", json::Array(report.attachments.files)}, + }; + return obj; +} diff --git a/lldb/source/Core/PluginManager.cpp b/lldb/source/Core/PluginManager.cpp index 47c7f4b422c41..f19e70e364292 100644 --- a/lldb/source/Core/PluginManager.cpp +++ b/lldb/source/Core/PluginManager.cpp @@ -8,6 +8,7 @@ #include "lldb/Core/PluginManager.h" +#include "lldb/Core/BugReporter.h" #include "lldb/Core/Debugger.h" #include "lldb/Host/FileSystem.h" #include "lldb/Host/HostInfo.h" @@ -196,6 +197,12 @@ llvm::ArrayRef PluginManager::GetPluginNamespaces() { PluginManager::SetArchitecturePluginEnabled, }, + { + "bug-reporter", + PluginManager::GetBugReporterPluginInfo, + PluginManager::SetBugReporterPluginEnabled, + }, + { "disassembler", PluginManager::GetDisassemblerPluginInfo, @@ -597,6 +604,42 @@ PluginManager::CreateArchitectureInstance(const ArchSpec &arch) { return nullptr; } +#pragma mark BugReporter + +typedef PluginInstance BugReporterInstance; +typedef PluginInstances BugReporterInstances; + +static BugReporterInstances &GetBugReporterInstances() { + static BugReporterInstances g_instances; + return g_instances; +} + +void PluginManager::RegisterPlugin(llvm::StringRef name, + llvm::StringRef description, + BugReporterCreateInstance create_callback) { + GetBugReporterInstances().RegisterPlugin(name, description, create_callback); +} + +void PluginManager::UnregisterPlugin( + BugReporterCreateInstance create_callback) { + GetBugReporterInstances().UnregisterPlugin(create_callback); +} + +std::unique_ptr +PluginManager::CreateBugReporterInstance(llvm::StringRef name) { + if (!name.empty()) { + if (auto create_callback = + GetBugReporterInstances().GetCallbackForName(name)) + return create_callback(); + return nullptr; + } + for (const auto &instance : GetBugReporterInstances().GetSnapshot()) { + if (auto plugin_up = instance.create_callback()) + return plugin_up; + } + return nullptr; +} + #pragma mark Disassembler typedef PluginInstance DisassemblerInstance; @@ -2318,6 +2361,14 @@ bool PluginManager::SetArchitecturePluginEnabled(llvm::StringRef name, return GetArchitectureInstances().SetInstanceEnabled(name, enable); } +std::vector PluginManager::GetBugReporterPluginInfo() { + return GetBugReporterInstances().GetPluginInfoForAllInstances(); +} +bool PluginManager::SetBugReporterPluginEnabled(llvm::StringRef name, + bool enable) { + return GetBugReporterInstances().SetInstanceEnabled(name, enable); +} + std::vector PluginManager::GetDisassemblerPluginInfo() { return GetDisassemblerInstances().GetPluginInfoForAllInstances(); } diff --git a/lldb/source/Host/common/Host.cpp b/lldb/source/Host/common/Host.cpp index 74e85ad4e0b1d..7c8288f28d0bd 100644 --- a/lldb/source/Host/common/Host.cpp +++ b/lldb/source/Host/common/Host.cpp @@ -7,6 +7,7 @@ //===----------------------------------------------------------------------===// // C includes +#include #include #include #include @@ -49,6 +50,7 @@ #include "lldb/Host/ProcessLauncher.h" #include "lldb/Host/ThreadLauncher.h" #include "lldb/Host/posix/ConnectionFileDescriptorPosix.h" +#include "lldb/Utility/Args.h" #include "lldb/Utility/FileSpec.h" #include "lldb/Utility/LLDBLog.h" #include "lldb/Utility/Log.h" @@ -56,9 +58,11 @@ #include "lldb/Utility/Status.h" #include "lldb/lldb-private-forward.h" #include "llvm/ADT/SmallString.h" +#include "llvm/ADT/StringExtras.h" #include "llvm/Config/llvm-config.h" // for LLVM_ON_UNIX #include "llvm/Support/Errno.h" #include "llvm/Support/FileSystem.h" +#include "llvm/Support/Program.h" #if defined(_WIN32) #include "lldb/Host/windows/ConnectionGenericFileWindows.h" @@ -626,8 +630,60 @@ llvm::Error Host::OpenFileInExternalEditor(llvm::StringRef editor, } bool Host::IsInteractiveGraphicSession() { return false; } + +llvm::Error Host::OpenURL(llvm::StringRef url) { + if (url.empty()) + return llvm::createStringError("cannot open empty URL"); + + LLDB_LOG(GetLog(LLDBLog::Host), "Opening URL: {0}", url); + +#if defined(_WIN32) + // TODO: open the URL with ShellExecuteW (needs a shell32 link dependency). + return llvm::errorCodeToError( + std::error_code(ENOTSUP, std::system_category())); +#else + // Resolve xdg-open and run it directly (run_in_shell=false) so the URL is a + // literal argument the shell never parses; this keeps query-string + // metacharacters from being interpreted regardless of the user's shell. + llvm::ErrorOr xdg_open = + llvm::sys::findProgramByName("xdg-open"); + if (!xdg_open) + return llvm::createStringError("could not find xdg-open to open the URL"); + + Args args; + args.AppendArgument(*xdg_open); + args.AppendArgument(url); + + int status = 0; + int signo = 0; + std::string output; + Status error = RunShellCommand( + args, /*working_dir=*/FileSpec(), &status, &signo, &output, + /*separated_error_output=*/nullptr, std::chrono::seconds(10), + /*run_in_shell=*/false); + if (error.Fail()) + return error.takeError(); + if (status != 0) + return llvm::createStringError( + llvm::formatv("xdg-open exited with status {0}", status)); + return llvm::Error::success(); +#endif +} #endif +std::string Host::URLEncode(llvm::StringRef str) { + std::string out; + llvm::raw_string_ostream os(out); + for (unsigned char c : str) { + if (std::isalnum(c) || llvm::StringRef("-_.~").contains(c)) + os << c; + else + os << '%' << llvm::hexdigit((c >> 4) & 0xF, /*LowerCase=*/false) + << llvm::hexdigit(c & 0xF, /*LowerCase=*/false); + } + return out; +} + std::unique_ptr Host::CreateDefaultConnection(llvm::StringRef url) { #if defined(_WIN32) if (url.starts_with("file://")) diff --git a/lldb/source/Initialization/SystemInitializerCommon.cpp b/lldb/source/Initialization/SystemInitializerCommon.cpp index 1a172a95aa147..13c1d977731c4 100644 --- a/lldb/source/Initialization/SystemInitializerCommon.cpp +++ b/lldb/source/Initialization/SystemInitializerCommon.cpp @@ -9,11 +9,11 @@ #include "lldb/Initialization/SystemInitializerCommon.h" #include "Plugins/Process/gdb-remote/ProcessGDBRemoteLog.h" +#include "lldb/Core/Diagnostics.h" #include "lldb/Host/FileSystem.h" #include "lldb/Host/Host.h" #include "lldb/Host/Socket.h" #include "lldb/Target/Statistics.h" -#include "lldb/Utility/Diagnostics.h" #include "lldb/Utility/LLDBLog.h" #include "lldb/Utility/Timer.h" #include "lldb/Version/Version.h" diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp index af82757de46db..25af04081e99f 100644 --- a/lldb/source/Interpreter/CommandInterpreter.cpp +++ b/lldb/source/Interpreter/CommandInterpreter.cpp @@ -443,6 +443,10 @@ void CommandInterpreter::Initialize() { if (cmd_obj_sp) AddAlias("image", cmd_obj_sp); + cmd_obj_sp = GetCommandSPExact("diagnostics report"); + if (cmd_obj_sp) + AddAlias("bugreport", cmd_obj_sp); + alias_arguments_vector_sp = std::make_shared(); cmd_obj_sp = GetCommandSPExact("dwim-print"); diff --git a/lldb/source/Plugins/BugReporter/CMakeLists.txt b/lldb/source/Plugins/BugReporter/CMakeLists.txt new file mode 100644 index 0000000000000..2e3133a03621e --- /dev/null +++ b/lldb/source/Plugins/BugReporter/CMakeLists.txt @@ -0,0 +1,12 @@ +set_property(DIRECTORY PROPERTY LLDB_PLUGIN_KIND BugReporter) + +option(LLDB_ENABLE_GITHUB_BUG_REPORTER + "Build the GitHub bug reporter so 'diagnostics report' can file a GitHub issue." + ON) + +# Reporters are tried in subdirectory order, so keep None (the fallback) last. +# Downstream adds its own reporter subdirectory ahead of these. +if(LLDB_ENABLE_GITHUB_BUG_REPORTER) + add_subdirectory(GitHub) +endif() +add_subdirectory(None) diff --git a/lldb/source/Plugins/BugReporter/GitHub/CMakeLists.txt b/lldb/source/Plugins/BugReporter/GitHub/CMakeLists.txt new file mode 100644 index 0000000000000..4964bc568e113 --- /dev/null +++ b/lldb/source/Plugins/BugReporter/GitHub/CMakeLists.txt @@ -0,0 +1,9 @@ +add_lldb_library(lldbPluginGitHubReporter PLUGIN + GitHubReporter.cpp + + LINK_COMPONENTS + Support + LINK_LIBS + lldbCore + lldbHost + ) diff --git a/lldb/source/Plugins/BugReporter/GitHub/GitHubReporter.cpp b/lldb/source/Plugins/BugReporter/GitHub/GitHubReporter.cpp new file mode 100644 index 0000000000000..edecd69cd8fd3 --- /dev/null +++ b/lldb/source/Plugins/BugReporter/GitHub/GitHubReporter.cpp @@ -0,0 +1,66 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#include "Plugins/BugReporter/GitHub/GitHubReporter.h" +#include "lldb/Core/PluginManager.h" +#include "lldb/Host/Host.h" + +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/raw_ostream.h" + +using namespace lldb_private; + +LLDB_PLUGIN_DEFINE(GitHubReporter) + +// A GitHub "new issue" URL is fetched with GET, so its length is bounded (the +// server rejects very long URLs). Keep the pre-filled body well under that so +// the rest of the URL always fits. The full payload lives in the bundle. +static constexpr size_t g_max_body_size = 6000; + +void GitHubReporter::Initialize() { + PluginManager::RegisterPlugin(GetPluginNameStatic(), + "File a bug as a GitHub issue.", + &GitHubReporter::CreateInstance); +} + +void GitHubReporter::Terminate() { + PluginManager::UnregisterPlugin(&GitHubReporter::CreateInstance); +} + +std::unique_ptr GitHubReporter::CreateInstance() { + return std::make_unique(); +} + +llvm::Error GitHubReporter::File(const Diagnostics::Report &report) { + std::string body; + llvm::raw_string_ostream os(body); + os << "### LLDB version\n" << report.version << "\n\n"; + os << "### Host\n" << report.os << "\n\n"; + if (!report.invocation.empty()) + os << "### Invocation\n`" << report.invocation << "`\n\n"; + os << "### Diagnostics\n" + << "Full diagnostics were written to:\n`" << report.attachments.directory + << "`\nPlease attach the contents of that directory to this issue.\n"; + + if (body.size() > g_max_body_size) { + // Back up off any UTF-8 continuation bytes so truncation lands on a + // character boundary rather than splitting a multi-byte sequence. + size_t cut = g_max_body_size; + while (cut > 0 && (static_cast(body[cut]) & 0xC0) == 0x80) + --cut; + body.resize(cut); + body += "\n\n...(truncated, see the attached diagnostics directory)"; + } + + std::string url = llvm::formatv("https://github.com/llvm/llvm-project/issues/" + "new?title={0}&body={1}&labels=lldb", + Host::URLEncode("[lldb] Bug report"), + Host::URLEncode(body)); + + return Host::OpenURL(url); +} diff --git a/lldb/source/Plugins/BugReporter/GitHub/GitHubReporter.h b/lldb/source/Plugins/BugReporter/GitHub/GitHubReporter.h new file mode 100644 index 0000000000000..af177cb50f76b --- /dev/null +++ b/lldb/source/Plugins/BugReporter/GitHub/GitHubReporter.h @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// 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_PLUGINS_BUGREPORTER_GITHUB_GITHUBREPORTER_H +#define LLDB_SOURCE_PLUGINS_BUGREPORTER_GITHUB_GITHUBREPORTER_H + +#include "lldb/Core/BugReporter.h" + +namespace lldb_private { + +/// Opens a pre-filled github.com/llvm/llvm-project "new issue" page. The body +/// carries a short summary and points at the on-disk bundle to attach, since +/// large artifacts cannot travel in the URL. +class GitHubReporter : public BugReporter { +public: + static void Initialize(); + static void Terminate(); + static llvm::StringRef GetPluginNameStatic() { return "github"; } + static std::unique_ptr CreateInstance(); + + llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); } + + llvm::Error File(const Diagnostics::Report &report) override; +}; + +} // namespace lldb_private + +#endif // LLDB_SOURCE_PLUGINS_BUGREPORTER_GITHUB_GITHUBREPORTER_H diff --git a/lldb/source/Plugins/BugReporter/None/BugReporterNone.cpp b/lldb/source/Plugins/BugReporter/None/BugReporterNone.cpp new file mode 100644 index 0000000000000..0fc87356fbb71 --- /dev/null +++ b/lldb/source/Plugins/BugReporter/None/BugReporterNone.cpp @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#include "Plugins/BugReporter/None/BugReporterNone.h" +#include "lldb/Core/PluginManager.h" +#include "lldb/Host/Config.h" + +using namespace lldb_private; + +LLDB_PLUGIN_DEFINE(BugReporterNone) + +void BugReporterNone::Initialize() { + PluginManager::RegisterPlugin( + GetPluginNameStatic(), "Fallback used when no bug tracker is configured.", + &BugReporterNone::CreateInstance); +} + +void BugReporterNone::Terminate() { + PluginManager::UnregisterPlugin(&BugReporterNone::CreateInstance); +} + +std::unique_ptr BugReporterNone::CreateInstance() { + return std::make_unique(); +} + +llvm::Error BugReporterNone::File(const Diagnostics::Report &) { + return llvm::createStringError("no bug tracker is configured: please file a " + "bug manually on " LLDB_BUG_REPORT_URL); +} diff --git a/lldb/source/Plugins/BugReporter/None/BugReporterNone.h b/lldb/source/Plugins/BugReporter/None/BugReporterNone.h new file mode 100644 index 0000000000000..ff905d3e4b5e8 --- /dev/null +++ b/lldb/source/Plugins/BugReporter/None/BugReporterNone.h @@ -0,0 +1,32 @@ +//===----------------------------------------------------------------------===// +// +// 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_PLUGINS_BUGREPORTER_NONE_BUGREPORTERNONE_H +#define LLDB_SOURCE_PLUGINS_BUGREPORTER_NONE_BUGREPORTERNONE_H + +#include "lldb/Core/BugReporter.h" + +namespace lldb_private { + +/// The fallback when no bug tracker is configured. Its File() returns an error +/// rather than succeeding, so the report command surfaces it to the user. +class BugReporterNone : public BugReporter { +public: + static void Initialize(); + static void Terminate(); + static llvm::StringRef GetPluginNameStatic() { return "none"; } + static std::unique_ptr CreateInstance(); + + llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); } + + llvm::Error File(const Diagnostics::Report &report) override; +}; + +} // namespace lldb_private + +#endif // LLDB_SOURCE_PLUGINS_BUGREPORTER_NONE_BUGREPORTERNONE_H diff --git a/lldb/source/Plugins/BugReporter/None/CMakeLists.txt b/lldb/source/Plugins/BugReporter/None/CMakeLists.txt new file mode 100644 index 0000000000000..38f904d4d83b7 --- /dev/null +++ b/lldb/source/Plugins/BugReporter/None/CMakeLists.txt @@ -0,0 +1,8 @@ +add_lldb_library(lldbPluginBugReporterNone PLUGIN + BugReporterNone.cpp + + LINK_COMPONENTS + Support + LINK_LIBS + lldbCore + ) diff --git a/lldb/source/Plugins/CMakeLists.txt b/lldb/source/Plugins/CMakeLists.txt index b6878b21ff71a..5a8d83e2b5c8e 100644 --- a/lldb/source/Plugins/CMakeLists.txt +++ b/lldb/source/Plugins/CMakeLists.txt @@ -1,5 +1,6 @@ add_subdirectory(ABI) add_subdirectory(Architecture) +add_subdirectory(BugReporter) add_subdirectory(Disassembler) add_subdirectory(DynamicLoader) add_subdirectory(ExpressionParser) diff --git a/lldb/source/Plugins/Language/Swift/LogChannelSwift.cpp b/lldb/source/Plugins/Language/Swift/LogChannelSwift.cpp index d070e9e25d2a7..20ebd1110434d 100644 --- a/lldb/source/Plugins/Language/Swift/LogChannelSwift.cpp +++ b/lldb/source/Plugins/Language/Swift/LogChannelSwift.cpp @@ -7,11 +7,11 @@ //===----------------------------------------------------------------------===// #include "LogChannelSwift.h" +#include "lldb/Core/Diagnostics.h" #include "lldb/Host/Host.h" -#include "lldb/Utility/Diagnostics.h" #include "lldb/Utility/Log.h" #include "lldb/Version/Version.h" -#include "llvm/Support/FileSystem.h" +#include "llvm/Support/raw_ostream.h" using namespace lldb_private; @@ -25,7 +25,7 @@ static Log::Channel g_channel(g_categories, SwiftLog::Health); static constexpr size_t g_health_log_size = 5000; static std::shared_ptr g_health_log_handler; -static std::optional g_diagnostics_callback_id; +static std::optional g_diagnostics_artifact_id; template <> Log::Channel &lldb_private::LogChannelFor() { return g_channel; @@ -34,7 +34,8 @@ template <> Log::Channel &lldb_private::LogChannelFor() { void LogChannelSwift::Initialize() { Log::Register("swift", g_channel); - g_health_log_handler = std::make_shared(g_health_log_size); + g_health_log_handler = + std::make_shared(g_health_log_size); auto system_log_handler_sp = std::make_shared(); auto log_handler_sp = std::make_shared(g_health_log_handler, system_log_handler_sp); @@ -50,25 +51,21 @@ void LogChannelSwift::Initialize() { lldb_private::GetVersion()); if (Diagnostics::Enabled()) { - g_diagnostics_callback_id = Diagnostics::Instance().AddCallback( - [](const FileSpec &dir) -> llvm::Error { - FileSpec log_file = - dir.CopyByAppendingPathComponent("swift-healthcheck.log"); - std::error_code ec; - llvm::raw_fd_ostream stream(log_file.GetPath(), ec); - if (ec) - return llvm::errorCodeToError(ec); + g_diagnostics_artifact_id = Diagnostics::Instance().AddArtifactProvider( + "swift-healthcheck.log", []() -> std::string { + std::string content; + llvm::raw_string_ostream stream(content); if (g_health_log_handler) g_health_log_handler->Dump(stream); - return llvm::Error::success(); + return content; }); } } void LogChannelSwift::Terminate() { - if (g_diagnostics_callback_id && Diagnostics::Enabled()) - Diagnostics::Instance().RemoveCallback(*g_diagnostics_callback_id); - g_diagnostics_callback_id.reset(); + if (g_diagnostics_artifact_id && Diagnostics::Enabled()) + Diagnostics::Instance().RemoveArtifactProvider(*g_diagnostics_artifact_id); + g_diagnostics_artifact_id.reset(); g_health_log_handler.reset(); Log::Unregister("swift"); } diff --git a/lldb/source/Utility/CMakeLists.txt b/lldb/source/Utility/CMakeLists.txt index 04f1692e53b35..f5459cc0e9d01 100644 --- a/lldb/source/Utility/CMakeLists.txt +++ b/lldb/source/Utility/CMakeLists.txt @@ -37,7 +37,6 @@ add_lldb_library(lldbUtility NO_INTERNAL_DEPENDENCIES DataBufferLLVM.cpp DataEncoder.cpp DataExtractor.cpp - Diagnostics.cpp Environment.cpp ErrorMessages.cpp Event.cpp diff --git a/lldb/source/Utility/Diagnostics.cpp b/lldb/source/Utility/Diagnostics.cpp deleted file mode 100644 index b2a08165dd6ca..0000000000000 --- a/lldb/source/Utility/Diagnostics.cpp +++ /dev/null @@ -1,115 +0,0 @@ -//===-- Diagnostics.cpp ---------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -#include "lldb/Utility/Diagnostics.h" -#include "lldb/Utility/LLDBAssert.h" - -#include "llvm/Support/Error.h" -#include "llvm/Support/FileSystem.h" -#include "llvm/Support/raw_ostream.h" -#include - -using namespace lldb_private; -using namespace lldb; -using namespace llvm; - -static constexpr size_t g_num_log_messages = 100; - -void Diagnostics::Initialize() { - lldbassert(!InstanceImpl() && "Already initialized."); - InstanceImpl().emplace(); -} - -void Diagnostics::Terminate() { - lldbassert(InstanceImpl() && "Already terminated."); - InstanceImpl().reset(); -} - -bool Diagnostics::Enabled() { return InstanceImpl().operator bool(); } - -std::optional &Diagnostics::InstanceImpl() { - static std::optional g_diagnostics; - return g_diagnostics; -} - -Diagnostics &Diagnostics::Instance() { return *InstanceImpl(); } - -Diagnostics::Diagnostics() : m_log_handler(g_num_log_messages) {} - -Diagnostics::~Diagnostics() {} - -Diagnostics::CallbackID Diagnostics::AddCallback(Callback callback) { - std::lock_guard guard(m_callbacks_mutex); - CallbackID id = m_callback_id++; - m_callbacks.emplace_back(id, callback); - return id; -} - -void Diagnostics::RemoveCallback(CallbackID id) { - std::lock_guard guard(m_callbacks_mutex); - llvm::erase_if(m_callbacks, - [id](const CallbackEntry &e) { return e.id == id; }); -} - -bool Diagnostics::Dump(raw_ostream &stream) { - Expected diagnostics_dir = CreateUniqueDirectory(); - if (!diagnostics_dir) { - stream << "unable to create diagnostic dir: " - << toString(diagnostics_dir.takeError()) << '\n'; - return false; - } - - return Dump(stream, *diagnostics_dir); -} - -bool Diagnostics::Dump(raw_ostream &stream, const FileSpec &dir) { - stream << "LLDB diagnostics will be written to " << dir.GetPath() << "\n"; - stream << "Please include the directory content when filing a bug report\n"; - - if (Error error = Create(dir)) { - stream << toString(std::move(error)) << '\n'; - return false; - } - - return true; -} - -llvm::Expected Diagnostics::CreateUniqueDirectory() { - SmallString<128> diagnostics_dir; - std::error_code ec = - sys::fs::createUniqueDirectory("diagnostics", diagnostics_dir); - if (ec) - return errorCodeToError(ec); - return FileSpec(diagnostics_dir.str()); -} - -Error Diagnostics::Create(const FileSpec &dir) { - if (Error err = DumpDiangosticsLog(dir)) - return err; - - for (CallbackEntry e : m_callbacks) { - if (Error err = e.callback(dir)) - return err; - } - - return Error::success(); -} - -llvm::Error Diagnostics::DumpDiangosticsLog(const FileSpec &dir) const { - FileSpec log_file = dir.CopyByAppendingPathComponent("diagnostics.log"); - std::error_code ec; - llvm::raw_fd_ostream stream(log_file.GetPath(), ec, llvm::sys::fs::OF_None); - if (ec) - return errorCodeToError(ec); - m_log_handler.Dump(stream); - return Error::success(); -} - -void Diagnostics::Report(llvm::StringRef message) { - m_log_handler.Emit(message); -} diff --git a/lldb/test/Shell/Diagnostics/TestDump.test b/lldb/test/Shell/Diagnostics/TestDump.test index 2adde6b86d35a..d13972de0cf29 100644 --- a/lldb/test/Shell/Diagnostics/TestDump.test +++ b/lldb/test/Shell/Diagnostics/TestDump.test @@ -1,15 +1,33 @@ -# Check that the diagnostics dump command uses the correct directory and -# creates one if needed. +# Check that 'diagnostics dump' collects a bundle and prints a JSON summary of +# what a bug report would carry. # Dump to an existing directory. # RUN: rm -rf %t.existing # RUN: mkdir -p %t.existing -# RUN: %lldb -o 'diagnostics dump -d %t.existing' -# RUN: file %t.existing | FileCheck %s +# RUN: %lldb --no-lldbinit -b -o 'diagnostics dump -d %t.existing' | FileCheck %s +# RUN: test -d %t.existing -# Dump to a non-existing directory. +# Dump to a non-existing directory; it is created. # RUN: rm -rf %t.nonexisting -# RUN: %lldb -o 'diagnostics dump -d %t.nonexisting' -# RUN: file %t.nonexisting | FileCheck %s +# RUN: %lldb --no-lldbinit -b -o 'diagnostics dump -d %t.nonexisting' +# RUN: test -d %t.nonexisting -# CHECK: directory +# The terminal output is a JSON summary. The bundle directory and the files +# written into it are nested under "attachments". +# CHECK-DAG: "version": +# CHECK-DAG: "os": +# CHECK-DAG: "attachments": +# CHECK-DAG: "directory": +# CHECK-DAG: "files": +# CHECK-DAG: "diagnostics.log" +# CHECK-DAG: "statistics.json" +# CHECK-DAG: "commands.txt" + +# The bundle holds the collected artifacts; the scalars above are not repeated +# as redundant files. +# RUN: test -f %t.existing/statistics.json +# RUN: test -f %t.existing/commands.txt + +# The command snapshot is populated, not just present. +# RUN: FileCheck %s --check-prefix=CMDS --input-file=%t.existing/commands.txt +# CMDS: target list diff --git a/lldb/test/Shell/Diagnostics/TestReport.test b/lldb/test/Shell/Diagnostics/TestReport.test new file mode 100644 index 0000000000000..ed5611b50c9a1 --- /dev/null +++ b/lldb/test/Shell/Diagnostics/TestReport.test @@ -0,0 +1,29 @@ +# diagnostics report writes a bundle without contacting a tracker, and the +# bugreport alias runs the same command. + +# RUN: rm -rf %t.report +# RUN: %lldb --no-lldbinit -b -o 'diagnostics report --no-open -d %t.report' 2>&1 | FileCheck %s +# CHECK: Bug report written to +# CHECK: Attach the following files to the issue: +# CHECK-DAG: [ ] statistics.json +# CHECK-DAG: [ ] commands.txt +# CHECK: warning: the report may contain + +# Scalars (version, OS, invocation) ride in the report, not as redundant files. +# RUN: test -f %t.report/statistics.json +# RUN: test -f %t.report/commands.txt + +# The snapshot has content, not just an empty file. +# RUN: FileCheck %s --check-prefix=CMDS --input-file=%t.report/commands.txt +# CMDS: target list + +# RUN: rm -rf %t.report2 +# RUN: %lldb --no-lldbinit -b -o 'bugreport --no-open -d %t.report2' 2>&1 | FileCheck %s --check-prefix=ALIAS +# ALIAS: Bug report written to +# RUN: test -f %t.report2/commands.txt + +# An unknown reporter name is rejected after the bundle is written. +# RUN: rm -rf %t.report3 +# RUN: not %lldb --no-lldbinit -b -o 'diagnostics report -P nonesuch -d %t.report3' 2>&1 | FileCheck %s --check-prefix=BADNAME +# BADNAME: error: no bug reporter named 'nonesuch' +# RUN: test -f %t.report3/commands.txt diff --git a/lldb/unittests/Core/CMakeLists.txt b/lldb/unittests/Core/CMakeLists.txt index 8be205cf17f9c..6af0b6cf016cf 100644 --- a/lldb/unittests/Core/CMakeLists.txt +++ b/lldb/unittests/Core/CMakeLists.txt @@ -11,6 +11,7 @@ add_lldb_unittest(LLDBCoreTests DebuggerTest.cpp CommunicationTest.cpp DiagnosticEventTest.cpp + DiagnosticsTest.cpp DumpDataExtractorTest.cpp DumpRegisterInfoTest.cpp FormatEntityTest.cpp diff --git a/lldb/unittests/Core/DiagnosticsTest.cpp b/lldb/unittests/Core/DiagnosticsTest.cpp new file mode 100644 index 0000000000000..6bf0e7720b8fe --- /dev/null +++ b/lldb/unittests/Core/DiagnosticsTest.cpp @@ -0,0 +1,140 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#include "lldb/Core/Diagnostics.h" +#include "Plugins/Platform/MacOSX/PlatformMacOSX.h" +#include "Plugins/Platform/MacOSX/PlatformRemoteMacOSX.h" +#include "TestingSupport/TestUtilities.h" +#include "lldb/Core/Debugger.h" +#include "lldb/Host/FileSystem.h" +#include "lldb/Host/HostInfo.h" +#include "lldb/Target/ExecutionContext.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Testing/Support/Error.h" +#include "gtest/gtest.h" + +#include + +using namespace lldb; +using namespace lldb_private; + +namespace { +class DiagnosticsTest : public ::testing::Test { +public: + void SetUp() override { + FileSystem::Initialize(); + HostInfo::Initialize(); + PlatformMacOSX::Initialize(); + std::call_once(TestUtilities::g_debugger_initialize_flag, + []() { Debugger::Initialize(nullptr); }); + ArchSpec arch("x86_64-apple-macosx-"); + Platform::SetHostPlatform( + PlatformRemoteMacOSX::CreateInstance(true, &arch)); + } + void TearDown() override { + PlatformMacOSX::Terminate(); + HostInfo::Terminate(); + FileSystem::Terminate(); + } +}; + +// Read a file from the bundle directory, or "" if it cannot be read. +std::string ReadBundleFile(const FileSpec &dir, llvm::StringRef name) { + FileSpec path = dir.CopyByAppendingPathComponent(name); + llvm::ErrorOr> buffer = + llvm::MemoryBuffer::getFile(path.GetPath()); + if (!buffer) + return ""; + return (*buffer)->getBuffer().str(); +} +} // namespace + +TEST_F(DiagnosticsTest, ArtifactProviderIDsAreUnique) { + Diagnostics diagnostics; + Diagnostics::ArtifactProviderID id0 = + diagnostics.AddArtifactProvider("a.txt", [] { return "a"; }); + Diagnostics::ArtifactProviderID id1 = + diagnostics.AddArtifactProvider("b.txt", [] { return "b"; }); + EXPECT_NE(id0, id1); +} + +TEST_F(DiagnosticsTest, ArtifactProviderContributesToBundle) { + DebuggerSP debugger_sp = Debugger::CreateInstance(); + ASSERT_TRUE(debugger_sp); + + Diagnostics diagnostics; + int call_count = 0; + Diagnostics::ArtifactProviderID id = diagnostics.AddArtifactProvider( + "my-artifact.txt", [&call_count]() -> std::string { + ++call_count; + return "hello diagnostics"; + }); + + ExecutionContext exe_ctx; + + // A registered provider is invoked and its content lands in the bundle and + // in the report's attachments. + { + llvm::Expected dir = Diagnostics::CreateUniqueDirectory(); + ASSERT_THAT_EXPECTED(dir, llvm::Succeeded()); + llvm::Expected report = + diagnostics.Collect(*debugger_sp, exe_ctx, *dir); + ASSERT_THAT_EXPECTED(report, llvm::Succeeded()); + + EXPECT_EQ(call_count, 1); + EXPECT_TRUE( + llvm::is_contained(report->attachments.files, "my-artifact.txt")); + EXPECT_EQ(ReadBundleFile(*dir, "my-artifact.txt"), "hello diagnostics"); + + llvm::sys::fs::remove_directories(dir->GetPath()); + } + + // After removal the provider is neither invoked nor present in the bundle. + diagnostics.RemoveArtifactProvider(id); + call_count = 0; + { + llvm::Expected dir = Diagnostics::CreateUniqueDirectory(); + ASSERT_THAT_EXPECTED(dir, llvm::Succeeded()); + llvm::Expected report = + diagnostics.Collect(*debugger_sp, exe_ctx, *dir); + ASSERT_THAT_EXPECTED(report, llvm::Succeeded()); + + EXPECT_EQ(call_count, 0); + EXPECT_FALSE( + llvm::is_contained(report->attachments.files, "my-artifact.txt")); + + llvm::sys::fs::remove_directories(dir->GetPath()); + } + + Debugger::Destroy(debugger_sp); +} + +TEST_F(DiagnosticsTest, RemoveArtifactProviderIgnoresUnknownID) { + Diagnostics diagnostics; + Diagnostics::ArtifactProviderID id = + diagnostics.AddArtifactProvider("kept.txt", [] { return "kept"; }); + + // Removing an id that was never handed out must not disturb other providers. + diagnostics.RemoveArtifactProvider(id + 1); + + DebuggerSP debugger_sp = Debugger::CreateInstance(); + ASSERT_TRUE(debugger_sp); + ExecutionContext exe_ctx; + llvm::Expected dir = Diagnostics::CreateUniqueDirectory(); + ASSERT_THAT_EXPECTED(dir, llvm::Succeeded()); + llvm::Expected report = + diagnostics.Collect(*debugger_sp, exe_ctx, *dir); + ASSERT_THAT_EXPECTED(report, llvm::Succeeded()); + + EXPECT_TRUE(llvm::is_contained(report->attachments.files, "kept.txt")); + + llvm::sys::fs::remove_directories(dir->GetPath()); + Debugger::Destroy(debugger_sp); +} diff --git a/lldb/unittests/Host/HostTest.cpp b/lldb/unittests/Host/HostTest.cpp index 325bcfa08e056..ebcdf45885b4a 100644 --- a/lldb/unittests/Host/HostTest.cpp +++ b/lldb/unittests/Host/HostTest.cpp @@ -168,3 +168,14 @@ TEST(Host, LaunchProcessDuplicatesHandle) { ASSERT_THAT_EXPECTED(bytes_read, llvm::Succeeded()); ASSERT_EQ(llvm::StringRef(msg, *bytes_read), test_msg); } + +TEST(Host, URLEncode) { + // Unreserved characters (RFC 3986) are kept literal. + EXPECT_EQ(Host::URLEncode("AZaz09-_.~"), "AZaz09-_.~"); + // Everything else, including query-string metacharacters, is percent-encoded. + EXPECT_EQ(Host::URLEncode("a b&c=d"), "a%20b%26c%3Dd"); + EXPECT_EQ(Host::URLEncode("/?#"), "%2F%3F%23"); + // High bytes are encoded as two upper-case hex digits. + EXPECT_EQ(Host::URLEncode("\xC3\xA9"), "%C3%A9"); + EXPECT_EQ(Host::URLEncode(""), ""); +}