using MessagePack; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Ipc; /// /// Writes length-prefixed, kind-tagged MessagePack frames to a stream. Thread-safe via /// . Byte-identical mirror of the sidecar's FrameWriter. /// public sealed class FrameWriter : IDisposable { private readonly Stream _stream; private readonly SemaphoreSlim _gate = new(1, 1); private readonly bool _leaveOpen; /// Initializes a new instance of the FrameWriter class. /// The underlying stream to write frames to. /// If true, the stream is not disposed when this writer is disposed. public FrameWriter(Stream stream, bool leaveOpen = false) { _stream = stream ?? throw new ArgumentNullException(nameof(stream)); _leaveOpen = leaveOpen; } /// Writes a length-prefixed, kind-tagged MessagePack frame to the stream. /// The type of the message to serialize. /// The frame message kind tag. /// The message object to serialize and write. /// The cancellation token. public async Task WriteAsync(MessageKind kind, T message, CancellationToken ct) { var body = MessagePackSerializer.Serialize(message, cancellationToken: ct); if (body.Length > Framing.MaxFrameBodyBytes) throw new InvalidOperationException( $"Sidecar IPC frame body {body.Length} exceeds {Framing.MaxFrameBodyBytes} byte cap."); // 5-byte header: [4-byte big-endian body length][1-byte message kind]. // The kind byte is folded into the header array so every write inside the gate // is async+cancellable — a synchronous Stream.WriteByte() blocks the calling // thread-pool thread and cannot be interrupted by the call-timeout token when // the peer's receive window is full (same class of bug as finding 005 on reads). var header = new byte[Framing.LengthPrefixSize + Framing.KindByteSize]; header[0] = (byte)((body.Length >> 24) & 0xFF); header[1] = (byte)((body.Length >> 16) & 0xFF); header[2] = (byte)((body.Length >> 8) & 0xFF); header[3] = (byte)( body.Length & 0xFF); header[4] = (byte)kind; await _gate.WaitAsync(ct).ConfigureAwait(false); try { await _stream.WriteAsync(header, ct).ConfigureAwait(false); await _stream.WriteAsync(body, ct).ConfigureAwait(false); await _stream.FlushAsync(ct).ConfigureAwait(false); } finally { _gate.Release(); } } /// Disposes the writer and underlying stream (if not left open). public void Dispose() { _gate.Dispose(); if (!_leaveOpen) _stream.Dispose(); } }