Skip to content

Add enable/disable api for SystemRuntime plugins #133794

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 2, 2025

Conversation

dmpots
Copy link
Contributor

@dmpots dmpots commented Mar 31, 2025

This commit adds support for enabling and disabling plugins by name. The changes are made generically in the PluginInstances class, but currently we only expose the ability to SystemRuntime plugins. Other plugins types can be added easily.

We had a few design goals for how disabled plugins should work

  1. Plugins that are disabled should still be visible to the system. This allows us to dynamically enable and disable plugins and report their state to the user.
  2. Plugin order should be stable across disable and enable changes. We want avoid changing the order of plugin lookup. When a plugin is re-enabled it should return to its original slot in the creation order.
  3. Disabled plugins should not appear in PluginManager operations. Clients should be able to assume that only enabled plugins will be returned from the PluginManager.

For the implementation we modify the plugin instance to maintain a bool of its enabled state. Existing clients external to the Instances class expect to iterate over only enabled instance so we skip over disabed instances in the query and snapshot apis. This way the client does not have to manually check which instances are enabled.

This commit adds support for enabling and disabling plugins by name.
The changes are made generically in the `PluginInstances` class, but
currently we only expose the ability to SystemRuntime plugins. Other
plugins types can be added easily.

We had a few design goals for how disabled plugins should work

  1. Plugins that are disabled should still be visible to the system.
     This allows us to dynamically enable and disable plugins
     and report their state to the user.
  2. Plugin order should be stable across disable and enable changes.
     We want avoid changing the order of plugin lookup. When a plugin
     is re-enabled it should return to its original slot in the
     creation order.
  3. Disabled plugins should not appear in PluginManager operations.
     Clients should be able to assume that only enabled plugins will
     be returned from the PluginManager.

For the implementation we modify the plugin instance to maintain a bool
of its enabled state.  Existing clients external to the Instances class
expect to iterate over only enabled instance so we skip over disabed
instances in the query and snapshot apis. This way the client does not
have to manually check which instances are enabled.
@dmpots dmpots requested review from clayborg and Jlalond March 31, 2025 20:49
@dmpots dmpots requested a review from JDevlieghere as a code owner March 31, 2025 20:49
@llvmbot llvmbot added the lldb label Mar 31, 2025
@llvmbot
Copy link
Member

llvmbot commented Mar 31, 2025

@llvm/pr-subscribers-lldb

Author: David Peixotto (dmpots)

Changes

This commit adds support for enabling and disabling plugins by name. The changes are made generically in the PluginInstances class, but currently we only expose the ability to SystemRuntime plugins. Other plugins types can be added easily.

We had a few design goals for how disabled plugins should work

  1. Plugins that are disabled should still be visible to the system. This allows us to dynamically enable and disable plugins and report their state to the user.
  2. Plugin order should be stable across disable and enable changes. We want avoid changing the order of plugin lookup. When a plugin is re-enabled it should return to its original slot in the creation order.
  3. Disabled plugins should not appear in PluginManager operations. Clients should be able to assume that only enabled plugins will be returned from the PluginManager.

For the implementation we modify the plugin instance to maintain a bool of its enabled state. Existing clients external to the Instances class expect to iterate over only enabled instance so we skip over disabed instances in the query and snapshot apis. This way the client does not have to manually check which instances are enabled.


Patch is 21.13 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/133794.diff

4 Files Affected:

  • (modified) lldb/include/lldb/Core/PluginManager.h (+13)
  • (modified) lldb/source/Core/PluginManager.cpp (+52-3)
  • (modified) lldb/unittests/Core/CMakeLists.txt (+1)
  • (added) lldb/unittests/Core/PluginManagerTest.cpp (+367)
diff --git a/lldb/include/lldb/Core/PluginManager.h b/lldb/include/lldb/Core/PluginManager.h
index e4e0c3eea67f8..a6dab045adf27 100644
--- a/lldb/include/lldb/Core/PluginManager.h
+++ b/lldb/include/lldb/Core/PluginManager.h
@@ -22,6 +22,7 @@
 
 #include <cstddef>
 #include <cstdint>
