using System; using System.Threading; using Grpc.Core; using Grpc.Core.Interceptors; using Serilog; using ZB.MOM.WW.LmxProxy.Host.Configuration; using ZB.MOM.WW.LmxProxy.Host.Grpc.Services; using ZB.MOM.WW.LmxProxy.Host.MxAccess; using ZB.MOM.WW.LmxProxy.Host.Security; using ZB.MOM.WW.LmxProxy.Host.Health; using ZB.MOM.WW.LmxProxy.Host.Metrics; using ZB.MOM.WW.LmxProxy.Host.Sessions; using ZB.MOM.WW.LmxProxy.Host.Status; using ZB.MOM.WW.LmxProxy.Host.Subscriptions; namespace ZB.MOM.WW.LmxProxy.Host { /// /// Service lifecycle manager. Created by Topshelf, handles Start/Stop/Pause/Continue. /// public class LmxProxyService { private static readonly ILogger Log = Serilog.Log.ForContext(); private readonly LmxProxyConfiguration _config; private MxAccessClient? _mxAccessClient; private SessionManager? _sessionManager; private SubscriptionManager? _subscriptionManager; private ApiKeyService? _apiKeyService; private PerformanceMetrics? _performanceMetrics; private HealthCheckService? _healthCheckService; private StatusReportService? _statusReportService; private StatusWebServer? _statusWebServer; private Server? _grpcServer; public LmxProxyService(LmxProxyConfiguration config) { _config = config; } /// /// Topshelf Start callback. Creates and starts all components. /// public bool Start() { try { Log.Information("LmxProxy service starting..."); // 1. Validate configuration ConfigurationValidator.ValidateAndLog(_config); // 2. Check/generate TLS certificates var credentials = TlsCertificateManager.CreateServerCredentials(_config.Tls); // 3. Create ApiKeyService _apiKeyService = new ApiKeyService(_config.ApiKeyConfigFile); // 4. Create MxAccessClient _mxAccessClient = new MxAccessClient( maxConcurrentOperations: _config.Connection.MaxConcurrentOperations, readTimeoutSeconds: _config.Connection.ReadTimeoutSeconds, writeTimeoutSeconds: _config.Connection.WriteTimeoutSeconds, monitorIntervalSeconds: _config.Connection.MonitorIntervalSeconds, autoReconnect: _config.Connection.AutoReconnect, nodeName: _config.Connection.NodeName, galaxyName: _config.Connection.GalaxyName, probeTestTagAddress: _config.HealthCheck.TestTagAddress, probeStaleThresholdMs: _config.HealthCheck.ProbeStaleThresholdMs, clientName: _config.ClientName); // 5. Connect to MxAccess synchronously (with timeout) Log.Information("Connecting to MxAccess (timeout: {Timeout}s)...", _config.Connection.ConnectionTimeoutSeconds); using (var cts = new CancellationTokenSource( TimeSpan.FromSeconds(_config.Connection.ConnectionTimeoutSeconds))) { _mxAccessClient.ConnectAsync(cts.Token).GetAwaiter().GetResult(); } // 6. Start auto-reconnect monitor _mxAccessClient.StartMonitorLoop(); // 7. Create SubscriptionManager var channelFullMode = System.Threading.Channels.BoundedChannelFullMode.DropOldest; if (_config.Subscription.ChannelFullMode.Equals("DropNewest", StringComparison.OrdinalIgnoreCase)) channelFullMode = System.Threading.Channels.BoundedChannelFullMode.DropNewest; else if (_config.Subscription.ChannelFullMode.Equals("Wait", StringComparison.OrdinalIgnoreCase)) channelFullMode = System.Threading.Channels.BoundedChannelFullMode.Wait; _subscriptionManager = new SubscriptionManager( _mxAccessClient, _config.Subscription.ChannelCapacity, channelFullMode); // Wire MxAccessClient data change events to SubscriptionManager _mxAccessClient.OnTagValueChanged = _subscriptionManager.OnTagValueChanged; // Wire MxAccessClient disconnect to SubscriptionManager _mxAccessClient.ConnectionStateChanged += (sender, e) => { if (e.CurrentState == Domain.ConnectionState.Disconnected || e.CurrentState == Domain.ConnectionState.Error) { _subscriptionManager.NotifyDisconnection(); } else if (e.CurrentState == Domain.ConnectionState.Connected && e.PreviousState == Domain.ConnectionState.Reconnecting) { _subscriptionManager.NotifyReconnection(); } }; // 8. Create SessionManager _sessionManager = new SessionManager(inactivityTimeoutMinutes: 5); _sessionManager.OnSessionScavenged(sessionId => { Log.Information("Cleaning up subscriptions for scavenged session {SessionId}", sessionId); _subscriptionManager.UnsubscribeClient(sessionId); }); // 9. Create performance metrics _performanceMetrics = new PerformanceMetrics(); // 10. Create health check services _healthCheckService = new HealthCheckService(_mxAccessClient, _subscriptionManager, _performanceMetrics); // 11. Create status report service _statusReportService = new StatusReportService( _mxAccessClient, _subscriptionManager, _performanceMetrics, _healthCheckService); // 12. Start status web server _statusWebServer = new StatusWebServer(_config.WebServer, _statusReportService); if (!_statusWebServer.Start()) { Log.Warning("Status web server failed to start — continuing without it"); } // 13. Create gRPC service var grpcService = new ScadaGrpcService( _mxAccessClient, _sessionManager, _subscriptionManager, _performanceMetrics, _apiKeyService); // 14. Create and configure interceptor var interceptor = new ApiKeyInterceptor(_apiKeyService); // 15. Build and start gRPC server _grpcServer = new Server { Services = { Scada.ScadaService.BindService(grpcService) .Intercept(interceptor) }, Ports = { new ServerPort("0.0.0.0", _config.GrpcPort, credentials) } }; _grpcServer.Start(); Log.Information("gRPC server started on port {Port}", _config.GrpcPort); Log.Information("LmxProxy service started successfully"); return true; } catch (Exception ex) { Log.Fatal(ex, "LmxProxy service failed to start"); return false; } } /// /// Topshelf Stop callback. Stops and disposes all components in reverse order. /// public bool Stop() { Log.Information("LmxProxy service stopping..."); try { // 1. Stop reconnect monitor (5s wait) _mxAccessClient?.StopMonitorLoop(); // 2. Stop status web server _statusWebServer?.Stop(); // 3. Dispose performance metrics _performanceMetrics?.Dispose(); // 4. Graceful gRPC shutdown (10s timeout, then kill) if (_grpcServer != null) { Log.Information("Shutting down gRPC server..."); _grpcServer.ShutdownAsync().Wait(TimeSpan.FromSeconds(10)); Log.Information("gRPC server stopped"); } // 3. Dispose components in reverse order _subscriptionManager?.Dispose(); _sessionManager?.Dispose(); _apiKeyService?.Dispose(); // 4. Disconnect MxAccess (10s timeout) if (_mxAccessClient != null) { Log.Information("Disconnecting from MxAccess..."); _mxAccessClient.DisposeAsync().AsTask().Wait(TimeSpan.FromSeconds(10)); Log.Information("MxAccess disconnected"); } } catch (Exception ex) { Log.Error(ex, "Error during shutdown"); } Log.Information("LmxProxy service stopped"); return true; } /// Topshelf Pause callback -- no-op. public bool Pause() { Log.Information("LmxProxy service paused (no-op)"); return true; } /// Topshelf Continue callback -- no-op. public bool Continue() { Log.Information("LmxProxy service continued (no-op)"); return true; } } }