Skip to content
Merged
Show file tree
Hide file tree
Changes from 57 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
537f7be
Unify graceful shutdown ladder for aspire run on Windows
danegsta Jun 2, 2026
e9508ea
Extend graceful shutdown ladder to .NET direct-launch AppHost path
danegsta Jun 2, 2026
d31fdda
Fix Process.ExitCode/HasExited on Windows isolated spawn path
danegsta Jun 2, 2026
53808ef
Fix aspire start on Windows: dedupe PROC_THREAD_ATTRIBUTE_HANDLE_LIST
danegsta Jun 2, 2026
65f3cda
Drop RequestShutdown/RequestedExitCode plumbing in favor of disposabl…
danegsta Jun 2, 2026
7636067
Merge remote-tracking branch 'origin/main' into danegsta/windows-tsx-…
danegsta Jun 4, 2026
1533fd8
Merge remote-tracking branch 'origin/main' into danegsta/windows-tsx-…
danegsta Jun 8, 2026
5e70a20
Merge remote-tracking branch 'origin/main' into danegsta/windows-tsx-…
danegsta Jun 10, 2026
0e8de7e
Fix publish-path backchannel wiring missed in merge
danegsta Jun 10, 2026
28b27c0
Potential fix for pull request finding
danegsta Jun 10, 2026
9c14441
Potential fix for pull request finding
danegsta Jun 10, 2026
4d3e8bc
Address PR review findings: env removal, gate disposal, dead code
danegsta Jun 11, 2026
7ee0c7a
Merge remote-tracking branch 'origin/main' into danegsta/windows-tsx-…
danegsta Jun 17, 2026
c392239
Merge remote-tracking branch 'origin/danegsta/windows-tsx-shutdown' i…
danegsta Jun 17, 2026
b9403bb
Fix graceful shutdown: parallel signal + 10s budget
danegsta Jun 17, 2026
369b6b1
Re-enable inherited CTRL+C handling in CLI startup on Windows
danegsta Jun 18, 2026
b5e287f
Revert graceful shutdown budget back to 5s
danegsta Jun 18, 2026
1d6743c
Strengthen CLI graceful shutdown regression tests
danegsta Jun 18, 2026
b4aaa8b
Merge remote-tracking branch 'origin/main' into danegsta/windows-tsx-…
danegsta Jun 18, 2026
eaf0ef3
Address shutdown PR review comments
danegsta Jun 18, 2026
ed42de6
Deduplicate CLI test helpers
danegsta Jun 18, 2026
e7e3315
Resolve Windows console job on-demand and rename shutdown service
danegsta Jun 18, 2026
190048f
Add ProcessShutdownCoordinator: single cancel->shutdown decision point
danegsta Jun 18, 2026
6565127
Unify ProcessExecution + IsolatedProcessExecution into one wrapper (P2)
danegsta Jun 18, 2026
30ec2ec
Make graceful shutdown a command-level policy owned by GracefulShutdo…
danegsta Jun 18, 2026
cd75220
Merge GracefulShutdownService into ConsoleCancellationManager; make I…
danegsta Jun 18, 2026
65ef50a
Unify process execution + shutdown ladder onto IProcessExecution
danegsta Jun 18, 2026
231530c
Fold ProcessShutdownCoordinator into ProcessExecution
danegsta Jun 18, 2026
048ad29
Merge ProcessTerminator into ProcessGracefulShutdownLadder
danegsta Jun 18, 2026
cf3763d
Fold ProcessGracefulShutdownLadder into ProcessExecution
danegsta Jun 19, 2026
b4ce224
Merge origin/main into danegsta/windows-tsx-shutdown
danegsta Jun 19, 2026
2bc02e2
Split AppHostServerSession.StartAsync into Start() + WaitForExitAsync()
danegsta Jun 19, 2026
f6459c1
Make AppHostServerSession start async (SemaphoreSlim gate + RunAsync)
danegsta Jun 19, 2026
900eb02
Make LayoutProcessRunner.Start async to await disposal on spawn failure
danegsta Jun 19, 2026
b9b6cff
Update stale StartAsync call comments referencing synchronous Start()
danegsta Jun 19, 2026
30829b7
Address PR review: fix env guard, document ProcessExit + RunAsync con…
danegsta Jun 19, 2026
7aba974
Streamline signaler drain in ShutdownLadderAsync
danegsta Jun 19, 2026
0151ba2
Fix Windows isolated-process exit wait and signaler drain race
danegsta Jun 19, 2026
192cf1b
Trim over-justified comments in shutdown path
danegsta Jun 19, 2026
37cc223
Throw plain OperationCanceledException from Windows WaitForExitAsync
danegsta Jun 19, 2026
4ad3154
Harden pid-file read against Windows sharing violation in ProcessGues…
danegsta Jun 19, 2026
8cd91e7
Restore IAppHostServerSessionFactory seam for short-lived codegen ses…
danegsta Jun 22, 2026
c884c08
Minor CLI cleanups: GetSafePid rename and Processes namespace import
danegsta Jun 22, 2026
4b96756
Remove redundant ConsoleCancellationManager DI registration
danegsta Jun 22, 2026
da58bb0
Dispose _startGate semaphore in AppHostServerSession.DisposeAsync
danegsta Jun 22, 2026
8be597a
Avoid hard-casting IProcessExecution to ProcessExecution
danegsta Jun 22, 2026
8159ae3
Drop unnecessary Interlocked machinery in GuestAppHostProject.RunAsync
danegsta Jun 22, 2026
e50fc82
Merge remote-tracking branch 'origin/main' into danegsta/windows-tsx-…
danegsta Jun 22, 2026
e15fcb9
Hoist graceful-shutdown budget to a virtual BaseCommand property
danegsta Jun 23, 2026
f5f4412
Make AppHostServerSession ctor params required; reorder token last
danegsta Jun 23, 2026
ec0acff
Collapse Ctrl+C ladder to a two-press flow
danegsta Jun 23, 2026
52dedf8
Rename signal-count local n to signalNumber in Cancel
danegsta Jun 23, 2026
663e9ce
Add CreatePrebuiltAppHostServer test helper to reduce boilerplate
danegsta Jun 23, 2026
e20586a
Remove accidentally committed ShutdownSmoke smoke-test project
danegsta Jun 23, 2026
a0dadd0
Replace NotStarted factory with SessionNotStartedException type
danegsta Jun 23, 2026
b1c6631
Route aspire run session through IAppHostServerSessionFactory
danegsta Jun 23, 2026
885b55d
Route aspire publish session through IAppHostServerSessionFactory
danegsta Jun 23, 2026
6cd35b7
Clarify undisposed execution comment for IAsyncDisposable
danegsta Jun 23, 2026
a5c7c12
Avoid parent-env snapshot in replace-don't-overlay spawn path
danegsta Jun 23, 2026
e191005
Use Assert.Skip for debugger-attached timing tests
danegsta Jun 23, 2026
c0189bf
Remove wasteful startup delays from guest apphost server startup
danegsta Jun 23, 2026
ad3cb77
Make ProcessGuestLauncher ctor and LaunchAsync args non-optional
danegsta Jun 23, 2026
e925699
Make remaining new optional args required where default is never used
danegsta Jun 23, 2026
dc29493
Fix run cancellation exit code, Windows force-kill escalation, and di…
danegsta Jun 23, 2026
a221fa9
Merge remote-tracking branch 'origin/main' into danegsta/windows-tsx-…
danegsta Jun 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Aspire.Cli/Commands/AppHostLauncher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ internal sealed class AppHostLauncher(
AspireCliTelemetry telemetry,
ProfilingTelemetry profilingTelemetry,
FileLoggerProvider fileLoggerProvider,
ProcessShutdownService processShutdownService,
ProcessTreeGracefulShutdownService processShutdownService,
IDetachedProcessLauncher detachedProcessLauncher,
ILogger<AppHostLauncher> logger,
TimeProvider timeProvider)
Expand Down
15 changes: 15 additions & 0 deletions src/Aspire.Cli/Commands/BaseCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ internal abstract class BaseCommand : Command
/// </summary>
internal virtual HelpGroup HelpGroup => HelpGroup.None;

