zcbot/windows-node/Zcbot.WindowsNode/NodeAdapters.cs

94 lines
3.8 KiB
C#

using System.Text.Json;
namespace Zcbot.WindowsNode;
internal interface INodeAdapter
{
string Capability { get; }
string DisplayName { get; }
string AdapterVersion { get; }
IReadOnlyList<string> Features { get; }
bool HasActiveJobs { get; }
string RunningDetail { get; }
AdapterRuntimeStatus DetectRuntime();
bool ValidateRequest(JsonElement request);
Task RunAsync(RecoverableJob job);
void Cancel(Guid jobId);
}
internal sealed class OriginPlotNodeAdapter(JobInboxStore inbox) : INodeAdapter
{
internal const string CurrentAdapterVersion = "0.4.0";
internal const string ContractFilename = "origin.plot.v2.json";
private static readonly NodeAdapterContract Contract =
NodeAdapterContract.Load(ContractFilename, CurrentAdapterVersion);
private readonly OriginWorkerRunner runner = new(inbox);
internal static string CapabilityName => Contract.Capability;
public string Capability => Contract.Capability;
public string DisplayName => Contract.DisplayName;
public string AdapterVersion => CurrentAdapterVersion;
public IReadOnlyList<string> Features => Contract.Features;
public bool HasActiveJobs => runner.HasActiveJobs;
public string RunningDetail => "Origin 正在生成图形";
public AdapterRuntimeStatus DetectRuntime() => OriginRuntimeProbe.Detect();
public bool ValidateRequest(JsonElement request) =>
JobInboxStore.IsValidOriginRequest(request, Features);
public Task RunAsync(RecoverableJob job) => runner.RunAsync(job);
public void Cancel(Guid jobId) => runner.Cancel(jobId);
}
internal sealed record NodeAdapterContract(
string Capability, string DisplayName, IReadOnlyList<string> Features)
{
internal static NodeAdapterContract Load(string filename, string adapterVersion)
{
var path = Path.GetFullPath(
Path.Combine(AppContext.BaseDirectory, "software-contracts", filename));
using var document = JsonDocument.Parse(File.ReadAllBytes(path));
var root = document.RootElement;
var capability = root.GetProperty("capability").GetString()
?? throw new InvalidDataException("Adapter contract capability is missing.");
var displayName = root.GetProperty("display_name").GetString()
?? throw new InvalidDataException("Adapter contract display name is missing.");
var current = ParseVersion(adapterVersion);
var features = root.GetProperty("features").EnumerateObject()
.Where(item => ParseVersion(item.Value.GetString() ?? "0.0.0") <= current)
.Select(item => item.Name)
.ToArray();
return new NodeAdapterContract(capability, displayName, features);
}
private static Version ParseVersion(string value) =>
Version.TryParse(value, out var version) ? version : new Version(0, 0, 0);
}
internal sealed class NodeAdapterRegistry
{
private readonly IReadOnlyDictionary<string, INodeAdapter> adapters;
internal NodeAdapterRegistry(IEnumerable<INodeAdapter> values)
{
adapters = values.ToDictionary(item => item.Capability, StringComparer.Ordinal);
}
internal static NodeAdapterRegistry CreateDefault(JobInboxStore inbox) =>
new([new OriginPlotNodeAdapter(inbox)]);
internal static IReadOnlyList<NodeAdapterContract> InstalledContracts { get; } =
[
NodeAdapterContract.Load(
OriginPlotNodeAdapter.ContractFilename,
OriginPlotNodeAdapter.CurrentAdapterVersion),
];
internal static IReadOnlyList<string> InstalledCapabilities { get; } =
InstalledContracts.Select(item => item.Capability).ToArray();
internal IReadOnlyCollection<INodeAdapter> All => adapters.Values.ToArray();
internal INodeAdapter? Find(string capability) =>
adapters.TryGetValue(capability, out var adapter) ? adapter : null;
}