+#include <vector>
 
 #define LLDB_PLUGIN_DEFINE_ADV(ClassName, PluginName)                          \
   namespace lldb_private {                                                     \
@@ -47,6 +48,12 @@ class CommandInterpreter;
 class Debugger;
 class StringList;
 
+struct RegisteredPluginInfo {
+  llvm::StringRef name = "";
+  llvm::StringRef description = "";
+  bool enabled = false;
+};
+
 class PluginManager {
 public:
   static void Initialize();
@@ -168,6 +175,12 @@ class PluginManager {
   static SystemRuntimeCreateInstance
   GetSystemRuntimeCreateCallbackAtIndex(uint32_t idx);
 
+  static std::vector<RegisteredPluginInfo> GetSystemRuntimePluginInfo();
+
+  // Modify the enabled state of a SystemRuntime plugin.
+  // Returns false if the plugin name is not found.
+  static bool SetSystemRuntimePluginEnabled(llvm::StringRef name, bool enabled);
+
   // ObjectFile
   static bool
   RegisterPlugin(llvm::StringRef name, llvm::StringRef description,
diff --git a/lldb/source/Core/PluginManager.cpp b/lldb/source/Core/PluginManager.cpp
index 95eb940efcef2..e6cb248ef31ce 100644
--- a/lldb/source/Core/PluginManager.cpp
+++ b/lldb/source/Core/PluginManager.cpp
@@ -188,11 +188,13 @@ template <typename Callback> struct PluginInstance {
   PluginInstance(llvm::StringRef name, llvm::StringRef description,
                  Callback create_callback,
                  DebuggerInitializeCallback debugger_init_callback = nullptr)
-      : name(name), description(description), create_callback(create_callback),
+      : name(name), description(description), enabled(true),
+        create_callback(create_callback),
         debugger_init_callback(debugger_init_callback) {}
 
   llvm::StringRef name;
   llvm::StringRef description;
+  bool enabled;
   Callback create_callback;
   DebuggerInitializeCallback debugger_init_callback;
 };
@@ -250,7 +252,9 @@ template <typename Instance> class PluginInstances {
   }
 
   void PerformDebuggerCallback(Debugger &debugger) {
-    for (auto &instance : m_instances) {
+    for (const auto &instance : m_instances) {
+      if (!instance.enabled)
+        continue;
       if (instance.debugger_init_callback)
         instance.debugger_init_callback(debugger);
     }
@@ -260,7 +264,14 @@ template <typename Instance> class PluginInstances {
   // Note that this is a copy of the internal state so modifications
   // to the returned instances will not be reflected back to instances
   // stored by the PluginInstances object.
-  std::vector<Instance> GetSnapshot() { return m_instances; }
+  std::vector<Instance> GetSnapshot() {
+    std::vector<Instance> enabled_instances;
+    for (const auto &instance : m_instances) {
+      if (instance.enabled)
+        enabled_instances.push_back(instance);
+    }
+    return enabled_instances;
+  }
 
   const Instance *GetInstanceAtIndex(uint32_t idx) {
     uint32_t count = 0;
@@ -280,12 +291,41 @@ template <typename Instance> class PluginInstances {
   const Instance *
   FindEnabledInstance(std::function<bool(const Instance &)> predicate) const {
     for (const auto &instance : m_instances) {
+      if (!instance.enabled)
+        continue;
       if (predicate(instance))
         return &instance;
     }
     return nullptr;
   }
 
+  // Return a list of all the registered plugin instances. This includes both
+  // enabled and disabled instances. The instances are listed in the order they
+  // were registered which is the order they would be queried if they were all
+  // enabled.
+  std::vector<RegisteredPluginInfo> GetPluginInfoForAllInstances() {
+    // Lookup the plugin info for each instance in the sorted order.
+    std::vector<RegisteredPluginInfo> plugin_infos;
+    plugin_infos.reserve(m_instances.size());
+    for (const Instance &instance : m_instances)
+      plugin_infos.push_back(
+          {instance.name, instance.description, instance.enabled});
+
+    return plugin_infos;
+  }
+
+  bool SetInstanceEnabled(llvm::StringRef name, bool enable) {
+    auto it = std::find_if(
+        m_instances.begin(), m_instances.end(),
+        [&](const Instance &instance) { return instance.name == name; });
+
+    if (it == m_instances.end())
+      return false;
+
+    it->enabled = enable;
+    return true;
+  }
+
 private:
   std::vector<Instance> m_instances;
 };
@@ -627,6 +667,15 @@ PluginManager::GetSystemRuntimeCreateCallbackAtIndex(uint32_t idx) {
   return GetSystemRuntimeInstances().GetCallbackAtIndex(idx);
 }
 
+std::vector<RegisteredPluginInfo> PluginManager::GetSystemRuntimePluginInfo() {
+  return GetSystemRuntimeInstances().GetPluginInfoForAllInstances();
+}
+
+bool PluginManager::SetSystemRuntimePluginEnabled(llvm::StringRef name,
+                                                  bool enable) {
+  return GetSystemRuntimeInstances().SetInstanceEnabled(name, enable);
+}
+
 #pragma mark ObjectFile
 
 struct ObjectFileInstance : public PluginInstance<ObjectFileCreateInstance> {
diff --git a/lldb/unittests/Core/CMakeLists.txt b/lldb/unittests/Core/CMakeLists.txt
index 60265f794b5e8..8580f5887ea2b 100644
--- a/lldb/unittests/Core/CMakeLists.txt
+++ b/lldb/unittests/Core/CMakeLists.txt
@@ -7,6 +7,7 @@ add_lldb_unittest(LLDBCoreTests
   FormatEntityTest.cpp
   MangledTest.cpp
   ModuleSpecTest.cpp
+  PluginManagerTest.cpp
   ProgressReportTest.cpp
   RichManglingContextTest.cpp
   SourceLocationSpecTest.cpp
diff --git a/lldb/unittests/Core/PluginManagerTest.cpp b/lldb/unittests/Core/PluginManagerTest.cpp
new file mode 100644
index 0000000000000..ca1003ca9a85a
--- /dev/null
+++ b/lldb/unittests/Core/PluginManagerTest.cpp
@@ -0,0 +1,367 @@
+
+#include "lldb/Core/PluginManager.h"
+
+#include "gtest/gtest.h"
+
+using namespace lldb;
+using namespace lldb_private;
+
+// Mock system runtime plugin create functions.
+SystemRuntime *CreateSystemRuntimePluginA(Process *process) { return nullptr; }
+
+SystemRuntime *CreateSystemRuntimePluginB(Process *process) { return nullptr; }
+
+SystemRuntime *CreateSystemRuntimePluginC(Process *process) { return nullptr; }
+
+// Test class for testing the PluginManager.
+// The PluginManager modifies global state when registering new plugins. This
+// class is intended to undo those modifications in the destructor to give each
+// test a clean slate with no registered plugins at the start of a test.
+class PluginManagerTest : public testing::Test {
+public:
+  // Remove any pre-registered plugins so we have a known starting point.
+  static void SetUpTestSuite() { RemoveAllRegisteredSystemRuntimePlugins(); }
+
+  // Add mock system runtime plugins for testing.
+  void RegisterMockSystemRuntimePlugins() {
+    ASSERT_TRUE(PluginManager::RegisterPlugin("a", "test instance A",
+                                              CreateSystemRuntimePluginA));
+    ASSERT_TRUE(PluginManager::RegisterPlugin("b", "test instance B",
+                                              CreateSystemRuntimePluginB));
+    ASSERT_TRUE(PluginManager::RegisterPlugin("c", "test instance C",
+                                              CreateSystemRuntimePluginC));
+  }
+
+  // Remove any plugins added during the tests.
+  virtual ~PluginManagerTest() override {
+    RemoveAllRegisteredSystemRuntimePlugins();
+  }
+
+protected:
+  std::vector<SystemRuntimeCreateInstance> m_system_runtime_plugins;
+
+  static void RemoveAllRegisteredSystemRuntimePlugins() {
+    // Enable all currently registered plugins so we can get a handle to
+    // their create callbacks in the loop below. Only enabled plugins
+    // are returned from the PluginManager Get*CreateCallbackAtIndex apis.
+    for (const RegisteredPluginInfo &PluginInfo :
+         PluginManager::GetSystemRuntimePluginInfo()) {
+      PluginManager::SetSystemRuntimePluginEnabled(PluginInfo.name, true);
+    }
+
+    // Get a handle to the create call backs for all the registered plugins.
+    std::vector<SystemRuntimeCreateInstance> registered_plugin_callbacks;
+    SystemRuntimeCreateInstance create_callback = nullptr;
+    for (uint32_t idx = 0;
+         (create_callback =
+              PluginManager::GetSystemRuntimeCreateCallbackAtIndex(idx)) !=
+         nullptr;
+         ++idx) {
+      registered_plugin_callbacks.push_back((create_callback));
+    }
+
+    // Remove all currently registered plugins.
+    for (SystemRuntimeCreateInstance create_callback :
+         registered_plugin_callbacks) {
+      PluginManager::UnregisterPlugin(create_callback);
+    }
+  }
+};
+
+// Test basic register functionality.
+TEST_F(PluginManagerTest, RegisterSystemRuntimePlugin) {
+  RegisterMockSystemRuntimePlugins();
+
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(0),
+            CreateSystemRuntimePluginA);
+
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(1),
+            CreateSystemRuntimePluginB);
+
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(2),
+            CreateSystemRuntimePluginC);
+
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(3), nullptr);
+}
+
+// Test basic un-register functionality.
+TEST_F(PluginManagerTest, UnRegisterSystemRuntimePlugin) {
+  RegisterMockSystemRuntimePlugins();
+
+  ASSERT_TRUE(PluginManager::UnregisterPlugin(CreateSystemRuntimePluginB));
+
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(0),
+            CreateSystemRuntimePluginA);
+
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(1),
+            CreateSystemRuntimePluginC);
+
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(2), nullptr);
+}
+
+// Test registered plugin info functionality.
+TEST_F(PluginManagerTest, SystemRuntimePluginInfo) {
+  RegisterMockSystemRuntimePlugins();
+
+  std::vector<RegisteredPluginInfo> plugin_info =
+      PluginManager::GetSystemRuntimePluginInfo();
+  ASSERT_EQ(plugin_info.size(), 3u);
+  ASSERT_EQ(plugin_info[0].name, "a");
+  ASSERT_EQ(plugin_info[0].description, "test instance A");
+  ASSERT_EQ(plugin_info[0].enabled, true);
+  ASSERT_EQ(plugin_info[1].name, "b");
+  ASSERT_EQ(plugin_info[1].description, "test instance B");
+  ASSERT_EQ(plugin_info[1].enabled, true);
+  ASSERT_EQ(plugin_info[2].name, "c");
+  ASSERT_EQ(plugin_info[2].description, "test instance C");
+  ASSERT_EQ(plugin_info[2].enabled, true);
+}
+
+// Test basic un-register functionality.
+TEST_F(PluginManagerTest, UnRegisterSystemRuntimePluginInfo) {
+  RegisterMockSystemRuntimePlugins();
+
+  // Initial plugin info has all three registered plugins.
+  std::vector<RegisteredPluginInfo> plugin_info =
+      PluginManager::GetSystemRuntimePluginInfo();
+  ASSERT_EQ(plugin_info.size(), 3u);
+
+  ASSERT_TRUE(PluginManager::UnregisterPlugin(CreateSystemRuntimePluginB));
+
+  // After un-registering a plugin it should be removed from plugin info.
+  plugin_info = PluginManager::GetSystemRuntimePluginInfo();
+  ASSERT_EQ(plugin_info.size(), 2u);
+  ASSERT_EQ(plugin_info[0].name, "a");
+  ASSERT_EQ(plugin_info[0].enabled, true);
+  ASSERT_EQ(plugin_info[1].name, "c");
+  ASSERT_EQ(plugin_info[1].enabled, true);
+}
+
+// Test plugin disable functionality.
+TEST_F(PluginManagerTest, SystemRuntimePluginDisable) {
+  RegisterMockSystemRuntimePlugins();
+
+  // Disable plugin should succeed.
+  ASSERT_TRUE(PluginManager::SetSystemRuntimePluginEnabled("b", false));
+
+  // Disabling a plugin does not remove it from plugin info.
+  std::vector<RegisteredPluginInfo> plugin_info =
+      PluginManager::GetSystemRuntimePluginInfo();
+  ASSERT_EQ(plugin_info.size(), 3u);
+  ASSERT_EQ(plugin_info[0].name, "a");
+  ASSERT_EQ(plugin_info[0].enabled, true);
+  ASSERT_EQ(plugin_info[1].name, "b");
+  ASSERT_EQ(plugin_info[1].enabled, false);
+  ASSERT_EQ(plugin_info[2].name, "c");
+  ASSERT_EQ(plugin_info[2].enabled, true);
+
+  // Disabling a plugin does remove it from available plugins.
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(0),
+            CreateSystemRuntimePluginA);
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(1),
+            CreateSystemRuntimePluginC);
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(2), nullptr);
+}
+
+// Test plugin disable and enable functionality.
+TEST_F(PluginManagerTest, SystemRuntimePluginDisableThenEnable) {
+  RegisterMockSystemRuntimePlugins();
+
+  // Initially plugin b is available in slot 1.
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(1),
+            CreateSystemRuntimePluginB);
+
+  // Disabling it will remove it from available plugins.
+  ASSERT_TRUE(PluginManager::SetSystemRuntimePluginEnabled("b", false));
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(0),
+            CreateSystemRuntimePluginA);
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(1),
+            CreateSystemRuntimePluginC);
+
+  // We can re-enable the plugin later and it should go back to the original
+  // slot.
+  ASSERT_TRUE(PluginManager::SetSystemRuntimePluginEnabled("b", true));
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(0),
+            CreateSystemRuntimePluginA);
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(1),
+            CreateSystemRuntimePluginB);
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(2),
+            CreateSystemRuntimePluginC);
+
+  // And show up in the plugin info correctly.
+  std::vector<RegisteredPluginInfo> plugin_info =
+      PluginManager::GetSystemRuntimePluginInfo();
+  ASSERT_EQ(plugin_info.size(), 3u);
+  ASSERT_EQ(plugin_info[0].name, "a");
+  ASSERT_EQ(plugin_info[0].enabled, true);
+  ASSERT_EQ(plugin_info[1].name, "b");
+  ASSERT_EQ(plugin_info[1].enabled, true);
+  ASSERT_EQ(plugin_info[2].name, "c");
+  ASSERT_EQ(plugin_info[2].enabled, true);
+}
+
+// Test calling disable on an already disabled plugin is ok.
+TEST_F(PluginManagerTest, SystemRuntimePluginDisableDisabled) {
+  RegisterMockSystemRuntimePlugins();
+
+  // Initial call to disable the plugin should succeed.
+  ASSERT_TRUE(PluginManager::SetSystemRuntimePluginEnabled("b", false));
+
+  // The second call should also succeed because the plugin is already disabled.
+  ASSERT_TRUE(PluginManager::SetSystemRuntimePluginEnabled("b", false));
+
+  // The call to re-enable the plugin should succeed.
+  ASSERT_TRUE(PluginManager::SetSystemRuntimePluginEnabled("b", true));
+
+  // The second call should also succeed since the plugin is already enabled.
+  ASSERT_TRUE(PluginManager::SetSystemRuntimePluginEnabled("b", true));
+}
+
+// Test calling disable on an already disabled plugin is ok.
+TEST_F(PluginManagerTest, SystemRuntimePluginDisableNonExistent) {
+  RegisterMockSystemRuntimePlugins();
+
+  // Both enable and disable should return false for a non-existent plugin.
+  ASSERT_FALSE(
+      PluginManager::SetSystemRuntimePluginEnabled("does_not_exist", true));
+  ASSERT_FALSE(
+      PluginManager::SetSystemRuntimePluginEnabled("does_not_exist", false));
+}
+
+// Test disabling all plugins and then re-enabling them in a different
+// order will restore the original plugin order.
+TEST_F(PluginManagerTest, SystemRuntimePluginDisableAll) {
+  RegisterMockSystemRuntimePlugins();
+
+  // Validate initial state of registered plugins.
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(0),
+            CreateSystemRuntimePluginA);
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(1),
+            CreateSystemRuntimePluginB);
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(2),
+            CreateSystemRuntimePluginC);
+
+  // Disable all the active plugins.
+  ASSERT_TRUE(PluginManager::SetSystemRuntimePluginEnabled("a", false));
+  ASSERT_TRUE(PluginManager::SetSystemRuntimePluginEnabled("b", false));
+  ASSERT_TRUE(PluginManager::SetSystemRuntimePluginEnabled("c", false));
+
+  // Should have no active plugins.
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(0), nullptr);
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(1), nullptr);
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(2), nullptr);
+
+  // And show up in the plugin info correctly.
+  std::vector<RegisteredPluginInfo> plugin_info =
+      PluginManager::GetSystemRuntimePluginInfo();
+  ASSERT_EQ(plugin_info.size(), 3u);
+  ASSERT_EQ(plugin_info[0].name, "a");
+  ASSERT_EQ(plugin_info[0].enabled, false);
+  ASSERT_EQ(plugin_info[1].name, "b");
+  ASSERT_EQ(plugin_info[1].enabled, false);
+  ASSERT_EQ(plugin_info[2].name, "c");
+  ASSERT_EQ(plugin_info[2].enabled, false);
+
+  // Enable plugins in reverse order and validate expected indicies.
+  // They should show up in the original plugin order.
+  ASSERT_TRUE(PluginManager::SetSystemRuntimePluginEnabled("c", true));
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(0),
+            CreateSystemRuntimePluginC);
+
+  ASSERT_TRUE(PluginManager::SetSystemRuntimePluginEnabled("a", true));
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(0),
+            CreateSystemRuntimePluginA);
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(1),
+            CreateSystemRuntimePluginC);
+
+  ASSERT_TRUE(PluginManager::SetSystemRuntimePluginEnabled("b", true));
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(0),
+            CreateSystemRuntimePluginA);
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(1),
+            CreateSystemRuntimePluginB);
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(2),
+            CreateSystemRuntimePluginC);
+}
+
+// Test un-registering a disabled plugin works.
+TEST_F(PluginManagerTest, UnRegisterDisabledSystemRuntimePlugin) {
+  RegisterMockSystemRuntimePlugins();
+
+  // Initial plugin info has all three registered plugins.
+  std::vector<RegisteredPluginInfo> plugin_info =
+      PluginManager::GetSystemRuntimePluginInfo();
+  ASSERT_EQ(plugin_info.size(), 3u);
+
+  // First disable a plugin, then unregister it. Both should succeed.
+  ASSERT_TRUE(PluginManager::SetSystemRuntimePluginEnabled("b", false));
+  ASSERT_TRUE(PluginManager::UnregisterPlugin(CreateSystemRuntimePluginB));
+
+  // After un-registering a plugin it should be removed from plugin info.
+  plugin_info = PluginManager::GetSystemRuntimePluginInfo();
+  ASSERT_EQ(plugin_info.size(), 2u);
+  ASSERT_EQ(plugin_info[0].name, "a");
+  ASSERT_EQ(plugin_info[0].enabled, true);
+  ASSERT_EQ(plugin_info[1].name, "c");
+  ASSERT_EQ(plugin_info[1].enabled, true);
+}
+
+// Test un-registering and then re-registering a plugin will change the order of
+// loaded plugins.
+TEST_F(PluginManagerTest, UnRegisterSystemRuntimePluginChangesOrder) {
+  RegisterMockSystemRuntimePlugins();
+
+  std::vector<RegisteredPluginInfo> plugin_info =
+      PluginManager::GetSystemRuntimePluginInfo();
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(0),
+            CreateSystemRuntimePluginA);
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(1),
+            CreateSystemRuntimePluginB);
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(2),
+            CreateSystemRuntimePluginC);
+
+  ASSERT_EQ(plugin_info.size(), 3u);
+  ASSERT_EQ(plugin_info[0].name, "a");
+  ASSERT_EQ(plugin_info[1].name, "b");
+  ASSERT_EQ(plugin_info[2].name, "c");
+
+  // Unregister and then registering a plugin puts it at the end of the order
+  // list.
+  ASSERT_TRUE(PluginManager::UnregisterPlugin(CreateSystemRuntimePluginB));
+  ASSERT_TRUE(PluginManager::RegisterPlugin("b", "New test instance B",
+                                            CreateSystemRuntimePluginB));
+
+  // Check the callback indices match as expected.
+  plugin_info = PluginManager::GetSystemRuntimePluginInfo();
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(0),
+            CreateSystemRuntimePluginA);
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(1),
+            CreateSystemRuntimePluginC);
+  ASSERT_EQ(PluginManager::GetSystemRuntimeCreateCallbackAtIndex(2),
+            CreateSystemRuntimePluginB);
+
+  // And plugin info should match as well.
+  ASSERT_EQ(plugin_info.size(), 3u);
+ ...
[truncated]

