diff --git a/pytests/test_plugin_runtime.py b/pytests/test_plugin_runtime.py index d38c06fa..f9d66e10 100644 --- a/pytests/test_plugin_runtime.py +++ b/pytests/test_plugin_runtime.py @@ -534,6 +534,22 @@ class TestSDK: class TestPluginSdkUsage: """验证仓库内插件按新 SDK 归一化返回值工作。""" + def test_runner_skips_signal_handler_registration_on_windows(self, monkeypatch): + """Windows 下不应尝试注册 add_signal_handler。""" + from src.plugin_runtime.runner import runner_main + + registered_signals = [] + + class DummyLoop: + def add_signal_handler(self, sig, callback): + registered_signals.append((sig, callback)) + + monkeypatch.setattr(runner_main.sys, "platform", "win32") + + runner_main._install_shutdown_signal_handlers(lambda: None, DummyLoop()) + + assert not registered_signals + @pytest.mark.asyncio async def test_builtin_emoji_plugin_handles_normalized_results(self): from maibot_sdk.context import PluginContext diff --git a/src/plugin_runtime/runner/runner_main.py b/src/plugin_runtime/runner/runner_main.py index 15fab5a7..245a5487 100644 --- a/src/plugin_runtime/runner/runner_main.py +++ b/src/plugin_runtime/runner/runner_main.py @@ -9,7 +9,7 @@ 6. 转发插件的能力调用到 Host """ -from typing import Any, List, Optional, Protocol, cast +from typing import Any, Callable, List, Optional, Protocol, cast from pathlib import Path @@ -47,6 +47,29 @@ class _ContextAwarePlugin(Protocol): def _set_context(self, context: Any) -> None: ... +def _install_shutdown_signal_handlers( + mark_runner_shutting_down: Callable[[], None], + loop: Optional[asyncio.AbstractEventLoop] = None, +) -> None: + """为 Runner 注册关停信号处理器。 + + Windows 默认事件循环不支持 add_signal_handler,且当前 Runner 在 Windows + 下由 Host 直接 terminate/kill,不依赖进程内信号回调进行优雅收尾。 + """ + if sys.platform == "win32": + return + + target_loop = loop or asyncio.get_running_loop() + for sig in (signal.SIGTERM, signal.SIGINT): + try: + target_loop.add_signal_handler(sig, mark_runner_shutting_down) + except Exception as exc: + if not isinstance(exc, (NotImplementedError, RuntimeError)): + raise + logger.debug(f"当前事件循环不支持注册 Runner 信号处理器: {exc}") + return + + def _disable_runner_console_logging() -> None: """关闭 Runner 的控制台日志输出,避免被 Host 从 stderr 二次包装。""" root_logger = stdlib_logging.getLogger() @@ -666,14 +689,7 @@ async def _async_main() -> None: def _mark_runner_shutting_down() -> None: runner._shutting_down = True - loop = asyncio.get_event_loop() - for sig in (signal.SIGTERM, signal.SIGINT): - try: - loop.add_signal_handler(sig, _mark_runner_shutting_down) - except NotImplementedError: - logger.warning(f"当前平台/事件循环不支持 signal handler,跳过注册信号 {sig!s}") - except RuntimeError as exc: - logger.warning(f"注册信号处理器失败,跳过信号 {sig!s}: {exc}") + _install_shutdown_signal_handlers(_mark_runner_shutting_down) await runner.run()