Files
llvm-project/lldb/source/Commands/CommandObjectPlugin.cpp
Dan Liew 3a50cfeba5 [LLDB][Part 1] Support enabling/disabling InstrumentationRuntime plugins in an debug session (#193328)
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
2026-04-30 13:26:24 -07:00

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;