@dmpots
Copy link
Contributor Author

dmpots commented Mar 31, 2025

This PR modifies the PluginManager to support enable/disable. A following commit will expose the commands to the user.

This is currently only working for the SystemRuntime plugins. Once these first commits land I plan to do the same for the other plugin types.

Copy link
Member

@JDevlieghere JDevlieghere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change LGTM.

Are you able to share the motivation behind wanting to enable/disable plugins at runtime?

I added support for disabling plugins at configuration/build time and at the time @labath had concerns about that as various parts of LLDB depend on certain plugins. Do you plan on making this opt-in for plugins or will users be able to disable arbitrary plugins at runtime?

@dmpots
Copy link
Contributor Author

dmpots commented Apr 2, 2025

Are you able to share the motivation behind wanting to enable/disable plugins at runtime?

The motivation is to enable easier testing and experimentation.

It came from our attempts to try lazy symbol loading (i.e. settings set target.preload-symbols false). We found that even with preload-symbols disabled there were various things that would trigger symbol loading unconditionally.

We fixed a source of symbol loading in #129593, but found that there were several plugins that would force load all symbols. Some of these plugins (e.g. ASAN) are loaded unconditionally with no way to disable them. This not only makes it hard to test, but also harder to do one-off experiments without modifying the code and rebuilding.

