Return 'process' or 'thread' for parallel parsing. Defaults to ``process`` (the original behavior, fastest on Linux/macOS). Auto-switches to ``thread`` when running on Windows with stdin not attached to a TTY — that combination indicates an MCP/stdio host, where ``ProcessPoolExecuto
()
| 26 | |
| 27 | |
| 28 | def _select_executor_kind() -> str: |
| 29 | """Return 'process' or 'thread' for parallel parsing. |
| 30 | |
| 31 | Defaults to ``process`` (the original behavior, fastest on Linux/macOS). |
| 32 | Auto-switches to ``thread`` when running on Windows with stdin not |
| 33 | attached to a TTY — that combination indicates an MCP/stdio host, where |
| 34 | ``ProcessPoolExecutor`` workers inherit the parent's pipe handles and |
| 35 | leak as zombies after the pool closes (issues #46, #136). |
| 36 | |
| 37 | Override explicitly with ``CRG_PARSE_EXECUTOR={process,thread}``. |
| 38 | |
| 39 | Tree-sitter parsing in the worker releases the GIL during native |
| 40 | parsing, so the speedup loss for falling back to threads is small |
| 41 | (typically <30% on the full-build path) and the trade is worth it |
| 42 | to avoid the deadlock + zombie process accumulation. |
| 43 | """ |
| 44 | explicit = os.environ.get("CRG_PARSE_EXECUTOR", "").strip().lower() |
| 45 | if explicit in ("process", "thread"): |
| 46 | return explicit |
| 47 | if sys.platform == "win32" and not sys.stdin.isatty(): |
| 48 | return "thread" |
| 49 | return "process" |
| 50 | |
| 51 | |
| 52 | def _make_executor(max_workers: int): |