/// <summary>
/// The graceful-shutdown budget this command grants its child processes before shutdown ladders
/// escalate to forceful termination. <see cref="BaseCommand"/> reads this and configures the
/// shared <see cref="ConsoleCancellationManager"/> centrally before invoking <see cref="ExecuteAsync"/>.
/// The default of zero preserves force-kill-on-cancel behavior for every command that does not opt in;
/// <c>aspire run</c> overrides it to give the AppHost a real cooperative-shutdown window.
/// </summary>
protected virtual TimeSpan GracefulShutdownBudget => TimeSpan.Zero;

private readonly CliExecutionContext _executionContext;

protected CliExecutionContext ExecutionContext => _executionContext;
Expand Down Expand Up @@ -60,6 +69,12 @@ protected BaseCommand(string name, string description, CommonCommandServices ser
var stoppingMessageShown = false;
try
{
// Configure the shared shutdown manager with this command's graceful-shutdown budget
// before the handler starts spawning child processes, so every per-child shutdown
// ladder observes the correct window from the first user signal. Commands that don't
// override GracefulShutdownBudget get the zero default (force-kill on cancel).
services.CancellationManager.ConfigureForCommand(GracefulShutdownBudget);

var handlerTask = ExecuteAsync(parseResult, cancellationToken);
services.CancellationManager.SetStartedHandler(handlerTask);

Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Cli/Commands/DashboardRunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ private async Task<CommandResult> ExecuteForegroundAsync(string managedPath, Lis
IProcessExecution process;
try
{
process = _layoutProcessRunner.Start(managedPath, dashboardArgs, environmentVariables: environmentVariables, options: options);
process = await _layoutProcessRunner.StartAsync(managedPath, dashboardArgs, environmentVariables: environmentVariables, options: options).ConfigureAwait(false);
}
catch (Exception ex)
{
Expand All @@ -380,7 +380,7 @@ private async Task<CommandResult> ExecuteForegroundAsync(string managedPath, Lis
return CommandResult.Failure(CliExitCodes.DashboardFailure);
}

using var _ = process;
await using var _ = process;

// Wait for the dashboard to become ready, the process to exit, or a timeout.
var processExitTask = process.WaitForExitAsync(cancellationToken);
Expand Down
24 changes: 19 additions & 5 deletions src/Aspire.Cli/Commands/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,25 @@ internal sealed class RunCommand : BaseCommand

private static readonly TimeSpan s_appHostStartupCancellationTimeout = TimeSpan.FromSeconds(5);

// Graceful shutdown budget for `aspire run`. DCP gets a cooperative window to drain
// resources before the central drain budget arms and ladders escalate to forceful kill.
// 5s is comfortably enough for a default starter template (apphost + 2 services +
// dashboard) once CTRL+C actually reaches the AppHost — measured graceful exits run
// ~700ms end-to-end. Earlier observations of ~6s drains were an artifact of the
// inherited "ignore CTRL+C" attribute (cleared in Program.cs Main on Windows) which
// caused DCP to fall back to its internal 6s SIGKILL deadline because the kernel was
// silently dropping the CTRL_C_EVENT.
internal static readonly TimeSpan s_gracefulShutdownBudget = TimeSpan.FromSeconds(5);
Comment thread
danegsta marked this conversation as resolved.

// Guest AppHosts can bring up the temporary server/backchannel and then fail immediately
// afterward when the guest startup process hits a syntax, pre-execute, or model validation
// error. Keep guest AppHost startup waits alive briefly so those failures are reported instead of hidden.
private static readonly TimeSpan s_startupFailureObservationWindow = TimeSpan.FromSeconds(2);

protected override bool UpdateNotificationsEnabled => !_isDetachMode;

protected override TimeSpan GracefulShutdownBudget => s_gracefulShutdownBudget;

private static readonly Option<bool> s_detachOption = new("--detach")
{
Description = RunCommandStrings.DetachArgumentDescription
Expand Down Expand Up @@ -504,9 +516,10 @@ await InteractionService.DisplayLiveAsync(BuildLiveRenderable(), async updateTar
{
runActivity?.SetTag(TelemetryConstants.Tags.ErrorType, "canceled");

// The user cancelled (e.g. Ctrl+C); the linked CTS we passed to project.RunAsync
// propagated the cancellation and the OCE bubbled out with the linked token.
// Treat as successful exit since the user intentionally stopped the AppHost.
// User Ctrl+C is the normal exit path for `aspire run`; surface as success.
// Internal failures `return X` directly from GuestAppHostProject.RunAsync rather
// than flowing through this catch, so we don't need to distinguish failure codes
// here.
return CommandResult.Cancelled(CliExitCodes.Success);
}
finally
Expand All @@ -529,8 +542,9 @@ await InteractionService.DisplayLiveAsync(BuildLiveRenderable(), async updateTar
{
runActivity?.SetTag(TelemetryConstants.Tags.ErrorType, "canceled");

// Command is designed to be cancellable by the user (e.g. Ctrl+C) at any time.
// Treat cancellation as a successful exit since the user intentionally stopped the AppHost.
// User Ctrl+C is the normal exit path for `aspire run`; surface as success.
// Internal failures `return X` directly from GuestAppHostProject.RunAsync rather
// than flowing through this catch.
return CommandResult.Cancelled(CliExitCodes.Success);
}
catch (ProjectLocatorException ex)
Expand Down
24 changes: 13 additions & 11 deletions src/Aspire.Cli/Commands/Sdk/SdkDumpCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ namespace Aspire.Cli.Commands.Sdk;
internal sealed class SdkDumpCommand : BaseCommand
{
private readonly IAppHostServerProjectFactory _appHostServerProjectFactory;
private readonly IAppHostServerSessionFactory _appHostServerSessionFactory;
private readonly IAppHostServerSessionFactory _serverSessionFactory;
private readonly ILogger<SdkDumpCommand> _logger;

private static readonly Argument<string[]> s_integrationArgument = new("integrations")
Expand All @@ -56,13 +56,13 @@ internal sealed class SdkDumpCommand : BaseCommand

public SdkDumpCommand(
IAppHostServerProjectFactory appHostServerProjectFactory,
IAppHostServerSessionFactory appHostServerSessionFactory,
IAppHostServerSessionFactory serverSessionFactory,
ILogger<SdkDumpCommand> logger,
CommonCommandServices services)
: base("dump", "Dump ATS capabilities from Aspire integration libraries.", services)
{
_appHostServerProjectFactory = appHostServerProjectFactory;
_appHostServerSessionFactory = appHostServerSessionFactory;
_serverSessionFactory = serverSessionFactory;
_logger = logger;

Arguments.Add(s_integrationArgument);
Expand Down Expand Up @@ -193,10 +193,11 @@ private async Task<int> DumpCapabilitiesAsync(
return CliExitCodes.FailedToBuildArtifacts;
}

await using var serverSession = _appHostServerSessionFactory.Start(
appHostServerProject,
environmentVariables: null,
debug: false);
await using var serverSession = _serverSessionFactory.Create(appHostServerProject, environmentVariables: null, debug: false, gracefulShutdownSignaler: null, shutdownService: null, isolateConsole: false, cancellationToken);
// Short-lived RPC session: StartAsync() spawns the server. We never observe the
// exit-code task (WaitForExitAsync) because disposal flows the exit code through the
// activity scope and the only failure mode we care about surfaces via the RPC call below.
await serverSession.StartAsync();

// Connect and get capabilities
var rpcClient = await serverSession.GetRpcClientAsync(cancellationToken);
Expand Down Expand Up @@ -291,10 +292,11 @@ private async Task<int> DumpCapabilitiesToDirectoryAsync(
return CliExitCodes.FailedToBuildArtifacts;
}

await using var serverSession = _appHostServerSessionFactory.Start(
appHostServerProject,
environmentVariables: null,
debug: false);
await using var serverSession = _serverSessionFactory.Create(appHostServerProject, environmentVariables: null, debug: false, gracefulShutdownSignaler: null, shutdownService: null, isolateConsole: false, cancellationToken);
// Short-lived RPC session: StartAsync() spawns the server. We never observe the
// exit-code task (WaitForExitAsync) because disposal flows the exit code through the
// activity scope and the only failure mode we care about surfaces via the RPC call below.
await serverSession.StartAsync();

var rpcClient = await serverSession.GetRpcClientAsync(cancellationToken);
outputDirectory.Create();
Expand Down
15 changes: 8 additions & 7 deletions src/Aspire.Cli/Commands/Sdk/SdkGenerateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ internal sealed class SdkGenerateCommand : BaseCommand
{
private readonly ILanguageDiscovery _languageDiscovery;
private readonly IAppHostServerProjectFactory _appHostServerProjectFactory;
private readonly IAppHostServerSessionFactory _appHostServerSessionFactory;
private readonly IAppHostServerSessionFactory _serverSessionFactory;
private readonly ILogger<SdkGenerateCommand> _logger;

private static readonly Argument<FileInfo> s_integrationArgument = new("integration")
Expand All @@ -41,14 +41,14 @@ internal sealed class SdkGenerateCommand : BaseCommand
public SdkGenerateCommand(
ILanguageDiscovery languageDiscovery,
IAppHostServerProjectFactory appHostServerProjectFactory,
IAppHostServerSessionFactory appHostServerSessionFactory,
IAppHostServerSessionFactory serverSessionFactory,
ILogger<SdkGenerateCommand> logger,
CommonCommandServices services)
: base("generate", "Generate typed SDKs from an Aspire integration library for use in other languages.", services)
{
_languageDiscovery = languageDiscovery;
_appHostServerProjectFactory = appHostServerProjectFactory;
_appHostServerSessionFactory = appHostServerSessionFactory;
_serverSessionFactory = serverSessionFactory;
_logger = logger;

Arguments.Add(s_integrationArgument);
Expand Down Expand Up @@ -154,10 +154,11 @@ private async Task<int> GenerateSdkAsync(
return CliExitCodes.FailedToBuildArtifacts;
}

await using var serverSession = _appHostServerSessionFactory.Start(
appHostServerProject,
environmentVariables: null,
debug: false);
await using var serverSession = _serverSessionFactory.Create(appHostServerProject, environmentVariables: null, debug: false, gracefulShutdownSignaler: null, shutdownService: null, isolateConsole: false, cancellationToken);
// Short-lived RPC session: StartAsync() spawns the server. We never observe the
// exit-code task (WaitForExitAsync) because disposal flows the exit code through the
// activity scope and the only failure mode we care about surfaces via the RPC call below.
await serverSession.StartAsync();

// Connect and generate code
var rpcClient = await serverSession.GetRpcClientAsync(cancellationToken);
Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Cli/Commands/StopCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal sealed class StopCommand : BaseCommand
private readonly AppHostConnectionResolver _connectionResolver;
private readonly ILogger<StopCommand> _logger;
private readonly ICliHostEnvironment _hostEnvironment;
private readonly ProcessShutdownService _processShutdownService;
private readonly ProcessTreeGracefulShutdownService _processShutdownService;
private readonly ProfilingTelemetry _profilingTelemetry;

private static readonly OptionWithLegacy<FileInfo?> s_appHostOption = new("--apphost", "--project", StopCommandStrings.ProjectArgumentDescription);
Expand All @@ -35,7 +35,7 @@ internal sealed class StopCommand : BaseCommand
public StopCommand(
AppHostConnectionResolver connectionResolver,
ICliHostEnvironment hostEnvironment,
ProcessShutdownService processShutdownService,
ProcessTreeGracefulShutdownService processShutdownService,
ILogger<StopCommand> logger,
ProfilingTelemetry profilingTelemetry,
CommonCommandServices services)
Expand Down
Loading