File: TestServices\TestAuxiliaryBackchannelMonitor.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Concurrent;
using Aspire.Cli.Backchannel;
 
namespace Aspire.Cli.Tests.TestServices;
 
internal sealed class TestAuxiliaryBackchannelMonitor : IAuxiliaryBackchannelMonitor
{
    // Outer key: hash, Inner key: socketPath
    private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, IAppHostAuxiliaryBackchannel>> _connectionsByHash = new();
 
    public IEnumerable<IAppHostAuxiliaryBackchannel> Connections =>
        _connectionsByHash.Values.SelectMany(d => d.Values);
 
    public IEnumerable<IAppHostAuxiliaryBackchannel> GetConnectionsByHash(string hash) =>
        _connectionsByHash.TryGetValue(hash, out var connections) ? connections.Values : [];
 
    public string? SelectedAppHostPath { get; set; }
 
    /// <summary>
    /// Gets the number of times ScanAsync was called.
    /// </summary>
    public int ScanCallCount { get; private set; }
 
    /// <summary>
    /// Triggers an immediate scan. In the test implementation, this just increments ScanCallCount.
    /// </summary>
    public Task ScanAsync(CancellationToken cancellationToken = default)
    {
        ScanCallCount++;
        return Task.CompletedTask;
    }
 
    public IAppHostAuxiliaryBackchannel? SelectedConnection
    {
        get
        {
            var connections = Connections.ToList();
 
            if (connections.Count == 0)
            {
                return null;
            }
 
            // Check if a specific AppHost was selected
            if (!string.IsNullOrEmpty(SelectedAppHostPath))
            {
                var selectedConnection = connections.FirstOrDefault(c =>
                    c.AppHostInfo?.AppHostPath != null &&
                    string.Equals(Path.GetFullPath(c.AppHostInfo.AppHostPath), Path.GetFullPath(SelectedAppHostPath), StringComparison.OrdinalIgnoreCase));
 
                if (selectedConnection != null)
                {
                    return selectedConnection;
                }
 
                // Clear the selection since the AppHost is no longer available
                SelectedAppHostPath = null;
            }
 
            // Look for in-scope connections
            var inScopeConnections = connections.Where(c => c.IsInScope).ToList();
 
            if (inScopeConnections.Count == 1)
            {
                return inScopeConnections[0];
            }
 
            // Fall back to the first available connection
            return connections.FirstOrDefault();
        }
    }
 
    public IReadOnlyList<IAppHostAuxiliaryBackchannel> GetConnectionsForWorkingDirectory(DirectoryInfo workingDirectory)
    {
        return Connections
            .Where(c => IsAppHostInScopeOfDirectory(c.AppHostInfo?.AppHostPath, workingDirectory.FullName))
            .ToList();
    }
 
    private static bool IsAppHostInScopeOfDirectory(string? appHostPath, string workingDirectory)
    {
        if (string.IsNullOrEmpty(appHostPath))
        {
            return false;
        }
 
        // Normalize the paths for comparison
        var normalizedWorkingDirectory = Path.GetFullPath(workingDirectory);
        var normalizedAppHostPath = Path.GetFullPath(appHostPath);
 
        // Check if the AppHost path is within the working directory
        var relativePath = Path.GetRelativePath(normalizedWorkingDirectory, normalizedAppHostPath);
        return !relativePath.StartsWith("..", StringComparison.Ordinal) && !Path.IsPathRooted(relativePath);
    }
 
    public void AddConnection(string hash, string socketPath, IAppHostAuxiliaryBackchannel connection)
    {
        var connectionsDict = _connectionsByHash.GetOrAdd(hash, _ => new ConcurrentDictionary<string, IAppHostAuxiliaryBackchannel>());
        connectionsDict[socketPath] = connection;
    }
 
    public void RemoveConnection(string hash, string socketPath)
    {
        if (_connectionsByHash.TryGetValue(hash, out var connectionsDict))
        {
            connectionsDict.TryRemove(socketPath, out _);
            if (connectionsDict.IsEmpty)
            {
                _connectionsByHash.TryRemove(hash, out _);
            }
        }
    }
 
    public void ClearConnections()
    {
        _connectionsByHash.Clear();
    }
}