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;
}
}
}