diff --git a/app/startup/lifecycle/__init__.py b/app/startup/lifecycle/__init__.py index d3e6cc66f..351d4b8e3 100644 --- a/app/startup/lifecycle/__init__.py +++ b/app/startup/lifecycle/__init__.py @@ -33,7 +33,12 @@ from app.startup.command_initializer import init_command, stop_command, restart_ from app.startup.domain_initializer import configure_domain_dependencies from app.startup.modules_initializer import init_modules, stop_modules from app.startup.monitor_initializer import stop_monitor, init_monitor -from app.startup.plugins_initializer import init_plugins, stop_plugins, sync_plugins +from app.startup.plugins_initializer import ( + configure_plugin_services, + init_plugins, + stop_plugins, + sync_plugins, +) from app.startup.routers_initializer import init_routers from app.startup.scheduler_initializer import ( stop_scheduler, @@ -112,6 +117,12 @@ async def run_startup_step( logger.info("启动%s完成,耗时=%.2fms", name, elapsed_ms) +def prepare_plugin_restore() -> None: + """先装配插件外部系统服务,再恢复插件及其依赖。""" + configure_plugin_services() + SystemChain().restore_plugins() + + def build_lifecycle_components(app: FastAPI) -> tuple[LifecycleComponent, ...]: """按现有顺序构建应用组件清单,回调在每次 lifespan 启动时重新绑定。""" return ( @@ -166,7 +177,7 @@ def build_lifecycle_components(app: FastAPI) -> tuple[LifecycleComponent, ...]: name="插件备份恢复", dependencies=("模块服务",), mode=LifecycleMode.NORMAL_ONLY, - start=lambda: SystemChain().restore_plugins(), + start=prepare_plugin_restore, start_order=70, start_timeout_seconds=300, ), diff --git a/app/startup/plugins_initializer.py b/app/startup/plugins_initializer.py index 94d12c661..3c12ef8fd 100644 --- a/app/startup/plugins_initializer.py +++ b/app/startup/plugins_initializer.py @@ -57,7 +57,7 @@ def _prepare_legacy_plugin_import(*, plugin_id: str, plugin_dir: Path) -> None: ) -def _configure_plugin_services() -> None: +def configure_plugin_services() -> None: """把兼容诊断、远程上报和站点认证等级装配到插件管理器。""" plugin_helper = PluginHelper() market_client = PluginMarketClient(plugin_helper) @@ -119,7 +119,7 @@ async def sync_plugins() -> bool: 初始化安装插件,并动态注册后台任务及API """ try: - _configure_plugin_services() + configure_plugin_services() loop = global_vars.loop plugin_manager = PluginManager() @@ -172,7 +172,7 @@ def init_plugins(): """ 初始化插件 """ - _configure_plugin_services() + configure_plugin_services() PluginManager().start() register_plugin_api() diff --git a/tests/test_lifecycle_shutdown.py b/tests/test_lifecycle_shutdown.py index 38b1131ca..129890530 100644 --- a/tests/test_lifecycle_shutdown.py +++ b/tests/test_lifecycle_shutdown.py @@ -33,6 +33,7 @@ def _patch_lifespan(monkeypatch, *, failing_step: str | None = None) -> dict: "init_workflow", ): monkeypatch.setattr(lifecycle, name, MagicMock()) + monkeypatch.setattr(lifecycle, "configure_plugin_services", MagicMock()) monkeypatch.setattr(lifecycle, "init_modules", AsyncMock()) # 启动期的引擎预热与额度核算也要打桩。不打的话这些用例会走真实的引擎创建,在测试 @@ -124,6 +125,7 @@ def test_lifespan_normal_mode_starts_full_runtime(monkeypatch): asyncio.run(run_lifespan()) lifecycle.init_modules.assert_awaited_once_with() + lifecycle.configure_plugin_services.assert_called_once_with() for name in ( "init_plugins", "init_scheduler", @@ -137,6 +139,25 @@ def test_lifespan_normal_mode_starts_full_runtime(monkeypatch): _assert_completed_once(step) +def test_lifespan_configures_plugin_services_before_restore(monkeypatch): + """插件恢复依赖的外部系统服务必须先于恢复阶段完成装配。""" + shutdown_steps = _patch_lifespan(monkeypatch) + order = [] + lifecycle.configure_plugin_services.side_effect = lambda: order.append("configure") + lifecycle.SystemChain.return_value.restore_plugins.side_effect = ( + lambda: order.append("restore") + ) + + async def run_lifespan(): + async with lifecycle.lifespan(FastAPI()): + pass + + asyncio.run(run_lifespan()) + + assert order == ["configure", "restore"] + _assert_completed_once(shutdown_steps["close_http"]) + + def test_lifespan_safe_mode_skips_optional_runtime(monkeypatch): """安全模式只启动基础模块,并跳过插件及可选后台服务。""" shutdown_steps = _patch_lifespan(monkeypatch)