aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/launch.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-25 11:51:38 +0200
committerblasty <blasty@local>2026-07-25 11:51:38 +0200
commitf4cb6eba274c411c503cbc8b4ed10fdfc6b70dcb (patch)
treec9bd1e37b16725099e4d0351f91211443a5e2bf1 /idatui/launch.py
parentprojects: worker pool with memory-budgeted residency (phase 1b) (diff)
downloadida-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.py63
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