using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Aging.Infrastructure.Broker;
using Aging.ResourceContracts;
using log4net;
namespace Aging.Infrastructure.Drivers
{
///
/// Broker 传输后端:命令经命名管道 RPC 委托 DeviceBroker 宿主执行(物理会话由宿主唯一持有)。
/// 三重关卡(白名单/configHash/租约栅栏)与 SessionGate 全在宿主侧,本类只做提交与态映射。
/// 接收帧由宿主随响应以 "|PUSHED:n|EV:RECV…" 后缀捎带返回,解析后经 FramePushed 上抛(§5.4 捎带模型)。
///
internal sealed class BrokerRpcTransport : ICanTransport
{
private static readonly ILog Log = LogManager.GetLogger(typeof(BrokerRpcTransport));
private readonly BrokerClientChannel _channel;
private readonly string _deviceId;
private readonly string _configHash;
private bool _disposed;
public bool IsConnected => _channel.IsConnected;
public event Action FramePushed;
public event Action TransportBroken;
public BrokerRpcTransport(string deviceId, string pipeName, string configHash)
{
_deviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId));
_configHash = configHash ?? "";
string instanceId = $"{Environment.MachineName}/{Process.GetCurrentProcess().Id}";
_channel = new BrokerClientChannel(pipeName, instanceId);
}
// busName/nodeIndex 对 Broker 无意义(物理会话在宿主按设备端点建立),CONNECT 即注册
public Task ConnectAsync(string busName, int nodeIndex, CancellationToken ct)
=> SubmitExpectOkAsync("CONNECT", "", ct);
public Task DisconnectAsync()
=> SubmitExpectOkAsync("DISCONNECT", "", CancellationToken.None);
public async Task TransceiveAsync(string line, CancellationToken ct)
{
if (_disposed) throw new ObjectDisposedException(nameof(BrokerRpcTransport));
// line 形如 "SEND:…" 或 "QUERY:…",映射回宿主白名单 opCode + payload:
// SEND → 宿主的 device.Driver.SendCommandAsync 需完整 "SEND:frameId:data",payload 保留前缀
// QUERY → 宿主的 device.Driver.QueryAsync 需去 QUERY: 前缀的裸查询
string opCode;
string payload;
if (line.StartsWith("SEND:", StringComparison.Ordinal))
{
opCode = "SEND";
payload = line;
}
else if (line.StartsWith("QUERY:", StringComparison.Ordinal))
{
opCode = "QUERY";
payload = line.Substring("QUERY:".Length);
}
else
{
opCode = "QUERY";
payload = line;
}
var result = await SubmitAsync(opCode, payload, ct).ConfigureAwait(false);
DrainPiggyback(result.Readback, out string clean);
return clean;
}
private async Task SubmitExpectOkAsync(string opCode, string payload, CancellationToken ct)
{
var r = await SubmitAsync(opCode, payload, ct).ConfigureAwait(false);
// CONNECT/DISCONNECT 无有效读回帧,忽略捎带
_ = r.Readback;
}
private async Task SubmitAsync(string opCode, string payload, CancellationToken ct)
{
try
{
await _channel.EnsureConnectedAsync(ct).ConfigureAwait(false);
var body = new BrokerIpc.SubmitBody
{
DeviceId = _deviceId,
OperationCode = opCode,
Payload = payload ?? "",
ResourceKey = _deviceId,
ClientInstanceId = $"{Environment.MachineName}/{Process.GetCurrentProcess().Id}"
};
var r = await _channel.SubmitAsync(body, _configHash, ct).ConfigureAwait(false);
ThrowForState(opCode, r);
return r;
}
catch (Exception ex) when (!(ex is ObjectDisposedException))
{
Log.Error($"[BrokerRpcCAN] {_deviceId} {opCode} 失败: {ex.Message}", ex);
throw;
}
}
/// 从 readback 尾部剥离 |PUSHED:n|EV:RECV… 段并触发 FramePushed。
private void DrainPiggyback(string readback, out string clean)
{
clean = readback ?? "";
if (string.IsNullOrEmpty(clean)) return;
int idx = clean.IndexOf("|PUSHED:", StringComparison.Ordinal);
if (idx < 0) return;
string pushSection = clean.Substring(idx + "|PUSHED:".Length);
clean = clean.Substring(0, idx);
// pushSection 形如 "n|EV:RECV:…|EV:RECV:…"
var segs = pushSection.Split('|');
for (int i = 1; i < segs.Length; i++)
{
if (segs[i].StartsWith("EV:", StringComparison.Ordinal))
FramePushed?.Invoke(segs[i]);
}
}
private void ThrowForState(string opCode, BrokerIpc.ResultBody r)
{
switch (r.State)
{
case "Succeeded":
return;
case "RejectedStaleLease":
throw new InvalidOperationException($"LEASE_STALE: {_deviceId} 租约栅栏校验失败: {r.ErrorCode} {r.Readback}");
case "RejectedConfigMismatch":
throw new InvalidOperationException($"CONFIG_MISMATCH: Broker 生效配置与本地快照不一致(INV-04): {r.Readback}");
case "RejectedQueueFull":
throw new InvalidOperationException($"QUEUE_FULL: {r.Readback}");
case "Unknown":
throw new TimeoutException($"UNKNOWN: {_deviceId} 动作 {opCode} 结果未知,禁止重试假定未执行(INV-06): {r.Readback}");
default:
throw new InvalidOperationException($"Broker 执行失败[{r.ErrorCode}]: {r.Readback}");
}
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_channel.Dispose();
}
}
}