From 579672d87df1875ddd719ea45252af974cccc59f Mon Sep 17 00:00:00 2001 From: wayne1992127 Date: Mon, 27 Jul 2026 23:20:40 +0800 Subject: [PATCH] fix(desktop): keep cold gateway config off event loop --- hermes_cli/web_server.py | 23 ++++++++++++++------ tests/hermes_cli/test_web_server.py | 33 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 857f89605ba24..5af536fdce13a 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -2953,6 +2953,20 @@ def _collect_profile_gateway_topology_cached() -> Dict[str, Any]: return data +def _load_configured_gateway_platforms() -> set[str]: + """Load connected platform names away from the asyncio event loop. + + The first ``load_gateway_config()`` call performs platform discovery and + can take longer than Desktop's WebSocket connect timeout on Windows. This + helper is synchronous by design; ``get_status`` runs it in Starlette's + worker pool so a concurrent ``/api/ws`` handshake can still complete. + """ + from gateway.config import load_gateway_config + + gateway_config = load_gateway_config() + return {platform.value for platform in gateway_config.get_connected_platforms()} + + @app.get("/api/ssh/ownership") async def get_ssh_ownership(request: Request): _require_token(request) @@ -3054,12 +3068,9 @@ async def get_status(profile: Optional[str] = None): gateway_updated_at = None configured_gateway_platforms: set[str] | None = None try: - from gateway.config import load_gateway_config - - gateway_config = load_gateway_config() - configured_gateway_platforms = { - platform.value for platform in gateway_config.get_connected_platforms() - } + configured_gateway_platforms = await run_in_threadpool( + _load_configured_gateway_platforms + ) except Exception: configured_gateway_platforms = None diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index 2a201966cab49..c7f3c5fc85da8 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -318,6 +318,39 @@ class TestWebServerEndpoints: monitor.close() writer.close() + def test_get_status_loads_gateway_config_off_event_loop(self, monkeypatch): + """Cold gateway config loading must not block the WebSocket loop. + + On Windows the first ``load_gateway_config()`` call imports and + discovers platform adapters and can take longer than Desktop's 15s + WebSocket timeout. Running it inline makes a concurrent /api/ws + handshake time out before ``gateway.ready`` can be sent. + """ + import gateway.config as gateway_config + import hermes_cli.web_server as web_server + + seen = {} + + class _Config: + @staticmethod + def get_connected_platforms(): + return [] + + def _load(): + seen["thread"] = threading.get_ident() + return _Config() + + monkeypatch.setattr(gateway_config, "load_gateway_config", _load) + + async def _run(): + event_loop_thread = threading.get_ident() + await web_server.get_status() + return event_loop_thread + + event_loop_thread = asyncio.run(_run()) + + assert seen["thread"] != event_loop_thread + def test_get_sessions_auto_archive_uses_maintenance_writer(self): from hermes_cli import web_server from hermes_cli.config import load_config, save_config