Owns one persistent ``cmdop-core`` subprocess and the id-mux over it.
| 85 | |
| 86 | |
| 87 | class Transport: |
| 88 | """Owns one persistent ``cmdop-core`` subprocess and the id-mux over it.""" |
| 89 | |
| 90 | def __init__(self, cfg: ClientConfig, bin_path: str) -> None: |
| 91 | self._cfg = cfg |
| 92 | self._bin = bin_path |
| 93 | self._proc: asyncio.subprocess.Process | None = None |
| 94 | self._reader_task: asyncio.Task | None = None |
| 95 | self._stderr_task: asyncio.Task | None = None |
| 96 | self._pending: dict[int, _Pending] = {} |
| 97 | self._next_id = 1 |
| 98 | self._closed = False |
| 99 | self._start_lock = asyncio.Lock() |
| 100 | |
| 101 | # -- lifecycle --------------------------------------------------------- |
| 102 | |
| 103 | async def _ensure_started(self) -> asyncio.subprocess.Process: |
| 104 | if self._proc is not None: |
| 105 | return self._proc |
| 106 | async with self._start_lock: |
| 107 | if self._proc is not None: |
| 108 | return self._proc |
| 109 | env = { |
| 110 | **os.environ, |
| 111 | "CMDOP_TOKEN": self._cfg.token, |
| 112 | "CMDOP_BASE_URL": self._cfg.base_url, |
| 113 | # Django platform plane (skills) — separate credential + base URL. |
| 114 | "CMDOP_API_BASE_URL": self._cfg.api_base_url, |
| 115 | } |
| 116 | if self._cfg.api_key: |
| 117 | env["CMDOP_API_KEY"] = self._cfg.api_key |
| 118 | proc = await asyncio.create_subprocess_exec( |
| 119 | self._bin, |
| 120 | "--stdio", |
| 121 | stdin=asyncio.subprocess.PIPE, |
| 122 | stdout=asyncio.subprocess.PIPE, |
| 123 | stderr=asyncio.subprocess.PIPE, |
| 124 | env=env, |
| 125 | ) |
| 126 | self._proc = proc |
| 127 | self._reader_task = asyncio.create_task(self._read_loop()) |
| 128 | self._stderr_task = asyncio.create_task(self._drain_stderr()) |
| 129 | return proc |
| 130 | |
| 131 | async def _drain_stderr(self) -> None: |
| 132 | """Drain the core's stderr so its diagnostics never block / corrupt the |
| 133 | protocol pipe. We discard it (set CMDOP_DEBUG=1 to have the core log to a |
| 134 | file); a host that wants it can read the core's log file.""" |
| 135 | assert self._proc is not None and self._proc.stderr is not None |
| 136 | try: |
| 137 | while True: |
| 138 | line = await self._proc.stderr.readline() |
| 139 | if not line: |
| 140 | return |
| 141 | except Exception: # noqa: BLE001 - stderr drain must never raise |
| 142 | return |
| 143 | |
| 144 | async def _read_loop(self) -> None: |
no outgoing calls