From b7717df55c8d9a230bd3672b82fb37a7552bbb76 Mon Sep 17 00:00:00 2001 From: blasty Date: Fri, 24 Jul 2026 15:11:54 +0200 Subject: docs: sweep for the worker-only reality (drop mcp supervisor/spawn.sh/--db) * RPC.md: the TUI is launched via `./ida-tui --rpc ` (was `idatui.tui --db`); point the "regression lock" note at rpcclient/drive + the pilot instead of the deleted rpc_smoke.py. * PAGING_FINDINGS.md: reframe the intro as the ida-pro-mcp tool functions the worker now calls in-process (shapes/caps unchanged); replace the supervisor-era "idle self-exit" + "max_workers cap" sections with the single-owned-worker lifecycle (no cap, no idle-exit, crash -> reconnect). * TEXTUAL_NOTES.md / TUI_DRIVING_BLUEPRINT.md: drop the ida-pro-mcp framing and the deleted rpc_smoke.py references (-> test_scenarios.py / rpcclient). Also updated the idatui + idatui-rpc skills (in ~/.pi, outside the repo) to the worker model: no supervisor/spawn.sh/--db, worker python + $IDATUI_WORKER_PYTHON, worker.py/worker_client.py/errors.py architecture, and the load-starvation gotcha. --- docs/PAGING_FINDINGS.md | 70 ++++++++++++++----------------------------- docs/RPC.md | 7 +++-- docs/TEXTUAL_NOTES.md | 2 +- docs/TUI_DRIVING_BLUEPRINT.md | 8 ++--- 4 files changed, 32 insertions(+), 55 deletions(-) (limited to 'docs') diff --git a/docs/PAGING_FINDINGS.md b/docs/PAGING_FINDINGS.md index d2187ae..bcde583 100644 --- a/docs/PAGING_FINDINGS.md +++ b/docs/PAGING_FINDINGS.md @@ -2,7 +2,10 @@ Measured against a real target: `libcrypto.so.3` (5.7 MB, **10,092 functions**, biggest function **52,120 instructions**). These constraints drive the domain / -paging layer. Reproduce with `tests/stress_paging.py --db `. +paging layer. They describe the ida-pro-mcp *tool functions* (`list_funcs`, +`disasm`, `decompile`, `xref_query`, …) which the idalib worker now calls +in-process (`idatui/worker.py`) — the shapes and caps below are the tools' +behaviour and are unchanged by dropping the HTTP transport. ## Response shape (list_* / *_query tools) @@ -93,52 +96,25 @@ fall back to the disassembly view or show an error panel. Normal decompile bodies are server-truncated with a `[N chars total]` marker (still to be solved for full-body display — see Phase 2). -## Workers self-exit when idle (`idle_ttl_sec`, default 600s) — SOLVED - -### Why it happens -Each idalib worker runs a `WorkerLifecycle` watchdog -(`ida_pro_mcp/worker_lifecycle.py`): if no JSON-RPC request hits its dispatcher -for `idle_ttl_sec` (default **600s**), the worker **self-exits cleanly**. It is -refcount-free — "activity == alive": every forwarded tool call `touch()`es the -timer. This is deliberate resource hygiene for a *shared* supervisor (each worker -holds an IDA license slot + the DB's RAM; default max 4 workers), aimed at -ephemeral/agent usage that opens a DB and wanders off. For an **interactive TUI -that should be able to just chill, it's the wrong default.** The startup binary -passed to `idalib-mcp` on the CLI always gets 600s (no CLI flag to change it). - -After self-exit the session lingers in `idb_list` but calls fail with -`"Worker for session is not reachable"` (distinct from `"Session not -found"`). - -### The fix (both implemented + verified in `tests/test_keepalive.py`) -1. **`IDAClient.bump_idle_ttl(idle_ttl_sec=1e9)`** — `idb_open` on the - already-open path is idempotent and re-applies the TTL; `set_idle_ttl` has no - upper cap, so ~1e9s is effectively 'never'. This is the intended knob and the - primary fix: the TUI calls it once at startup. -2. **`IDAClient.keepalive(interval=120)` -> `KeepAlive`** — a background - heartbeat issuing a cheap `server_health` (~2ms) that resets the watchdog. - Belt-and-suspenders, and it also covers *adopted* sessions whose path we don't - control / don't want to re-open. - -Recommended TUI policy: run a `KeepAlive` heartbeat (interval < TTL) to keep the -*running* session warm, and use a **moderate** idle TTL (not ‘never’) so the -worker is reclaimed after the TUI exits. "Worker not reachable" then only occurs -on a real crash, which the app handles by re-`idb_open`. - -## Supervisor worker cap (`max_workers`, default 4) - -The supervisor allows at most `max_workers` concurrently-open binaries; a 5th -`idb_open` fails with "Maximum idalib worker count reached". Before counting it -**prunes unreachable workers**, so a dead/killed worker frees its slot on the -next open. Two consequences drove design choices: - -* Do **not** make sessions immortal (`bump_idle_ttl(1e9)`) — they pile up to the - cap and never free. Instead: `KeepAlive` while running + a moderate TTL - (idatui uses 1800s on `--open`) so idle sessions self-exit after the TUI - closes and free their slot. -* `spawn.sh` sets `IDA_MCP_MAX_WORKERS` (default raised to 8) for headroom; to - free a stuck slot immediately, kill the idle worker process (it gets pruned on - the next open). +## Worker lifecycle (idatui's own idalib worker) + +idatui no longer uses ida-pro-mcp's shared HTTP supervisor. `idatui/worker.py` +opens exactly **one** database with `idapro.open_database(...)` in its own process +and serves tool calls over a unix socket (`WorkerClient`). Consequences vs the +old supervisor model, which several design choices here were built around: + +* **No `max_workers` cap, no cross-session contention.** Each TUI owns its + worker; there is no "Maximum idalib worker count reached" and no shared license + slot to free. +* **No idle self-exit / keepalive dance.** The old per-worker `WorkerLifecycle` + watchdog (`idle_ttl_sec`, default 600s) and the `KeepAlive` heartbeat that + fought it are gone with the supervisor. The worker lives as long as the TUI + holds the socket and dies with it. `WorkerClient.keepalive()` is a no-op kept + for API parity, and `--ttl` is passed through but the single owned worker does + not self-reap. +* **A crashed worker drops the socket**, surfacing as `IDAConnectionError`; the + app's `_reconnect` respawns a fresh worker (re-opening + re-analyzing the + binary). The hard-kill lock recovery below still applies. ## Writable path requirement (operational) diff --git a/docs/RPC.md b/docs/RPC.md index 4a68fbe..d1290f5 100644 --- a/docs/RPC.md +++ b/docs/RPC.md @@ -16,7 +16,7 @@ this doc is the underlying protocol reference. ``` # pane A — the TUI (real render) + a socket listener -python -m idatui.tui --db --rpc /tmp/ida.sock +./ida-tui /abs/path/to/binary --rpc /tmp/ida.sock # pane B — drive it. Ergonomic (auto-finds the socket, terse text): python -m idatui.drive where @@ -117,6 +117,7 @@ to read the pseudocode → `cursor line=.. col=..` onto a token → `rename name a generic worker-drain — read `state()` to confirm. - IDA renders some names without a leading `.` (e.g. `.init_proc` → `init_proc` in pseudocode); resolve/goto by the list name, but match tokens by what's shown. -- `tests/rpc_smoke.py` boots the app on a socket in-process and drives the whole - surface end-to-end — the protocol's regression lock. +- Drive the surface end-to-end from another process with `idatui.rpcclient` / + `idatui.drive`; the headless pilot (`tests/test_scenarios.py`) is the UI + regression lock and shares `_sync.py`'s settle logic with the RPC server. ``` diff --git a/docs/TEXTUAL_NOTES.md b/docs/TEXTUAL_NOTES.md index 6ce9037..4bf7fcb 100644 --- a/docs/TEXTUAL_NOTES.md +++ b/docs/TEXTUAL_NOTES.md @@ -1,7 +1,7 @@ # Textual notes & app patterns Hard-won Textual behaviour and the patterns this app relies on. Pairs with -`PAGING_FINDINGS.md` (the ida-pro-mcp/idalib side). +`PAGING_FINDINGS.md` (the idalib side). ## Textual pitfalls diff --git a/docs/TUI_DRIVING_BLUEPRINT.md b/docs/TUI_DRIVING_BLUEPRINT.md index 094f87a..cb16d73 100644 --- a/docs/TUI_DRIVING_BLUEPRINT.md +++ b/docs/TUI_DRIVING_BLUEPRINT.md @@ -328,9 +328,9 @@ terse, socket auto-resolved, every action visible in the gdb TUI pane. vocabulary — don't invent; mirror what a power user types). 4. Wire the **spawn command** + **readiness** into the pane manager. 5. Reuse transport, client, and ergonomic CLI unchanged. -6. Add a **smoke test** that spawns the target on a socket and drives the whole - surface end-to-end (idatui's `rpc_smoke.py` is the template — it also locks the - protocol against regressions). +6. Add a **regression test** that drives the surface end-to-end (idatui uses a + headless Textual Pilot suite, `tests/test_scenarios.py`, sharing the settle + logic with the live RPC server via `_sync.py`). ## 13. Pitfalls & lessons (paid for once already) @@ -352,4 +352,4 @@ terse, socket auto-resolved, every action visible in the gdb TUI pane. --- *Reference implementation: `idatui/{rpc,rpcclient,drive,pane,_sync}.py`, -`docs/RPC.md`, `tests/rpc_smoke.py` in this repo.* +`docs/RPC.md`, `tests/test_scenarios.py` in this repo.* -- cgit v1.3.1-sl0p