feat(historian-sidecar): TCP bootstrap + env, drop allowed-SID
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Security.Principal;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using Serilog;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend;
|
||||
@@ -8,10 +9,11 @@ using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Ipc;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware;
|
||||
|
||||
/// <summary>
|
||||
/// Entry point for the Wonderware Historian sidecar. Reads pipe name, allowed-SID,
|
||||
/// shared secret, and historian connection config from environment (the supervisor
|
||||
/// passes them at spawn time per <c>driver-stability.md</c>). Hosts a named-pipe server
|
||||
/// dispatching the five sidecar contracts (PR 3.3) to the Wonderware Historian SDK.
|
||||
/// Entry point for the Wonderware Historian sidecar. Reads the shared secret, TCP
|
||||
/// bind/port, optional TLS settings, and historian connection config from environment
|
||||
/// (the supervisor passes them at spawn time per <c>driver-stability.md</c>). Hosts a
|
||||
/// TCP server (optionally over TLS) dispatching the five sidecar contracts (PR 3.3) to
|
||||
/// the Wonderware Historian SDK.
|
||||
/// </summary>
|
||||
public static class Program
|
||||
{
|
||||
@@ -29,19 +31,19 @@ public static class Program
|
||||
|
||||
try
|
||||
{
|
||||
var pipeName = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_PIPE")
|
||||
?? throw new InvalidOperationException("OTOPCUA_HISTORIAN_PIPE not set — supervisor must pass the sidecar pipe name");
|
||||
var allowedSidValue = Environment.GetEnvironmentVariable("OTOPCUA_ALLOWED_SID")
|
||||
?? throw new InvalidOperationException("OTOPCUA_ALLOWED_SID not set — supervisor must pass the server principal SID");
|
||||
var sharedSecret = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_SECRET")
|
||||
?? throw new InvalidOperationException("OTOPCUA_HISTORIAN_SECRET not set — supervisor must pass the per-process secret at spawn time");
|
||||
|
||||
var allowedSid = new SecurityIdentifier(allowedSidValue);
|
||||
var tcpPort = TryParseInt("OTOPCUA_HISTORIAN_TCP_PORT", 32569);
|
||||
var bindRaw = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_BIND");
|
||||
var bind = string.IsNullOrWhiteSpace(bindRaw) ? IPAddress.Any : IPAddress.Parse(bindRaw);
|
||||
var tlsEnabled = string.Equals(Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_TLS_ENABLED"), "true", StringComparison.OrdinalIgnoreCase);
|
||||
X509Certificate2? tlsCert = tlsEnabled ? LoadTlsCert() : null;
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
|
||||
|
||||
// Sidecar can boot in "pipe-only" mode (no real Wonderware Historian SDK
|
||||
// Sidecar can boot in "tcp idle" mode (no real Wonderware Historian SDK
|
||||
// initialization) for smoke + IPC tests. Production sets ENABLED=true so the
|
||||
// SDK opens its connection up front.
|
||||
var historianEnabled = string.Equals(
|
||||
@@ -50,7 +52,7 @@ public static class Program
|
||||
|
||||
if (!historianEnabled)
|
||||
{
|
||||
Log.Information("Wonderware historian sidecar starting in pipe-only mode (OTOPCUA_HISTORIAN_ENABLED!=true) — pipe={Pipe} allowedSid={Sid}", pipeName, allowedSidValue);
|
||||
Log.Information("Wonderware historian sidecar starting in tcp idle mode (SDK disabled) (OTOPCUA_HISTORIAN_ENABLED!=true) — bind={Bind} port={Port} tls={Tls}", bind, tcpPort, tlsCert is not null);
|
||||
cts.Token.WaitHandle.WaitOne();
|
||||
Log.Information("Wonderware historian sidecar stopping cleanly");
|
||||
return 0;
|
||||
@@ -59,9 +61,9 @@ public static class Program
|
||||
using var historian = BuildHistorian();
|
||||
var alarmWriter = BuildAlarmWriter();
|
||||
var handler = new HistorianFrameHandler(historian, Log.Logger, alarmWriter);
|
||||
using var server = new PipeServer(pipeName, allowedSid, sharedSecret, Log.Logger);
|
||||
using var server = new TcpFrameServer(bind, tcpPort, sharedSecret, tlsCert, Log.Logger);
|
||||
|
||||
Log.Information("Wonderware historian sidecar serving — pipe={Pipe} allowedSid={Sid}", pipeName, allowedSidValue);
|
||||
Log.Information("Wonderware historian sidecar serving — bind={Bind} port={Port} tls={Tls}", bind, tcpPort, tlsCert is not null);
|
||||
try { server.RunAsync(handler, cts.Token).GetAwaiter().GetResult(); }
|
||||
catch (OperationCanceledException) { /* clean shutdown via Ctrl-C */ }
|
||||
|
||||
@@ -112,6 +114,26 @@ public static class Program
|
||||
return int.TryParse(raw, out var parsed) ? parsed : defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the TLS server certificate when TLS is enabled. The reference is either a
|
||||
/// <c>.pfx</c> file path (decrypted with the optional password env var) or, if not a
|
||||
/// file, a thumbprint resolved from the <c>LocalMachine\My</c> store.
|
||||
/// </summary>
|
||||
private static X509Certificate2 LoadTlsCert()
|
||||
{
|
||||
var certRef = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_TLS_CERT")
|
||||
?? throw new InvalidOperationException("OTOPCUA_HISTORIAN_TLS_CERT not set but TLS enabled — supply a .pfx path or a LocalMachine\\My store thumbprint");
|
||||
var pwd = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_TLS_CERT_PASSWORD");
|
||||
if (System.IO.File.Exists(certRef))
|
||||
return new X509Certificate2(certRef, pwd, X509KeyStorageFlags.MachineKeySet);
|
||||
// else treat as a thumbprint in LocalMachine\My
|
||||
using var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
|
||||
store.Open(OpenFlags.ReadOnly);
|
||||
var found = store.Certificates.Find(X509FindType.FindByThumbprint, certRef.Replace(" ", ""), validOnly: false);
|
||||
if (found.Count == 0) throw new InvalidOperationException($"OTOPCUA_HISTORIAN_TLS_CERT thumbprint '{certRef}' not found in LocalMachine\\My and is not a file path");
|
||||
return found[0];
|
||||
}
|
||||
|
||||
/// <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
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Smoke test confirming the sidecar project links and the test project resolves a
|
||||
/// ProjectReference to it. Real behavioural tests arrive in PR 3.2 (backend lift) and
|
||||
/// PR 3.3 (pipe server). For PR 3.1 we just verify the assembly identity is what the
|
||||
/// ProjectReference to it. Real behavioural tests live with the TCP frame server
|
||||
/// (<c>TcpFrameServer</c>); here we just verify the assembly identity is what the
|
||||
/// csproj declares.
|
||||
/// </summary>
|
||||
public class ProgramSmokeTests
|
||||
|
||||
Reference in New Issue
Block a user