Initial commit
This commit is contained in:
6
CommIpc/Class1.cs
Normal file
6
CommIpc/Class1.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace CommIpc;
|
||||
|
||||
public class Class1
|
||||
{
|
||||
|
||||
}
|
||||
9
CommIpc/CommIpc.csproj
Normal file
9
CommIpc/CommIpc.csproj
Normal 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
15
CommIpc/IpcFrame.cs
Normal 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
12
CommIpc/IpcJson.cs
Normal 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
16
CommIpc/IpcKinds.cs
Normal 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
129
CommIpc/IpcProtocol.cs
Normal 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
20
CommIpc/PipeName.cs
Normal 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}";
|
||||
}
|
||||
Reference in New Issue
Block a user