Skip to content

Fixes several use cases where aspire-managed processes may leak - #18566

Merged
Karol Zadora-Przylecki (karolz-ms) merged 39 commits into
mainfrom
dev/karolz/analyze-managed-process-leaks
Jul 7, 2026
Merged

Fixes several use cases where aspire-managed processes may leak#18566
Karol Zadora-Przylecki (karolz-ms) merged 39 commits into
mainfrom
dev/karolz/analyze-managed-process-leaks

Conversation

@karolz-ms

@karolz-ms Karol Zadora-Przylecki (karolz-ms) commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Description

There are at least 4 scenarios when aspire-managed processes may leak:

  1. PID-reuse. RemoteHost/OrphanDetector.cs polls the parent by PID only (REMOTE_APP_HOST_PID), no start-time check, and the CLI's start time is never propagated. When the launching CLI dies and its PID is reused (pretty common if machine is heavily used, e.g. during test run), the server thinks its parent is alive and never exits → permanent leak. The .NET AppHost path (CliOrphanDetector) already does PID+start-time; the RemoteHost path was never brought to that bar.

  2. dashboard and nuget commands have no orphan detection at all. A hard-killed CLI (SIGKILL from a test runner, Ctrl-C during a slow restore) leaves them lingering forever.

  3. During aspire start, if the launcher CLI process is killed (SIGKILL) mid-start, the detached CLI process wil just complete the startup and keep running.

  4. There is no active reaping of already-leaked orphans. once leaked they persist and get rediscovered by the auxiliary backchannel monitor, producing the "started successfully but isn't running" false success.

Fixes #18050

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 -- not really necessary
    • 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

Copilot AI review requested due to automatic review settings June 29, 2026 23:34
@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 -- 18566

Or

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

@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.

Pull request overview

