File: Cli\CliOrphanDetector.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
 
namespace Aspire.Hosting.Cli;
 
internal sealed class CliOrphanDetector(IConfiguration configuration, IHostApplicationLifetime lifetime, TimeProvider timeProvider) : BackgroundService
{
    internal Func<int, bool> IsProcessRunning { get; set; } = (int pid) =>
    {
        try
        {
            return !Process.GetProcessById(pid).HasExited;
        }
        catch (ArgumentException)
        {
            // If Process.GetProcessById throws it means the process in not running.
            return false;
        }
    };
 
    internal Func<int, long, bool> IsProcessRunningWithStartTime { get; set; } = (int pid, long expectedStartTimeUnix) =>
    {
        try
        {
            var process = Process.GetProcessById(pid);
            if (process.HasExited)
            {
                return false;
            }
            
            // Check if the process start time matches the expected start time exactly.
            var actualStartTimeUnix = ((DateTimeOffset)process.StartTime).ToUnixTimeSeconds();
            return actualStartTimeUnix == expectedStartTimeUnix;
        }
        catch
        {
            // If we can't get the process and/or can't get the start time, 
            // then we interpret both exceptions as the process not being there.
            return false;
        }
    };
 
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            if (configuration[KnownConfigNames.CliProcessId] is not { } pidString || !int.TryParse(pidString, out var pid))
            {
                // If there is no PID environment variable, we assume that the process is not a child process
                // of the .NET Aspire CLI and we won't continue monitoring.
                return;
            }
 
            // Try to get the CLI process start time for robust orphan detection
            long? expectedStartTimeUnix = null;
            if (configuration[KnownConfigNames.CliProcessStarted] is { } startTimeString && 
                long.TryParse(startTimeString, out var startTimeUnix))
            {
                expectedStartTimeUnix = startTimeUnix;
            }
 
            using var periodic = new PeriodicTimer(TimeSpan.FromSeconds(1), timeProvider);
 
            do
            {
                bool isProcessStillRunning;
                
                if (expectedStartTimeUnix.HasValue)
                {
                    // Use robust process checking with start time verification
                    isProcessStillRunning = IsProcessRunningWithStartTime(pid, expectedStartTimeUnix.Value);
                }
                else
                {
                    // Fall back to PID-only logic for backwards compatibility
                    isProcessStillRunning = IsProcessRunning(pid);
                }
 
                if (!isProcessStillRunning)
                {
                    lifetime.StopApplication();
                    return;
                }
            } while (await periodic.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false));
        }
        catch (TaskCanceledException)
        {
            // This is expected when the app is shutting down.
        }
    }
}