Files
zeekay 2aa8261f86 rebrand: complete capnp → ZAP across all C# source/build files
Finishes the in-flight capnp→ZAP rebrand on this branch. The prior commits
renamed identifiers/namespaces/files/projects (Capnp.Net.Runtime→Zap.Net.Runtime,
CapnpC→ZapC, .capnp→.zap) but left the human-readable brand name "Cap'n Proto"
throughout doc-comments, .csproj/.nuspec descriptions, README, MSBuild .xaml
rule display names, and the generated XML doc. This completes brand neutrality:

- Replace 154 "Cap'n Proto" / 4 "Cap'n-Proto" / 1 "Cap'n'p" prose occurrences
  with "ZAP" across 32 first-party files (runtime, generator, zapc-csharp,
  MSBuild integration, chocolatey nuspecs, README, docs, XML doc).
- Revert the LICENSE provenance URL to the real upstream fork
  (c80k/capnproto-dotnetcore) — upstream-fork license headers must stay.
- Canonicalize project/repo URLs to github.com/zap-proto/cs (were fabricated
  c80k/zap-dotnetcore) in csproj/nuspec/VERIFICATION/README badges.
- Canonicalize docs URLs zap-protocol(.github.io|/zap-cs|/zap-cpp-core)
  → zap-proto.dev / github.com/zap-proto/{cs,cpp-core}.
- Collapse redundant "zap \"ZAP\"" PackageTags to "zap".

Verified consistent (no toolchain on host): every .sln/.csproj ProjectReference
resolves; all namespaces are Zap*/ZapC*; MSBuild task chain
(GenerateZapFileCodeBehindTask, ZapFiles ItemGroup, _ZapcCsharp_TaskAssembly)
is uniformly renamed; props/targets/xaml/coverage/chocolatey carry no capnp.

Deliberately preserved as upstream-derived test data (not first-party source):
upstream Cap'n Proto schema fixtures (rpc.zap, rpc-twoparty.zap, json.zap,
rpc-csharp.zap), golden compiler-error outputs (errors.txt), and the
machine-generated DO-NOT-EDIT test.zap.{c++,h}. Their "Cap'n Proto" strings are
the literal output of the external schema compiler the compat test matches.
2026-06-19 23:46:57 -07:00

219 lines
7.9 KiB
C#

using Zap.FrameTracing;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
namespace Zap
{
/// <summary>
/// The FramePump handles sending and receiving ZAP messages over a stream. It exposes a Send method for writing frames to the stream, and an
/// event handler for processing received frames. It does not fork any new thread by itself, but instead exposes a synchronous blocking Run method that
/// implements the receive loop. Invoke this method in the thread context of your choice.
/// </summary>
public class FramePump: IDisposable
{
ILogger Logger { get; } = Logging.CreateLogger<FramePump>();
int _disposing;
readonly Stream _stream;
readonly BinaryWriter? _writer;
readonly object _writeLock = new object();
readonly List<IFrameTracer> _tracers = new List<IFrameTracer>();
/// <summary>
/// Constructs a new instance for given stream.
/// </summary>
/// <param name="stream">The stream for message I/O.
/// If you intend to receive messages, the stream must support reading (CanRead).
/// If you intend to send messages, the stream must support writing (CanWrite).
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="stream"/> is null.</exception>
public FramePump(Stream stream)
{
_stream = stream ?? throw new ArgumentNullException(nameof(stream));
if (stream.CanWrite)
_writer = new BinaryWriter(_stream);
}
/// <summary>
/// Disposes this instance and the underlying stream. This will also cause the Run method to return.
/// </summary>
public void Dispose()
{
if (0 == Interlocked.Exchange(ref _disposing, 1))
{
foreach (var tracer in _tracers)
{
tracer.Dispose();
}
_writer?.Dispose();
_stream.Dispose();
}
}
/// <summary>
/// Event handler for frame reception.
/// </summary>
public event Action<WireFrame>? FrameReceived;
/// <summary>
/// Sends a message over the stream.
/// </summary>
/// <param name="frame">Message to be sent</param>
/// <exception cref="InvalidOperationException">The underlying stream does not support writing.</exception>
/// <exception cref="ArgumentException">The message does not provide at least one segment, or one of its segments is empty.</exception>
/// <exception cref="IOException">An I/O error occurs.</exception>
/// <exception cref="ObjectDisposedException">This instance or stream is diposed.</exception>
public void Send(WireFrame frame)
{
if (_writer == null)
throw new InvalidOperationException("Stream is not writable");
if (frame.Segments == null)
throw new ArgumentException("Do not pass default(WireFrame)");
if (frame.Segments.Count == 0)
throw new ArgumentException("Expected at least one segment");
foreach (var segment in frame.Segments)
{
if (segment.Length == 0)
throw new ArgumentException("Segment must not have zero length");
}
lock (_writeLock)
{
foreach (var tracer in _tracers)
{
tracer.TraceFrame(FrameDirection.Tx, frame);
}
_writer.Write(frame.Segments.Count - 1);
foreach (var segment in frame.Segments)
{
_writer.Write(segment.Length);
}
if ((frame.Segments.Count & 1) == 0)
{
// Padding
_writer.Write(0);
}
foreach (var segment in frame.Segments)
{
#if NETSTANDARD2_0
var bytes = MemoryMarshal.Cast<ulong, byte>(segment.Span).ToArray();
#else
var bytes = MemoryMarshal.Cast<ulong, byte>(segment.Span);
#endif
_writer.Write(bytes);
}
}
}
/// <summary>
/// Flushes all buffered frames.
/// </summary>
public void Flush()
{
if (Monitor.TryEnter(_writeLock))
{
try
{
_writer?.Flush();
}
finally
{
Monitor.Exit(_writeLock);
}
}
}
long _waitingForData;
/// <summary>
/// Whether the pump is currently waiting for data to receive.
/// </summary>
public bool IsWaitingForData
{
get => Interlocked.Read(ref _waitingForData) != 0;
private set => Interlocked.Exchange(ref _waitingForData, value ? 1 : 0);
}
/// <summary>
/// Synchronously runs the frame reception loop. Will only return after calling Dispose() or upon error condition.
/// The method does not propagate EndOfStreamException or ObjectDisposedException to the caller, since these conditions are considered
/// to be part of normal operation. It does pass exceptions which arise due to I/O errors or invalid data.
/// </summary>
/// <exception cref="ArgumentException">The underlying stream does not support reading or is already closed.</exception>
/// <exception cref="InvalidDataException">Encountered Invalid Framing Data</exception>
/// <exception cref="OutOfMemoryException">Received a message with too many or too big segments, probably dues to invalid data.</exception>
/// <exception cref="IOException">An I/O error occurs.</exception>
public void Run()
{
try
{
using (var reader = new BinaryReader(_stream, Encoding.Default))
{
while (true)
{
IsWaitingForData = true;
var frame = reader.ReadWireFrame();
IsWaitingForData = false;
foreach (var tracer in _tracers)
{
tracer.TraceFrame(FrameDirection.Rx, frame);
}
FrameReceived?.Invoke(frame);
}
}
}
catch (Exception exception) when (exception is EndOfStreamException ||
exception is IOException ioex &&
ioex.InnerException is SocketException sockex &&
sockex.SocketErrorCode == SocketError.Interrupted)
{
Logger.LogInformation("Encountered end of stream");
}
catch (InvalidDataException e)
{
Logger.LogWarning(e.Message);
}
catch (ObjectDisposedException)
{
}
catch (Exception exception)
{
// When disposing, all kinds of errors might happen here,
// not worth logging.
if (_disposing == 0)
{
Logger.LogWarning(exception.Message);
}
}
finally
{
IsWaitingForData = false;
}
}
/// <summary>
/// Attaches an observer for tracing RPC traffic
/// </summary>
/// <param name="tracer">observer implementation</param>
public void AttachTracer(IFrameTracer tracer)
{
_tracers.Add(tracer);
}
}
}