Close guest process stdin to avoid TTY hang on macOS#17562
Conversation
ProcessGuestLauncher and NpmRunner spawn child processes (npm/pnpm/yarn/bun install, plus the guest AppHost itself) with stdout/stderr redirected but left stdin inheriting the parent CLI's TTY. On macOS/Linux, if any child (e.g. an npm postinstall script, husky, or a package-manager permission prompt) reads from stdin, it blocks indefinitely waiting on the terminal, making 'aspire new' for the TypeScript starter (and 'aspire init/add/ restore') appear to stall with no output and ~0% CPU. Redirect stdin and close it immediately after Process.Start() so any child read surfaces as EOF instead of blocking. We never write to the guest process or npm stdin, so closing is safe. dotnet-based invocations already redirect stdin via ProcessExecutionFactory. Add a regression test in GuestRuntimeTests that launches a shell script which reads stdin and asserts it observes EOF and exits within 10s. Fixes #16791 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 17562Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 17562" |
There was a problem hiding this comment.
Pull request overview
Fixes a macOS/Linux hang in Aspire CLI guest process execution (notably TypeScript scaffolding flows) by ensuring spawned child processes do not inherit the parent TTY for stdin, preventing lifecycle scripts or prompts from blocking indefinitely.
Changes:
- Redirect and immediately close stdin for guest processes launched via
ProcessGuestLauncherso child reads observe EOF instead of blocking on the terminal. - Apply the same stdin redirect+close behavior to
NpmRunnersubprocess invocations. - Add a regression test that launches a command which attempts to read stdin and asserts it exits promptly with EOF observed.
Show a summary per file
| File | Description |
|---|---|
| tests/Aspire.Cli.Tests/Projects/GuestRuntimeTests.cs | Adds regression coverage ensuring child stdin is closed so reads return EOF (prevents macOS TTY hangs). |
| src/Aspire.Cli/Projects/ProcessGuestLauncher.cs | Redirects stdin and closes it immediately after start to avoid inheriting the CLI’s TTY. |
| src/Aspire.Cli/Npm/NpmRunner.cs | Redirects stdin for npm processes and closes it right after start to prevent interactive hangs. |
Copilot's findings
- Files reviewed: 3/3 changed files
- Comments generated: 0
Extend the TTY-hang fix to the two AppHost server launch paths used by BuildAndGenerateSdkAsync during 'aspire new'/'init'/'add'/'restore': - DotNetBasedAppHostServerProject.Run (dev/source-based AppHost server) - PrebuiltAppHostServer (shipped AppHost server) Both previously redirected stdout/stderr but left stdin inheriting the parent CLI's TTY. The CLI communicates with the server over a Unix socket (REMOTE_APP_HOST_SOCKET_PATH), not stdin, so closing the redirected stdin pipe immediately after Process.Start() is safe and ensures any stdin read in the server (or a library it loads) surfaces as EOF instead of blocking. Combined with the earlier ProcessGuestLauncher / NpmRunner changes, this covers every child process spawned during the TypeScript starter scaffolding flow that previously inherited the parent TTY. Refs #16791 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
The \Cli.EndToEnd-EmptyAppHostTemplateTests\ failure (C# empty AppHost) looks like a flaky E2E test rather than a regression from this PR:
Will request a re-run once the current CI run completes. |
|
❌ CLI E2E Tests failed — 106 passed, 1 failed, 2 unknown (commit Failed Tests
View all recordings
📹 Recordings uploaded automatically from CI run #26541139669 |
Description
aspire newfor the TypeScript starter (andaspire init/add/restore) hangs silently on macOS, producing no further output at ~0% CPU. Issue #16791 documents the symptom and a working< /dev/nullworkaround, which is strong evidence that some spawned child inherits the parent CLI's TTY and blocks on a stdin read.Root cause
Several subprocess launch paths in
Aspire.ClisetRedirectStandardOutput/Error = trueandUseShellExecute = false, but leave stdin inheriting the parent TTY. On macOS/Linux any read from stdin in those children then blocks forever:ProcessGuestLaunchernpm/pnpm/yarn/buninstall (and the guest AppHost itself)NpmRunnernpm view/pack/audit signatures/install -gDotNetBasedAppHostServerProject.RunPrebuiltAppHostServer.CreateStartInfoThe TypeScript starter flow exercises all of these via
BuildAndGenerateSdkAsync: prepare → start AppHost server → RPC codegen →npm install. By contrast,dotnet new installalready goes throughProcessExecutionFactory, which setsRedirectStandardInput = true— that's why C#-only template scaffolding doesn't hit this.Fix
In each of the four launch paths above:
RedirectStandardInput = trueon theProcessStartInfo.process.Start(), callprocess.StandardInput.Close()(wrapped intry/catch (IOException)) so any child read surfaces as EOF instead of blocking on the inherited TTY.The CLI controls these processes via Ctrl+C / backchannel cancellation and Unix-socket IPC (
REMOTE_APP_HOST_SOCKET_PATH), never via stdin, so closing the pipe is safe.Tests
Added
ProcessGuestLauncher_ClosesChildStdinSoReadsObserveEofinGuestRuntimeTests. It launches a short shell snippet that reads stdin and asserts the launcher returns within 10s with the child reporting EOF. Without the fix the test would block on the inherited TTY (or be killed by the 10s CTS).All
GuestRuntimeTests,AppHostServerProjectTests,AppHostServerSessionTests, andNpmRunnerTestspass locally (49 succeeded, 4 Windows-only skipped).Fixes #16791