This PR addresses leaked aspire-managed (and AppHost) processes — most visible on non-Windows platforms — that accumulate when a launching/parent process dies abnormally during startup or PID reuse defeats the existing PID-only orphan detection (issue #18050). It introduces a shared, dependency-free ProcessStartTimeHelper (PID + start-time identity, surviving PID reuse) and a reusable ParentProcessLivenessMonitor, then wires several watchdogs across the CLI, RemoteHost, and the aspire-managed NuGet helper, plus an on-demand OrphanedAppHostCollector invoked by aspire ps / aspire stop --all.

Changes:

  • New shared primitives: ProcessStartTimeHelper (PID + start-time liveness checks) and ParentProcessLivenessMonitor (poll loop with idempotent teardown), with ProcessSignaler.AreClose refactored to reuse the helper.
  • RemoteHost OrphanDetector now verifies parent PID and start time (via IConfiguration), falling back to PID-only for older CLIs; aspire-managed nuget gains a ParentProcessWatchdog; detached aspire start/run --detach children watch their launcher during startup via LauncherLivenessMonitor and disarm at readiness.
  • New OrphanedAppHostCollector reaps AppHost trees whose launching CLI is gone (used by ps and stop --all); new ASPIRE_LAUNCHER_* / REMOTE_APP_HOST_STARTED config names and start-time propagation in the AppHost server spawn paths.

Reviewed changes

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

Show a summary per file
File Description
src/Shared/ProcessStartTimeHelper.cs New dependency-free PID+start-time liveness/parse helpers with 1s tolerance.
src/Shared/ParentProcessLivenessMonitor.cs New shared poll-based monitor invoking a callback once when the parent is gone; idempotent async disposal.
src/Shared/ProcessSignaler.cs AreClose now delegates the truncate-and-compare math to the shared helper (behavior preserved).
src/Shared/KnownConfigNames.cs Adds REMOTE_APP_HOST_STARTED, ASPIRE_LAUNCHER_PID/STARTED constants.
src/Aspire.Hosting.RemoteHost/OrphanDetector.cs Reads config instead of env directly; adds start-time verification with PID-only fallback.
src/Aspire.Managed/Program.cs · NuGet/ParentProcessWatchdog.cs NuGet helper self-terminates if the launching CLI dies (cancel then force-exit 124).
src/Aspire.Cli/Processes/LauncherLivenessMonitor.cs Watches the foreground launcher of a detached child during startup.
src/Aspire.Cli/Commands/RunCommand.cs Starts the launcher monitor for detached children; disarms at readiness; disposes on all exit paths.
src/Aspire.Cli/Commands/AppHostLauncher.cs Stamps launcher PID + start time onto the detached child environment.
src/Aspire.Cli/Layout/LayoutProcessRunner.cs Stamps CLI identity onto layout tool env without mutating the caller's dictionary.
src/Aspire.Cli/Backchannel/OrphanedAppHostCollector.cs New collector that stops AppHosts whose launching CLI is dead and removes their sockets.
src/Aspire.Cli/Commands/PsCommand.cs · StopCommand.cs · Program.cs Inject and invoke the collector; register it in DI.
src/Aspire.Cli/Projects/{PrebuiltAppHostServer,DotNetBasedAppHostServerProject}.cs Use config-name constants and propagate parent start time.
*.csproj (Aspire.Cli, Aspire.Hosting, Aspire.Hosting.RemoteHost, Aspire.Managed) Link the new shared files into each consuming assembly.
tests/** (7 new/updated test files) Cover helper behavior, the monitors, the orphan detector start-time paths, env stamping, and IsOrphaned.

I verified the cross-cutting concerns that this kind of change typically breaks: every project linking ProcessSignaler.cs also links the new ProcessStartTimeHelper.cs it now depends on; the RemoteHost default host builder surfaces environment variables through IConfiguration (so the env→config switch is safe); the detached launcher stays alive until the child reports readiness (so disarming at readiness is correct and a normal launcher exit won't tear down a healthy AppHost); and the watchdog/monitor disposal ordering avoids use-after-dispose and skips the hard Environment.Exit when the operation finishes cleanly. No blocking correctness issues were found, but the change spans process-killing, Environment.Exit, and concurrent teardown across multiple components, which carries meaningful regression risk.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Posted 2 review comments: 1 incomplete process-leak coverage issue and 1 missing regression coverage issue.

Comment thread src/Aspire.Cli/Layout/LayoutProcessRunner.cs Outdated
Comment thread src/Aspire.Cli/Commands/AppHostLauncher.cs Outdated
@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.

@github-actions

This comment has been minimized.

@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.

Comment thread src/Aspire.Cli/Processes/LauncherLivenessMonitor.cs
Comment thread src/Aspire.Managed/ParentProcessWatchdog.cs
Two failures were found while testing detached `aspire start` (PR #18566):

- Scenario 2 (happy path): the launcher liveness monitor was disarmed only
  after full readiness (after GetDashboardUrlsAsync + the 2s early-exit
  observation window + NotifyAppHostReadyAsync). The foreground launcher
  exits as soon as it observes readiness over the auxiliary backchannel, so
  its ~1s-later detection by the monitor cancelled a healthy run. Disarm the
  monitor as soon as the child<->AppHost backchannel is established instead;
  from that point the AppHost's orphan detector anchors to this child via
  ASPIRE_CLI_PID, so the launcher's normal exit is irrelevant.

- Scenario 3 (launcher killed before readiness): a termination signal during
  startup unwound to the cancellation catch without awaiting pendingRun, so
  the CLI exited while the dotnet run shutdown ladder was still running,
  orphaning an AppHost that aspire ps could not see. Hoist pendingRun and the
  run CTS to the outer scope and add a bounded teardown backstop in the outer
  finally so a detached child waits for the AppHost process tree to be killed
  before it returns.

Adds two deterministic regression tests (verified to fail without the fixes).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 30, 2026 04:01
@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.

Pull request overview

Copilot reviewed 33 out of 33 changed files in this pull request and generated 1 comment.

Comment thread src/Aspire.Cli/Commands/PsCommand.cs
@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.

Pull request overview

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

@github-actions

This comment has been minimized.

@github-actions

github-actions Bot commented Jul 7, 2026

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

This comment has been minimized.

@github-actions

github-actions Bot commented Jul 7, 2026

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.

Copilot AI review requested due to automatic review settings July 7, 2026 23:02
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Tests selector (audit mode)

The full test matrix and all jobs still run in audit mode. The tests and jobs below are what selective CI would run under enforcement.

53 / 99 test projects · 5 jobs, from 71 changed files.

Selected test projects (53 / 99)

Aspire.Cli.EndToEnd.Tests, Aspire.Cli.Tests, Aspire.Dashboard.Components.Tests, Aspire.Dashboard.Tests, Aspire.EndToEnd.Tests, Aspire.Hosting.Analyzers.Tests, Aspire.Hosting.Azure.Kubernetes.Tests, Aspire.Hosting.Azure.Kusto.Tests, Aspire.Hosting.Azure.Tests, Aspire.Hosting.Blazor.Tests, Aspire.Hosting.Browsers.Tests, Aspire.Hosting.CodeGeneration.Go.Tests, Aspire.Hosting.CodeGeneration.Java.Tests, Aspire.Hosting.CodeGeneration.Python.Tests, Aspire.Hosting.CodeGeneration.Rust.Tests, Aspire.Hosting.CodeGeneration.TypeScript.Tests, Aspire.Hosting.Containers.Tests, Aspire.Hosting.DevTunnels.Tests, Aspire.Hosting.Docker.Tests, Aspire.Hosting.Dotnet.Tests, Aspire.Hosting.DotnetTool.Tests, Aspire.Hosting.EntityFrameworkCore.Tests, Aspire.Hosting.Foundry.Tests, Aspire.Hosting.Garnet.Tests, Aspire.Hosting.GitHub.Models.Tests, Aspire.Hosting.Go.Tests, Aspire.Hosting.JavaScript.Tests, Aspire.Hosting.Kafka.Tests, Aspire.Hosting.Keycloak.Tests, Aspire.Hosting.Kubernetes.Tests, Aspire.Hosting.Maui.Tests, Aspire.Hosting.Milvus.Tests, Aspire.Hosting.MongoDB.Tests, Aspire.Hosting.MySql.Tests, Aspire.Hosting.Nats.Tests, Aspire.Hosting.OpenAI.Tests, Aspire.Hosting.Oracle.Tests, Aspire.Hosting.Orleans.Tests, Aspire.Hosting.PostgreSQL.Tests, Aspire.Hosting.Python.Tests, Aspire.Hosting.Qdrant.Tests, Aspire.Hosting.RabbitMQ.Tests, Aspire.Hosting.Redis.Tests, Aspire.Hosting.RemoteHost.Tests, Aspire.Hosting.Seq.Tests, Aspire.Hosting.SqlServer.Tests, Aspire.Hosting.Testing.Tests, Aspire.Hosting.Tests, Aspire.Hosting.Valkey.Tests, Aspire.Hosting.Yarp.Tests, Aspire.Managed.Tests, Aspire.Playground.Tests, Aspire.Templates.Tests

Selected jobs (5)

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


How these were chosen — grouped by what changed

⚠️ 31 of the 53 selected test projects come from a single change — src/Aspire.Hosting/Aspire.Hosting.csproj.

🔧 src/Aspire.Hosting/Aspire.Hosting.csproj (changed source)
31 via the project graph

show 31

Aspire.Hosting.Analyzers.Tests (2 hops), Aspire.Hosting.Azure.Kubernetes.Tests (2 hops), Aspire.Hosting.Azure.Kusto.Tests (2 hops), Aspire.Hosting.Azure.Tests, Aspire.Hosting.DevTunnels.Tests (2 hops), Aspire.Hosting.Docker.Tests (2 hops), Aspire.Hosting.EntityFrameworkCore.Tests (2 hops), Aspire.Hosting.Foundry.Tests (2 hops), Aspire.Hosting.Garnet.Tests (2 hops), Aspire.Hosting.Go.Tests (2 hops), Aspire.Hosting.JavaScript.Tests (2 hops), Aspire.Hosting.Kafka.Tests (2 hops), Aspire.Hosting.Keycloak.Tests (2 hops), Aspire.Hosting.Kubernetes.Tests (2 hops), Aspire.Hosting.Maui.Tests, Aspire.Hosting.Milvus.Tests (2 hops), Aspire.Hosting.MongoDB.Tests (2 hops), Aspire.Hosting.MySql.Tests (2 hops), Aspire.Hosting.Nats.Tests (2 hops), Aspire.Hosting.Oracle.Tests (2 hops), Aspire.Hosting.Orleans.Tests (2 hops), Aspire.Hosting.PostgreSQL.Tests (2 hops), Aspire.Hosting.Python.Tests (2 hops), Aspire.Hosting.Qdrant.Tests (2 hops), Aspire.Hosting.RabbitMQ.Tests (2 hops), Aspire.Hosting.Redis.Tests (2 hops), Aspire.Hosting.Seq.Tests (2 hops), Aspire.Hosting.SqlServer.Tests (2 hops), Aspire.Hosting.Valkey.Tests (2 hops), Aspire.Hosting.Yarp.Tests (2 hops), Aspire.Playground.Tests

🔧 src/Aspire.Hosting.RemoteHost/Aspire.Hosting.RemoteHost.csproj (changed source)
1 directly: Aspire.Hosting.RemoteHost.Tests
5 via the project graph: Aspire.Hosting.CodeGeneration.Go.Tests, Aspire.Hosting.CodeGeneration.Java.Tests, Aspire.Hosting.CodeGeneration.Python.Tests, Aspire.Hosting.CodeGeneration.Rust.Tests, Aspire.Hosting.CodeGeneration.TypeScript.Tests

🔧 src/Shared/KnownConfigNames.cs (changed source)
6 via the project graph: Aspire.Hosting.Browsers.Tests, Aspire.Hosting.Containers.Tests, Aspire.Hosting.GitHub.Models.Tests, Aspire.Hosting.OpenAI.Tests, Aspire.Hosting.Testing.Tests, Aspire.Templates.Tests

🧪 tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj (changed test)
1 directly: Aspire.Hosting.Tests
2 via the project graph: Aspire.Hosting.Blazor.Tests, Aspire.Hosting.Dotnet.Tests

🔧 src/Aspire.Dashboard/DashboardWebApplication.cs (changed source)
2 via the project graph: Aspire.Dashboard.Components.Tests, Aspire.Hosting.DotnetTool.Tests (2 hops)

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

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

🔧 src/Aspire.Hosting.RemoteHost/OrphanDetector.cs (changed source)
1 directly: Aspire.Hosting.RemoteHost.Tests

🧪 tests/Aspire.Cli.Tests/Aspire.Cli.Tests.csproj (changed test)
1 directly: Aspire.Cli.Tests

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

🧪 tests/Aspire.Cli.Tests/Backchannel/OrphanedAppHostCollectorTests.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/DotNet/ProcessInvocationOptionsTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Layout/LayoutProcessRunnerTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/NuGet/BundleNuGetPackageCacheTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Processes/DetachedProcessLauncherTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Processes/LauncherLivenessMonitorTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Processes/OrphanDetectionEnvironmentTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Processes/ParentProcessLivenessMonitorTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Processes/ProcessTreeGracefulShutdownServiceTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Processes/WindowsConsoleProcessJobTests.cs (changed test)
1 directly: Aspire.Cli.Tests

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

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

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

🧪 tests/Aspire.Dashboard.Tests/Integration/StartupTests.cs (changed test)
1 directly: Aspire.Dashboard.Tests

🧪 tests/Aspire.Hosting.RemoteHost.Tests/OrphanDetectorTests.cs (changed test)
1 directly: Aspire.Hosting.RemoteHost.Tests

🧪 tests/Aspire.Hosting.Tests/Backchannel/AuxiliaryBackchannelRpcTargetTests.cs (changed test)
1 directly: Aspire.Hosting.Tests

🧪 tests/Aspire.Hosting.Tests/Backchannel/AuxiliaryBackchannelTests.cs (changed test)
1 directly: Aspire.Hosting.Tests

🧪 tests/Aspire.Hosting.Tests/Cli/CliOrphanDetectorTests.cs (changed test)
1 directly: Aspire.Hosting.Tests

🧪 tests/Aspire.Hosting.Tests/Cli/ProcessSignalerTests.cs (changed test)
1 directly: Aspire.Hosting.Tests

🧪 tests/Aspire.Hosting.Tests/Cli/ProcessStartTimeHelperTests.cs (changed test)
1 directly: Aspire.Hosting.Tests

🧪 tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs (changed test)
1 directly: Aspire.Hosting.Tests

🧪 tests/Aspire.Managed.Tests/ParentProcessWatchdogTests.cs (changed test)
1 directly: Aspire.Managed.Tests

Job reasons

Job Triggered by
cli-starter • affected project Aspire.Cli
• selected test Aspire.Cli.Tests
deployment-e2e affected project Aspire.Cli
extension-e2e src/Aspire.Cli/Aspire.Cli.csproj, src/Aspire.Cli/Backchannel/AuxiliaryBackchannelMonitor.cs, src/Aspire.Cli/Backchannel/OrphanedAppHostCollector.cs, src/Aspire.Cli/Commands/AppHostLauncher.cs, src/Aspire.Cli/Commands/DashboardRunCommand.cs, src/Aspire.Cli/Commands/PsCommand.cs, src/Aspire.Cli/Commands/RunCommand.cs, src/Aspire.Cli/Commands/StopCommand.cs, src/Aspire.Cli/DotNet/DotNetCliRunner.cs, src/Aspire.Cli/DotNet/ProcessExecution.cs, src/Aspire.Cli/DotNet/ProcessExecutionFactory.cs, src/Aspire.Cli/Layout/LayoutProcessRunner.cs, src/Aspire.Cli/NuGet/BundleNuGetPackageCache.cs, src/Aspire.Cli/NuGet/BundleNuGetService.cs, src/Aspire.Cli/Processes/DetachedProcessLauncher.Windows.cs, src/Aspire.Cli/Processes/IAppHostStopper.cs, src/Aspire.Cli/Processes/IsolatedProcess.Windows.cs, src/Aspire.Cli/Processes/IsolatedProcess.cs, src/Aspire.Cli/Processes/LauncherLivenessMonitor.cs, src/Aspire.Cli/Processes/OrphanDetectionEnvironment.cs, src/Aspire.Cli/Processes/ProcessTreeGracefulShutdownService.cs, src/Aspire.Cli/Processes/WindowsConsoleProcessJob.cs, src/Aspire.Cli/Processes/WindowsProcessInterop.cs, src/Aspire.Cli/Profiling/ProfileCaptureService.cs, src/Aspire.Cli/Program.cs, src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs, src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs, src/Aspire.Dashboard/DashboardWebApplication.cs, src/Aspire.Hosting.RemoteHost/Aspire.Hosting.RemoteHost.csproj, src/Aspire.Hosting.RemoteHost/OrphanDetector.cs, src/Aspire.Hosting/Aspire.Hosting.csproj, src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelRpcTarget.cs, src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelService.cs, src/Aspire.Hosting/Backchannel/BackchannelDataTypes.cs, src/Aspire.Hosting/Cli/CliOrphanDetector.cs, src/Aspire.Hosting/Dcp/DcpProcessMonitor.cs, tests/Aspire.Cli.Tests/Aspire.Cli.Tests.csproj, tests/Aspire.Cli.Tests/Backchannel/AuxiliaryBackchannelMonitorTests.cs, tests/Aspire.Cli.Tests/Backchannel/OrphanedAppHostCollectorTests.cs, tests/Aspire.Cli.Tests/Commands/AppHostLauncherTests.cs, tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs, tests/Aspire.Cli.Tests/DotNet/ProcessInvocationOptionsTests.cs, tests/Aspire.Cli.Tests/Layout/LayoutProcessRunnerTests.cs, tests/Aspire.Cli.Tests/NuGet/BundleNuGetPackageCacheTests.cs, tests/Aspire.Cli.Tests/Processes/DetachedProcessLauncherTests.cs, tests/Aspire.Cli.Tests/Processes/LauncherLivenessMonitorTests.cs, tests/Aspire.Cli.Tests/Processes/OrphanDetectionEnvironmentTests.cs, tests/Aspire.Cli.Tests/Processes/ParentProcessLivenessMonitorTests.cs, tests/Aspire.Cli.Tests/Processes/ProcessTreeGracefulShutdownServiceTests.cs, tests/Aspire.Cli.Tests/Processes/WindowsConsoleProcessJobTests.cs, tests/Aspire.Cli.Tests/TestServices/TestAppHostStopper.cs, tests/Aspire.Cli.Tests/Utils/AppHostHelperTests.cs, tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs, tests/Aspire.Hosting.Tests/Cli/CliOrphanDetectorTests.cs, tests/Aspire.Hosting.Tests/Cli/ProcessSignalerTests.cs, tests/Aspire.Hosting.Tests/Cli/ProcessStartTimeHelperTests.cs
• affected project Aspire.Cli
polyglot affected project Aspire.Cli
typescript-api-compat affected project Aspire.Cli

Selection computed for commit 6c3705e.

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

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

@github-actions

github-actions Bot commented Jul 7, 2026

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.

@karolz-ms

Copy link
Copy Markdown
Contributor Author

PR Testing Report

PR Information

Artifact Version Verification

  • Expected Commit: 6c3705ee56e0a5537e408fde1116acf2f8bcb379
  • Installed CLI version: 13.5.0-pr.18566.g6c3705ee
  • Status: ✅ Verified (version contains the head short SHA 6c3705ee; PR hive used for templates)

Changes Analyzed

Change Categories

  • CLI changes (process lifecycle: launchers, isolated/detached processes, Windows job objects, orphan detection, backchannel)
  • Hosting changes (CliOrphanDetector, DcpProcessMonitor, auxiliary backchannel)
  • New Aspire.Managed watchdog (ParentProcessWatchdog) for dashboard/nuget helpers
  • Aspire.Hosting.RemoteHost orphan detector (PID → PID+start-time)
  • Dashboard command (DashboardRunCommand) — env-var passing + managed host
  • Shared (ProcessStartTimeHelper, ParentProcessLivenessMonitor, ProcessSignaler)
  • Tests (extensive new unit coverage)

The 4 leak scenarios the PR fixes

  1. RemoteHost PID-reuse — orphan detector now verifies PID + start time (stable ms / legacy seconds).
  2. dashboard/nuget had no orphan detection — new ParentProcessWatchdog (Unix) + Windows job-object kill-on-close.
  3. aspire start launcher killed mid-start — new LauncherLivenessMonitor aborts the detached child.
  4. No active reaping of leaked orphans — new OrphanedAppHostCollector wired into ps/stop.

Environment Notes

  • Docker: Redis container never reached Running (cache stuck in Starting, docker ps -a empty) — a DCP/Docker warmup issue unrelated to this PR. Container-cleanup-on-kill was therefore not exercised. All process-leak scenarios were validated with the aspire-managed process tree (CLI supervisor, aspire-managed dashboard, AppHost, 4× DCP, project processes), which is exactly what the PR targets.
  • Tests ran locally on Windows (where most of the platform-specific job-object code lives). Isolated install under a temp dir; gh auth untouched.

Test Scenarios Executed

Scenario 1: Regression — clean start / ps / stop

Coverage: Happy path · Status: ✅ Passed

  • aspire new aspire-starter (Redis on) → aspire start succeeded (~21–30s).
  • aspire ps accurately listed the AppHost (PID 42408, CLI PID 60908) + dashboard URL.
  • aspire describe showed apiservice Healthy, cache Starting, webfrontend Waiting.
  • aspire stop reported success in 1s; the entire tracked tree (CLI supervisor, AppHost, aspire-managed dashboard, 4× DCP, apiservice) was gone within 8s.

Scenario 2: Core leak fix — hard-kill the CLI supervisor

Coverage: Core fix (leak) · Status: ✅ Passed

  • Started app; captured 9-process tree; identified CLI supervisor aspire.exe PID 51592 (parent of AppHost 14816).
  • Stop-Process -Force on only the supervisor (simulating SIGKILL from a test runner).
  • Within 3 seconds the entire tree self-terminated (AppHost, dashboard, 4× DCP, apiservice). Process count → 0. No leak.
  • Validates Windows job-object kill-on-close + CliOrphanDetector backstop.

Scenario 3: Standalone dashboard orphan (fix #2)

Coverage: New behavior · Status: ✅ Passed

  • aspire dashboard run --allow-anonymous → CLI PID 70576 → aspire-managed.exe dashboard PID 71344; served HTTP 200 on :18888.
  • Hard-killed the CLI → the managed dashboard terminated within 2s; no aspire/aspire-managed left. Previously aspire dashboard had no orphan detection and would linger.

Scenario 4: ps/stop accuracy + orphan reaping (fix #4)

Coverage: Correctness / backstop · Status: ✅ Passed (with note)

  • After the hard-kills, aspire ps correctly reported "No running AppHost found" — no false "started successfully but isn't running" status. This is the user-visible half of fix Move repo to 8.0.100-rtm build #4.
  • End-to-end active-reaping of a live orphan by OrphanedAppHostCollector could not be triggered on Windows: the job-object kill + in-process CliOrphanDetector reap the tree first (the desired behavior), so a live orphan doesn't persist for ps/stop to collect. That path is covered by unit tests (OrphanedAppHostCollectorTests, 406 lines).
  • Minor: 0-byte backchannel socket files accumulate in ~/.aspire/cli/bch and are not pruned by ps/stop (count grew 8→9 across runs). Harmless — ps never reports them as running — and likely pre-existing Windows AF_UNIX residue.

Scenario 5: Launcher killed mid-start (fix #3, LauncherLivenessMonitor)

Coverage: New behavior (leak) · Status: ✅ Passed

  • aspire start on a fresh unbuilt app; launcher PID 32204 spawned detached child supervisor 62580.
  • Killed only the launcher while still mid-build (no "started successfully"). The detached child self-aborted within 2s, leaving 0 processes. No AppHost leaked.
  • Direct log evidence from the detached child:
    • Detached child: watching launcher process 32204 until the AppHost is ready.
    • Launcher process 32204 exited before the AppHost reached readiness. Shutting the detached AppHost down to avoid leaking it.
  • Minor: the abort surfaces in the child log as ❌ An unexpected error occurred: A task was canceled. — ungraceful but harmless (no one observes it; the launcher was killed).

Scenario 6: Unhappy paths

Coverage: Negative / boundary · Status: ✅ Passed

Command Result Exit
aspire stop (nothing running) "No running AppHost found." 0
aspire stop --apphost <valid, not running> "No AppHost is currently running for '…'. Use 'aspire run' to start it first." 0
aspire stop --apphost <non-existent path> "❌ The --apphost option specified a project that does not exist." 7
aspire ps (nothing running) "No running AppHost found." 0

Not reproduced end-to-end (covered by unit tests)

  • RemoteHost PID-reuse fix (PR scenario 1): requires a remote-apphost server context not reachable via the dogfood CLI. Covered by OrphanDetectorTests (166 lines) verifying PID+start-time and legacy fallback.
  • Active reaping via OrphanedAppHostCollector (fix Move repo to 8.0.100-rtm build #4): see Scenario 4 note.

Summary

Scenario Status Notes
1. Regression clean start/stop ✅ Passed stop in 1s, full teardown
2. Hard-kill CLI supervisor ✅ Passed whole tree reaped in 3s, no leak
3. Standalone dashboard orphan ✅ Passed dashboard reaped in 2s after CLI kill
4. ps/stop accuracy + reaping ✅ Passed no false "running"; socket-residue note
5. Launcher killed mid-start ✅ Passed detached child aborts in 2s; log-confirmed
6. Unhappy paths ✅ Passed correct messages + exit codes

Overall Result

✅ PR VERIFIED — All executed scenarios passed. The core process-leak fixes behave as intended on Windows: hard-killing the CLI supervisor or the dashboard CLI reaps the whole tree promptly, and killing the launcher mid-start aborts the detached child instead of leaking an AppHost.

Recommendations / minor observations (non-blocking)

  1. Consider a cleaner detached-child abort message instead of the raw TaskCanceledException surface ("An unexpected error occurred: A task was canceled.") when LauncherLivenessMonitor cancels a mid-start run.
  2. Consider whether stale 0-byte backchannel socket files in ~/.aspire/cli/bch should be pruned by ps/stop on Windows (harmless today, but they accumulate). Verify whether this predates the PR.
  3. Re-run the Docker/Redis container path in an environment where the container reaches Running to also confirm container cleanup on hard-kill (not exercised here due to a Docker warmup issue unrelated to the PR).

@karolz-ms
Karol Zadora-Przylecki (karolz-ms) merged commit a89e7ad into main Jul 7, 2026
665 of 668 checks passed
@karolz-ms
Karol Zadora-Przylecki (karolz-ms) deleted the dev/karolz/analyze-managed-process-leaks branch July 7, 2026 23:41
@github-actions github-actions Bot added this to the 13.5 milestone Jul 7, 2026
@aspire-repo-bot

This comment has been minimized.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

⚠️ CI Failure Analysis: Possible Flaky Test(s)

The CI build failed due to test failure(s) that appear unrelated to the PR changes. These may be flaky tests.

Suspected flaky test(s):

  • Aspire.Cli.Tests.Commands.RunCommandTests.RunCommand_DetachedChild_WhenSignaledBeforeReadiness_AwaitsAppHostTeardownBeforeExit in job Tests / Cli / Cli (ubuntu-latest)
    • Error: System.TimeoutException : The operation at /home/runner/work/aspire/aspire/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs:469 timed out after reaching the limit of 30000ms.
    • Stack Trace (first frames):
      at Microsoft.AspNetCore.InternalTesting.AsyncTestHelpers.TimeoutAfter[T](Task`1 task, TimeSpan timeout, String filePath, Int32 lineNumber) in /home/runner/work/aspire/aspire/tests/Shared/AsyncTestHelpers.cs:line 160
         at Aspire.Cli.Tests.Commands.RunCommandTests.RunCommand_DetachedChild_WhenSignaledBeforeReadiness_AwaitsAppHostTeardownBeforeExit() in /home/runner/work/aspire/aspire/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs:line 469
      
    • Why likely flaky: TimeoutException (not an assertion or build failure) in a timing-sensitive test involving detached child process management, signal handling before app host readiness, and waiting for teardown. The 30-second timeout may be insufficient under CI load for such complex async process coordination. The test was added by this PR (+290 lines to RunCommandTests.cs), testing new process lifecycle functionality, but the intermittent timeout pattern indicates environmental sensitivity rather than a definitive code defect.

Suggested actions:

  • Re-run the failed CI jobs to confirm if the failure is intermittent
  • If the test continues to fail, consider quarantining it using /quarantine-test <test name> <issue URL>
  • Search existing issues to see if this test is already known to be flaky

You can re-run the failed jobs from the workflow run page.

@aspire-repo-bot

Copy link
Copy Markdown
Contributor

📝 Documentation has been drafted in microsoft/aspire.dev#1439 targeting release/13.5.

Triggered signal cli_command_file_changed (DashboardRunCommand.cs, PsCommand.cs, RunCommand.cs, StopCommand.cs). Reviewed the diffs: PsCommand.cs and StopCommand.cs now call OrphanedAppHostCollector.CollectAsync before listing/stopping AppHosts, a user-visible behavior change to already-documented commands. Updated reference/cli/commands/aspire-ps.mdx and reference/cli/commands/aspire-stop.mdx to note the best-effort orphaned-AppHost cleanup that now runs before each command's main action.

Note

This draft PR needs human review before merging.

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 13.4.3 - Starts successfully but then isn't running

6 participants