deprecate(lmxproxy): move all LmxProxy code, tests, and docs to deprecated/
LmxProxy is no longer needed. Moved the entire lmxproxy/ workspace, DCL adapter files, and related docs to deprecated/. Removed LmxProxy registration from DataConnectionFactory, project reference from DCL, protocol option from UI, and cleaned up all requirement docs.
This commit is contained in:
@@ -0,0 +1,592 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Windows service that hosts the gRPC server and MxAccess client.
|
||||
/// Manages lifecycle of gRPC server, SCADA client, subscription manager, and API key service.
|
||||
/// </summary>
|
||||
public class LmxProxyService
|
||||
{
|
||||
private static readonly ILogger Logger = Log.ForContext<LmxProxyService>();
|
||||
private readonly LmxProxyConfiguration _configuration;
|
||||
private readonly SemaphoreSlim _reconnectSemaphore = new(1, 1);
|
||||
private readonly Func<LmxProxyConfiguration, IScadaClient> _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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LmxProxyService" /> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">Configuration settings for the service.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown if configuration is null.</exception>
|
||||
public LmxProxyService(LmxProxyConfiguration configuration,
|
||||
Func<LmxProxyConfiguration, IScadaClient>? scadaClientFactory = null)
|
||||
{
|
||||
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||
_scadaClientFactory = scadaClientFactory ?? (config => new MxAccessClient(config.Connection));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the LmxProxy service, initializing all required components and starting the gRPC server.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the service started successfully; otherwise, <c>false</c>.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the LmxProxy service, shutting down the gRPC server and disposing all resources.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the service stopped successfully; otherwise, <c>false</c>.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pauses the LmxProxy service. No operation is performed except logging.
|
||||
/// </summary>
|
||||
public void Pause() => Logger.Information("LmxProxy service paused");
|
||||
|
||||
/// <summary>
|
||||
/// Continues the LmxProxy service after a pause. No operation is performed except logging.
|
||||
/// </summary>
|
||||
public void Continue() => Logger.Information("LmxProxy service continued");
|
||||
|
||||
/// <summary>
|
||||
/// Requests shutdown of the LmxProxy service and stops all components.
|
||||
/// </summary>
|
||||
public void Shutdown()
|
||||
{
|
||||
Logger.Information("LmxProxy service shutdown requested");
|
||||
Stop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles connection state changes from the SCADA client.
|
||||
/// </summary>
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Monitors the connection and attempts to reconnect when disconnected.
|
||||
/// </summary>
|
||||
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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates TLS server credentials from configuration
|
||||
/// </summary>
|
||||
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<KeyCertificatePair>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the service configuration and returns false if any critical issues are found
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user