After discussing with @clayborg we thought a generic mechanism to disable plugins would be useful and could help with things beyond our original motivation.

We also have some internal patches to disable some of these plugins, but if we have a generic way to do it then we can add it to our custom lldb init. This should reduce the number of internal diffs we have.

One last idea I should mention is that we thought we could also extend this mechanism to support reordering of plugins. So that, for example, when running on linux we could reorder the ObjectFile plugins to have the Elf plugin come first. Just an idea at this point, but something we might explore in the future.

I added support for disabling plugins at configuration/build time and at the time @labath had concerns about that as various parts of LLDB depend on certain plugins. Do you plan on making this opt-in for plugins or will users be able to disable arbitrary plugins at runtime?

The plan is to allow disabling arbitrary plugins at runtime. We could have an opt-in or opt-out mechanism, but my feeling is that giving the user full control is not too bad. Most end users will not touch it and lldb developers can manage their self-imposed difficulties.

@dmpots dmpots merged commit 2026873 into llvm:main Apr 2, 2025
12 checks passed
@dmpots dmpots deleted the plugin-enable-disable branch April 2, 2025 20:15
@llvm-ci
Copy link
Collaborator

llvm-ci commented Apr 2, 2025

LLVM Buildbot has detected a new failure on builder lldb-aarch64-ubuntu running on linaro-lldb-aarch64-ubuntu while building lldb at step 6 "test".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/59/builds/15347

