File: Backchannel\TrayResourceHealth.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 Aspire.Hosting.ApplicationModel;
 
namespace Aspire.Cli.Backchannel;
 
/// <summary>
/// Aggregates visible resource state and health using the dashboard's lifetime conventions.
/// </summary>
internal static class TrayResourceHealth
{
    public static string? Aggregate(IEnumerable<ResourceSnapshot> resources)
    {
        string? aggregate = null;
        foreach (var resource in resources)
        {
            if (ResourceSnapshotMapper.IsHiddenResource(resource))
            {
                continue;
            }
 
            var health = GetHealth(resource);
            if (health == "unhealthy")
            {
                return health;
            }
            if (health == "warning" || aggregate is null)
            {
                aggregate = health;
            }
        }
        return aggregate;
    }
 
    private static string? GetHealth(ResourceSnapshot resource)
    {
        var completed = IsState(resource, KnownResourceStates.Finished) || IsState(resource, KnownResourceStates.Exited);
        var pendingCheck = resource.HealthReports.Any(report => report.Status is null);
        if (IsState(resource, KnownResourceStates.FailedToStart)
            || IsState(resource, KnownResourceStates.RuntimeUnhealthy)
            || (completed && resource.ExitCode is not null and not 0)
            || string.Equals(resource.StateStyle, "error", StringComparisons.ResourceState)
            || resource.HealthReports.Any(report => IsHealth(report.Status, "Unhealthy"))
            || (IsHealth(resource.HealthStatus, "Unhealthy") && !pendingCheck))
        {
            return "unhealthy";
        }
 
        // The AppHost reports aggregate Unhealthy until the first registered check returns.
        // For example: healthStatus: "Unhealthy", healthReports: [{ name: "ready", status: null }].
        // An actual failing report still takes precedence over another check that is pending.
        if (pendingCheck
            || string.Equals(resource.StateStyle, "warning", StringComparisons.ResourceState)
            || IsHealth(resource.HealthStatus, "Degraded")
            || resource.HealthReports.Any(report => IsHealth(report.Status, "Degraded")))
        {
            return "warning";
        }
 
        // The dashboard treats successful jobs and resources without a lifetime (such as
        // resolved parameters) as neutral, not services waiting to become Running.
        if (completed || ((string.IsNullOrEmpty(resource.State) || IsState(resource, KnownResourceStates.Active))
            && resource.HealthStatus is null && resource.HealthReports.Length == 0))
        {
            return null;
        }
 
        if (!IsState(resource, KnownResourceStates.Running))
        {
            return "warning";
        }
 
        // As in CustomResourceSnapshot.ComputeHealthStatus, Running with no registered
        // checks is healthy. Do not extend that assumption to pending or unknown reports.
        return (resource.HealthStatus is null || IsHealth(resource.HealthStatus, "Healthy"))
            && resource.HealthReports.All(report => IsHealth(report.Status, "Healthy"))
            ? "healthy"
            : "warning";
    }
 
    private static bool IsState(ResourceSnapshot resource, string state)
        => string.Equals(resource.State, state, StringComparisons.ResourceState);
 
    private static bool IsHealth(string? status, string expected)
        => string.Equals(status, expected, StringComparison.OrdinalIgnoreCase);
}