Skip to content

Fix aspire run graceful shtudown behavior and unify with aspire start/aspire stop.#17814

Merged
danegsta merged 65 commits into
mainfrom
danegsta/windows-tsx-shutdown
Jun 23, 2026
Merged

Fix aspire run graceful shtudown behavior and unify with aspire start/aspire stop.#17814
danegsta merged 65 commits into
mainfrom
danegsta/windows-tsx-shutdown

Conversation

@danegsta

@danegsta danegsta commented Jun 2, 2026

Copy link
Copy Markdown
Member

Description

This PR unifies graceful shutdown for long-running aspire run child processes so AppHosts and guest/polyglot AppHosts follow one shutdown ladder instead of each owner having ad-hoc cancellation and teardown behavior.

The shared ladder is:

  1. cooperative command cancellation
  2. graceful signal dispatch
  3. bounded wait for the child process to exit
  4. forceful process-tree termination after the graceful budget expires
  5. short drain for process/output cleanup

The main motivation is Windows: TypeScript/JavaScript guest AppHosts often run through wrappers such as tsx/npm.cmd, and relying on inherited console Ctrl+C/Ctrl+Break propagation is unreliable. The PR now launches aspire run children in an isolated console on Windows and requests graceful shutdown through DCP stop-process-tree, which performs the AttachConsole + GenerateConsoleCtrlEvent(CTRL_C_EVENT) dance against the AppHost/process tree without also signaling the CLI.

The final Windows issue found during smoke testing was inherited CTRL+C suppression. If the CLI is launched as a descendant of a CREATE_NEW_PROCESS_GROUP root, Windows propagates the process-level "ignore CTRL+C" attribute to child processes, causing DCP's generated CTRL_C_EVENT to succeed but be silently dropped before it reaches the AppHost. Program.Main now calls SetConsoleCtrlHandler(NULL, FALSE) on Windows before registering signal handlers or launching children, so the AppHost and DCP-launched services inherit CTRL+C-enabled state.

On macOS/Linux the same ladder is used, but the graceful signal is SIGTERM via ProcessSignaler rather than DCP console-control events. aspire stop also targets the AppHost PID directly with SIGTERM on Unix to avoid launcher/dotnet run process-tree races.

Approach

The shutdown machinery is split into focused services:

  • ConsoleCancellationManager — process-level signal handler. First signal requests cooperative cancellation and starts the graceful/drain watcher; second signal expires the graceful window; third signal or drain timeout completes the final termination source. Handles SIGINT, SIGTERM, Windows SIGQUIT/CTRL_BREAK, Console.CancelKeyPress, and ProcessExit through the same path.
  • GracefulShutdownService — owns the shared graceful-window token consumed by process ladders.
  • IsolatedConsoleSpawner / IsolatedProcess / IsolatedProcessExecution — Windows CREATE_NEW_CONSOLE child launch for aspire run, with stdout/stderr pumping and integration into the existing IProcessExecution shape. Unix stays a thin process wrapper.
  • WindowsConsoleProcessJob — Windows kill-on-close safety net for isolated children, with BREAKAWAY_OK so DCP can outlive the CLI long enough to perform cleanup.
  • ProcessGracefulShutdownLadder — shared graceful signal -> bounded wait -> force tree-kill -> drain implementation used by AppHostServerSession, ProcessGuestLauncher, and direct .NET AppHost execution.
  • DetachedAppHostShutdownService — renamed from ProcessShutdownService; implements IProcessTreeGracefulShutdownSignaler. On Windows it shells out to DCP stop-process-tree; on Unix it sends SIGTERM.
  • ProcessTerminator, ProcessLifetimeAdapter, and ProcessPump — shared lower-level process lifetime helpers.

AppHostServerSession, ProcessGuestLauncher, and the direct .NET AppHost path now all consume the same graceful ladder. Short-lived RPC-style paths (publish, sdk generate, sdk dump, scaffolding, build/restore/package-add calls) intentionally remain on the inherited-console/default cancellation behavior.

Things worth a careful look

  • Parallel signaler execution. DCP stop-process-tree can deliver the signal quickly but then block until the target exits. The ladder invokes the signaler in parallel with WaitForExitAsync(gracefulToken) so DCP's wait cannot consume the entire graceful budget before the child gets its own exit wait.
  • Windows CTRL+C inheritance reset. SetConsoleCtrlHandler(NULL, FALSE) is called once at CLI startup to clear any inherited "ignore CTRL+C" state before child processes are spawned.
  • Direct .NET AppHost path. DotNetAppHostProject.RunAsync now opts into the same graceful infrastructure via ProcessInvocationOptions (IsolateConsole, ConsoleProcessJob, GracefulShutdownSignaler, ShutdownService). Non-run dotnet invocations leave those unset.
  • Backchannel-fault race. GuestAppHostProject.RunAsync uses an interlocked gate so the backchannel-fault continuation and normal finally cleanup cannot both own shutdown.
  • Force-kill escalation. After the graceful token expires, the ladder uses Kill(entireProcessTree: true) for the root process to avoid wrapper/orphan cases.

Validation

Windows smoke testing with a local build of the CLI and default aspire-starter AppHost:

  • Real terminal shape (CREATE_NEW_CONSOLE) — Signaler attaches to the CLI console and sends CTRL_C_EVENT; CLI exits gracefully in ~650-700ms, all AppHost sentinels (started, run-returned, finally) are written, and there are zero survivor descendants.
  • CREATE_NEW_PROCESS_GROUP grandparent regression — before the SetConsoleCtrlHandler(NULL, FALSE) fix, DCP fell back to its ~6s kill path and only the started sentinel was written. After the fix, the same case exits gracefully in ~650-700ms with all sentinels and zero survivors.
  • 5s budget re-check — both the real terminal and NPG-grandparent scenarios still complete in ~650ms with the graceful budget reverted to 5s.

Earlier macOS/Linux smoke testing confirmed the Unix SIGTERM path exercises the same ladder: graceful-respecting AppHosts exit cleanly, stalled AppHosts escalate at the graceful deadline, and double interrupt collapses the graceful window early.

Test impact

  • New/expanded coverage for GracefulShutdownService, ConsoleCancellationManager, ProcessGracefulShutdownLadder behavior through ProcessGuestLauncher, AppHostServerSession, ProcessExecution, IsolatedProcess, WindowsConsoleProcessJob, and DetachedAppHostShutdownService.
  • Added regression coverage for:
    • blocking DCP-style signalers not consuming the graceful budget sequentially
    • process-tree kill escalation removing descendants
    • direct .NET AppHost graceful branches for isolated and non-isolated execution paths
    • signaler failure still escalating to kill
    • avoiding flaky negative wall-clock assertions in cancellation timing tests
  • Focused post-merge CLI tests passed locally: ConsoleCancellationManagerTests, ProcessGuestLauncherTests, ProcessExecutionTests, and AppHostServerSessionTests — 45/45 passed.

Fixes #17638

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

On Windows, the TypeScript guest apphost runs under tsx, which swallows
Ctrl+Break. Our graceful-shutdown paths never succeed and the process is
eventually force-terminated without a chance to drain DCP-managed
resources.

This change introduces a unified termination ladder (cooperative cancel ->
graceful expire -> final drain -> force terminate) shared by every aspire
run child process, and routes guest-apphost shutdown through DCP
stop-process-tree on Windows instead of relying on Ctrl+Break propagation.

Key components:
- ConsoleCancellationManager: process-level signal handler with a
  three-stage escalation (Ctrl+C, second Ctrl+C, third Ctrl+C / drain
  timeout). RequestShutdown lets internal teardown drive the same ladder
  without consuming a user signal.
- GracefulShutdownService: shared graceful-window token, expired by the
  cancellation manager when the user's grace period elapses or escalates.
- IsolatedConsoleSpawner / IsolatedProcess: console-isolated child spawn
  used for aspire run, so Ctrl+C in the CLI does not propagate
  uncontrolled into the apphost system.
- WindowsConsoleProcessJob: Windows-only job-object safety net
  (KILL_ON_JOB_CLOSE + BREAKAWAY_OK so DCP can fork its own descendants).
- ProcessTerminator + ProcessLifetimeAdapter + ProcessPump: shared shutdown
  primitives consumed by AppHostServerSession and ProcessGuestLauncher.
- DetachedAppHostShutdownService (renamed from ProcessShutdownService) now
  implements IProcessTreeGracefulShutdownSignaler, the abstraction that
  AppHostServerSession + ProcessGuestLauncher call to ask DCP to drive
  graceful resource teardown.

