| File: Terminal\DefaultTerminalConnectionResolver.cs | Web Access |
| Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Dashboard.Model; using Hex1b; namespace Aspire.Dashboard.Terminal; /// <summary> /// Resolves per-replica HMP v1 producer streams by walking the live resource /// snapshot stream from <see cref="IDashboardClient"/>. The dashboard receives /// the consumer UDS path inside each replica snapshot's properties; this /// resolver looks up the requested resource by display name + replica index and /// connects to the matching local socket. /// </summary> /// <remarks> /// <para>The path itself is included in the gRPC stream from the AppHost. In Aspire's /// single-user, single-machine local-dev scenario the path is not a privileged /// secret (the user already controls the AppHost process and can read or write /// anything in its temp directory), but the path never reaches the browser via /// the terminal WebSocket because the proxy takes only /// <c>resource</c>/<c>replica</c> identifiers.</para> /// <para>The resolver intentionally does <i>not</i> use Hex1b's /// <c>WithHmp1UdsClient</c> builder. That builder is for in-process Hex1b /// applications that want to <i>embed</i> the HMP1 stream into a Hex1b /// terminal (the CLI's <c>aspire terminal attach</c> path does exactly that). /// The dashboard never instantiates a Hex1b terminal — it is a byte-level /// proxy between the browser's HMP1 client and the remote terminal host — /// so the resolver only needs the raw stream and reaches for the lower-level /// <see cref="Hmp1Transports.ConnectUnixSocket"/> helper instead.</para> /// </remarks> internal sealed class DefaultTerminalConnectionResolver : ITerminalConnectionResolver { private readonly IDashboardClient _client; public DefaultTerminalConnectionResolver(IDashboardClient client) { _client = client ?? throw new ArgumentNullException(nameof(client)); } public async Task<Stream?> ConnectAsync(string resourceName, int replicaIndex, CancellationToken cancellationToken) { if (!_client.IsEnabled) { return null; } // Locate the replica snapshot for (resourceName, replicaIndex). DCP names // each replica with a random suffix (e.g. myapp-abc123), but every snapshot // also carries the user-facing display name and the stable terminal // properties stamped by DashboardServiceData. We match by display name + // replica index rather than by the synthetic snapshot name. ResourceViewModel? match = null; foreach (var resource in _client.GetResources()) { if (!string.Equals(resource.DisplayName, resourceName, StringComparison.Ordinal)) { continue; } if (!resource.HasTerminal()) { continue; } if (!resource.TryGetTerminalReplicaInfo(out var index, out _) || index != replicaIndex) { continue; } match = resource; break; } if (match is null) { return null; } if (!match.TryGetTerminalConsumerUdsPath(out var udsPath)) { return null; } return await Hmp1Transports.ConnectUnixSocket(udsPath, cancellationToken).ConfigureAwait(false); } }