cli(windows): pass sys.stdio handles explicitly to powershell.exe

The previous Write-Host capture attempts (47432b0b -Command + *>&1
and f2c2b3f3 [Console]::Out mirror in setup.ps1) still produced an
empty update.log on windows-latest because the powershell.exe child
had no stdio handles at all to write to.

Root cause: subprocess.run on Windows with the default close_fds=True
(Python 3.7+ default) sets bInheritHandles=False on CreateProcess.
Combined with CREATE_NO_WINDOW (added by _windows_hidden_subprocess_
kwargs in non-TTY runs), the child gets:
  - no console (CREATE_NO_WINDOW)
  - no inherited std handles (bInheritHandles=False)
GetStdHandle in the child returns INVALID_HANDLE_VALUE, so even
[Console]::Out.WriteLine and Write-Output -- not just Write-Host --
write into the void.

Fix: pass stdout=sys.stdout, stderr=sys.stderr (and stdin) when
running the setup script on Windows. With explicit handles, Python's
subprocess sets up PROC_THREAD_ATTRIBUTE_HANDLE_LIST containing the
std handles + bInheritHandles=True, so the child inherits exactly
the three std handles regardless of close_fds=True. CREATE_NO_WINDOW
still applies (no transient console window), but the child can now
write to the inherited stdout file handle, which lands on bash's
`tee logs/update.log` in CI.

A small _stream_for_subprocess helper guards against test harnesses
that swap sys.stdout for a stream without a real fileno (pytest
capsys, in-memory IO buffers, etc) -- those fall back to None so
subprocess uses its default.

Verified locally on PowerShell 7.4.6 / Linux that the explicit
stdout handoff doesn't regress the existing direct-inherit path,
and the marker line "prebuilt up to date and validated" reaches
both the child's stdout and a parent `tee` consumer.
This commit is contained in:
Daniel Han 2026-05-08 03:24:52 +00:00
commit 2453134084

View file

@ -133,6 +133,26 @@ def _windows_hidden_subprocess_kwargs() -> dict[str, object]:
return kwargs
def _stream_for_subprocess(stream):
"""Return *stream* if it has a real OS file descriptor, else None.
subprocess.run on Windows refuses to inherit std handles unless
they're passed explicitly (otherwise close_fds=True forces
bInheritHandles=False, and a CREATE_NO_WINDOW child ends up with
no stdio at all). When sys.stdout / sys.stderr is a real fd-backed
stream we want to hand it through; when it's been captured by a
test harness (pytest's capsys, an in-memory wrapper, etc) we fall
back to None so subprocess uses its default.
"""
if stream is None:
return None
try:
stream.fileno()
except (AttributeError, OSError, ValueError):
return None
return stream
def _studio_venv_python() -> Optional[Path]:
"""Return the studio venv Python binary, or None if not set up."""
if platform.system() == "Windows":
@ -1015,9 +1035,26 @@ def _run_setup_script(*, verbose: bool = False) -> None:
f"& '{script_pwsh_literal}' *>&1",
]
)
# Explicitly hand stdin/stdout/stderr to the child so the
# CI tee actually sees setup.ps1's output. Without this,
# subprocess.run on Windows uses close_fds=True (default,
# since Python 3.7) which sets bInheritHandles=False on
# CreateProcess. With CREATE_NO_WINDOW also set (via
# _windows_hidden_subprocess_kwargs in non-TTY runs), the
# child has neither a console nor any inherited std
# handles, so PowerShell's Write-Host -- and even
# [Console]::Out.WriteLine -- writes to nothing. Passing
# stdout=sys.stdout / stderr=sys.stderr makes Python set up
# PROC_THREAD_ATTRIBUTE_HANDLE_LIST with the std handles
# explicitly inheritable, which works alongside
# CREATE_NO_WINDOW. Empty update.log on the windows-latest
# CI was the smoking gun (run 25533694490 and 25534292239).
result = subprocess.run(
powershell_args,
env = env,
stdin = _stream_for_subprocess(sys.stdin),
stdout = _stream_for_subprocess(sys.stdout),
stderr = _stream_for_subprocess(sys.stderr),
**_windows_hidden_subprocess_kwargs(),
)
else: