aboutsummaryrefslogtreecommitdiffstats
path: root/idatui
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-24 01:42:43 +0200
committerblasty <blasty@local>2026-07-24 01:42:43 +0200
commit784df98d19b70eca5906ce0ac7fcf2bfcd0f3515 (patch)
tree21a8bc2b8fce7df70a79ac0361cc51c70eb64061 /idatui
parentapp: --backend {mcp,worker} — run the TUI on our idalib worker (migration s... (diff)
downloadida-tui-784df98d19b70eca5906ce0ac7fcf2bfcd0f3515.tar.gz
ida-tui-784df98d19b70eca5906ce0ac7fcf2bfcd0f3515.tar.xz
ida-tui-784df98d19b70eca5906ce0ac7fcf2bfcd0f3515.zip
worker: surface the real startup failure (not just "code 1")
The worker's stderr was swallowed by the TUI, so an open failure showed only "worker exited during startup (code 1)". Now: * WorkerClient captures the worker's stdout+stderr to /tmp/idatui-worker-*.log and, on a startup exit, surfaces the last meaningful line in the error (the worker prints a clean 'WORKER-FATAL: ...' marker; _log_tail prefers it). * worker.py wraps main() to print that marker + traceback before exiting 1, and gives an ACTIONABLE open error: "failed to open <bin>: the .i64 is likely held by a running ida-mcp worker (pkill -f idalib) or wedged (delete .id0/.id1/ .id2/.nam/.til)". Also calls ida_auto.auto_wait() after open to fully match ida-mcp's session manager (open_database + auto_wait). Root cause of the reported failure is almost certainly a leftover ida-mcp worker still holding bash's .i64 from earlier --backend mcp runs: idalib can't open a database another process has locked. Fix: pkill -f idalib, then retry --backend worker; the error message now says so instead of "code 1".
Diffstat (limited to 'idatui')
-rw-r--r--idatui/worker.py21
-rw-r--r--idatui/worker_client.py27
2 files changed, 41 insertions, 7 deletions
diff --git a/idatui/worker.py b/idatui/worker.py
index d5fab20..e1d3c44 100644
--- a/idatui/worker.py
+++ b/idatui/worker.py
@@ -64,9 +64,13 @@ def _open_and_register(binpath: str):
against this live database. Returns (tools_dict, module_name, save_fn)."""
import idapro
idapro.enable_console_messages(False)
- rc = idapro.open_database(binpath, run_auto_analysis=True)
- if rc:
- raise RuntimeError(f"open_database({binpath!r}) failed rc={rc}")
+ if idapro.open_database(binpath, run_auto_analysis=True): # nonzero == failure
+ raise RuntimeError(
+ f"failed to open {binpath}: the .i64 is likely held by a running "
+ f"ida-mcp worker (try: pkill -f idalib) or wedged from a crash "
+ f"(delete its .id0/.id1/.id2/.nam/.til next to the binary)")
+ import ida_auto
+ ida_auto.auto_wait() # block until auto-analysis settles (match ida-mcp)
# importing the package registers all api_*/patched tools against MCP_SERVER
from ida_pro_mcp.ida_mcp import MCP_SERVER # noqa: WPS433
@@ -153,7 +157,16 @@ def main(argv=None) -> None:
if len(argv) < 2:
sys.stderr.write("usage: python -m idatui.worker <sock> <binary>\n")
raise SystemExit(2)
- serve(argv[0], argv[1])
+ try:
+ serve(argv[0], argv[1])
+ except SystemExit:
+ raise
+ except BaseException as e: # noqa: BLE001 -- surface a clean cause + code 1
+ import traceback
+ sys.stderr.write(f"\nWORKER-FATAL: {type(e).__name__}: {e}\n")
+ traceback.print_exc()
+ sys.stderr.flush()
+ raise SystemExit(1)
if __name__ == "__main__":
diff --git a/idatui/worker_client.py b/idatui/worker_client.py
index 49da8fa..70de11f 100644
--- a/idatui/worker_client.py
+++ b/idatui/worker_client.py
@@ -46,7 +46,9 @@ class WorkerClient:
python: str | None = None) -> None:
self._bin = os.path.abspath(os.path.expanduser(binary_path))
self._python = python or sys.executable
- self._sock_path = f"/tmp/idatui-worker-{os.getpid()}-{uuid.uuid4().hex[:8]}.sock"
+ tag = f"{os.getpid()}-{uuid.uuid4().hex[:8]}"
+ self._sock_path = f"/tmp/idatui-worker-{tag}.sock"
+ self._log_path = f"/tmp/idatui-worker-{tag}.log"
self._proc: subprocess.Popen | None = None
self._sock: socket.socket | None = None
self._sid = uuid.uuid4().hex[:8]
@@ -64,6 +66,8 @@ class WorkerClient:
[self._python, "-m", "idatui.worker",
self._sock_path, self._bin],
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+ stdout=open(self._log_path, "wb"),
+ stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL,
)
deadline = time.time() + timeout
t0 = time.time()
@@ -76,8 +80,8 @@ class WorkerClient:
except OSError:
if self._proc.poll() is not None:
raise IDAConnectionError(
- f"worker exited during startup (code "
- f"{self._proc.returncode})")
+ f"worker exited (code {self._proc.returncode}): "
+ f"{self._log_tail()} [full log: {self._log_path}]")
if progress:
progress(f"auto-analyzing {os.path.basename(self._bin)}… "
f"({int(time.time() - t0)}s)")
@@ -157,6 +161,23 @@ class WorkerClient:
def keepalive(self, interval: float = 120.0) -> _NoopKeepAlive:
return _NoopKeepAlive()
+ def _log_tail(self, n: int = 400) -> str:
+ """Last meaningful line(s) of the worker log (skip IDA's licence banner),
+ so a startup crash surfaces the real cause instead of just 'code 1'."""
+ try:
+ with open(self._log_path, encoding="utf-8", errors="replace") as f:
+ lines = [ln.strip() for ln in f if ln.strip()]
+ except OSError:
+ return "(no worker log)"
+ # the worker prints a clean 'WORKER-FATAL: ...' line on a startup crash
+ for ln in reversed(lines):
+ if ln.startswith("WORKER-FATAL:"):
+ return ln[len("WORKER-FATAL:"):].strip()[-n:]
+ skip = ("thank you", "licensed to", "[mcp]", "ida ", "hex-rays")
+ meaningful = [ln for ln in lines
+ if not any(s in ln.lower() for s in skip)]
+ return " | ".join((meaningful or lines)[-3:])[-n:]
+
# context manager parity with IDAClient
def __enter__(self) -> "WorkerClient":
return self.connect()