using System; using System.Collections.Generic; using System.IO; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Grpc.Core; using Grpc.Core.Interceptors; using Serilog; using ZB.MOM.WW.LmxProxy.Host.Configuration; using ZB.MOM.WW.LmxProxy.Host.Domain; using ZB.MOM.WW.LmxProxy.Host.Grpc.Services; using ZB.MOM.WW.LmxProxy.Host.Implementation; using ZB.MOM.WW.LmxProxy.Host.Security; using ZB.MOM.WW.LmxProxy.Host.Services; using ZB.MOM.WW.LmxProxy.Host.Grpc; using ConnectionState = ZB.MOM.WW.LmxProxy.Host.Domain.ConnectionState; namespace ZB.MOM.WW.LmxProxy.Host { /// /// Windows service that hosts the gRPC server and MxAccess client. /// Manages lifecycle of gRPC server, SCADA client, subscription manager, and API key service. /// public class LmxProxyService { private static readonly ILogger Logger = Log.ForContext(); private readonly LmxProxyConfiguration _configuration; private readonly SemaphoreSlim _reconnectSemaphore = new(1, 1); private readonly Func _scadaClientFactory; private readonly CancellationTokenSource _shutdownCts = new(); private ApiKeyService? _apiKeyService; private Task? _connectionMonitorTask; private DetailedHealthCheckService? _detailedHealthCheckService; private Server? _grpcServer; private HealthCheckService? _healthCheckService; private PerformanceMetrics? _performanceMetrics; private IScadaClient? _scadaClient; private SessionManager? _sessionManager; private StatusReportService? _statusReportService; private StatusWebServer? _statusWebServer; private SubscriptionManager? _subscriptionManager; /// /// Initializes a new instance of the class. /// /// Configuration settings for the service. /// Thrown if configuration is null. public LmxProxyService(LmxProxyConfiguration configuration, Func? scadaClientFactory = null) { _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _scadaClientFactory = scadaClientFactory ?? (config => new MxAccessClient(config.Connection)); } /// /// Starts the LmxProxy service, initializing all required components and starting the gRPC server. /// /// true if the service started successfully; otherwise, false. public bool Start() { try { Logger.Information("Starting LmxProxy service on port {Port}", _configuration.GrpcPort); // Validate configuration before proceeding if (!ValidateConfiguration()) { Logger.Error("Configuration validation failed"); return false; } // Check and ensure TLS certificates are valid if (_configuration.Tls.Enabled) { Logger.Information("Checking TLS certificate configuration"); var tlsManager = new TlsCertificateManager(_configuration.Tls); if (!tlsManager.EnsureCertificatesValid()) { Logger.Error("Failed to ensure valid TLS certificates"); throw new InvalidOperationException("TLS certificate validation or generation failed"); } Logger.Information("TLS certificates validated successfully"); } // Create performance metrics service _performanceMetrics = new PerformanceMetrics(); Logger.Information("Performance metrics service initialized"); // Create API key service string apiKeyConfigPath = Path.GetFullPath(_configuration.ApiKeyConfigFile); _apiKeyService = new ApiKeyService(apiKeyConfigPath); Logger.Information("API key service initialized with config file: {ConfigFile}", apiKeyConfigPath); // Create SCADA client via factory _scadaClient = _scadaClientFactory(_configuration) ?? throw new InvalidOperationException("SCADA client factory returned null."); // Subscribe to connection state changes _scadaClient.ConnectionStateChanged += OnConnectionStateChanged; // Automatically connect to MxAccess on startup try { Logger.Information("Connecting to MxAccess..."); Task connectTask = _scadaClient.ConnectAsync(); if (!connectTask.Wait(TimeSpan.FromSeconds(_configuration.Connection.ConnectionTimeoutSeconds))) { throw new TimeoutException( $"Timeout connecting to MxAccess after {_configuration.Connection.ConnectionTimeoutSeconds} seconds"); } Logger.Information("Successfully connected to MxAccess"); } catch (Exception ex) { Logger.Error(ex, "Failed to connect to MxAccess on startup"); throw; } // Start connection monitoring if auto-reconnect is enabled if (_configuration.Connection.AutoReconnect) { _connectionMonitorTask = Task.Run(() => MonitorConnectionAsync(_shutdownCts.Token)); Logger.Information("Connection monitoring started with {Interval} second interval", _configuration.Connection.MonitorIntervalSeconds); } // Create subscription manager with configuration _subscriptionManager = new SubscriptionManager(_scadaClient, _configuration.Subscription); // Create session manager for tracking client sessions _sessionManager = new SessionManager(); Logger.Information("Session manager initialized"); // Create health check services _healthCheckService = new HealthCheckService(_scadaClient, _subscriptionManager, _performanceMetrics); _detailedHealthCheckService = new DetailedHealthCheckService(_scadaClient); Logger.Information("Health check services initialized"); // Create status report service and web server _statusReportService = new StatusReportService( _scadaClient, _subscriptionManager, _performanceMetrics, _healthCheckService, _detailedHealthCheckService); _statusWebServer = new StatusWebServer(_configuration.WebServer, _statusReportService); Logger.Information("Status web server initialized"); // Create gRPC service with session manager and performance metrics var scadaService = new ScadaGrpcService(_scadaClient, _subscriptionManager, _sessionManager, _performanceMetrics); // Create API key interceptor var apiKeyInterceptor = new ApiKeyInterceptor(_apiKeyService); // Configure server credentials based on TLS configuration ServerCredentials serverCredentials; if (_configuration.Tls.Enabled) { serverCredentials = CreateTlsCredentials(_configuration.Tls); Logger.Information("TLS enabled for gRPC server"); } else { serverCredentials = ServerCredentials.Insecure; Logger.Warning("gRPC server running without TLS encryption - not recommended for production"); } // Configure and start gRPC server with interceptor _grpcServer = new Server { Services = { ScadaService.BindService(scadaService).Intercept(apiKeyInterceptor) }, Ports = { new ServerPort("0.0.0.0", _configuration.GrpcPort, serverCredentials) } }; _grpcServer.Start(); string securityMode = _configuration.Tls.Enabled ? "TLS/SSL" : "INSECURE"; Logger.Information("LmxProxy service started successfully on port {Port} ({SecurityMode})", _configuration.GrpcPort, securityMode); Logger.Information("gRPC server listening on 0.0.0.0:{Port}", _configuration.GrpcPort); // Start status web server if (_statusWebServer != null && !_statusWebServer.Start()) { Logger.Warning("Failed to start status web server, continuing without it"); } return true; } catch (Exception ex) { Logger.Fatal(ex, "Failed to start LmxProxy service"); return false; } } /// /// Stops the LmxProxy service, shutting down the gRPC server and disposing all resources. /// /// true if the service stopped successfully; otherwise, false. public bool Stop() { try { Logger.Information("Stopping LmxProxy service"); _shutdownCts.Cancel(); // Stop connection monitoring if (_connectionMonitorTask != null) { try { _connectionMonitorTask.Wait(TimeSpan.FromSeconds(5)); } catch (Exception ex) { Logger.Warning(ex, "Error stopping connection monitor"); } } // Shutdown gRPC server if (_grpcServer != null) { Logger.Information("Shutting down gRPC server"); Task? shutdownTask = _grpcServer.ShutdownAsync(); // Wait up to 10 seconds for graceful shutdown if (!shutdownTask.Wait(TimeSpan.FromSeconds(10))) { Logger.Warning("gRPC server shutdown timeout, forcing kill"); _grpcServer.KillAsync().Wait(TimeSpan.FromSeconds(5)); } _grpcServer = null; } // Stop status web server if (_statusWebServer != null) { Logger.Information("Stopping status web server"); try { _statusWebServer.Stop(); _statusWebServer.Dispose(); _statusWebServer = null; } catch (Exception ex) { Logger.Warning(ex, "Error stopping status web server"); } } // Dispose status report service if (_statusReportService != null) { Logger.Information("Disposing status report service"); _statusReportService = null; } // Dispose health check services if (_detailedHealthCheckService != null) { Logger.Information("Disposing detailed health check service"); _detailedHealthCheckService = null; } if (_healthCheckService != null) { Logger.Information("Disposing health check service"); _healthCheckService = null; } // Dispose subscription manager if (_subscriptionManager != null) { Logger.Information("Disposing subscription manager"); _subscriptionManager.Dispose(); _subscriptionManager = null; } // Dispose session manager if (_sessionManager != null) { Logger.Information("Disposing session manager"); _sessionManager.Dispose(); _sessionManager = null; } // Dispose API key service if (_apiKeyService != null) { Logger.Information("Disposing API key service"); _apiKeyService.Dispose(); _apiKeyService = null; } // Dispose performance metrics if (_performanceMetrics != null) { Logger.Information("Disposing performance metrics service"); _performanceMetrics.Dispose(); _performanceMetrics = null; } // Disconnect and dispose SCADA client if (_scadaClient != null) { Logger.Information("Disconnecting SCADA client"); // Unsubscribe from events _scadaClient.ConnectionStateChanged -= OnConnectionStateChanged; try { Task disconnectTask = _scadaClient.DisconnectAsync(); if (!disconnectTask.Wait(TimeSpan.FromSeconds(10))) { Logger.Warning("SCADA client disconnect timeout"); } } catch (Exception ex) { Logger.Warning(ex, "Error disconnecting SCADA client"); } try { Task? disposeTask = _scadaClient.DisposeAsync().AsTask(); if (!disposeTask.Wait(TimeSpan.FromSeconds(5))) { Logger.Warning("SCADA client dispose timeout"); } } catch (Exception ex) { Logger.Warning(ex, "Error disposing SCADA client"); } _scadaClient = null; } Logger.Information("LmxProxy service stopped successfully"); return true; } catch (Exception ex) { Logger.Error(ex, "Error stopping LmxProxy service"); return false; } } /// /// Pauses the LmxProxy service. No operation is performed except logging. /// public void Pause() => Logger.Information("LmxProxy service paused"); /// /// Continues the LmxProxy service after a pause. No operation is performed except logging. /// public void Continue() => Logger.Information("LmxProxy service continued"); /// /// Requests shutdown of the LmxProxy service and stops all components. /// public void Shutdown() { Logger.Information("LmxProxy service shutdown requested"); Stop(); } /// /// Handles connection state changes from the SCADA client. /// private void OnConnectionStateChanged(object? sender, ConnectionStateChangedEventArgs e) { Logger.Information("MxAccess connection state changed from {Previous} to {Current}", e.PreviousState, e.CurrentState); if (e.CurrentState == ConnectionState.Disconnected && e.PreviousState == ConnectionState.Connected) { Logger.Warning("MxAccess connection lost. Automatic reconnection will be attempted."); } } /// /// Monitors the connection and attempts to reconnect when disconnected. /// private async Task MonitorConnectionAsync(CancellationToken cancellationToken) { Logger.Information("Starting connection monitor"); while (!cancellationToken.IsCancellationRequested) { try { await Task.Delay(TimeSpan.FromSeconds(_configuration.Connection.MonitorIntervalSeconds), cancellationToken); if (_scadaClient != null && !_scadaClient.IsConnected && !cancellationToken.IsCancellationRequested) { await _reconnectSemaphore.WaitAsync(cancellationToken); try { if (_scadaClient != null && !_scadaClient.IsConnected) { Logger.Information("Attempting to reconnect to MxAccess..."); try { await _scadaClient.ConnectAsync(cancellationToken); Logger.Information("Successfully reconnected to MxAccess"); } catch (Exception ex) { Logger.Warning(ex, "Failed to reconnect to MxAccess. Will retry in {Interval} seconds.", _configuration.Connection.MonitorIntervalSeconds); } } } finally { _reconnectSemaphore.Release(); } } } catch (OperationCanceledException) { // Expected when shutting down break; } catch (Exception ex) { Logger.Error(ex, "Error in connection monitor"); } } Logger.Information("Connection monitor stopped"); } /// /// Creates TLS server credentials from configuration /// private static ServerCredentials CreateTlsCredentials(TlsConfiguration tlsConfig) { try { // Read certificate and key files string serverCert = File.ReadAllText(tlsConfig.ServerCertificatePath); string serverKey = File.ReadAllText(tlsConfig.ServerKeyPath); var keyCertPairs = new List { new(serverCert, serverKey) }; // Configure client certificate requirements if (tlsConfig.RequireClientCertificate && !string.IsNullOrWhiteSpace(tlsConfig.ClientCaCertificatePath)) { string clientCaCert = File.ReadAllText(tlsConfig.ClientCaCertificatePath); return new SslServerCredentials( keyCertPairs, clientCaCert, tlsConfig.CheckCertificateRevocation ? SslClientCertificateRequestType.RequestAndRequireAndVerify : SslClientCertificateRequestType.RequestAndRequireButDontVerify); } if (tlsConfig.RequireClientCertificate) { // Require client certificate but no CA specified - use system CA return new SslServerCredentials( keyCertPairs, null, SslClientCertificateRequestType.RequestAndRequireAndVerify); } // No client certificate required return new SslServerCredentials(keyCertPairs); } catch (Exception ex) { Logger.Error(ex, "Failed to create TLS credentials"); throw new InvalidOperationException("Failed to configure TLS for gRPC server", ex); } } /// /// Validates the service configuration and returns false if any critical issues are found /// private bool ValidateConfiguration() { try { // Validate gRPC port if (_configuration.GrpcPort <= 0 || _configuration.GrpcPort > 65535) { Logger.Error("Invalid gRPC port: {Port}. Port must be between 1 and 65535", _configuration.GrpcPort); return false; } // Validate API key configuration file if (string.IsNullOrWhiteSpace(_configuration.ApiKeyConfigFile)) { Logger.Error("API key configuration file path is null or empty"); return false; } // Check if API key file exists or can be created string apiKeyPath = Path.GetFullPath(_configuration.ApiKeyConfigFile); string? apiKeyDirectory = Path.GetDirectoryName(apiKeyPath); if (!string.IsNullOrEmpty(apiKeyDirectory) && !Directory.Exists(apiKeyDirectory)) { try { Directory.CreateDirectory(apiKeyDirectory); } catch (Exception ex) { Logger.Error(ex, "Cannot create directory for API key file: {Directory}", apiKeyDirectory); return false; } } // If API key file exists, validate it can be read if (File.Exists(apiKeyPath)) { try { string content = File.ReadAllText(apiKeyPath); if (!string.IsNullOrWhiteSpace(content)) { // Try to parse as JSON to validate format JsonDocument.Parse(content); } } catch (Exception ex) { Logger.Error(ex, "API key configuration file is invalid or unreadable: {FilePath}", apiKeyPath); return false; } } // Validate TLS configuration if enabled if (_configuration.Tls.Enabled) { if (!_configuration.Tls.Validate()) { Logger.Error("TLS configuration validation failed"); return false; } } // Validate web server configuration if enabled if (_configuration.WebServer.Enabled) { if (_configuration.WebServer.Port <= 0 || _configuration.WebServer.Port > 65535) { Logger.Error("Invalid web server port: {Port}. Port must be between 1 and 65535", _configuration.WebServer.Port); return false; } // Check for port conflicts if (_configuration.WebServer.Port == _configuration.GrpcPort) { Logger.Error("Web server port {WebPort} conflicts with gRPC port {GrpcPort}", _configuration.WebServer.Port, _configuration.GrpcPort); return false; } } Logger.Information("Configuration validation passed"); return true; } catch (Exception ex) { Logger.Error(ex, "Error during configuration validation"); return false; } } } }