Here is the relevant piece of the build log for the reference
Step 6 (test) failure: build (failure)
...
UNSUPPORTED: lldb-api :: tools/lldb-dap/runInTerminal/TestDAP_runInTerminal.py (1170 of 2115)
UNSUPPORTED: lldb-api :: tools/lldb-dap/save-core/TestDAP_save_core.py (1171 of 2115)
PASS: lldb-api :: tools/lldb-dap/send-event/TestDAP_sendEvent.py (1172 of 2115)
PASS: lldb-api :: tools/lldb-dap/restart/TestDAP_restart.py (1173 of 2115)
PASS: lldb-api :: tools/lldb-dap/launch/TestDAP_launch.py (1174 of 2115)
PASS: lldb-api :: tools/lldb-dap/source/TestDAP_source.py (1175 of 2115)
UNSUPPORTED: lldb-api :: tools/lldb-dap/stackTrace/subtleFrames/TestDAP_subtleFrames.py (1176 of 2115)
PASS: lldb-api :: tools/lldb-dap/stackTrace/TestDAP_stackTrace.py (1177 of 2115)
PASS: lldb-api :: tools/lldb-dap/stackTraceMissingFunctionName/TestDAP_stackTraceMissingFunctionName.py (1178 of 2115)
PASS: lldb-api :: tools/lldb-dap/startDebugging/TestDAP_startDebugging.py (1179 of 2115)
FAIL: lldb-api :: tools/lldb-dap/progress/TestDAP_Progress.py (1180 of 2115)
******************** TEST 'lldb-api :: tools/lldb-dap/progress/TestDAP_Progress.py' FAILED ********************
Script:
--
/usr/bin/python3.10 /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/llvm-project/lldb/test/API/dotest.py -u CXXFLAGS -u CFLAGS --env LLVM_LIBS_DIR=/home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/./lib --env LLVM_INCLUDE_DIR=/home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/include --env LLVM_TOOLS_DIR=/home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/./bin --arch aarch64 --build-dir /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/lldb-test-build.noindex --lldb-module-cache-dir /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/lldb-test-build.noindex/module-cache-lldb/lldb-api --clang-module-cache-dir /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/lldb-test-build.noindex/module-cache-clang/lldb-api --executable /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/./bin/lldb --compiler /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/./bin/clang --dsymutil /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/./bin/dsymutil --make /usr/bin/gmake --llvm-tools-dir /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/./bin --lldb-obj-root /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/tools/lldb --lldb-libs-dir /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/./lib /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/llvm-project/lldb/test/API/tools/lldb-dap/progress -p TestDAP_Progress.py
--
Exit Code: 1

