| 22 | |
| 23 | |
| 24 | class TTYSession: |
| 25 | def __init__(self, cmd, *, cwd=None, env=None, encoding="utf-8", echo=False): |
| 26 | self.cmd = cmd if isinstance(cmd, str) else " ".join(cmd) |
| 27 | self.cwd = cwd |
| 28 | self.env = env or os.environ.copy() |
| 29 | self.encoding = encoding |
| 30 | self.echo = echo # ← store preference |
| 31 | self._proc = None |
| 32 | self._buf: asyncio.Queue = None # type: ignore |
| 33 | self._pump_task = None |
| 34 | self._pty_master = None |
| 35 | self._pty_master_ref = None |
| 36 | |
| 37 | def __del__(self): |
| 38 | # Simple cleanup on object destruction |
| 39 | import nest_asyncio |
| 40 | |
| 41 | nest_asyncio.apply() |
| 42 | if hasattr(self, "close"): |
| 43 | try: |
| 44 | asyncio.run(self.close()) |
| 45 | except Exception: |
| 46 | pass |
| 47 | |
| 48 | # ── user-facing coroutines ──────────────────────────────────────── |
| 49 | async def start(self): |
| 50 | self._buf = asyncio.Queue() |
| 51 | if _IS_WIN: |
| 52 | self._proc = await _spawn_winpty( |
| 53 | self.cmd, self.cwd, self.env, self.echo |
| 54 | ) # ← pass echo |
| 55 | else: |
| 56 | self._proc = await _spawn_posix_pty( |
| 57 | self.cmd, self.cwd, self.env, self.echo |
| 58 | ) # ← pass echo |
| 59 | self._pty_master_ref = getattr(self._proc, "_pty_master_ref", None) |
| 60 | self._pty_master = ( |
| 61 | self._pty_master_ref.get("fd") |
| 62 | if self._pty_master_ref is not None |
| 63 | else getattr(self._proc, "_pty_master", None) |
| 64 | ) |
| 65 | self._pump_task = asyncio.create_task(self._pump_stdout()) |
| 66 | |
| 67 | async def close(self): |
| 68 | # Cancel the pump task if it exists |
| 69 | if self._pump_task: |
| 70 | self._pump_task.cancel() |
| 71 | try: |
| 72 | await self._pump_task |
| 73 | except asyncio.CancelledError: |
| 74 | pass |
| 75 | except Exception: |
| 76 | pass |
| 77 | |
| 78 | # Terminate the process if it exists |
| 79 | if self._proc: |
| 80 | try: |
| 81 | if getattr(self._proc, "returncode", None) is None: |