plugin_manager.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746
  1. """
  2. Main PluginManager Class for Trixy Application
  3. This module provides the central PluginManager class that orchestrates:
  4. - Plugin discovery and lifecycle management
  5. - Thread-safe plugin operations
  6. - Plugin health monitoring and error isolation
  7. - Hot-reload capability for development
  8. - Integration with application container and event system
  9. - Plugin enable/disable functionality
  10. - Plugin dependency management
  11. - Configuration management
  12. - Plugin statistics and reporting
  13. The PluginManager serves as the main interface for the plugin system,
  14. providing high-level operations while delegating to specialized components:
  15. - PluginLoader: Dynamic loading and unloading
  16. - PluginConfigHandler: Configuration management
  17. - TrixyPlugin: Base plugin class and lifecycle
  18. Features:
  19. - Thread-safe plugin operations
  20. - Automatic plugin discovery
  21. - Lifecycle management (load, initialize, enable, disable, unload)
  22. - Health monitoring with error isolation
  23. - Hot-reload for development
  24. - Event system integration
  25. - Configuration management
  26. - Dependency resolution
  27. - Plugin statistics and reporting
  28. """
  29. import os
  30. import threading
  31. import time
  32. import weakref
  33. from typing import Dict, List, Optional, Any, Set, Callable, Union
  34. from pathlib import Path
  35. from dataclasses import dataclass, field
  36. from enum import Enum
  37. import json
  38. # Import plugin system components
  39. from .trixy_plugin import TrixyPlugin, PluginState, PluginError, PluginMetadata
  40. from .plugin_loader import PluginLoader, PluginLoadResult, LoadResult
  41. from .config_handler import PluginConfigHandler
  42. # Import event system for integration
  43. try:
  44. from ..events import TrixyEvent
  45. except ImportError:
  46. TrixyEvent = None
  47. def pprint(message: str) -> None:
  48. """Plugin manager logging function."""
  49. print(f"[PLUGIN_MGR] {message}")
  50. class PluginHealthStatus(Enum):
  51. """Plugin health status levels."""
  52. HEALTHY = "healthy"
  53. WARNING = "warning"
  54. ERROR = "error"
  55. CRITICAL = "critical"
  56. UNKNOWN = "unknown"
  57. @dataclass
  58. class PluginInfo:
  59. """Comprehensive plugin information."""
  60. name: str
  61. state: PluginState
  62. enabled: bool
  63. health_status: PluginHealthStatus
  64. load_time: Optional[float] = None
  65. init_time: Optional[float] = None
  66. last_error: Optional[str] = None
  67. error_count: int = 0
  68. metadata: Optional[PluginMetadata] = None
  69. dependencies: List[str] = field(default_factory=list)
  70. path: Optional[Path] = None
  71. config_keys: Set[str] = field(default_factory=set)
  72. last_health_check: float = field(default_factory=time.time)
  73. @property
  74. def is_healthy(self) -> bool:
  75. """Check if plugin is healthy."""
  76. return self.health_status == PluginHealthStatus.HEALTHY
  77. @property
  78. def is_active(self) -> bool:
  79. """Check if plugin is active (enabled and running)."""
  80. return self.enabled and self.state == PluginState.ENABLED
  81. class PluginManagerError(Exception):
  82. """Base exception for plugin manager errors."""
  83. pass
  84. class PluginManager:
  85. """
  86. Central plugin management system for Trixy Application.
  87. This class provides the main interface for plugin operations:
  88. - Loading and unloading plugins
  89. - Enabling and disabling plugins
  90. - Health monitoring and error handling
  91. - Configuration management
  92. - Hot-reload for development
  93. - Plugin discovery and lifecycle management
  94. - Thread-safe operations
  95. - Integration with event system
  96. Usage:
  97. # Create plugin manager
  98. plugin_manager = PluginManager(
  99. application=app_container,
  100. plugins_directory=Path("./plugins")
  101. )
  102. # Load all plugins
  103. plugin_manager.load_all_plugins()
  104. # Enable/disable specific plugins
  105. plugin_manager.enable_plugin("my_plugin")
  106. plugin_manager.disable_plugin("my_plugin")
  107. # Get plugin information
  108. info = plugin_manager.get_plugin_info("my_plugin")
  109. # Health monitoring
  110. health_report = plugin_manager.get_health_report()
  111. # Hot reload for development
  112. plugin_manager.reload_plugin("my_plugin")
  113. """
  114. def __init__(
  115. self,
  116. application: Any,
  117. plugins_directory: Optional[Path] = None,
  118. auto_load: bool = True,
  119. enable_hot_reload: bool = False,
  120. health_check_interval: float = 60.0,
  121. max_error_count: int = 5
  122. ):
  123. """
  124. Initialize the plugin manager.
  125. Args:
  126. application: Application container instance
  127. plugins_directory: Directory containing plugins (defaults to ./plugins)
  128. auto_load: Automatically load plugins on initialization
  129. enable_hot_reload: Enable hot-reload for development
  130. health_check_interval: Interval between health checks in seconds
  131. max_error_count: Maximum errors before marking plugin as critical
  132. """
  133. self.application = application
  134. self.application_ref = weakref.ref(application)
  135. # Plugin directory setup
  136. if plugins_directory is None:
  137. plugins_directory = Path.cwd() / "plugins"
  138. self.plugins_directory = Path(plugins_directory)
  139. # Configuration
  140. self.auto_load = auto_load
  141. self.enable_hot_reload = enable_hot_reload
  142. self.health_check_interval = health_check_interval
  143. self.max_error_count = max_error_count
  144. # Plugin tracking
  145. self._plugins: Dict[str, TrixyPlugin] = {}
  146. self._plugin_info: Dict[str, PluginInfo] = {}
  147. self._plugin_configs: Dict[str, PluginConfigHandler] = {}
  148. self._plugins_lock = threading.RLock()
  149. # Plugin loader
  150. self._loader = PluginLoader(
  151. plugins_directory=self.plugins_directory,
  152. application=application,
  153. auto_reload=enable_hot_reload
  154. )
  155. # Health monitoring
  156. self._health_monitor_thread: Optional[threading.Thread] = None
  157. self._health_monitor_shutdown = threading.Event()
  158. self._last_health_check = 0.0
  159. # Event callbacks
  160. self._event_callbacks: List[Callable[[str, str, Any], None]] = []
  161. # Statistics
  162. self._stats = {
  163. 'total_plugins': 0,
  164. 'loaded_plugins': 0,
  165. 'enabled_plugins': 0,
  166. 'healthy_plugins': 0,
  167. 'error_plugins': 0,
  168. 'total_loads': 0,
  169. 'total_enables': 0,
  170. 'total_disables': 0,
  171. 'total_reloads': 0,
  172. 'uptime': time.time()
  173. }
  174. pprint(f"PluginManager initialized with directory: {self.plugins_directory}")
  175. # Start health monitoring
  176. if self.health_check_interval > 0:
  177. self._start_health_monitor()
  178. # Auto-load plugins if requested
  179. if self.auto_load:
  180. self.load_all_plugins()
  181. # Core Plugin Management Methods
  182. def load_all_plugins(self) -> Dict[str, PluginLoadResult]:
  183. """
  184. Load all plugins from the plugins directory.
  185. Returns:
  186. Dict mapping plugin names to their load results
  187. """
  188. pprint("Loading all plugins...")
  189. results = self._loader.load_all_plugins()
  190. with self._plugins_lock:
  191. # Process load results
  192. for plugin_name, result in results.items():
  193. if result.success and result.plugin_instance:
  194. self._register_plugin(plugin_name, result.plugin_instance)
  195. # Initialize plugin if not in error state
  196. try:
  197. if result.plugin_instance.state == PluginState.LOADED:
  198. result.plugin_instance.initialize_plugin()
  199. # Enable plugin if configured to be enabled
  200. if result.plugin_instance.enabled:
  201. result.plugin_instance.enable_plugin()
  202. self._stats['total_enables'] += 1
  203. except Exception as e:
  204. pprint(f"Failed to initialize plugin {plugin_name}: {e}")
  205. self._update_plugin_error(plugin_name, str(e))
  206. # Update statistics
  207. self._update_stats()
  208. successful = len([r for r in results.values() if r.success])
  209. pprint(f"Plugin loading complete: {successful}/{len(results)} plugins loaded")
  210. return results
  211. def load_plugin(self, plugin_name: str) -> PluginLoadResult:
  212. """
  213. Load a specific plugin.
  214. Args:
  215. plugin_name: Name of the plugin to load
  216. Returns:
  217. PluginLoadResult with load status
  218. """
  219. pprint(f"Loading plugin: {plugin_name}")
  220. result = self._loader.load_plugin(plugin_name)
  221. self._stats['total_loads'] += 1
  222. if result.success and result.plugin_instance:
  223. with self._plugins_lock:
  224. self._register_plugin(plugin_name, result.plugin_instance)
  225. # Initialize plugin
  226. try:
  227. if result.plugin_instance.state == PluginState.LOADED:
  228. result.plugin_instance.initialize_plugin()
  229. # Enable if configured
  230. if result.plugin_instance.enabled:
  231. result.plugin_instance.enable_plugin()
  232. self._stats['total_enables'] += 1
  233. except Exception as e:
  234. pprint(f"Failed to initialize plugin {plugin_name}: {e}")
  235. self._update_plugin_error(plugin_name, str(e))
  236. self._update_stats()
  237. return result
  238. def unload_plugin(self, plugin_name: str) -> bool:
  239. """
  240. Unload a specific plugin.
  241. Args:
  242. plugin_name: Name of plugin to unload
  243. Returns:
  244. bool: True if successfully unloaded
  245. """
  246. pprint(f"Unloading plugin: {plugin_name}")
  247. with self._plugins_lock:
  248. if plugin_name in self._plugins:
  249. # Disable first if enabled
  250. if self._plugins[plugin_name].state == PluginState.ENABLED:
  251. self.disable_plugin(plugin_name)
  252. # Unregister plugin
  253. self._unregister_plugin(plugin_name)
  254. # Unload from loader
  255. success = self._loader.unload_plugin(plugin_name)
  256. if success:
  257. pprint(f"Plugin {plugin_name} unloaded successfully")
  258. self._update_stats()
  259. return success
  260. def enable_plugin(self, plugin_name: str) -> bool:
  261. """
  262. Enable a plugin.
  263. Args:
  264. plugin_name: Name of plugin to enable
  265. Returns:
  266. bool: True if successfully enabled
  267. """
  268. with self._plugins_lock:
  269. if plugin_name not in self._plugins:
  270. pprint(f"Plugin {plugin_name} not loaded")
  271. return False
  272. plugin = self._plugins[plugin_name]
  273. try:
  274. if plugin.state in [PluginState.INITIALIZED, PluginState.DISABLED]:
  275. plugin.enable_plugin()
  276. self._update_plugin_info(plugin_name)
  277. self._stats['total_enables'] += 1
  278. self._notify_event("plugin_enabled", plugin_name, {"state": plugin.state.value})
  279. pprint(f"Plugin {plugin_name} enabled")
  280. return True
  281. else:
  282. pprint(f"Plugin {plugin_name} cannot be enabled from state {plugin.state.value}")
  283. return False
  284. except Exception as e:
  285. error_msg = f"Failed to enable plugin {plugin_name}: {e}"
  286. pprint(error_msg)
  287. self._update_plugin_error(plugin_name, error_msg)
  288. return False
  289. def disable_plugin(self, plugin_name: str) -> bool:
  290. """
  291. Disable a plugin.
  292. Args:
  293. plugin_name: Name of plugin to disable
  294. Returns:
  295. bool: True if successfully disabled
  296. """
  297. with self._plugins_lock:
  298. if plugin_name not in self._plugins:
  299. pprint(f"Plugin {plugin_name} not loaded")
  300. return False
  301. plugin = self._plugins[plugin_name]
  302. try:
  303. if plugin.state in [PluginState.ENABLED, PluginState.INITIALIZED]:
  304. plugin.disable_plugin()
  305. self._update_plugin_info(plugin_name)
  306. self._stats['total_disables'] += 1
  307. self._notify_event("plugin_disabled", plugin_name, {"state": plugin.state.value})
  308. pprint(f"Plugin {plugin_name} disabled")
  309. return True
  310. else:
  311. pprint(f"Plugin {plugin_name} cannot be disabled from state {plugin.state.value}")
  312. return False
  313. except Exception as e:
  314. error_msg = f"Failed to disable plugin {plugin_name}: {e}"
  315. pprint(error_msg)
  316. self._update_plugin_error(plugin_name, error_msg)
  317. return False
  318. def reload_plugin(self, plugin_name: str) -> PluginLoadResult:
  319. """
  320. Reload a plugin (hot-reload).
  321. Args:
  322. plugin_name: Name of plugin to reload
  323. Returns:
  324. PluginLoadResult with reload status
  325. """
  326. pprint(f"Reloading plugin: {plugin_name}")
  327. # Unload first
  328. self.unload_plugin(plugin_name)
  329. # Load again
  330. result = self.load_plugin(plugin_name)
  331. self._stats['total_reloads'] += 1
  332. if result.success:
  333. self._notify_event("plugin_reloaded", plugin_name, {"load_time": result.load_time})
  334. return result
  335. # Plugin Registration and Tracking
  336. def _register_plugin(self, plugin_name: str, plugin_instance: TrixyPlugin) -> None:
  337. """Register a plugin instance."""
  338. self._plugins[plugin_name] = plugin_instance
  339. # Create plugin info
  340. info = PluginInfo(
  341. name=plugin_name,
  342. state=plugin_instance.state,
  343. enabled=plugin_instance.enabled,
  344. health_status=PluginHealthStatus.HEALTHY,
  345. load_time=plugin_instance.load_time,
  346. init_time=plugin_instance.init_time,
  347. metadata=plugin_instance.metadata,
  348. path=plugin_instance.plugin_path
  349. )
  350. self._plugin_info[plugin_name] = info
  351. # Create config handler
  352. config_handler = PluginConfigHandler(
  353. config_path=plugin_instance.plugin_path / "config.json",
  354. plugin_name=plugin_name
  355. )
  356. self._plugin_configs[plugin_name] = config_handler
  357. pprint(f"Plugin {plugin_name} registered")
  358. def _unregister_plugin(self, plugin_name: str) -> None:
  359. """Unregister a plugin."""
  360. if plugin_name in self._plugins:
  361. del self._plugins[plugin_name]
  362. if plugin_name in self._plugin_info:
  363. del self._plugin_info[plugin_name]
  364. if plugin_name in self._plugin_configs:
  365. del self._plugin_configs[plugin_name]
  366. pprint(f"Plugin {plugin_name} unregistered")
  367. def _update_plugin_info(self, plugin_name: str) -> None:
  368. """Update plugin information."""
  369. if plugin_name not in self._plugins or plugin_name not in self._plugin_info:
  370. return
  371. plugin = self._plugins[plugin_name]
  372. info = self._plugin_info[plugin_name]
  373. # Update basic info
  374. info.state = plugin.state
  375. info.enabled = plugin.enabled
  376. info.last_error = plugin.last_error
  377. info.error_count = plugin.error_count
  378. info.last_health_check = time.time()
  379. # Update health status
  380. if plugin.last_error:
  381. if plugin.error_count >= self.max_error_count:
  382. info.health_status = PluginHealthStatus.CRITICAL
  383. else:
  384. info.health_status = PluginHealthStatus.ERROR
  385. elif plugin.state == PluginState.ERROR:
  386. info.health_status = PluginHealthStatus.ERROR
  387. elif plugin.state in [PluginState.ENABLED, PluginState.INITIALIZED]:
  388. info.health_status = PluginHealthStatus.HEALTHY
  389. else:
  390. info.health_status = PluginHealthStatus.WARNING
  391. def _update_plugin_error(self, plugin_name: str, error_message: str) -> None:
  392. """Update plugin error information."""
  393. if plugin_name in self._plugin_info:
  394. info = self._plugin_info[plugin_name]
  395. info.last_error = error_message
  396. info.error_count += 1
  397. info.health_status = PluginHealthStatus.ERROR
  398. info.last_health_check = time.time()
  399. if info.error_count >= self.max_error_count:
  400. info.health_status = PluginHealthStatus.CRITICAL
  401. pprint(f"Plugin {plugin_name} marked as critical due to {info.error_count} errors")
  402. # Health Monitoring
  403. def _start_health_monitor(self) -> None:
  404. """Start the health monitoring thread."""
  405. if self._health_monitor_thread is None or not self._health_monitor_thread.is_alive():
  406. self._health_monitor_thread = threading.Thread(
  407. target=self._health_monitor_loop,
  408. name="PluginHealthMonitor",
  409. daemon=True
  410. )
  411. self._health_monitor_thread.start()
  412. pprint("Health monitoring started")
  413. def _health_monitor_loop(self) -> None:
  414. """Health monitoring loop."""
  415. while not self._health_monitor_shutdown.wait(self.health_check_interval):
  416. try:
  417. self._perform_health_check()
  418. except Exception as e:
  419. pprint(f"Error in health monitor: {e}")
  420. def _perform_health_check(self) -> None:
  421. """Perform health check on all plugins."""
  422. with self._plugins_lock:
  423. for plugin_name in list(self._plugins.keys()):
  424. try:
  425. plugin = self._plugins[plugin_name]
  426. health_data = plugin.check_health()
  427. # Update plugin info based on health check
  428. self._update_plugin_info(plugin_name)
  429. except Exception as e:
  430. pprint(f"Health check failed for plugin {plugin_name}: {e}")
  431. self._update_plugin_error(plugin_name, f"Health check failed: {e}")
  432. self._last_health_check = time.time()
  433. self._update_stats()
  434. # Statistics and Reporting
  435. def _update_stats(self) -> None:
  436. """Update plugin statistics."""
  437. with self._plugins_lock:
  438. self._stats.update({
  439. 'total_plugins': len(self._plugin_info),
  440. 'loaded_plugins': len(self._plugins),
  441. 'enabled_plugins': len([p for p in self._plugins.values() if p.enabled]),
  442. 'healthy_plugins': len([i for i in self._plugin_info.values() if i.is_healthy]),
  443. 'error_plugins': len([i for i in self._plugin_info.values() if i.health_status in [PluginHealthStatus.ERROR, PluginHealthStatus.CRITICAL]]),
  444. })
  445. def get_stats(self) -> Dict[str, Any]:
  446. """Get plugin manager statistics."""
  447. self._update_stats()
  448. stats = self._stats.copy()
  449. stats['uptime'] = time.time() - stats['uptime']
  450. return stats
  451. def get_health_report(self) -> Dict[str, Any]:
  452. """
  453. Get comprehensive health report for all plugins.
  454. Returns:
  455. Dict containing health information for all plugins
  456. """
  457. with self._plugins_lock:
  458. plugins_health = {}
  459. for plugin_name, info in self._plugin_info.items():
  460. plugins_health[plugin_name] = {
  461. 'name': info.name,
  462. 'state': info.state.value,
  463. 'enabled': info.enabled,
  464. 'healthy': info.is_healthy,
  465. 'active': info.is_active,
  466. 'health_status': info.health_status.value,
  467. 'last_error': info.last_error,
  468. 'error_count': info.error_count,
  469. 'load_time': info.load_time,
  470. 'init_time': info.init_time,
  471. 'last_health_check': info.last_health_check,
  472. 'metadata': info.metadata.__dict__ if info.metadata else None
  473. }
  474. return {
  475. 'timestamp': time.time(),
  476. 'last_health_check': self._last_health_check,
  477. 'plugins': plugins_health,
  478. 'summary': self.get_stats()
  479. }
  480. # Query Methods
  481. def get_plugin_names(self) -> List[str]:
  482. """Get list of all plugin names."""
  483. with self._plugins_lock:
  484. return list(self._plugins.keys())
  485. def get_enabled_plugins(self) -> List[str]:
  486. """Get list of enabled plugin names."""
  487. with self._plugins_lock:
  488. return [name for name, plugin in self._plugins.items() if plugin.enabled]
  489. def get_disabled_plugins(self) -> List[str]:
  490. """Get list of disabled plugin names."""
  491. with self._plugins_lock:
  492. return [name for name, plugin in self._plugins.items() if not plugin.enabled]
  493. def get_plugin_info(self, plugin_name: str) -> Optional[PluginInfo]:
  494. """
  495. Get information about a specific plugin.
  496. Args:
  497. plugin_name: Name of the plugin
  498. Returns:
  499. PluginInfo object or None if plugin not found
  500. """
  501. with self._plugins_lock:
  502. if plugin_name in self._plugin_info:
  503. # Update info before returning
  504. self._update_plugin_info(plugin_name)
  505. return self._plugin_info[plugin_name]
  506. return None
  507. def get_plugin_instance(self, plugin_name: str) -> Optional[TrixyPlugin]:
  508. """
  509. Get plugin instance.
  510. Args:
  511. plugin_name: Name of the plugin
  512. Returns:
  513. Plugin instance or None if not found
  514. """
  515. with self._plugins_lock:
  516. return self._plugins.get(plugin_name)
  517. def is_plugin_loaded(self, plugin_name: str) -> bool:
  518. """Check if a plugin is loaded."""
  519. return plugin_name in self._plugins
  520. def is_plugin_enabled(self, plugin_name: str) -> bool:
  521. """Check if a plugin is enabled."""
  522. with self._plugins_lock:
  523. if plugin_name in self._plugins:
  524. return self._plugins[plugin_name].enabled
  525. return False
  526. def is_plugin_healthy(self, plugin_name: str) -> bool:
  527. """Check if a plugin is healthy."""
  528. info = self.get_plugin_info(plugin_name)
  529. return info.is_healthy if info else False
  530. # Event System Integration
  531. def add_event_callback(self, callback: Callable[[str, str, Any], None]) -> None:
  532. """Add callback for plugin events."""
  533. self._event_callbacks.append(callback)
  534. def remove_event_callback(self, callback: Callable[[str, str, Any], None]) -> None:
  535. """Remove event callback."""
  536. if callback in self._event_callbacks:
  537. self._event_callbacks.remove(callback)
  538. def _notify_event(self, event_type: str, plugin_name: str, data: Any) -> None:
  539. """Notify event callbacks."""
  540. for callback in self._event_callbacks:
  541. try:
  542. callback(event_type, plugin_name, data)
  543. except Exception as e:
  544. pprint(f"Error in event callback: {e}")
  545. # Also trigger system events if event handler is available
  546. try:
  547. app = self.application_ref()
  548. if app and hasattr(app, 'get_event_handler'):
  549. event_handler = app.get_event_handler()
  550. if event_handler:
  551. event_data = {
  552. 'plugin_name': plugin_name,
  553. 'event_type': event_type,
  554. 'data': data,
  555. 'timestamp': time.time()
  556. }
  557. event_handler.trigger_event(f"plugin_{event_type}", event_data)
  558. except Exception as e:
  559. pprint(f"Error triggering system event: {e}")
  560. # Configuration Management
  561. def get_plugin_config(self, plugin_name: str) -> Optional[Dict[str, Any]]:
  562. """Get plugin configuration."""
  563. if plugin_name in self._plugin_configs:
  564. return self._plugin_configs[plugin_name].get_config_copy()
  565. return None
  566. def set_plugin_config_value(self, plugin_name: str, key: str, value: Any) -> bool:
  567. """Set a plugin configuration value."""
  568. if plugin_name in self._plugin_configs:
  569. try:
  570. self._plugin_configs[plugin_name].set_value(key, value)
  571. return True
  572. except Exception as e:
  573. pprint(f"Failed to set config value for {plugin_name}: {e}")
  574. return False
  575. # Lifecycle Management
  576. def shutdown(self) -> None:
  577. """Shutdown the plugin manager and all plugins."""
  578. pprint("Shutting down plugin manager...")
  579. # Stop health monitor
  580. if self._health_monitor_thread:
  581. self._health_monitor_shutdown.set()
  582. self._health_monitor_thread.join(timeout=10.0)
  583. # Disable all plugins first
  584. with self._plugins_lock:
  585. for plugin_name in list(self._plugins.keys()):
  586. try:
  587. if self._plugins[plugin_name].state == PluginState.ENABLED:
  588. self.disable_plugin(plugin_name)
  589. except Exception as e:
  590. pprint(f"Error disabling plugin {plugin_name}: {e}")
  591. # Unload all plugins
  592. for plugin_name in list(self._plugins.keys()):
  593. try:
  594. self.unload_plugin(plugin_name)
  595. except Exception as e:
  596. pprint(f"Error unloading plugin {plugin_name}: {e}")
  597. # Shutdown loader
  598. self._loader.shutdown()
  599. pprint("Plugin manager shutdown complete")
  600. def __repr__(self) -> str:
  601. """String representation of the plugin manager."""
  602. with self._plugins_lock:
  603. enabled_count = len([p for p in self._plugins.values() if p.enabled])
  604. return f"<PluginManager loaded={len(self._plugins)} enabled={enabled_count}>"