Protect CLI bundle versions with leases#17282
Conversation
Add per-version bundle leases so cleanup skips versions that are actively used during an upgrade. Launch bundle-owned child processes from version-rooted layouts and pass lease handoff metadata so aspire-managed can acquire its own lease. 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 -- 17282Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 17282" |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update race smoke test validationI ran a fresh smoke test focused on the original issue in #16306: concurrent bundle update/extraction (setup --force) while another CLI process launches bundle-owned child tools (sdk dump / �spire-managed). The PR dogfood workflow for the latest artifact was still pending at the time of testing and cli-native-archives-win-x64 was not yet downloadable, so I used a freshly built local win-x64 bundled CLI from this branch as the fallback validation path. Scenario: two timestamp-distinct copies of the bundled CLI shared one install root, forcing alternating bundle version IDs and cleanup decisions. Four workers ran concurrently for 90 seconds:
Result: Passed - 90 total iterations, 0 failures. CLI version tested: $(@{Status=Passed; Source=local Bundle.proj win-x64 build from current branch; SourceCli=C:\Users\danegsta\source\repos\aspire\main\artifacts\bin\Aspire.Cli\Debug\net10.0\win-x64\publish\aspire.exe; CliVersion=13.4.0-pr.17282.g85cda484b2; ArtifactDir=C:\Users\danegsta.copilot\session-state\bca2d521-ddd4-4f6e-abd3-7251972f2f5f\files\update-smoke-local-bundle-20260519-164340; TestDir=C:\Users\danegsta\AppData\Local\Temp\aspire-update-smoke-local-ef38007eabf34307a9fe5e917437587d; DurationSeconds=90; TotalIterations=90; Results=System.Object[]}.CliVersion) This directly exercises the original repro shape (setup --force racing with sdk dump) plus the cross-version cleanup path that motivated the lease changes. |
There was a problem hiding this comment.
Pull request overview
This PR introduces per-version “lease” files for Aspire CLI bundled layouts so upgrades/cleanup won’t delete a bundle version directory while another CLI/AppHost/child process is still using it. It shifts bundle-owned process launches to stable, version-rooted paths and propagates the version directory via ASPIRE_BUNDLE_VERSION_DIR so long-running child processes can self-protect.
Changes:
- Add
BundleVersionLease(shared) andBundleLayoutLease(CLI) to acquire/hold per-version leases and pass lease handoff env vars to child processes. - Update
BundleServiceto return a leased, version-rooted layout and to skip cleanup of versions with active leases. - Update CLI and
aspire-managedto use/propagate leases in key flows (AppHost server, dashboard, NuGet helper, profiling, DCP stop).
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs | Updates test bundle service fakes to the new lease-returning API. |
| tests/Aspire.Cli.Tests/Processes/ProcessShutdownServiceTests.cs | Adds coverage that DCP stop uses the version-rooted (leased) DCP path when available. |
| tests/Aspire.Cli.Tests/Commands/AppHostLauncherTests.cs | Fixes test wiring for the new ProcessShutdownService dependency. |
| tests/Aspire.Cli.Tests/BundleServiceIntegrationTests.cs | Adds integration tests for lease acquisition and lease-aware stale version cleanup. |
| src/Shared/BundleVersionLease.cs | Introduces the cross-process lease primitive (lease files under .leases). |
| src/Shared/BundleDiscovery.cs | Adds ASPIRE_BUNDLE_VERSION_DIR env var constant for lease handoff. |
| src/Aspire.Managed/Program.cs | Makes aspire-managed self-acquire a lease based on ASPIRE_BUNDLE_VERSION_DIR. |
| src/Aspire.Managed/Aspire.Managed.csproj | Links shared bundle discovery + lease sources into Aspire.Managed. |
| src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs | Carries and disposes a bundle layout lease; forwards ASPIRE_BUNDLE_VERSION_DIR to the server process. |
| src/Aspire.Cli/Projects/DotNetAppHostProject.cs | Updates AspireUseCliBundle flow to acquire/hold a lease and pass lease env vars to children. |
| src/Aspire.Cli/Projects/AppHostServerSession.cs | Adds disposal plumbing so disposable projects can be cleaned up with the session lifecycle. |
| src/Aspire.Cli/Projects/AppHostServerProject.cs | Uses lease-aware layout acquisition and hands the lease to PrebuiltAppHostServer. |
| src/Aspire.Cli/Profiling/ProfileCaptureService.cs | Acquires a lease for aspire-managed dashboard profiling and passes lease env vars. |
| src/Aspire.Cli/Processes/ProcessShutdownService.cs | Uses leased version-rooted DCP path when stopping a process tree. |
| src/Aspire.Cli/NuGet/BundleNuGetService.cs | Uses a lease (when available) and passes lease env vars to the NuGet helper. |
| src/Aspire.Cli/NuGet/BundleNuGetPackageCache.cs | Leases the layout before launching aspire-managed for NuGet search. |
| src/Aspire.Cli/Commands/DashboardRunCommand.cs | Leases the layout before launching dashboard to avoid races with bundle cleanup. |
| src/Aspire.Cli/Bundles/IBundleService.cs | Replaces “get layout” API with “acquire leased layout” API. |
| src/Aspire.Cli/Bundles/BundleService.cs | Implements lease acquisition + lease-aware cleanup and returns version-rooted layouts. |
| src/Aspire.Cli/Bundles/BundleLayoutLease.cs | Adds the CLI-side disposable wrapper that carries layout + optional lease + env handoff. |
| src/Aspire.Cli/Aspire.Cli.csproj | Links the shared lease implementation into the CLI build. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Re-running the failed jobs in the CI workflow for this pull request because 1 job was identified as retry-safe transient failures in the CI run attempt.
Matched test failure patterns (1 test)
|
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PR Testing ReportPR Information
CLI Version Verification
Changes AnalyzedThe PR changes CLI bundle extraction/layout lease handling, bundle NuGet cache/service behavior, AppHost server/session startup handoff, process shutdown, and related tests. Test Scenarios ExecutedScenario 1: Dogfood CLI install and version checkObjective: Verify the PR dogfood artifact is available and corresponds to the current PR head. Result: Passed Evidence:
Scenario 2: Fresh starter AppHost lifecycle with detached runObjective: Exercise the changed bundle/server/process paths by creating a project, building it, starting the AppHost in detached mode, listing the running AppHost, and stopping it. Steps:
Result: Passed Evidence:
Summary
Overall ResultPassed. The tested CLI lifecycle path worked with the PR dogfood build. |
Description
Fixes #16306
This prevents Aspire CLI bundle upgrades from deleting or invalidating a bundle version while another CLI/AppHost process is still using it. Before this change, upgraded CLIs could flip the
bundlereparse point and clean up olderversions\<id>directories while a concurrent process was about to launchaspire-managedor DCP from the previous bundle version.The CLI now acquires per-version leases under
versions\<id>\.leases, returns leased layouts rooted directly atversions\<id>, and makes cleanup skip versions with active lease files. Bundle-owned child processes receiveASPIRE_BUNDLE_VERSION_DIR, andaspire-managedself-leases on startup so long-running dashboard/server/NuGet helper processes keep their backing version alive after parent startup.Behavior by install/layout type
Bundled installs always extract into a versioned bundle layout before bundle-owned component paths are used. The install route only changes the extraction root:
winget,brew, anddotnet-toolinstalls extract beside the CLI binary, then useversions\<id>plus the publicbundle\pointer under that binary directory.scriptandprinstalls extract under the install prefix parent ofbin\, then use the sameversions\<id>plusbundle\pointer shape there.Layouts without a versioned bundle folder are treated as non-owned fallback layouts. That covers dev/SDK/external layouts where the running CLI has no embedded bundle payload. In those cases
EnsureExtractedAndAcquireLayoutAsyncfalls back to normal layout discovery and returns an unleased layout becauseBundleServicedoes not own or clean up those files, so there is no bundle cleanup race to protect.User-facing usage
No command syntax changes are required. Existing bundle flows such as setup/update, dashboard launch, NuGet restore/search, AppHost server startup, and
AspireUseCliBundlenow use stable version-rooted paths internally instead of launching through the mutablebundlepointer.Validation
.\restore.cmddotnet build .\src\Aspire.Cli\Aspire.Cli.csproj --no-restoredotnet build .\src\Aspire.Managed\Aspire.Managed.csproj --no-restoredotnet test --project .\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj --no-launch-profile -- --filter-class "*.BundleServiceIntegrationTests" --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true"dotnet test --project .\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj --no-launch-profile -- --filter-class "*.BundleServiceIntegrationTests" --filter-class "*.DotNetAppHostProjectTests" --filter-class "*.BundleNuGetPackageCacheTests" --filter-class "*.BundleNuGetServiceTests" --filter-class "*.DashboardRunCommandTests" --filter-class "*.ProfileCaptureServiceTests" --filter-class "*.PrebuiltAppHostServerTests" --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true"Full
Aspire.Cli.Testswas also run with quarantine/outerloop exclusions; it failed only in two unrelated git safecrlf tests (fatal: LF would be replaced by CRLF in .gitignore).Checklist
<remarks />and<code />elements on your triple slash comments?