blob: 08adcaebdf0383ae646805f4b2d54d0f45be957b [file] [edit]
//===-- 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/Core/Module.h"
#include "lldb/Host/FileSystem.h"
#include "lldb/Host/Host.h"
#include "lldb/Host/HostInfo.h"
#include "lldb/Interpreter/CommandInterpreter.h"
#include "lldb/Interpreter/CommandReturnObject.h"
#include "lldb/Interpreter/OptionValueProperties.h"
#include "lldb/Target/ExecutionContext.h"
#include "lldb/Target/Platform.h"
#include "lldb/Target/Process.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 <mutex>
#include <optional>
#include <string>
#include <vector>
using namespace lldb_private;
using namespace lldb;
using namespace llvm;
namespace {
#define LLDB_PROPERTIES_diagnostics
#include "CoreProperties.inc"
enum {
#define LLDB_PROPERTIES_diagnostics
#include "CorePropertiesEnum.inc"
};
} // namespace
DiagnosticsProperties::DiagnosticsProperties() {
m_collection_sp = std::make_shared<OptionValueProperties>("diagnostics");
m_collection_sp->Initialize(g_diagnostics_properties_def);
}
bool DiagnosticsProperties::GetCollectBinaries() const {
const uint32_t idx = ePropertyCollectBinaries;
return GetPropertyAtIndexAs<bool>(
idx, g_diagnostics_properties[idx].default_uint_value != 0);
}
bool DiagnosticsProperties::SetCollectBinaries(bool collect) {
return SetPropertyAtIndex(ePropertyCollectBinaries, collect);
}
DiagnosticsProperties &Diagnostics::GetGlobalProperties() {
static DiagnosticsProperties g_settings;
return g_settings;
}
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);
}
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
// 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());
}
// Copy a file into the bundle, best-effort like WriteArtifact. The basename is
// disambiguated because an executable and its symbol file can share one (a
// Mach-O and the DWARF binary inside its .dSYM), which would otherwise clobber.
static void CopyBinary(const FileSpec &src, const FileSpec &dir,
std::vector<std::string> &files) {
if (!src || !FileSystem::Instance().Exists(src))
return;
std::string name = src.GetFilename().str();
FileSpec dst = dir.CopyByAppendingPathComponent(name);
for (unsigned i = 1; FileSystem::Instance().Exists(dst); ++i) {
name = formatv("{0}.{1}", src.GetFilename(), i).str();
dst = dir.CopyByAppendingPathComponent(name);
}
if (llvm::sys::fs::copy_file(src.GetPath(), dst.GetPath()))
return;
llvm::sys::fs::setPermissions(dst.GetPath(), llvm::sys::fs::owner_read |
llvm::sys::fs::owner_write);
files.push_back(std::move(name));
}
// 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);
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);
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);
}
void Diagnostics::CollectBinaries(const ExecutionContext &exe_ctx,
const FileSpec &dir,
std::vector<std::string> &files) {
if (Target *target = exe_ctx.GetTargetPtr()) {
if (Module *exe = target->GetExecutableModulePointer()) {
CopyBinary(exe->GetFileSpec(), dir, files);
// Skip when symbols are inline: the symbol file is then the executable.
if (exe->GetSymbolFileFileSpec() != exe->GetFileSpec())
CopyBinary(exe->GetSymbolFileFileSpec(), dir, files);
}
}
if (Process *process = exe_ctx.GetProcessPtr())
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();
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;
}