| 307 | |
| 308 | |
| 309 | async def _spawn_winpty(cmd, cwd, env, echo): |
| 310 | # Clean PowerShell startup: no logo, no profile, bypass execution policy for deterministic behavior |
| 311 | if cmd.strip().lower().startswith("powershell"): |
| 312 | if "-nolog" not in cmd.lower(): |
| 313 | cmd = cmd.replace("powershell.exe", "powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass", 1) |
| 314 | |
| 315 | cols, rows = 80, 25 |
| 316 | child = winpty.PtyProcess.spawn(cmd, dimensions=(rows, cols), cwd=cwd or os.getcwd(), env=env) # type: ignore |
| 317 | |
| 318 | loop = asyncio.get_running_loop() |
| 319 | reader = asyncio.StreamReader() |
| 320 | |
| 321 | async def _on_data(): |
| 322 | while child.isalive(): |
| 323 | try: |
| 324 | # Run blocking read in executor to not block event loop |
| 325 | data = await loop.run_in_executor(None, child.read, 1 << 16) |
| 326 | if data: |
| 327 | reader.feed_data(data.encode('utf-8') if isinstance(data, str) else data) |
| 328 | except EOFError: |
| 329 | break |
| 330 | except Exception: |
| 331 | await asyncio.sleep(0.01) |
| 332 | reader.feed_eof() |
| 333 | |
| 334 | # Start pumping output in background |
| 335 | asyncio.create_task(_on_data()) |
| 336 | |
| 337 | class _Stdin: |
| 338 | def write(self, d): |
| 339 | # Use winpty's write method, not os.write |
| 340 | if isinstance(d, bytes): |
| 341 | d = d.decode('utf-8', errors='replace') |
| 342 | # Windows needs \r\n for proper line endings |
| 343 | if _IS_WIN: |
| 344 | d = d.replace('\n', '\r\n') |
| 345 | child.write(d) |
| 346 | |
| 347 | async def drain(self): |
| 348 | await asyncio.sleep(0.01) # Give write time to complete |
| 349 | |
| 350 | class _Proc: |
| 351 | def __init__(self): |
| 352 | self.stdin = _Stdin() # type: ignore |
| 353 | self.stdout = reader |
| 354 | self.pid = child.pid |
| 355 | self.returncode = None |
| 356 | |
| 357 | async def wait(self): |
| 358 | while child.isalive(): |
| 359 | await asyncio.sleep(0.2) |
| 360 | self.returncode = 0 |
| 361 | return 0 |
| 362 | |
| 363 | def terminate(self): |
| 364 | if child.isalive(): |
| 365 | child.terminate() |
| 366 | |