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(); } } }