File: OrphanDetector.cs
Web Access
Project: src\src\Aspire.Hosting.RemoteHost\Aspire.Hosting.RemoteHost.csproj (Aspire.Hosting.RemoteHost)
// 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 Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting.RemoteHost;
 
internal sealed class OrphanDetector : BackgroundService
{
    private readonly IConfiguration _configuration;
    private readonly IHostApplicationLifetime _lifetime;
    private readonly ILogger<OrphanDetector> _logger;
 
    public OrphanDetector(IConfiguration configuration, IHostApplicationLifetime lifetime, ILogger<OrphanDetector> logger)
    {
        _configuration = configuration;
        _lifetime = lifetime;
        _logger = logger;
    }
 
    // PID-only liveness check. Used as a fallback when no start time was supplied (older CLIs).
    internal Func<int, bool> IsProcessRunning { get; set; } = static pid => ProcessStartTimeHelper.IsProcessRunning(pid);
 
    // PID + start-time liveness check. Verifying the start time prevents a recycled PID from making
    // an orphaned server believe its long-dead parent is still alive (the cause of leaked
    // aspire-managed processes under high process churn).
    internal Func<int, long, bool> IsProcessRunningWithStartTime { get; set; } = static (pid, expectedStartTimeUnix) => ProcessStartTimeHelper.IsProcessRunning(pid, expectedStartTimeUnix);
 
    internal Func<int, long, bool> IsProcessRunningWithLegacyStartTime { get; set; } = static (pid, expectedStartTimeUnix) =>
    {
        return ProcessStartTimeHelper.IsProcessRunningWithRuntimeStartTime(pid, expectedStartTimeUnix, ProcessStartTimeHelper.LegacyStartTimeMatchTolerance);
    };
 
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            if (_configuration[KnownConfigNames.RemoteAppHostProcessId] is not { } pidString || !int.TryParse(pidString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var pid))
            {
                // If there is no PID environment variable, we assume that the process is not a child process
                // of the Aspire CLI and we won't continue monitoring.
                _logger.LogDebug("No parent PID specified, orphan detection disabled");
                return;
            }
 
            _logger.LogDebug("Monitoring parent process PID: {ParentPid}", pid);
 
            // Prefer start times that current CLIs produce from /proc. ASPIRE_CLI_STARTED is kept in the
            // Process.StartTime clock domain for released AppHosts, so only use it with the legacy verifier.
            // When no start time is present, degrade to PID-only detection.
            long? expectedStartTimeUnix = null;
            var useLegacyStartTime = false;
            var startTimeString = _configuration[KnownConfigNames.RemoteAppHostProcessStarted]
                ?? _configuration[KnownConfigNames.CliProcessStartedStable];
            if (startTimeString is null)
            {
                startTimeString = _configuration[KnownConfigNames.CliProcessStarted];
                useLegacyStartTime = true;
            }
 
            if (ProcessStartTimeHelper.TryParseStartTimeUnixSeconds(startTimeString) is { } parsedStartTime)
            {
                expectedStartTimeUnix = parsedStartTime;
                _logger.LogDebug("Using start time verification. Expected start time: {StartTime}", expectedStartTimeUnix);
            }
            else
            {
                _logger.LogDebug("No valid start time configured. Using PID-only detection.");
            }
 
            using var periodic = new PeriodicTimer(TimeSpan.FromSeconds(1), TimeProvider.System);
 
            do
            {
                var isProcessStillRunning = expectedStartTimeUnix switch
                {
                    { } expected when useLegacyStartTime => IsProcessRunningWithLegacyStartTime(pid, expected),
                    { } expected => IsProcessRunningWithStartTime(pid, expected),
                    _ => IsProcessRunning(pid)
                };
 
                if (!isProcessStillRunning)
                {
                    _logger.LogWarning("Parent process {ParentPid} is no longer running, shutting down...", pid);
                    _lifetime.StopApplication();
                    return;
                }
            } while (await periodic.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false));
        }
        catch (OperationCanceledException)
        {
            // This is expected when the app is shutting down.
            _logger.LogDebug("OrphanDetector: Stopped");
        }
    }
}