| 223 | |
| 224 | |
| 225 | async def _spawn_posix_pty(cmd, cwd, env, echo): |
| 226 | import pty, asyncio, os, termios |
| 227 | |
| 228 | master, slave = pty.openpty() |
| 229 | |
| 230 | # ── Disable ECHO on the slave side if requested ── |
| 231 | if not echo: |
| 232 | attrs = termios.tcgetattr(slave) |
| 233 | attrs[3] &= ~termios.ECHO # lflag |
| 234 | termios.tcsetattr(slave, termios.TCSANOW, attrs) |
| 235 | |
| 236 | proc = await asyncio.create_subprocess_shell( |
| 237 | cmd, |
| 238 | stdin=slave, |
| 239 | stdout=slave, |
| 240 | stderr=slave, |
| 241 | cwd=cwd, |
| 242 | env=env, |
| 243 | close_fds=True, |
| 244 | ) |
| 245 | os.close(slave) |
| 246 | |
| 247 | loop = asyncio.get_running_loop() |
| 248 | reader = asyncio.StreamReader() |
| 249 | master_ref = {"fd": master} |
| 250 | |
| 251 | def _release_master_fd(): |
| 252 | cur = master_ref.get("fd") |
| 253 | if cur is None: |
| 254 | return |
| 255 | # Invalidate before close so later cleanup cannot close a reused fd. |
| 256 | master_ref["fd"] = None |
| 257 | try: |
| 258 | proc._pty_master = None # type: ignore[attr-defined] |
| 259 | except Exception: |
| 260 | pass |
| 261 | try: |
| 262 | loop.remove_reader(cur) |
| 263 | except Exception: |
| 264 | pass |
| 265 | try: |
| 266 | os.close(cur) |
| 267 | except OSError: |
| 268 | pass |
| 269 | |
| 270 | def _on_data(): |
| 271 | cur = master_ref.get("fd") |
| 272 | if cur is None: |
| 273 | reader.feed_eof() |
| 274 | return |
| 275 | try: |
| 276 | data = os.read(cur, 1 << 16) |
| 277 | except OSError as e: |
| 278 | if e.errno != errno.EIO: # EIO == EOF on some systems |
| 279 | raise |
| 280 | data = b"" |
| 281 | if data: |
| 282 | reader.feed_data(data) |