Skip to content

Backport CLI cancellation fixes to release/13.4 - #17641

Merged
David Fowler (davidfowl) merged 2 commits into
release/13.4from
jamesnk/cancellation-13.4
May 29, 2026
Merged

Backport CLI cancellation fixes to release/13.4#17641
David Fowler (davidfowl) merged 2 commits into
release/13.4from
jamesnk/cancellation-13.4

Conversation

@JamesNK

Copy link
Copy Markdown
Member

Description

Backport of CLI cancellation fixes from main to release/13.4. This PR contains changes from two PRs that are combined to avoid merge conflicts:

From #17576 — Add TerminalRun IAsyncDisposable for consistent CLI E2E diagnostics capture

Adds a TerminalRun type (IAsyncDisposable) that wraps the CLI E2E terminal lifecycle, ensuring CaptureAspireDiagnosticsAsync always runs at the end of a test — even when the test fails. This replaces the manual exit/await pendingRun boilerplate and guarantees diagnostics are consistently captured across all tests.

From #17588 — Fix CLI Ctrl+C/SIGTERM shutdown: responsive cancellation and double-signal bug

Fixes the CLI's Ctrl+C/SIGTERM handling to be responsive during all phases and prevents a double-signal bug that caused immediate force-kill instead of graceful shutdown.

Problems fixed:

  1. Sluggish Ctrl+C during AppHost startup: When a user pressed Ctrl+C during AppHost startup (before the AppHost connects back to the CLI), the CLI would wait for the full 5-second startup timeout before exiting.
  2. Double-signal bug causing force-kill: Both PosixSignalRegistration(SIGINT) and Console.CancelKeyPress were registered simultaneously. On Linux, when SIGINT arrives both handlers fire for the same signal, each calling Cancel(), so _cancelCalled reaches 2 immediately — triggering the force-kill path instead of graceful shutdown.
  3. No way to force-exit: If graceful shutdown was taking too long, there was no mechanism for a second Ctrl+C to force immediate termination.

Fixes #17569

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
    • No

