feat(lmxproxy): phase 3 — host gRPC server, security, configuration, service hosting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
191
lmxproxy/src/ZB.MOM.WW.LmxProxy.Host/LmxProxyService.cs
Normal file
191
lmxproxy/src/ZB.MOM.WW.LmxProxy.Host/LmxProxyService.cs
Normal file
@@ -0,0 +1,191 @@
|
||||
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.Sessions;
|
||||
using ZB.MOM.WW.LmxProxy.Host.Subscriptions;
|
||||
|
||||
namespace ZB.MOM.WW.LmxProxy.Host
|
||||
{
|
||||
/// <summary>
|
||||
/// Service lifecycle manager. Created by Topshelf, handles Start/Stop/Pause/Continue.
|
||||
/// </summary>
|
||||
public class LmxProxyService
|
||||
{
|
||||
private static readonly ILogger Log = Serilog.Log.ForContext<LmxProxyService>();
|
||||
|
||||
private readonly LmxProxyConfiguration _config;
|
||||
|
||||
private MxAccessClient? _mxAccessClient;
|
||||
private SessionManager? _sessionManager;
|
||||
private SubscriptionManager? _subscriptionManager;
|
||||
private ApiKeyService? _apiKeyService;
|
||||
private Server? _grpcServer;
|
||||
|
||||
public LmxProxyService(LmxProxyConfiguration config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Topshelf Start callback. Creates and starts all components.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
// 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();
|
||||
}
|
||||
};
|
||||
|
||||
// 8. Create SessionManager
|
||||
_sessionManager = new SessionManager(inactivityTimeoutMinutes: 5);
|
||||
|
||||
// 9. Create gRPC service
|
||||
var grpcService = new ScadaGrpcService(
|
||||
_mxAccessClient, _sessionManager, _subscriptionManager);
|
||||
|
||||
// 10. Create and configure interceptor
|
||||
var interceptor = new ApiKeyInterceptor(_apiKeyService);
|
||||
|
||||
// 11. 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Topshelf Stop callback. Stops and disposes all components in reverse order.
|
||||
/// </summary>
|
||||
public bool Stop()
|
||||
{
|
||||
Log.Information("LmxProxy service stopping...");
|
||||
|
||||
try
|
||||
{
|
||||
// 1. Stop reconnect monitor (5s wait)
|
||||
_mxAccessClient?.StopMonitorLoop();
|
||||
|
||||
// 2. 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;
|
||||
}
|
||||
|
||||
/// <summary>Topshelf Pause callback -- no-op.</summary>
|
||||
public bool Pause()
|
||||
{
|
||||
Log.Information("LmxProxy service paused (no-op)");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Topshelf Continue callback -- no-op.</summary>
|
||||
public bool Continue()
|
||||
{
|
||||
Log.Information("LmxProxy service continued (no-op)");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user