diff options
| -rw-r--r-- | idatui/domain.py | 17 | ||||
| -rw-r--r-- | idatui/rpc.py | 21 | ||||
| -rw-r--r-- | idatui/rpcclient.py | 21 |
3 files changed, 55 insertions, 4 deletions
diff --git a/idatui/domain.py b/idatui/domain.py index 826d0f7..cc95b21 100644 --- a/idatui/domain.py +++ b/idatui/domain.py @@ -36,6 +36,7 @@ from .client import IDAClient, IDAToolError LIST_PAGE = 500 DISASM_BLOCK = 256 # instructions per cached/fetched block (<= disasm cap) HEX_BLOCK = 4096 # bytes per cached/fetched hex block +DECOMPILE_TIMEOUT = 15.0 # s; cap per decompile so a failing one can't hang the CLI _TRUNC_RE = re.compile(r"\[(\d+) chars total\]\s*$") @@ -741,7 +742,21 @@ class Program: self.client.call("force_recompile", items=[{"addr": hex(ea)}]) except Exception: # noqa: BLE001 pass - envelope = self.client.call_envelope("decompile", addr=hex(ea)) + # Bound the decompile: a function Hex-Rays can't handle tends to stall + # near the client's default 30s timeout, and the transport retries a + # dropped connection up to max_retries+1 times, re-running the failing + # decompile each time. Cap it so the worst case stays well under the + # rpcclient socket timeout, and cache the failure below so a re-request + # returns instantly instead of re-grinding. + try: + envelope = self.client.call_envelope( + "decompile", addr=hex(ea), timeout=DECOMPILE_TIMEOUT + ) + except Exception as e: # noqa: BLE001 -- surface as a failed decompile + dec = Decompilation(ea, None, True, f"decompile error: {e}", False, None) + with self._lock: + self._decomp[ea] = (dec, self._name_gen) + return dec result = envelope.get("result", {}) payload = result.get("structuredContent") if payload is None: # fall back to text content diff --git a/idatui/rpc.py b/idatui/rpc.py index 426b337..f41c734 100644 --- a/idatui/rpc.py +++ b/idatui/rpc.py @@ -587,7 +587,26 @@ class RpcServer: return await self._press(["escape"], timeout=timeout) if method == "toggle_view": before = app._active - return await self._press(["tab"], lambda: app._active != before, timeout) + + def _toggled(): + # Normal case: the shown view flipped. + if app._active != before: + return True + # Fallback case: a tab toward pseudocode on a function Hex-Rays + # can't decompile snaps `_active` back to disasm (see + # App._apply_decomp), so `_active` never changes and the naive + # `_active != before` predicate would block for the full + # timeout. Treat "requested decomp but it's known-failed" as + # settled (the decompile is cached, so this is cheap). + cur = app._cur + if before == "disasm" and cur is not None: + try: + return app.program.decompile(cur.ea).failed + except Exception: # noqa: BLE001 + return False + return False + + return await self._press(["tab"], _toggled, timeout) if method == "hex": return await self._press(["backslash"], lambda: app._active == "hex", timeout) if method == "xrefs": diff --git a/idatui/rpcclient.py b/idatui/rpcclient.py index ecbdfed..e0a5602 100644 --- a/idatui/rpcclient.py +++ b/idatui/rpcclient.py @@ -25,11 +25,20 @@ from typing import Any class RpcClient: - def __init__(self, sock_path: str): + #: Default read timeout (s). Bounds any single call so a slow/hung server + #: (e.g. Hex-Rays grinding on an undecompilable function) can't block the + #: CLI forever. Override via ctor or the IDATUI_RPC_TIMEOUT env var. + DEFAULT_TIMEOUT = 90.0 + + def __init__(self, sock_path: str, timeout: float | None = None): self.path = sock_path self._sock: socket.socket | None = None self._buf = b"" self._id = 0 + if timeout is None: + env = os.environ.get("IDATUI_RPC_TIMEOUT") + timeout = float(env) if env else self.DEFAULT_TIMEOUT + self.timeout = timeout def __enter__(self) -> "RpcClient": self.connect() @@ -41,6 +50,8 @@ class RpcClient: def connect(self) -> None: s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.connect(self.path) + if self.timeout and self.timeout > 0: + s.settimeout(self.timeout) self._sock = s def close(self) -> None: @@ -54,7 +65,13 @@ class RpcClient: req = {"id": self._id, "method": method, "params": params} self._sock.sendall(json.dumps(req).encode() + b"\n") while b"\n" not in self._buf: - chunk = self._sock.recv(65536) + try: + chunk = self._sock.recv(65536) + except socket.timeout as e: + raise RpcError( + f"no response for {method!r} within {self.timeout}s " + f"(server busy or the op is hung, e.g. a failed decompile)" + ) from e if not chunk: raise ConnectionError("server closed the connection") self._buf += chunk |
