| //===-- 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; | |
| } |