Command Output (stdout):
--
lldb version 21.0.0git (https://github.com/llvm/llvm-project.git revision 2026873fb8a1f654aa920cd5ea8074e55053973b)
  clang revision 2026873fb8a1f654aa920cd5ea8074e55053973b
  llvm revision 2026873fb8a1f654aa920cd5ea8074e55053973b
Skipping the following test categories: ['libc++', 'dsym', 'gmodules', 'debugserver', 'objc']

--
Command Output (stderr):
--
========= DEBUG ADAPTER PROTOCOL LOGS =========
1743625926.648605585 --> (stdin/stdout) {"command":"initialize","type":"request","arguments":{"adapterID":"lldb-native","clientID":"vscode","columnsStartAt1":true,"linesStartAt1":true,"locale":"en-us","pathFormat":"path","supportsRunInTerminalRequest":true,"supportsVariablePaging":true,"supportsVariableType":true,"supportsStartDebuggingRequest":true,"supportsProgressReporting":true,"$__lldb_sourceInitFile":false},"seq":1}
1743625926.650660038 <-- (stdin/stdout) {"body":{"$__lldb_version":"lldb version 21.0.0git (https://github.com/llvm/llvm-project.git revision 2026873fb8a1f654aa920cd5ea8074e55053973b)\n  clang revision 2026873fb8a1f654aa920cd5ea8074e55053973b\n  llvm revision 2026873fb8a1f654aa920cd5ea8074e55053973b","completionTriggerCharacters":["."," ","\t"],"exceptionBreakpointFilters":[{"default":false,"filter":"cpp_catch","label":"C++ Catch"},{"default":false,"filter":"cpp_throw","label":"C++ Throw"},{"default":false,"filter":"objc_catch","label":"Objective-C Catch"},{"default":false,"filter":"objc_throw","label":"Objective-C Throw"}],"supportTerminateDebuggee":true,"supportsBreakpointLocationsRequest":true,"supportsCompletionsRequest":true,"supportsConditionalBreakpoints":true,"supportsConfigurationDoneRequest":true,"supportsDataBreakpoints":true,"supportsDelayedStackTraceLoading":true,"supportsDisassembleRequest":true,"supportsEvaluateForHovers":true,"supportsExceptionInfoRequest":true,"supportsExceptionOptions":true,"supportsFunctionBreakpoints":true,"supportsHitConditionalBreakpoints":true,"supportsInstructionBreakpoints":true,"supportsLogPoints":true,"supportsModulesRequest":true,"supportsReadMemoryRequest":true,"supportsRestartRequest":true,"supportsSetVariable":true,"supportsStepInTargetsRequest":true,"supportsSteppingGranularity":true,"supportsValueFormattingOptions":true},"command":"initialize","request_seq":1,"seq":0,"success":true,"type":"response"}
1743625926.650849581 --> (stdin/stdout) {"command":"launch","type":"request","arguments":{"program":"/home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/lldb-test-build.noindex/tools/lldb-dap/progress/TestDAP_Progress.test/a.out","initCommands":["settings clear -all","settings set symbols.enable-external-lookup false","settings set target.inherit-tcc true","settings set target.disable-aslr false","settings set target.detach-on-error false","settings set target.auto-apply-fixits false","settings set plugin.process.gdb-remote.packet-timeout 60","settings set symbols.clang-modules-cache-path \"/home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/lldb-test-build.noindex/module-cache-lldb/lldb-api\"","settings set use-color false","settings set show-statusline false"],"disableASLR":false,"enableAutoVariableSummaries":false,"enableSyntheticChildDebugging":false,"displayExtendedBacktrace":false,"commandEscapePrefix":null},"seq":2}
1743625926.651052475 <-- (stdin/stdout) {"body":{"category":"console","output":"Running initCommands:\n"},"event":"output","seq":0,"type":"event"}
1743625926.651071787 <-- (stdin/stdout) {"body":{"category":"console","output":"(lldb) settings clear -all\n"},"event":"output","seq":0,"type":"event"}
1743625926.651081800 <-- (stdin/stdout) {"body":{"category":"console","output":"(lldb) settings set symbols.enable-external-lookup false\n"},"event":"output","seq":0,"type":"event"}
1743625926.651091814 <-- (stdin/stdout) {"body":{"category":"console","output":"(lldb) settings set target.inherit-tcc true\n"},"event":"output","seq":0,"type":"event"}
1743625926.651100636 <-- (stdin/stdout) {"body":{"category":"console","output":"(lldb) settings set target.disable-aslr false\n"},"event":"output","seq":0,"type":"event"}
1743625926.651108503 <-- (stdin/stdout) {"body":{"category":"console","output":"(lldb) settings set target.detach-on-error false\n"},"event":"output","seq":0,"type":"event"}
1743625926.651116610 <-- (stdin/stdout) {"body":{"category":"console","output":"(lldb) settings set target.auto-apply-fixits false\n"},"event":"output","seq":0,"type":"event"}
1743625926.651124477 <-- (stdin/stdout) {"body":{"category":"console","output":"(lldb) settings set plugin.process.gdb-remote.packet-timeout 60\n"},"event":"output","seq":0,"type":"event"}
1743625926.651146173 <-- (stdin/stdout) {"body":{"category":"console","output":"(lldb) settings set symbols.clang-modules-cache-path \"/home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/lldb-test-build.noindex/module-cache-lldb/lldb-api\"\n"},"event":"output","seq":0,"type":"event"}
1743625926.651155710 <-- (stdin/stdout) {"body":{"category":"console","output":"(lldb) settings set use-color false\n"},"event":"output","seq":0,"type":"event"}
1743625926.651163340 <-- (stdin/stdout) {"body":{"category":"console","output":"(lldb) settings set show-statusline false\n"},"event":"output","seq":0,"type":"event"}
1743625926.728641033 <-- (stdin/stdout) {"command":"launch","request_seq":2,"seq":0,"success":true,"type":"response"}
1743625926.728751898 <-- (stdin/stdout) {"body":{"isLocalProcess":true,"name":"/home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/lldb-test-build.noindex/tools/lldb-dap/progress/TestDAP_Progress.test/a.out","startMethod":"launch","systemProcessId":367788},"event":"process","seq":0,"type":"event"}
1743625926.728763103 <-- (stdin/stdout) {"event":"initialized","seq":0,"type":"event"}
1743625926.728963137 --> (stdin/stdout) {"command":"threads","type":"request","arguments":{},"seq":3}
1743625926.729055405 <-- (stdin/stdout) {"body":{"threads":[{"id":367788,"name":"a.out"}]},"command":"threads","request_seq":3,"seq":0,"success":true,"type":"response"}
1743625926.729110718 --> (stdin/stdout) {"command":"stackTrace","type":"request","arguments":{"threadId":367788,"startFrame":0,"levels":1},"seq":4}

@dmpots
Copy link
Contributor Author

dmpots commented Apr 2, 2025

LLVM Buildbot has detected a new failure on builder lldb-aarch64-ubuntu running on linaro-lldb-aarch64-ubuntu while building lldb at step 6 "test".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/59/builds/15347

Here is the relevant piece of the build log for the reference

I suspect this is related to #134048, which is also seeing the failure.

@llvm-ci
Copy link
Collaborator

llvm-ci commented Apr 2, 2025

LLVM Buildbot has detected a new failure on builder lldb-remote-linux-win running on as-builder-10 while building lldb at step 16 "test-check-lldb-unit".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/197/builds/3500

Here is the relevant piece of the build log for the reference
Step 16 (test-check-lldb-unit) failure: Test just built components: check-lldb-unit completed (failure)
******************** TEST 'lldb-unit :: Core/./LLDBCoreTests.exe/45/75' FAILED ********************
Script(shard):
--
GTEST_OUTPUT=json:C:\buildbot\as-builder-10\lldb-x-aarch64\build\tools\lldb\unittests\Core\.\LLDBCoreTests.exe-lldb-unit-14416-45-75.json GTEST_SHUFFLE=0 GTEST_TOTAL_SHARDS=75 GTEST_SHARD_INDEX=45 C:\buildbot\as-builder-10\lldb-x-aarch64\build\tools\lldb\unittests\Core\.\LLDBCoreTests.exe
--

Script:
--
C:\buildbot\as-builder-10\lldb-x-aarch64\build\tools\lldb\unittests\Core\.\LLDBCoreTests.exe --gtest_filter=PluginManagerTest.UnRegisterSystemRuntimePluginInfo
--
C:\buildbot\as-builder-10\lldb-x-aarch64\llvm-project\lldb\unittests\Core\PluginManagerTest.cpp(134): error: Expected equality of these values:
  plugin_info[0].name
    Which is: "b"
  "a"
    Which is: 00007FF65F8EB2A0


C:\buildbot\as-builder-10\lldb-x-aarch64\llvm-project\lldb\unittests\Core\PluginManagerTest.cpp:134
Expected equality of these values:
  plugin_info[0].name
    Which is: "b"
  "a"
    Which is: 00007FF65F8EB2A0



********************


@llvm-ci
Copy link
Collaborator

llvm-ci commented Apr 2, 2025

LLVM Buildbot has detected a new failure on builder lldb-aarch64-windows running on linaro-armv8-windows-msvc-05 while building lldb at step 6 "test".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/141/builds/7555

Here is the relevant piece of the build log for the reference
Step 6 (test) failure: build (failure)
...
UNSUPPORTED: lldb-shell :: Watchpoint/ExpressionLanguage.test (1831 of 2107)
UNSUPPORTED: lldb-shell :: Watchpoint/LocalVariableWatchpointDisabler.test (1832 of 2107)
PASS: lldb-shell :: Watchpoint/SetErrorCases.test (1833 of 2107)
UNSUPPORTED: lldb-shell :: Watchpoint/netbsd-nouserdbregs.test (1834 of 2107)
PASS: lldb-shell :: lldb-server/TestErrorMessages.test (1835 of 2107)
PASS: lldb-unit :: Breakpoint/./LLDBBreakpointTests.exe/0/2 (1836 of 2107)
PASS: lldb-unit :: Breakpoint/./LLDBBreakpointTests.exe/1/2 (1837 of 2107)
PASS: lldb-unit :: Callback/./LLDBCallbackTests.exe/0/1 (1838 of 2107)
PASS: lldb-shell :: Target/dependent-modules-nodupe-windows.test (1839 of 2107)
PASS: lldb-unit :: Core/./LLDBCoreTests.exe/0/10 (1840 of 2107)
FAIL: lldb-unit :: Core/./LLDBCoreTests.exe/1/10 (1841 of 2107)
******************** TEST 'lldb-unit :: Core/./LLDBCoreTests.exe/1/10' FAILED ********************
Script(shard):
--
GTEST_OUTPUT=json:C:\Users\tcwg\llvm-worker\lldb-aarch64-windows\build\tools\lldb\unittests\Core\.\LLDBCoreTests.exe-lldb-unit-5140-1-10.json GTEST_SHUFFLE=0 GTEST_TOTAL_SHARDS=10 GTEST_SHARD_INDEX=1 C:\Users\tcwg\llvm-worker\lldb-aarch64-windows\build\tools\lldb\unittests\Core\.\LLDBCoreTests.exe
--

Script:
--
C:\Users\tcwg\llvm-worker\lldb-aarch64-windows\build\tools\lldb\unittests\Core\.\LLDBCoreTests.exe --gtest_filter=PluginManagerTest.UnRegisterDisabledSystemRuntimePlugin
--
C:\Users\tcwg\llvm-worker\lldb-aarch64-windows\llvm-project\lldb\unittests\Core\PluginManagerTest.cpp(302): error: Expected equality of these values:
  plugin_info[0].name
    Which is: "b"
  "a"
    Which is: 00007FF72D1A1E20


C:\Users\tcwg\llvm-worker\lldb-aarch64-windows\llvm-project\lldb\unittests\Core\PluginManagerTest.cpp:302
Expected equality of these values:
  plugin_info[0].name
    Which is: "b"
  "a"
    Which is: 00007FF72D1A1E20



********************
FAIL: lldb-unit :: Core/./LLDBCoreTests.exe/2/10 (1842 of 2107)
******************** TEST 'lldb-unit :: Core/./LLDBCoreTests.exe/2/10' FAILED ********************
Script(shard):
--
GTEST_OUTPUT=json:C:\Users\tcwg\llvm-worker\lldb-aarch64-windows\build\tools\lldb\unittests\Core\.\LLDBCoreTests.exe-lldb-unit-5140-2-10.json GTEST_SHUFFLE=0 GTEST_TOTAL_SHARDS=10 GTEST_SHARD_INDEX=2 C:\Users\tcwg\llvm-worker\lldb-aarch64-windows\build\tools\lldb\unittests\Core\.\LLDBCoreTests.exe
--

Script:
--
C:\Users\tcwg\llvm-worker\lldb-aarch64-windows\build\tools\lldb\unittests\Core\.\LLDBCoreTests.exe --gtest_filter=PluginManagerTest.UnRegisterSystemRuntimePluginChangesOrder
--

dmpots added a commit to dmpots/llvm-project that referenced this pull request Apr 2, 2025
This is an attempt to fix a test failure from llvm#133794 when running on
windows builds. I suspect we are running into a case where the
[ICF](https://learn.microsoft.com/en-us/cpp/build/reference/opt-optimizations?view=msvc-170)
optimization kicks in and combines the CreateSystemRuntimePlugin*
functions into a single address. This means that we cannot uniquely
unregister the plugin based on its create function address.

The fix is have each create function return a different (bogus)
value.
@dmpots
Copy link
Contributor Author

dmpots commented Apr 2, 2025

I have an attempted fix for these failures in #134173. I suspect ICF is combining the create functions into a single address which make us unable to remove them based on a unique address (which the plugin manager assumes).

dmpots added a commit that referenced this pull request Apr 3, 2025
This is an attempt to fix a test failure from #133794 when running on
windows builds. I suspect we are running into a case where the
[ICF](https://learn.microsoft.com/en-us/cpp/build/reference/opt-optimizations?view=msvc-170)
optimization kicks in and combines the CreateSystemRuntimePlugin*
functions into a single address. This means that we cannot uniquely
unregister the plugin based on its create function address.

The fix is have each create function return a different (bogus) value.
dmpots added a commit to dmpots/llvm-project that referenced this pull request Apr 3, 2025
dmpots added a commit to dmpots/llvm-project that referenced this pull request Apr 3, 2025
Revert the below two commits while we investigate the test failures.

Revert "[lldb] Fix plugin manager test failure on windows (llvm#134173)"

This reverts commit b55bab2.

Revert "Add enable/disable api for SystemRuntime plugins (llvm#133794)"

This reverts commit 2026873.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants