// 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.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Aspire.Dashboard.Utils;
namespace Aspire.Dashboard.Model;
internal static class ResourceViewModelExtensions
{
/// <summary>
/// Converts the resource properties to a dictionary of string values.
/// This is used to provide a consistent interface for code that works with both
/// ResourceViewModel (Dashboard) and ResourceSnapshot (CLI).
/// </summary>
public static IReadOnlyDictionary<string, string?> GetPropertiesAsDictionary(this ResourceViewModel resource)
{
var result = new Dictionary<string, string?>(StringComparer.Ordinal);
foreach (var (key, property) in resource.Properties)
{
if (property.Value.TryConvertToString(out var stringValue))
{
result[key] = stringValue;
}
}
return result;
}
public static bool IsContainer(this ResourceViewModel resource)
{
return string.Equals(resource.ResourceType, KnownResourceTypes.Container, StringComparisons.ResourceType);
}
public static bool IsProject(this ResourceViewModel resource)
{
return string.Equals(resource.ResourceType, KnownResourceTypes.Project, StringComparisons.ResourceType);
}
public static bool IsTool(this ResourceViewModel resource)
{
return string.Equals(resource.ResourceType, KnownResourceTypes.Tool, StringComparisons.ResourceType);
}
public static bool IsExecutable(this ResourceViewModel resource, bool allowSubtypes)
{
if (string.Equals(resource.ResourceType, KnownResourceTypes.Executable, StringComparisons.ResourceType))
{
return true;
}
if (allowSubtypes)
{
return string.Equals(resource.ResourceType, KnownResourceTypes.Project, StringComparisons.ResourceType);
}
return false;
}
public static bool TryGetExitCode(this ResourceViewModel resource, out int exitCode)
{
return resource.TryGetCustomDataInt(KnownProperties.Resource.ExitCode, out exitCode);
}
public static bool TryGetContainerImage(this ResourceViewModel resource, [NotNullWhen(returnValue: true)] out string? containerImage)
{
return resource.TryGetCustomDataString(KnownProperties.Container.Image, out containerImage);
}
public static bool TryGetProjectPath(this ResourceViewModel resource, [NotNullWhen(returnValue: true)] out string? projectPath)
{
return resource.TryGetCustomDataString(KnownProperties.Project.Path, out projectPath);
}
public static bool TryGetToolPackage(this ResourceViewModel resource, [NotNullWhen(returnValue: true)] out string? projectPath)
{
return resource.TryGetCustomDataString(KnownProperties.Tool.Package, out projectPath);
}
public static bool TryGetExecutablePath(this ResourceViewModel resource, [NotNullWhen(returnValue: true)] out string? executablePath)
{
return resource.TryGetCustomDataString(KnownProperties.Executable.Path, out executablePath);
}
public static bool TryGetExecutableArguments(this ResourceViewModel resource, out ImmutableArray<string> arguments)
{
return resource.TryGetCustomDataStringArray(KnownProperties.Executable.Args, out arguments);
}
public static bool TryGetAppArgs(this ResourceViewModel resource, out ImmutableArray<string> arguments)
{
return resource.TryGetCustomDataStringArray(KnownProperties.Resource.AppArgs, out arguments);
}
public static bool TryGetAppArgsSensitivity(this ResourceViewModel resource, out ImmutableArray<bool> argParams)
{
return resource.TryGetCustomDataBoolArray(KnownProperties.Resource.AppArgsSensitivity, out argParams);
}
/// <summary>
/// Returns <c>true</c> if the resource has an interactive terminal session available.
/// </summary>
public static bool HasTerminal(this ResourceViewModel resource)
{
return resource.Properties.ContainsKey(KnownProperties.Terminal.Enabled);
}
/// <summary>
/// Tries to get the per-replica terminal info: the stable replica index and the
/// total replica count for the parent resource. Both values are stamped onto
/// each replica snapshot by the AppHost when the resource has
/// <c>WithTerminal()</c> applied. The pair is sufficient for the dashboard to
/// build a <c>?resource=<name>&replica=<index></c> URL that the
/// terminal WebSocket proxy can resolve to a per-replica HMP v1 producer
/// socket without exposing the socket path to the browser.
/// </summary>
public static bool TryGetTerminalReplicaInfo(this ResourceViewModel resource, out int replicaIndex, out int replicaCount)
{
replicaIndex = 0;
replicaCount = 0;
if (!resource.TryGetCustomDataString(KnownProperties.Terminal.ReplicaIndex, out var indexString) ||
!int.TryParse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture, out replicaIndex))
{
return false;
}
if (!resource.TryGetCustomDataString(KnownProperties.Terminal.ReplicaCount, out var countString) ||
!int.TryParse(countString, NumberStyles.Integer, CultureInfo.InvariantCulture, out replicaCount))
{
return false;
}
return true;
}
/// <summary>
/// Tries to get the per-replica consumer UDS path stamped onto the snapshot
/// by the AppHost. Used by the dashboard's terminal WebSocket proxy to
/// connect a local HMP v1 client to the terminal host. Returns <c>false</c>
/// for resources that don't have a terminal or for snapshots from older
/// AppHost builds that don't stamp this property.
/// </summary>
public static bool TryGetTerminalConsumerUdsPath(this ResourceViewModel resource, [NotNullWhen(returnValue: true)] out string? consumerUdsPath)
{
return resource.TryGetCustomDataString(KnownProperties.Terminal.ConsumerUdsPath, out consumerUdsPath);
}
public static bool TryGetWaitingForDependencies(this ResourceViewModel resource, out ImmutableArray<string> dependencies)
{
return resource.TryGetCustomDataStringArray(KnownProperties.Resource.WaitingFor, out dependencies) && dependencies.Length > 0;
}
public static bool TryGetResolvedWaitingForDependencies(
this ResourceViewModel resource,
IEnumerable<ResourceViewModel> allResources,
out ImmutableArray<string> dependencies)
{
if (!resource.TryGetWaitingForDependencies(out var waitingForDependencies))
{
dependencies = default;
return false;
}
var builder = ImmutableArray.CreateBuilder<string>(waitingForDependencies.Length);
var seenDependencies = new HashSet<string>(StringComparers.ResourceName);
foreach (var dependency in waitingForDependencies)
{
var resolvedDependency = dependency;
var matchingResource = FindResourceByName(dependency, allResources);
if (matchingResource is null)
{
matchingResource = FindSingleResourceByDisplayName(dependency, allResources);
}
if (matchingResource is not null)
{
resolvedDependency = ResourceViewModel.GetResourceName(matchingResource, allResources);
}
if (seenDependencies.Add(resolvedDependency))
{
builder.Add(resolvedDependency);
}
}
dependencies = builder.ToImmutable();
return dependencies.Length > 0;
}
private static ResourceViewModel? FindResourceByName(string name, IEnumerable<ResourceViewModel> allResources)
{
foreach (var resource in allResources)
{
if (string.Equals(resource.Name, name, StringComparisons.ResourceName))
{
return resource;
}
}
return null;
}
private static ResourceViewModel? FindSingleResourceByDisplayName(string displayName, IEnumerable<ResourceViewModel> allResources)
{
ResourceViewModel? matchingResource = null;
var matchCount = 0;
foreach (var resource in allResources)
{
if (!string.Equals(resource.DisplayName, displayName, StringComparisons.ResourceName))
{
continue;
}
matchCount++;
if (matchCount > 1)
{
return null;
}
matchingResource = resource;
}
return matchCount == 1 ? matchingResource : null;
}
private static bool TryGetCustomDataString(this ResourceViewModel resource, string key, [NotNullWhen(returnValue: true)] out string? s)
{
if (resource.Properties.TryGetValue(key, out var property) && property.Value.TryConvertToString(out var valueString))
{
s = valueString;
return true;
}
s = null;
return false;
}
private static bool TryGetCustomDataStringArray(this ResourceViewModel resource, string key, out ImmutableArray<string> strings)
{
if (resource.Properties.TryGetValue(key, out var property) && property is { Value: { ListValue: not null } value })
{
var builder = ImmutableArray.CreateBuilder<string>(value.ListValue.Values.Count);
foreach (var element in value.ListValue.Values)
{
if (!element.TryConvertToString(out var elementString))
{
strings = default;
return false;
}
builder.Add(elementString);
}
strings = builder.MoveToImmutable();
return true;
}
strings = default;
return false;
}
private static bool TryGetCustomDataBoolArray(this ResourceViewModel resource, string key, out ImmutableArray<bool> bools)
{
if (resource.Properties.TryGetValue(key, out var property) && property is { Value: { ListValue: not null } value })
{
var builder = ImmutableArray.CreateBuilder<bool>(value.ListValue.Values.Count);
foreach (var element in value.ListValue.Values)
{
if (!element.HasNumberValue)
{
bools = default;
return false;
}
builder.Add(Convert.ToBoolean(element.NumberValue));
}
bools = builder.MoveToImmutable();
return true;
}
bools = default;
return false;
}
private static bool TryGetCustomDataInt(this ResourceViewModel resource, string key, out int i)
{
if (resource.Properties.TryGetValue(key, out var property) && property.Value.TryConvertToInt(out i))
{
return true;
}
i = 0;
return false;
}
}