Skip to content

Fix extension CLI teardown during AppHost build - #20021

Open
Ella Hathaway (ellahathaway) wants to merge 4 commits into
mainfrom
ellahathaway-fix-dynamic-debug-teardown
Open

Fix extension CLI teardown during AppHost build#20021
Ella Hathaway (ellahathaway) wants to merge 4 commits into
mainfrom
ellahathaway-fix-dynamic-debug-teardown

Conversation

@ellahathaway

@ellahathaway Ella Hathaway (ellahathaway) commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Description

Stopping a dynamic debug session could terminate the Aspire CLI while a cold single-file AppHost build was still running. The extension's stopCli RPC used immediate process exit, bypassing normal command cancellation and child-process cleanup. On Windows, the orphaned dotnet build process retained the fixture workspace as its working directory, so E2E teardown failed with EBUSY while removing the second dynamic-debug workspace.

This change routes extension stop requests through the shared ConsoleCancellationManager, allowing the CLI's existing cancellation and process-tree cleanup paths to run. RunCommand also keeps pending pre-build work owned by the active command handler during manager-triggered shutdown. That is important because the cancellation manager's graceful deadline and final drain are the process-level escape hatch; returning from the handler after a separate local timeout could otherwise make shutdown appear complete before nested build cleanup finished.

Direct callers that cancel RunCommand with an unrelated token still use the bounded five-second wait. Those invocations do not have the cancellation manager's process-level deadline, so waiting indefinitely there could hang embedded or test callers.

The manager-owned cancellation regression requests the stop RPC synchronously from the fake project's RunAsync callback. This ensures the command enters its cleanup wait before fake time advances, rather than relying on Task.Yield() to schedule the handler. The pending run is released and awaited in finally, and the signaling time provider is shared with AppHostLauncherTests.

Validation:

  • 169 passed and two expected platform skips across RunCommandTests, AppHostLauncherTests, and ExtensionBackchannelTests (one Unix-only case and one case requiring Windows symlink privileges).
  • Negative control: temporarily restoring the five-second local timeout for manager-owned cancellation makes the focused regression fail at the timeout-policy assertion. The correct production code was restored and the affected classes were rerun successfully.
  • Earlier validation of the production fix: the Windows dynamic-debug-configuration shard passed three complete runs (9/9 tests), including validation with a temporary delayed AppHost build that reproduced the directory lock before the fix. The follow-up changes only refine test coverage and share its helper.

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
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No

Route extension stop requests through cooperative cancellation and wait for in-flight pre-build cleanup before the CLI exits.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Sep 9, 2026

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 -- 20021

Or

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

@github-actions

This comment has been minimized.

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.

🟡 Changes recommended

The five-second cleanup timeout can still let a pending build outlive the CLI.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Routes extension stop requests through cooperative CLI cancellation and waits for AppHost build cleanup.

Changes:

  • Replaces immediate CLI termination with cancellation-manager signaling.
  • Adds pending build cleanup handling and regression tests.
File summaries
File Description
src/Aspire.Cli/Backchannel/ExtensionRpcTarget.cs Cooperatively cancels CLI execution.
src/Aspire.Cli/Commands/RunCommand.cs Waits for canceled pre-build work.
tests/Aspire.Cli.Tests/Backchannel/ExtensionBackchannelTests.cs Tests stop-request cancellation.
tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs Tests cleanup ordering.
tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs Supplies the cancellation manager.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Aspire.Cli/Commands/RunCommand.cs
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

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.

🟢 Approval recommended

The cancellation lifecycle is correctly bounded and covered by focused regression tests.

Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Consolidate startup and pre-build cancellation into one helper with an explicit wait budget. Keep manager-owned shutdown under the central deadline, preserve bounded direct cancellation, and retain late task fault observation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

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.

🟢 Approval recommended

The shutdown paths are correctly distinguished and covered by focused regression tests.

Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Cancel synchronously at the fake project boundary and share the signaling time provider between command tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

Tests selector

2 / 99 PR test projects · 4 PR jobs · 1 advisory-only target, from 7 changed files.

Selected PR test projects (2 / 99)

Aspire.Cli.EndToEnd.Tests, Aspire.Cli.Tests

Selected PR jobs (4)

cli-starter-validation, extension-e2e, polyglot, typescript-api-compat

Advisory workflow impact (1)

  • deployment-e2e (schedule/dispatch-only)

How these were chosen — grouped by what changed

📦 affected project Aspire.Cli
1 test: Aspire.Cli.EndToEnd.Tests

🧪 tests/Aspire.Cli.Tests/Backchannel/ExtensionBackchannelTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Commands/AppHostLauncherTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/TestServices/SignalingFakeTimeProvider.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs (changed test)
1 directly: Aspire.Cli.Tests

Job reasons

Job Triggered by
cli-starter-validation affected project Aspire.Cli
deployment-e2e affected project Aspire.Cli
extension-e2e src/Aspire.Cli/Backchannel/ExtensionRpcTarget.cs, src/Aspire.Cli/Commands/RunCommand.cs, tests/Aspire.Cli.Tests/Backchannel/ExtensionBackchannelTests.cs, tests/Aspire.Cli.Tests/Commands/AppHostLauncherTests.cs, tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs, tests/Aspire.Cli.Tests/TestServices/SignalingFakeTimeProvider.cs, tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs
• affected project Aspire.Cli
polyglot affected project Aspire.Cli
typescript-api-compat affected project Aspire.Cli

Selection computed for commit d00bf50.

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.

🟢 Approval recommended

The cooperative shutdown behavior and both timeout paths are covered without unresolved correctness issues.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

{
}
catch (TimeoutException ex)
catch (TimeoutException ex) when (timeout != Timeout.InfiniteTimeSpan)

@JamesNK James Newton-King (JamesNK) Sep 11, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Timeout can be infinite? How does that then work with the WaitAsync above?

Also, how could there be a TimeoutException when timeout is infinite?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One lifecycle issue needs to be addressed before merge: extension cancellation after build completion can still let RunCommand finish before the pending AppHost run task has cleaned up. I confirmed it with a focused negative-control test and verified the minimal gate removal against the existing cancellation tests.

Proof: the PR's focused CLI classes pass (169 passed, 2 expected platform skips). The added post-build negative control fails on the PR head, then it and the three related cancellation tests pass after removing only the buildWaitCompleted gate. I did not rerun the full VS Code dynamic-debug E2E shard; that remains the real-path proof gap.

Comment on lines +682 to +685
if (!buildWaitCompleted &&
runCts is not null &&
runTask is not null &&
!runTask.IsCompleted)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This gate leaves the same ownership gap after the build finishes. If stopCli arrives while WaitForAppHostStartupAsync is waiting for the backchannel/dashboard, buildWaitCompleted is already true, so the cancellation catch returns and the finally disposes runCts while runTask is still incomplete. I reproduced that with a focused test that completes BuildCompletionSource, waits for the ConnectingToAppHost status, sends stopCli, and blocks runTask: the command completed before cleanup. Removing only the buildWaitCompleted check made that test and the three existing cancellation tests pass. Please drain any incomplete runTask here; the existing infinite manager-owned versus five-second direct-caller policy still provides the intended bounds.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants