mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
fix(plugins): align watcher lifecycle with startup (#6366)
This commit is contained in:
@@ -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):
|
||||
"""
|
||||
停止监测插件文件修改监测
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user