using System;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Aging.ResourceContracts;
using log4net;
namespace Aging.Infrastructure.Broker
{
///
/// DeviceBroker 命名管道客户端通道(V3.0 §13 同机基线)。
/// 修复现有 PipeClient 的三大问题:响应关联可达、超时生效、取消可传播。
/// 帧格式:4 字节大端长度前缀 + UTF8 JSON 封套;请求-响应经 RequestId/CorrelationId 配对。
/// 线程模型:单后台读循环 + pending 字典;写入用信号量串行化。线程安全,可被多租约共享。
///
public sealed class BrokerClientChannel : IDisposable
{
private static readonly ILog Log = LogManager.GetLogger(typeof(BrokerClientChannel));
private readonly string _pipeName;
private readonly string _clientInstanceId;
private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
private readonly ConcurrentDictionary> _pending = new();
private NamedPipeClientStream? _pipe;
private Task? _readLoop;
private volatile bool _disposed;
private volatile bool _connected;
private int _inFlight;
/// 握手成功后的 Broker 纪元(重启后递增,用于本地陈旧判定)
public long ServerEpoch { get; private set; }
/// Broker 生效配置哈希(INV-04 对账参照)
public string BrokerConfigHash { get; private set; } = "";
public string BrokerId { get; private set; } = "";
public bool IsConnected => _connected && !_disposed;
/// 连接/握手默认超时(毫秒)
public int ConnectTimeoutMs { get; set; } = 3000;
/// 单次 RPC 默认等待上限(毫秒)
public int DefaultRequestTimeoutMs { get; set; } = 10_000;
public BrokerClientChannel(string pipeName, string clientInstanceId)
{
_pipeName = pipeName ?? throw new ArgumentNullException(nameof(pipeName));
_clientInstanceId = clientInstanceId ?? throw new ArgumentNullException(nameof(clientInstanceId));
}
///
/// 确保连接并完成 Hello 握手(幂等;断线后可重呼)。
/// 协议主版本不匹配时抛出 InvalidOperationException 并拒绝后续请求。
///
public async Task EnsureConnectedAsync(CancellationToken ct)
{
if (_disposed) throw new ObjectDisposedException(nameof(BrokerClientChannel));
if (_connected) return;
var pipe = new NamedPipeClientStream("localhost", _pipeName,
PipeDirection.InOut, PipeOptions.Asynchronous);
try
{
using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct))
{
timeoutCts.CancelAfter(ConnectTimeoutMs);
await pipe.ConnectAsync(timeoutCts.Token).ConfigureAwait(false);
}
}
catch (Exception ex)
{
pipe.Dispose();
throw new InvalidOperationException($"BROKER_UNREACHABLE: 无法连接 DeviceBroker 管道 [{_pipeName}]: {ex.Message}", ex);
}
_pipe = pipe;
_connected = true;
_readLoop = Task.Run(() => ReadLoopAsync(pipe, ct));
// Hello 握手
var hello = NewEnvelope(IpcMessageTypes.Hello);
var resp = await SendAsync(hello, ct).ConfigureAwait(false);
var result = BrokerIpc.DeserializeBody(resp.BodyJson);
if (result == null)
throw new InvalidOperationException("BROKER_HANDSHAKE_FAILED: Hello 响应无效");
if (result.ProtocolVersion != BrokerIpc.ProtocolVersion)
{
_connected = false;
throw new InvalidOperationException(
$"BROKER_PROTOCOL_MISMATCH: 客户端 v{BrokerIpc.ProtocolVersion}, Broker v{result.ProtocolVersion}");
}
ServerEpoch = result.ServerEpoch;
BrokerConfigHash = result.ConfigHash;
BrokerId = result.BrokerId;
Log.Info($"[BrokerChannel] 已连接 {_pipeName}: BrokerId={BrokerId}, epoch={ServerEpoch}, configHash={ShortHash(BrokerConfigHash)}");
}
///
/// 发送一帧请求并等待其关联响应。超时/断线均返回可区分的错误封套(不抛裸异常)。
///
public async Task SendAsync(IpcEnvelope request, CancellationToken ct)
{
if (_disposed) throw new ObjectDisposedException(nameof(BrokerClientChannel));
var pipe = _pipe;
if (!_connected || pipe == null)
return ErrorEnvelope(request, "BROKER_UNREACHABLE", "未连接 DeviceBroker");
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
_pending[request.RequestId] = tcs;
Interlocked.Increment(ref _inFlight);
try
{
byte[] frame;
try
{
frame = BrokerIpc.EncodeFrame(BrokerIpc.EncodeEnvelope(request));
}
catch (Exception ex)
{
return ErrorEnvelope(request, "PROTOCOL_ERROR", ex.Message);
}
// 写入串行化;pipe 半双工交错下 async 写可能挂死,故写入不加应用级超时(依赖读侧 deadline + 断线感知)
await _writeLock.WaitAsync(ct).ConfigureAwait(false);
try
{
await pipe.WriteAsync(frame, 0, frame.Length, ct).ConfigureAwait(false);
await pipe.FlushAsync(ct).ConfigureAwait(false);
}
catch (IOException ex)
{
Fault($"管道写失败: {ex.Message}");
return ErrorEnvelope(request, "BROKER_IO_ERROR", ex.Message);
}
finally
{
_writeLock.Release();
}
using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct))
{
int waitMs = request.DeadlineUtcMs > 0
? (int)Math.Max(1, request.DeadlineUtcMs - BrokerIpc.NowUtcMs())
: DefaultRequestTimeoutMs;
var delay = Task.Delay(waitMs, timeoutCts.Token);
var winner = await Task.WhenAny(tcs.Task, delay).ConfigureAwait(false);
if (winner == tcs.Task)
{
timeoutCts.Cancel(); // 释放 delay 定时器
return await tcs.Task.ConfigureAwait(false);
}
// INV-06:请求已写出且可能在途 → 结果未知(Unknown),禁止当未执行
return UnknownEnvelope(request, "BROKER_TIMEOUT",
"等待响应超时,结果未知,按未完成处理");
}
}
finally
{
_pending.TryRemove(request.RequestId, out _);
Interlocked.Decrement(ref _inFlight);
}
}
/// 便捷方法:提交一次 OneShot 设备动作,返回结果体。
public async Task SubmitAsync(BrokerIpc.SubmitBody body, string configHash, CancellationToken ct)
{
await EnsureConnectedAsync(ct).ConfigureAwait(false);
var env = NewEnvelope(IpcMessageTypes.SubmitTask);
env.ConfigHash = configHash ?? "";
env.BodyJson = BrokerIpc.SerializeBody(body);
var resp = await SendAsync(env, ct).ConfigureAwait(false);
var result = BrokerIpc.DeserializeBody(resp.BodyJson)
?? new BrokerIpc.ResultBody { State = "Unknown", ErrorCode = "BAD_RESPONSE" };
return result;
}
// ==================== 内部 ====================
private async Task ReadLoopAsync(NamedPipeClientStream pipe, CancellationToken ct)
{
var lenBuf = new byte[4];
try
{
while (!ct.IsCancellationRequested && !_disposed)
{
await ReadExactAsync(pipe, lenBuf, 4, ct).ConfigureAwait(false);
int len = BinaryPrimitives.ReadInt32BigEndian(lenBuf);
if (len <= 0 || len > BrokerIpc.MaxFrameBytes)
throw new IOException($"非法帧长度 {len}");
var payload = new byte[len];
await ReadExactAsync(pipe, payload, len, ct).ConfigureAwait(false);
IpcEnvelope env;
try
{
env = BrokerIpc.DecodeEnvelope(Encoding.UTF8.GetString(payload));
}
catch (Exception)
{
continue; // 无法解析的单帧丢弃,不影响后续关联
}
if (!string.IsNullOrEmpty(env.CorrelationId)
&& _pending.TryGetValue(env.CorrelationId, out var tcs))
{
tcs.TrySetResult(env);
}
}
}
catch (OperationCanceledException) { /* 正常退出 */ }
catch (Exception ex)
{
if (!_disposed) Fault($"读循环终止: {ex.Message}");
}
}
private static async Task ReadExactAsync(Stream s, byte[] buf, int count, CancellationToken ct)
{
int off = 0;
while (off < count)
{
int n = await s.ReadAsync(buf, off, count - off, ct).ConfigureAwait(false);
if (n <= 0) throw new EndOfStreamException("管道已关闭");
off += n;
}
}
/// 连接失效:置降级态,等待下次 EnsureConnectedAsync 重连;在途请求由其自身超时收口。
private void Fault(string reason)
{
if (!_connected) return;
_connected = false;
Log.Warn($"[BrokerChannel] 连接失效({reason}),将在下次请求时重连");
try { _pipe?.Dispose(); } catch { }
_pipe = null;
}
private IpcEnvelope NewEnvelope(string type) => new IpcEnvelope
{
ProtocolVersion = BrokerIpc.ProtocolVersion,
MessageType = type,
RequestId = Guid.NewGuid().ToString("N"),
CorrelationId = "",
ClientInstanceId = _clientInstanceId,
AuthorityId = BrokerId
};
private static IpcEnvelope ErrorEnvelope(IpcEnvelope? request, string code, string message)
=> BuildEnvelope(request, "Failed", code, message);
private static IpcEnvelope UnknownEnvelope(IpcEnvelope? request, string code, string message)
=> BuildEnvelope(request, "Unknown", code, message);
private static IpcEnvelope BuildEnvelope(IpcEnvelope? request, string state, string code, string message)
=> new IpcEnvelope
{
ProtocolVersion = BrokerIpc.ProtocolVersion,
MessageType = "Result",
CorrelationId = request?.RequestId ?? "",
BodyJson = "{\"State\":\"" + state + "\",\"ErrorCode\":\"" + code + "\",\"Readback\":\"" +
message.Replace("\"", "'") + "\"}"
};
private static string ShortHash(string h)
=> string.IsNullOrEmpty(h) ? "" : (h.Length > 8 ? h.Substring(0, 8) : h);
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_connected = false;
try { _pipe?.Dispose(); } catch { }
_pipe = null;
_writeLock.Dispose();
}
}
}