diff options
| -rw-r--r-- | idatui/app.py | 97 | ||||
| -rw-r--r-- | idatui/launch.py | 23 |
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 |
