fix(windows): terminal drain and cwd path conversion for native Windows

Two fixes for the local terminal backend on Windows (Git Bash):

1. `_drain()` in base.py: `select.select()` only works on sockets on
   Windows, not pipe file descriptors. On Windows, use blocking
   `os.read()` in the daemon thread instead. EOF arrives promptly
   when bash exits, so this is safe.

2. `_run_bash()` in local.py: When `self.cwd` is updated from `pwd`
   output, it contains Git Bash-style paths (`/c/Users/...`).
   `subprocess.Popen(cwd=...)` needs a native Windows path
   (`C:\Users\...`). Added a conversion before Popen.

Without these fixes, all terminal() calls on Windows return empty
output (exit code 126), and cwd tracking breaks.

Tested on Windows 11 with Git for Windows + Python 3.13.

Fixes #14638
This commit is contained in:
Alan Chen 2026-05-03 22:39:15 +08:00 committed by Teknium
parent 7244a1f0d3
commit c2d6b385f1
2 changed files with 28 additions and 1 deletions

View file

@ -489,6 +489,26 @@ class BaseEnvironment(ABC):
def _drain(): def _drain():
fd = proc.stdout.fileno() fd = proc.stdout.fileno()
# select.select does NOT work on pipe fds on Windows (only sockets).
# Use blocking os.read in a daemon thread instead — safe because
# EOF arrives promptly when bash exits.
if os.name == "nt":
try:
while True:
chunk = os.read(fd, 4096)
if not chunk:
break
output_chunks.append(decoder.decode(chunk))
except (ValueError, OSError):
pass
finally:
try:
tail = decoder.decode(b"", final=True)
if tail:
output_chunks.append(tail)
except Exception:
pass
return
idle_after_exit = 0 idle_after_exit = 0
try: try:
while True: while True:

View file

@ -3,6 +3,7 @@
import logging import logging
import os import os
import platform import platform
import re
import shutil import shutil
import signal import signal
import subprocess import subprocess
@ -403,6 +404,12 @@ class LocalEnvironment(BaseEnvironment):
) )
self.cwd = safe_cwd self.cwd = safe_cwd
# On Windows, self.cwd may be a Git Bash-style path (/c/Users/...)
# from pwd output. subprocess.Popen needs a native Windows path.
_popen_cwd = self.cwd
if _IS_WINDOWS and _popen_cwd and re.match(r'^/[a-zA-Z]/', _popen_cwd):
_popen_cwd = _popen_cwd[1].upper() + ':' + _popen_cwd[2:].replace('/', '\\')
proc = subprocess.Popen( proc = subprocess.Popen(
args, args,
text=True, text=True,
@ -413,7 +420,7 @@ class LocalEnvironment(BaseEnvironment):
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
stdin=subprocess.PIPE if stdin_data is not None else subprocess.DEVNULL, stdin=subprocess.PIPE if stdin_data is not None else subprocess.DEVNULL,
preexec_fn=None if _IS_WINDOWS else os.setsid, preexec_fn=None if _IS_WINDOWS else os.setsid,
cwd=self.cwd, cwd=_popen_cwd,
) )
if not _IS_WINDOWS: if not _IS_WINDOWS:
try: try: