[lldb] Add an artifact provider mechanism to Diagnostics#210417
Conversation
Add a hook that lets a subsystem contribute a file to the diagnostics bundle, without breaking layering and making Core depend on it. A plugin registers an ArtifactProvider callback and a file name via AddArtifactProvider. Providers run when a bundle is collected, and each one that yields content is written into the bundle and recorded in the report's attachments. This restores, in a simpler form, the collection point that the removed Diagnostics callback mechanism offered. The motivating consumer is the downstream Swift health check, which registers a provider to write its "swift-healthcheck.log" into the bundle. Unlike the old callback, a provider does not open or write its own file: it just returns the contents as a string and Diagnostics handles the rest. I have a follow-up PR that takes advantage of this new mechanism upstream to collect the GDB remote packet log.
be1c338 to
b41eda2
Compare
|
@llvm/pr-subscribers-lldb Author: Jonas Devlieghere (JDevlieghere) ChangesAdd a hook that lets a subsystem contribute a file to the diagnostics This restores, in a simpler form, the collection point that the removed I have a follow-up PR that takes advantage of this new mechanism <sub>Stack created with <a href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub> Full diff: https://github.com/llvm/llvm-project/pull/210417.diff 4 Files Affected:
diff --git a/lldb/include/lldb/Core/Diagnostics.h b/lldb/include/lldb/Core/Diagnostics.h
index 554314e32e8ce..bcacf3b9c301d 100644
--- a/lldb/include/lldb/Core/Diagnostics.h
+++ b/lldb/include/lldb/Core/Diagnostics.h
@@ -14,6 +14,8 @@
#include "lldb/Utility/Log.h"
#include "llvm/Support/Error.h"
+#include <functional>
+#include <mutex>
#include <optional>
#include <string>
#include <vector>
@@ -90,6 +92,19 @@ class Diagnostics {
/// 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<std::string()>;
+ 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 DiagnosticsProperties &GetGlobalProperties();
@@ -122,6 +137,8 @@ class Diagnostics {
static void CollectBinaries(const ExecutionContext &exe_ctx,
const FileSpec &dir,
std::vector<std::string> &files);
+ void CollectArtifactProviders(const FileSpec &dir,
+ std::vector<std::string> &files);
/// @}
/// Scalars carried in the report rather than written as files.
@@ -131,6 +148,19 @@ class Diagnostics {
/// @}
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<ArtifactProviderEntry> m_artifact_providers;
+ std::mutex m_artifact_providers_mutex;
+ /// @}
};
/// Render a diagnostics report as JSON, for `diagnostics dump`'s terminal
diff --git a/lldb/source/Core/Diagnostics.cpp b/lldb/source/Core/Diagnostics.cpp
index 311d8fab2b1bc..08adcaebdf038 100644
--- a/lldb/source/Core/Diagnostics.cpp
+++ b/lldb/source/Core/Diagnostics.cpp
@@ -25,12 +25,14 @@
#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 <mutex>
#include <optional>
#include <string>
#include <vector>
@@ -149,6 +151,20 @@ void Diagnostics::Record(llvm::StringRef message) {
m_log_handler.Emit(message);
}
+Diagnostics::ArtifactProviderID
+Diagnostics::AddArtifactProvider(std::string name, ArtifactProvider provider) {
+ std::lock_guard<std::mutex> 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<std::mutex> 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
@@ -253,6 +269,7 @@ Diagnostics::Collect(Debugger &debugger, const ExecutionContext &exe_ctx,
CollectCommands(debugger, exe_ctx, dir, report.attachments.files);
if (GetGlobalProperties().GetCollectBinaries())
CollectBinaries(exe_ctx, dir, report.attachments.files);
+ CollectArtifactProviders(dir, report.attachments.files);
report.version = lldb_private::GetVersion();
report.os = GetHostDescription(exe_ctx);
@@ -317,6 +334,19 @@ void Diagnostics::CollectBinaries(const ExecutionContext &exe_ctx,
CopyBinary(process->GetCoreFile(), dir, files);
}
+void Diagnostics::CollectArtifactProviders(const FileSpec &dir,
+ std::vector<std::string> &files) {
+ // Snapshot under the lock, then run providers without it: a provider can be
+ // slow and must not block registration or removal.
+ std::vector<ArtifactProviderEntry> providers;
+ {
+ std::lock_guard<std::mutex> 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();
diff --git a/lldb/unittests/Core/CMakeLists.txt b/lldb/unittests/Core/CMakeLists.txt
index d69432d332f44..959793d7d4de0 100644
--- a/lldb/unittests/Core/CMakeLists.txt
+++ b/lldb/unittests/Core/CMakeLists.txt
@@ -3,6 +3,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 <mutex>
+
+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<std::unique_ptr<llvm::MemoryBuffer>> 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<FileSpec> dir = Diagnostics::CreateUniqueDirectory();
+ ASSERT_THAT_EXPECTED(dir, llvm::Succeeded());
+ llvm::Expected<Diagnostics::Report> 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<FileSpec> dir = Diagnostics::CreateUniqueDirectory();
+ ASSERT_THAT_EXPECTED(dir, llvm::Succeeded());
+ llvm::Expected<Diagnostics::Report> 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<FileSpec> dir = Diagnostics::CreateUniqueDirectory();
+ ASSERT_THAT_EXPECTED(dir, llvm::Succeeded());
+ llvm::Expected<Diagnostics::Report> 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);
+}
|
* [lldb] Add a non-Darwin Host::OpenURL and a Host::URLEncode helper (llvm#206129) Host::OpenURL was only defined for Darwin (in Host.mm). Add a portable implementation in the common Host.cpp: on Unix it launches xdg-open; on Windows it returns "unsupported" for now. xdg-open is run without a shell (run_in_shell=false) so query-string metacharacters in the URL are never interpreted by the shell. Also add Host::URLEncode, an RFC 3986 percent-encoder for assembling tracker URLs. These are the building blocks for an upcoming "diagnostics report" command that opens a pre-filled bug URL, and the encoder is shared with a downstream tap-to-radar reporter. (cherry picked from commit dbd4528) * [lldb] Remove the Diagnostics callback mechanism (llvm#206132) The Diagnostics framework had a callback registry (AddCallback / RemoveCallback) so subsystems could contribute files to a diagnostics directory, intended to also run during crash handling. That crash-time path never materialized, and the sole registered callback was the Debugger copying its file-backed logs. If you had no logging enabled, the directory would be empty, confusing the users. Remove the registry and the callback loop in Diagnostics::Create (which now just writes the in-memory log), and expose the log copying as Debugger::CopyLogFilesToDirectory, which "diagnostics dump" calls directly. The dump command now copies the invoking debugger's logs rather than every debugger's, which is the more useful behavior I want to double down on. (cherry picked from commit 9f34f1c) * [lldb] Move Diagnostics from Utility to Core (NFC) (llvm#206152) Nothing in the Utility or Host layers uses Diagnostics. Its only callers are Debugger (the always-on log feeder), SBDebugger, and the SystemInitializerCommon lifecycle. Those all live in Core or above. The header depends only on Utility primitives (FileSpec, Log, Error), and lldbInitialization already links lldbCore, so the move adds no new link dependency anywhere. Relocating it to Core lets Diagnostics reach Debugger, Target, CommandInterpreter, and Host, which simplifies an upcoming change that collect a richer diagnostics bundle (statistics, command snapshots, invocation, etc) and allows us to implement that directly in the Diagnostics class. (cherry picked from commit 2ceab13) * [lldb] Collect a diagnostics bundle on the Diagnostics class (llvm#206189) Add Diagnostics::Collect, which gathers the state a triager needs into a directory, best-effort (one failed section never sinks the rest): the always-on log plus the debugger's file logs, statistics.json from DebuggerStats, and a snapshot of the commands run first when triaging (target list, image list, thread list, backtraces, image lookup, frame variable). It returns a Diagnostics::Report with the LLDB version, host, and how LLDB was invoked, plus an Attachments holding the bundle directory and the files written into it. Each file is recorded as it is written, so a file that could not be created is simply absent from the list. The report is expected to grow more fields over time. `diagnostics dump` now calls Collect and prints the report as JSON to the terminal instead of only reporting where the directory was written. Here's what this all looks like: ``` (lldb) diagnostics dump { "attachments": { "directory": "/var/folders/6b/3sb80ks56rz5vwlhsdvpsxmh0000gn/T/diagnostics-4b5e9f", "files": [ "diagnostics.log", "statistics.json", "commands.txt" ] }, "invocation": "./build/bin/count", "os": "arm64-apple-macosx platform=host os=27.0 build=26A374", "version": "lldb version 23.0.0git (git@github.com:llvm/llvm-project.git revision 85fa95f)\n clang revision 85fa95f\n llvm revision 85fa95f" } ``` This is in preparation for a future PR which adds the ability to take all this information and pre-fill a bug report with it. (cherry picked from commit a873660) * [lldb] Add a BugReporter plugin type and "diagnostics report" (llvm#206578) Introduce a BugReporter plugin kind that files an assembled Diagnostics::Report through a pluggable destination, plus a "diagnostics report" command (aliased "bugreport") that collects the bundle and files it through the first registered reporter. CreateBugReporterInstance() returns the first registered reporter, so a reporter registered earlier wins and a downstream tree can take over by registering ahead of the built-ins. BugReporterNone is the always-registered, last-in-order fallback. Its File() returns an error pointing at LLDB_BUG_REPORT_URL, so the command surfaces "no tracker configured" through the normal error path instead of special-casing it. "diagnostics report" writes the bundle, prints a review warning, and files it unless --no-open is given. The upcoming GitHub reporter, gated by a CMake option, is the first real destination. (cherry picked from commit 8cf09c5) * [lldb] Add a GitHub bug reporter (llvm#206607) Add a BugReporter plugin that files a diagnostics bundle as an llvm/llvm-project GitHub issue. File() renders a short Markdown body from the Diagnostics::Report (version, host, invocation, and a pointer to the bundle directory to attach), truncates it under a GET-safe URL length on a UTF-8 character boundary, and opens a pre-filled issues/new page with Host::OpenURL. It is gated by LLDB_ENABLE_GITHUB_BUG_REPORTER (default on) and registers ahead of the no-op fallback, so it is the default destination for "diagnostics report" while a downstream tree can still register its own reporter ahead of it. (cherry picked from commit 1556da0) * [lldb] Add an artifact provider mechanism to Diagnostics (llvm#210417) Add a hook that lets a subsystem contribute a file to the diagnostics bundle, without breaking layering and making Core depend on it. A plugin registers an ArtifactProvider callback and a file name via AddArtifactProvider. Providers run when a bundle is collected, and each one that yields content is written into the bundle and recorded in the report's attachments. This restores, in a simpler form, the collection point that the removed Diagnostics callback mechanism offered. The motivating consumer is the downstream Swift health check, which registers a provider to write its "swift-healthcheck.log" into the bundle. Unlike the old callback, a provider does not open or write its own file: it just returns the contents as a string and Diagnostics handles the rest. (cherry picked from commit 2120490) * [lldb] Contribute swift-healthcheck to the diagnostics bundle The Swift language plugin added swift-healthcheck.log to the diagnostics directory through the Diagnostics callback registry, which the bundle series removed. Port it to the artifact provider mechanism (llvm#210417): LogChannelSwift returns the health log's contents on demand and Diagnostics writes the file, so it lands in the bundle's attachments and the plugin no longer touches the filesystem itself.
Add a hook that lets a subsystem contribute a file to the diagnostics
bundle, without breaking layering and making Core depend on it. A plugin registers an
ArtifactProvider callback and a file name via AddArtifactProvider.
Providers run when a bundle is collected, and each one that yields
content is written into the bundle and recorded in the report's
attachments.
This restores, in a simpler form, the collection point that the removed
Diagnostics callback mechanism offered. The motivating consumer is the
downstream Swift health check, which registers a provider to write its
"swift-healthcheck.log" into the bundle. Unlike the old callback, a
provider does not open or write its own file: it just returns the
contents as a string and Diagnostics handles the rest.
I have a follow-up PR that takes advantage of this new mechanism
upstream to collect the GDB remote packet log.
Stack created with GitHub Stacks CLI • Give Feedback 💬