aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-24 01:47:05 +0200
committerblasty <blasty@local>2026-07-24 01:47:05 +0200
commit2392521a1cd59420a1d75d741948dd0f03b080d3 (patch)
tree56e668396b54e959d452991510fc29a89f68d7b6
parentworker: surface the real startup failure (not just "code 1") (diff)
downloadida-tui-2392521a1cd59420a1d75d741948dd0f03b080d3.tar.gz
ida-tui-2392521a1cd59420a1d75d741948dd0f03b080d3.tar.xz
ida-tui-2392521a1cd59420a1d75d741948dd0f03b080d3.zip
worker: spawn under the IDA python (has ida_pro_mcp), not the TUI's
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.
-rw-r--r--idatui/worker_client.py37
1 files 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,
)