| File: Processes\LauncherLivenessMonitor.cs | Web Access |
| Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; using Aspire.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Aspire.Cli.Processes; /// <summary> /// Watches the foreground launcher process of a detached <c>aspire start</c> / <c>aspire run --detach</c> /// and cancels the AppHost run if that launcher dies before the app reaches readiness. /// </summary> /// <remarks> /// <para> /// A detached child CLI is the supervisor that runs the AppHost for its whole lifetime; it is meant to /// outlive the foreground that spawned it. But during the startup window (build + waiting for the app to /// become ready) the child has no anchor to the launcher, so if the launcher is killed mid-start /// — e.g. the user gets impatient and hits Ctrl-C, or a test runner times it out — /// the AppHost + dashboard are leaked as orphaned processes. /// </para> /// </remarks> internal sealed class LauncherLivenessMonitor : IAsyncDisposable { private readonly ParentProcessLivenessMonitor _monitor; private LauncherLivenessMonitor(ParentProcessLivenessMonitor monitor) { _monitor = monitor; } /// <summary> /// Starts a monitor when the launcher identity is present in configuration (i.e. this process is a detached child). /// Returns <see langword="null"/> for a normal foreground run, where there is no launcher process to watch. /// </summary> public static LauncherLivenessMonitor? StartIfConfigured( IConfiguration configuration, CancellationTokenSource cancelOnLauncherExit, TimeProvider timeProvider, ILogger logger) { if (!int.TryParse(configuration[KnownConfigNames.CliLauncherProcessId], NumberStyles.Integer, CultureInfo.InvariantCulture, out var launcherPid)) { return null; } var launcherStartedUnix = ProcessStartTimeHelper.TryParseStartTimeUnixSeconds(configuration[KnownConfigNames.CliLauncherProcessStarted]); logger.LogDebug("Detached child: watching launcher process {LauncherPid} until the AppHost is ready.", launcherPid); var monitor = ParentProcessLivenessMonitor.Start( launcherPid, launcherStartedUnix, _ => { logger.LogWarning( "Launcher process {LauncherPid} exited before the AppHost reached readiness. Shutting the detached AppHost down to avoid leaking it.", launcherPid); try { if (!cancelOnLauncherExit.IsCancellationRequested) { cancelOnLauncherExit.Cancel(); } } catch (ObjectDisposedException) { // The run already completed and disposed its cancellation source; nothing to do. } return Task.CompletedTask; }, timeProvider); return new LauncherLivenessMonitor(monitor); } public ValueTask DisposeAsync() => _monitor.DisposeAsync(); }