using System; using System.Threading; using System.Threading.Tasks; using MessagePack; using Serilog; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Contracts; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Ipc; /// /// Real IPC dispatcher — routes each to the matching /// method. Replaces . Heartbeat /// stays handled inline so liveness detection works regardless of backend health. /// public sealed class GalaxyFrameHandler(IGalaxyBackend backend, ILogger logger) : IFrameHandler { public async Task HandleAsync(MessageKind kind, byte[] body, FrameWriter writer, CancellationToken ct) { try { switch (kind) { case MessageKind.Heartbeat: { var hb = Deserialize(body); await writer.WriteAsync(MessageKind.HeartbeatAck, new HeartbeatAck { SequenceNumber = hb.SequenceNumber, UtcUnixMs = hb.UtcUnixMs }, ct); return; } case MessageKind.OpenSessionRequest: { var resp = await backend.OpenSessionAsync(Deserialize(body), ct); await writer.WriteAsync(MessageKind.OpenSessionResponse, resp, ct); return; } case MessageKind.CloseSessionRequest: await backend.CloseSessionAsync(Deserialize(body), ct); return; // one-way case MessageKind.DiscoverHierarchyRequest: { var resp = await backend.DiscoverAsync(Deserialize(body), ct); await writer.WriteAsync(MessageKind.DiscoverHierarchyResponse, resp, ct); return; } case MessageKind.ReadValuesRequest: { var resp = await backend.ReadValuesAsync(Deserialize(body), ct); await writer.WriteAsync(MessageKind.ReadValuesResponse, resp, ct); return; } case MessageKind.WriteValuesRequest: { var resp = await backend.WriteValuesAsync(Deserialize(body), ct); await writer.WriteAsync(MessageKind.WriteValuesResponse, resp, ct); return; } case MessageKind.SubscribeRequest: { var resp = await backend.SubscribeAsync(Deserialize(body), ct); await writer.WriteAsync(MessageKind.SubscribeResponse, resp, ct); return; } case MessageKind.UnsubscribeRequest: await backend.UnsubscribeAsync(Deserialize(body), ct); return; // one-way case MessageKind.AlarmSubscribeRequest: await backend.SubscribeAlarmsAsync(Deserialize(body), ct); return; // one-way; subsequent alarm events are server-pushed case MessageKind.AlarmAckRequest: await backend.AcknowledgeAlarmAsync(Deserialize(body), ct); return; case MessageKind.HistoryReadRequest: { var resp = await backend.HistoryReadAsync(Deserialize(body), ct); await writer.WriteAsync(MessageKind.HistoryReadResponse, resp, ct); return; } case MessageKind.RecycleHostRequest: { var resp = await backend.RecycleAsync(Deserialize(body), ct); await writer.WriteAsync(MessageKind.RecycleStatusResponse, resp, ct); return; } default: await SendErrorAsync(writer, "unknown-kind", $"Frame kind {kind} not handled by Host", ct); return; } } catch (OperationCanceledException) { throw; } catch (Exception ex) { logger.Error(ex, "GalaxyFrameHandler threw on {Kind}", kind); await SendErrorAsync(writer, "handler-exception", ex.Message, ct); } } /// /// Subscribes the backend's server-pushed events for the lifetime of the connection. /// The returned disposable unsubscribes when the connection closes — without it the /// backend's static event invocation list would accumulate dead writer references and /// leak memory + raise on every push. /// public IDisposable AttachConnection(FrameWriter writer) { var sink = new ConnectionSink(backend, writer, logger); sink.Attach(); return sink; } private static T Deserialize(byte[] body) => MessagePackSerializer.Deserialize(body); private static Task SendErrorAsync(FrameWriter writer, string code, string message, CancellationToken ct) => writer.WriteAsync(MessageKind.ErrorResponse, new ErrorResponse { Code = code, Message = message }, ct); private sealed class ConnectionSink : IDisposable { private readonly IGalaxyBackend _backend; private readonly FrameWriter _writer; private readonly ILogger _logger; private EventHandler? _onData; private EventHandler? _onAlarm; private EventHandler? _onHost; public ConnectionSink(IGalaxyBackend backend, FrameWriter writer, ILogger logger) { _backend = backend; _writer = writer; _logger = logger; } public void Attach() { _onData = (_, e) => Push(MessageKind.OnDataChangeNotification, e); _onAlarm = (_, e) => Push(MessageKind.AlarmEvent, e); _onHost = (_, e) => Push(MessageKind.RuntimeStatusChange, new RuntimeStatusChangeNotification { Status = e }); _backend.OnDataChange += _onData; _backend.OnAlarmEvent += _onAlarm; _backend.OnHostStatusChanged += _onHost; } private void Push(MessageKind kind, T payload) { // Fire-and-forget — pushes can race with disposal of the writer. We swallow // ObjectDisposedException because the dispose path will detach this sink shortly. try { _writer.WriteAsync(kind, payload, CancellationToken.None).GetAwaiter().GetResult(); } catch (ObjectDisposedException) { } catch (Exception ex) { _logger.Warning(ex, "ConnectionSink push failed for {Kind}", kind); } } public void Dispose() { if (_onData is not null) _backend.OnDataChange -= _onData; if (_onAlarm is not null) _backend.OnAlarmEvent -= _onAlarm; if (_onHost is not null) _backend.OnHostStatusChanged -= _onHost; } } }