fix(plugins): bound async plugin command await with 30s timeout
Follow-up to #17963. The threaded branch of resolve_plugin_command_result previously called Event.wait() with no timeout — a hung async plugin handler would wedge the terminal indefinitely. Cap the wait at 30s and raise TimeoutError instead. Added a regression test covering the hung handler path.
This commit is contained in:
parent
ca9a61ae38
commit
447a2bba3a
@ -1229,13 +1229,18 @@ def get_plugin_command_handler(name: str) -> Optional[Callable]:
|
||||
return entry["handler"] if entry else None
|
||||
|
||||
|
||||
_PLUGIN_COMMAND_AWAIT_TIMEOUT_SECS = 30.0
|
||||
|
||||
|
||||
def resolve_plugin_command_result(result: Any) -> Any:
|
||||
"""Resolve a plugin command return value, awaiting async handlers when needed.
|
||||
|
||||
Sync CLI/TUI dispatch sites call plugin handlers from plain functions.
|
||||
If a handler is async, await it directly when no loop is running; if
|
||||
we're already inside an active loop, run it in a helper thread with its
|
||||
own loop so the caller still gets a concrete result synchronously.
|
||||
own loop so the caller still gets a concrete result synchronously. The
|
||||
threaded path is bounded by a 30s timeout so a hung async handler cannot
|
||||
wedge the terminal indefinitely.
|
||||
"""
|
||||
if not inspect.isawaitable(result):
|
||||
return result
|
||||
@ -1263,7 +1268,11 @@ def resolve_plugin_command_result(result: Any) -> Any:
|
||||
daemon=True,
|
||||
)
|
||||
thread.start()
|
||||
done.wait()
|
||||
if not done.wait(timeout=_PLUGIN_COMMAND_AWAIT_TIMEOUT_SECS):
|
||||
raise TimeoutError(
|
||||
"Plugin command async handler did not complete within "
|
||||
f"{_PLUGIN_COMMAND_AWAIT_TIMEOUT_SECS:.0f}s"
|
||||
)
|
||||
if "exc" in failure:
|
||||
raise failure["exc"]
|
||||
return outcome.get("value")
|
||||
|
||||
@ -1082,6 +1082,24 @@ class TestPluginCommandResultResolution:
|
||||
monkeypatch.setattr("hermes_cli.plugins.asyncio.get_running_loop", lambda: _Loop())
|
||||
assert resolve_plugin_command_result(_handler()) == "threaded-ok"
|
||||
|
||||
def test_running_loop_timeout_does_not_hang_forever(self, monkeypatch):
|
||||
"""Threaded path must abort a hung async handler instead of blocking the caller."""
|
||||
import asyncio as _asyncio
|
||||
|
||||
class _Loop:
|
||||
pass
|
||||
|
||||
async def _slow_handler():
|
||||
await _asyncio.sleep(10)
|
||||
return "should-not-reach"
|
||||
|
||||
monkeypatch.setattr("hermes_cli.plugins.asyncio.get_running_loop", lambda: _Loop())
|
||||
monkeypatch.setattr("hermes_cli.plugins._PLUGIN_COMMAND_AWAIT_TIMEOUT_SECS", 0.1)
|
||||
|
||||
import pytest
|
||||
with pytest.raises(TimeoutError):
|
||||
resolve_plugin_command_result(_slow_handler())
|
||||
|
||||
|
||||
# ── TestPluginDispatchTool ────────────────────────────────────────────────
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user