File: Projects\RunningInstanceManager.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.Tool.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.Diagnostics;
using System.Globalization;
using Aspire.Cli.Backchannel;
using Aspire.Cli.Interaction;
using Aspire.Cli.Resources;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Cli.Projects;
 
/// <summary>
/// Provides shared utilities for managing running AppHost instances.
/// </summary>
internal sealed class RunningInstanceManager
{
    private const int ProcessTerminationTimeoutMs = 10000; // Wait up to 10 seconds for processes to terminate
    private const int ProcessTerminationPollIntervalMs = 250; // Check process status every 250ms
 
    private readonly ILogger _logger;
    private readonly IInteractionService _interactionService;
    private readonly TimeProvider _timeProvider;
 
    public RunningInstanceManager(
        ILogger logger,
        IInteractionService interactionService,
        TimeProvider timeProvider)
    {
        _logger = logger;
        _interactionService = interactionService;
        _timeProvider = timeProvider;
    }
 
    /// <summary>
    /// Stops a running AppHost instance by connecting to its auxiliary backchannel.
    /// </summary>
    /// <param name="socketPath">The path to the auxiliary backchannel socket.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>True if the instance was stopped successfully, false otherwise.</returns>
    public async Task<bool> StopRunningInstanceAsync(string socketPath, CancellationToken cancellationToken)
    {
        try
        {
            // Connect to the auxiliary backchannel
            using var backchannel = await AppHostAuxiliaryBackchannel.ConnectAsync(socketPath, _logger, cancellationToken).ConfigureAwait(false);
 
            // Get the AppHost information
            var appHostInfo = backchannel.AppHostInfo;
 
            if (appHostInfo is null)
            {
                _logger.LogWarning("Failed to get AppHost information from running instance");
                return false;
            }
 
            // Display message that we're stopping the previous instance
            var cliPidText = appHostInfo.CliProcessId.HasValue ? appHostInfo.CliProcessId.Value.ToString(CultureInfo.InvariantCulture) : "N/A";
            _interactionService.DisplayMessage("stop_sign", $"Stopping previous instance (AppHost PID: {appHostInfo.ProcessId.ToString(CultureInfo.InvariantCulture)}, CLI PID: {cliPidText})");
 
            // Call StopAppHostAsync on the auxiliary backchannel
            await backchannel.StopAppHostAsync(cancellationToken).ConfigureAwait(false);
 
            // Monitor the PIDs for termination
            var stopped = await MonitorProcessesForTerminationAsync(appHostInfo, cancellationToken).ConfigureAwait(false);
 
            if (stopped)
            {
                _interactionService.DisplaySuccess(RunCommandStrings.RunningInstanceStopped);
            }
            else
            {
                _logger.LogWarning("Failed to stop running instance within timeout");
            }
 
            return stopped;
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to stop running instance");
            return false;
        }
    }
 
    /// <summary>
    /// Monitors a set of processes for termination within a timeout period.
    /// </summary>
    /// <param name="appHostInfo">Information about the AppHost processes to monitor.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>True if all processes terminated within the timeout, false otherwise.</returns>
    public async Task<bool> MonitorProcessesForTerminationAsync(AppHostInformation appHostInfo, CancellationToken cancellationToken)
    {
        var startTime = _timeProvider.GetUtcNow();
        var pidsToMonitor = new List<int> { appHostInfo.ProcessId };
 
        if (appHostInfo.CliProcessId.HasValue)
        {
            pidsToMonitor.Add(appHostInfo.CliProcessId.Value);
        }
 
        while ((_timeProvider.GetUtcNow() - startTime).TotalMilliseconds < ProcessTerminationTimeoutMs)
        {
            var allStopped = true;
 
            foreach (var pid in pidsToMonitor)
            {
                try
                {
                    var process = Process.GetProcessById(pid);
                    // If we can get the process, it's still running
                    allStopped = false;
                }
                catch (ArgumentException)
                {
                    // Process doesn't exist, it has stopped
                }
            }
 
            if (allStopped)
            {
                return true;
            }
 
            await Task.Delay(ProcessTerminationPollIntervalMs, cancellationToken).ConfigureAwait(false);
        }
 
        // Timeout reached
        return false;
    }
}