aspire stop is left untouched - the new machinery is scoped to the run
path. Short-lived RPC sessions (publish, sdk generate, sdk dump,
scaffolding) keep the inherited-console default so Ctrl+C continues to
exit immediately.

Also fixed three issues caught in pre-PR review:
- ConsoleCancellationManager separated _ladderStarted (CAS gate) from
  _userSignalCount so internal RequestShutdown no longer makes the user's
  first Ctrl+C collapse the graceful window.
- GuestAppHostProject backchannel-fault continuation now uses
  CompareExchange on runEnded to race safely with the finally block.
- ProcessTerminator.ShutdownAsync(int pid, ...) is now async so the
  underlying Process handle outlives the inner WaitForExitAsync.

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

github-actions Bot commented Jun 2, 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 -- 17814

Or

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

The original PR fixed graceful shutdown for the TypeScript guest path. Mac smoke
testing surfaced that pure .NET AppHosts (the direct-launch path through
DotNetAppHostProject.RunAsync -> ProcessExecutionFactory -> ProcessExecution)
bypassed the new ladder entirely because their ProcessInvocationOptions don't
opt into it. On Unix this collapsed graceful shutdown to microseconds (the OCE
catch in ProcessExecution.WaitForExitAsync passed the already-cancelled token
as the graceful budget to ProcessTerminator); on Windows graceful was skipped
outright.

Changes:

- Extract the four-phase ladder into a shared ProcessGracefulShutdownLadder so
  AppHostServerSession, ProcessGuestLauncher, and the new IsolatedProcessExecution
  share the same escalation shape.
- Extend ProcessInvocationOptions with four opt-in fields (IsolateConsole,
  ConsoleProcessJob, GracefulShutdownSignaler, ShutdownService). All default
  null/false so the 11 non-Run callers (build, restore, package add, layout,
  etc.) preserve their existing ProcessTerminator force-terminate behavior.
- Add IsolatedProcessExecution to host an IsolatedProcess inside the existing
  IProcessExecution shape so the direct-launch path can opt into console
  isolation on Windows.
- Branch ProcessExecutionFactory on IsolateConsole. The Windows-only console
  isolation requires WindowsConsoleProcessJob to be wired through; fail fast
  if it isn't (defense-in-depth against the silent-fallthrough footgun the
  isolated path is meant to prevent).
- Make DotNetAppHostProject's ctor require GracefulShutdownService and
  IProcessTreeGracefulShutdownSignaler. ConsoleProcessJob stays optional
  (Windows-only DI registration). Only the run-path runOptions populate the
  four new fields; build/restore/publish callsites leave them unset.
- Forward the four new fields through DotNetCliRunner.CreateInstrumentedProcessOptions.
  Without this, the wiring at DotNetAppHostProject.RunAsync line 318 silently
  reverts when piped through CLI instrumentation -- the smoke run on Mac
  initially appeared green only because DCP did the work the CLI ladder
  thought it was doing.

Verified on macOS via a throwaway smoke harness:
- Single SIGINT against a graceful-respecting AppHost: clean exit in <1s.
- Single SIGINT against an AppHost stalling 15s in ApplicationStopping:
  process-tree escalation fires at exactly T+5s (matches the configured
  graceful budget), all descendants gone, exit 0.
- Double SIGINT (1s apart) against the same stalled AppHost: graceful window
  collapses to T+1s, AppHost forcibly terminated, exit 0. Documented tradeoff:
  an abrupt termination leaves DCP unable to SIGKILL its TERM-ignoring
  children, so the slow-script orphan in this scenario matches existing
  semantics.

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

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

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.
GitHub was asked to rerun all failed jobs for that attempt, and the rerun is being tracked in the rerun attempt.
The job links below point to the failed attempt jobs that matched the retry-safe transient failure rules.

