Initial commit

This commit is contained in:
Holger Börchers
2026-01-30 15:31:43 +01:00
commit 894fbbfa5a
22 changed files with 1701 additions and 0 deletions

6
CommIpc/Class1.cs Normal file
View File

@@ -0,0 +1,6 @@
namespace CommIpc;
public class Class1
{
}

9
CommIpc/CommIpc.csproj Normal file
View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

15
CommIpc/IpcFrame.cs Normal file
View File

@@ -0,0 +1,15 @@
using System.Text.Json;
namespace CommIpc;
/// <summary>
/// Single protocol unit sent over the pipe. This is intentionally generic.
///
/// Transport framing: 4-byte little-endian length prefix + UTF-8 JSON bytes.
/// </summary>
public sealed record IpcFrame(
string Kind,
string? CorrelationId = null,
JsonElement? Payload = null,
DateTimeOffset? Timestamp = null
);

12
CommIpc/IpcJson.cs Normal file
View File

@@ -0,0 +1,12 @@
using System.Text.Json;
namespace CommIpc;
internal static class IpcJson
{
public static readonly JsonSerializerOptions Options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
}

16
CommIpc/IpcKinds.cs Normal file
View File

@@ -0,0 +1,16 @@
namespace CommIpc;
public static class IpcKinds
{
public const string Hello = "hello";
public const string Ping = "ping";
public const string Pong = "pong";
public const string StartWork = "startWork";
public const string CancelWork = "cancelWork";
public const string Log = "log";
public const string Progress = "progress";
public const string Result = "result";
public const string Error = "error";
}

129
CommIpc/IpcProtocol.cs Normal file
View File

@@ -0,0 +1,129 @@
using System.Buffers;
using System.Text.Json;
namespace CommIpc;
public static class IpcProtocol
{
// Keep the prototype safe from accidental runaway memory usage.
public const int DefaultMaxFrameBytes = 4 * 1024 * 1024; // 4 MiB
public static async Task WriteFrameAsync(
Stream stream,
IpcFrame frame,
CancellationToken cancellationToken = default)
{
// Always include a timestamp if the sender didn't set one.
if (frame.Timestamp is null)
{
frame = frame with { Timestamp = DateTimeOffset.UtcNow };
}
byte[] json = JsonSerializer.SerializeToUtf8Bytes(frame, IpcJson.Options);
byte[] header = new byte[4];
int len = json.Length;
header[0] = (byte)(len & 0xFF);
header[1] = (byte)((len >> 8) & 0xFF);
header[2] = (byte)((len >> 16) & 0xFF);
header[3] = (byte)((len >> 24) & 0xFF);
await stream.WriteAsync(header, cancellationToken).ConfigureAwait(false);
await stream.WriteAsync(json, cancellationToken).ConfigureAwait(false);
await stream.FlushAsync(cancellationToken).ConfigureAwait(false);
}
public static async Task<IpcFrame?> ReadFrameAsync(
Stream stream,
int maxFrameBytes = DefaultMaxFrameBytes,
CancellationToken cancellationToken = default)
{
byte[] header = ArrayPool<byte>.Shared.Rent(4);
try
{
int headerRead = await ReadExactOrEofAsync(stream, header, 0, 4, cancellationToken).ConfigureAwait(false);
if (headerRead == 0)
{
return null; // clean EOF
}
if (headerRead != 4)
{
throw new EndOfStreamException("Unexpected end of stream while reading frame header.");
}
int len = header[0]
| (header[1] << 8)
| (header[2] << 16)
| (header[3] << 24);
if (len < 0)
{
throw new InvalidDataException("Negative frame length.");
}
if (len == 0)
{
throw new InvalidDataException("Zero-length frame.");
}
if (len > maxFrameBytes)
{
throw new InvalidDataException($"Frame too large: {len} bytes (limit {maxFrameBytes}).");
}
byte[] payload = ArrayPool<byte>.Shared.Rent(len);
try
{
int read = await ReadExactOrEofAsync(stream, payload, 0, len, cancellationToken).ConfigureAwait(false);
if (read != len)
{
throw new EndOfStreamException("Unexpected end of stream while reading frame payload.");
}
// Deserialize from the rented buffer slice.
return JsonSerializer.Deserialize<IpcFrame>(payload.AsSpan(0, len), IpcJson.Options);
}
finally
{
ArrayPool<byte>.Shared.Return(payload);
}
}
finally
{
ArrayPool<byte>.Shared.Return(header);
}
}
public static JsonElement ToJsonElement<T>(T value)
{
using JsonDocument doc = JsonDocument.Parse(JsonSerializer.SerializeToUtf8Bytes(value, IpcJson.Options));
return doc.RootElement.Clone();
}
public static T? FromJsonElement<T>(JsonElement? element)
{
if (element is null)
{
return default;
}
return element.Value.Deserialize<T>(IpcJson.Options);
}
private static async Task<int> ReadExactOrEofAsync(
Stream stream,
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken)
{
int total = 0;
while (total < count)
{
int n = await stream.ReadAsync(buffer.AsMemory(offset + total, count - total), cancellationToken)
.ConfigureAwait(false);
if (n == 0)
{
return total; // EOF
}
total += n;
}
return total;
}
}

20
CommIpc/PipeName.cs Normal file
View File

@@ -0,0 +1,20 @@
using System.Diagnostics;
namespace CommIpc;
public static class PipeName
{
/// <summary>
/// Creates a pipe name that is unique per parent process instance and child id.
/// </summary>
public static string ForChild(int childId, int? parentPid = null)
{
parentPid ??= Environment.ProcessId;
return $"CommTester_{parentPid}_{childId}";
}
/// <summary>
/// Helpful for logs / debugging.
/// </summary>
public static string Describe(string pipeName) => $"\\\\.\\pipe\\{pipeName}";
}