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 85fa95f05bc5f0c6b4fd845141e522cd4b9589d7)\n clang revision 85fa95f05bc5f0c6b4fd845141e522cd4b9589d7\n llvm revision 85fa95f05bc5f0c6b4fd845141e522cd4b9589d7"
}
```
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.
292 lines
9.7 KiB
C++
292 lines
9.7 KiB
C++
//===-- 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/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 <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
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> &Diagnostics::InstanceImpl() {
|
|
static std::optional<Diagnostics> 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<FileSpec> 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<FileSpec> 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);
|
|
}
|
|
|
|
// 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<std::string> &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::Report>
|
|
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);
|
|
|
|
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<std::string> &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<std::string> &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<std::string> &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);
|
|
}
|
|
|
|
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<std::string> 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;
|
|
}
|