aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--idatui/app.py97
-rw-r--r--idatui/launch.py23
2 files changed, 104 insertions, 16 deletions
diff --git a/idatui/app.py b/idatui/app.py
index 122ffcc..462aaf2 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -1939,6 +1939,34 @@ class ConfirmScreen(ModalScreen):
self.dismiss(False)
+class LoadingScreen(ModalScreen):
+ """Startup overlay shown while the binary is opened + analyzed, so a slow
+ load (big binary) isn't just empty panes and dead air. The app updates the
+ note line with progress and dismisses it once we land on a function."""
+
+ BINDINGS = [Binding("escape", "hide", "Hide")]
+
+ def __init__(self, title: str, note: str = "opening\u2026") -> None:
+ super().__init__()
+ self._title = title
+ self._note = note
+
+ def compose(self) -> ComposeResult:
+ with Vertical(id="loading-box"):
+ yield Static(f"\u23f3 loading {self._title}", id="loading-title")
+ yield Static(self._note, id="loading-note")
+ yield Static("analyzing the binary\u2026 (Esc to hide)", id="loading-help")
+
+ def update_note(self, text: str) -> None:
+ try:
+ self.query_one("#loading-note", Static).update(text)
+ except Exception: # noqa: BLE001 -- not mounted yet / already gone
+ pass
+
+ def action_hide(self) -> None:
+ self.dismiss()
+
+
# --------------------------------------------------------------------------- #
# Struct editor (C-style local type editor)
# --------------------------------------------------------------------------- #
@@ -2239,6 +2267,12 @@ class IdaTui(App):
background: $panel; padding: 1 2; }
#confirm-msg { height: auto; }
#confirm-help { height: 1; color: $text-muted; margin-top: 1; }
+ LoadingScreen { align: center middle; }
+ #loading-box { width: 64; height: auto; border: thick $accent;
+ background: $panel; padding: 1 2; }
+ #loading-title { height: 1; text-style: bold; }
+ #loading-note { height: 1; color: $text-muted; margin-top: 1; }
+ #loading-help { height: 1; color: $text-muted; }
"""
BINDINGS = [
@@ -2255,16 +2289,19 @@ class IdaTui(App):
]
def __init__(self, url: str, db: str | None, keepalive: bool = True,
- open_path: str | None = None, rpc_path: str | None = None) -> None:
+ open_path: str | None = None, rpc_path: str | None = None,
+ ttl: int = 1800) -> None:
super().__init__()
self._url = url
self._db = db
self._open_path = open_path
+ self._ttl = ttl
self._do_keepalive = keepalive
self._rpc_path = rpc_path
self._rpc = None
self.client: IDAClient | None = None
self.program: Program | None = None
+ self._loading_screen: LoadingScreen | None = None
self._ka = None
self._nav: list[NavEntry] = []
self._func_index = None # the unfiltered FunctionIndex (source of truth)
@@ -2337,10 +2374,30 @@ class IdaTui(App):
# The names pane is an overlay now (Ctrl+N); focus the default code view
# (pseudocode) so app bindings work before anything is opened.
self.query_one(ListingView).focus()
+ # Show a loading overlay immediately so a slow open/analysis (big binary)
+ # isn't just dead air behind empty panes; dismissed once we land.
+ self._loading_screen = LoadingScreen(self._loading_title())
+ self.push_screen(self._loading_screen)
self._connect()
if self._rpc_path:
self._start_rpc()
+ def _loading_title(self) -> str:
+ if self._open_path:
+ return os.path.basename(self._open_path)
+ if self._db:
+ return str(self._db)
+ return "database"
+
+ def _dismiss_loading(self) -> None:
+ ls = self._loading_screen
+ self._loading_screen = None
+ if ls is not None:
+ try:
+ ls.dismiss()
+ except Exception: # noqa: BLE001 -- already popped
+ pass
+
def _start_rpc(self) -> None:
from .rpc import RpcServer
self._rpc = RpcServer(self, self._rpc_path)
@@ -2358,6 +2415,8 @@ class IdaTui(App):
# -- status helper ----------------------------------------------------- #
def _status(self, text: str) -> None:
self.query_one("#status", Static).update(text)
+ if self._loading_screen is not None: # mirror progress into the overlay
+ self._loading_screen.update_note(text)
# -- clipboard --------------------------------------------------------- #
def _copy(self, text: str) -> int:
@@ -2387,17 +2446,37 @@ class IdaTui(App):
client = IDAClient(self._url, db=self._db)
client.connect()
if self._open_path is not None:
- self.app.call_from_thread(self._status, f"opening {self._open_path}…")
- import os
path = os.path.abspath(os.path.expanduser(self._open_path))
- # Moderate idle TTL: the keepalive heartbeat (below) keeps the
- # session alive while the TUI runs; once it exits the worker
- # idles out and frees its slot (avoids piling up to max-workers).
- res = client.call("idb_open", input_path=path,
- idle_ttl_sec=1800, timeout=1800.0)
+ base = os.path.basename(path)
+ self.app.call_from_thread(
+ self._status,
+ f"opening {base} — analyzing (a first open can take a while)…")
+
+ def _do_open():
+ # Moderate idle TTL: the keepalive heartbeat (below) keeps the
+ # session alive while the TUI runs; once it exits the worker
+ # idles out and frees its slot (avoids piling to max-workers).
+ return client.call("idb_open", input_path=path,
+ idle_ttl_sec=self._ttl, timeout=1800.0)
+
+ res = _do_open()
+ if not (isinstance(res, dict) and res.get("success")):
+ # A hard-killed worker can wedge the DB; sweep its stale
+ # unpacked lock files (never the .i64) and retry once.
+ try:
+ from .launch import _sweep_locks
+ swept = _sweep_locks(path)
+ except Exception: # noqa: BLE001
+ swept = 0
+ if swept:
+ self.app.call_from_thread(
+ self._status, f"recovering {base} (removed {swept} "
+ f"stale lock file(s))…")
+ res = _do_open()
if not (isinstance(res, dict) and res.get("success")):
err = res.get("error") if isinstance(res, dict) else res
self.app.call_from_thread(self._status, f"open failed: {err}")
+ self.app.call_from_thread(self._dismiss_loading)
return
client.set_db(res["session"]["session_id"])
elif self._db is None:
@@ -2411,6 +2490,7 @@ class IdaTui(App):
program = Program(client)
except Exception as e: # noqa: BLE001
self.app.call_from_thread(self._status, f"connect failed: {e}")
+ self.app.call_from_thread(self._dismiss_loading)
return
self.client = client
self.program = program
@@ -2456,6 +2536,7 @@ class IdaTui(App):
if self._cur is not None or self._did_auto_land or self._func_index is None:
return
self._did_auto_land = True
+ self._dismiss_loading() # binary is up; hand the screen back to the views
fn = self._entry_func()
if fn is not None:
self._open_function(fn.addr, fn.name)
diff --git a/idatui/launch.py b/idatui/launch.py
index c585668..b02bf05 100644
--- a/idatui/launch.py
+++ b/idatui/launch.py
@@ -202,21 +202,28 @@ def main(argv: list[str] | None = None) -> int:
_log("supervisor is down and --no-server was given")
return 1
- # 2) resolve the session (open/adopt the binary, or use --db / the sole one)
+ # 2) resolve the session. If a session is already open on this binary, adopt
+ # it (instant). Otherwise DON'T open here — defer the (possibly long)
+ # analysis to the TUI so its chrome + "loading…" overlay are on screen
+ # during the wait, instead of a blank terminal before the TUI even starts.
db = args.db
- if binary is not None:
+ open_path = None
+ if binary is not None and db is None:
try:
client = IDAClient(args.url)
client.connect()
- db = _open_binary(client, binary, args.ttl)
- except IDAError as e:
- _log(f"could not open {binary}: {e}")
- return 1
+ db = _existing_session(client, binary)
+ except Exception: # noqa: BLE001 -- probe only; the TUI will open it
+ db = None
finally:
try:
client.close()
except Exception: # noqa: BLE001
pass
+ if db:
+ _log(f"adopting the open session for {os.path.basename(binary)} ({db})")
+ else:
+ open_path = binary # TUI opens it with the loading overlay up
# 3) hand off to the TUI (imported late so --help works without textual)
try:
@@ -225,8 +232,8 @@ def main(argv: list[str] | None = None) -> int:
_log(f"the TUI needs textual; run with ~/ida-venv/bin/python ({e})")
return 1
rpc_path = os.path.abspath(os.path.expanduser(args.rpc)) if args.rpc else None
- IdaTui(url=args.url, db=db, keepalive=not args.no_keepalive,
- rpc_path=rpc_path).run()
+ IdaTui(url=args.url, db=db, open_path=open_path, ttl=args.ttl,
+ keepalive=not args.no_keepalive, rpc_path=rpc_path).run()
return 0