114 lines
4.2 KiB
C#
114 lines
4.2 KiB
C#
using System.IO.Pipes;
|
|
using MessagePack;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Contracts;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Ipc;
|
|
|
|
/// <summary>
|
|
/// Client-side IPC channel to a running <c>Driver.Galaxy.Host</c>. Owns the data-plane pipe
|
|
/// connection and serializes request/response round-trips. One instance per session.
|
|
/// </summary>
|
|
public sealed class GalaxyIpcClient : IAsyncDisposable
|
|
{
|
|
private readonly NamedPipeClientStream _stream;
|
|
private readonly FrameReader _reader;
|
|
private readonly FrameWriter _writer;
|
|
private readonly SemaphoreSlim _callGate = new(1, 1);
|
|
|
|
private GalaxyIpcClient(NamedPipeClientStream stream)
|
|
{
|
|
_stream = stream;
|
|
_reader = new FrameReader(stream, leaveOpen: true);
|
|
_writer = new FrameWriter(stream, leaveOpen: true);
|
|
}
|
|
|
|
/// <summary>Connects, sends Hello with the shared secret, and awaits HelloAck. Throws on rejection.</summary>
|
|
public static async Task<GalaxyIpcClient> ConnectAsync(
|
|
string pipeName, string sharedSecret, TimeSpan connectTimeout, CancellationToken ct)
|
|
{
|
|
var stream = new NamedPipeClientStream(
|
|
serverName: ".",
|
|
pipeName: pipeName,
|
|
direction: PipeDirection.InOut,
|
|
options: PipeOptions.Asynchronous);
|
|
|
|
await stream.ConnectAsync((int)connectTimeout.TotalMilliseconds, ct);
|
|
|
|
var client = new GalaxyIpcClient(stream);
|
|
try
|
|
{
|
|
await client._writer.WriteAsync(MessageKind.Hello,
|
|
new Hello { PeerName = "Galaxy.Proxy", SharedSecret = sharedSecret }, ct);
|
|
|
|
var ack = await client._reader.ReadFrameAsync(ct);
|
|
if (ack is null || ack.Value.Kind != MessageKind.HelloAck)
|
|
throw new InvalidOperationException("Did not receive HelloAck from Galaxy.Host");
|
|
|
|
var ackMsg = FrameReader.Deserialize<HelloAck>(ack.Value.Body);
|
|
if (!ackMsg.Accepted)
|
|
throw new UnauthorizedAccessException($"Galaxy.Host rejected Hello: {ackMsg.RejectReason}");
|
|
|
|
return client;
|
|
}
|
|
catch
|
|
{
|
|
await client.DisposeAsync();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>Round-trips a request and returns the first frame of the response.</summary>
|
|
public async Task<TResp> CallAsync<TReq, TResp>(
|
|
MessageKind requestKind, TReq request, MessageKind expectedResponseKind, CancellationToken ct)
|
|
{
|
|
await _callGate.WaitAsync(ct);
|
|
try
|
|
{
|
|
await _writer.WriteAsync(requestKind, request, ct);
|
|
|
|
var frame = await _reader.ReadFrameAsync(ct);
|
|
if (frame is null) throw new EndOfStreamException("IPC peer closed before response");
|
|
|
|
if (frame.Value.Kind == MessageKind.ErrorResponse)
|
|
{
|
|
var err = MessagePackSerializer.Deserialize<ErrorResponse>(frame.Value.Body);
|
|
throw new GalaxyIpcException(err.Code, err.Message);
|
|
}
|
|
|
|
if (frame.Value.Kind != expectedResponseKind)
|
|
throw new InvalidOperationException(
|
|
$"Expected {expectedResponseKind}, got {frame.Value.Kind}");
|
|
|
|
return MessagePackSerializer.Deserialize<TResp>(frame.Value.Body);
|
|
}
|
|
finally { _callGate.Release(); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fire-and-forget request — used for unsubscribe, alarm-ack, close-session, and other
|
|
/// calls where the protocol is one-way. The send is still serialized through the call
|
|
/// gate so it doesn't interleave a frame with a concurrent <see cref="CallAsync{TReq, TResp}"/>.
|
|
/// </summary>
|
|
public async Task SendOneWayAsync<TReq>(MessageKind requestKind, TReq request, CancellationToken ct)
|
|
{
|
|
await _callGate.WaitAsync(ct);
|
|
try { await _writer.WriteAsync(requestKind, request, ct); }
|
|
finally { _callGate.Release(); }
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
_callGate.Dispose();
|
|
_reader.Dispose();
|
|
_writer.Dispose();
|
|
await _stream.DisposeAsync();
|
|
}
|
|
}
|
|
|
|
public sealed class GalaxyIpcException(string code, string message)
|
|
: Exception($"[{code}] {message}")
|
|
{
|
|
public string Code { get; } = code;
|
|
}
|