aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-23 23:52:23 +0200
committerblasty <blasty@local>2026-07-23 23:52:23 +0200
commit976fcb8440242d45565e19897f3ef1164a3c9940 (patch)
tree8644a5ac67645c1e9fda822cab3f712469a8c840
parentlaunch: sweep a crashed worker's stale DB locks before starting the supervisor (diff)
downloadida-tui-976fcb8440242d45565e19897f3ef1164a3c9940.tar.gz
ida-tui-976fcb8440242d45565e19897f3ef1164a3c9940.tar.xz
ida-tui-976fcb8440242d45565e19897f3ef1164a3c9940.zip
ux: start the analysis server from inside the TUI (no more dead window)
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.
-rw-r--r--idatui/app.py14
-rw-r--r--idatui/launch.py82
2 files changed, 46 insertions, 50 deletions
diff --git a/idatui/app.py b/idatui/app.py
index 462aaf2..6e9388f 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -2290,12 +2290,13 @@ class IdaTui(App):
def __init__(self, url: str, db: str | None, keepalive: bool = True,
open_path: str | None = None, rpc_path: str | None = None,
- ttl: int = 1800) -> None:
+ ttl: int = 1800, ensure_server: bool = False) -> None:
super().__init__()
self._url = url
self._db = db
self._open_path = open_path
self._ttl = ttl
+ self._ensure_server = ensure_server
self._do_keepalive = keepalive
self._rpc_path = rpc_path
self._rpc = None
@@ -2443,6 +2444,17 @@ class IdaTui(App):
@work(thread=True, exclusive=True, group="connect")
def _connect(self) -> None:
try:
+ if self._ensure_server:
+ # Start the analysis server here (not in the launcher) so the
+ # chrome + overlay are already up while we wait for it.
+ from .launch import _ensure_server as _ensure
+ if not _ensure(self._url, self._open_path,
+ progress=lambda m: self.app.call_from_thread(
+ self._status, m)):
+ self.app.call_from_thread(
+ self._status, "could not start the analysis server")
+ self.app.call_from_thread(self._dismiss_loading)
+ return
client = IDAClient(self._url, db=self._db)
client.connect()
if self._open_path is not None:
diff --git a/idatui/launch.py b/idatui/launch.py
index 1acd99a..7432a53 100644
--- a/idatui/launch.py
+++ b/idatui/launch.py
@@ -68,30 +68,34 @@ def _start_supervisor(target: str | None) -> subprocess.Popen | None:
return None
-def _ensure_server(url: str, target: str | None, timeout: float = 90.0) -> bool:
+def _ensure_server(url: str, target: str | None, timeout: float = 90.0,
+ progress=None) -> bool:
"""Make sure the supervisor is listening; start it if not. Returns True when
- the port is up. Only auto-starts a local server."""
+ the port is up. Only auto-starts a local server. ``progress`` (if given) gets
+ human-readable status lines instead of stderr — so the TUI can narrate the
+ wait in its loading overlay."""
+ def note(msg: str) -> None:
+ (progress or _log)(msg)
+
host, port = _server_addr(url)
if _server_up(host, port):
return True
if host not in ("127.0.0.1", "localhost", "::1"):
- _log(f"server at {host}:{port} is down and not local — cannot auto-start")
+ note(f"server at {host}:{port} is down and not local — cannot auto-start")
return False
- _log(f"supervisor not running — starting it (log: {_server_log_path()})")
+ note(f"starting analysis server (log: {_server_log_path()})…")
proc = _start_supervisor(target)
deadline = time.time() + timeout
- spun = False
+ t0 = time.time()
while time.time() < deadline:
if _server_up(host, port):
- if spun:
- _log("supervisor is up")
return True
if proc is not None and proc.poll() is not None:
- _log("supervisor exited during startup — check the log above")
+ note("analysis server exited during startup — check the log")
return False
- spun = True
+ note(f"starting analysis server… ({int(time.time() - t0)}s)")
time.sleep(0.5)
- _log(f"supervisor did not come up within {timeout:.0f}s")
+ note(f"analysis server did not come up within {timeout:.0f}s")
return False
@@ -194,54 +198,34 @@ def main(argv: list[str] | None = None) -> int:
f"{os.path.dirname(binary)}")
return 2
- # 1) supervisor up (seed it with our target so its initial worker is useful)
- if not args.no_server:
- # If we're about to start a fresh supervisor (it's down), nothing holds
- # this binary's DB, so clear any stale unpacked lock files a crashed
- # worker left behind. The supervisor is seeded with this target and opens
- # it on startup; a wedged DB there crash-loops the whole supervisor
- # before the TUI's own recovery can ever run.
- if binary is not None and not _server_up(*_server_addr(args.url)):
- swept = _sweep_locks(binary)
- if swept:
- _log(f"cleared {swept} stale lock file(s) from a crashed worker")
- if not _ensure_server(args.url, binary):
- return 1
- elif not _server_up(*_server_addr(args.url)):
+ # Everything slow — starting the supervisor and opening/analyzing the binary
+ # — is deferred to the TUI so its chrome + "loading…" overlay are on screen
+ # the whole time, instead of a blank terminal before the TUI even starts.
+ # Here we only do the instant checks.
+ server_down = not _server_up(*_server_addr(args.url))
+ if args.no_server and server_down:
_log("supervisor is down and --no-server was given")
return 1
+ if binary is not None and server_down:
+ # About to start a fresh supervisor (nothing holds the DB): clear any
+ # stale unpacked lock files a crashed worker left, so the supervisor's
+ # seeded initial open doesn't crash-loop on a wedged DB.
+ swept = _sweep_locks(binary)
+ if swept:
+ _log(f"cleared {swept} stale lock file(s) from a crashed worker")
- # 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
- open_path = None
- if binary is not None and db is None:
- try:
- client = IDAClient(args.url)
- client.connect()
- 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)
+ # Hand off to the TUI (imported late so --help works without textual). It
+ # ensures the server (with on-screen progress), then opens/adopts the binary
+ # — idb_open is idempotent, so an already-open session is adopted instantly.
try:
from .app import IdaTui
except ImportError as e:
_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, open_path=open_path, ttl=args.ttl,
+ IdaTui(url=args.url, db=args.db,
+ open_path=(binary if args.db is None else None), ttl=args.ttl,
+ ensure_server=not args.no_server,
keepalive=not args.no_keepalive, rpc_path=rpc_path).run()
return 0