Skip to content

Close guest process stdin to avoid TTY hang on macOS#17562

Open
spboyer wants to merge 2 commits into
mainfrom
spboyer/fix-guest-launcher-stdin-tty-hang
Open

Close guest process stdin to avoid TTY hang on macOS#17562
spboyer wants to merge 2 commits into
mainfrom
spboyer/fix-guest-launcher-stdin-tty-hang

Conversation

@spboyer
Copy link
Copy Markdown
Member

@spboyer spboyer commented May 27, 2026

Description

aspire new for the TypeScript starter (and aspire init/add/restore) hangs silently on macOS, producing no further output at ~0% CPU. Issue #16791 documents the symptom and a working < /dev/null workaround, 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.Cli set RedirectStandardOutput/Error = true and UseShellExecute = false, but leave stdin inheriting the parent TTY. On macOS/Linux any read from stdin in those children then blocks forever:

Launch path Used for
ProcessGuestLauncher npm/pnpm/yarn/bun install (and the guest AppHost itself)
NpmRunner npm view / pack / audit signatures / install -g
DotNetBasedAppHostServerProject.Run Dev/source-based AppHost server process
PrebuiltAppHostServer.CreateStartInfo Shipped AppHost server process

The TypeScript starter flow exercises all of these via BuildAndGenerateSdkAsync: prepare → start AppHost server → RPC codegen → npm install. By contrast, dotnet new install already goes through ProcessExecutionFactory, which sets RedirectStandardInput = true — that's why C#-only template scaffolding doesn't hit this.

Fix