The isolated Windows spawn path uses CreateProcessW + Process.GetProcessById
to obtain a managed Process for the child. Process objects derived from
GetProcessById cannot reliably surface ExitCode on Windows — the BCL state
machine depends on the Process instance itself having started the child,
and throws InvalidOperationException("Process was not started by this
object") otherwise (see dotnet/runtime#45003).

This silently broke 'aspire run' on Windows on every code path that reads
the AppHost server / guest exit code: AppHostServerSession.Exited handler,
AppHostServerSession's post-Run HasExited probe, the backchannel
SocketException filter in GuestAppHostProject, IsolatedProcessExecution,
and ProcessGuestLauncher's telemetry / return value.

Fix it by holding the original CreateProcess HANDLE in a SafeProcessHandle
and exposing it via overrides on IsolatedProcess.ExitCode / HasExited that
call GetExitCodeProcess + WaitForSingleObject(0) directly. The
WaitForSingleObject step disambiguates the 'process exited with code 259'
case from the 'still running' STILL_ACTIVE sentinel.

Plumb the overrides through:
- AppHostServerRunResult gains ExitCodeOverride / HasExitedOverride
  callbacks plus ReadExitCode() / ReadHasExited() accessors. Non-isolated
  callers leave the overrides null and the accessors fall back to
  Process.ExitCode / Process.HasExited, which work correctly for processes
  the BCL itself started.
- DotNetBasedAppHostServerProject and PrebuiltAppHostServer's isolated
  branches wire the overrides to the IsolatedProcess wrapper.
- AppHostServerSession captures the readers into private fields and routes
  every status check through them. New TryGetServerExitCode() / existing
  HasServerExited accessors are the recommended public API.
- ProcessGuestLauncher captures readExitCode / readHasExited locals on both
  the isolated and inherited branches and uses them for telemetry +
  return-value reads.
- GuestAppHostProject's StartBackchannelConnectionAsync now takes an
  AppHostServerSession and uses HasServerExited / TryGetServerExitCode in
  the SocketException filter instead of the raw Process.

The failing IsolatedProcessTests.Start_EchoesLine test that originally
exposed this on Windows CI now passes via the IsolatedProcess override
path; production paths that previously consumed Process.ExitCode directly
on the isolated spawn now go through the wrapper-backed readers.

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

JamesNK commented Jun 2, 2026

Copy link
Copy Markdown
Member

Three stages of Ctrl+C seems like overkill.

You're going to get some merge conflicts from #17652

DetachedProcessLauncher.StartWindows points both Stdout and Stderr at the
same NUL handle (child writes go nowhere). When the unified
WindowsProcessInterop.SpawnConsoleIsolatedProcess started building the
PROC_THREAD_ATTRIBUTE_HANDLE_LIST from the (Stdin, Stdout, Stderr) slots,
that NUL handle appeared twice. PROC_THREAD_ATTRIBUTE_HANDLE_LIST does not
allow duplicate handle entries — CreateProcessW returns
ERROR_INVALID_PARAMETER (87), which surfaced as 'aspire start' failing
immediately on Windows with exit code 2 in the dogfood starter validation
job. See https://devblogs.microsoft.com/oldnewthing/20111216-00/?p=8873 for
the documented Win32 constraint.

The old standalone DetachedProcessLauncher.StartWindows whitelisted only
the single NUL handle (one entry), so this regressed when the launcher
was switched over to the unified helper.

Dedupe by value before populating the attribute list, and add a Windows-
only regression test that spawns a detached process via the production
DetachedProcessLauncher path.

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

danegsta commented Jun 2, 2026

Copy link
Copy Markdown
Member Author

Three stages of Ctrl+C seems like overkill.

You're going to get some merge conflicts from #17652

Ah, for some reason I thought that had already been merged; I was playing around with the idea of a second cancellation token to communicate the graceful shutdown timeout, but it's not strictly necessary since we still have disposable process closure, so I'll likely back that part out. Goal is to have it work with your two stage model, but actually make our Windows child processes able to respond to Ctrl+C as well.

…e-driven cleanup

Internal failure paths in GuestAppHostProject.RunAsync (backchannel fault,
dependency install failure, guest non-zero exit, normal completion) no longer
route through ConsoleCancellationManager. They return their exit code directly
and rely on the existing 'await using' disposables on the AppHost session and
guest launcher to force-kill children as the run scope unwinds. DCP runs in a
separate process tree and handles its own resource cleanup.

The dedicated mutator API (CCM.RequestShutdown / CCM.RequestedExitCode) and
the ladder-started/user-signal-count split it required are removed. Cancel is
now a single 3-stage signal counter (cancel + watcher, expire graceful, force
exit) driven exclusively by user signals from PosixSignalRegistration /
Console.CancelKeyPress. The backchannel-fault continuation cancels a local CTS
linked to the outer cancellation token, surfacing FailedToDotnetRunAppHost via
a local 'internalFaultCode' captured by the outer OCE catch.

RunCommand and Program.Main OCE catches now unconditionally map cancellation
to CliExitCodes.Cancelled / Success — the only thing that reaches them is
user-initiated cancellation, since internal failures return their exit code
directly without throwing OCE.

Net: -193 lines / +88 lines, with one new regression test removed (the four
tests covering RequestShutdown semantics are gone; the FirstSignal test drops
its RequestedExitCode assertions). Behavior preserved for user Ctrl+C: still
gets the full 5s graceful + 5s drain ladder.

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

# Conflicts:
#	src/Aspire.Cli/Commands/RunCommand.cs
#	src/Aspire.Cli/ConsoleCancellationManager.cs
#	src/Aspire.Cli/Projects/IAppHostServerSession.cs
@danegsta danegsta mentioned this pull request Jun 8, 2026
14 tasks
danegsta added 2 commits June 8, 2026 12:48
…shutdown

# Conflicts:
#	src/Aspire.Cli/Projects/GuestAppHostProject.cs
#	src/Aspire.Cli/Projects/GuestRuntime.cs
#	tests/Aspire.Cli.Tests/CliBootstrapTests.cs
#	tests/Aspire.Cli.Tests/Telemetry/TelemetryConfigurationTests.cs
…shutdown

# Conflicts:
#	src/Aspire.Cli/Projects/AppHostServerSession.cs
#	src/Aspire.Cli/Projects/GuestAppHostProject.cs
#	tests/Aspire.Cli.Tests/Projects/AppHostServerSessionTests.cs
Copilot AI review requested due to automatic review settings June 10, 2026 23:32

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 introduces a unified graceful shutdown ladder for aspire run child processes on Windows (and strengthens Unix behavior). Previously, Ctrl+C propagation relied on OS-level mechanisms like Ctrl+Break, which TypeScript guests under tsx swallow, leading to force-kills without proper resource drain. The new architecture replaces ad-hoc per-component teardown with a shared four-phase escalation: cooperative cancel → graceful signal (via DCP stop-process-tree) → bounded wait → force tree-kill → drain.

Changes:

  • New shutdown infrastructure: GracefulShutdownService (central CTS facade), ProcessGracefulShutdownLadder (four-phase static helper), IProcessTreeGracefulShutdownSignaler interface, ConsoleCancellationManager refactored to three-stage user-signal ladder with configurable graceful budget.
  • Console-isolated child spawn: IsolatedProcess (cross-platform, CREATE_NEW_CONSOLE + anonymous pipes on Windows), IsolatedConsoleSpawner, WindowsConsoleProcessJob (kill-on-close + breakaway job object), IsolatedProcessExecution (IProcessExecution wrapper for the direct-launch path). Shared P/Invoke declarations consolidated in WindowsProcessInterop.
  • Session and launcher refactors: AppHostServerSession replaced static factory with constructor+StartAsync() pattern (deleting IAppHostServerSession/IAppHostServerSessionFactory interfaces); ProcessGuestLauncher and GuestAppHostProject wired through GuestLaunchOptions; DotNetAppHostProject extended with optional shutdown fields for the direct-launch path.

Note: The PR description references design identifiers (RequestShutdown, _ladderStarted, _userSignalCount, RequestShutdown_ThenSingleUserCancel_DoesNotCollapseGracefulWindow) that don't appear in the final code. The implementation evolved after the description was written.

Reviewed changes

Copilot reviewed 55 out of 55 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/Aspire.Cli/ConsoleCancellationManager.cs Refactored to three-stage user signal ladder with GracefulShutdownService dependency and configurable graceful budget
src/Aspire.Cli/GracefulShutdownService.cs New — thin CTS facade for the central graceful-shutdown token
src/Aspire.Cli/Processes/ProcessGracefulShutdownLadder.cs New — shared four-phase shutdown escalation (signal → wait → kill → drain)
src/Aspire.Cli/Processes/IProcessTreeGracefulShutdownSignaler.cs New — interface for graceful process-tree shutdown signaling
src/Aspire.Cli/Processes/DetachedAppHostShutdownService.cs Renamed from ProcessShutdownService, implements IProcessTreeGracefulShutdownSignaler
src/Aspire.Cli/Processes/WindowsProcessInterop.cs New — consolidated P/Invoke declarations, BuildCommandLine, BuildEnvironmentBlock, SpawnConsoleIsolatedProcess
src/Aspire.Cli/Processes/WindowsConsoleProcessJob.cs New — Windows job object wrapper (KILL_ON_JOB_CLOSE + BREAKAWAY_OK)
src/Aspire.Cli/Processes/IsolatedProcess.cs New — cross-platform spawn abstraction with pumps and exit-code overrides
src/Aspire.Cli/Processes/IsolatedProcess.Windows.cs New — Windows CreateProcessW with CREATE_NEW_CONSOLE + anonymous pipes
src/Aspire.Cli/Processes/IsolatedProcess.Unix.cs New — Unix thin Process.Start wrapper
src/Aspire.Cli/Processes/IsolatedConsoleSpawner.cs New — ProcessStartInfoIsolatedProcessStartInfo translation
src/Aspire.Cli/Processes/ProcessTerminator.cs Made ShutdownAsync(int pid, ...) properly async; shared termination primitives
src/Aspire.Cli/Processes/ProcessPump.cs New — line-by-line pipe pump with exception recording
src/Aspire.Cli/Processes/ProcessLifetimeAdapter.cs New — IAsyncDisposable wrapper for Process
src/Aspire.Cli/Processes/DetachedProcessLauncher.Windows.cs Refactored to use shared WindowsProcessInterop
src/Aspire.Cli/Projects/ProcessGuestLauncher.cs Major refactor: two spawn paths (isolated/inherited), GuestLaunchOptions, shared ladder
src/Aspire.Cli/Projects/IGuestProcessLauncher.cs New GuestLaunchOptions record with optional shutdown services
src/Aspire.Cli/Projects/AppHostServerSession.cs Refactored: constructor + StartAsync() replacing static factory; owns process lifetime
src/Aspire.Cli/Projects/IAppHostServerSession.cs Deleted — interface removed
src/Aspire.Cli/Projects/IAppHostServerProject.cs New AppHostServerRunResult record; Run method updated with isolation params
src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs Updated Run signature
src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs Updated Run signature
src/Aspire.Cli/Projects/DotNetAppHostProject.cs New optional shutdown fields; run path opts into ladder
src/Aspire.Cli/Projects/GuestAppHostProject.cs Wires shutdown services; CAS-guarded backchannel fault escalation
src/Aspire.Cli/Projects/GuestRuntime.cs Forwards GuestLaunchOptions
src/Aspire.Cli/Projects/ExtensionGuestLauncher.cs Ignores options (intentional — extension hosts have their own lifecycle)
src/Aspire.Cli/Program.cs DI wiring for new services; OCE catch in Main reads CCM exit code
src/Aspire.Cli/Commands/RunCommand.cs Configures 5s graceful budget at command entry
src/Aspire.Cli/DotNet/ProcessExecutionFactory.cs New isolated execution path via IsolatedProcessExecution
src/Aspire.Cli/DotNet/ProcessExecution.cs OCE catch routes to ProcessGracefulShutdownLadder when signaler+service present
src/Aspire.Cli/DotNet/IsolatedProcessExecution.cs New — IProcessExecution wrapper for IsolatedProcess
src/Aspire.Cli/DotNet/DotNetCliRunner.cs CreateInstrumentedProcessOptions forwards all 4 new opt-in fields
tests/Aspire.Cli.Tests/ConsoleCancellationManagerTests.cs Updated for three-stage ladder; new budget/drain tests
tests/Aspire.Cli.Tests/GracefulShutdownServiceTests.cs New — token/expire/dispose coverage
tests/Aspire.Cli.Tests/Projects/AppHostServerSessionTests.cs Significant new coverage for session lifecycle and graceful shutdown
tests/Aspire.Cli.Tests/Projects/ProcessGuestLauncherTests.cs New — four test scenarios: force-kill, graceful success, escalation, signaler fault
tests/Aspire.Cli.Tests/Projects/GuestAppHostProjectTests.cs Updated construction with shutdown services
tests/Aspire.Cli.Tests/Processes/IsolatedProcessTests.cs New — spawn, pump, exit-code, kill coverage
tests/Aspire.Cli.Tests/Processes/WindowsConsoleProcessJobTests.cs New — job object lifecycle tests
tests/Aspire.Cli.Tests/Processes/DetachedProcessLauncherTests.cs New — detached launch tests
tests/Aspire.Cli.Tests/Processes/DetachedAppHostShutdownServiceTests.cs Renamed from ProcessShutdownServiceTests
tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs DI registrations updated to mirror production wiring
tests/Aspire.Cli.Tests/TestServices/TestAppHostServerSessionFactory.cs Deleted — no longer needed after interface removal
tests/Aspire.Cli.Tests/TestServices/FakeFailingAppHostServerProject.cs Updated Run signature
tests/Aspire.Cli.Tests/Scaffolding/ChannelReseedTests.cs Updated Run signature in test double
tests/Aspire.Cli.Tests/Telemetry/TelemetryConfigurationTests.cs Updated CCM construction

Comment thread tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs Outdated
Comment thread src/Aspire.Cli/Commands/RunCommand.cs Outdated
danegsta and others added 2 commits June 10, 2026 16:40
The merge commit captured an unstaged version of GuestAppHostProject.cs
that still referenced main's old 'appHostServerProcess' variable and kept
a duplicate eager backchannel start. Adopt main's deferred backchannel
pattern for the publish path: remove the eager start and use serverSession
in the after-launch callback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 10, 2026 23:49
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

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 55 out of 55 changed files in this pull request and generated no new comments.

@github-actions

Copy link
Copy Markdown
Contributor

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.
GitHub was asked to rerun all failed jobs for that attempt, and the rerun is being tracked in the rerun attempt.
The job links below point to the failed attempt jobs that matched the retry-safe transient failure rules.

- IsolatedConsoleSpawner: replace overlay-without-clear with full Clear()+
  copy so callers that did startInfo.Environment.Remove(key) see the
  removal honored on the isolated path. Without this, a caller like
  PrebuiltAppHostServer.CreateStartInfo that defensively removes
  IntegrationLibsPath/IntegrationProbeManifestPath would silently re-
  inherit those vars from the parent CLI's env block. Matches the Unix
  partial's Environment.Clear()+add semantics.

- AppHostServerSession.StartAsync: move the failed-lifetime DisposeAsync
  out of the lock(_startGate) block. The catch path was running a sync-
  over-async disposal while holding the start gate, which pinned the gate
  for whatever IsolatedProcess.DisposeAsync took (pipe pump drain etc.)
  and queued any concurrent DisposeAsync behind that wait. Capture the
  failed lifetime and dispose after lock release; rethrow via
  ExceptionDispatchInfo.

- ProcessGracefulShutdownLadder: drop the TryGetStartTime() helper. The
  sole call site hardcodes includeStartTimeForDcp: false, so the value
  was always computed and discarded.

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

Copy link
Copy Markdown
Contributor

CLI E2E Tests unknown — 113 passed, 0 failed, 2 unknown (commit 4d3e8bc)

View all recordings
- Test Detail
AddPackageInteractiveWhileAppHostRunningDetached Recording · Job · CLI logs
AddPackageWhileAppHostRunningDetached Recording · Job · CLI logs
AgentCommands_AllHelpOutputs_AreCorrect Recording · Job · CLI logs
AgentInitCommand_DefaultSelection_InstallsDefaultSkills Recording · Job · CLI logs
AgentInitCommand_MigratesDeprecatedConfig Recording · Job · CLI logs
AgentInit_NonInteractive_BundleOnlySkillsNotInCatalog Recording · Job · CLI logs
AgentMcpListStructuredLogsReturnsLogsFromStarterApp Recording · Job · CLI logs
AgentMcpListStructuredLogsReturnsLogsFromStarterApp_DevLocalhost Recording · Job · CLI logs
AgentMcpListStructuredLogsReturnsLogsFromStarterApp_Isolated Recording · Job · CLI logs
AllPublishMethodsBuildDockerImages Recording · Job · CLI logs
AspireAddAndStartWorkAgainstLegacyAppHostTs Recording · Job · CLI logs
AspireAddPackageVersionToDirectoryPackagesProps Recording · Job · CLI logs
AspireInitSingleFileAppHostRunsViaDotnetRunAppHost Recording · Job · CLI logs
AspireInit_ExistingAppHostDir_RecreatesNuGetConfigKeepsFiles Recording · Job · CLI logs
AspireInit_SolutionFile_BuildsAgainstChannelHive Recording · Job · CLI logs
AspireStartUpdatesStaleTypeScriptAppHostPath Recording · Job · CLI logs
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps Recording · Job · CLI logs
AspireUpdateRemovesOrphanAppHostPackageVersionWhenSdkAlreadyCurrent Recording · Job · CLI logs
Banner_DisplayedOnFirstRun Recording · Job · CLI logs
Banner_DisplayedWithExplicitFlag Recording · Job · CLI logs
Banner_NotDisplayedWithNoLogoFlag Recording · Job · CLI logs
CertificatesClean_RemovesCertificates Recording · Job · CLI logs
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate Recording · Job · CLI logs
CertificatesTrust_WithUntrustedCert_TrustsCertificate Recording · Job · CLI logs
ConfigSetGet_CreatesNestedJsonFormat Recording · Job · CLI logs
CreateAndRunAspireStarterProject Recording · Job · CLI logs
CreateAndRunAspireStarterProjectWithBundle Recording · Job · CLI logs
CreateAndRunEmptyAppHostProject Recording · Job · CLI logs
CreateAndRunJavaEmptyAppHostProject Recording · Job · CLI logs
CreateAndRunJsReactProject Recording · Job · CLI logs
CreateAndRunPolyglotAppHostWithDevLocalhostUrls Recording · Job · CLI logs
CreateAndRunPythonReactProject Recording · Job · CLI logs
CreateAndRunTypeScriptEmptyAppHostProject Recording · Job · CLI logs
CreateAndRunTypeScriptStarterProject Recording · Job · CLI logs
CreateJavaAppHostWithViteApp Recording · Job · CLI logs
CreateTypeScriptAppHostWithViteApp_UsesConfiguredToolchain Recording · Job · CLI logs
DashboardRunWithAgentMcpListTracesReturnsNoTraces Recording · Job · CLI logs
DashboardRunWithAgentMcpListTracesReturnsNoTraces_DevLocalhost Recording · Job · CLI logs
DashboardRunWithOtelTracesReturnsNoTraces Recording · Job · CLI logs
DashboardRunWithOtelTracesReturnsNoTraces_DevLocalhost Recording · Job · CLI logs
DeployK8sBasicApiService Recording · Job · CLI logs
DeployK8sWithExternalHelmChart Recording · Job · CLI logs
DeployK8sWithGarnet Recording · Job · CLI logs
DeployK8sWithMongoDB Recording · Job · CLI logs
DeployK8sWithMySql Recording · Job · CLI logs
DeployK8sWithPostgres Recording · Job · CLI logs
DeployK8sWithRabbitMQ Recording · Job · CLI logs
DeployK8sWithRedis Recording · Job · CLI logs
DeployK8sWithSqlServer Recording · Job · CLI logs
DeployK8sWithValkey Recording · Job · CLI logs
DeployTypeScriptAppToKubernetes Recording · Job · CLI logs
DescribeCommandResolvesReplicaNames Recording · Job · CLI logs
DescribeCommandShowsRunningResources Recording · Job · CLI logs
DetachFormatJsonProducesValidJson Recording · Job · CLI logs
DetachFormatJsonProducesValidJsonWhenRestartingExistingInstance Recording · Job · CLI logs
DoPublishAndDeployListStepsWork Recording · Job · CLI logs
DocsCommand_RendersInteractiveMarkdownFromLocalSource Recording · Job · CLI logs
DoctorCommand_DetectsDeprecatedAgentConfig Recording · Job · CLI logs
DoctorCommand_TypeScriptAppHostReportsMissingConfiguredToolchain Recording · Job · CLI logs
DoctorCommand_WithSslCertDir_ShowsTrusted Recording · Job · CLI logs
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted Recording · Job · CLI logs
DotNetRunFileBasedAppHostUsesAspireCliBundle Recording · Job · CLI logs
DotNetRunProjectAppHostUsesAspireCliBundle Recording · Job · CLI logs
GatewayWithoutExternalEndpoint_FailsPublishWithGuidance Recording · Job · CLI logs
GeneratedAspireDevScript_StartsWatchMode_WithConfiguredToolchain Recording · Job · CLI logs
GlobalMigration_HandlesCommentsAndTrailingCommas Recording · Job · CLI logs
GlobalMigration_HandlesMalformedLegacyJson Recording · Job · CLI logs
GlobalMigration_PreservesAllValueTypes Recording · Job · CLI logs
GlobalMigration_SkipsWhenNewConfigExists Recording · Job · CLI logs
GlobalSettings_MigratedFromLegacyFormat Recording · Job · CLI logs
IngressWithoutExternalEndpoint_FailsPublishWithGuidance Recording · Job · CLI logs
InitTypeScriptAppHost_AugmentsExistingViteRepoInWorkspaceSubdirectory Recording · Job · CLI logs
InteractiveCSharpInitCreatesExpectedFiles Recording · Job · CLI logs
InvalidAppHostPathWithComments_IsHealedOnRun Recording · Job · CLI logs
JavaScriptHostingApisRunFromTypeScriptAppHost Recording · Job · CLI logs
LatestCliCanStartStableChannelAppHost Recording · Job · CLI logs
LatestCliCanStartStableChannelTypeScriptAppHost Recording · Job · CLI logs
LegacySettingsMigration_AdjustsRelativeAppHostPath Recording · Job · CLI logs
LogsCommandShowsResourceLogs Recording · Job · CLI logs
OtelLogsReturnsStructuredLogsFromStarterApp Recording · Job · CLI logs
OtelLogsReturnsStructuredLogsFromStarterAppIsolated Recording · Job · CLI logs
ProcessCommandCallbackReceivesCliArguments Recording · Job · CLI logs
PsCommandListsRunningAppHost Recording · Job · CLI logs
PsFormatJsonOutputsOnlyJsonToStdout Recording · Job · CLI logs
PublishJavaScriptPatternsGeneratesExpectedDockerComposeArtifacts Recording · Job · CLI logs
PublishWithConfigureEnvFileUpdatesEnvOutput Recording · Job · CLI logs
PublishWithDockerComposeServiceCallbackSucceeds Recording · Job · CLI logs
PublishWithoutOutputPathUsesAppHostDirectoryDefault Recording · Job · CLI logs
ResourceCommand_FailedExec_ShowsLogPathAndLogHasEntries Recording · Job · CLI logs
ResourceCommand_SetAndDeleteParameterUpdatesDescribeOutput Recording · Job · CLI logs
RestoreGeneratesSdkFiles Recording · Job · CLI logs
RestoreGeneratesSdkFiles_WithConfiguredToolchain Recording · Job · CLI logs
RestoreRefreshesGeneratedSdkAfterAddingIntegration Recording · Job · CLI logs
RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes Recording · Job · CLI logs
RunFromParentDirectory_UsesExistingConfigNearAppHost Recording · Job · CLI logs
RunReportsSyntaxErrorsForDotNetAppHost Recording · Job · CLI logs
RunReportsSyntaxErrorsForTypeScriptAppHost Recording · Job · CLI logs
SecretCrudOnDotNetAppHost Recording · Job · CLI logs
SecretCrudOnTypeScriptAppHost Recording · Job · CLI logs
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels Recording · Job · CLI logs
StartAndWaitForTypeScriptSqlServerAppHostWithNativeAssets Recording · Job · CLI logs
StartReportsSyntaxErrorsForDotNetAppHost Recording · Job · CLI logs
StartReportsSyntaxErrorsForTypeScriptAppHost Recording · Job · CLI logs
StopAllAppHostsFromAppHostDirectory Recording · Job · CLI logs
StopJavaPolyglotAppHostUsingApphostDirectory Recording · Job · CLI logs
StopNonInteractiveSingleAppHost Recording · Job · CLI logs
StopTypeScriptPolyglotAppHostUsingApphostDirectory Recording · Job · CLI logs
StopWithNoRunningAppHostExitsSuccessfully Recording · Job · CLI logs
TypeScriptAppHostRunDoesNotDeadlockWhenLazyOptionsInvokeAsyncCallback Recording · Job · CLI logs
TypeScriptAppHostWithVite_AllowsDifferentGuestPkgManager Recording · Job · CLI logs
UnAwaitedChainsCompileWithAutoResolvePromises Recording · Job · CLI logs
UpdateToStable_CSharpEmptyAppHost_KeepsConfigChannel Recording · Job · CLI logs
UpdateToStable_CSharpSingleFileInit_KeepsConfigChannel Recording · Job · CLI logs
UpdateToStable_TypeScriptSingleFileInit_KeepsConfigChannel Recording · Job · CLI logs
UpdateToStable_TypeScript_PreviewsStablePkgsAndKeepsChannel Recording · Job · CLI logs

📹 Recordings uploaded automatically from CI run #27316663922

danegsta added 2 commits June 17, 2026 11:57
…shutdown

# Conflicts:
#	src/Aspire.Cli/Projects/DotNetAppHostProject.cs
#	src/Aspire.Cli/Projects/GuestAppHostProject.cs
#	src/Aspire.Cli/Projects/IAppHostServerSession.cs
#	tests/Aspire.Cli.Tests/Projects/AppHostServerSessionTests.cs
#	tests/Aspire.Cli.Tests/TestServices/TestAppHostServerSessionFactory.cs
Copilot AI review requested due to automatic review settings June 17, 2026 19:02
Copilot AI review requested due to automatic review settings June 23, 2026 18:36

@adamint adamint left a comment

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.

(automated review)

I think there are a few shutdown edge cases that need to be fixed before this can merge:

  1. StartBackchannelConnectionAsync catches OperationCanceledException from ConnectAsync / Task.Delay when Ctrl+C cancels appHostSystemToken, then faults the backchannel TCS. That continuation sets internalFaultCode = FailedToDotnetRunAppHost, so the outer cancellation path returns a run failure instead of the normal cancelled exit code. Could you handle cancellation separately and avoid turning user cancellation into a backchannel failure?

  2. ProcessTreeGracefulShutdownService.ForceKillRemainingProcesses uses killEntireProcessTree = !OperatingSystem.IsWindows(). If aspire stop's graceful DCP stop fails or times out on Windows, the fallback only kills the root AppHost/CLI process and can leave descendants like DCP/tsx/node running. Once graceful shutdown has failed, I think the escalation should tree-kill on Windows too.

  3. AppHostServerSession.DisposeAsync disposes _startGate, but a second dispose calls _startGate.WaitAsync() before checking _disposed, which will throw after the first disposal. Can we make the idempotent check happen before touching the semaphore, or just not dispose the semaphore?

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 69 out of 69 changed files in this pull request and generated no new comments.

danegsta and others added 2 commits June 23, 2026 11:55
DisposeAsync is not invoked by the finalizer, so update the stale 'let the GC
handle cleanup' note to explain that the Process native handles are reclaimed
by SafeHandle finalizers instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ProcessExecutionFactory's replace path called Environment.Clear(), which lazily
seeded ~50-100 parent env entries only to discard them immediately. Add
IsolatedProcessStartInfo.UseEmptyEnvironment() to initialize the env block empty
(skipping the parent snapshot) and mark HasCustomEnvironment, and route the
replace path through it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 23, 2026 19:05
Replace 'if (Debugger.IsAttached) return;' early exits with Assert.Skip so the
tests surface as skipped (not silently passed) when their CancelAfter-based
timing is disabled under a debugger.

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

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 69 out of 69 changed files in this pull request and generated no new comments.

The run and publish paths each slept a fixed 500ms after starting the
AppHost server, then checked serverCompletion.IsCompleted to detect an
early crash. This was both wasteful (every healthy startup paid the full
delay) and fragile (a slow-spawning server could still race the check).

The delay+precheck is redundant: GetRpcClientAsync already races the RPC
connect against the server-exit signal and surfaces an early crash
immediately, and the surrounding catch blocks display the same output +
"App host exited unexpectedly." error and return the same exit code. On
the publish path the catch additionally faults the backchannel waiter,
which the precheck never did, so relying on it is strictly better.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 23, 2026 19:16

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 69 out of 69 changed files in this pull request and generated no new comments.

danegsta and others added 4 commits June 23, 2026 12:31
The ProcessGuestLauncher constructor previously defaulted fileLoggerProvider,
commandResolver, and processExecutionFactory purely to avoid updating call
sites. Make them required and push the defaults (the NullLogger-backed
execution factory and the PATH command resolver) to the callers that actually
own those choices.

Likewise, the GuestLaunchOptions argument added to IGuestProcessLauncher.LaunchAsync
in this PR (and the pre-existing afterLaunchAsync) were optional only for
call-site brevity. Make them non-optional and move cancellationToken to the
end, matching the convention used elsewhere in the PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Audit pass over arguments added in this PR: make a new optional param
required when no caller relies on its default.

- IAppHostServerProject.RunAsync: env/additionalArgs/debug/runControl all
  required (sole caller passes them); update doc + 2 impls + 2 fakes.
- GuestAppHostProject.ExecuteGuestAppHostAsync: appHostLaunchOptions
  required and cancellationToken moved last (sole caller always passes it).
- IsolatedProcess.Kill(bool): drop the default to match the non-optional
  sibling ProcessExecution.Kill(bool) (zero callers relied on it).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…spose idempotency

Address three review findings:

- GuestAppHostProject: Ctrl+C during backchannel connect faulted the
  completion source, tripping the IsFaulted continuation that sets
  FailedToDotnetRunAppHost. Guard cancellation so user cancellation
  surfaces as Cancelled (130) instead of a run failure.
- ProcessTreeGracefulShutdownService: the force-kill fallback only tree-
  killed on Unix. Once graceful shutdown has failed/timed out, tree-kill
  on Windows too so orphaned descendants (DCP, tsx/node, guest) are not
  left running. The success-path mop-up still avoids tree-kill.
- AppHostServerSession: DisposeAsync disposed _startGate, so a second
  DisposeAsync threw ObjectDisposedException from WaitAsync before the
  idempotency guard could observe _disposed. Stop disposing the semaphore
  (AvailableWaitHandle is never used).

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

# Conflicts:
#	src/Aspire.Cli/Commands/BaseCommand.cs
#	src/Aspire.Cli/Projects/GuestAppHostProject.cs
#	src/Aspire.Cli/Projects/GuestRuntime.cs
#	src/Aspire.Cli/Projects/ProcessGuestLauncher.cs
#	tests/Aspire.Cli.Tests/Projects/GuestAppHostProjectTests.cs
#	tests/Aspire.Cli.Tests/Projects/GuestRuntimeTests.cs
#	tests/Aspire.Cli.Tests/Scaffolding/ChannelReseedTests.cs
#	tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs
@github-actions

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.

42 / 97 test projects · 5 jobs, from 72 changed files.

Selected test projects (42 / 97)

Aspire.Cli.EndToEnd.Tests, Aspire.Cli.Tests, Aspire.Dashboard.Components.Tests, Aspire.Dashboard.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.Containers.Tests, Aspire.Hosting.DevTunnels.Tests, Aspire.Hosting.Docker.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.PostgreSQL.Tests, Aspire.Hosting.Python.Tests, Aspire.Hosting.Qdrant.Tests, Aspire.Hosting.RabbitMQ.Tests, Aspire.Hosting.Redis.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.Playground.Tests, Aspire.TerminalHost.Tests

Selected jobs (5)

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


How these were chosen — grouped by what changed

⚠️ 40 of the 42 selected test projects come from a single change — tests/Shared/AsyncTestHelpers.cs.

🧪 tests/Shared/AsyncTestHelpers.cs (changed test)
40 via the project graph

show 40

Aspire.Dashboard.Components.Tests, Aspire.Dashboard.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.Containers.Tests, Aspire.Hosting.DevTunnels.Tests, Aspire.Hosting.Docker.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.PostgreSQL.Tests, Aspire.Hosting.Python.Tests, Aspire.Hosting.Qdrant.Tests, Aspire.Hosting.RabbitMQ.Tests, Aspire.Hosting.Redis.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.Playground.Tests, Aspire.TerminalHost.Tests

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

🧪 tests/Aspire.Cli.Tests/CliBootstrapTests.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/DashboardRunCommandTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Configuration/DotNetBasedAppHostServerChannelResolutionTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Configuration/PrebuiltAppHostServerChannelResolutionTests.cs (changed test)
1 directly: Aspire.Cli.Tests

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

🧪 tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs (changed test)
1 directly: Aspire.Cli.Tests

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

🧪 tests/Aspire.Cli.Tests/Processes/ProcessShutdownServiceTests.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/Projects/AppHostServerProjectTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Projects/AppHostServerSessionTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Projects/DotNetAppHostProjectTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Projects/GuestAppHostProjectTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Projects/GuestRuntimeTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Projects/ProcessGuestLauncherTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Scaffolding/ChannelReseedTests.cs (changed test)
1 directly: Aspire.Cli.Tests

🧪 tests/Aspire.Cli.Tests/Telemetry/TelemetryConfigurationTests.cs (changed test)
1 directly: Aspire.Cli.Tests

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

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

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

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

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

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

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

🧪 tests/Aspire.Cli.Tests/TestServices/TestProcessExecutionFactory.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 • affected project Aspire.Cli
• selected test Aspire.Cli.Tests
deployment-e2e affected project Aspire.Cli
extension-e2e src/Aspire.Cli/Commands/AppHostLauncher.cs, src/Aspire.Cli/Commands/BaseCommand.cs, src/Aspire.Cli/Commands/DashboardRunCommand.cs, src/Aspire.Cli/Commands/RunCommand.cs, src/Aspire.Cli/Commands/Sdk/SdkDumpCommand.cs, src/Aspire.Cli/Commands/Sdk/SdkGenerateCommand.cs, src/Aspire.Cli/Commands/StopCommand.cs, src/Aspire.Cli/ConsoleCancellationManager.cs, src/Aspire.Cli/DotNet/DotNetCliRunner.cs, src/Aspire.Cli/DotNet/IProcessExecution.cs, src/Aspire.Cli/DotNet/IProcessExecutionFactory.cs, src/Aspire.Cli/DotNet/ProcessExecution.cs, src/Aspire.Cli/DotNet/ProcessExecutionFactory.cs, src/Aspire.Cli/Layout/LayoutProcessRunner.cs, src/Aspire.Cli/Processes/DetachedProcessLauncher.Windows.cs, src/Aspire.Cli/Processes/IProcessTreeGracefulShutdownSignaler.cs, src/Aspire.Cli/Processes/IsolatedProcess.Windows.cs, src/Aspire.Cli/Processes/IsolatedProcess.cs, src/Aspire.Cli/Processes/ProcessPump.cs, src/Aspire.Cli/Processes/ProcessShutdownService.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/AppHostServerProject.cs, src/Aspire.Cli/Projects/AppHostServerSession.cs, src/Aspire.Cli/Projects/DotNetAppHostProject.cs, src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs, src/Aspire.Cli/Projects/ExtensionGuestLauncher.cs, src/Aspire.Cli/Projects/GuestAppHostProject.cs, src/Aspire.Cli/Projects/GuestRuntime.cs, src/Aspire.Cli/Projects/IAppHostServerProject.cs, src/Aspire.Cli/Projects/IAppHostServerSession.cs, src/Aspire.Cli/Projects/IGuestProcessLauncher.cs, src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs, src/Aspire.Cli/Projects/ProcessGuestLauncher.cs, src/Aspire.Cli/Scaffolding/ScaffoldingService.cs, src/Aspire.Cli/Utils/EnvironmentChecker/DcpConnectionChecker.cs, tests/Aspire.Cli.Tests/CliBootstrapTests.cs, tests/Aspire.Cli.Tests/Commands/AppHostLauncherTests.cs, tests/Aspire.Cli.Tests/Commands/DashboardRunCommandTests.cs, tests/Aspire.Cli.Tests/Configuration/DotNetBasedAppHostServerChannelResolutionTests.cs, tests/Aspire.Cli.Tests/Configuration/PrebuiltAppHostServerChannelResolutionTests.cs, tests/Aspire.Cli.Tests/ConsoleCancellationManagerTests.cs, tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs, tests/Aspire.Cli.Tests/DotNet/ProcessExecutionTests.cs, tests/Aspire.Cli.Tests/Processes/DetachedProcessLauncherTests.cs, tests/Aspire.Cli.Tests/Processes/IsolatedProcessTests.cs, tests/Aspire.Cli.Tests/Processes/ProcessShutdownServiceTests.cs, tests/Aspire.Cli.Tests/Processes/ProcessTreeGracefulShutdownServiceTests.cs, tests/Aspire.Cli.Tests/Processes/WindowsConsoleProcessJobTests.cs, tests/Aspire.Cli.Tests/Projects/AppHostServerProjectTests.cs, tests/Aspire.Cli.Tests/Projects/AppHostServerSessionTests.cs, tests/Aspire.Cli.Tests/Projects/DotNetAppHostProjectTests.cs, tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs, tests/Aspire.Cli.Tests/Projects/GuestAppHostProjectTests.cs, tests/Aspire.Cli.Tests/Projects/GuestRuntimeTests.cs, tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs, tests/Aspire.Cli.Tests/Projects/ProcessGuestLauncherTests.cs, tests/Aspire.Cli.Tests/Scaffolding/ChannelReseedTests.cs, tests/Aspire.Cli.Tests/Telemetry/TelemetryConfigurationTests.cs, tests/Aspire.Cli.Tests/TestServices/FakeAppHostServerSession.cs, tests/Aspire.Cli.Tests/TestServices/FakeFailingAppHostServerProject.cs, tests/Aspire.Cli.Tests/TestServices/FakeSucceedingAppHostServerProject.cs, tests/Aspire.Cli.Tests/TestServices/ProcessTestHelpers.cs, tests/Aspire.Cli.Tests/TestServices/RecordingGracefulSignaler.cs, tests/Aspire.Cli.Tests/TestServices/TestAppHostServerSessionFactory.cs, tests/Aspire.Cli.Tests/TestServices/TestGracefulShutdownWindow.cs, tests/Aspire.Cli.Tests/TestServices/TestProcessExecutionFactory.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 a221fa9.

@danegsta

Copy link
Copy Markdown
Member Author

(automated review)

I think there are a few shutdown edge cases that need to be fixed before this can merge:

  1. StartBackchannelConnectionAsync catches OperationCanceledException from ConnectAsync / Task.Delay when Ctrl+C cancels appHostSystemToken, then faults the backchannel TCS. That continuation sets internalFaultCode = FailedToDotnetRunAppHost, so the outer cancellation path returns a run failure instead of the normal cancelled exit code. Could you handle cancellation separately and avoid turning user cancellation into a backchannel failure?
  2. ProcessTreeGracefulShutdownService.ForceKillRemainingProcesses uses killEntireProcessTree = !OperatingSystem.IsWindows(). If aspire stop's graceful DCP stop fails or times out on Windows, the fallback only kills the root AppHost/CLI process and can leave descendants like DCP/tsx/node running. Once graceful shutdown has failed, I think the escalation should tree-kill on Windows too.
  3. AppHostServerSession.DisposeAsync disposes _startGate, but a second dispose calls _startGate.WaitAsync() before checking _disposed, which will throw after the first disposal. Can we make the idempotent check happen before touching the semaphore, or just not dispose the semaphore?

Pushed fixes for all these.

@danegsta danegsta requested a review from adamint June 23, 2026 20:35
@danegsta

Copy link
Copy Markdown
Member Author

PR Testing Report

PR Information

Artifact Version Verification

  • Expected Commit: a221fa9
  • Installed Version: 13.5.0-pr.17814.ga221fa94
  • Status: Verified - installed CLI version contains a221fa9
  • CLI Path Tested: C:\Users\danegsta\AppData\Local\Temp\aspire-pr-test-81dcda793a5c4dc98669d8495054181c\dogfood\pr-17814\bin\aspire.exe
  • Template Hive: C:\Users\danegsta\AppData\Local\Temp\aspire-pr-test-81dcda793a5c4dc98669d8495054181c\hives\pr-17814\packages

Changes Analyzed

Files Changed

  • src/Aspire.Cli/Commands/AppHostLauncher.cs
  • src/Aspire.Cli/Commands/BaseCommand.cs
  • src/Aspire.Cli/Commands/DashboardRunCommand.cs
  • src/Aspire.Cli/Commands/RunCommand.cs
  • src/Aspire.Cli/Commands/Sdk/SdkDumpCommand.cs
  • src/Aspire.Cli/Commands/Sdk/SdkGenerateCommand.cs
  • src/Aspire.Cli/Commands/StopCommand.cs
  • src/Aspire.Cli/ConsoleCancellationManager.cs
  • src/Aspire.Cli/DotNet/DotNetCliRunner.cs
  • src/Aspire.Cli/DotNet/IProcessExecution.cs
  • src/Aspire.Cli/DotNet/IProcessExecutionFactory.cs
  • src/Aspire.Cli/DotNet/ProcessExecution.cs
  • src/Aspire.Cli/DotNet/ProcessExecutionFactory.cs
  • src/Aspire.Cli/Layout/LayoutProcessRunner.cs
  • src/Aspire.Cli/Processes/DetachedProcessLauncher.Windows.cs
  • src/Aspire.Cli/Processes/IProcessTreeGracefulShutdownSignaler.cs
  • src/Aspire.Cli/Processes/IsolatedProcess.cs
  • src/Aspire.Cli/Processes/IsolatedProcess.Windows.cs
  • src/Aspire.Cli/Processes/ProcessPump.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/AppHostServerProject.cs
  • src/Aspire.Cli/Projects/AppHostServerSession.cs
  • src/Aspire.Cli/Projects/DotNetAppHostProject.cs
  • src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs
  • src/Aspire.Cli/Projects/ExtensionGuestLauncher.cs
  • src/Aspire.Cli/Projects/GuestAppHostProject.cs
  • src/Aspire.Cli/Projects/GuestRuntime.cs
  • src/Aspire.Cli/Projects/IAppHostServerProject.cs
  • src/Aspire.Cli/Projects/IAppHostServerSession.cs
  • src/Aspire.Cli/Projects/IGuestProcessLauncher.cs
  • src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs
  • src/Aspire.Cli/Projects/ProcessGuestLauncher.cs
  • src/Aspire.Cli/Scaffolding/ScaffoldingService.cs
  • src/Aspire.Cli/Utils/EnvironmentChecker/DcpConnectionChecker.cs
  • tests/Aspire.Cli.Tests/CliBootstrapTests.cs
  • tests/Aspire.Cli.Tests/Commands/AppHostLauncherTests.cs
  • tests/Aspire.Cli.Tests/Commands/DashboardRunCommandTests.cs
  • tests/Aspire.Cli.Tests/Configuration/DotNetBasedAppHostServerChannelResolutionTests.cs
  • tests/Aspire.Cli.Tests/Configuration/PrebuiltAppHostServerChannelResolutionTests.cs
  • tests/Aspire.Cli.Tests/ConsoleCancellationManagerTests.cs
  • tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs
  • tests/Aspire.Cli.Tests/DotNet/ProcessExecutionTests.cs
  • tests/Aspire.Cli.Tests/Processes/DetachedProcessLauncherTests.cs
  • tests/Aspire.Cli.Tests/Processes/IsolatedProcessTests.cs
  • tests/Aspire.Cli.Tests/Processes/ProcessTreeGracefulShutdownServiceTests.cs
  • tests/Aspire.Cli.Tests/Processes/WindowsConsoleProcessJobTests.cs
  • tests/Aspire.Cli.Tests/Projects/AppHostServerProjectTests.cs
  • tests/Aspire.Cli.Tests/Projects/AppHostServerSessionTests.cs
  • tests/Aspire.Cli.Tests/Projects/DotNetAppHostProjectTests.cs
  • tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs
  • tests/Aspire.Cli.Tests/Projects/GuestAppHostProjectTests.cs
  • tests/Aspire.Cli.Tests/Projects/GuestRuntimeTests.cs
  • tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs
  • tests/Aspire.Cli.Tests/Projects/ProcessGuestLauncherTests.cs
  • tests/Aspire.Cli.Tests/Scaffolding/ChannelReseedTests.cs
  • tests/Aspire.Cli.Tests/Telemetry/TelemetryConfigurationTests.cs
  • tests/Aspire.Cli.Tests/TestServices/FakeAppHostServerSession.cs
  • tests/Aspire.Cli.Tests/TestServices/FakeFailingAppHostServerProject.cs
  • tests/Aspire.Cli.Tests/TestServices/FakeSucceedingAppHostServerProject.cs
  • tests/Aspire.Cli.Tests/TestServices/ProcessTestHelpers.cs
  • tests/Aspire.Cli.Tests/TestServices/RecordingGracefulSignaler.cs
  • tests/Aspire.Cli.Tests/TestServices/TestAppHostServerSessionFactory.cs
  • tests/Aspire.Cli.Tests/TestServices/TestGracefulShutdownWindow.cs
  • tests/Aspire.Cli.Tests/TestServices/TestProcessExecutionFactory.cs
  • tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs
  • tests/Shared/AsyncTestHelpers.cs

Change Categories

  • CLI changes detected - process execution, Windows isolated console/process job handling, Ctrl+C/ProcessExit cancellation, AppHost session lifecycle, BaseCommand/run/start/stop/dashboard-run call sites
  • Hosting integration changes
  • Dashboard changes
  • Client/component changes
  • Template changes
  • VS Code extension changes
  • Test changes detected - CLI process/shutdown/session regression coverage
  • CI infrastructure changes

Test Scenarios Executed

Scenario 1: Dogfood artifact verification

Objective: Verify the tested CLI artifact contains the PR's latest changes.
Coverage Type: Artifact validation
Status: Passed

Steps:

  1. Installed the PR dogfood CLI with get-aspire-cli-pr.ps1 17814 -InstallPath -SkipExtension -SkipPath.
  2. Ran the installed aspire.exe --version directly.
  3. Compared the reported version to PR head a221fa9.

Evidence:

  • Install log: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\install-output.txt
  • Version log: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\version.txt

Observations:

  • Installed version was 13.5.0-pr.17814.ga221fa94, matching the PR head short SHA.

Scenario 2: .NET AppHost aspire run Ctrl+C graceful shutdown

Objective: Validate a fresh .NET starter AppHost launched by aspire run exits gracefully when Ctrl+C is delivered to the CLI's console group.
Coverage Type: Happy path
Status: Passed

Steps:

  1. Created a fresh aspire-starter project from the PR hive with Redis disabled and prompt-suppressing flags.
  2. Launched aspire run in a new console using the installed PR CLI.
  3. Sent CTRL_C_EVENT by attaching a side-process signaler to the CLI console, matching real terminal Ctrl+C behavior.
  4. Waited for the CLI to exit and scanned descendants for survivors.

Evidence:

  • Creation log: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\new-dotnet-starter.txt
  • Harness log: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\dotnet-ctrlc-smoke.txt
  • Captured run output: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\dotnet-run-output.txt
  • Signaler output: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\dotnet-signaler-output.txt

Observations:

  • aspire run exited after 764 ms.
  • Descendants before signal: 3.
  • Survivor descendants after shutdown: 0.

Scenario 3: TypeScript/polyglot AppHost aspire run Ctrl+C graceful shutdown

Objective: Validate a fresh TypeScript + C# starter AppHost launched by aspire run exits gracefully after Ctrl+C, including the Vite/frontend resource path affected by the process-tree shutdown changes.
Coverage Type: Happy path
Status: Passed

Steps:

  1. Created a fresh aspire-ts-cs-starter project from the PR hive with Redis disabled and prompt-suppressing flags.
  2. Launched aspire run in a new console using the installed PR CLI.
  3. While the run was active, executed aspire wait webfrontend --status up --apphost and verified the frontend resource was running.
  4. Sent CTRL_C_EVENT by attaching a side-process signaler to the CLI console.
  5. Waited for the CLI to exit and scanned descendants for survivors.

Evidence:

  • Creation log: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\new-ts-starter.txt
  • Resource wait log: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\ts-wait-webfrontend.txt
  • Harness log: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\ts-ctrlc-smoke-with-wait.txt
  • Captured run output: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\ts-run-output.txt
  • Signaler output: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\ts-signaler-output.txt

Observations:

  • webfrontend reached up (running) before Ctrl+C.
  • aspire run exited after 680 ms.
  • Survivor descendants after shutdown: 0.

Scenario 4: Detached aspire start / aspire stop lifecycle

Objective: Validate detached AppHost launch and shutdown still work through the shared process execution and graceful shutdown path.
Coverage Type: Happy path
Status: Passed

Steps:

  1. Created a fresh aspire-starter project from the PR hive.
  2. Ran aspire start --apphost --non-interactive.
  3. Ran aspire wait webfrontend --status up --apphost --non-interactive.
  4. Ran aspire describe --apphost --non-interactive.
  5. Ran aspire stop --apphost --non-interactive.
  6. Scanned for processes still referencing the scenario root.

Evidence:

  • Creation log: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\new-start-stop-starter.txt
  • Start log: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\start-stop-start.txt
  • Wait log: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\start-stop-wait.txt
  • Describe log: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\start-stop-describe.txt
  • Stop log: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\start-stop-stop.txt
  • Survivor scan: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\start-stop-survivor-check.txt

Observations:

  • start, wait, describe, and stop all exited with code 0.
  • webfrontend reached up (running).
  • Stop reported the AppHost stopped successfully.
  • Process survivor scan found 0 processes referencing the scenario root.

Scenario 5: Standalone aspire dashboard run launch and Ctrl+C shutdown

Objective: Validate dashboard run still launches through the updated process execution path and exits cleanly on Ctrl+C.
Coverage Type: Happy path
Status: Passed

Steps:

  1. Launched aspire dashboard run in a new console with anonymous access and explicit loopback frontend/OTLP ports.
  2. Polled the frontend URL until it responded.
  3. Sent CTRL_C_EVENT by attaching a side-process signaler to the CLI console.
  4. Waited for the CLI to exit and scanned descendants for survivors.

Evidence:

  • Harness log: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\dashboard-run-smoke.txt
  • Captured output: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\dashboard-run-output.txt
  • Signaler output: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\dashboard-run-signaler.txt

Observations:

  • Dashboard frontend responded before shutdown: True.
  • dashboard run exited after 720 ms.
  • Survivor descendants after shutdown: 0.

Scenario 6: Invalid --apphost path

Objective: Verify a user-facing CLI command fails safely when given a non-existent AppHost path.
Coverage Type: Unhappy path
Status: Passed

Steps:

  1. Ran aspire start --apphost .csproj --non-interactive.
  2. Verified non-zero exit code and clear error text.
  3. Scanned for processes referencing the negative-test root.

Evidence:

  • Error log: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\invalid-apphost-start.txt
  • Survivor scan: C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\invalid-apphost-survivor-check.txt

Expected Unhappy-Path Outcome: A clear validation error, non-zero exit code, and no orphaned processes.

Observations:

  • Exit code: 7.
  • Error text included: The --apphost option specified a project that does not exist.
  • Survivor processes: 0.

Summary

Scenario Status Notes
Dogfood artifact verification Passed 13.5.0-pr.17814.ga221fa94 matched a221fa9
.NET aspire run Ctrl+C shutdown Passed Graceful exit in 764 ms; 0 survivors
TypeScript/polyglot aspire run Ctrl+C shutdown Passed webfrontend up before Ctrl+C; graceful exit in 680 ms; 0 survivors
Detached aspire start / aspire stop Passed Start/wait/describe/stop succeeded; 0 survivor processes
Standalone aspire dashboard run Passed Frontend responded; Ctrl+C exit in 720 ms; 0 survivors
Invalid --apphost path Passed Clear error, exit code 7, 0 survivors

Overall Result

PR VERIFIED

The PR dogfood CLI at 13.5.0-pr.17814.ga221fa94 successfully handled Ctrl+C graceful shutdown for .NET, TypeScript/polyglot, and standalone dashboard-run scenarios on Windows, and detached aspire start / aspire stop remained functional.

Artifacts

  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\install-output.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\version.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\new-dotnet-starter.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\new-ts-starter.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\new-start-stop-starter.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\dotnet-ctrlc-smoke.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\dotnet-run-output.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\dotnet-signaler-output.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\ts-ctrlc-smoke-with-wait.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\ts-wait-webfrontend.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\ts-run-output.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\ts-signaler-output.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\start-stop-start.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\start-stop-wait.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\start-stop-describe.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\start-stop-stop.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\start-stop-survivor-check.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\dashboard-run-smoke.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\dashboard-run-output.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\dashboard-run-signaler.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\invalid-apphost-start.txt
  • C:\Users\danegsta.copilot\session-state\c74423b1-b53d-46e1-86dc-c8b7890496e8\files\pr-17814-testing-artifacts-a221fa94\invalid-apphost-survivor-check.txt

@danegsta

Copy link
Copy Markdown
Member Author

Also verified working on MacOS.

@danegsta danegsta merged commit 15bab02 into main Jun 23, 2026
325 checks passed
@danegsta danegsta deleted the danegsta/windows-tsx-shutdown branch June 23, 2026 22:05
@github-actions github-actions Bot added this to the 13.5 milestone Jun 23, 2026
aspire-repo-bot Bot added a commit to microsoft/aspire.dev that referenced this pull request Jun 23, 2026
- Expand aspire-run.mdx 'Stopping the AppHost' section with the full
  three-step shutdown ladder (cooperative cancellation → graceful wait →
  automatic force-kill) and clarify the second Ctrl+C behavior.
- Add a Windows note about isolated console session for tsx/npm AppHosts.
- Correct aspire-stop.mdx description: signal targets the AppHost process
  directly, not an intermediary CLI process.

Source: microsoft/aspire#17814

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@aspire-repo-bot

Copy link
Copy Markdown
Contributor

Pull request created: #1291

Generated by PR Documentation Check · 3.2K AIC · ⌖ 21.3 AIC · ⊞ 45.7K

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.

CLI: aspire run with TypeScript AppHost doesn't exit gracefully on Ctrl+C

4 participants