fix(plugins): align watcher lifecycle with startup (#6366)

This commit is contained in:
InfinityPacer
2026-08-20 15:24:15 +08:00
committed by GitHub
parent 4e3a6c8032
commit 2e5cc6c669
4 changed files with 160 additions and 26 deletions
+5 -10
View File
@@ -275,10 +275,6 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
"plugins",
self.resolve_event_handler_instance,
)
# 开发者模式监测插件修改
if settings.DEV or settings.PLUGIN_AUTO_RELOAD:
self.__start_monitor()
def resolve_event_handler_instance(
self,
owner_class: Type[Any],
@@ -387,6 +383,11 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
"""返回配置重载日志使用的功能名称。"""
return "插件文件修改监测"
def start_monitor(self):
"""按当前配置启动插件文件修改监测。"""
if settings.DEV or settings.PLUGIN_AUTO_RELOAD:
self._plugin_monitor.start()
def reload_monitor(self):
"""
重新加载插件文件修改监测
@@ -395,12 +396,6 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
enabled=settings.DEV or settings.PLUGIN_AUTO_RELOAD
)
def __start_monitor(self):
"""
启用监测插件文件修改监测
"""
self._plugin_monitor.start()
def stop_monitor(self):
"""
停止监测插件文件修改监测
+8 -11
View File
@@ -5,6 +5,7 @@ from app.runtime.compat.diagnostics import (
scan_plugin_legacy_imports,
)
from app.runtime.compat.resource_imports import scan_plugin_resource_imports
from app.application.plugin.routes import register_plugin_api
from app.runtime.config import global_vars
from app.runtime.config import settings
from app.runtime.extensions.plugin_manager import (
@@ -162,21 +163,15 @@ async def execute_task(loop, task_func, task_name):
return []
def register_plugin_api():
"""
插件启动后注册插件API
"""
from app.api.endpoints import plugin
plugin.register_plugin_api()
def init_plugins():
"""
初始化插件
"""
configure_plugin_services()
PluginManager().start()
plugin_manager = PluginManager()
plugin_manager.start()
register_plugin_api()
plugin_manager.start_monitor()
def stop_plugins():
@@ -185,7 +180,9 @@ def stop_plugins():
"""
try:
plugin_manager = PluginManager()
plugin_manager.stop()
plugin_manager.stop_monitor()
try:
plugin_manager.stop_monitor()
finally:
plugin_manager.stop()
except Exception as e:
logger.error(f"停止插件时发生错误:{e}", exc_info=True)
+3 -5
View File
@@ -13,8 +13,8 @@
"runtime_to_db": [],
"workflow_to_db": []
},
"edge_count": 6069,
"edge_sha256": "a47f8e0ee6d4855e106fefae05d5fa27beea9a1fd5710009260615d232af3c03",
"edge_count": 6067,
"edge_sha256": "a10a5353df10ba2817b49f6994eefd99266c853eda5119f36b2a68bafb9221ed",
"edges": [
"app -> app.runtime",
"app -> app.runtime.compat",
@@ -5871,12 +5871,10 @@
"app.startup.plugins_initializer -> app.adapters.system.plugin.dependency",
"app.startup.plugins_initializer -> app.adapters.system.plugin.manifest",
"app.startup.plugins_initializer -> app.adapters.system.plugin.package",
"app.startup.plugins_initializer -> app.api",
"app.startup.plugins_initializer -> app.api.endpoints",
"app.startup.plugins_initializer -> app.api.endpoints.plugin",
"app.startup.plugins_initializer -> app.application",
"app.startup.plugins_initializer -> app.application.plugin",
"app.startup.plugins_initializer -> app.application.plugin.catalog",
"app.startup.plugins_initializer -> app.application.plugin.routes",
"app.startup.plugins_initializer -> app.application.site",
"app.startup.plugins_initializer -> app.db",
"app.startup.plugins_initializer -> app.db.oper",
+144
View File
@@ -0,0 +1,144 @@
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from app.foundation.singleton import Singleton
from app.runtime.extensions.plugin.monitor import PluginMonitorController
from app.runtime.extensions.plugin.system import reset_plugin_system
from app.runtime.extensions.plugin_manager import PluginManager
from app.startup import plugins_initializer
def _reset_plugin_manager() -> None:
"""清除插件管理器单例,保证构造时序测试彼此隔离。"""
Singleton._instances.pop((PluginManager, (), frozenset()), None)
@pytest.mark.parametrize(
("dev", "auto_reload"),
((True, False), (False, True)),
)
def test_plugin_manager_constructor_does_not_start_monitor_before_runtime(
monkeypatch,
dev: bool,
auto_reload: bool,
) -> None:
"""组合根尚未装配时,构造插件管理器不得提前启动开发监控线程。"""
_reset_plugin_manager()
reset_plugin_system()
start = MagicMock()
monkeypatch.setattr(
"app.runtime.extensions.plugin_manager.settings",
SimpleNamespace(
DEV=dev,
PLUGIN_AUTO_RELOAD=auto_reload,
ROOT_PATH=MagicMock(),
),
)
monkeypatch.setattr(PluginMonitorController, "start", start)
PluginManager()
start.assert_not_called()
_reset_plugin_manager()
def test_init_plugins_starts_monitor_after_runtime_and_routes(monkeypatch) -> None:
"""插件运行时和动态路由就绪后,启动层才允许文件监控接收变化。"""
order: list[str] = []
manager = MagicMock()
manager.start.side_effect = lambda: order.append("plugins")
manager.start_monitor.side_effect = lambda: order.append("monitor")
monkeypatch.setattr(
plugins_initializer,
"configure_plugin_services",
lambda: order.append("services"),
)
monkeypatch.setattr(plugins_initializer, "PluginManager", lambda: manager)
monkeypatch.setattr(
plugins_initializer,
"register_plugin_api",
lambda: order.append("routes"),
)
plugins_initializer.init_plugins()
assert order == ["services", "plugins", "routes", "monitor"]
@pytest.mark.parametrize(
("dev", "auto_reload", "expected_calls"),
((True, False, 1), (False, True, 1), (False, False, 0)),
)
def test_start_monitor_respects_runtime_configuration(
monkeypatch,
dev: bool,
auto_reload: bool,
expected_calls: int,
) -> None:
"""首次启动只在开发模式或插件自动重载启用时创建监控线程。"""
_reset_plugin_manager()
reset_plugin_system()
monkeypatch.setattr(
"app.runtime.extensions.plugin_manager.settings",
SimpleNamespace(
DEV=dev,
PLUGIN_AUTO_RELOAD=auto_reload,
ROOT_PATH=MagicMock(),
),
)
manager = PluginManager()
start = MagicMock()
manager._plugin_monitor.start = start
manager.start_monitor()
assert start.call_count == expected_calls
_reset_plugin_manager()
def test_config_change_reloads_monitor(monkeypatch) -> None:
"""配置热更新继续使用重建语义,不复用首次启动入口。"""
_reset_plugin_manager()
reset_plugin_system()
monkeypatch.setattr(
"app.runtime.extensions.plugin_manager.settings",
SimpleNamespace(
DEV=False,
PLUGIN_AUTO_RELOAD=False,
ROOT_PATH=MagicMock(),
),
)
manager = PluginManager()
reload_monitor = MagicMock()
manager.reload_monitor = reload_monitor
manager.on_config_changed()
reload_monitor.assert_called_once_with()
_reset_plugin_manager()
def test_stop_plugins_stops_monitor_before_plugin_runtime(monkeypatch) -> None:
"""关闭时先隔离文件变化,再停止插件实例。"""
order: list[str] = []
manager = MagicMock()
manager.stop_monitor.side_effect = lambda: order.append("monitor")
manager.stop.side_effect = lambda: order.append("plugins")
monkeypatch.setattr(plugins_initializer, "PluginManager", lambda: manager)
plugins_initializer.stop_plugins()
assert order == ["monitor", "plugins"]
def test_stop_plugins_still_stops_runtime_when_monitor_stop_fails(monkeypatch) -> None:
"""监控线程停止异常不得阻止插件实例释放资源。"""
manager = MagicMock()
manager.stop_monitor.side_effect = RuntimeError("monitor stop failed")
monkeypatch.setattr(plugins_initializer, "PluginManager", lambda: manager)
plugins_initializer.stop_plugins()
manager.stop.assert_called_once_with()