| File: Commands\TerminalResourceResolver.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 System.Globalization; using System.Text.Json.Nodes; using Aspire.Cli.Backchannel; using Aspire.Cli.Interaction; using Aspire.Cli.Resources; using Aspire.Dashboard.Model; namespace Aspire.Cli.Commands; /// <summary> /// Resolves a resource name or instance ID to exactly one terminal endpoint. /// </summary> internal sealed class TerminalResourceResolver(IInteractionService interactionService) { public async Task<(string ResourceName, TerminalReplicaInfo? Replica)> ResolveAsync( IAppHostAuxiliaryBackchannel connection, string resourceName, int? requestedReplica, CancellationToken cancellationToken) { var snapshots = await interactionService.ShowStatusAsync( TerminalCommandStrings.LookingUpResource, async () => await connection.GetResourceSnapshotsAsync(includeHidden: true, cancellationToken).ConfigureAwait(false)); var matches = ResourceSnapshotMapper.ResolveResources(resourceName, snapshots); if (matches.Count == 0) { // Keep ambiguous display-name matches for diagnostics and legacy numeric selection, // without allowing them to override the shared resolver's exact instance-name match. matches = ResourceSnapshotMapper.WhereMatchesResourceName(snapshots, resourceName).ToList(); } if (matches.Count == 0) { interactionService.DisplayError(string.Format(CultureInfo.CurrentCulture, TerminalCommandStrings.ResourceNotFound, resourceName)); return (resourceName, null); } if (matches.Count > 1 && requestedReplica is null) { interactionService.DisplayError(string.Format(CultureInfo.CurrentCulture, TerminalCommandStrings.ResourceAmbiguous, resourceName, string.Join(", ", matches.Select(s => s.Name).Order(StringComparers.ResourceName)))); return (resourceName, null); } var snapshot = matches[0]; // Discovery still takes the parent model name. Keep the instance snapshot to map // its terminal.replicaIndex to an endpoint instead of losing the selected identity. var canonicalName = !string.IsNullOrEmpty(snapshot.DisplayName) ? snapshot.DisplayName : snapshot.Name; var info = await interactionService.ShowStatusAsync( TerminalCommandStrings.DiscoveringSessions, async () => await connection.GetTerminalInfoAsync(canonicalName, cancellationToken).ConfigureAwait(false)); if (!info.IsAvailable || info.Replicas is not { Length: > 0 } replicas) { interactionService.DisplayError(string.Format(CultureInfo.CurrentCulture, TerminalCommandStrings.TerminalUnavailable, canonicalName)); return (canonicalName, null); } // Preserve explicit numeric selection for callers using the hidden legacy option. if (requestedReplica is { } requestedIndex) { var match = Array.Find(replicas, r => r.ReplicaIndex == requestedIndex); if (match is null) { interactionService.DisplayError(string.Format(CultureInfo.CurrentCulture, TerminalCommandStrings.ReplicaNotFound, requestedIndex, canonicalName, string.Join(", ", replicas.Select(r => r.ReplicaIndex.ToString(CultureInfo.InvariantCulture))))); } return (canonicalName, match); } if (replicas.Length > 1 && string.Equals(resourceName, canonicalName, StringComparisons.ResourceName)) { interactionService.DisplayError(string.Format(CultureInfo.CurrentCulture, TerminalCommandStrings.ResourceInstanceRequired, resourceName)); return (canonicalName, null); } // Backchannel snapshots stamp the index as a JSON string, e.g. "terminal.replicaIndex": "1". // Labels such as "shell-1" are not the DCP instance ID ("shell-abc123"). if (snapshot.Properties.TryGetValue(KnownProperties.Terminal.ReplicaIndex, out var property)) { if (property is JsonValue value && value.TryGetValue<string>(out var indexText) && int.TryParse(indexText, NumberStyles.None, CultureInfo.InvariantCulture, out var index) && Array.Find(replicas, r => r.ReplicaIndex == index) is { } replica) { return (canonicalName, replica); } } else if (replicas.Length == 1 && ResourceSnapshotMapper.WhereMatchesResourceName(snapshots, canonicalName).Count() == 1) { return (canonicalName, replicas[0]); } interactionService.DisplayError(string.Format(CultureInfo.CurrentCulture, TerminalCommandStrings.InstanceTerminalUnavailable, snapshot.Name)); return (canonicalName, null); } }