@@ -57,7 +57,7 @@ enum class LoadPolicy {
5757
5858// Plugin metadata + dependency list — used for discovery and topological sort.
5959// Public so tests and hosts can construct instances for PluginManager::add_plugin().
60- struct PluginInfo {
60+ struct DiscoveredPlugin {
6161 PluginEntry entry;
6262 std::vector<std::string> deps; // type strings (version stripped, for topo sort)
6363 std::vector<std::string> version_constraints; // original strings (for version checking)
@@ -69,7 +69,7 @@ struct PluginInfo {
6969// Throws on cycles or missing dependencies.
7070// Free function so it can be tested independently.
7171inline std::vector<std::vector<std::size_t >> topological_sort_levels (
72- const std::vector<PluginInfo >& infos,
72+ const std::vector<DiscoveredPlugin >& infos,
7373 const std::unordered_map<std::string, std::size_t >& type_to_index) {
7474 const std::size_t n = infos.size ();
7575
@@ -192,7 +192,7 @@ inline std::vector<std::vector<std::size_t>> topological_sort_levels(
192192// Check version constraints between discovered plugins.
193193// Throws in strict mode, records errors in best_effort mode.
194194inline void check_version_constraints (
195- const std::vector<PluginInfo >& infos,
195+ const std::vector<DiscoveredPlugin >& infos,
196196 const std::unordered_map<std::string, std::size_t >& type_to_index,
197197 LoadPolicy policy,
198198 std::vector<ErrorRecord>* errors_out) {
@@ -233,7 +233,7 @@ inline void check_version_constraints(
233233// Check mutual exclusion constraints between discovered plugins.
234234// Throws in strict mode, records errors in best_effort mode.
235235inline void check_conflict_constraints (
236- const std::vector<PluginInfo >& infos,
236+ const std::vector<DiscoveredPlugin >& infos,
237237 const std::unordered_map<std::string, std::size_t >& type_to_index,
238238 LoadPolicy policy,
239239 std::vector<ErrorRecord>* errors_out) {
@@ -305,7 +305,7 @@ inline void apply_config_defaults(IPlugin* instance, PluginConfig& config) {
305305class PluginManager {
306306 public:
307307 PluginManager () = default ;
308- ~PluginManager () = default ;
308+ virtual ~PluginManager () = default ;
309309
310310 PluginManager (const PluginManager&) = delete ;
311311 PluginManager& operator =(const PluginManager&) = delete ;
@@ -568,7 +568,7 @@ class PluginManager {
568568 {std::move (instance), std::nullopt , entry, std::move (deps)});
569569 rebuild_name_index ();
570570 wire_instance_tracked (plugins_.back (), config_map, /* skip_validation=*/ true );
571- notify_loaded (entry.name , entry.type );
571+ notify_loaded (entry.name , entry.type , entry );
572572
573573 }
574574
@@ -593,12 +593,12 @@ class PluginManager {
593593 // Shutdown reverse dependents
594594 for (std::size_t idx : dep_indices) {
595595 shutdown_plugin (plugins_[idx]);
596- notify_unloaded (plugins_[idx].entry .name , plugins_[idx].entry .type );
596+ notify_unloaded (plugins_[idx].entry .name , plugins_[idx].entry .type , plugins_[idx]. entry );
597597 }
598598
599599 // Shutdown the target
600600 shutdown_plugin (plugins_[it->second ]);
601- notify_unloaded (name, target_type);
601+ notify_unloaded (name, target_type, plugins_[it-> second ]. entry );
602602
603603 // Collect all indices to remove
604604 std::unordered_set<std::size_t > to_remove (dep_indices.begin (),
@@ -688,7 +688,7 @@ class PluginManager {
688688 wire_instance_tracked (plugins_[idx], config_map);
689689 }
690690
691- notify_reloaded (name, target_type);
691+ notify_reloaded (name, target_type, target. entry );
692692
693693 }
694694
@@ -754,7 +754,7 @@ class PluginManager {
754754 lp.enabled = true ;
755755 rebuild_locator ();
756756 wire_instance_tracked (lp, config_map);
757- notify_enabled (lp.entry .name , lp.entry .type );
757+ notify_enabled (lp.entry .name , lp.entry .type , lp. entry );
758758 }
759759
760760 // Check if a plugin is enabled.
@@ -961,20 +961,20 @@ class PluginManager {
961961
962962 // --- Observer notification ---
963963
964- void notify_loaded (const std::string& name, const std::string& type) {
965- for (auto * obs : observers_) { try { obs->on_plugin_loaded (name, type); } catch (...) {} }
964+ void notify_loaded (const std::string& name, const std::string& type, const PluginEntry& entry ) {
965+ for (auto * obs : observers_) { try { obs->on_plugin_loaded (name, type, entry ); } catch (...) {} }
966966 }
967- void notify_unloaded (const std::string& name, const std::string& type) {
968- for (auto * obs : observers_) { try { obs->on_plugin_unloaded (name, type); } catch (...) {} }
967+ void notify_unloaded (const std::string& name, const std::string& type, const PluginEntry& entry ) {
968+ for (auto * obs : observers_) { try { obs->on_plugin_unloaded (name, type, entry ); } catch (...) {} }
969969 }
970- void notify_reloaded (const std::string& name, const std::string& type) {
971- for (auto * obs : observers_) { try { obs->on_plugin_reloaded (name, type); } catch (...) {} }
970+ void notify_reloaded (const std::string& name, const std::string& type, const PluginEntry& entry ) {
971+ for (auto * obs : observers_) { try { obs->on_plugin_reloaded (name, type, entry ); } catch (...) {} }
972972 }
973- void notify_enabled (const std::string& name, const std::string& type) {
974- for (auto * obs : observers_) { try { obs->on_plugin_enabled (name, type); } catch (...) {} }
973+ void notify_enabled (const std::string& name, const std::string& type, const PluginEntry& entry ) {
974+ for (auto * obs : observers_) { try { obs->on_plugin_enabled (name, type, entry ); } catch (...) {} }
975975 }
976- void notify_disabled (const std::string& name, const std::string& type) {
977- for (auto * obs : observers_) { try { obs->on_plugin_disabled (name, type); } catch (...) {} }
976+ void notify_disabled (const std::string& name, const std::string& type, const PluginEntry& entry ) {
977+ for (auto * obs : observers_) { try { obs->on_plugin_disabled (name, type, entry ); } catch (...) {} }
978978 }
979979
980980 // --- Name index ---
@@ -1008,7 +1008,7 @@ class PluginManager {
10081008 }
10091009 lp.subscription_ids .clear ();
10101010 lp.enabled = false ;
1011- notify_disabled (lp.entry .name , lp.entry .type );
1011+ notify_disabled (lp.entry .name , lp.entry .type , lp. entry );
10121012 }
10131013
10141014 // --- Reverse dependency graph ---
@@ -1048,25 +1048,10 @@ class PluginManager {
10481048 return result;
10491049 }
10501050
1051- // --- Plugin shutdown ---
1052-
1053- // Calls on_shutdown() if the plugin implements ILifecycleAware.
1054- // Swallows exceptions — one bad plugin must not prevent others from
1055- // shutting down (same principle as EventBus::publish).
1056- static void shutdown_plugin (LoadedPlugin& lp) {
1057- auto * lifecycle = dynamic_cast <ILifecycleAware*>(lp.instance .get ());
1058- if (lifecycle) {
1059- try {
1060- lifecycle->on_shutdown ();
1061- } catch (...) {
1062- }
1063- }
1064- }
1065-
10661051 // --- Discovery ---
10671052
10681053 struct DiscoveryResult {
1069- std::vector<PluginInfo > infos;
1054+ std::vector<DiscoveredPlugin > infos;
10701055 std::vector<std::vector<std::size_t >> levels;
10711056 };
10721057
@@ -1077,11 +1062,11 @@ class PluginManager {
10771062 const auto & entries = registry.entries ();
10781063 if (entries.empty ()) return {};
10791064
1080- std::vector<PluginInfo > infos;
1065+ std::vector<DiscoveredPlugin > infos;
10811066 std::unordered_map<std::string, std::size_t > type_to_index;
10821067
10831068 for (const auto & e : entries) {
1084- PluginInfo info;
1069+ DiscoveredPlugin info;
10851070 info.entry = e;
10861071
10871072 // Probe for dependencies and conflicts (only typed plugins)
@@ -1134,15 +1119,15 @@ class PluginManager {
11341119 // --- Dependency cascade ---
11351120
11361121 static bool has_failed_dep (
1137- const PluginInfo & info,
1122+ const DiscoveredPlugin & info,
11381123 const std::unordered_set<std::string>& failed_types) {
11391124 for (const auto & dep : info.deps ) {
11401125 if (failed_types.contains (dep)) return true ;
11411126 }
11421127 return false ;
11431128 }
11441129
1145- void record_cascade_skip (const PluginInfo & info,
1130+ void record_cascade_skip (const DiscoveredPlugin & info,
11461131 std::unordered_set<std::string>& failed_types) {
11471132 failed_types.insert (info.entry .type );
11481133 load_errors_.push_back (
@@ -1167,29 +1152,6 @@ class PluginManager {
11671152 }
11681153 }
11691154
1170- void load_and_wire (const PluginEntry& entry,
1171- const std::vector<std::string>& deps,
1172- const ConfigMap& config_map) {
1173- std::shared_ptr<IPlugin> instance;
1174- std::optional<PluginLoader<IPlugin>> loader;
1175-
1176- if (entry.is_dynamic ) {
1177- instance = DynamicPluginAdapter::load (entry.path .string ());
1178- } else {
1179- PluginLoader<IPlugin> pl (entry.path .string ());
1180- instance = pl.get_instance ();
1181- loader = std::move (pl);
1182- }
1183-
1184- locator_.add (instance);
1185- plugins_.push_back (
1186- {std::move (instance), std::move (loader), entry, deps});
1187- rebuild_name_index ();
1188- wire_instance_tracked (plugins_.back (), config_map);
1189- notify_loaded (entry.name , entry.type );
1190-
1191- }
1192-
11931155 // Resolve, validate, and apply config for a plugin.
11941156 // Returns the final config if the plugin is configurable, or nullopt.
11951157 static std::optional<PluginConfig> resolve_plugin_config (
@@ -1219,9 +1181,33 @@ class PluginManager {
12191181 return config;
12201182 }
12211183
1184+ // Wire + track EventBus subscription IDs (for enable/disable).
1185+ //
1186+ // Relies on EventBus::next_id_ being monotonically increasing with no
1187+ // reuse: all IDs allocated between [before, after) belong to this plugin.
1188+ // This holds because PluginManager is single-threaded (or exclusively
1189+ // locked via ThreadSafe), and wire_instance may call on_init() /
1190+ // set_event_bus() which subscribe — those subscriptions correctly belong
1191+ // to this plugin. Unsubscribes during wiring don't break the invariant
1192+ // because IDs are never recycled.
1193+ void wire_instance_tracked (LoadedPlugin& lp, const ConfigMap& config_map,
1194+ bool skip_validation = false ) {
1195+ auto before = event_bus_.next_subscription_id ();
1196+ wire_instance (lp.instance , lp.entry , config_map, skip_validation);
1197+ auto after = event_bus_.next_subscription_id ();
1198+
1199+ lp.subscription_ids .clear ();
1200+ for (auto id = before; id < after; ++id) {
1201+ lp.subscription_ids .push_back (id);
1202+ }
1203+ }
1204+
1205+ protected:
1206+ // --- Extensibility points (override in subclasses) ---
1207+
12221208 // Wire all opt-in mixins on an instance.
12231209 // Order: validate+configure → inject services → inject events → custom wirers → init.
1224- void wire_instance (const std::shared_ptr<IPlugin>& instance,
1210+ virtual void wire_instance (const std::shared_ptr<IPlugin>& instance,
12251211 const PluginEntry& entry, const ConfigMap& config_map,
12261212 bool skip_validation = false ) {
12271213 auto resolved = resolve_plugin_config (
@@ -1252,25 +1238,40 @@ class PluginManager {
12521238 }
12531239 }
12541240
1255- // Wire + track EventBus subscription IDs (for enable/disable).
1256- //
1257- // Relies on EventBus::next_id_ being monotonically increasing with no
1258- // reuse: all IDs allocated between [before, after) belong to this plugin.
1259- // This holds because PluginManager is single-threaded (or exclusively
1260- // locked via ThreadSafe), and wire_instance may call on_init() /
1261- // set_event_bus() which subscribe — those subscriptions correctly belong
1262- // to this plugin. Unsubscribes during wiring don't break the invariant
1263- // because IDs are never recycled.
1264- void wire_instance_tracked (LoadedPlugin& lp, const ConfigMap& config_map,
1265- bool skip_validation = false ) {
1266- auto before = event_bus_.next_subscription_id ();
1267- wire_instance (lp.instance , lp.entry , config_map, skip_validation);
1268- auto after = event_bus_.next_subscription_id ();
1241+ // Calls on_shutdown() if the plugin implements ILifecycleAware.
1242+ // Swallows exceptions — one bad plugin must not prevent others from
1243+ // shutting down (same principle as EventBus::publish).
1244+ virtual void shutdown_plugin (LoadedPlugin& lp) {
1245+ auto * lifecycle = dynamic_cast <ILifecycleAware*>(lp.instance .get ());
1246+ if (lifecycle) {
1247+ try {
1248+ lifecycle->on_shutdown ();
1249+ } catch (...) {
1250+ }
1251+ }
1252+ }
12691253
1270- lp.subscription_ids .clear ();
1271- for (auto id = before; id < after; ++id) {
1272- lp.subscription_ids .push_back (id);
1254+ // Load a plugin from disk and wire it into the manager.
1255+ virtual void load_and_wire (const PluginEntry& entry,
1256+ const std::vector<std::string>& deps,
1257+ const ConfigMap& config_map) {
1258+ std::shared_ptr<IPlugin> instance;
1259+ std::optional<PluginLoader<IPlugin>> loader;
1260+
1261+ if (entry.is_dynamic ) {
1262+ instance = DynamicPluginAdapter::load (entry.path .string ());
1263+ } else {
1264+ PluginLoader<IPlugin> pl (entry.path .string ());
1265+ instance = pl.get_instance ();
1266+ loader = std::move (pl);
12731267 }
1268+
1269+ locator_.add (instance);
1270+ plugins_.push_back (
1271+ {std::move (instance), std::move (loader), entry, deps});
1272+ rebuild_name_index ();
1273+ wire_instance_tracked (plugins_.back (), config_map);
1274+ notify_loaded (entry.name , entry.type , entry);
12741275 }
12751276};
12761277
0 commit comments