fix(abcip): budget expected reply size in RequestScheduler.TakeBatch (#582); private lock + struct-handle check across read fragments (#567)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-07 22:53:10 -04:00
parent 04b6563286
commit 9641cefe2e
8 changed files with 187 additions and 16 deletions
@@ -175,11 +175,15 @@ internal sealed class NativeAbLegacyTagRuntime : IAbLegacyTagRuntime
return (Math.Max(1, PcccServices.MaxDataBytes / _elementSize), address);
}
private async Task<ReadOnlyMemory<byte>> ExchangeAsync(Func<ushort, byte[]> pccc, CancellationToken ct)
/// <summary>Bytes of an Execute PCCC reply beyond its data: CIP header, requestor id, PCCC command / status / TNS
/// (rounded up) — the scheduler budgets <c>PcccReplyOverhead + data</c> against the connection (#582).</summary>
private const int PcccReplyOverhead = 32;
private async Task<ReadOnlyMemory<byte>> ExchangeAsync(Func<ushort, byte[]> pccc, CancellationToken ct, int replyDataBytes = 0)
{
var tns = unchecked((ushort)Interlocked.Increment(ref s_tns));
var request = PcccServices.ExecutePccc(_vendor, _serial, pccc(tns));
var reply = await _scheduler.SendAsync(request, ct).ConfigureAwait(false);
var reply = await _scheduler.SendAsync(request, ct, expectedReplyBytes: PcccReplyOverhead + replyDataBytes).ConfigureAwait(false);
return PcccServices.ParseReply(reply, tns, _requestorId);
}
@@ -200,7 +204,7 @@ internal sealed class NativeAbLegacyTagRuntime : IAbLegacyTagRuntime
tns => plc5
? PcccServices.Plc5RangeRead(tns, at, (ushort)(bytes / 2), bytes)
: PcccServices.SlcTypedRead(tns, at, bytes),
ct).ConfigureAwait(false);
ct, bytes).ConfigureAwait(false);
if (data.Length < bytes)
throw new AbTransportException(AbStatus.BadReply, $"PCCC read returned {data.Length} of {bytes} bytes.");
if (data.Length > bytes)