2026-04-02 19:06:42 -05:00
|
|
|
import json
|
2026-04-03 19:52:50 -05:00
|
|
|
import signal
|
2026-04-02 19:06:42 -05:00
|
|
|
import sys
|
|
|
|
|
|
fix(tui-gateway): dispatch slow RPC handlers on a thread pool (#12546)
The stdin-read loop in entry.py calls handle_request() inline, so the
five handlers that can block for seconds to minutes
(slash.exec, cli.exec, shell.exec, session.resume, session.branch)
freeze the dispatcher. While one is running, any inbound RPC —
notably approval.respond and session.interrupt — sits unread in the
pipe buffer and lands only after the slow handler returns.
Route only those five onto a small ThreadPoolExecutor; every other
handler stays on the main thread so the fast-path ordering is
unchanged and the audit surface stays small. write_json is already
_stdout_lock-guarded, so concurrent response writes are safe. Pool
size defaults to 4 (overridable via HERMES_TUI_RPC_POOL_WORKERS).
- add _LONG_HANDLERS set + ThreadPoolExecutor + atexit shutdown
- new dispatch(req) function: pool for long handlers, inline for rest
- _run_and_emit wraps pool work in a try/except so a misbehaving
handler still surfaces as a JSON-RPC error instead of silently
dying in a worker
- entry.py swaps handle_request → dispatch
- 5 new tests: sync path still inline, long handlers emit via stdout,
fast handler not blocked behind slow one, handler exceptions map to
error responses, non-long methods always take the sync path
Manual repro confirms the fix: shell.exec(sleep 3) + terminal.resize
sent back-to-back now returns the resize response at t=0s while the
sleep finishes independently at t=3s. Before, both landed together
at t=3s.
Fixes #12546.
2026-04-19 07:47:15 -05:00
|
|
|
from tui_gateway.server import dispatch, resolve_skin, write_json
|
2026-04-02 19:06:42 -05:00
|
|
|
|
2026-04-03 19:52:50 -05:00
|
|
|
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
2026-04-13 21:20:55 -05:00
|
|
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
|
|
|
|
2026-04-03 19:52:50 -05:00
|
|
|
|
2026-04-02 19:06:42 -05:00
|
|
|
def main():
|
2026-04-06 18:38:13 -05:00
|
|
|
if not write_json({
|
2026-04-02 19:06:42 -05:00
|
|
|
"jsonrpc": "2.0",
|
|
|
|
|
"method": "event",
|
|
|
|
|
"params": {"type": "gateway.ready", "payload": {"skin": resolve_skin()}},
|
2026-04-06 18:38:13 -05:00
|
|
|
}):
|
|
|
|
|
sys.exit(0)
|
2026-04-02 19:06:42 -05:00
|
|
|
|
|
|
|
|
for raw in sys.stdin:
|
|
|
|
|
line = raw.strip()
|
|
|
|
|
if not line:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
req = json.loads(line)
|
|
|
|
|
except json.JSONDecodeError:
|
2026-04-06 18:38:13 -05:00
|
|
|
if not write_json({"jsonrpc": "2.0", "error": {"code": -32700, "message": "parse error"}, "id": None}):
|
|
|
|
|
sys.exit(0)
|
2026-04-02 19:06:42 -05:00
|
|
|
continue
|
|
|
|
|
|
fix(tui-gateway): dispatch slow RPC handlers on a thread pool (#12546)
The stdin-read loop in entry.py calls handle_request() inline, so the
five handlers that can block for seconds to minutes
(slash.exec, cli.exec, shell.exec, session.resume, session.branch)
freeze the dispatcher. While one is running, any inbound RPC —
notably approval.respond and session.interrupt — sits unread in the
pipe buffer and lands only after the slow handler returns.
Route only those five onto a small ThreadPoolExecutor; every other
handler stays on the main thread so the fast-path ordering is
unchanged and the audit surface stays small. write_json is already
_stdout_lock-guarded, so concurrent response writes are safe. Pool
size defaults to 4 (overridable via HERMES_TUI_RPC_POOL_WORKERS).
- add _LONG_HANDLERS set + ThreadPoolExecutor + atexit shutdown
- new dispatch(req) function: pool for long handlers, inline for rest
- _run_and_emit wraps pool work in a try/except so a misbehaving
handler still surfaces as a JSON-RPC error instead of silently
dying in a worker
- entry.py swaps handle_request → dispatch
- 5 new tests: sync path still inline, long handlers emit via stdout,
fast handler not blocked behind slow one, handler exceptions map to
error responses, non-long methods always take the sync path
Manual repro confirms the fix: shell.exec(sleep 3) + terminal.resize
sent back-to-back now returns the resize response at t=0s while the
sleep finishes independently at t=3s. Before, both landed together
at t=3s.
Fixes #12546.
2026-04-19 07:47:15 -05:00
|
|
|
resp = dispatch(req)
|
2026-04-02 19:06:42 -05:00
|
|
|
if resp is not None:
|
2026-04-06 18:38:13 -05:00
|
|
|
if not write_json(resp):
|
|
|
|
|
sys.exit(0)
|
2026-04-02 19:06:42 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|