Files
scadalink-design/deprecated/lmxproxy/src-reference/ZB.MOM.WW.LmxProxy.Client/LmxProxyClient.Connection.cs
Joseph Doherty 9dccf8e72f 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.
2026-04-08 15:56:23 -04:00

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