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 { /// public async Task ConnectAsync(CancellationToken cancellationToken = default) { await _connectionLock.WaitAsync(cancellationToken); try { ObjectDisposedException.ThrowIf(_disposed, this); if (IsConnected) return; var endpoint = BuildEndpointUri(); _logger.LogInformation("Connecting to LmxProxy at {Endpoint}", endpoint); GrpcChannel channel = GrpcChannelFactory.CreateChannel(endpoint, _tlsConfiguration, _logger, _apiKey); IScadaService client; try { client = channel.CreateGrpcService(); } catch { channel.Dispose(); throw; } ConnectResponse response; try { var request = new ConnectRequest { ClientId = $"ScadaBridge-{Guid.NewGuid():N}", ApiKey = _apiKey ?? string.Empty }; response = await client.ConnectAsync(request); } catch { channel.Dispose(); throw; } if (!response.Success) { channel.Dispose(); throw new InvalidOperationException($"Connect failed: {response.Message}"); } _channel = channel; _client = client; _sessionId = response.SessionId; _isConnected = true; StartKeepAlive(); _logger.LogInformation("Connected to LmxProxy, session={SessionId}", _sessionId); } catch (Exception ex) { _channel = null; _client = null; _sessionId = string.Empty; _isConnected = false; _logger.LogError(ex, "Failed to connect to LmxProxy"); throw; } finally { _connectionLock.Release(); } } /// public async Task DisconnectAsync() { await _connectionLock.WaitAsync(); try { StopKeepAlive(); if (_client is not null && !string.IsNullOrEmpty(_sessionId)) { try { await _client.DisconnectAsync(new DisconnectRequest { SessionId = _sessionId }); } catch (Exception ex) { _logger.LogWarning(ex, "Error sending disconnect request"); } } _client = null; _sessionId = string.Empty; _isConnected = false; _channel?.Dispose(); _channel = null; } finally { _connectionLock.Release(); } } /// public async Task SubscribeAsync( IEnumerable addresses, Action onUpdate, Action? onStreamError = null, CancellationToken cancellationToken = default) { EnsureConnected(); var subscription = new CodeFirstSubscription( _client!, _sessionId, addresses.ToList(), onUpdate, onStreamError, _logger, sub => { lock (_subscriptionLock) { _activeSubscriptions.Remove(sub); } }); lock (_subscriptionLock) { _activeSubscriptions.Add(subscription); } await subscription.StartAsync(cancellationToken); return subscription; } private void StartKeepAlive() { _keepAliveTimer = new Timer( async _ => await KeepAliveCallback(), null, _keepAliveInterval, _keepAliveInterval); } private async Task KeepAliveCallback() { try { if (_client is null || string.IsNullOrEmpty(_sessionId)) return; await _client.GetConnectionStateAsync(new GetConnectionStateRequest { SessionId = _sessionId }); } catch (Exception ex) { _logger.LogWarning(ex, "Keep-alive failed, marking disconnected"); StopKeepAlive(); await MarkDisconnectedAsync(ex); } } private void StopKeepAlive() { _keepAliveTimer?.Dispose(); _keepAliveTimer = null; } internal async Task MarkDisconnectedAsync(Exception ex) { if (_disposed) return; await _connectionLock.WaitAsync(); try { _isConnected = false; _client = null; _sessionId = string.Empty; _channel?.Dispose(); _channel = null; } finally { _connectionLock.Release(); } List subscriptions; lock (_subscriptionLock) { subscriptions = [.. _activeSubscriptions]; _activeSubscriptions.Clear(); } foreach (var sub in subscriptions) { try { sub.Dispose(); } catch { /* swallow */ } } _logger.LogWarning(ex, "Client marked as disconnected"); } private Uri BuildEndpointUri() { string scheme = _tlsConfiguration?.UseTls == true ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; return new UriBuilder { Scheme = scheme, Host = _host, Port = _port }.Uri; } }