This patch is the first part in a patch series that will allow enabling/disabling InstrumentationRuntime plugins in a running debug session. This part adds the `--domain` flag to the `enable`, `disable`, `list` sub commands of `plugin` shell command and plumbs the value of this flag to where it will be needed in a subsequent patch. From the user perspective the flag does nothing useful yet because all values passed to the flag except `global` (the default and what represents LLDB's existing behavior) are rejected. Subsequent patches will allow the flag to do something useful. The `--domain` flag adds a notion of "domain" to plugins with respect to their enablement. Previously all plugins were treated as global and have their enablement stored globally. This is despite the fact that some plugins clearly are not global. For example the `instrumentation-runtime` plugins clearly exist on a per-target basis (the instances of the `InstrumentationRuntime` exist in each process). In addition to this plugins being "global" means instances of the `Debugger` instance are not properly isolated from each other. This PR is a stepping stone towards fixing these design problems. The PR introduces three different domains for plugins: * `global` - Enablement of the plugin can be controlled globally. This is the existing behavior of all LLDB plugins. * `debugger` - Enablement of the plugin can be controlled on a per `Debugger` basis. * `target` - Enablement of the plugin can be controlled on a per `Target` basis. These values are encoded in the new `PluginDomainKind` enum. It is important to note that the design in this PR means a plugin can support more than one domain. In particular in future patches when `instrumentation-runtime` plugins gain support for more than just the `global` domain they will support the `debugger` and `target` domain as well. The key reason that the `instrumentation-runtime` plugins need to support more than one domain is that the plugins need a default enablement value **before** the target exists. That default value will need to come from the `global` domain. Architecturally it should probably come from the `debugger` domain instead but refactoring enablement into Debugger instances is much too large a refactor for this patch series and is a problem that can be tackled later. This patch modifies the `PluginNamespace` struct to: * Store the set of domains supported by the namespace and provided some helper methods to determine what is supported. * Store one of two callbacks. Either `SetPluginEnabledGlobalDomain` (the existing function interface used by most plugins) or `SetPluginEnabledAllDomains` (a new interface used by `InstrumentationRuntime` plugins). In this patch the `InstrumentationRuntime` plugins use the new `SetPluginEnabledAllDomains` function interface for enablement (i.e. the interface of `PluginManager::SetInstrumentationRuntimePluginEnabled` has changed) which passes the `Debugger` instance that made the request and the domain the user provided to the `plugin enable` or `plugin disable` command. To make this patch easier to review the `PluginManager::SetInstrumentationRuntimePluginEnabled` function actually rejects all domains except `global` to keep the behavior change down to a minimum. Proper support for enabling/disabling instrumentation-runtime plugins in the `target`, and `debugger` domains will be implemented in a subsequent patch. The `plugin list` command implementations also reject any domain that isn't `global`. Support for other domains will be added in the subsequent patch that adds support for other domains in the `instrumentation-runtime` plugins. The `plugin enable`, `plugin disable`, `plugin list` commands will use the `global` domain by default so that there is no behavior change for existing workflows. Two new shell tests are included that exercise the new code paths: * `command-plugin-enable-disable-domain-flag.test` validates that `--domain global` works for both global-only and multi-domain plugin namespaces, and that `--domain debugger` and `--domain target` are correctly rejected for now. * `command-plugin-list-domain-flag.test` validates the same behavior for the list command in both text and JSON output modes. I am not experienced at adding flags to LLDB shell commands so I had Claude Code write that part and also help write test cases. Assisted-by: Claude Code rdar://167725878
494 lines
17 KiB
C++
494 lines
17 KiB
C++
//===-- CommandObjectPlugin.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 "CommandObjectPlugin.h"
|
|
#include "lldb/Core/PluginManager.h"
|
|
#include "lldb/Host/OptionParser.h"
|
|
#include "lldb/Interpreter/CommandInterpreter.h"
|
|
#include "lldb/Interpreter/CommandOptionArgumentTable.h"
|
|
#include "lldb/Interpreter/CommandReturnObject.h"
|
|
#include "lldb/Interpreter/OptionArgParser.h"
|
|
|
|
using namespace lldb;
|
|
using namespace lldb_private;
|
|
|
|
class CommandObjectPluginLoad : public CommandObjectParsed {
|
|
public:
|
|
CommandObjectPluginLoad(CommandInterpreter &interpreter)
|
|
: CommandObjectParsed(interpreter, "plugin load",
|
|
"Import a dylib that implements an LLDB plugin.",
|
|
nullptr) {
|
|
AddSimpleArgumentList(eArgTypeFilename);
|
|
}
|
|
|
|
~CommandObjectPluginLoad() override = default;
|
|
|
|
protected:
|
|
void DoExecute(Args &command, CommandReturnObject &result) override {
|
|
size_t argc = command.GetArgumentCount();
|
|
|
|
if (argc != 1) {
|
|
result.AppendError("'plugin load' requires one argument");
|
|
return;
|
|
}
|
|
|
|
Status error;
|
|
|
|
FileSpec dylib_fspec(command[0].ref());
|
|
FileSystem::Instance().Resolve(dylib_fspec);
|
|
|
|
if (GetDebugger().LoadPlugin(dylib_fspec, error))
|
|
result.SetStatus(eReturnStatusSuccessFinishResult);
|
|
else {
|
|
result.AppendError(error.AsCString());
|
|
}
|
|
}
|
|
};
|
|
|
|
namespace {
|
|
// Helper function to perform an action on each matching plugin.
|
|
// The action callback is given the containing namespace along with plugin info
|
|
// for each matching plugin.
|
|
static int ActOnMatchingPlugins(
|
|
const llvm::StringRef pattern,
|
|
std::function<void(const PluginNamespace &plugin_namespace,
|
|
const std::vector<RegisteredPluginInfo> &plugin_info)>
|
|
action) {
|
|
int num_matching = 0;
|
|
|
|
for (const PluginNamespace &plugin_namespace :
|
|
PluginManager::GetPluginNamespaces()) {
|
|
|
|
std::vector<RegisteredPluginInfo> matching_plugins;
|
|
for (const RegisteredPluginInfo &plugin_info :
|
|
plugin_namespace.get_info()) {
|
|
if (PluginManager::MatchPluginName(pattern, plugin_namespace,
|
|
plugin_info))
|
|
matching_plugins.push_back(plugin_info);
|
|
}
|
|
|
|
if (!matching_plugins.empty()) {
|
|
num_matching += matching_plugins.size();
|
|
action(plugin_namespace, matching_plugins);
|
|
}
|
|
}
|
|
|
|
return num_matching;
|
|
}
|
|
|
|
// Call the "SetEnable" function for each matching plugins.
|
|
// Used to share the majority of the code between the enable
|
|
// and disable commands.
|
|
int SetEnableOnMatchingPlugins(const llvm::StringRef &pattern,
|
|
CommandReturnObject &result, bool enabled,
|
|
Debugger &requesting_debugger,
|
|
PluginDomainKind domain) {
|
|
return ActOnMatchingPlugins(
|
|
pattern, [&](const PluginNamespace &plugin_namespace,
|
|
const std::vector<RegisteredPluginInfo> &plugins) {
|
|
auto PrintEnablement = [enabled,
|
|
&result](const RegisteredPluginInfo plugin) {
|
|
result.AppendMessageWithFormatv(" {0} {1, -30} {2}",
|
|
enabled ? "[+]" : "[-]", plugin.name,
|
|
plugin.description);
|
|
};
|
|
|
|
result.AppendMessage(plugin_namespace.name);
|
|
for (const auto &plugin : plugins) {
|
|
if (plugin_namespace.SupportsOnlyDomain(
|
|
PluginDomainKind::ePluginDomainKindGlobal)) {
|
|
bool success = true;
|
|
if (domain != ePluginDomainKindGlobal) {
|
|
result.AppendErrorWithFormatv(
|
|
"failed to {} plugin {}.{}: {} domain is not supported",
|
|
enabled ? "enable" : "disable", plugin_namespace.name,
|
|
plugin.name, PluginManager::PluginDomainKindToStr(domain));
|
|
continue;
|
|
}
|
|
success = (*plugin_namespace.GetSetEnabledGlobalFn())(plugin.name,
|
|
enabled);
|
|
|
|
if (!success) {
|
|
result.AppendErrorWithFormatv("failed to {} plugin {}.{}",
|
|
enabled ? "enable" : "disable",
|
|
plugin_namespace.name, plugin.name);
|
|
continue;
|
|
}
|
|
PrintEnablement(plugin);
|
|
continue;
|
|
}
|
|
|
|
// Handle plugin namespace that supports more than just the global
|
|
// domain. Currently this is just the instrumentation-runtime
|
|
// namespace.
|
|
if (!plugin_namespace.SupportsDomain(domain)) {
|
|
result.AppendErrorWithFormatv(
|
|
"failed to {0} plugin {1}.{2}: the {1} namespace "
|
|
"does not support the {3} domain",
|
|
enabled ? "enable" : "disable", plugin_namespace.name,
|
|
plugin.name, PluginManager::PluginDomainKindToStr(domain));
|
|
continue;
|
|
}
|
|
assert(plugin_namespace.GetSetEnabledAllDomainsFn().has_value());
|
|
llvm::Error error = (*plugin_namespace.GetSetEnabledAllDomainsFn())(
|
|
plugin.name, enabled, requesting_debugger, domain);
|
|
|
|
if (error) {
|
|
result.AppendErrorWithFormatv("failed to {} plugin {}.{}: {}",
|
|
enabled ? "enable" : "disable",
|
|
plugin_namespace.name, plugin.name,
|
|
llvm::toString(std::move(error)));
|
|
continue;
|
|
}
|
|
PrintEnablement(plugin);
|
|
}
|
|
});
|
|
}
|
|
|
|
static std::string ConvertJSONToPrettyString(const llvm::json::Value &json) {
|
|
std::string str;
|
|
llvm::raw_string_ostream os(str);
|
|
os << llvm::formatv("{0:2}", json).str();
|
|
os.flush();
|
|
return str;
|
|
}
|
|
|
|
#define LLDB_OPTIONS_plugin_list
|
|
#include "CommandOptions.inc"
|
|
|
|
// These option definitions are used by the plugin list command.
|
|
class PluginListCommandOptions : public Options {
|
|
static constexpr const PluginDomainKind kDefaultDomain =
|
|
ePluginDomainKindGlobal;
|
|
|
|
public:
|
|
PluginListCommandOptions() = default;
|
|
|
|
~PluginListCommandOptions() override = default;
|
|
|
|
Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg,
|
|
ExecutionContext *execution_context) override {
|
|
Status error;
|
|
const int short_option = m_getopt_table[option_idx].val;
|
|
|
|
switch (short_option) {
|
|
case 'j':
|
|
m_json_format = true;
|
|
break;
|
|
case 'd':
|
|
m_domain = static_cast<PluginDomainKind>(OptionArgParser::ToOptionEnum(
|
|
option_arg, GetDefinitions()[option_idx].enum_values, kDefaultDomain,
|
|
error));
|
|
break;
|
|
default:
|
|
llvm_unreachable("Unimplemented option");
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
void OptionParsingStarting(ExecutionContext *execution_context) override {
|
|
m_json_format = false;
|
|
m_domain = kDefaultDomain;
|
|
}
|
|
|
|
llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
|
|
return llvm::ArrayRef(g_plugin_list_options);
|
|
}
|
|
|
|
// Instance variables to hold the values for command options.
|
|
bool m_json_format = false;
|
|
PluginDomainKind m_domain = kDefaultDomain;
|
|
};
|
|
} // namespace
|
|
|
|
class CommandObjectPluginList : public CommandObjectParsed {
|
|
public:
|
|
CommandObjectPluginList(CommandInterpreter &interpreter)
|
|
: CommandObjectParsed(interpreter, "plugin list",
|
|
"Report info about registered LLDB plugins.",
|
|
nullptr) {
|
|
AddSimpleArgumentList(eArgTypeManagedPlugin);
|
|
SetHelpLong(R"(
|
|
Display information about registered plugins.
|
|
The plugin information is formatted as shown below:
|
|
|
|
<plugin-namespace>
|
|
[+] <plugin-name> Plugin #1 description
|
|
[-] <plugin-name> Plugin #2 description
|
|
|
|
An enabled plugin is marked with [+] and a disabled plugin is marked with [-].
|
|
|
|
Plugins can be listed by namespace and name with:
|
|
|
|
plugin list <plugin-namespace>[.<plugin-name>]
|
|
|
|
Plugins can be listed by namespace alone or with a fully qualified name. When listed
|
|
with just a namespace all plugins in that namespace are listed. When no arguments
|
|
are given all plugins are listed.
|
|
|
|
Examples:
|
|
List all plugins
|
|
|
|
(lldb) plugin list
|
|
|
|
List all plugins in the system-runtime namespace
|
|
|
|
(lldb) plugin list system-runtime
|
|
|
|
List only the plugin 'foo' matching a fully qualified name exactly
|
|
|
|
(lldb) plugin list system-runtime.foo
|
|
)");
|
|
}
|
|
|
|
~CommandObjectPluginList() override = default;
|
|
|
|
Options *GetOptions() override { return &m_options; }
|
|
|
|
void
|
|
HandleArgumentCompletion(CompletionRequest &request,
|
|
OptionElementVector &opt_element_vector) override {
|
|
lldb_private::CommandCompletions::InvokeCommonCompletionCallbacks(
|
|
GetCommandInterpreter(), lldb::eManagedPluginCompletion, request,
|
|
nullptr);
|
|
}
|
|
|
|
protected:
|
|
void DoExecute(Args &command, CommandReturnObject &result) override {
|
|
size_t argc = command.GetArgumentCount();
|
|
result.SetStatus(eReturnStatusSuccessFinishResult);
|
|
|
|
// Create a temporary vector to hold the patterns to simplify the logic
|
|
// for the case when the user passes no patterns
|
|
std::vector<llvm::StringRef> patterns;
|
|
patterns.reserve(argc == 0 ? 1 : argc);
|
|
if (argc == 0)
|
|
patterns.push_back("");
|
|
else
|
|
for (size_t i = 0; i < argc; ++i)
|
|
patterns.push_back(command[i].ref());
|
|
|
|
if (m_options.m_json_format)
|
|
OutputJsonFormat(patterns, result, GetDebugger(), m_options.m_domain);
|
|
else
|
|
OutputTextFormat(patterns, result, GetDebugger(), m_options.m_domain);
|
|
}
|
|
|
|
private:
|
|
void OutputJsonFormat(const std::vector<llvm::StringRef> &patterns,
|
|
CommandReturnObject &result,
|
|
Debugger &requesting_debugger,
|
|
PluginDomainKind domain) {
|
|
if (domain != PluginDomainKind::ePluginDomainKindGlobal) {
|
|
result.AppendErrorWithFormatv(
|
|
"{} domain is not supported",
|
|
PluginManager::PluginDomainKindToStr(domain));
|
|
return;
|
|
}
|
|
|
|
llvm::json::Object obj;
|
|
bool found_empty = false;
|
|
for (const llvm::StringRef pattern : patterns) {
|
|
llvm::json::Object pat_obj = PluginManager::GetJSON(pattern);
|
|
if (pat_obj.empty()) {
|
|
found_empty = true;
|
|
result.AppendErrorWithFormat(
|
|
"Found no matching plugins for pattern '%s'", pattern.data());
|
|
break;
|
|
}
|
|
for (auto &entry : pat_obj) {
|
|
obj[entry.first] = std::move(entry.second);
|
|
}
|
|
}
|
|
if (!found_empty) {
|
|
result.AppendMessage(ConvertJSONToPrettyString(std::move(obj)));
|
|
}
|
|
}
|
|
|
|
void OutputTextFormat(const std::vector<llvm::StringRef> &patterns,
|
|
CommandReturnObject &result,
|
|
Debugger &requesting_debugger,
|
|
PluginDomainKind domain) {
|
|
if (domain != PluginDomainKind::ePluginDomainKindGlobal) {
|
|
result.AppendErrorWithFormatv(
|
|
"{} domain is not supported",
|
|
PluginManager::PluginDomainKindToStr(domain));
|
|
return;
|
|
}
|
|
|
|
for (const llvm::StringRef pattern : patterns) {
|
|
int num_matching = ActOnMatchingPlugins(
|
|
pattern, [&](const PluginNamespace &plugin_namespace,
|
|
const std::vector<RegisteredPluginInfo> &plugins) {
|
|
result.AppendMessage(plugin_namespace.name);
|
|
for (auto &plugin : plugins) {
|
|
result.AppendMessageWithFormatv(" {0} {1, -30} {2}",
|
|
plugin.enabled ? "[+]" : "[-]",
|
|
plugin.name, plugin.description);
|
|
}
|
|
});
|
|
if (num_matching == 0) {
|
|
result.AppendErrorWithFormat(
|
|
"Found no matching plugins for pattern '%s'", pattern.data());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
PluginListCommandOptions m_options;
|
|
};
|
|
|
|
static void DoPluginEnableDisable(Args &command, CommandReturnObject &result,
|
|
bool enable, Debugger &requesting_debugger,
|
|
PluginDomainKind domain) {
|
|
const char *name = enable ? "enable" : "disable";
|
|
size_t argc = command.GetArgumentCount();
|
|
if (argc == 0) {
|
|
result.AppendErrorWithFormat("'plugin %s' requires one or more arguments",
|
|
name);
|
|
return;
|
|
}
|
|
result.SetStatus(eReturnStatusSuccessFinishResult);
|
|
|
|
for (size_t i = 0; i < argc; ++i) {
|
|
llvm::StringRef pattern = command[i].ref();
|
|
int num_matching = SetEnableOnMatchingPlugins(pattern, result, enable,
|
|
requesting_debugger, domain);
|
|
|
|
if (num_matching == 0) {
|
|
result.AppendErrorWithFormat(
|
|
"Found no matching plugins to %s for pattern '%s'", name,
|
|
pattern.data());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
#define LLDB_OPTIONS_plugin_enable
|
|
#include "CommandOptions.inc"
|
|
|
|
#define LLDB_OPTIONS_plugin_disable
|
|
#include "CommandOptions.inc"
|
|
|
|
// Options class for the --domain flag, shared by plugin enable and
|
|
// plugin disable (and reusable by plugin status in the future).
|
|
class PluginDomainOptions : public Options {
|
|
static constexpr const PluginDomainKind kDefaultDomain =
|
|
ePluginDomainKindGlobal;
|
|
|
|
public:
|
|
PluginDomainOptions(llvm::ArrayRef<OptionDefinition> definitions)
|
|
: m_definitions(definitions) {}
|
|
|
|
Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg,
|
|
ExecutionContext *execution_context) override {
|
|
Status error;
|
|
const int short_option = m_getopt_table[option_idx].val;
|
|
switch (short_option) {
|
|
case 'd':
|
|
m_domain = static_cast<PluginDomainKind>(OptionArgParser::ToOptionEnum(
|
|
option_arg, GetDefinitions()[option_idx].enum_values, kDefaultDomain,
|
|
error));
|
|
break;
|
|
default:
|
|
llvm_unreachable("Unimplemented option");
|
|
}
|
|
return error;
|
|
}
|
|
|
|
void OptionParsingStarting(ExecutionContext *execution_context) override {
|
|
m_domain = kDefaultDomain;
|
|
}
|
|
|
|
llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
|
|
return m_definitions;
|
|
}
|
|
|
|
PluginDomainKind m_domain = kDefaultDomain;
|
|
|
|
private:
|
|
llvm::ArrayRef<OptionDefinition> m_definitions;
|
|
};
|
|
|
|
class CommandObjectPluginEnable : public CommandObjectParsed {
|
|
public:
|
|
CommandObjectPluginEnable(CommandInterpreter &interpreter)
|
|
: CommandObjectParsed(interpreter, "plugin enable",
|
|
"Enable registered LLDB plugins.", nullptr),
|
|
m_options(llvm::ArrayRef(g_plugin_enable_options)) {
|
|
AddSimpleArgumentList(eArgTypeManagedPlugin);
|
|
}
|
|
|
|
void
|
|
HandleArgumentCompletion(CompletionRequest &request,
|
|
OptionElementVector &opt_element_vector) override {
|
|
lldb_private::CommandCompletions::InvokeCommonCompletionCallbacks(
|
|
GetCommandInterpreter(), lldb::eManagedPluginCompletion, request,
|
|
nullptr);
|
|
}
|
|
|
|
~CommandObjectPluginEnable() override = default;
|
|
|
|
Options *GetOptions() override { return &m_options; }
|
|
|
|
protected:
|
|
void DoExecute(Args &command, CommandReturnObject &result) override {
|
|
DoPluginEnableDisable(command, result, /*enable=*/true, GetDebugger(),
|
|
m_options.m_domain);
|
|
}
|
|
|
|
PluginDomainOptions m_options;
|
|
};
|
|
|
|
class CommandObjectPluginDisable : public CommandObjectParsed {
|
|
public:
|
|
CommandObjectPluginDisable(CommandInterpreter &interpreter)
|
|
: CommandObjectParsed(interpreter, "plugin disable",
|
|
"Disable registered LLDB plugins.", nullptr),
|
|
m_options(llvm::ArrayRef(g_plugin_disable_options)) {
|
|
AddSimpleArgumentList(eArgTypeManagedPlugin);
|
|
}
|
|
|
|
void
|
|
HandleArgumentCompletion(CompletionRequest &request,
|
|
OptionElementVector &opt_element_vector) override {
|
|
lldb_private::CommandCompletions::InvokeCommonCompletionCallbacks(
|
|
GetCommandInterpreter(), lldb::eManagedPluginCompletion, request,
|
|
nullptr);
|
|
}
|
|
|
|
~CommandObjectPluginDisable() override = default;
|
|
|
|
Options *GetOptions() override { return &m_options; }
|
|
|
|
protected:
|
|
void DoExecute(Args &command, CommandReturnObject &result) override {
|
|
DoPluginEnableDisable(command, result, /*enable=*/false, GetDebugger(),
|
|
m_options.m_domain);
|
|
}
|
|
|
|
PluginDomainOptions m_options;
|
|
};
|
|
|
|
CommandObjectPlugin::CommandObjectPlugin(CommandInterpreter &interpreter)
|
|
: CommandObjectMultiword(interpreter, "plugin",
|
|
"Commands for managing LLDB plugins.",
|
|
"plugin <subcommand> [<subcommand-options>]") {
|
|
LoadSubCommand("load",
|
|
CommandObjectSP(new CommandObjectPluginLoad(interpreter)));
|
|
LoadSubCommand("list",
|
|
CommandObjectSP(new CommandObjectPluginList(interpreter)));
|
|
LoadSubCommand("enable",
|
|
CommandObjectSP(new CommandObjectPluginEnable(interpreter)));
|
|
LoadSubCommand("disable",
|
|
CommandObjectSP(new CommandObjectPluginDisable(interpreter)));
|
|
}
|
|
|
|
CommandObjectPlugin::~CommandObjectPlugin() = default;
|