From 89db0e023d5bafbd1868f22b9b31e5006066cdad Mon Sep 17 00:00:00 2001 From: blasty Date: Fri, 7 Aug 2026 00:14:28 +0200 Subject: 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. --- idatui/worker_client.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) (limited to 'idatui/worker_client.py') 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: -- cgit v1.3.1-sl0p