fix: seal child-process stdin on Windows (first-run hang)#74
Merged
Conversation
mcpp's first-run flow on Windows was hanging at xlings / xim / curl / git grandchildren that block on terminal stdin, forcing users to press Enter repeatedly to advance bootstrap and toolchain install. Root cause: process::seal_stdin was a no-op on Windows, and install_with_progress's direct-install path deliberately bypassed it. The POSIX side has had </dev/null sealing since PR #55 (macOS xcrun hang fix); Windows never received the equivalent fix. PR #57 only suppressed stdout/stderr noise (>/dev/null 2>&1) and did not touch stdin. Changes: - process.cppm: seal_stdin now appends "<NUL" on Windows (matches POSIX behavior). All capture / run_silent / run_streaming / run_passthrough callers gain the protection automatically. - xlings.cppm: install_with_progress's direct path explicitly appends "<NUL" on Windows. POSIX keeps the original behavior conservatively. - shell.cppm: silent_redirect docstring corrected — it never touched stdin, that's seal_stdin's job. Implementation unchanged. Regression coverage: - tests/unit/test_process_seal_stdin.cpp — deterministic reproduction test. Rebinds the test process's own stdin to an open, empty, never-closing pipe, then calls run_silent / capture / run_streaming with a child that reads stdin (more on Windows, cat on POSIX). Without the fix the child would block forever waiting on our pipe; with the fix it reads NUL / /dev/null and exits immediately. 5-second upper bound (real runs complete in <100ms). - ci-windows.yml — adds a step that launches mcpp via System.Diagnostics.Process with RedirectStandardInput=$true (parent holds the child's stdin open but never writes). Runs mcpp --version, mcpp build, mcpp run. Without the fix, any grandchild reading stdin blocks → step times out → CI fails. With the fix → all complete.
…$Args
Two issues with the regression step from the previous commit (both showed
up only on the actual Windows runner, not in local validation):
1. MCPP_SELF was set in an earlier bash step via `pwd` (git-bash) so the
value is MSYS-style (e.g. /d/a/mcpp/...). Bash steps tolerate it but
pwsh's `&` operator can't exec it ("not recognized as a name of a
cmdlet, function, script file, or executable program"). Convert via
cygpath -w before use.
2. `$Args` is a PowerShell automatic variable inside function scope; a
`param([string]$Args)` does not bind cleanly. Renamed to $McppArgs
to avoid the collision (also updated call sites).
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
mcpp's first-run flow on Windows was hanging at xlings / xim / curl / git grandchildren that block on terminal stdin, forcing users to press Enter repeatedly to advance bootstrap and toolchain install.
This PR seals child-process stdin on Windows (matching the POSIX behavior added in #55 for the macOS xcrun hang) and adds a deterministic regression test at both the unit and CI integration layers.
Companion analysis:
.agents/docs/2026-05-23-windows-stdin-hang-analysis.md(not in this PR — kept as working notes).Root cause
process.cppmseal_stdin()</dev/null)xlings.cppminstall_with_progressdirect pathseal_stdineven on POSIXshell.cppmsilent_redirectstdin+stdout+stderr, implementation only>/dev/null 2>&1Net result: on Windows, any subprocess descendant that calls
read(stdin)blocks on the user's terminal until they press Enter.Changes
src/platform/process.cppm—seal_stdinnow appends<NULon Windows. Allcapture/run_silent/run_streaming/run_passthroughcallers gain the protection automatically.src/xlings.cppm—install_with_progressdirect path explicitly appends<NULon Windows (this path deliberately bypassesseal_stdin). POSIX keeps the original behavior to stay conservative.src/platform/shell.cppm—silent_redirectdocstring corrected (it never touched stdin; implementation unchanged).tests/unit/test_process_seal_stdin.cpp— new unit test, see below..github/workflows/ci-windows.yml— new regression step, see below.How the fix is tested
Unit test (
tests/unit/test_process_seal_stdin.cpp)Deterministic reproduction at the unit level:
STDINto an open, empty, never-closing pipe (Win32CreatePipe+SetStdHandle+ CRT_dup2on Windows;pipe()+dup2()on POSIX).run_silent/capture/run_streamingwith a child that reads stdin (moreon Windows,caton POSIX).NUL//dev/null→ exits in <100ms.Runs on every CI (Linux + macOS + Windows) via
mcpp test.Integration test (
ci-windows.yml)Adds a new step
Regression: mcpp survives open-empty-stdin (Windows hang fix). Launches mcpp via[System.Diagnostics.Process]::StartwithRedirectStandardInput = $true(parent holds the child's stdin open, never writes, never closes). Runs three commands inside this hostile-stdin scenario:mcpp --version(sanity)mcpp build(full bootstrap → toolchain resolve → dep resolve → compile)mcpp run(post-build run path)Without the fix → any grandchild reading stdin blocks → step times out (15 min step budget, per-command 5/10/2 min) → CI fails.
With the fix → all three complete cleanly.
Test plan
test_process_seal_stdinruns on all three platformsRisk assessment
Low. Plan A (seal_stdin Windows branch) only adds
<NULto the command string. Plan B (install_with_progress) only adds<NULto Windows path; POSIX unchanged. Both are append-only — no API or behavior change for callers that don't read stdin.The only theoretical concern (raised in the pre-fix comment "xlings may need stdin for subprocess coordination during large package extraction") was never observed in practice on Linux/macOS over the past two months; we preserve POSIX behavior conservatively and only seal on Windows where the hang was reported.