f40409a688
Tracking IDs are review bookkeeping that rots in shipped source; the rationale they tagged is kept. Comment-only change (CommentChecker 458 -> 16 false positives).
137 lines
5.8 KiB
C#
137 lines
5.8 KiB
C#
using Microsoft.AspNetCore.SignalR;
|
|
using Microsoft.Extensions.Options;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
using ZB.MOM.WW.MxGateway.Server.Alarms;
|
|
using ZB.MOM.WW.MxGateway.Server.Configuration;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs;
|
|
|
|
/// <summary>
|
|
/// Background service that subscribes to
|
|
/// <see cref="IGatewayAlarmService.StreamAsync"/> (no filter) and re-broadcasts
|
|
/// every <see cref="AlarmFeedMessage"/> to every <see cref="AlarmsHub"/>
|
|
/// client. The hub itself is session-less; clients filter / route messages
|
|
/// in the browser.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When <c>MxGateway:Dashboard:ShowTagValues</c> is false (the default), the
|
|
/// alarm value fields are stripped from a redacted copy before the message
|
|
/// reaches any browser client — the same rule
|
|
/// <see cref="DashboardEventBroadcaster"/> applies to the events-hub mirror, so
|
|
/// the two SignalR seams cannot disagree about whether values leave the gateway.
|
|
/// The source message is never mutated: it fans out from one feed to the gRPC
|
|
/// <c>StreamAlarms</c> subscribers and the alarms page as well, and none of
|
|
/// those audiences is subject to this dashboard-display flag.
|
|
/// <para>
|
|
/// The broadcast stays deliberately unfiltered. Audience is decided once, at hub
|
|
/// admission — <see cref="AlarmsHub"/> carries
|
|
/// <see cref="DashboardAuthenticationDefaults.AlarmsPolicy"/>, so only an authenticated caller
|
|
/// with a real dashboard role is ever in <see cref="AlarmsHub.AllAlarmsGroup"/> — and the alarm
|
|
/// monitor is gateway-central with no per-session provenance, so there is no per-connection
|
|
/// narrowing this loop could apply even in principle. Adding a filter here would imply a
|
|
/// per-caller scope the feed does not carry.
|
|
/// </para>
|
|
/// </remarks>
|
|
/// <param name="alarmService">The gateway's central alarm feed.</param>
|
|
/// <param name="hubContext">Hub context used to broadcast to the alarms group.</param>
|
|
/// <param name="options">Gateway options supplying <c>Dashboard:ShowTagValues</c>.</param>
|
|
/// <param name="logger">Logger for best-effort broadcast failures.</param>
|
|
public sealed class AlarmsHubPublisher(
|
|
IGatewayAlarmService alarmService,
|
|
IHubContext<AlarmsHub> hubContext,
|
|
IOptions<GatewayOptions> options,
|
|
ILogger<AlarmsHubPublisher> logger) : BackgroundService
|
|
{
|
|
private readonly bool _showTagValues = options.Value.Dashboard.ShowTagValues;
|
|
|
|
/// <inheritdoc />
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
// Loop forever — when StreamAsync completes (monitor restart, etc.)
|
|
// reopen the subscription. The hosted-service lifetime ends only
|
|
// when the host stops.
|
|
while (!stoppingToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
await foreach (AlarmFeedMessage message in alarmService
|
|
.StreamAsync(alarmFilterPrefix: null, stoppingToken)
|
|
.ConfigureAwait(false))
|
|
{
|
|
if (stoppingToken.IsCancellationRequested)
|
|
{
|
|
break;
|
|
}
|
|
|
|
try
|
|
{
|
|
AlarmFeedMessage outbound = _showTagValues ? message : RedactValues(message);
|
|
await hubContext.Clients
|
|
.Group(AlarmsHub.AllAlarmsGroup)
|
|
.SendAsync(AlarmsHub.AlarmMessage, outbound, stoppingToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
|
{
|
|
logger.LogWarning(ex, "Alarm broadcast failed; continuing.");
|
|
}
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
return;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "Alarm subscription faulted; reconnecting in 5s.");
|
|
try
|
|
{
|
|
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken).ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Produces a copy of <paramref name="source"/> with the alarm value fields
|
|
/// cleared, leaving every other field — reference, severity, state, category,
|
|
/// operator, timestamps — intact so the alarms page still renders the row.
|
|
/// </summary>
|
|
/// <param name="source">The message as the alarm feed produced it.</param>
|
|
/// <returns>
|
|
/// A redacted deep clone for the two value-bearing payload arms; the source
|
|
/// instance itself for every other arm, which carries no value to strip.
|
|
/// New payload arms therefore pass through unchanged by default — the switch
|
|
/// names only the arms that have something to redact, so adding a valueless
|
|
/// arm to the contract needs no change here.
|
|
/// </returns>
|
|
private static AlarmFeedMessage RedactValues(AlarmFeedMessage source)
|
|
{
|
|
switch (source.PayloadCase)
|
|
{
|
|
case AlarmFeedMessage.PayloadOneofCase.Transition:
|
|
{
|
|
AlarmFeedMessage redacted = source.Clone();
|
|
redacted.Transition.CurrentValue = null;
|
|
redacted.Transition.LimitValue = null;
|
|
return redacted;
|
|
}
|
|
|
|
case AlarmFeedMessage.PayloadOneofCase.ActiveAlarm:
|
|
{
|
|
AlarmFeedMessage redacted = source.Clone();
|
|
redacted.ActiveAlarm.CurrentValue = null;
|
|
redacted.ActiveAlarm.LimitValue = null;
|
|
return redacted;
|
|
}
|
|
|
|
default:
|
|
return source;
|
|
}
|
|
}
|
|
}
|