From 24531340847aafba2cdbc6d44502d4a602d3d89e Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 8 May 2026 03:24:52 +0000 Subject: [PATCH] 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. --- unsloth_cli/commands/studio.py | 37 ++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/unsloth_cli/commands/studio.py b/unsloth_cli/commands/studio.py index d0d6e308c1..74f52feecc 100644 --- a/unsloth_cli/commands/studio.py +++ b/unsloth_cli/commands/studio.py @@ -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: