diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Program.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Program.cs
index db77d55d..cd2088b3 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Program.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Program.cs
@@ -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;
///
-/// 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 driver-stability.md). 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 driver-stability.md). Hosts a
+/// TCP server (optionally over TLS) dispatching the five sidecar contracts (PR 3.3) to
+/// the Wonderware Historian SDK.
///
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;
}
+ ///
+ /// Loads the TLS server certificate when TLS is enabled. The reference is either a
+ /// .pfx file path (decrypted with the optional password env var) or, if not a
+ /// file, a thumbprint resolved from the LocalMachine\My store.
+ ///
+ 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];
+ }
+
///
/// Constructs the alarm-event writer when the alarm-write toggle is on, otherwise
/// returns null so falls back to the
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/ProgramSmokeTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/ProgramSmokeTests.cs
index e6647b0f..0ba6059d 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/ProgramSmokeTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/ProgramSmokeTests.cs
@@ -6,8 +6,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests;
///
/// 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
+/// (TcpFrameServer); here we just verify the assembly identity is what the
/// csproj declares.
///
public class ProgramSmokeTests