File: src\Shared\ProcessScope.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 Microsoft.Extensions.Logging;
 
namespace Aspire.Shared;
 
/// <summary>
/// Retains a contained execution and its root-exit signal for a supervising owner.
/// </summary>
internal sealed class ProcessScope : IAsyncDisposable
{
    private readonly IChildProcess _execution;
    private readonly ILogger _logger;
    private readonly string _description;
    private int _disposed;
 
    internal ProcessScope(IChildProcess execution, ILogger logger, string description)
    {
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(execution.ProcessId);
        if (execution.ProcessId == Environment.ProcessId)
        {
            throw new InvalidOperationException("A supervised child process scope cannot own its caller.");
        }
        _execution = execution;
        _logger = logger;
        _description = description;
        ProcessId = execution.ProcessId;
        Exit = execution.WaitForRootExitAsync(CancellationToken.None);
        _logger.LogDebug("Process scope '{Name}' started with supervisor PID {Pid}.", description, ProcessId);
    }
 
    public int ProcessId { get; }
 
    public Task<int> Exit { get; }
 
    public Task<int> WaitForExitAsync(CancellationToken cancellationToken)
        => _execution.WaitForExitAsync(cancellationToken);
 
    public async ValueTask DisposeAsync()
    {
        if (Interlocked.Exchange(ref _disposed, 1) != 0)
        {
            return;
        }
        try
        {
            // ChildProcess owns the common shutdown ladder, containment verification, and
            // output drain. This owner wrapper must not implement another termination policy.
            await _execution.DisposeAsync().ConfigureAwait(false);
            _logger.LogDebug("Process scope '{Name}' (supervisor PID {Pid}) terminated and cleanup verified.", _description, ProcessId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to clean up process scope '{Name}' (supervisor PID {Pid}).", _description, ProcessId);
            throw;
        }
    }
 
    internal async ValueTask DisposeAsync(Exception? operationFailure)
    {
        try
        {
            await DisposeAsync().ConfigureAwait(false);
        }
        catch (Exception cleanupFailure) when (operationFailure is not null)
        {
            throw new AggregateException(
                $"Operation '{_description}' failed and its process scope could not be cleaned up.",
                operationFailure, cleanupFailure);
        }
    }
}