In each of the four launch paths above:

  1. Set RedirectStandardInput = true on the ProcessStartInfo.
  2. Immediately after process.Start(), call process.StandardInput.Close() (wrapped in try/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_ClosesChildStdinSoReadsObserveEof in GuestRuntimeTests. 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, and NpmRunnerTests pass locally (49 succeeded, 4 Windows-only skipped).

Fixes #16791

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>
Copilot AI review requested due to automatic review settings May 27, 2026 21:46
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 27, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 17562

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 17562"

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ProcessGuestLauncher so child reads observe EOF instead of blocking on the terminal.
  • Apply the same stdin redirect+close behavior to NpmRunner subprocess 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>
@spboyer
Copy link
Copy Markdown
Member Author

spboyer commented May 27, 2026

The \Cli.EndToEnd-EmptyAppHostTemplateTests\ failure (C# empty AppHost) looks like a flaky E2E test rather than a regression from this PR:

  • The changes in this PR only affect TypeScript/npm guest process launching (\ProcessGuestLauncher, \NpmRunner) and the code-gen AppHost server (\DotNetBasedAppHostServerProject, \PrebuiltAppHostServer). They do not touch the C# AppHost runtime used by \�spire start/\�spire stop.
  • The TypeScript variant of the same test (\TypeScriptEmptyAppHostTemplateTests) passed ✅ — confirming the TypeScript path is healthy.
  • The failure is \�spire stop\ timing out after 8:20 while waiting for the apphost to stop — a known infrastructure flakiness pattern for this test suite.

Will request a re-run once the current CI run completes.

@github-actions
Copy link
Copy Markdown
Contributor

CLI E2E Tests failed — 106 passed, 1 failed, 2 unknown (commit 23af241)

Failed Tests

View all recordings
Status Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View recording
AddPackageWhileAppHostRunningDetached ▶️ View recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View recording
AgentInitCommand_DefaultSelection_InstallsDefaultSkills ▶️ View recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View recording
AgentMcpListStructuredLogsReturnsLogsFromStarterApp ▶️ View recording
AgentMcpListStructuredLogsReturnsLogsFromStarterApp_DevLocalhost ▶️ View recording
AgentMcpListStructuredLogsReturnsLogsFromStarterApp_Isolated ▶️ View recording
AllPublishMethodsBuildDockerImages ▶️ View recording
AspireAddAndStartWorkAgainstLegacyAppHostTs ▶️ View recording
AspireAddPackageVersionToDirectoryPackagesProps ▶️ View recording
AspireInitSingleFileAppHostRunsViaDotnetRunAppHost ▶️ View recording
AspireInitWithExistingAppHostDirRecreatesMissingNuGetConfigAndPreservesFiles ▶️ View recording
AspireInitWithSolutionFileGeneratesAppHostThatBuildsAgainstChannelHive ▶️ View recording
AspireStartUpdatesStaleTypeScriptAppHostPath ▶️ View recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View recording
AspireUpdateRemovesOrphanAppHostPackageVersionWhenSdkAlreadyCurrent ▶️ View recording
Banner_DisplayedOnFirstRun ▶️ View recording
Banner_DisplayedWithExplicitFlag ▶️ View recording
Banner_NotDisplayedWithNoLogoFlag ▶️ View recording
CertificatesClean_RemovesCertificates ▶️ View recording
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate ▶️ View recording
CertificatesTrust_WithUntrustedCert_TrustsCertificate ▶️ View recording
ConfigSetGet_CreatesNestedJsonFormat ▶️ View recording
CreateAndRunAspireStarterProject ▶️ View recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View recording
CreateAndRunEmptyAppHostProject ▶️ View failure recording
CreateAndRunJavaEmptyAppHostProject ▶️ View recording
CreateAndRunJsReactProject ▶️ View recording
CreateAndRunPythonReactProject ▶️ View recording
CreateAndRunTypeScriptEmptyAppHostProject ▶️ View recording
CreateAndRunTypeScriptStarterProject ▶️ View recording
CreateJavaAppHostWithViteApp ▶️ View recording
CreateTypeScriptAppHostWithViteApp_AllowsGuestAppPackageManagerToDiffer ▶️ View recording
CreateTypeScriptAppHostWithViteApp_UsesConfiguredToolchain ▶️ View recording
DashboardRunWithAgentMcpListTracesReturnsNoTraces ▶️ View recording
DashboardRunWithAgentMcpListTracesReturnsNoTraces_DevLocalhost ▶️ View recording
DashboardRunWithOtelTracesReturnsNoTraces ▶️ View recording
DashboardRunWithOtelTracesReturnsNoTraces_DevLocalhost ▶️ View recording
DeployK8sBasicApiService ▶️ View recording
DeployK8sWithExternalHelmChart ▶️ View recording
DeployK8sWithGarnet ▶️ View recording
DeployK8sWithMongoDB ▶️ View recording
DeployK8sWithMySql ▶️ View recording
DeployK8sWithPostgres ▶️ View recording
DeployK8sWithRabbitMQ ▶️ View recording
DeployK8sWithRedis ▶️ View recording
DeployK8sWithSqlServer ▶️ View recording
DeployK8sWithValkey ▶️ View recording
DeployTypeScriptAppToKubernetes ▶️ View recording
DescribeCommandResolvesReplicaNames ▶️ View recording
DescribeCommandShowsRunningResources ▶️ View recording
DetachFormatJsonProducesValidJson ▶️ View recording
DetachFormatJsonProducesValidJsonWhenRestartingExistingInstance ▶️ View recording
DoPublishAndDeployListStepsWork ▶️ View recording
DocsCommand_RendersInteractiveMarkdownFromLocalSource ▶️ View recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View recording
DoctorCommand_TypeScriptAppHostReportsMissingConfiguredToolchain ▶️ View recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View recording
GatewayWithoutExternalEndpoint_FailsPublishWithGuidance ▶️ View recording
GeneratedAspireDevScript_StartsWatchMode_WithConfiguredToolchain ▶️ View recording
GlobalMigration_HandlesCommentsAndTrailingCommas ▶️ View recording
GlobalMigration_HandlesMalformedLegacyJson ▶️ View recording
GlobalMigration_PreservesAllValueTypes ▶️ View recording
GlobalMigration_SkipsWhenNewConfigExists ▶️ View recording
GlobalSettings_MigratedFromLegacyFormat ▶️ View recording
IngressWithoutExternalEndpoint_FailsPublishWithGuidance ▶️ View recording
InitTypeScriptAppHost_AugmentsExistingViteRepoInWorkspaceSubdirectory ▶️ View recording
InteractiveCSharpInitCreatesExpectedFiles ▶️ View recording
InvalidAppHostPathWithComments_IsHealedOnRun ▶️ View recording
JavaScriptHostingApisRunFromTypeScriptAppHost ▶️ View recording
LatestCliCanStartStableChannelAppHost ▶️ View recording
LatestCliCanStartStableChannelTypeScriptAppHost ▶️ View recording
LegacySettingsMigration_AdjustsRelativeAppHostPath ▶️ View recording
LogsCommandShowsResourceLogs ▶️ View recording
OtelLogsReturnsStructuredLogsFromStarterApp ▶️ View recording
OtelLogsReturnsStructuredLogsFromStarterAppIsolated ▶️ View recording
PsCommandListsRunningAppHost ▶️ View recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View recording
PublishJavaScriptPatternsGeneratesExpectedDockerComposeArtifacts ▶️ View recording
PublishWithConfigureEnvFileUpdatesEnvOutput ▶️ View recording
PublishWithDockerComposeServiceCallbackSucceeds ▶️ View recording
PublishWithoutOutputPathUsesAppHostDirectoryDefault ▶️ View recording
ResourceCommand_FailedExecution_DisplaysAppHostLogPathAndLogContainsEntries ▶️ View recording
ResourceCommand_SetAndDeleteParameterUpdatesDescribeOutput ▶️ View recording
RestoreGeneratesSdkFiles ▶️ View recording
RestoreGeneratesSdkFiles_WithConfiguredToolchain ▶️ View recording
RestoreRefreshesGeneratedSdkAfterAddingIntegration ▶️ View recording
RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes ▶️ View recording
RunFromParentDirectory_UsesExistingConfigNearAppHost ▶️ View recording
RunReportsSyntaxErrorsForDotNetAppHost ▶️ View recording
RunReportsSyntaxErrorsForTypeScriptAppHost ▶️ View recording
SecretCrudOnDotNetAppHost ▶️ View recording
SecretCrudOnTypeScriptAppHost ▶️ View recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View recording
StartAndWaitForTypeScriptSqlServerAppHostWithNativeAssets ▶️ View recording
StartReportsSyntaxErrorsForDotNetAppHost ▶️ View recording
StartReportsSyntaxErrorsForTypeScriptAppHost ▶️ View recording
StopAllAppHostsFromAppHostDirectory ▶️ View recording
StopJavaPolyglotAppHostUsingApphostDirectory ▶️ View recording
StopNonInteractiveSingleAppHost ▶️ View recording
StopTypeScriptPolyglotAppHostUsingApphostDirectory ▶️ View recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View recording
UnAwaitedChainsCompileWithAutoResolvePromises ▶️ View recording
UpdateProjectChannelToStable_CSharpEmptyAppHost_PreservesAspireConfigChannel ▶️ View recording
UpdateProjectChannelToStable_CSharpSingleFileInit_PreservesAspireConfigChannel ▶️ View recording
UpdateProjectChannelToStable_TypeScriptSingleFileInit_PreservesAspireConfigChannel ▶️ View recording
UpdateProjectChannelToStable_TypeScript_PreviewsStablePackagesAndPreservesChannel ▶️ View recording

📹 Recordings uploaded automatically from CI run #26541139669

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

aspire init/add/restore can hang silently when stdin is a TTY (macOS)

2 participants