File: Dcp\DcpStates.cs
Web Access
Project: src\src\Aspire.Hosting.Native.Server\Aspire.Hosting.Native.Server.csproj (Aspire.Hosting.Native.Server)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Text.Json;
using System.Text.Json.Serialization;
using Aspire.Hosting.Native.Runtime;
 
namespace Aspire.Hosting.Native.Dcp;
 
[JsonConverter(typeof(DcpProcessStateConverter))]
internal enum DcpProcessState
{
    Unknown, Pending, Building, Starting, Running, Paused, Stopping, Exited, Finished, Terminated, FailedToStart, RuntimeUnhealthy
}
 
[JsonConverter(typeof(DcpExecutionStateConverter))]
internal enum DcpExecutionState { Unknown, Running, Stopping, CleaningResources, CleanupComplete }
 
[JsonConverter(typeof(DcpResourceCleanupConverter))]
internal enum DcpResourceCleanup { Full, None }
 
internal enum DcpResourceCollection { Containers, Executables, Services }
 
[JsonConverter(typeof(DcpAddressAllocationModeConverter))]
internal enum DcpAddressAllocationMode { Proxyless, Localhost }
 
[JsonConverter(typeof(DcpExecutionTypeConverter))]
internal enum DcpExecutionType { Process }
 
internal sealed class DcpResourceCleanupConverter() : JsonStringEnumConverter<DcpResourceCleanup>(allowIntegerValues: false);
internal sealed class DcpAddressAllocationModeConverter() : JsonStringEnumConverter<DcpAddressAllocationMode>(allowIntegerValues: false);
internal sealed class DcpExecutionTypeConverter() : JsonStringEnumConverter<DcpExecutionType>(allowIntegerValues: false);
 
/// <summary>Separates DCP's extensible status vocabulary from native lifecycle decisions.</summary>
internal static class DcpStates
{
    public static string ToPath(this DcpResourceCollection collection) => collection switch
    {
        DcpResourceCollection.Containers => "containers",
        DcpResourceCollection.Executables => "executables",
        DcpResourceCollection.Services => "services",
        _ => throw new ArgumentOutOfRangeException(nameof(collection))
    };
 
    public static string ToKind(this DcpResourceCollection collection) => collection switch
    {
        DcpResourceCollection.Containers => "Container",
        DcpResourceCollection.Executables => "Executable",
        DcpResourceCollection.Services => "Service",
        _ => throw new ArgumentOutOfRangeException(nameof(collection))
    };
    // DCP replies use {"status":{"state":"Running","exitCode":0}}.
    // Missing/new labels are Unknown; malformed JSON field types are errors.
    public static DcpProcessState ParseProcess(string? value) => value switch
    {
        "Pending" => DcpProcessState.Pending,
        "Building" => DcpProcessState.Building,
        "Starting" => DcpProcessState.Starting,
        "Running" => DcpProcessState.Running,
        "Paused" => DcpProcessState.Paused,
        "Stopping" => DcpProcessState.Stopping,
        "Exited" => DcpProcessState.Exited,
        "Finished" => DcpProcessState.Finished,
        "Terminated" => DcpProcessState.Terminated,
        "FailedToStart" => DcpProcessState.FailedToStart,
        "RuntimeUnhealthy" => DcpProcessState.RuntimeUnhealthy,
        _ => DcpProcessState.Unknown
    };
 
    public static bool IsTerminal(this DcpProcessState state) =>
        state is DcpProcessState.Exited or DcpProcessState.Finished or DcpProcessState.Terminated or DcpProcessState.FailedToStart;
 
    public static WorkloadStatus Observe(DcpStatus? status) => status?.State switch
    {
        DcpProcessState.Running => new(WorkloadPhase.Running, status.ExitCode),
        DcpProcessState.Exited or DcpProcessState.Finished or DcpProcessState.Terminated => new(WorkloadPhase.Exited, status.ExitCode),
        DcpProcessState.FailedToStart => new(WorkloadPhase.Failed, status.ExitCode),
        DcpProcessState.Pending or DcpProcessState.Building or DcpProcessState.Starting or DcpProcessState.Paused or
            DcpProcessState.Stopping or DcpProcessState.RuntimeUnhealthy => new(WorkloadPhase.Pending, status.ExitCode),
        _ => throw new InvalidDataException("DCP did not report a recognized workload state.")
    };
 
    public static DcpExecutionState ParseExecution(string? value) => value switch
    {
        "Running" => DcpExecutionState.Running,
        "Stopping" => DcpExecutionState.Stopping,
        "CleaningResources" => DcpExecutionState.CleaningResources,
        "CleanupComplete" => DcpExecutionState.CleanupComplete,
        _ => DcpExecutionState.Unknown
    };
}
 
internal sealed class WorkloadProtocolConverter : JsonConverter<WorkloadProtocol>
{
    public override WorkloadProtocol Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
        RuntimeStates.ParseProtocol(reader.GetString() ?? throw new JsonException("A protocol is required."));
    public override void Write(Utf8JsonWriter writer, WorkloadProtocol value, JsonSerializerOptions options) =>
        writer.WriteStringValue(value.ToWire());
}
 
internal sealed class WorkloadMountKindConverter : JsonConverter<WorkloadMountKind>
{
    public override WorkloadMountKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
        RuntimeStates.ParseMountKind(reader.GetString() ?? throw new JsonException("A mount kind is required."));
    public override void Write(Utf8JsonWriter writer, WorkloadMountKind value, JsonSerializerOptions options) =>
        writer.WriteStringValue(value.ToWire());
}
 
internal sealed class DcpProcessStateConverter : JsonConverter<DcpProcessState>
{
    public override DcpProcessState Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType is not (JsonTokenType.String or JsonTokenType.Null))
        {
            throw new JsonException("DCP process state must be a string or null.");
        }
 
        return DcpStates.ParseProcess(reader.GetString());
    }
 
    public override void Write(Utf8JsonWriter writer, DcpProcessState value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value switch
        {
            DcpProcessState.Unknown => "Unknown",
            DcpProcessState.Pending => "Pending",
            DcpProcessState.Building => "Building",
            DcpProcessState.Starting => "Starting",
            DcpProcessState.Running => "Running",
            DcpProcessState.Paused => "Paused",
            DcpProcessState.Stopping => "Stopping",
            DcpProcessState.Exited => "Exited",
            DcpProcessState.Finished => "Finished",
            DcpProcessState.Terminated => "Terminated",
            DcpProcessState.FailedToStart => "FailedToStart",
            DcpProcessState.RuntimeUnhealthy => "RuntimeUnhealthy",
            _ => throw new ArgumentOutOfRangeException(nameof(value))
        });
    }
}
 
internal sealed class DcpExecutionStateConverter : JsonConverter<DcpExecutionState>
{
    public override DcpExecutionState Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType is not (JsonTokenType.String or JsonTokenType.Null))
        {
            throw new JsonException("DCP execution state must be a string or null.");
        }
 
        return DcpStates.ParseExecution(reader.GetString());
    }
 
    public override void Write(Utf8JsonWriter writer, DcpExecutionState value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value switch
        {
            DcpExecutionState.Unknown => "Unknown",
            DcpExecutionState.Running => "Running",
            DcpExecutionState.Stopping => "Stopping",
            DcpExecutionState.CleaningResources => "CleaningResources",
            DcpExecutionState.CleanupComplete => "CleanupComplete",
            _ => throw new ArgumentOutOfRangeException(nameof(value))
        });
    }
}