diff options
| author | blasty <blasty@local> | 2026-07-25 11:51:38 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-25 11:51:38 +0200 |
| commit | f4cb6eba274c411c503cbc8b4ed10fdfc6b70dcb (patch) | |
| tree | c9bd1e37b16725099e4d0351f91211443a5e2bf1 /idatui/launch.py | |
| parent | projects: worker pool with memory-budgeted residency (phase 1b) (diff) | |
| download | ida-tui-f4cb6eba274c411c503cbc8b4ed10fdfc6b70dcb.tar.gz ida-tui-f4cb6eba274c411c503cbc8b4ed10fdfc6b70dcb.tar.xz ida-tui-f4cb6eba274c411c503cbc8b4ed10fdfc6b70dcb.zip | |
projects: switch between a project's binaries in the TUI (phase 1c)
Wires the project model + worker pool into the app. Project mode is ADDITIVE —
without --project the app is exactly the single-binary tool it was, which is what
keeps the 167-check pilot meaningful.
* IdaTui(project=...) builds a WorkerPool and opens the project's first binary;
_open_worker_client asks the pool instead of spawning directly.
* BinaryState snapshots what a switch leaves behind (program, func_index, nav,
cur, view prefs, filter). Switching reuses the _after_reconnect shape: swap
client+program, rebuild the index, reopen the entry. A still-resident binary
restores instantly (Program + index are in memory); an evicted one gets a fresh
worker but keeps its nav history, which is just addresses.
* ProjectPalette (Ctrl+O, + a "Switch binary…" palette command): the project's
binaries with resident/analysed/pinned/active state and memory, filterable.
* launch.py --project FILE, creating the project when binaries are also given;
stages everything up front so the source tree is never written to.
Two bugs found while testing:
* _did_auto_land is app-wide, but landing is per-binary: after the first binary
landed, a cold switch never landed at all AND left the switch overlay up
forever. Reset it per switch.
* PRE-EXISTING: the status Static had Textual markup enabled, so a single-word
bracket marker parses as a style tag and is silently eaten — [listing] and
[pseudocode] have never actually rendered (only [split · listing] survived,
because the · makes it an invalid tag). Status is plain text with brackets and
symbol names, so markup=False.
tests/test_project_ui.py: end-to-end pilot on two real binaries (18 checks) —
boot, switcher contents, switch, per-binary index, both workers resident,
switch back with state intact, return-to-where-you-were, and the promise that
the source tree stays pristine while every artifact lands in the sidecar.
Full single-binary suite unchanged at 167/2 (the standing flakes).
Diffstat (limited to 'idatui/launch.py')
| -rw-r--r-- | idatui/launch.py | 63 |
1 files changed, 50 insertions, 13 deletions
diff --git a/idatui/launch.py b/idatui/launch.py index e2845e7..0ae63a1 100644 --- a/idatui/launch.py +++ b/idatui/launch.py @@ -45,7 +45,12 @@ def main(argv: list[str] | None = None) -> int: p = argparse.ArgumentParser( prog="ida-tui", description="Open a binary in the IDA TUI (private idalib worker).") - p.add_argument("binary", help="binary to open and analyze") + p.add_argument("binary", nargs="*", + help="binary to open and analyze (several with --project " + "creates/extends that project)") + p.add_argument("--project", metavar="FILE", + help="open a multi-binary project (created from the given " + "binaries if FILE doesn't exist)") p.add_argument("--ttl", type=int, default=1800, help="worker idle-TTL seconds (default 1800)") p.add_argument("--no-keepalive", action="store_true", @@ -54,17 +59,49 @@ def main(argv: list[str] | None = None) -> int: help="listen for RPC on this unix socket (puppeteer the TUI)") args = p.parse_args(argv) - binary = os.path.abspath(os.path.expanduser(args.binary)) - if not os.path.isfile(binary): - _log(f"no such file: {binary}") - return 2 - if not os.access(os.path.dirname(binary), os.W_OK): - _log(f"directory not writable (IDA writes a .i64 there): " - f"{os.path.dirname(binary)}") - return 2 - swept = _sweep_locks(binary) # a crashed worker can leave the DB wedged - if swept: - _log(f"cleared {swept} stale lock file(s) from a crashed worker") + project = None + binary = None + if args.project: + from .project import Project, ProjectError + ppath = os.path.abspath(os.path.expanduser(args.project)) + try: + if os.path.isfile(ppath): + project = Project.load(ppath) + for b in args.binary: # extend an existing project + project.add(b) + if args.binary: + project.save() + elif args.binary: + project = Project.create(ppath, args.binary) + _log(f"created project {ppath} with {len(project.refs)} binaries") + else: + _log(f"no such project: {ppath} (pass binaries to create it)") + return 2 + except ProjectError as e: + _log(str(e)) + return 2 + # Everything IDA writes lives in the project's sidecar, so the source + # tree is never touched and never needs to be writable. + try: + project.stage_all(progress=lambda m: _log(m)) + except ProjectError as e: + _log(str(e)) + return 2 + else: + if len(args.binary) != 1: + _log("give exactly one binary, or use --project for several") + return 2 + binary = os.path.abspath(os.path.expanduser(args.binary[0])) + if not os.path.isfile(binary): + _log(f"no such file: {binary}") + return 2 + if not os.access(os.path.dirname(binary), os.W_OK): + _log(f"directory not writable (IDA writes a .i64 there): " + f"{os.path.dirname(binary)}") + return 2 + swept = _sweep_locks(binary) # a crashed worker can leave the DB wedged + if swept: + _log(f"cleared {swept} stale lock file(s) from a crashed worker") # Hand off to the TUI (imported late so --help works without textual). It # spawns the worker behind its loading overlay while auto-analysis runs. @@ -75,7 +112,7 @@ def main(argv: list[str] | None = None) -> int: return 1 rpc_path = os.path.abspath(os.path.expanduser(args.rpc)) if args.rpc else None IdaTui(open_path=binary, keepalive=not args.no_keepalive, - rpc_path=rpc_path, ttl=args.ttl).run() + rpc_path=rpc_path, ttl=args.ttl, project=project).run() return 0 |
