aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/rpc.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-25 23:03:10 +0200
committerblasty <blasty@local>2026-07-25 23:03:10 +0200
commitb8d9aa4fce78f4523cffaba8b46095d2dfb66310 (patch)
tree97e73ec6b027a19cbeafe026aaf8b90bad0773d1 /idatui/rpc.py
parentprojects phase 3: follow an import into the binary that implements it (diff)
downloadida-tui-b8d9aa4fce78f4523cffaba8b46095d2dfb66310.tar.gz
ida-tui-b8d9aa4fce78f4523cffaba8b46095d2dfb66310.tar.xz
ida-tui-b8d9aa4fce78f4523cffaba8b46095d2dfb66310.zip
projects phase 4: cross-binary back, linkage-guided pre-warm, project verbs
Three things, all following from phase 3 making cross-binary jumps ordinary. **A cross-binary jump was a one-way door.** Nav history is per-binary, so arriving in another binary — a project search hit, or now following an import into the library that implements it — landed you in an empty history with nothing to take you back. _switch_then_goto records the binary it came FROM, and action_back falls through to that hop once local history is spent: Esc walks back through the function you were in, then the binary you were in. Manual Ctrl+O switching records nothing, because that isn't navigation. **Pre-warm follows the linkage graph, not list order.** _prewarm_provider warms the binary providing the most of this one's imports — where a follow is most likely to go, so its startup is paid before you ask for it. "Next in the list" would have been arbitrary; phase 3 gave us something better to ask. pool.prewarm() refuses rather than making room. Evicting a binary the user visited to speculatively load one they haven't is a straight downgrade, and it throws away that binary's caches as well; at a tight budget pre-warm just does nothing. The cost of a worker that doesn't exist yet can only be estimated, so it uses the largest resident one (same program, different database) — and if that estimate proves wrong, the speculative worker is the one evicted, never a chosen one. **Driving a project.** pane spawn --project FILE [--open BIN]; `binaries` lists the inventory (active / resident / indexed / where Esc returns to) and `switch {binary,addr?}` makes another active — with an address it takes the search-hit path, so it records a hop. state gains `binary` and `hops`, which it should have had the moment project mode existed. Verified on real sessions: drive binaries/switch against an echo+cat project pane; Esc crossing back from a switch; and prewarm on echo+libc picking libc (provider of echo's imports) and warming it after an evict. tests: +5 pool (prewarm warms, no-ops when resident, refuses at budget, evicts nothing when refusing, ignores unknown labels) and +4 project UI (jump records the hop, Esc crosses back, hop consumed). Confirmed the Esc-back checks fail with the branch removed. 195/0 scenarios, 27/0 project UI, 36/0 index, 27/0 pool, 33/0 project. Left open: project-level persistence across sessions.
Diffstat (limited to 'idatui/rpc.py')
-rw-r--r--idatui/rpc.py40
1 files changed, 40 insertions, 0 deletions
diff --git a/idatui/rpc.py b/idatui/rpc.py
index dd0c9c6..0921b89 100644
--- a/idatui/rpc.py
+++ b/idatui/rpc.py
@@ -72,6 +72,8 @@ METHODS = {
"search": "{term,direction?=1} incremental search in the code view",
"select": "{index?} choose the highlighted/nth item in the open modal",
"save": "persist the .i64 (Ctrl+S)",
+ "binaries": "-> project binaries {label,active,resident,indexed} (project mode)",
+ "switch": "{binary,addr?} make another project binary active (addr also jumps)",
"close": "dismiss a modal (Escape)",
"move": "{dir,n?=1} fast movement (down/up/.../pagedown)",
"cursor": "{line?,col?} set the code-pane cursor directly",
@@ -173,7 +175,9 @@ def snapshot(app) -> dict[str, Any]:
"cursor": _cursor_info(app, w),
"status": st,
"filter": app._filter_term,
+ "binary": app._binary, # None outside project mode
"nav_depth": len(app._nav),
+ "hops": list(getattr(app, "_hops", [])),
"dirty": bool(app._dirty),
"modal": _modal_snapshot(app),
**_readiness(app),
@@ -526,6 +530,42 @@ class RpcServer:
if method == "functions":
return functions(app, params.get("filter"), int(params.get("limit", 50)))
+ # -- projects ------------------------------------------------------ #
+ if method == "binaries":
+ if app._project is None:
+ raise ValueError("not a project session (launch with --project)")
+ counts = app._index.counts() if app._index is not None else {}
+ resident = set(app._pool.resident()) if app._pool is not None else set()
+ return {"active": app._binary, "hops": list(app._hops),
+ "binaries": [{"label": r.label, "source": r.source,
+ "active": r.label == app._binary,
+ "resident": r.label in resident,
+ "indexed": int(counts.get(r.label, 0))}
+ for r in app._project.refs]}
+
+ if method == "switch":
+ if app._project is None:
+ raise ValueError("not a project session (launch with --project)")
+ label = str(params.get("binary") or params.get("label") or "")
+ if app._project.by_label(label) is None:
+ have = ", ".join(r.label for r in app._project.refs)
+ raise ValueError(f"no such binary {label!r} (have: {have})")
+ addr = params.get("addr")
+ if label == app._binary and addr is None:
+ return snapshot(app)
+ if addr is None:
+ app._switch_binary(label)
+ else:
+ # Same path a project search hit takes, so it records a hop and
+ # Esc comes back here.
+ app._switch_then_goto(label, int(str(addr), 0)
+ if isinstance(addr, str) else int(addr))
+ await settle(app, lambda: app._binary == label
+ and app._func_index is not None
+ and app._func_index.complete,
+ timeout=float(params.get("timeout", 300.0)))
+ return snapshot(app)
+
# -- structured introspection (heavy: run off the UI loop) -------- #
loop = asyncio.get_running_loop()
if method == "pseudocode":