//===-- CommandObjectReproducer.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 "CommandObjectReproducer.h" #include "lldb/Host/HostInfo.h" #include "lldb/Host/OptionParser.h" #include "lldb/Interpreter/CommandInterpreter.h" #include "lldb/Interpreter/CommandReturnObject.h" #include "lldb/Interpreter/OptionArgParser.h" #include "lldb/Utility/GDBRemote.h" #include "lldb/Utility/ProcessInfo.h" #include "lldb/Utility/Reproducer.h" #include <csignal> using namespace lldb; using namespace llvm; using namespace lldb_private; using namespace lldb_private::repro; enum ReproducerProvider { eReproducerProviderCommands, eReproducerProviderFiles, eReproducerProviderSymbolFiles, eReproducerProviderGDB, eReproducerProviderProcessInfo, eReproducerProviderVersion, eReproducerProviderWorkingDirectory, eReproducerProviderHomeDirectory, eReproducerProviderNone }; static constexpr OptionEnumValueElement g_reproducer_provider_type[] = { { eReproducerProviderCommands, "commands", "Command Interpreter Commands", }, { eReproducerProviderFiles, "files", "Files", }, { eReproducerProviderSymbolFiles, "symbol-files", "Symbol Files", }, { eReproducerProviderGDB, "gdb", "GDB Remote Packets", }, { eReproducerProviderProcessInfo, "processes", "Process Info", }, { eReproducerProviderVersion, "version", "Version", }, { eReproducerProviderWorkingDirectory, "cwd", "Working Directory", }, { eReproducerProviderHomeDirectory, "home", "Home Directory", }, { eReproducerProviderNone, "none", "None", }, }; static constexpr OptionEnumValues ReproducerProviderType() { return OptionEnumValues(g_reproducer_provider_type); } #define LLDB_OPTIONS_reproducer_dump #include "CommandOptions.inc" enum ReproducerCrashSignal { eReproducerCrashSigill, eReproducerCrashSigsegv, }; static constexpr OptionEnumValueElement g_reproducer_signaltype[] = { { eReproducerCrashSigill, "SIGILL", "Illegal instruction", }, { eReproducerCrashSigsegv, "SIGSEGV", "Segmentation fault", }, }; static constexpr OptionEnumValues ReproducerSignalType() { return OptionEnumValues(g_reproducer_signaltype); } #define LLDB_OPTIONS_reproducer_xcrash #include "CommandOptions.inc" #define LLDB_OPTIONS_reproducer_verify #include "CommandOptions.inc" template <typename T> llvm::Expected<T> static ReadFromYAML(StringRef filename) { auto error_or_file = MemoryBuffer::getFile(filename); if (auto err = error_or_file.getError()) { return errorCodeToError(err); } T t; yaml::Input yin((*error_or_file)->getBuffer()); yin >> t; if (auto err = yin.error()) { return errorCodeToError(err); } return t; } static void SetError(CommandReturnObject &result, Error err) { result.GetErrorStream().Printf("error: %s\n", toString(std::move(err)).c_str()); result.SetStatus(eReturnStatusFailed); } /// Create a loader from the given path if specified. Otherwise use the current /// loader used for replay. static Loader * GetLoaderFromPathOrCurrent(llvm::Optional<Loader> &loader_storage, CommandReturnObject &result, FileSpec reproducer_path) { if (reproducer_path) { loader_storage.emplace(reproducer_path); Loader *loader = &(*loader_storage); if (Error err = loader->LoadIndex()) { // This is a hard error and will set the result to eReturnStatusFailed. SetError(result, std::move(err)); return nullptr; } return loader; } if (Loader *loader = Reproducer::Instance().GetLoader()) return loader; // This is a soft error because this is expected to fail during capture. result.SetError("Not specifying a reproducer is only support during replay."); result.SetStatus(eReturnStatusSuccessFinishNoResult); return nullptr; } class CommandObjectReproducerGenerate : public CommandObjectParsed { public: CommandObjectReproducerGenerate(CommandInterpreter &interpreter) : CommandObjectParsed( interpreter, "reproducer generate", "Generate reproducer on disk. When the debugger is in capture " "mode, this command will output the reproducer to a directory on " "disk and quit. In replay mode this command in a no-op.", nullptr) {} ~CommandObjectReproducerGenerate() override = default; protected: bool DoExecute(Args &command, CommandReturnObject &result) override { if (!command.empty()) { result.AppendErrorWithFormat("'%s' takes no arguments", m_cmd_name.c_str()); return false; } auto &r = Reproducer::Instance(); if (auto generator = r.GetGenerator()) { generator->Keep(); if (llvm::Error e = repro::Finalize(r.GetReproducerPath())) { SetError(result, std::move(e)); return result.Succeeded(); } } else if (r.IsReplaying()) { // Make this operation a NO-OP in replay mode. result.SetStatus(eReturnStatusSuccessFinishNoResult); return result.Succeeded(); } else { result.AppendErrorWithFormat("Unable to get the reproducer generator"); result.SetStatus(eReturnStatusFailed); return false; } result.GetOutputStream() << "Reproducer written to '" << r.GetReproducerPath() << "'\n"; result.GetOutputStream() << "Please have a look at the directory to assess if you're willing to " "share the contained information.\n"; m_interpreter.BroadcastEvent( CommandInterpreter::eBroadcastBitQuitCommandReceived); result.SetStatus(eReturnStatusQuit); return result.Succeeded(); } }; class CommandObjectReproducerXCrash : public CommandObjectParsed { public: CommandObjectReproducerXCrash(CommandInterpreter &interpreter) : CommandObjectParsed(interpreter, "reproducer xcrash", "Intentionally force the debugger to crash in " "order to trigger and test reproducer generation.", nullptr) {} ~CommandObjectReproducerXCrash() override = default; Options *GetOptions() override { return &m_options; } class CommandOptions : public Options { public: CommandOptions() : Options() {} ~CommandOptions() override = default; Status SetOptionValue(uint32_t option_idx, StringRef option_arg, ExecutionContext *execution_context) override { Status error; const int short_option = m_getopt_table[option_idx].val; switch (short_option) { case 's': signal = (ReproducerCrashSignal)OptionArgParser::ToOptionEnum( option_arg, GetDefinitions()[option_idx].enum_values, 0, error); if (!error.Success()) error.SetErrorStringWithFormat("unrecognized value for signal '%s'", option_arg.str().c_str()); break; default: llvm_unreachable("Unimplemented option"); } return error; } void OptionParsingStarting(ExecutionContext *execution_context) override { signal = eReproducerCrashSigsegv; } ArrayRef<OptionDefinition> GetDefinitions() override { return makeArrayRef(g_reproducer_xcrash_options); } ReproducerCrashSignal signal = eReproducerCrashSigsegv; }; protected: bool DoExecute(Args &command, CommandReturnObject &result) override { if (!command.empty()) { result.AppendErrorWithFormat("'%s' takes no arguments", m_cmd_name.c_str()); return false; } auto &r = Reproducer::Instance(); if (!r.IsCapturing() && !r.IsReplaying()) { result.SetError( "forcing a crash is only supported when capturing a reproducer."); result.SetStatus(eReturnStatusSuccessFinishNoResult); return false; } switch (m_options.signal) { case eReproducerCrashSigill: std::raise(SIGILL); break; case eReproducerCrashSigsegv: std::raise(SIGSEGV); break; } result.SetStatus(eReturnStatusQuit); return result.Succeeded(); } private: CommandOptions m_options; }; class CommandObjectReproducerStatus : public CommandObjectParsed { public: CommandObjectReproducerStatus(CommandInterpreter &interpreter) : CommandObjectParsed( interpreter, "reproducer status", "Show the current reproducer status. In capture mode the " "debugger " "is collecting all the information it needs to create a " "reproducer. In replay mode the reproducer is replaying a " "reproducer. When the reproducers are off, no data is collected " "and no reproducer can be generated.", nullptr) {} ~CommandObjectReproducerStatus() override = default; protected: bool DoExecute(Args &command, CommandReturnObject &result) override { if (!command.empty()) { result.AppendErrorWithFormat("'%s' takes no arguments", m_cmd_name.c_str()); return false; } auto &r = Reproducer::Instance(); if (r.IsCapturing()) { result.GetOutputStream() << "Reproducer is in capture mode.\n"; } else if (r.IsReplaying()) { result.GetOutputStream() << "Reproducer is in replay mode.\n"; } else { result.GetOutputStream() << "Reproducer is off.\n"; } if (r.IsCapturing() || r.IsReplaying()) { result.GetOutputStream() << "Path: " << r.GetReproducerPath().GetPath() << '\n'; } // Auto generate is hidden unless enabled because this is mostly for // development and testing. if (Generator *g = r.GetGenerator()) { if (g->IsAutoGenerate()) result.GetOutputStream() << "Auto generate: on\n"; } result.SetStatus(eReturnStatusSuccessFinishResult); return result.Succeeded(); } }; class CommandObjectReproducerDump : public CommandObjectParsed { public: CommandObjectReproducerDump(CommandInterpreter &interpreter) : CommandObjectParsed(interpreter, "reproducer dump", "Dump the information contained in a reproducer. " "If no reproducer is specified during replay, it " "dumps the content of the current reproducer.", nullptr) {} ~CommandObjectReproducerDump() override = default; Options *GetOptions() override { return &m_options; } class CommandOptions : public Options { public: CommandOptions() : Options(), file() {} ~CommandOptions() override = default; Status SetOptionValue(uint32_t option_idx, StringRef option_arg, ExecutionContext *execution_context) override { Status error; const int short_option = m_getopt_table[option_idx].val; switch (short_option) { case 'f': file.SetFile(option_arg, FileSpec::Style::native); FileSystem::Instance().Resolve(file); break; case 'p': provider = (ReproducerProvider)OptionArgParser::ToOptionEnum( option_arg, GetDefinitions()[option_idx].enum_values, 0, error); if (!error.Success()) error.SetErrorStringWithFormat("unrecognized value for provider '%s'", option_arg.str().c_str()); break; default: llvm_unreachable("Unimplemented option"); } return error; } void OptionParsingStarting(ExecutionContext *execution_context) override { file.Clear(); provider = eReproducerProviderNone; } ArrayRef<OptionDefinition> GetDefinitions() override { return makeArrayRef(g_reproducer_dump_options); } FileSpec file; ReproducerProvider provider = eReproducerProviderNone; }; protected: bool DoExecute(Args &command, CommandReturnObject &result) override { if (!command.empty()) { result.AppendErrorWithFormat("'%s' takes no arguments", m_cmd_name.c_str()); return false; } llvm::Optional<Loader> loader_storage; Loader *loader = GetLoaderFromPathOrCurrent(loader_storage, result, m_options.file); if (!loader) return false; switch (m_options.provider) { case eReproducerProviderFiles: { FileSpec vfs_mapping = loader->GetFile<FileProvider::Info>(); // Read the VFS mapping. ErrorOr<std::unique_ptr<MemoryBuffer>> buffer = vfs::getRealFileSystem()->getBufferForFile(vfs_mapping.GetPath()); if (!buffer) { SetError(result, errorCodeToError(buffer.getError())); return false; } // Initialize a VFS from the given mapping. IntrusiveRefCntPtr<vfs::FileSystem> vfs = vfs::getVFSFromYAML( std::move(buffer.get()), nullptr, vfs_mapping.GetPath()); // Dump the VFS to a buffer. std::string str; raw_string_ostream os(str); static_cast<vfs::RedirectingFileSystem &>(*vfs).dump(os); os.flush(); // Return the string. result.AppendMessage(str); result.SetStatus(eReturnStatusSuccessFinishResult); return true; } case eReproducerProviderSymbolFiles: { Expected<std::string> symbol_files = loader->LoadBuffer<SymbolFileProvider>(); if (!symbol_files) { SetError(result, symbol_files.takeError()); return false; } std::vector<SymbolFileProvider::Entry> entries; llvm::yaml::Input yin(*symbol_files); yin >> entries; for (const auto &entry : entries) { result.AppendMessageWithFormat("- uuid: %s\n", entry.uuid.c_str()); result.AppendMessageWithFormat(" module path: %s\n", entry.module_path.c_str()); result.AppendMessageWithFormat(" symbol path: %s\n", entry.symbol_path.c_str()); } result.SetStatus(eReturnStatusSuccessFinishResult); return true; } case eReproducerProviderVersion: { Expected<std::string> version = loader->LoadBuffer<VersionProvider>(); if (!version) { SetError(result, version.takeError()); return false; } result.AppendMessage(*version); result.SetStatus(eReturnStatusSuccessFinishResult); return true; } case eReproducerProviderWorkingDirectory: { Expected<std::string> cwd = repro::GetDirectoryFrom<WorkingDirectoryProvider>(loader); if (!cwd) { SetError(result, cwd.takeError()); return false; } result.AppendMessage(*cwd); result.SetStatus(eReturnStatusSuccessFinishResult); return true; } case eReproducerProviderHomeDirectory: { Expected<std::string> home = repro::GetDirectoryFrom<HomeDirectoryProvider>(loader); if (!home) { SetError(result, home.takeError()); return false; } result.AppendMessage(*home); result.SetStatus(eReturnStatusSuccessFinishResult); return true; } case eReproducerProviderCommands: { std::unique_ptr<repro::MultiLoader<repro::CommandProvider>> multi_loader = repro::MultiLoader<repro::CommandProvider>::Create(loader); if (!multi_loader) { SetError(result, make_error<StringError>("Unable to create command loader.", llvm::inconvertibleErrorCode())); return false; } // Iterate over the command files and dump them. llvm::Optional<std::string> command_file; while ((command_file = multi_loader->GetNextFile())) { if (!command_file) break; auto command_buffer = llvm::MemoryBuffer::getFile(*command_file); if (auto err = command_buffer.getError()) { SetError(result, errorCodeToError(err)); return false; } result.AppendMessage((*command_buffer)->getBuffer()); } result.SetStatus(eReturnStatusSuccessFinishResult); return true; } case eReproducerProviderGDB: { std::unique_ptr<repro::MultiLoader<repro::GDBRemoteProvider>> multi_loader = repro::MultiLoader<repro::GDBRemoteProvider>::Create(loader); if (!multi_loader) { SetError(result, make_error<StringError>("Unable to create GDB loader.", llvm::inconvertibleErrorCode())); return false; } llvm::Optional<std::string> gdb_file; while ((gdb_file = multi_loader->GetNextFile())) { if (llvm::Expected<std::vector<GDBRemotePacket>> packets = ReadFromYAML<std::vector<GDBRemotePacket>>(*gdb_file)) { for (GDBRemotePacket &packet : *packets) { packet.Dump(result.GetOutputStream()); } } else { SetError(result, packets.takeError()); return false; } } result.SetStatus(eReturnStatusSuccessFinishResult); return true; } case eReproducerProviderProcessInfo: { std::unique_ptr<repro::MultiLoader<repro::ProcessInfoProvider>> multi_loader = repro::MultiLoader<repro::ProcessInfoProvider>::Create(loader); if (!multi_loader) { SetError(result, make_error<StringError>( llvm::inconvertibleErrorCode(), "Unable to create process info loader.")); return false; } llvm::Optional<std::string> process_file; while ((process_file = multi_loader->GetNextFile())) { if (llvm::Expected<ProcessInstanceInfoList> infos = ReadFromYAML<ProcessInstanceInfoList>(*process_file)) { for (ProcessInstanceInfo info : *infos) info.Dump(result.GetOutputStream(), HostInfo::GetUserIDResolver()); } else { SetError(result, infos.takeError()); return false; } } result.SetStatus(eReturnStatusSuccessFinishResult); return true; } case eReproducerProviderNone: result.SetError("No valid provider specified."); return false; } result.SetStatus(eReturnStatusSuccessFinishNoResult); return result.Succeeded(); } private: CommandOptions m_options; }; class CommandObjectReproducerVerify : public CommandObjectParsed { public: CommandObjectReproducerVerify(CommandInterpreter &interpreter) : CommandObjectParsed(interpreter, "reproducer verify", "Verify the contents of a reproducer. " "If no reproducer is specified during replay, it " "verifies the content of the current reproducer.", nullptr) {} ~CommandObjectReproducerVerify() override = default; Options *GetOptions() override { return &m_options; } class CommandOptions : public Options { public: CommandOptions() : Options(), file() {} ~CommandOptions() override = default; Status SetOptionValue(uint32_t option_idx, StringRef option_arg, ExecutionContext *execution_context) override { Status error; const int short_option = m_getopt_table[option_idx].val; switch (short_option) { case 'f': file.SetFile(option_arg, FileSpec::Style::native); FileSystem::Instance().Resolve(file); break; default: llvm_unreachable("Unimplemented option"); } return error; } void OptionParsingStarting(ExecutionContext *execution_context) override { file.Clear(); } ArrayRef<OptionDefinition> GetDefinitions() override { return makeArrayRef(g_reproducer_verify_options); } FileSpec file; }; protected: bool DoExecute(Args &command, CommandReturnObject &result) override { if (!command.empty()) { result.AppendErrorWithFormat("'%s' takes no arguments", m_cmd_name.c_str()); return false; } llvm::Optional<Loader> loader_storage; Loader *loader = GetLoaderFromPathOrCurrent(loader_storage, result, m_options.file); if (!loader) return false; bool errors = false; auto error_callback = [&](llvm::StringRef error) { errors = true; result.AppendError(error); }; bool warnings = false; auto warning_callback = [&](llvm::StringRef warning) { warnings = true; result.AppendWarning(warning); }; auto note_callback = [&](llvm::StringRef warning) { result.AppendMessage(warning); }; Verifier verifier(loader); verifier.Verify(error_callback, warning_callback, note_callback); if (warnings || errors) { result.AppendMessage("reproducer verification failed"); result.SetStatus(eReturnStatusFailed); } else { result.AppendMessage("reproducer verification succeeded"); result.SetStatus(eReturnStatusSuccessFinishResult); } return result.Succeeded(); } private: CommandOptions m_options; }; CommandObjectReproducer::CommandObjectReproducer( CommandInterpreter &interpreter) : CommandObjectMultiword( interpreter, "reproducer", "Commands for manipulating reproducers. Reproducers make it " "possible " "to capture full debug sessions with all its dependencies. The " "resulting reproducer is used to replay the debug session while " "debugging the debugger.\n" "Because reproducers need the whole the debug session from " "beginning to end, you need to launch the debugger in capture or " "replay mode, commonly though the command line driver.\n" "Reproducers are unrelated record-replay debugging, as you cannot " "interact with the debugger during replay.\n", "reproducer <subcommand> [<subcommand-options>]") { LoadSubCommand( "generate", CommandObjectSP(new CommandObjectReproducerGenerate(interpreter))); LoadSubCommand("status", CommandObjectSP( new CommandObjectReproducerStatus(interpreter))); LoadSubCommand("dump", CommandObjectSP(new CommandObjectReproducerDump(interpreter))); LoadSubCommand("verify", CommandObjectSP( new CommandObjectReproducerVerify(interpreter))); LoadSubCommand("xcrash", CommandObjectSP( new CommandObjectReproducerXCrash(interpreter))); } CommandObjectReproducer::~CommandObjectReproducer() = default;