22
33import contextlib
44import json
5+ import os
6+ import selectors
57import socket
68import subprocess
79import sys
2224from .schema import RequestEnvelope , response_ok
2325
2426IDALIB_CONNECT_RETRIES = 3
25- IDALIB_STARTUP_PROBE_TIMEOUT = 1.0
26- IDALIB_STARTUP_TIMEOUT = 10.0
27+ IDALIB_READY_MAX_BYTES = 65_536
2728
2829
2930@dataclass (frozen = True )
@@ -163,10 +164,45 @@ def _build_target_row(instance: IdaLibInstance) -> dict[str, Any]:
163164 )
164165
165166
167+ def _terminate_process (proc : subprocess .Popen [str ]) -> None :
168+ with contextlib .suppress (ProcessLookupError ):
169+ proc .terminate ()
170+ try :
171+ proc .wait (timeout = 5.0 )
172+ except subprocess .TimeoutExpired :
173+ with contextlib .suppress (ProcessLookupError ):
174+ proc .kill ()
175+ with contextlib .suppress (subprocess .TimeoutExpired ):
176+ proc .wait (timeout = 5.0 )
177+
178+
179+ def _read_ready_payload (read_fd : int , * , timeout : float | None ) -> dict [str , Any ]:
180+ try :
181+ with selectors .DefaultSelector () as selector :
182+ selector .register (read_fd , selectors .EVENT_READ )
183+ events = selector .select (timeout )
184+ if not events :
185+ raise socket .timeout ()
186+ raw = os .read (read_fd , IDALIB_READY_MAX_BYTES + 1 )
187+ finally :
188+ with contextlib .suppress (OSError ):
189+ os .close (read_fd )
190+
191+ if not raw :
192+ raise EOFError ("idalib daemon exited before reporting readiness" )
193+ if len (raw ) > IDALIB_READY_MAX_BYTES :
194+ raise RuntimeError ("idalib daemon readiness payload is too large" )
195+ raw = raw .split (b"\n " , 1 )[0 ].strip ()
196+ payload = json .loads (raw .decode ("utf-8" ))
197+ if not isinstance (payload , dict ):
198+ raise RuntimeError ("idalib daemon returned a non-object readiness payload" )
199+ return payload
200+
201+
166202def _start_daemon_for_database (
167203 database_path : str ,
168204 * ,
169- probe_timeout : float ,
205+ startup_timeout : float | None ,
170206 run_auto_analysis : bool ,
171207) -> IdaLibInstance :
172208 ensure_user_runtime_dir ()
@@ -179,40 +215,53 @@ def _start_daemon_for_database(
179215 ]
180216 if not run_auto_analysis :
181217 cmd .append ("--no-auto-analysis" )
218+ read_fd , write_fd = os .pipe ()
219+ os .set_inheritable (write_fd , True )
220+ cmd .extend (["--ready-fd" , str (write_fd )])
182221 with tempfile .TemporaryFile ("w+" , encoding = "utf-8" ) as stderr_log :
183- proc = subprocess .Popen (
184- cmd ,
185- stdin = subprocess .DEVNULL ,
186- stdout = subprocess .DEVNULL ,
187- stderr = stderr_log ,
188- text = True ,
189- start_new_session = True ,
190- )
222+ try :
223+ proc = subprocess .Popen (
224+ cmd ,
225+ stdin = subprocess .DEVNULL ,
226+ stdout = subprocess .DEVNULL ,
227+ stderr = stderr_log ,
228+ text = True ,
229+ start_new_session = True ,
230+ pass_fds = (write_fd ,),
231+ )
232+ except Exception :
233+ with contextlib .suppress (OSError ):
234+ os .close (read_fd )
235+ raise
236+ finally :
237+ with contextlib .suppress (OSError ):
238+ os .close (write_fd )
191239
192240 expected_registry = idalib_registry_path (proc .pid )
193- deadline = time .monotonic () + IDALIB_STARTUP_TIMEOUT
194- while time .monotonic () < deadline :
195- if proc .poll () is not None :
196- stderr_log .seek (0 )
197- detail = stderr_log .read ().strip ()
198- if detail :
199- raise RuntimeError (detail )
200- raise RuntimeError (f"idalib daemon failed to start for `{ database_path } `" )
241+ try :
242+ payload = _read_ready_payload (read_fd , timeout = startup_timeout )
243+ if not bool (payload .get ("ok" )):
244+ raise RuntimeError (str (payload .get ("error" ) or "idalib daemon failed to start" ))
201245 instance = _instance_from_registry (expected_registry )
202- if instance is not None and instance .database_path == database_path :
203- try :
204- if _probe_instance (
205- instance ,
206- timeout = probe_timeout ,
207- purge_on_failure = False ,
208- ):
209- return instance
210- except socket .timeout :
211- pass
212- time .sleep (0.05 )
213- with contextlib .suppress (ProcessLookupError ):
214- proc .terminate ()
215- raise RuntimeError (f"timed out waiting for idalib daemon to start for `{ database_path } `" )
246+ if instance is None :
247+ raise RuntimeError ("idalib daemon reported readiness but registry was unavailable" )
248+ if instance .database_path != database_path :
249+ raise RuntimeError (
250+ f"idalib daemon opened `{ instance .database_path } ` while `{ database_path } ` was requested"
251+ )
252+ return instance
253+ except socket .timeout as exc :
254+ _terminate_process (proc )
255+ raise RuntimeError (
256+ f"timed out after { _timeout_text (startup_timeout )} waiting for idalib daemon "
257+ f"to start for `{ database_path } `"
258+ ) from exc
259+ except EOFError as exc :
260+ stderr_log .seek (0 )
261+ detail = stderr_log .read ().strip ()
262+ if detail :
263+ raise RuntimeError (detail ) from exc
264+ raise RuntimeError (f"idalib daemon failed to start for `{ database_path } `" ) from exc
216265
217266
218267def _ensure_instance_for_database (
@@ -235,7 +284,7 @@ def _ensure_instance_for_database(
235284 return (
236285 _start_daemon_for_database (
237286 normalized ,
238- probe_timeout = IDALIB_STARTUP_PROBE_TIMEOUT ,
287+ startup_timeout = timeout ,
239288 run_auto_analysis = run_auto_analysis ,
240289 ),
241290 False ,
0 commit comments