feat(v0.5.1): 插件进程隔离 — 集成到 PluginService
- PluginService._should_isolate() 检查全局配置 + 插件级配置 - _load_plugin_isolated() 使用 IsolatedPlugin (multiprocessing.Process) - unload_plugin() 兼容隔离/非隔离两种实例 - 配置: plugins.isolation (全局默认) + settings.isolation (插件级) - 插件崩溃不影响框架主进程 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -218,7 +218,8 @@ class InitService:
|
|||||||
'plugins': {
|
'plugins': {
|
||||||
'auto_load': True,
|
'auto_load': True,
|
||||||
'hot_reload': True,
|
'hot_reload': True,
|
||||||
'max_retry_count': 3
|
'max_retry_count': 3,
|
||||||
|
'isolation': False, # 默认不隔离,插件可在 settings.isolation 中声明
|
||||||
},
|
},
|
||||||
'auto_start_scripts': {
|
'auto_start_scripts': {
|
||||||
'enabled': True,
|
'enabled': True,
|
||||||
|
|||||||
+95
-20
@@ -192,36 +192,105 @@ class PluginService:
|
|||||||
await self.unload_plugin(name)
|
await self.unload_plugin(name)
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
|
|
||||||
|
def _should_isolate(self, plugin_config: dict) -> bool:
|
||||||
|
"""检查插件是否应使用进程隔离模式"""
|
||||||
|
global_isolation = self.config.get('plugins', {}).get('isolation', False)
|
||||||
|
plugin_isolation = plugin_config.get('settings', {}).get('isolation', None)
|
||||||
|
if plugin_isolation is not None:
|
||||||
|
return bool(plugin_isolation)
|
||||||
|
return global_isolation
|
||||||
|
|
||||||
async def load_all_plugins(self):
|
async def load_all_plugins(self):
|
||||||
"""加载所有插件"""
|
"""加载所有插件"""
|
||||||
try:
|
try:
|
||||||
logger.debug("开始加载所有插件")
|
logger.debug("开始加载所有插件")
|
||||||
|
|
||||||
if not self.plugins_dir.exists():
|
if not self.plugins_dir.exists():
|
||||||
logger.warning("插件目录不存在,跳过加载")
|
logger.warning("插件目录不存在,跳过加载")
|
||||||
return
|
return
|
||||||
|
|
||||||
loaded_count = 0
|
loaded_count = 0
|
||||||
error_count = 0
|
error_count = 0
|
||||||
|
isolated_count = 0
|
||||||
|
|
||||||
# 遍历插件目录
|
# 遍历插件目录
|
||||||
for plugin_dir in self.plugins_dir.iterdir():
|
for plugin_dir in self.plugins_dir.iterdir():
|
||||||
if plugin_dir.is_dir():
|
if plugin_dir.is_dir():
|
||||||
try:
|
try:
|
||||||
success = await self.load_plugin(plugin_dir.name)
|
# 先读配置判断是否需要隔离
|
||||||
if success:
|
config_file = plugin_dir / "config.yaml"
|
||||||
loaded_count += 1
|
plugin_config = {}
|
||||||
|
if config_file.exists():
|
||||||
|
with open(config_file, 'r') as f:
|
||||||
|
plugin_config = yaml.safe_load(f) or {}
|
||||||
|
|
||||||
|
if self._should_isolate(plugin_config):
|
||||||
|
success = await self._load_plugin_isolated(plugin_dir.name)
|
||||||
|
if success:
|
||||||
|
isolated_count += 1
|
||||||
|
loaded_count += 1
|
||||||
|
else:
|
||||||
|
error_count += 1
|
||||||
else:
|
else:
|
||||||
error_count += 1
|
success = await self.load_plugin(plugin_dir.name)
|
||||||
|
if success:
|
||||||
|
loaded_count += 1
|
||||||
|
else:
|
||||||
|
error_count += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"加载插件 {plugin_dir.name} 时出错: {str(e)}", exc_info=True)
|
logger.error(f"加载插件 {plugin_dir.name} 时出错: {str(e)}", exc_info=True)
|
||||||
error_count += 1
|
error_count += 1
|
||||||
|
|
||||||
logger.info(f"插件加载完成: 成功 {loaded_count}, 失败 {error_count}")
|
logger.info(
|
||||||
|
f"插件加载完成: 成功 {loaded_count} (其中隔离 {isolated_count}), 失败 {error_count}"
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"加载所有插件时出错: {str(e)}", exc_info=True)
|
logger.error(f"加载所有插件时出错: {str(e)}", exc_info=True)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
async def _load_plugin_isolated(self, plugin_name: str) -> bool:
|
||||||
|
"""在独立子进程中加载插件(进程隔离模式)"""
|
||||||
|
try:
|
||||||
|
from services.process_isolated import IsolatedPlugin
|
||||||
|
|
||||||
|
plugin_path = self.plugins_dir / plugin_name
|
||||||
|
main_module = plugin_path / "__init__.py"
|
||||||
|
if not main_module.exists():
|
||||||
|
logger.error(f"隔离插件主模块不存在: {main_module}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
config_file = plugin_path / "config.yaml"
|
||||||
|
plugin_config = {}
|
||||||
|
if config_file.exists():
|
||||||
|
with open(config_file) as f:
|
||||||
|
plugin_config = yaml.safe_load(f) or {}
|
||||||
|
|
||||||
|
iso = IsolatedPlugin(
|
||||||
|
plugin_name,
|
||||||
|
str(main_module),
|
||||||
|
plugin_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.plugins[plugin_name] = iso
|
||||||
|
self.plugin_info[plugin_name] = PluginInfo(
|
||||||
|
name=plugin_config.get('name', plugin_name),
|
||||||
|
version=plugin_config.get('version', '0.1.0'),
|
||||||
|
description=plugin_config.get('description', ''),
|
||||||
|
author=plugin_config.get('author', ''),
|
||||||
|
enabled=True,
|
||||||
|
loaded=True,
|
||||||
|
error_count=0,
|
||||||
|
permissions=[],
|
||||||
|
plugin_path=plugin_path,
|
||||||
|
commands={},
|
||||||
|
)
|
||||||
|
logger.info(f"🔒 隔离插件加载成功: {plugin_name} (PID={iso.pid})")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"加载隔离插件 {plugin_name} 失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
async def load_plugin(self, plugin_name: str) -> bool:
|
async def load_plugin(self, plugin_name: str) -> bool:
|
||||||
"""加载单个插件 - 支持异步权限处理"""
|
"""加载单个插件 - 支持异步权限处理"""
|
||||||
@@ -454,17 +523,18 @@ class PluginService:
|
|||||||
"""卸载插件"""
|
"""卸载插件"""
|
||||||
try:
|
try:
|
||||||
logger.debug(f"开始卸载插件: {plugin_name}")
|
logger.debug(f"开始卸载插件: {plugin_name}")
|
||||||
|
|
||||||
if plugin_name not in self.plugins:
|
if plugin_name not in self.plugins:
|
||||||
logger.warning(f"插件未加载: {plugin_name}")
|
logger.warning(f"插件未加载: {plugin_name}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
plugin_instance = self.plugins[plugin_name]
|
plugin_instance = self.plugins[plugin_name]
|
||||||
plugin_info = self.plugin_info[plugin_name]
|
plugin_info = self.plugin_info[plugin_name]
|
||||||
|
|
||||||
# 注销插件命令
|
# 注销插件命令 (隔离插件可能没有注册命令)
|
||||||
await self._unregister_plugin_commands(plugin_name)
|
if not self._is_isolated(plugin_instance):
|
||||||
|
await self._unregister_plugin_commands(plugin_name)
|
||||||
|
|
||||||
# 调用插件的清理方法
|
# 调用插件的清理方法
|
||||||
try:
|
try:
|
||||||
if hasattr(plugin_instance, 'shutdown'):
|
if hasattr(plugin_instance, 'shutdown'):
|
||||||
@@ -474,23 +544,28 @@ class PluginService:
|
|||||||
plugin_instance.shutdown()
|
plugin_instance.shutdown()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"插件清理时出错 {plugin_name}: {str(e)}", exc_info=True)
|
logger.error(f"插件清理时出错 {plugin_name}: {str(e)}", exc_info=True)
|
||||||
|
|
||||||
# 从模块缓存中移除
|
# 从模块缓存中移除
|
||||||
module_name = f"plugins.{plugin_name}"
|
module_name = f"plugins.{plugin_name}"
|
||||||
if module_name in sys.modules:
|
if module_name in sys.modules:
|
||||||
del sys.modules[module_name]
|
del sys.modules[module_name]
|
||||||
|
|
||||||
# 移除插件实例和信息
|
# 移除插件实例和信息
|
||||||
del self.plugins[plugin_name]
|
del self.plugins[plugin_name]
|
||||||
plugin_info.loaded = False
|
plugin_info.loaded = False
|
||||||
plugin_info.enabled = False
|
plugin_info.enabled = False
|
||||||
|
|
||||||
logger.info(f"插件卸载成功: {plugin_name}")
|
logger.info(f"插件卸载成功: {plugin_name}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"卸载插件 {plugin_name} 时出错: {str(e)}", exc_info=True)
|
logger.error(f"卸载插件 {plugin_name} 时出错: {str(e)}", exc_info=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_isolated(plugin_instance) -> bool:
|
||||||
|
from services.process_isolated import IsolatedPlugin
|
||||||
|
return isinstance(plugin_instance, IsolatedPlugin)
|
||||||
|
|
||||||
async def _unregister_plugin_commands(self, plugin_name: str):
|
async def _unregister_plugin_commands(self, plugin_name: str):
|
||||||
"""注销插件命令"""
|
"""注销插件命令"""
|
||||||
|
|||||||
Reference in New Issue
Block a user