aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--idatui/app.py97
1 files changed, 95 insertions, 2 deletions
diff --git a/idatui/app.py b/idatui/app.py
index e54c418..6a47fd3 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -42,7 +42,7 @@ from textual.widgets.option_list import Option
from .highlight import highlight_c
-from .client import IDAClient, IDAToolError
+from .client import IDAClient, IDAToolError, IDAConnectionError
from .domain import DisasmModel, Func, Head, ListingModel, Program, Struct
# Styles for the disassembly listing.
@@ -2347,6 +2347,8 @@ class IdaTui(App):
self._decomp_return: NavEntry | None = None # listing to return to from F5
self._busy_screen: BusyScreen | None = None
self._xref_active = False # an xref gather is in flight (blocks re-entry)
+ self._conn_screen: LoadingScreen | None = None
+ self._reconnecting = False # a reconnect attempt is in flight
self._search_ctx: tuple[object | None, int] = (None, 1)
self._rename_ctx: tuple[object | None, str] = (None, "")
self._rename_addr: int | None = None # set for address-based (listing) naming
@@ -2444,7 +2446,10 @@ class IdaTui(App):
# -- status helper ----------------------------------------------------- #
def _status(self, text: str) -> None:
- self.query_one("#status", Static).update(text)
+ try:
+ self.query_one("#status", Static).update(text)
+ except Exception: # noqa: BLE001 -- status bar transiently unavailable
+ pass
if self._loading_screen is not None: # mirror progress into the overlay
self._loading_screen.update_note(text)
@@ -2469,6 +2474,94 @@ class IdaTui(App):
pass
return len(text)
+ # -- connection loss / recovery --------------------------------------- #
+ def _handle_exception(self, error: BaseException) -> None:
+ """Intercept a lost-connection error from any worker so the whole app
+ doesn't die when the analysis server goes away (it can idle out, be
+ killed, or the box can sleep). Everything else crashes as usual."""
+ from textual.worker import WorkerFailed
+ orig = error.error if isinstance(error, WorkerFailed) else error
+ if isinstance(orig, IDAConnectionError):
+ self._on_connection_lost()
+ return
+ super()._handle_exception(error)
+
+ def _on_connection_lost(self) -> None:
+ if self._reconnecting:
+ return
+ self._reconnecting = True
+ self._conn_screen = LoadingScreen(
+ "the analysis server", note="connection lost \u2014 reconnecting\u2026")
+ self.push_screen(self._conn_screen)
+ self._reconnect()
+
+ def _conn_note(self, text: str) -> None:
+ if self._conn_screen is not None:
+ self._conn_screen.update_note(text)
+
+ def _dismiss_conn(self) -> None:
+ cs = self._conn_screen
+ self._conn_screen = None
+ if cs is not None:
+ try:
+ cs.dismiss()
+ except Exception: # noqa: BLE001 -- already popped (Esc)
+ pass
+
+ @work(thread=True, exclusive=True, group="reconnect")
+ def _reconnect(self) -> None:
+ try:
+ from .launch import _ensure_server
+ if not _ensure_server(
+ self._url, self._open_path,
+ progress=lambda m: self.app.call_from_thread(self._conn_note, m)):
+ self.app.call_from_thread(self._reconnect_failed,
+ "analysis server is unreachable")
+ return
+ client = IDAClient(self._url, db=None)
+ client.connect()
+ if self._open_path is not None:
+ path = os.path.abspath(os.path.expanduser(self._open_path))
+ self.app.call_from_thread(
+ self._conn_note, f"re-opening {os.path.basename(path)}\u2026")
+ res = client.call("idb_open", input_path=path,
+ idle_ttl_sec=self._ttl, timeout=1800.0)
+ if not (isinstance(res, dict) and res.get("success")):
+ self.app.call_from_thread(self._reconnect_failed, "re-open failed")
+ return
+ client.set_db(res["session"]["session_id"])
+ else:
+ client.set_db(client.resolve_db())
+ client.health()
+ if self._ka is not None:
+ try:
+ self._ka.stop()
+ except Exception: # noqa: BLE001
+ pass
+ if self._do_keepalive:
+ self._ka = client.keepalive(interval=120.0).start()
+ program = Program(client)
+ except Exception as e: # noqa: BLE001
+ self.app.call_from_thread(self._reconnect_failed, str(e))
+ return
+ self.app.call_from_thread(self._after_reconnect, client, program)
+
+ def _after_reconnect(self, client: IDAClient, program: "Program") -> None:
+ self.client = client
+ self.program = program
+ self._reconnecting = False
+ self._dismiss_conn()
+ self._status("reconnected \u2014 reloading\u2026")
+ self._load_functions() # rebuild the function index against the new client
+ cur = self._cur
+ if cur is not None: # refresh the current view with the new program
+ self._open_entry(cur, push=False)
+
+ def _reconnect_failed(self, why: str) -> None:
+ self._reconnecting = False
+ self._conn_note(f"reconnect failed: {why} \u2014 retry on next action, or 'q'")
+ self._status(f"reconnect failed: {why}")
+
# -- connection + initial load ---------------------------------------- #
@work(thread=True, exclusive=True, group="connect")
def _connect(self) -> None: