Task #154 — driver-diagnostics RPC: HTTP endpoint + Admin client
Foundation for surfacing per-driver runtime state from the Server process to the Admin UI. #152 shipped GetAutoProhibitedRanges() as an in-process accessor; #154 makes it reachable across processes. Server side (HealthEndpointsHost): - New URL family: /diagnostics/drivers/{driverInstanceId}/{driverType}/{topic} - First wired topic: /diagnostics/drivers/{id}/modbus/auto-prohibited - Driver-agnostic at the URL level — future driver types add their own segments[3] cases (e.g. /diagnostics/drivers/{id}/s7/dropped-pdus). - 404 when the driver instance doesn't exist; 400 when the driver exists but isn't a Modbus driver (the per-type endpoint is wrong for this row). - Response shape is flat JSON (unitId / region / startAddress / endAddress / lastProbedUtc / bisectionPending) so consumers don't have to reference the Driver.Modbus assembly's ModbusAutoProhibition record. - Re-uses the existing HttpListener bound to localhost:4841 — same auth / reachability story as /healthz and /readyz. Admin side: - DriverDiagnosticsClient (Services/) — HttpClient wrapper that fetches the per-driver Modbus prohibition list. Returns null on 404/400 (driver missing or wrong type); throws on transport failures. - ModbusAutoProhibitionsResponse + ModbusAutoProhibitionRow flat DTOs — client doesn't take a dep on Driver.Modbus. - ModbusDiagnostics.razor at /modbus/diagnostics/{driverInstanceId} — table view with BISECTING (warning yellow) / ISOLATED (danger red) badges, relative timestamps (e.g. "5m ago"), Refresh button. Errors surface inline rather than swallowing. - HttpClient registration in Program.cs reads DriverDiagnostics:ServerBaseUrl from appsettings.json (default http://localhost:4841/ for same-host deployments). Tests (3 new in HealthEndpointsHostTests): - Diagnostics_ReturnsModbusAutoProhibitions_ForLiveDriver — registers a Modbus driver with a programmable transport that protects register 102, records the prohibition via a coalesced ReadAsync, hits the endpoint, asserts the returned JSON matches (unitId / region / start / end / pending). - Diagnostics_404_When_Driver_Not_Found - Diagnostics_400_When_Driver_Is_Wrong_Type Architecture note: the Admin-side bUnit-style component test isn't included because Admin.Tests doesn't have bUnit set up. The DriverDiagnosticsClient is unit-testable on its own with a mock HandlerStub if needed — left as a follow-up alongside the broader bUnit setup task. The diagnostic page is now reachable at /modbus/diagnostics/{driverId} from any Admin instance pointing at a Server endpoint URL. Future driver types (S7, AbCip) plug into the same channel by adding their own URL segments in HealthEndpointsHost.WriteDriverDiagnosticsAsync.
This commit is contained in:
@@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Observability;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Modbus;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Server.Observability;
|
||||
|
||||
@@ -85,7 +86,14 @@ public sealed class HealthEndpointsHost : IAsyncDisposable
|
||||
await WriteReadyzAsync(ctx).ConfigureAwait(false);
|
||||
break;
|
||||
default:
|
||||
ctx.Response.StatusCode = 404;
|
||||
// #154 — driver-diagnostics path family. URL shape:
|
||||
// /diagnostics/drivers/{driverInstanceId}/modbus/auto-prohibited
|
||||
// Driver-agnostic at the URL level so future driver types (S7, AbCip,
|
||||
// FOCAS) can add their own per-type subpaths.
|
||||
if (path.StartsWith("/diagnostics/drivers/", StringComparison.Ordinal))
|
||||
await WriteDriverDiagnosticsAsync(ctx, path).ConfigureAwait(false);
|
||||
else
|
||||
ctx.Response.StatusCode = 404;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -157,6 +165,64 @@ public sealed class HealthEndpointsHost : IAsyncDisposable
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #154 — driver-diagnostics endpoint family. Routes
|
||||
/// <c>/diagnostics/drivers/{driverId}/modbus/auto-prohibited</c> to the live
|
||||
/// <see cref="ModbusDriver"/> instance's <see cref="ModbusDriver.GetAutoProhibitedRanges"/>.
|
||||
/// 404 when the driver instance doesn't exist; 400 when it exists but isn't a Modbus
|
||||
/// driver (the per-type endpoint is wrong for this row).
|
||||
/// </summary>
|
||||
private async Task WriteDriverDiagnosticsAsync(HttpListenerContext ctx, string path)
|
||||
{
|
||||
// Path shape: /diagnostics/drivers/{id}/modbus/auto-prohibited
|
||||
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (segments.Length < 4 || segments[0] != "diagnostics" || segments[1] != "drivers")
|
||||
{
|
||||
ctx.Response.StatusCode = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
var driverId = segments[2];
|
||||
var driver = _driverHost.GetDriver(driverId);
|
||||
if (driver is null)
|
||||
{
|
||||
ctx.Response.StatusCode = 404;
|
||||
await WriteBodyAsync(ctx, JsonSerializer.Serialize(new { error = $"Driver '{driverId}' not found" })).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Per-driver-type subpath dispatch. Today only Modbus is wired; future drivers add
|
||||
// their own segments[3] cases.
|
||||
if (segments.Length >= 5 && segments[3] == "modbus" && segments[4] == "auto-prohibited")
|
||||
{
|
||||
if (driver is not ModbusDriver modbus)
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await WriteBodyAsync(ctx, JsonSerializer.Serialize(new { error = $"Driver '{driverId}' is not a Modbus driver (type: {driver.DriverType})" })).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
var ranges = modbus.GetAutoProhibitedRanges();
|
||||
ctx.Response.StatusCode = 200;
|
||||
await WriteBodyAsync(ctx, JsonSerializer.Serialize(new
|
||||
{
|
||||
driverInstanceId = driverId,
|
||||
count = ranges.Count,
|
||||
ranges = ranges.Select(r => new
|
||||
{
|
||||
unitId = r.UnitId,
|
||||
region = r.Region.ToString(),
|
||||
startAddress = r.StartAddress,
|
||||
endAddress = r.EndAddress,
|
||||
lastProbedUtc = r.LastProbedUtc,
|
||||
bisectionPending = r.BisectionPending,
|
||||
}).ToArray(),
|
||||
})).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.Response.StatusCode = 404;
|
||||
}
|
||||
|
||||
private static async Task WriteBodyAsync(HttpListenerContext ctx, string body)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(body);
|
||||
|
||||
Reference in New Issue
Block a user