da141497f8
DriverHostActor.ApplyAndAck now reads the deployment artifact and reconciles its set of DriverInstanceActor children — spawn the missing, ApplyDelta to those with changed config, stop the removed/disabled. The diff lives in pure DriverSpawnPlanner so it can be unit-tested without an ActorSystem. Adds IDriverFactory in Core.Abstractions (consumed by Runtime) + DriverFactoryRegistryAdapter in Core.Hosting that wraps the existing v1 DriverFactoryRegistry — Runtime stays decoupled from Polly/Serilog, the Host wires the adapter once driver assemblies have registered. ShouldStub(type, roles) is now actually called on every spawn — Galaxy + Wonderware-Historian boot stubbed on macOS/Linux or whenever the host carries the dev role. Missing factory ⇒ stub fallback, never a crash. Tests: 24 → 34 in Runtime (+10): - DriverSpawnPlannerTests x7 (diff cases, type change ⇒ stop+respawn) - DeploymentArtifactTests x5 (empty/malformed/missing fields tolerant) - DriverHostActorReconcileTests x4 (spawn count, stub fallback, ShouldStub gate, second-apply stops the removed) All 6 v2 test suites green: 120 tests passing. Closes F20 (ShouldStub wired). F7 marked partial — subscription publishing + write path still stubbed in DriverInstanceActor itself.
79 lines
2.9 KiB
C#
79 lines
2.9 KiB
C#
using System.Text.Json;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
|
|
|
/// <summary>
|
|
/// Minimal driver-side view of the deployment artifact emitted by
|
|
/// <c>ConfigComposer.SnapshotAndFlattenAsync</c>. The artifact JSON is the full snapshot —
|
|
/// for driver spawning we only need the <c>DriverInstances</c> array. Reading just the
|
|
/// subset keeps allocations cheap on every deploy.
|
|
/// </summary>
|
|
public sealed record DriverInstanceSpec(
|
|
Guid DriverInstanceRowId,
|
|
string DriverInstanceId,
|
|
string Name,
|
|
string DriverType,
|
|
bool Enabled,
|
|
string DriverConfig);
|
|
|
|
public static class DeploymentArtifact
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
};
|
|
|
|
/// <summary>
|
|
/// Parse a deployment artifact blob into the list of driver-instance specs to spawn.
|
|
/// Empty / malformed blobs return an empty list — callers log + treat as "no drivers".
|
|
/// </summary>
|
|
public static IReadOnlyList<DriverInstanceSpec> ParseDriverInstances(ReadOnlySpan<byte> blob)
|
|
{
|
|
if (blob.IsEmpty) return Array.Empty<DriverInstanceSpec>();
|
|
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(blob.ToArray());
|
|
if (!doc.RootElement.TryGetProperty("DriverInstances", out var arr)
|
|
|| arr.ValueKind != JsonValueKind.Array)
|
|
{
|
|
return Array.Empty<DriverInstanceSpec>();
|
|
}
|
|
|
|
var result = new List<DriverInstanceSpec>(arr.GetArrayLength());
|
|
foreach (var el in arr.EnumerateArray())
|
|
{
|
|
if (el.ValueKind != JsonValueKind.Object) continue;
|
|
var spec = TryReadSpec(el);
|
|
if (spec is not null) result.Add(spec);
|
|
}
|
|
return result;
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
return Array.Empty<DriverInstanceSpec>();
|
|
}
|
|
}
|
|
|
|
private static DriverInstanceSpec? TryReadSpec(JsonElement el)
|
|
{
|
|
var rowId = el.TryGetProperty("DriverInstanceRowId", out var rowEl)
|
|
&& rowEl.TryGetGuid(out var rid) ? rid : Guid.Empty;
|
|
var id = el.TryGetProperty("DriverInstanceId", out var idEl) ? idEl.GetString() : null;
|
|
var name = el.TryGetProperty("Name", out var nameEl) ? nameEl.GetString() : null;
|
|
var type = el.TryGetProperty("DriverType", out var typeEl) ? typeEl.GetString() : null;
|
|
var enabled = !el.TryGetProperty("Enabled", out var enEl) || enEl.GetBoolean();
|
|
var config = el.TryGetProperty("DriverConfig", out var cfgEl) ? cfgEl.GetString() : null;
|
|
|
|
if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(type)) return null;
|
|
|
|
return new DriverInstanceSpec(
|
|
DriverInstanceRowId: rowId,
|
|
DriverInstanceId: id!,
|
|
Name: name ?? id!,
|
|
DriverType: type!,
|
|
Enabled: enabled,
|
|
DriverConfig: config ?? "{}");
|
|
}
|
|
}
|