using System; using System.Security.Principal; using System.Threading; using Serilog; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Galaxy; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Historian; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.MxAccess; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Ipc; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Sta; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host; /// /// Entry point for the OtOpcUaGalaxyHost Windows service / console host. Reads the /// pipe name, allowed-SID, and shared secret from environment (passed by the supervisor at /// spawn time per driver-stability.md). /// public static class Program { public static int Main(string[] args) { Log.Logger = new LoggerConfiguration() .MinimumLevel.Information() .WriteTo.File( @"%ProgramData%\OtOpcUa\galaxy-host-.log".Replace("%ProgramData%", Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData)), rollingInterval: RollingInterval.Day) .CreateLogger(); try { var pipeName = Environment.GetEnvironmentVariable("OTOPCUA_GALAXY_PIPE") ?? "OtOpcUaGalaxy"; 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_GALAXY_SECRET") ?? throw new InvalidOperationException("OTOPCUA_GALAXY_SECRET not set — supervisor must pass the per-process secret at spawn time"); var allowedSid = new SecurityIdentifier(allowedSidValue); using var server = new PipeServer(pipeName, allowedSid, sharedSecret, Log.Logger); using var cts = new CancellationTokenSource(); Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; Log.Information("OtOpcUaGalaxyHost starting — pipe={Pipe} allowedSid={Sid}", pipeName, allowedSidValue); // Backend selection — env var picks the implementation: // OTOPCUA_GALAXY_BACKEND=stub → StubGalaxyBackend (no Galaxy required) // OTOPCUA_GALAXY_BACKEND=db → DbBackedGalaxyBackend (Discover only, against ZB) // OTOPCUA_GALAXY_BACKEND=mxaccess → MxAccessGalaxyBackend (real COM + ZB; default) var backendKind = Environment.GetEnvironmentVariable("OTOPCUA_GALAXY_BACKEND")?.ToLowerInvariant() ?? "mxaccess"; var zbConn = Environment.GetEnvironmentVariable("OTOPCUA_GALAXY_ZB_CONN") ?? "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;"; var clientName = Environment.GetEnvironmentVariable("OTOPCUA_GALAXY_CLIENT_NAME") ?? "OtOpcUa-Galaxy.Host"; IGalaxyBackend backend; StaPump? pump = null; MxAccessClient? mx = null; switch (backendKind) { case "stub": backend = new StubGalaxyBackend(); break; case "db": backend = new DbBackedGalaxyBackend(new GalaxyRepository(new GalaxyRepositoryOptions { ConnectionString = zbConn })); break; default: // mxaccess pump = new StaPump("Galaxy.Sta"); pump.WaitForStartedAsync().GetAwaiter().GetResult(); mx = new MxAccessClient(pump, new MxProxyAdapter(), clientName); var historian = BuildHistorianIfEnabled(); backend = new MxAccessGalaxyBackend( new GalaxyRepository(new GalaxyRepositoryOptions { ConnectionString = zbConn }), mx, historian); break; } Log.Information("OtOpcUaGalaxyHost backend={Backend}", backendKind); var handler = new GalaxyFrameHandler(backend, Log.Logger); try { server.RunAsync(handler, cts.Token).GetAwaiter().GetResult(); } finally { (backend as IDisposable)?.Dispose(); mx?.Dispose(); pump?.Dispose(); } Log.Information("OtOpcUaGalaxyHost stopped cleanly"); return 0; } catch (Exception ex) { Log.Fatal(ex, "OtOpcUaGalaxyHost fatal"); return 2; } finally { Log.CloseAndFlush(); } } /// /// Builds a from the OTOPCUA_HISTORIAN_* environment /// variables the supervisor passes at spawn time. Returns null when the historian is /// disabled (default) so MxAccessGalaxyBackend.HistoryReadAsync returns a clear /// "not configured" error instead of attempting an SDK connection to localhost. /// private static IHistorianDataSource? BuildHistorianIfEnabled() { var enabled = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_ENABLED"); if (!string.Equals(enabled, "true", StringComparison.OrdinalIgnoreCase) && enabled != "1") return null; 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("Historian enabled — {NodeCount} configured 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; } }