| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The Ctrl+P palette was Textual's stock system commands (change theme / take
screenshot / quit) -- useless for RE. Add an IdaCommands command Provider and set
IdaTui.COMMANDS = {IdaCommands} so the palette lists real actions instead:
goto, find symbol, follow, xrefs, toggle disasm/pseudocode, continuous listing,
hex, rename, retype, comment, define code/func, make data/string, undefine,
toggle opcodes, structs editor, filter, names pane, save, quit -- each with its
keybind as help text, fuzzy-searchable.
App-level actions run directly; cursor-scoped ones (rename/xrefs/comment/...) are
dispatched to the active code view via IdaTui._palette_action (focus + run the
view's action_*), so a palette pick does exactly what the key does.
Verified live over RPC: 'hex' switches view, 'goto' opens the prompt, 'struct'
opens the StructEditor modal, and the stock 'theme' command is gone. Pilot
command_palette scenario: Ctrl+P opens it and a command executes (7/7).
|
| |
|
|
|
|
|
|
|
|
|
| |
The logo.ans art is 33 rows tall; on a short/narrow terminal the auto-height
loading box would overflow and clip both the art and the title/note/help
(center-aligned, so the useful text scrolls off top and bottom). LoadingScreen
now only mounts the logo when app.size fits it plus the chrome (height >= lines+9
and width >= 64); otherwise it falls back to the text-only overlay.
Verified across sizes: 90x44 -> shown, 90x30 -> omitted (too short), 50x44 ->
omitted (too narrow), 120x50 -> shown.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Replace the clamp-to-nearest-edge follow with a true screen-position freeze:
watch_scroll_y now shifts the byte cursor by the exact scroll delta on a user
scroll (wheel/scrollbar), so it points at a new byte but stays on the same screen
row. Cursor-driven scrolls (_scroll_to_cursor via _apply_scroll) are marked with
_internal_top and skipped, so key-nav/click don't double-move the cursor. All
scroll sources funnel through the one watch point.
Pilot `hex` scenario: put the cursor mid-viewport, scroll 30 rows, assert its
screen row is unchanged (+ clicks still land on the exact byte). 10/10 pass.
|
| |
|
|
|
|
|
| |
_load_logo() reads logo.ans (repo root) once, strips the cursor show/hide escapes
so Rich sees only SGR, and parses it with Text.from_ansi (no_wrap). LoadingScreen
renders it above the title, centered in the box; missing/unreadable logo degrades
to no splash art. Verified: 33x60 art mounts in the modal and renders in 256-color.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Override watch_scroll_y so a viewport scroll drags the byte cursor along: when
the rounded scroll changes, clamp the cursor's row into the visible range
[top, top+height) keeping its column, so it rides the nearest edge instead of
being left off-screen. No feedback loop -- key-nav's _scroll_to_cursor already
puts the cursor in view (clamp is then a no-op), and HexView.Moved only updates
the status line. Pairs with the click-to-place support.
Pilot `hex` scenario updated: after a scroll the cursor must now be within the
visible rows (was: asserted it stayed put). 9/9 hex checks pass.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
HexView subclasses ScrollView directly and had no mouse handler, so clicking
never moved the byte cursor -- after a wheel-scroll the cursor was stuck off
screen with no way to reposition it by mouse. Add on_click: map the content
offset (scroll_offset + click, past the 1-col padding) to a row, and the x column
to a byte 0..15 across both the hex cells (3 cols each, +1 gap before byte 8) and
the ascii pane. Double-click jumps to code, mirroring Enter.
Pilot scenario extends `hex`: scroll the viewport (cursor goes off-screen), then
click in the hex pane and the ascii pane and assert the cursor lands on the exact
clicked row+byte. 9/9 hex checks pass.
|
| |
|
|
|
|
|
|
|
| |
The Architecture section still described the deleted client.py (HTTP MCP client);
replace it with worker.py + worker_client.py + errors.py, and note the
patch_server custom-tool injection. Modernize "What it does" (unified IDA-style
listing as the default view, F5/Tab decompile, xref kinds, the new line-motion
keys, fuzzy palette) and drop stale mentions of the 127.0.0.1:8745 supervisor and
"page over the MCP server".
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
ida-pro-mcp's xref_query only classifies xrefs as code/data (xr.iscode). Add an
injected `xref_types` tool (server/patch_server.py) that mirrors xref_query's
query/envelope shape but derives a fine `kind` from the IDA xref type:
call/jump/flow for code (fl_CF/CN/JF/JN/F), read/write/offset/text/info for data
(dr_R/W/O/T/I). The worker self-injects it on startup like the other custom tools.
* domain: Xref gains a `kind` field; _parse_xrefs reads it; xrefs_to() now calls
xref_types (falling back to xref_query if absent). xrefs_from is unchanged.
* app: the `x` dialog shows the kind as an aligned column after the address
(`000034F4 read sub_34F0+0x4`).
* rpc: the structured xrefs_to read carries `kind` too.
Verified live over the worker/RPC harness on targets/echo: sub_2C00 callers ->
call; __progname -> offset (GOT), read (sub_34F0), write (sub_3500); stdout ->
read/offset.
|
| |
|
|
|
|
|
|
|
|
|
| |
<home> goes to the true start of line (the address); shift+home now lands on the
mnemonic, skipping the address + opcode-bytes gutter. _insn_col() mirrors
_line_plain()'s prefix (address + indent + opcode field + member indent + name
prefix) so the column matches what's rendered; funchdr/label land after the
address, sep after the indent.
Verified live over RPC: on a code line, <home> -> col 0 (word '0000249A', the
address), shift+home -> col 40 (word 'push', the mnemonic).
|
| |
|
|
|
|
|
|
|
| |
Mirror the <end> change: in the unified ListingView, `home` now maps to col_home
(start of current line) instead of goto_top. Top/bottom move to the conventional
ctrl+home / ctrl+end (G still does bottom too), so no navigation is lost.
Verified live over RPC: <home> moves col 47->0 on the SAME line, scroll unchanged;
ctrl+home lands at line 0, ctrl+end/G at the last line.
|
| |
|
|
|
|
|
|
|
|
|
| |
In the unified ListingView, `end` was bound (with `G`) to goto_bottom, so it
triggered a vertical scroll. Rebind `end` -> col_end (same as `$`) so it moves the
column cursor to the end of the current line; `G` still does goto_bottom, and
`home`/`0` are unchanged.
Verified live over RPC: with the cursor forced to col 0, <end> moves col 0->47 on
the SAME line with scroll_y unchanged (no vertical jump); `G` still lands at the
last line.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
* RPC.md: the TUI is launched via `./ida-tui <bin> --rpc <sock>` (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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The idalib worker is the only backend now, so remove the dead HTTP/supervisor
surface entirely (~2200 lines):
* deleted idatui/client.py (the IDAClient HTTP/JSON-RPC transport + session
manager), idatui/tui.py (the old mcp TUI entry, superseded by launch.py),
spawn.sh, and systemd/ (the supervisor unit).
* deleted the mcp-only tests (stress_client, smoke_client, test_keepalive,
stress_paging, rpc_smoke, serverctl.sh, pane_smoke, test_domain) -- the worker
pilot (tests/test_scenarios.py) supersedes them.
* migrated the tmux RPC harness (idatui/pane.py) to the worker: it spawns
`idatui.launch <binary> --rpc <sock>` instead of the mcp `idatui.tui`, drops
the supervisor auto-start/ensure machinery, and reaps our own worker
(idatui/worker.py) instead of ida_pro_mcp.idalib_server. --db/--url/--no-
ensure-server are gone; --open is required.
* __init__ / __main__ / domain no longer import client (exceptions come from
errors.py, the domain client hint is WorkerClient); pyproject points both
console scripts at idatui.launch; README + ida-tui header describe the
worker-only flow.
What stays (by design): the ida_pro_mcp *package* (the worker reuses its @tool
functions in-process) and server/patch_server.py (the worker injects its custom
tools on startup). Verified: whole package imports + IdaTui constructs + pilot
lists 31 scenarios. The worker pilot (134 pass / 2 known flakes) is the E2E gate.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The idalib worker is now the sole backend for opening a binary, so remove the
ida-pro-mcp code paths from the hot path:
* app.py: IdaTui.__init__ drops url/db/ensure_server/backend (now just
open_path/keepalive/rpc_path/ttl); _connect calls _open_worker_client directly;
_open_mcp_client deleted; _reconnect respawns the worker only; self.client and
_after_reconnect typed WorkerClient. No more IDAClient import.
* launch.py: rewritten worker-only -- validate the binary, sweep stale locks,
spawn the TUI (which starts the private worker behind its overlay). The whole
supervisor dance (_ensure_server/_start_supervisor/_open_binary/
_existing_session) is gone; `ida-tui foo.elf` is the one usage.
* tests/test_scenarios.py: pilot is worker-only (run(binary); binary via
positional/--worker/--binary, defaults to targets/echo).
Verified: app + launch + pilot import and construct; pilot lists 31 scenarios.
The mcp modules (client.py/pane.py/tui.py/spawn.sh) still exist as dead code and
are deleted in the next commit. Worker pilot (134/2-known-flakes) still the gate.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
The IDAError/IDAConnectionError/IDAToolError/... exceptions and the Session
dataclass were defined in client.py (the ida-pro-mcp HTTP client), but the idalib
worker path (worker_client/domain/app) needs them without the HTTP transport.
Move them to a transport-agnostic errors.py; client.py re-exports them so the
deprecated mcp tooling and stress tests are unchanged (verified:
errors.IDAToolError IS client.IDAToolError, so cross-module `except` still works).
worker_client, domain (TYPE_CHECKING-guarded IDAClient hint), app, and __init__
now import the shared types from errors.py. This decouples the worker path from
client.py at runtime -- the prerequisite for deleting the mcp transport.
|
| |
|
|
|
|
| |
IdaTui.__init__ requires url and db positionally (the launcher passes url=args.url,
db=None even for the worker). The pilot's --worker branch omitted them ->
TypeError. Pass url="" (unused by the worker backend) and db=None.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
run()/main() gain a worker path: IdaTui(open_path=binary, backend="worker",
ensure_server=False) instead of attaching to an mcp supervisor via --db. boot()
and every scenario are backend-agnostic (they drive app.program), so the full
31-scenario suite runs unchanged on the worker. This is the verification gate
before deleting the mcp transport:
~/ida-venv/bin/python tests/test_scenarios.py --worker targets/echo
The default (no --worker) still uses the mcp supervisor + --db, so nothing
regresses until we pull the plug.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
The worker reuses ida-pro-mcp's @tool functions, but our custom tools
(heads/read_raw/resolve_names/func_types/del_type/set_lvar_type) only exist
because server/patch_server.py injected them into the installed api_types.py --
which historically only ran via spawn.sh. On a fresh box (or worker-only setup
with spawn.sh gone) those tools would be missing and heads/listing would break.
_ensure_tools_injected() now runs patch_server.main() (idempotent, IDA-free) at
the top of _open_and_register, before ida_pro_mcp.ida_mcp is imported, so the
worker guarantees its own tool surface. Verified under /usr/bin/python: injection
runs and heads/read_raw/resolve_names/func_types land in api_types.py. This is
the prerequisite for deleting spawn.sh from the worker path.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Opening a binary now defaults to our own idalib worker; the ida-pro-mcp HTTP
supervisor path is deprecated (kept only for --db/attach and --backend mcp).
* launch.py: --backend default resolves to worker for a fresh binary open, mcp
for the attach modes (--db / bare `ida-tui`, which have no worker equivalent);
explicit --backend or IDATUI_BACKEND still wins. Logs a deprecation notice when
the mcp path is used.
* Deprecation markers on client.py and server/patch_server.py; the ida-tui shell
header and README now describe the worker as primary and note
$IDATUI_WORKER_PYTHON. TODO tracks the removal checklist.
No code deleted yet — the mcp fallback stays until the worker is proven on a box
where idalib can spawn (pilot against --backend worker is the gate). Backend
resolution matrix verified: `ida-tui bash`->worker, bare/`--db`->mcp, explicit
flag/env honored.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The MCP server sets structuredContent = result if isinstance(result, dict) else
{"result": result} (zeromcp mcp.py:832), and domain.py parses that exact shape —
e.g. function_of() reads payload["result"] from lookup_funcs, which returns a
bare list. Our worker returned the raw list, so function_of got a non-dict and
returned None: hence "F5 — cursor is not inside a defined function" on Tab.
Replicate the rule in the worker's dispatch: dict passes through, anything else
(list/scalar) is wrapped as {"result": ...}. Fixes function_of and every other
list-returning tool (resolve_names, xref_query, list_funcs) in one shot.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Root cause of "ModuleNotFoundError: No module named 'ida_pro_mcp'": the worker was
spawned with sys.executable — the TUI's python (~/ida-venv) which has idalib +
textual but NOT ida_pro_mcp. The package split on this box:
/usr/bin/python : idapro + ida_pro_mcp (the "IDA python")
~/ida-venv/python : idapro + textual (the "TUI python", runs the app)
Fix: WorkerClient now auto-detects a python that can import ida_pro_mcp
(IDATUI_WORKER_PYTHON override, else /usr/bin/python[3], else sys.executable) and
runs worker.py as a SCRIPT rather than `-m idatui.worker`, so it doesn't import
the textual-dependent idatui package __init__ under a python that has no textual.
worker.py itself is pure stdlib at load; idapro/ida_pro_mcp are imported at
runtime (both present in the IDA python).
Verified: detection returns /usr/bin/python; worker.py loads clean there.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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".
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
step 3)
Wires WorkerClient into the app as a selectable backend, so you can launch:
ida-tui --backend worker /path/to/binary # or IDATUI_BACKEND=worker
* IdaTui gains a `backend` param. _connect is refactored into _open_mcp_client()
(the existing ida-pro-mcp path, unchanged) and _open_worker_client() (spawns a
private idalib worker via WorkerClient that opens+analyzes THIS binary and
streams progress into the loading overlay). The common tail (health, keepalive,
Program, load_functions) is shared, so domain.py and every view are untouched.
* _reconnect branches the same way: a dropped worker (segfault → closed socket)
respawns a fresh WorkerClient — the connection-loss recovery already built
works verbatim for the worker.
* launch.py adds --backend (env IDATUI_BACKEND); the worker path skips all the
supervisor/server plumbing (it owns a private worker) and just sweeps stale
locks + requires a binary. keepalive is a no-op for the worker (it never idles
out). mcp remains the default — nothing changes unless you opt in.
Verified without idalib: all four files parse; --backend is in --help; IdaTui
constructs for both backends with both openers present; WorkerClient covers the
full client surface. The idalib E2E (experiments/worker_smoke.py, and actually
launching --backend worker) still can't run in this sandbox — it now reaps every
idalib spawn before a byte is written — but the mcp default is untouched and the
worker path reuses proven pieces (the unix protocol benched at ~50us/call; the
worker dispatches the same tool functions the HTTP path does).
To validate on a real box: ida-tui --backend worker targets/echo (or run
experiments/worker_smoke.py for the headless read-path check).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
step 1
First concrete step off the mcp HTTP transport. Instead of reimplementing ~25
tools, reuse ida-pro-mcp's tool *functions* verbatim and replace only the
transport + process management:
* idatui/worker.py — opens ONE database in-process on the main thread (as idalib
requires), imports ida_pro_mcp (which registers every stock + our patched-in
custom tool against MCP_SERVER), then serves MCP_SERVER.tools.methods[name]
(**args) over a unix socket with length-prefixed pickle. Serial on the main
thread (idalib is single-threaded; tools run inline through execute_sync).
Session-management tools (idb_open/idb_save/server_health/idb_list) are shimmed
since the worker *is* the single session.
* idatui/worker_client.py — WorkerClient exposes the exact surface the app/domain
use on the client (call/call_envelope/connect/set_db/resolve_db/list_sessions/
health/keepalive/close) and returns byte-identical payloads (the worker calls
the same functions IDAClient.call ultimately hits). So domain.py and the app
are UNCHANGED — you just construct a WorkerClient instead of an IDAClient.
Calls are serialized under a lock over one socket; keepalive is a no-op (the
worker is ours and never idles out).
Not wired into the app yet — the mcp path is fully intact.
Verified without idalib: pickle framing round-trips arbitrary payloads incl raw
bytes; WorkerClient has full IDAClient surface; call_envelope produces the
result.structuredContent shape domain.decompile() reads. The idalib E2E
(experiments/worker_smoke.py drives the real domain.Program read path through the
worker) is written but couldn't run here — this sandbox has degraded to reaping
any idalib spawn; the underlying unix-socket protocol already ran clean in the
inproc_spike bench (~50us/call), and the worker dispatches the same tool
functions the HTTP path does, so shapes match by construction.
Next: stand up progress reporting during analysis, then flip _connect/_reconnect
to build a WorkerClient behind a flag and run the pilot suite against it.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Adds UnixWorkerBackend (Option C): the same DirectBackend, but in a child
process that opens idalib on ITS main thread and serves one client serially over
an AF_UNIX socket with length-prefixed pickle (bytes ride raw — no hex, no JSON).
bench() is now generic over {direct, unix, mcp}; --worker runs the child.
3-way result (echo, us/call):
op direct unix mcp unix-vs-mcp
resolve 1.4 42.9 4809 112x
read_bytes(16) 0.8 78.6 4450 57x
read_bytes(4096) 117.1 144.4 6312 44x
disasm_line 2.9 81.2 5423 67x
xrefs_to 40.6 77.9 4770 61x
decompile(cached) 2907 2712 47456 18x
Takeaways:
* A lean local IPC round-trip is ~40-80us — ~60-110x cheaper than the mcp
HTTP/JSON path (~5ms/call floor), while KEEPING crash isolation and the
main-thread decoupling (the freeze/segfault costs of full in-process).
* Bulk bytes are the tell: read_bytes(4096) is 144us unix vs 117us direct (1.2x
overhead) but 6.3ms over mcp — pickle ships 4096 raw bytes; mcp hex-encodes +
JSON-wraps them. The hex view would feel instant on unix.
* ~50us/call = ~20k calls/sec vs mcp's ~200/sec: most of idatui's prefetch/
paging/caching machinery exists to hide the 5ms; on a unix worker you'd barely
need it.
Conclusion this run supports: the sweet spot is Option C (own thin worker), not
full in-process — you capture ~99% of the practical latency win without the UI
freeze during analysis or the loss of crash isolation.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Standalone, not wired into the app. A tiny Backend seam (functions/resolve/
read_bytes/disasm_line/decompile/xrefs_to) with two impls — DirectBackend
(import idapro, in-process) and McpBackend (the current HTTP/JSON tool calls) —
so the "keep the transport or go direct?" question is measurable and feelable.
--bench : A/B latency table (opens a copy in-process; also hits :8745 if up)
--tui : minimal Textual app on the in-process backend; F5 decompiles INLINE
so you feel the main-thread hitch, 'd' decompiles all (big freeze)
Findings (echo, this box), all reproducible:
* open+auto-analysis in-process: ~0.4s (the whole "loading" cost, on the main
thread).
* per-op latency, direct vs mcp:
resolve 2.0us 4755us 2392x
read_bytes(16) 1.2us 4657us 3845x
read_bytes(4096) 112us 6037us 54x
disasm_line 2.9us 5239us 1805x
xrefs_to 25us 4962us 200x
decompile(cached) 2.8ms 51ms 18x
i.e. the mcp transport has a ~5ms/call floor regardless of op; the fast ops
idatui spams while scrolling are 1000-4000x cheaper in-process (which is why
the prefetch/paging/caching machinery exists).
* hard constraints proven separately: idalib must be imported/opened on the MAIN
python thread (installs a SIGINT handler) and every call must be on it
("Function can be called from the main thread only"); execute_sync from a
worker thread HANGS (no UI pump in headless). So in-process, IDA owns the one
main thread and blocks the event loop for each call — fine at <1ms, a hitch at
decompile (~150ms cold), a freeze during analysis. That main-thread coupling,
not just crash isolation, is what the subprocess boundary buys.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Leaving the TUI idle could let the analysis server go away (idle exit, killed,
box slept). The next action's worker then raised IDAConnectionError, which
Textual escalated to a fatal app exit (exit_on_error) — the whole TUI crashed
with a traceback.
Now IdaTui._handle_exception intercepts a WorkerFailed whose cause is
IDAConnectionError and, instead of dying, runs a reconnect:
* show a "connection lost — reconnecting…" overlay;
* _reconnect() restarts the supervisor if it's down (_ensure_server), makes a
fresh client, re-opens the binary (open_path) or re-resolves the sole session,
restarts the keepalive, and swaps in the new client/Program;
* _after_reconnect rebuilds the function index and refreshes the current view
with the new program; on failure it leaves a message and retries on the next
action. A re-entry guard avoids stacking attempts when many in-flight calls
fail at once. Any other exception still crashes as before.
Also harden _status() to never throw (query_one can miss #status during a screen
transition), so a status update can't take down a worker.
Verified: injecting IDAConnectionError into a nav worker -> no crash, overlay
shown, new Program swapped in, overlay cleared, post-reconnect calls work. Full
pilot suite green.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
DecompView.goto (repositioning within an already-loaded function, e.g. an xref
jump inside the current function) derived the horizontal scroll from the CURRENT
offset and kept it when the target column fell inside the (stale) viewport. So a
jump could land the cursor on the right column but leave the pane still scrolled
right from wherever you were, showing the line shifted/off.
Baseline the horizontal offset at 0 before computing: reset to 0, then scroll
right only if the target column is beyond the viewport width. The derive branch
is jump-only; back/forward restores pass an explicit scroll_x and are untouched.
Verified: goto with a stale scroll_x=19 -> scroll_x=0 after (col fits);
scroll_restore + decomp_nav/decomp_follow_self/follow_xrefs/xref_labels/
view_toggle/disasm_nav/search/mouse/listing_view/continuous_view/func_banners
green.
|
| |
|
|
|
|
|
|
| |
The "auto-analyzing…" note crammed the "first open of a big binary can take a
while" caveat onto one line that overflowed the fixed-width splash box (it showed
truncated as "…first open of a big binary can"). Move the caveat to the splash's
static help line, keep the dynamic note short, and (in app.py, already committed)
widen the box to 72 and let the note/help wrap (height: auto) as a safety net.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Gathering xrefs (xrefs_to + function_of over every site) runs in a worker, so the
UI stayed live and a second 'x' (or other action) fired into a half-finished
operation — duplicate requests, dialogs stacking, generally confusing behavior.
Add a small blocking BusyScreen modal shown the instant 'x' is pressed:
* re-entry guard (_xref_active) ignores a second 'x' while one gather is in
flight, and the modal captures input anyway;
* _present_xrefs dismisses it and opens the dialog when results are ready;
* Esc cancels — a worker that finishes after the cancel won't pop a stale dialog
(the _xref_active flag gates it);
* every exit path clears it (no-subject case, and a try/except around the
worker's slow calls) so it can't get stuck.
Verified: 1st 'x' -> BusyScreen (xref_active=True); 2nd 'x' ignored (stack
unchanged); results -> dialog + busy gone; Esc -> cancelled, no late dialog.
follow_xrefs/xref_labels/decomp_nav/decomp_follow_self/view_toggle/disasm_nav/
search/mouse/listing_view/continuous_view/func_banners/startup green.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Hex-Rays can attribute an address to a pseudocode line a step off from where the
referenced symbol actually appears, so an xref jump could land on a line that
doesn't contain the token (cursor at column 0, wrong line).
Add _decomp_locate: anchor on the /*0xEA*/ marker line for the address, but when
a token is known, snap to the whole-word occurrence NEAREST the anchor
(preferring the anchor line, then the line just below). This fixes the marker/
symbol line discrepancy and disambiguates a symbol that appears multiple times by
picking the occurrence closest to the address. The mid-function decomp-jump
branch now uses it.
Verified: real xref (sub_2060 from sub_20DD) still lands on 'sub_2060(a2);'
cursor_x=4; synthetic discrepancy (marker on a line without the symbol) snaps to
the nearest call-site line, not the far unrelated occurrence. Full suite green.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The decompiler-jump column fix used fn.name (the target's containing function),
which is correct when jumping to a function's own prototype but wrong for an
xref jump to a call SITE: the token on that line is the symbol xrefs was invoked
on, not the enclosing function. So x-pane jumps landed at column 0.
Use focus_name (the xref's subject, already threaded through _goto_ea) as the
token for a mid-function target; keep fn.name for the ea == fn.addr (prototype)
case.
Verified: xrefs to sub_2060 -> pick the caller site -> lands in sub_20DD's
pseudocode on ' sub_2060(a2);' with cursor_x=4 = 'sub_2060'. Full suite
(disasm_nav/view_toggle/search/follow_xrefs/xref_labels/mouse/decomp_nav/
decomp_follow_self/rename/rename_history/listing_view/continuous_view/
func_banners) green.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Jumping to a function in the decompiler (follow/xref into pseudocode) parked the
cursor at column 0 of the prototype line. Now it lands on the function's name in
the prototype instead, mirroring the listing's ref-column behavior.
_do_navigate's prefer_decomp branch: when the target is the function itself
(ea == fn.addr) it uses the prototype line (0) and computes the name's column via
_decomp_col_for; a mid-function target uses the ea-matched line (name column when
present, else 0). The column rides through _open_decomp_entry -> NavEntry
.dec_cursor_x -> _apply_decomp's view.show(cursor_x=...).
Verified: following sub_2060 from pseudocode lands on line 0, cursor_x=28, which
is exactly 'sub_2060' in "unsigned __int64 __fastcall sub_2060(int a1)".
decomp_nav/decomp_follow_self/xref_labels/view_toggle/disasm_nav/search/mouse/
listing_view/continuous_view/func_banners/rename green (follow_xrefs is a
pre-existing ordering flake; passes in isolation).
|
| |
|
|
|
|
|
|
|
|
|
| |
The supervisor opens + auto-analyzes its seed binary BEFORE it binds the port
(idalib_supervisor.main), so the whole "starting analysis server" wait is really
IDA running initial auto-analysis. Relabel the overlay notes to say so:
"starting IDA — initial auto-analysis of <bin>…" then
"auto-analyzing <bin>… (Ns) first open of a big binary can take a while".
Message-only change in _ensure_server (with the progress callback); no behavior
change. Verified the emitted notes with a mocked server-up poll.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The remaining "long wait with no feedback" was the launcher's _ensure_server: it
started the supervisor and blocked ~10s polling for the port BEFORE the TUI (and
its overlay) existed. Only a one-line stderr message covered it.
Move that wait behind the overlay. The launcher now only does instant checks
(sweep stale locks if the server is down, honor --no-server) and hands the binary
straight to the TUI with ensure_server=True. The TUI's _connect starts the
server itself and narrates it in the loading overlay ("starting analysis
server… (Ns)") before opening/analyzing the binary. idb_open is idempotent, so an
already-open session is still adopted instantly (the launcher no longer needs to
probe sessions).
_ensure_server gained a progress callback so it reports to the overlay instead of
stderr (which would corrupt the TUI screen).
Verified with the server DOWN: chrome + overlay appear within ~0.5s, notes go
"starting analysis server… (0s)" -> "opening echo — analyzing…" ->
"echo — 128 functions…" -> land on main. Pilot suite (db/ensure_server=False path)
green: startup/palette/view_toggle/disasm_nav/search/rename/follow_xrefs/
decomp_nav/mouse/listing_view/continuous_view/func_banners.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A binary whose worker was hard-killed mid-analysis leaves unpacked .id0/.id1/
.id2/.nam/.til files (no .i64). The supervisor is seeded with the target and
opens it on startup, so a wedged DB there crash-loops the whole supervisor
("Failed to open initial binary: Remote end closed connection") and the launcher
reports "supervisor exited during startup" -- before the TUI's own open-time
recovery can ever run.
When the launcher is about to start a fresh supervisor (server is down, so
nothing can hold the DB), proactively sweep the target's stale unpacked lock
files first (never the packed .i64). Safe because no server == no live session.
Verified: after clearing bash's stale locks the supervisor starts and stays up
20/20 checks over 20s with no crash loop and a clean log.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two parts:
1) A LoadingScreen modal is pushed on mount and dismissed once we land on a
function (or the symbol picker). Its note line mirrors _status, so you see
"opening… analyzing", then "N functions…", instead of staring at empty panes.
Esc hides it if you'd rather watch the status bar.
2) The real fix for the dead air BEFORE the TUI drew anything: the launcher used
to block on idb_open (full analysis of a big binary) *before* starting the
TUI, so the overlay only flashed after the wait was already over. Now the
launcher only adopts an already-open session (instant); otherwise it defers
the open to the TUI via open_path, and _connect does idb_open (with the
crashed-worker lock-sweep + retry moved here) while the chrome + overlay are
on screen. Added a ttl arg to IdaTui for the deferred open.
Verified: overlay pushed on mount, present through open+function-load, dismissed
on landing; TUI-driven open (open_path, db=None) opens echo and lands on main
with notes "opening echo — analyzing…" -> "echo — 128 functions…"; startup/
palette/view_toggle/disasm_nav/search/rename/follow_xrefs/decomp_nav/mouse/
listing_view/continuous_view/func_banners green.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Renaming a symbol in the decompiler re-decompiles and refreshes (good), but the
pane rendered at the wrong scroll until the first cursor move nudged it back.
Cause: _reload_active_code only cleared loaded_ea and re-showed, and dec_scroll_y
isn't tracked on every move, so _apply_decomp fell into show()'s DERIVE branch --
a bare scroll_to that, on a freshly shown pane whose size isn't computed yet,
clamps/leaves a stale frame until the next interaction.
Now the decomp refresh snapshots the LIVE pseudocode cursor + scroll from the
widget before forcing the recompile, so _apply_decomp takes show()'s robust
_apply_scroll path (deferred re-apply + refresh(layout=True)) and repaints at the
right place immediately.
Verified on main (421-line pseudocode, 22-row viewport): before rename
cursor=54/scroll=33; after rename cursor=54/scroll=33, cursor visible -- position
preserved with no stale frame. rename/rename_history/decomp_nav/
decomp_follow_self/view_toggle/follow_xrefs/xref_labels/listing_view/
continuous_view/func_banners/search/mouse/disasm_nav green (follow_xrefs was an
ordering flake; 14/0 in isolation).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
When an xref/follow jump targets a line, the cursor now lands on the COLUMN of
the referenced token (like search does) instead of the start of the line. The
focus_name already threaded into _do_navigate (but unused) is now carried to
_open_at -> _open_entry -> ListingView.load as a one-shot _pending_focus; once
the row is loaded, _on_primed locates the token via _word_occurrences (after the
opcode-column width is finalized) and sets cursor_x, then h-scrolls it into view.
Also pass focus_name from the double-click follow path. When the token isn't on
the destination line (e.g. jumping to a function entry), it's a no-op and the
cursor stays at column 0.
Verified: xref-jump to a 'call sub_2060' site lands cursor_x on 'sub_2060';
follow_xrefs/xref_labels/search/scroll_restore/disasm_nav/mouse/decomp_nav/
listing_view/continuous_view/func_banners/rename/view_toggle 70/0.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
On a large binary the renamed label appeared not to update: the edit refresh
reloaded the listing from a nav entry whose cursor/scroll could be stale, so on a
huge segment the rebuilt model primed/scrolled to the wrong index and the renamed
line landed off-screen (or in an unloaded region) -- looking like the rename
never propagated. (Small binaries hid it because everything is near the top.)
_reload_active_code now captures the LIVE listing position straight from the
widget (the source of truth) instead of trusting nav-entry tracking, so the
rebuilt model primes around the real cursor and the edited line stays put.
Verified: with _cur.cursor deliberately corrupted to 0, renaming a label at row
56 keeps it on screen (0000210E DEEPLBL:); decomp->Tab->rename and pure-listing
rename still stay on the label. rename/rename_history/comment/retype/view_toggle/
decomp_nav/decomp_follow_self/follow_xrefs/xref_labels/disasm_nav/search/mouse/
listing_view/continuous_view/func_banners/scroll_restore 70/0.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The previous fix reopened the listing via _goto_ea(push=False), which made _cur a
DETACHED entry (not _nav[-1]). Cursor moves update _nav[-1], so _cur.cursor went
stale: after moving to a label and renaming it, _reload_active_code ->
_open_entry(cur) restored the stale cursor and scrolled the just-renamed label
off-screen. The rename actually applied (model showed the new name) but the view
jumped away, so it looked like it "didn't propagate".
Now Tab from a jumped-to pseudocode view converts the CURRENT entry (which is
_nav[-1]) to a listing view in place (_toggle_to_listing): set view="listing",
position at the current line's address, and reopen. Cursor moves keep updating
the same entry, so a subsequent rename reloads at the cursor and the renamed
label stays on screen.
Verified: decomp jump -> Tab -> move to loc_ label -> rename: cursor stays on the
label, which now shows the new name (00002040 ZZLABEL:) in the viewport.
view_toggle/decomp_nav/decomp_follow_self/follow_xrefs/xref_labels/rename/
rename_history/disasm_nav/search/mouse/listing_view/continuous_view/func_banners/
scroll_restore 70/0.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Regression from the decomp-jump feature: _open_decomp_entry clears
_decomp_return, so Tab out of a jumped-to pseudocode view hit the fallback that
just re-showed the (stale) listing widget WITHOUT reopening it and left
_cur.view == "decomp". Two visible symptoms:
* the listing showed the wrong/old function, and
* a subsequent rename ran _reload_active_code -> _open_entry(cur), which saw
view == "decomp" and bounced back into the decompiler; the rename also
appeared not to propagate (you were looking at stale pseudocode).
Fixes:
* Tab from decomp with no F5 snapshot now opens the listing at the current
pseudocode line's address (a real listing view, _cur.view = "listing"),
so the correct function shows.
* _reload_active_code forces _cur.view = "listing" when refreshing the
listing, keeping the entry consistent with what's on screen.
Verified: F5 -> follow ref in decomp -> Tab -> listing shows the TARGET function;
renaming a label there stays in the listing and reflects the new name
(RELABELED: and 'jnz RELABELED'). view_toggle/decomp_nav/decomp_follow_self/
follow_xrefs/xref_labels/rename/rename_history/disasm_nav/search/mouse/
listing_view/continuous_view/func_banners 70/0.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Following a reference or picking an xref target from the pseudocode dropped you
back into the linear listing. Now such jumps stay in the decompiler when the
target is a decompilable function, landing on the pseudocode line that matches
the target address; non-decompilable targets still fall back to the listing.
* NavEntry gains a `view` field ("listing"/"decomp"); _open_entry restores an
entry in whichever view it was seen in (so back/forward also keep pseudocode).
* _do_navigate/_goto_ea take prefer_decomp; when set and the target function
decompiles, _open_decomp_entry opens it in the decompiler as a real nav-history
push (snapshotting the source pseudocode position so 'back' returns to it).
* _follow_decomp and _on_xref_chosen pass prefer_decomp when the source view is
the decompiler.
Verified: follow a ref from decomp -> lands in the target's pseudocode; 'back'
returns to the source pseudocode; listing follow still lands in the listing.
decomp_nav/decomp_follow_self/follow_xrefs/xref_labels/view_toggle/disasm_nav/
search/mouse/listing_view/func_banners/continuous_view 62/0.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Extend the jump-context behaviour: if the target is already visible in the
current viewport (same listing model, cursor index within [top, top+height)),
leave the scroll offset untouched and only move the cursor to the target. Only
off-screen jumps re-scroll (target parked _JUMP_CONTEXT lines below the top).
Verified: an in-viewport jump keeps scroll_top constant while moving the cursor
onto the target; off-screen jumps still get the 4-line context margin.
scroll_restore/disasm_nav/follow_xrefs/xref_labels/mouse/search/listing_view/
continuous_view/func_banners 54/0.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Jumping (goto/follow/xref/open) parked the target's first line on the very top
row of the viewport, which is cramped. Now a fresh navigation scrolls so the
target sits _JUMP_CONTEXT (4) lines down, leaving surrounding context above it;
the cursor still lands on the target's first instruction.
Only applies when the scroll is being derived (NavEntry.scroll_y == -1). Back/
forward restores carry their own saved scroll_y, so they still return to the
exact previous viewport.
Verified: jumps to sub_3720/sub_2060/main land the cursor on the entry with a
4-line margin above; scroll_restore/disasm_nav/follow_xrefs/mouse/search/
listing_view/continuous_view/func_banners 51/0.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
resolve(name) went through lookup_funcs, which for a mid-function label
(loc_/locret_) does get_name_ea -> label ea, then get_function(ea) and reports
the CONTAINING function's entry address. So double-clicking (or following) a
label jumped to the top of the function instead of the label's own address.
Add a server tool resolve_names that returns the address a name actually denotes
via idaapi.get_name_ea (functions, labels and data alike), and route
Program.resolve() through it, keeping lookup_funcs only as a fallback for the
"did you mean ..." suggestion on an unknown name.
Verified: resolve('loc_2040') -> 0x2040 (was the function entry); double-click
follow on a label row lands on the label ea. Regression sweep
follow_xrefs/xref_labels/rename/rename_history/decomp_follow_self/search/
listing_view/disasm_nav/mouse 50/0. Needs a supervisor restart (new tool).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Real, data-loss-capable bug: opening a function that is already the current one
(e.g. after scrolling away from it) schedules an async re-navigation. If the user
opens the search prompt ('/') before that navigation lands, the navigation's
_show_active() called lst.focus() and yanked focus OUT of the search box. The
next typed characters then routed to the listing as verbs -- and 'u' = undefine,
silently deleting a function. (This is what produced the flaky
"search finds nothing" + "no function for <ea>" cascade in the pilot.)
Fix: _show_active() no longer grabs focus for the code view while any prompt
overlay (search/rename/comment/retype/goto/func-filter) is visible -- those own
the keyboard until dismissed. View visibility is unchanged; only the focus grab
is suppressed.
Also harden the pilot's open(): wait for the listing cursor to actually LAND on
the target address, not merely for _cur.ea to match (which is stale-true when
re-opening the same function), so tests can't proceed on a not-yet-moved cursor.
Verified: the exact failing order disasm_nav->search->follow_xrefs is green;
broad focus-sensitive sweep (startup/disasm_nav/view_toggle/hex/search/rename/
comment_func/retype/follow_xrefs/decomp_nav/mouse/listing*/continuous_view/
func_banners/region_define) 89/0.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Revert the "search only the loaded portion" shortcut from f5aaf50 and go back to
a single, guarded background load_all before matching, so a search finds every
occurrence in the segment (not just whatever streamed in so far). The guard
(_search_loading + queued callbacks) shares ONE load across per-keystroke
updates instead of spawning/cancelling a worker each keypress.
The wedge that shortcut was meant to avoid turned out to be unrelated to the
search load: it is a pre-existing navigation bug (opening a function right after
scrolling to the bottom of another leaves the listing cursor out of range and
'/' fails to focus the search input, so typed chars leak into the view as
listing verbs, one of which is 'u'=undefine). Reproduces at 0c097f5, before any
of this work; tracked separately.
Verified in isolation: search 3.1s, matches found; search/mouse/func_banners/
listing_view/continuous_view/decomp_nav/follow_xrefs 45/0.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Editor-style highlight-all: whenever the cursor lands on an identifier, every
other occurrence of that same whole-word token is highlighted across the visible
listing AND the decompiler view (IDA's identifier-highlight behaviour).
* new _word_occurrences() finds whole-word spans in a line (reuses the same
char==cell alignment search already relies on).
* ColumnCursor gains _hl_word + _refresh_hl(): after any cursor move (h/l, w/b,
0/eol, j/k, mouse click, search jump) it recomputes the token under the cursor
and repaints the viewport only when it changed. Tokens must be >=2 chars and
start with a letter/underscore.
* ListingView.render_line and DecompView.render_line overlay every occurrence
with the existing _S_WORD background; the block cursor still marks the current.
Verified: cursor on 'rbp' highlights 5 visible listing rows; cursor on 'v23'
highlights it across the decompiler; mouse/disasm_nav/decomp_nav/listing_view/
func_banners/follow_xrefs/continuous_view/decomp_follow_self 42/0.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Code labels (loc_XXX/jump targets) get their OWN line at depth 0, like IDA,
instead of inline with the instruction: the heads walker (annotate) emits a
kind=label row ('loc_XXX:') before a named non-function-start code head and
strips the name from the instruction. ListingModel doesn't index label rows
(goto/xref/follow still land on the code); ListingView renders them at depth 0.
Also fix a search regression the extra rows exposed: search no longer forces a
blocking full-segment load_all (which stalled/thrashed the worker and wedged the
session on the larger annotated listing). It searches the loaded portion; the
background grower streams the rest and unloaded lines are skipped until they
arrive.
Verified: labels on their own line; search 2.9s (was 51.9s+wedge); search/mouse/
rename_history/continuous_view/func_banners/follow_xrefs 26/0.
|