263 lines
9.0 KiB
C#
263 lines
9.0 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Grpc.Net.Client;
|
|
using Microsoft.Extensions.Logging;
|
|
using ProtoBuf.Grpc.Client;
|
|
using ZB.MOM.WW.LmxProxy.Client.Domain;
|
|
using ZB.MOM.WW.LmxProxy.Client.Security;
|
|
|
|
namespace ZB.MOM.WW.LmxProxy.Client
|
|
{
|
|
public partial class LmxProxyClient
|
|
{
|
|
/// <summary>
|
|
/// Connects to the LmxProxy service and establishes a session
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
public async Task ConnectAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
GrpcChannel? provisionalChannel = null;
|
|
|
|
await _connectionLock.WaitAsync(cancellationToken);
|
|
try
|
|
{
|
|
if (_disposed)
|
|
{
|
|
throw new ObjectDisposedException(nameof(LmxProxyClient));
|
|
}
|
|
|
|
if (_isConnected && _client != null && !string.IsNullOrEmpty(_sessionId))
|
|
{
|
|
_logger.LogDebug("LmxProxyClient already connected to {Host}:{Port} with session {SessionId}",
|
|
_host, _port, _sessionId);
|
|
return;
|
|
}
|
|
|
|
string securityMode = _tlsConfiguration?.UseTls == true ? "TLS/SSL" : "INSECURE";
|
|
_logger.LogInformation("Creating new {SecurityMode} connection to LmxProxy at {Host}:{Port}",
|
|
securityMode, _host, _port);
|
|
|
|
Uri endpoint = BuildEndpointUri();
|
|
provisionalChannel = GrpcChannelFactory.CreateChannel(endpoint, _tlsConfiguration, _logger);
|
|
|
|
// Create code-first gRPC client
|
|
IScadaService provisionalClient = provisionalChannel.CreateGrpcService<IScadaService>();
|
|
|
|
// Establish session with the server
|
|
var connectRequest = new ConnectRequest
|
|
{
|
|
ClientId = $"ScadaBridge-{Guid.NewGuid():N}",
|
|
ApiKey = _apiKey ?? string.Empty
|
|
};
|
|
|
|
ConnectResponse connectResponse = await provisionalClient.ConnectAsync(connectRequest);
|
|
|
|
if (!connectResponse.Success)
|
|
{
|
|
provisionalChannel.Dispose();
|
|
throw new InvalidOperationException($"Failed to establish session: {connectResponse.Message}");
|
|
}
|
|
|
|
// Dispose any existing channel before replacing it
|
|
_channel?.Dispose();
|
|
|
|
_channel = provisionalChannel;
|
|
_client = provisionalClient;
|
|
_sessionId = connectResponse.SessionId;
|
|
_isConnected = true;
|
|
|
|
provisionalChannel = null;
|
|
|
|
StartKeepAlive();
|
|
|
|
_logger.LogInformation("Successfully connected to LmxProxy with session {SessionId}", _sessionId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_isConnected = false;
|
|
_client = null;
|
|
_sessionId = string.Empty;
|
|
_logger.LogError(ex, "Failed to connect to LmxProxy");
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
provisionalChannel?.Dispose();
|
|
_connectionLock.Release();
|
|
}
|
|
}
|
|
|
|
private void StartKeepAlive()
|
|
{
|
|
StopKeepAlive();
|
|
|
|
_keepAliveTimer = new Timer(async _ =>
|
|
{
|
|
try
|
|
{
|
|
if (_isConnected && _client != null && !string.IsNullOrEmpty(_sessionId))
|
|
{
|
|
// Send a lightweight ping to keep session alive
|
|
var request = new GetConnectionStateRequest { SessionId = _sessionId };
|
|
await _client.GetConnectionStateAsync(request);
|
|
|
|
_logger.LogDebug("Keep-alive ping sent successfully for session {SessionId}", _sessionId);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogDebug(ex, "Keep-alive ping failed");
|
|
StopKeepAlive();
|
|
await MarkDisconnectedAsync(ex).ConfigureAwait(false);
|
|
}
|
|
}, null, _keepAliveInterval, _keepAliveInterval);
|
|
}
|
|
|
|
private void StopKeepAlive()
|
|
{
|
|
_keepAliveTimer?.Dispose();
|
|
_keepAliveTimer = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disconnects from the LmxProxy service
|
|
/// </summary>
|
|
public async Task DisconnectAsync()
|
|
{
|
|
await _connectionLock.WaitAsync();
|
|
try
|
|
{
|
|
StopKeepAlive();
|
|
|
|
if (_client != null && !string.IsNullOrEmpty(_sessionId))
|
|
{
|
|
try
|
|
{
|
|
var request = new DisconnectRequest { SessionId = _sessionId };
|
|
await _client.DisconnectAsync(request);
|
|
_logger.LogInformation("Session {SessionId} disconnected", _sessionId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Error during disconnect");
|
|
}
|
|
}
|
|
|
|
_client = null;
|
|
_sessionId = string.Empty;
|
|
_isConnected = false;
|
|
|
|
_channel?.Dispose();
|
|
_channel = null;
|
|
}
|
|
finally
|
|
{
|
|
_connectionLock.Release();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Connects the LmxProxy to MxAccess (legacy method - session now established in ConnectAsync)
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
public Task<(bool Success, string? ErrorMessage)> ConnectToMxAccessAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
// Session is now established in ConnectAsync
|
|
if (IsConnected)
|
|
return Task.FromResult((true, (string?)null));
|
|
|
|
return Task.FromResult<(bool Success, string? ErrorMessage)>((false, "Not connected. Call ConnectAsync first."));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disconnects the LmxProxy from MxAccess (legacy method)
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
public async Task<(bool Success, string? ErrorMessage)> DisconnectFromMxAccessAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
await DisconnectAsync();
|
|
return (true, null);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return (false, ex.Message);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the connection state of the LmxProxy
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
public async Task<(bool IsConnected, string? ClientId)> GetConnectionStateAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
EnsureConnected();
|
|
|
|
var request = new GetConnectionStateRequest { SessionId = _sessionId };
|
|
GetConnectionStateResponse response = await _client!.GetConnectionStateAsync(request);
|
|
return (response.IsConnected, response.ClientId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds the gRPC endpoint URI (http/https) based on TLS configuration.
|
|
/// </summary>
|
|
private Uri BuildEndpointUri()
|
|
{
|
|
string scheme = _tlsConfiguration?.UseTls == true ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
|
|
return new UriBuilder
|
|
{
|
|
Scheme = scheme,
|
|
Host = _host,
|
|
Port = _port
|
|
}.Uri;
|
|
}
|
|
|
|
private async Task MarkDisconnectedAsync(Exception? ex = null)
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
await _connectionLock.WaitAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
_isConnected = false;
|
|
_client = null;
|
|
_sessionId = string.Empty;
|
|
_channel?.Dispose();
|
|
_channel = null;
|
|
}
|
|
finally
|
|
{
|
|
_connectionLock.Release();
|
|
}
|
|
|
|
List<ISubscription> subsToDispose;
|
|
lock (_subscriptionLock)
|
|
{
|
|
subsToDispose = new List<ISubscription>(_activeSubscriptions);
|
|
_activeSubscriptions.Clear();
|
|
}
|
|
|
|
foreach (ISubscription sub in subsToDispose)
|
|
{
|
|
try
|
|
{
|
|
await sub.DisposeAsync().ConfigureAwait(false);
|
|
}
|
|
catch (Exception disposeEx)
|
|
{
|
|
_logger.LogWarning(disposeEx, "Error disposing subscription after disconnect");
|
|
}
|
|
}
|
|
|
|
if (ex != null)
|
|
{
|
|
_logger.LogWarning(ex, "Connection marked disconnected due to keep-alive failure");
|
|
}
|
|
}
|
|
}
|
|
}
|