…ignal bug (#17588)

* Fix CLI Ctrl+C shutdown taking too long during AppHost startup

- Make ConsoleCancellationManager.Cancel() non-blocking so Ctrl+C handler
  returns immediately
- Pass cancellation token through to WaitAsync in CancelAppHostStartupAsync
  so Ctrl+C exits promptly instead of waiting for the full 5s timeout
- Support second Ctrl+C for immediate force exit (Environment.Exit)
- Add logging support to ConsoleCancellationManager via SetLogger()
- Add comprehensive unit tests for ConsoleCancellationManager
- Add integration test for RunCommand cancellation during startup timeout

Fixes #17569

* Fix review comments: volatile logger, accurate comments, clearer warning

* Clean up

* Use WaitForSuccessPromptFailFastAsync in CLI E2E tests

Replace WaitForSuccessPromptAsync with WaitForSuccessPromptFailFastAsync
across all CLI E2E tests. The fail-fast variant detects error prompts
immediately and throws instead of hanging for up to 500s waiting for a
success prompt that will never arrive. This prevents 10+ minute CI
timeouts when a command fails with a non-zero exit code.

* Fix double-signal bug and consolidate test helpers

- Fix ConsoleCancellationManager double-signal bug: move Console.CancelKeyPress
  to else branch so it only registers on platforms without PosixSignalRegistration.
  Previously both handlers fired for the same SIGINT, causing immediate force-kill.
- Add SIGQUIT/Ctrl+Break registration for Windows parity.
- Remove old WaitForSuccessPromptAsync (no fail-fast) and rename
  WaitForSuccessPromptFailFastAsync to WaitForSuccessPromptAsync.
- Remove duplicate RunCommandFailFastAsync (identical to RunCommandAsync).

* Fix stale comment and restrict SIGQUIT to Windows only

* Remove unused legacy builder methods and update E2E skill docs

* Don't force when debugging

* More logging
@github-actions

Copy link
Copy Markdown
Contributor

🚀 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 -- 17641

Or

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR backports two already-merged main PRs (#17576 and #17588) to release/13.4, combined to avoid merge conflicts. It addresses CLI Ctrl+C/SIGTERM responsiveness issues (issue #17569) and introduces a TerminalRun IAsyncDisposable to guarantee diagnostics capture in CLI E2E tests.

Changes:

  • Rewires ConsoleCancellationManager: async forced-termination timeout, second-signal force-kill, SIGQUIT on Windows, and removes the double-signal bug by registering Console.CancelKeyPress only on platforms without PosixSignalRegistration. RunCommand.CancelAppHostStartupAsync now plumbs the outer cancellation token to WaitAsync so Ctrl+C exits the startup wait immediately.
  • Introduces TerminalRun/CliE2ETestHelpers.StartRun and migrates ~10 E2E tests + several others away from manual exit/await pendingRun boilerplate; copies .aspire-diagnostics/ from workspace to testresults/workspaces/<test> for CI artifacts.
  • Renames WaitForSuccessPromptFailFastAsyncWaitForSuccessPromptAsync and removes the duplicate RunCommandFailFastAsync, updating ~59 call sites across CLI E2E and deployment E2E tests; adds unit tests in ConsoleCancellationManagerTests and a RunCommandTests Ctrl+C test.

Reviewed changes

Copilot reviewed 88 out of 88 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/Aspire.Cli/ConsoleCancellationManager.cs Async timeout, double-signal handling, SIGQUIT, logger, debugger bypass
src/Aspire.Cli/Program.cs Wires logger into cancellation manager; updates termination log message
src/Aspire.Cli/Commands/RunCommand.cs Threads cancellationToken through CancelAppHostStartupAsync to exit startup wait promptly on Ctrl+C
src/Aspire.Cli/Projects/ProcessGuestLauncher.cs Adds debug/info logging around guest-process start, cancel, kill, exit
tests/Aspire.Cli.Tests/ConsoleCancellationManagerTests.cs New unit tests: cancellation, second-signal force, timeout, non-blocking Cancel, dispose semantics
tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs New test: Ctrl+C during startup exits within 3s instead of waiting full 5s timeout
tests/Aspire.Cli.EndToEnd.Tests/Helpers/TerminalRun.cs New IAsyncDisposable capturing diagnostics, exiting terminal, copying workspace diagnostics to test artifacts
tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs Adds StartRun(...) factory
tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs Consolidates diagnostics under .aspire-diagnostics/; replaces RunCommandFailFastAsync calls
tests/Shared/Hex1bAutomatorTestHelpers.cs Removes old WaitForSuccessPromptAsync; renames *FailFastAsync variants; removes RunCommandFailFastAsync
tests/Shared/Hex1bTestHelpers.cs Removes unused WaitForSuccessPromptFailFast builder and InstallAspireBundleFromPullRequest helper
tests/Aspire.Cli.EndToEnd.Tests/*.cs (~50 files) Mechanical: adopt StartRun, drop manual exit/pendingRun, rename *FailFastAsync*Async
tests/Aspire.Deployment.EndToEnd.Tests/*.cs Rename RunCommandFailFastAsyncRunCommandAsync
.github/skills/cli-e2e-testing/SKILL.md Documents the StartRun/TerminalRun pattern and updated helper names

@github-actions

Copy link
Copy Markdown
Contributor

CLI E2E Tests unknown — 107 passed, 0 failed, 2 unknown (commit bf397c0)

View all recordings
Status Test Recording Job Artifacts
AddPackageInteractiveWhileAppHostRunningDetached Recording #78424403693 Logs
AddPackageWhileAppHostRunningDetached Recording #78424403693 Logs
AgentCommands_AllHelpOutputs_AreCorrect Recording #78424403616 Logs
AgentInitCommand_DefaultSelection_InstallsDefaultSkills Recording #78424403616 Logs
AgentInitCommand_MigratesDeprecatedConfig Recording #78424403616 Logs
AgentMcpListStructuredLogsReturnsLogsFromStarterApp Recording #78424404018 Logs
AgentMcpListStructuredLogsReturnsLogsFromStarterApp_DevLocalhost Recording #78424404018 Logs
AgentMcpListStructuredLogsReturnsLogsFromStarterApp_Isolated Recording #78424404018 Logs
AllPublishMethodsBuildDockerImages Recording #78424404280 Logs
AspireAddAndStartWorkAgainstLegacyAppHostTs Recording #78424404094 Logs
AspireAddPackageVersionToDirectoryPackagesProps Recording #78424404169 Logs
AspireInitSingleFileAppHostRunsViaDotnetRunAppHost Recording #78424404178 Logs
AspireInitWithExistingAppHostDirRecreatesMissingNuGetConfigAndPreservesFiles Recording #78424403867 Logs
AspireInitWithSolutionFileGeneratesAppHostThatBuildsAgainstChannelHive Recording #78424403867 Logs
AspireStartUpdatesStaleTypeScriptAppHostPath Recording #78424404113 Logs
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps Recording #78424404169 Logs
AspireUpdateRemovesOrphanAppHostPackageVersionWhenSdkAlreadyCurrent Recording #78424404169 Logs
Banner_DisplayedOnFirstRun Recording #78424403816 Logs
Banner_DisplayedWithExplicitFlag Recording #78424403816 Logs
Banner_NotDisplayedWithNoLogoFlag Recording #78424403816 Logs
CertificatesClean_RemovesCertificates Recording #78424403989 Logs
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate Recording #78424403989 Logs
CertificatesTrust_WithUntrustedCert_TrustsCertificate Recording #78424403989 Logs
ConfigSetGet_CreatesNestedJsonFormat Recording #78424403746 Logs
CreateAndRunAspireStarterProject Recording #78424403909 Logs
CreateAndRunAspireStarterProjectWithBundle Recording #78424403716 Logs
CreateAndRunEmptyAppHostProject Recording #78424403688 Logs
CreateAndRunJavaEmptyAppHostProject Recording #78424404157 Logs
CreateAndRunJsReactProject Recording #78424403938 Logs
CreateAndRunPythonReactProject Recording #78424403747 Logs
CreateAndRunTypeScriptEmptyAppHostProject Recording #78424403697 Logs
CreateAndRunTypeScriptStarterProject Recording #78424404011 Logs
CreateJavaAppHostWithViteApp Recording #78424403620 Logs
CreateTypeScriptAppHostWithViteApp_AllowsGuestAppPackageManagerToDiffer Recording #78424403603 Logs
CreateTypeScriptAppHostWithViteApp_UsesConfiguredToolchain Recording #78424403603 Logs
DashboardRunWithAgentMcpListTracesReturnsNoTraces Recording #78424404173 Logs
DashboardRunWithAgentMcpListTracesReturnsNoTraces_DevLocalhost Recording #78424404173 Logs
DashboardRunWithOtelTracesReturnsNoTraces Recording #78424404173 Logs
DashboardRunWithOtelTracesReturnsNoTraces_DevLocalhost Recording #78424404173 Logs
DeployK8sBasicApiService Recording #78424403660 Logs
DeployK8sWithExternalHelmChart Recording #78424404107 Logs
DeployK8sWithGarnet Recording #78424403894 Logs
DeployK8sWithMongoDB Recording #78424403933 Logs
DeployK8sWithMySql Recording #78424403724 Logs
DeployK8sWithPostgres Recording #78424404089 Logs
DeployK8sWithRabbitMQ Recording #78424404099 Logs
DeployK8sWithRedis Recording #78424403975 Logs
DeployK8sWithSqlServer Recording #78424403743 Logs
DeployK8sWithValkey Recording #78424404175 Logs
DeployTypeScriptAppToKubernetes Recording #78424403965 Logs
DescribeCommandResolvesReplicaNames Recording #78424404036 Logs
DescribeCommandShowsRunningResources Recording #78424404036 Logs
DetachFormatJsonProducesValidJson Recording #78424403840 Logs
DetachFormatJsonProducesValidJsonWhenRestartingExistingInstance Recording #78424403840 Logs
DoPublishAndDeployListStepsWork Recording #78424404027 Logs
DocsCommand_RendersInteractiveMarkdownFromLocalSource Recording #78424403934 Logs
DoctorCommand_DetectsDeprecatedAgentConfig Recording #78424403616 Logs
DoctorCommand_TypeScriptAppHostReportsMissingConfiguredToolchain Recording #78424403917 Logs
DoctorCommand_WithSslCertDir_ShowsTrusted Recording #78424403917 Logs
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted Recording #78424403917 Logs
GatewayWithoutExternalEndpoint_FailsPublishWithGuidance Recording #78424403799 Logs
GeneratedAspireDevScript_StartsWatchMode_WithConfiguredToolchain Recording #78424403603 Logs
GlobalMigration_HandlesCommentsAndTrailingCommas Recording #78424403746 Logs
GlobalMigration_HandlesMalformedLegacyJson Recording #78424403746 Logs
GlobalMigration_PreservesAllValueTypes Recording #78424403746 Logs
GlobalMigration_SkipsWhenNewConfigExists Recording #78424403746 Logs
GlobalSettings_MigratedFromLegacyFormat Recording #78424403746 Logs
IngressWithoutExternalEndpoint_FailsPublishWithGuidance Recording #78424403799 Logs
InitTypeScriptAppHost_AugmentsExistingViteRepoInWorkspaceSubdirectory Recording #78424403603 Logs
InteractiveCSharpInitCreatesExpectedFiles Recording #78424403669 Logs
InvalidAppHostPathWithComments_IsHealedOnRun Recording #78424403675 Logs
JavaScriptHostingApisRunFromTypeScriptAppHost Recording #78424404280 Logs
LatestCliCanStartStableChannelAppHost Recording #78424403909 Logs
LatestCliCanStartStableChannelTypeScriptAppHost Recording #78424403909 Logs
LegacySettingsMigration_AdjustsRelativeAppHostPath Recording #78424404113 Logs
LogsCommandShowsResourceLogs Recording #78424403673 Logs
OtelLogsReturnsStructuredLogsFromStarterApp Recording #78424404176 Logs
OtelLogsReturnsStructuredLogsFromStarterAppIsolated Recording #78424404176 Logs
PsCommandListsRunningAppHost Recording #78424403902 Logs
PsFormatJsonOutputsOnlyJsonToStdout Recording #78424403902 Logs
PublishJavaScriptPatternsGeneratesExpectedDockerComposeArtifacts Recording #78424404204 Logs
PublishWithConfigureEnvFileUpdatesEnvOutput Recording #78424404204 Logs
PublishWithDockerComposeServiceCallbackSucceeds Recording #78424404204 Logs
PublishWithoutOutputPathUsesAppHostDirectoryDefault Recording #78424404204 Logs
ResourceCommand_FailedExecution_DisplaysAppHostLogPathAndLogContainsEntries Recording #78424403639 Logs
ResourceCommand_SetAndDeleteParameterUpdatesDescribeOutput Recording #78424403639 Logs
RestoreGeneratesSdkFiles Recording #78424403737 Logs
RestoreGeneratesSdkFiles_WithConfiguredToolchain Recording #78424403733 Logs
RestoreRefreshesGeneratedSdkAfterAddingIntegration Recording #78424403733 Logs
RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes Recording #78424404001 Logs
RunFromParentDirectory_UsesExistingConfigNearAppHost Recording #78424403719 Logs
RunReportsSyntaxErrorsForDotNetAppHost Recording #78424403892 Logs
RunReportsSyntaxErrorsForTypeScriptAppHost Recording #78424403892 Logs
SecretCrudOnDotNetAppHost Recording #78424404052 Logs
SecretCrudOnTypeScriptAppHost Recording #78424403755 Logs
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels Recording #78424404146 Logs
StartAndWaitForTypeScriptSqlServerAppHostWithNativeAssets Recording #78424404225 Logs
StartReportsSyntaxErrorsForDotNetAppHost Recording #78424403892 Logs
StartReportsSyntaxErrorsForTypeScriptAppHost Recording #78424403892 Logs
StopAllAppHostsFromAppHostDirectory Recording #78424403905 Logs
StopJavaPolyglotAppHostUsingApphostDirectory Recording #78424403648 Logs
StopNonInteractiveSingleAppHost Recording #78424403905 Logs
StopTypeScriptPolyglotAppHostUsingApphostDirectory Recording #78424403695 Logs
StopWithNoRunningAppHostExitsSuccessfully Recording #78424403693 Logs
UnAwaitedChainsCompileWithAutoResolvePromises Recording #78424403733 Logs
UpdateProjectChannelToStable_CSharpEmptyAppHost_PreservesAspireConfigChannel Recording #78424403769 Logs
UpdateProjectChannelToStable_CSharpSingleFileInit_PreservesAspireConfigChannel Recording #78424403769 Logs
UpdateProjectChannelToStable_TypeScriptSingleFileInit_PreservesAspireConfigChannel Recording #78424403769 Logs
UpdateProjectChannelToStable_TypeScript_PreviewsStablePackagesAndPreservesChannel Recording #78424403769 Logs

📹 Recordings uploaded automatically from CI run #26612978519

@davidfowl
David Fowler (davidfowl) merged commit cc4b7ea into release/13.4 May 29, 2026
313 checks passed
@davidfowl
David Fowler (davidfowl) deleted the jamesnk/cancellation-13.4 branch May 29, 2026 14:15
@microsoft-github-policy-service microsoft-github-policy-service Bot modified the milestone: 13.4 May 29, 2026
@aspire-repo-bot

Copy link
Copy Markdown
Contributor

✅ No documentation update needed.

Decision: docs_optional → bug_fix_restores_documented_behavior

Triggered signals (1): cli_command_file_changedsrc/Aspire.Cli/Commands/RunCommand.cs (path matched CLI command file pattern).

Rationale: The change to RunCommand.cs is a pure bug fix that threads a cancellationToken through CancelAppHostStartupAsync so that pressing Ctrl+C during AppHost startup exits immediately rather than waiting the full 5-second timeout. No new CLI options, flags, or public API were introduced (the PR checklist confirms "Did you add public API? No"). The companion ConsoleCancellationManager.cs fix resolves a double-signal bug (SIGINT + CancelKeyPress both firing) that incorrectly triggered force-kill.

The existing aspire-run.mdx docs already document the intended behavior verbatim: "stop the running AppHost with Ctrl+C" and "Press CTRL+C to stop the apphost and exit." — the bugs were implementation discrepancies with this documented behavior. No new user-facing surface was introduced.

@github-actions github-actions Bot locked and limited conversation to collaborators Jun 29, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Servicing-consider Issue for next servicing release review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants