From 2392521a1cd59420a1d75d741948dd0f03b080d3 Mon Sep 17 00:00:00 2001 From: blasty Date: Fri, 24 Jul 2026 01:47:05 +0200 Subject: worker: spawn under the IDA python (has ida_pro_mcp), not the TUI's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of "ModuleNotFoundError: No module named 'ida_pro_mcp'": the worker was spawned with sys.executable — the TUI's python (~/ida-venv) which has idalib + textual but NOT ida_pro_mcp. The package split on this box: /usr/bin/python : idapro + ida_pro_mcp (the "IDA python") ~/ida-venv/python : idapro + textual (the "TUI python", runs the app) Fix: WorkerClient now auto-detects a python that can import ida_pro_mcp (IDATUI_WORKER_PYTHON override, else /usr/bin/python[3], else sys.executable) and runs worker.py as a SCRIPT rather than `-m idatui.worker`, so it doesn't import the textual-dependent idatui package __init__ under a python that has no textual. worker.py itself is pure stdlib at load; idapro/ida_pro_mcp are imported at runtime (both present in the IDA python). Verified: detection returns /usr/bin/python; worker.py loads clean there. --- idatui/worker_client.py | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/idatui/worker_client.py b/idatui/worker_client.py index 70de11f..8dd8491 100644 --- a/idatui/worker_client.py +++ b/idatui/worker_client.py @@ -27,6 +27,34 @@ from .client import IDAToolError, IDAConnectionError, Session from .worker import recv as _recv from .worker import send as _send +_WORKER_PY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "worker.py") +_worker_python_cache: str | None = None + + +def _find_worker_python() -> str: + """A python that can import ``ida_pro_mcp`` (and thus idalib) — NOT necessarily + the TUI's python. On a typical box the TUI runs under a venv that has textual + + idalib but not ida_pro_mcp, while the system python has idalib + + ida_pro_mcp. Override with IDATUI_WORKER_PYTHON.""" + global _worker_python_cache + if _worker_python_cache: + return _worker_python_cache + override = os.environ.get("IDATUI_WORKER_PYTHON") + candidates = [override] if override else [] + candidates += ["/usr/bin/python", "/usr/bin/python3", sys.executable] + for py in candidates: + if not py or not os.path.exists(py): + continue + try: + r = subprocess.run([py, "-c", "import ida_pro_mcp"], + capture_output=True, timeout=30) + if r.returncode == 0: + _worker_python_cache = py + return py + except Exception: # noqa: BLE001 + continue + return sys.executable # last resort; the worker will report the real error + class _NoopKeepAlive: """The worker is ours and never idles out, so keepalive is a no-op.""" @@ -45,7 +73,7 @@ class WorkerClient: def __init__(self, binary_path: str, *, ttl: int = 0, python: str | None = None) -> None: self._bin = os.path.abspath(os.path.expanduser(binary_path)) - self._python = python or sys.executable + self._python = python or _find_worker_python() tag = f"{os.getpid()}-{uuid.uuid4().hex[:8]}" self._sock_path = f"/tmp/idatui-worker-{tag}.sock" self._log_path = f"/tmp/idatui-worker-{tag}.log" @@ -62,10 +90,11 @@ class WorkerClient: if self._sock is not None: return self if self._proc is None or self._proc.poll() is not None: + # run worker.py as a SCRIPT (not -m idatui.worker) so we don't + # import the textual-dependent idatui package __init__ under the + # IDA python, which usually has no textual. self._proc = subprocess.Popen( - [self._python, "-m", "idatui.worker", - self._sock_path, self._bin], - cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + [self._python, _WORKER_PY, self._sock_path, self._bin], stdout=open(self._log_path, "wb"), stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, ) -- cgit v1.3.1-sl0p