aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--idatui/domain.py17
-rw-r--r--idatui/rpc.py21
-rw-r--r--idatui/rpcclient.py21
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