using System; using System.Security.Principal; using System.Threading; using Serilog; using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend; 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. /// public static class Program { public static int Main(string[] args) { Log.Logger = new LoggerConfiguration() .MinimumLevel.Information() .WriteTo.File( @"%ProgramData%\OtOpcUa\historian-wonderware-.log".Replace("%ProgramData%", Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData)), rollingInterval: RollingInterval.Day) .CreateLogger(); 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); 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 // initialization) for smoke + IPC tests. Production sets ENABLED=true so the // SDK opens its connection up front. var historianEnabled = string.Equals( Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_ENABLED"), "true", StringComparison.OrdinalIgnoreCase); if (!historianEnabled) { Log.Information("Wonderware historian sidecar starting in pipe-only mode (OTOPCUA_HISTORIAN_ENABLED!=true) — pipe={Pipe} allowedSid={Sid}", pipeName, allowedSidValue); cts.Token.WaitHandle.WaitOne(); Log.Information("Wonderware historian sidecar stopping cleanly"); return 0; } using var historian = BuildHistorian(); var handler = new HistorianFrameHandler(historian, Log.Logger); using var server = new PipeServer(pipeName, allowedSid, sharedSecret, Log.Logger); Log.Information("Wonderware historian sidecar serving — pipe={Pipe} allowedSid={Sid}", pipeName, allowedSidValue); try { server.RunAsync(handler, cts.Token).GetAwaiter().GetResult(); } catch (OperationCanceledException) { /* clean shutdown via Ctrl-C */ } Log.Information("Wonderware historian sidecar stopped cleanly"); return 0; } catch (Exception ex) { Log.Fatal(ex, "Wonderware historian sidecar fatal"); return 2; } finally { Log.CloseAndFlush(); } } /// /// Builds the Wonderware Historian data source from environment variables. Mirrors /// the env-var contract that Driver.Galaxy.Host used in v1; PR 3.W reaffirms /// this contract in install scripts. /// private static HistorianDataSource BuildHistorian() { var cfg = 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), MaxValuesPerRead = TryParseInt("OTOPCUA_HISTORIAN_MAX_VALUES", 10000), FailureCooldownSeconds = TryParseInt("OTOPCUA_HISTORIAN_COOLDOWN_SEC", 60), }; var servers = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_SERVERS"); if (!string.IsNullOrWhiteSpace(servers)) cfg.ServerNames = new System.Collections.Generic.List( servers.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)); Log.Information("Sidecar Historian config — {NodeCount} node(s), port={Port}", cfg.ServerNames.Count > 0 ? cfg.ServerNames.Count : 1, cfg.Port); return new HistorianDataSource(cfg); } private static int TryParseInt(string envName, int defaultValue) { var raw = Environment.GetEnvironmentVariable(envName); return int.TryParse(raw, out var parsed) ? parsed : defaultValue; } }