| 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(); } }