sidecar: wire IAlarmEventWriter into Program.cs (PR C.2)
Fifth PR of the alarms-over-gateway epic (docs/plans/alarms-over-gateway.md). Depends on PR C.1 (AahClientManagedAlarmEventWriter), already merged. Today HistorianFrameHandler is constructed at Program.cs line 57 without an alarmWriter, so every WriteAlarmEvents frame replies "Sidecar not configured with an alarm-event writer" and the lmxopcua side keeps the row queued. C.2 wires a real writer behind a new OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED toggle. - Program.BuildAlarmWriter — gated on the env var (default true, fail-open under accidental misconfiguration). Constructs an AahClientManagedAlarmEventWriter wrapping a SdkAlarmHistorianWriteBackend with the same connection config the read path uses. - Install-Services.ps1 — appends OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED=true to the OtOpcUaWonderwareHistorian service env block when the sidecar is installed. Read-only deployments flip it to false at service-config edit time without re-installing. - HistorianFrameHandler signature already accepts IAlarmEventWriter? — supplying non-null at line 57 lights up the WriteAlarmEvents reply path that's been dormant since PR 3.3. Until PR D.1 pins the live aahClientManaged entry point, the SdkAlarmHistorianWriteBackend reports RetryPlease for every event with a structured diagnostic. The lmxopcua-side SqliteStoreAndForwardSink retains queued events; same effective behaviour as today's NullAlarmHistorianSink fallback but with visible diagnostics rather than silent discard. Tests: - 6 BuildAlarmWriter env-var cases — unset / true / false / unrecognized → default-on / capitalization variants. - Full sidecar test suite: 56 passed (was 48; 8 new). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -87,6 +87,10 @@ if ($InstallWonderwareHistorian) {
|
||||
"OTOPCUA_ALLOWED_SID=$sid"
|
||||
"OTOPCUA_HISTORIAN_SECRET=$HistorianSharedSecret"
|
||||
"OTOPCUA_HISTORIAN_ENABLED=true"
|
||||
# Default-on when the historian sidecar is installed; flip to false for a
|
||||
# read-only deployment that still loads aahClientManaged for reads but
|
||||
# rejects WriteAlarmEvents frames.
|
||||
"OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED=true"
|
||||
"OTOPCUA_HISTORIAN_SERVER=$HistorianServer"
|
||||
"OTOPCUA_HISTORIAN_PORT=$HistorianPort"
|
||||
) -join "`0"
|
||||
|
||||
@@ -54,7 +54,8 @@ public static class Program
|
||||
}
|
||||
|
||||
using var historian = BuildHistorian();
|
||||
var handler = new HistorianFrameHandler(historian, Log.Logger);
|
||||
var alarmWriter = BuildAlarmWriter();
|
||||
var handler = new HistorianFrameHandler(historian, Log.Logger, alarmWriter);
|
||||
using var server = new PipeServer(pipeName, allowedSid, sharedSecret, Log.Logger);
|
||||
|
||||
Log.Information("Wonderware historian sidecar serving — pipe={Pipe} allowedSid={Sid}", pipeName, allowedSidValue);
|
||||
@@ -107,4 +108,46 @@ public static class Program
|
||||
var raw = Environment.GetEnvironmentVariable(envName);
|
||||
return int.TryParse(raw, out var parsed) ? parsed : defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs the alarm-event writer when the alarm-write toggle is on, otherwise
|
||||
/// returns <c>null</c> so <see cref="HistorianFrameHandler"/> falls back to the
|
||||
/// "not configured" reply for any incoming <c>WriteAlarmEvents</c> frame.
|
||||
/// Default is <c>true</c> when <c>OTOPCUA_HISTORIAN_ENABLED=true</c>; explicitly
|
||||
/// set <c>OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED=false</c> to keep a read-only
|
||||
/// deployment that still loads the SDK for reads.
|
||||
/// </summary>
|
||||
internal static IAlarmEventWriter? BuildAlarmWriter()
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED");
|
||||
var enabled = string.IsNullOrWhiteSpace(raw)
|
||||
? true
|
||||
: !string.Equals(raw, "false", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
Log.Information("Alarm-event writer disabled (OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED=false); historian sidecar will reject WriteAlarmEvents frames.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var cfg = BuildAlarmWriterConfig();
|
||||
var backend = new SdkAlarmHistorianWriteBackend(cfg);
|
||||
Log.Information("Alarm-event writer enabled — backend=SdkAlarmHistorianWriteBackend server={Server}", cfg.ServerName);
|
||||
return new AahClientManagedAlarmEventWriter(backend);
|
||||
}
|
||||
|
||||
private static HistorianConfiguration BuildAlarmWriterConfig()
|
||||
{
|
||||
return new HistorianConfiguration
|
||||
{
|
||||
Enabled = true,
|
||||
ServerName = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_SERVER") ?? "localhost",
|
||||
Port = TryParseInt("OTOPCUA_HISTORIAN_PORT", 32568),
|
||||
IntegratedSecurity = !string.Equals(Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_INTEGRATED"), "false", StringComparison.OrdinalIgnoreCase),
|
||||
UserName = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_USER"),
|
||||
Password = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_PASS"),
|
||||
CommandTimeoutSeconds = TryParseInt("OTOPCUA_HISTORIAN_TIMEOUT_SEC", 30),
|
||||
FailureCooldownSeconds = TryParseInt("OTOPCUA_HISTORIAN_COOLDOWN_SEC", 60),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// PR C.2 — pins the env-var contract that gates whether the sidecar boots an
|
||||
/// alarm-event writer. Default-on (when the historian itself is enabled) so a
|
||||
/// fresh deploy picks up the writer without a service-config edit; explicit
|
||||
/// <c>false</c> opts a read-only deployment out.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class ProgramAlarmWriterTests
|
||||
{
|
||||
[Fact]
|
||||
public void BuildAlarmWriter_returns_writer_when_env_unset()
|
||||
{
|
||||
using var _ = ScopedEnv("OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED", null);
|
||||
|
||||
var writer = Program.BuildAlarmWriter();
|
||||
|
||||
writer.ShouldNotBeNull();
|
||||
writer.ShouldBeOfType<AahClientManagedAlarmEventWriter>();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("true")]
|
||||
[InlineData("True")]
|
||||
[InlineData("TRUE")]
|
||||
public void BuildAlarmWriter_returns_writer_when_env_explicitly_true(string value)
|
||||
{
|
||||
using var _ = ScopedEnv("OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED", value);
|
||||
|
||||
var writer = Program.BuildAlarmWriter();
|
||||
|
||||
writer.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("false")]
|
||||
[InlineData("False")]
|
||||
[InlineData("FALSE")]
|
||||
public void BuildAlarmWriter_returns_null_when_env_false(string value)
|
||||
{
|
||||
using var _ = ScopedEnv("OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED", value);
|
||||
|
||||
var writer = Program.BuildAlarmWriter();
|
||||
|
||||
writer.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildAlarmWriter_treats_unrecognized_value_as_enabled()
|
||||
{
|
||||
// Anything other than the literal "false" (case-insensitive) keeps the writer
|
||||
// wired — fail-open under accidental misconfiguration so an alarm-write deploy
|
||||
// doesn't silently lose alarms because of a typo.
|
||||
using var _ = ScopedEnv("OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED", "yes");
|
||||
|
||||
var writer = Program.BuildAlarmWriter();
|
||||
|
||||
writer.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
private static IDisposable ScopedEnv(string name, string? value)
|
||||
{
|
||||
var prior = Environment.GetEnvironmentVariable(name);
|
||||
Environment.SetEnvironmentVariable(name, value);
|
||||
return new DisposableAction(() => Environment.SetEnvironmentVariable(name, prior));
|
||||
}
|
||||
|
||||
private sealed class DisposableAction : IDisposable
|
||||
{
|
||||
private readonly Action _action;
|
||||
public DisposableAction(Action action) { _action = action; }
|
||||
public void Dispose() => _action();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user