aboutsummaryrefslogtreecommitdiffstats
path: root/idatui
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-08-07 00:14:28 +0200
committerblasty <blasty@local>2026-08-07 00:14:28 +0200
commit89db0e023d5bafbd1868f22b9b31e5006066cdad (patch)
treea0e96eae22822be537d847e2c684ee5f166eb057 /idatui
parentapp: the view mode is a type, and 'disasm' is gone (diff)
downloadida-tui-89db0e023d5bafbd1868f22b9b31e5006066cdad.tar.gz
ida-tui-89db0e023d5bafbd1868f22b9b31e5006066cdad.tar.xz
ida-tui-89db0e023d5bafbd1868f22b9b31e5006066cdad.zip
worker_client: tests, and stop resurrecting a closed worker
The layer between the app and idalib had no tests, which is awkward: it is where failures are silent. A worker that dies during startup, a socket that drops mid-call, two UI threads sharing one socket -- none of those look like bugs from outside, they look like the TUI hanging or showing stale data. None of it needs IDA. WorkerClient spawns whatever _WORKER_PY points at, so the suite points it at a fake speaking the same length-prefixed pickle and tells it to misbehave on demand: die at startup, never bind, drop the connection, fail a tool, take its time. 40 checks in the --fast tier. Two things the tests found: call() reconnects when _sock is None, which is what makes a dropped socket recoverable -- but it made an explicitly CLOSED client resurrect too, spawning a whole new idalib worker to serve one stray call (verified: pid 1066961 -> 1066962). close() runs on teardown and on binary-switch while @work threads are still in flight, so quitting during a decompile could leave a fresh process re-opening the .i64 we had just released, which is the wedging hazard. A closed client now refuses; connect() still revives it, which is all _reconnect needs (it builds a new client anyway). connect() polled on a flat 0.2s sleep, so every caller paid a fifth of a second even when the worker was ready in milliseconds -- a seeded .i64, a small binary. Backs off from 5ms instead. 786 checks, 144.6s; --fast is 297 in 3.3s.
Diffstat (limited to 'idatui')
-rw-r--r--idatui/worker_client.py20
1 files changed, 19 insertions, 1 deletions
diff --git a/idatui/worker_client.py b/idatui/worker_client.py
index 79a6db9..59c62a1 100644
--- a/idatui/worker_client.py
+++ b/idatui/worker_client.py
@@ -83,11 +83,19 @@ class WorkerClient:
self._sid = uuid.uuid4().hex[:8]
self._lock = threading.Lock() # serialize socket use
self._spawn_lock = threading.Lock()
+ #: Set by close(). A dropped socket is respawned on the next call (the
+ #: worker segfaulted and we want it back); a CLOSED one must not be.
+ #: Teardown and binary-switch both close while @work threads are still
+ #: in flight, so without this, quitting during a decompile spawned a
+ #: fresh idalib worker that re-opened the database nobody was looking
+ #: at any more -- a stray process holding the .i64 we just released.
+ self._closed = False
# -- lifecycle --------------------------------------------------------- #
def connect(self, timeout: float = 1800.0, progress=None) -> "WorkerClient":
"""Spawn the worker (opens + analyzes the DB) and connect once ready."""
with self._spawn_lock:
+ self._closed = False # an explicit reconnect revives this client
if self._sock is not None:
return self
if self._proc is None or self._proc.poll() is not None:
@@ -104,6 +112,11 @@ class WorkerClient:
)
deadline = time.time() + timeout
t0 = time.time()
+ # Poll fast at first, then back off. A flat 0.2s cost every caller a
+ # fifth of a second even when the worker was ready in milliseconds
+ # (a small binary, or a seeded .i64), which is most of the time in
+ # the tests and noticeable on a re-open.
+ delay = 0.005
while time.time() < deadline:
try:
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
@@ -118,7 +131,8 @@ class WorkerClient:
if progress:
progress(f"auto-analyzing {os.path.basename(self._bin)}… "
f"({int(time.time() - t0)}s)")
- time.sleep(0.2)
+ time.sleep(delay)
+ delay = min(delay * 1.6, 0.2)
raise IDAConnectionError("worker did not become ready in time")
@property
@@ -137,6 +151,7 @@ class WorkerClient:
with self._lock:
s = self._sock
self._sock = None
+ self._closed = True
if s is not None:
try:
_send(s, ("__shutdown__", {}))
@@ -161,6 +176,9 @@ class WorkerClient:
# -- the call surface -------------------------------------------------- #
def call(self, tool: str, *, timeout: float | None = None, **args) -> Any:
+ if self._closed:
+ raise IDAConnectionError(
+ f"{tool}: this worker was closed (call connect() to revive it)")
if self._sock is None:
self.connect()
with self._lock: