<!DOCTYPE html>
|
<html lang="zh-CN">
|
<head>
|
<meta charset="UTF-8">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<title>仪器注册与硬件调试 · 软件逻辑与时序梳理(V3.0 混合架构 · 2026-11-09)</title>
|
<style>
|
:root{--bg:#f6f7f9;--card:#fff;--ink:#24292f;--sub:#57606a;--line:#d0d7de;--blue:#0b5cad;--green:#1a7f37;--red:#cf222e;--orange:#bc4c00;--purple:#8250df;--code:#f0f2f5}
|
*{box-sizing:border-box}body{margin:0;font-family:"Microsoft YaHei","Segoe UI",sans-serif;background:var(--bg);color:var(--ink);line-height:1.7}
|
.wrap{max-width:1080px;margin:0 auto;padding:24px 20px 80px}
|
header{background:linear-gradient(135deg,#0b3d6e,#0b5cad 60%,#127fc4);color:#fff;border-radius:12px;padding:26px 30px;margin-bottom:22px}
|
header h1{margin:0 0 8px;font-size:24px}header p{margin:2px 0;opacity:.88;font-size:13.5px}
|
.badge{display:inline-block;background:rgba(255,255,255,.16);border:1px solid rgba(255,255,255,.35);border-radius:20px;padding:1px 12px;font-size:12px;margin:6px 8px 0 0}
|
h2{font-size:19px;margin:34px 0 12px;padding-left:12px;border-left:5px solid var(--blue)}
|
h3{font-size:15.5px;margin:22px 0 8px;color:#0b3d6e}
|
.card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:16px 20px;margin:10px 0}
|
pre.seq{background:#0f1720;color:#cfe3f5;border-radius:8px;padding:14px 16px;overflow-x:auto;font-family:Consolas,monospace;font-size:12.5px;line-height:1.55}
|
pre.seq b{color:#7fd0ff}pre.seq i{color:#85e3a5;font-style:normal}pre.seq u{color:#ffc46b;text-decoration:none}pre.seq s{color:#ff9b9b;text-decoration:none}
|
table{border-collapse:collapse;width:100%;margin:10px 0;font-size:13px}
|
th{background:#eef3f8;text-align:left}th,td{border:1px solid var(--line);padding:6px 10px;vertical-align:top}
|
code{background:var(--code);border-radius:4px;padding:1px 6px;font-family:Consolas,monospace;font-size:12.5px}
|
.tag{display:inline-block;border-radius:4px;padding:0 8px;font-size:11.5px;font-weight:600;color:#fff;margin-right:6px;white-space:nowrap}
|
.t-local{background:var(--green)}.t-broker{background:var(--blue)}.t-warn{background:var(--orange)}.t-err{background:var(--red)}.t-core{background:var(--purple)}
|
.note{background:#fff8e6;border:1px solid #e8d48a;border-radius:8px;padding:10px 14px;font-size:13px;margin:10px 0}
|
.fixed{background:#e8f5ec;border:1px solid #9fd3ae;border-radius:8px;padding:10px 14px;font-size:13px;margin:10px 0}
|
ul,ol{margin:6px 0;padding-left:24px}li{margin:3px 0}
|
.small{font-size:12px;color:var(--sub)}
|
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px}@media(max-width:820px){.grid2{grid-template-columns:1fr}}
|
.toc a{display:inline-block;margin:2px 10px 2px 0;font-size:13px;color:var(--blue);text-decoration:none}.toc a:hover{text-decoration:underline}
|
</style>
|
</head>
|
<body><div class="wrap">
|
|
<header>
|
<h1>仪器注册与硬件调试 · 软件逻辑与时序梳理</h1>
|
<p>依据《Local + DeviceBroker 混合架构详细设计说明书 V3.0》与当前代码实况整理(含近期缺陷修复后的最终逻辑)</p>
|
<p class="small">范围:Plugin.DeviceConfig(仪器仪表注册) · Plugin.DeviceDebug(硬件调试) · Aging.Infrastructure / Aging.ResourceContracts(基础设施与契约) · DeviceBroker.exe(共享仪器宿主) · Plugin.IntegrationTest(集成测试环境)</p>
|
<span class="badge">状态:现行实现基线 2026-11-09</span><span class="badge">net472 / WPF / SQLite / Named Pipes</span><span class="badge">两进程仲裁域:local-main + broker-b01</span>
|
</header>
|
|
<div class="card toc">
|
<b>目录</b>:
|
<a href="#s1">1 进程与数据拓扑</a><a href="#s2">2 启动初始化时序</a><a href="#s3">3 仪器注册逻辑</a><a href="#s4">4 驱动分派与配置哈希</a><a href="#s5">5 调试页选设备时序</a><a href="#s6">6 维护模式状态机与租约</a><a href="#s7">7 报文收发全链路</a><a href="#s8">8 异常与自愈机制</a><a href="#s9">9 关键代码索引</a><a href="#s10">10 已知限制</a>
|
</div>
|
|
<!-- ============================================================ -->
|
<h2 id="s1">1 进程与数据拓扑</h2>
|
<div class="card">
|
<pre class="seq">
|
┌───────────────────────── 测试/操作员工作站(单机) ─────────────────────────┐
|
│ │
|
│ <b>Plugin.IntegrationTest.exe</b>(主程序进程,一切 UI 与业务调用方的宿主) │
|
│ ├─ Plugin.DeviceConfig 仪器仪表注册页(编辑草稿/发布门禁/版本管理) │
|
│ ├─ Plugin.DeviceDebug 硬件调试页(会话状态机/维护申请/CAN·电源·PLC面板)│
|
│ ├─ Aging.Infrastructure DeviceManager / DeviceService / AutoDeviceFacade │
|
│ │ ├─ Local 设备 → 直驱驱动(CanDriverAdapter 等,物理会话在本进程) │
|
│ │ └─ Broker 设备 → <b>BrokeredCanDriver 代理驱动</b>(不持物理会话) │
|
│ └─ LayeredResourceManager ──┐(进程内缓存 grants + 委托 DbLeaseStore) │
|
│ │ │
|
│ 命名管道 RPC │ ┌───────────────────────────────────┤
|
│ aging.<u>brokerId</u>.v1(帧协议) │ │ <b>DeviceBroker.exe</b>(共享仪器宿主进程)│
|
│ ◄──────────────────────┴────────┤ BrokerServer:三重关卡+SessionGate │
|
│ │ DbLeaseStore(broker-b01) 读仲裁库 │
|
│ │ CanDriverAdapter → 物理会话唯一持有│
|
│ └───────────────────────────────────┘
|
│ 数据层(本机 SQLite,禁止网络盘): │
|
│ <i>data.db</i> 配置库:DeviceDefinition(含执行位置5列)/DeviceChannel/Slot映射/版本 │
|
│ <i>arb.db</i> 仲裁库:resource_state/lease_history/recovery_checkpoint/authority_meta │
|
│ <u>debug.db</u> 调试会话库:debug_session(状态机持久化)+操作日志 │
|
└────────────────────────────────────────────────────────────────────────────┘
|
</pre>
|
<p><span class="tag t-local">Local 直连</span>适合已分配给唯一执行进程、可独立连接的设备;物理会话在客户端进程内。<br>
|
<span class="tag t-broker">Broker 代理</span>适合跨进程共享设备:物理会话由 DeviceBroker.exe <b>唯一持有</b>,客户端只做 RPC 委托。</p>
|
<p class="small">注:CAN 总线模拟器(CanSimulator)是<b>进程内</b>虚拟总线——主程序与宿主各自持有一套 CAN_BUS_1 实例,跨进程不可见;真实硬件(物理卡/总线)天然共享,无此限制。测试环境用同宿主的 CAN-004 作为 CAN-003 的对端。</p>
|
</div>
|
|
<!-- ============================================================ -->
|
<h2 id="s2">2 启动初始化时序(MainWindow.InitializePlugins)</h2>
|
<div class="card">
|
<pre class="seq">
|
1. DeviceConfig.InitFinish → 建库/迁移 data.db(PRAGMA 检查→ALTER 补执行位置5列)
|
2. SeedPresetCanDevices() → 预置 CAN-001/002/003(<b>必须先于基础设施</b>:Bootstrapper 扫库决定驱动注册)
|
3. InitializeAutoServices()
|
3.1 InfrastructureBootstrapper.Initialize(data.db 路径)
|
a. DbLeaseStore(arb.db, "local-main")
|
b. <u>ReconcileAfterBoot</u> ← 启动对账:SweepExpired + ApproveRecovery 回收上次进程泄漏租约
|
c. LayeredResourceManager 注册进 UtilContainer("ResourceManager"/"LeaseCoordinator")
|
d. DeviceManager / DeviceService / PowerSupplyService / AutoDeviceFacade / CanBusService
|
e. <b>RegisterBrokeredDrivers</b> ← 扫 data.db:ExecutionMode='Broker' 的设备注册
|
BrokeredCanDriver(管道=aging.{BrokerId}.v1, 携启动时 configHash)
|
3.2 从 UtilContainer 拉取 IAutoDeviceFacade / ICanBusService,注册 Local CAN 直驱
|
3.3 <b>TryStartDeviceBroker()</b> ← 若存在 Broker 设备:Process.Start DeviceBroker.exe
|
--db data.db --arb arb.db --broker broker-b01
|
4. DeviceDebug.InitFinish → 从 UtilContainer 取 DeviceManager/DeviceService/ResourceManager
|
<b>(必须在 3 之后!倒置会取到 null,仲裁/驱动复用链路全部静默降级)</b>
|
5. ForceRefreshForTest()×2 → 两插件列表刷新
|
Window_Closing:Kill DeviceBroker 子进程(其物理会话随进程消亡,租约靠 TTL+启动对账收口)
|
</pre>
|
<div class="fixed">✅ <b>修复记录</b>:原时序中 DeviceDebug.InitFinish 早于 Bootstrapper.Initialize,导致调试页取到 null 服务——维护仲裁被绕过、Broker 设备连接状态恒"未连接"、面板退化为本地直驱。现已调整为"建库→预置→基础设施→调试插件"。</div>
|
</div>
|
|
<!-- ============================================================ -->
|
<h2 id="s3">3 仪器注册逻辑(Plugin.DeviceConfig)</h2>
|
<div class="grid2">
|
<div class="card">
|
<h3>3.1 编辑弹窗(DeviceEditModal)字段与联动</h3>
|
<table>
|
<tr><th>区块</th><th>字段</th><th>规则</th></tr>
|
<tr><td rowspan="5">设备信息</td><td>DeviceId</td><td>新增可编辑、编辑只读;保存时查重</td></tr>
|
<tr><td>Kind / DriverType</td><td>类型联动驱动建议(CanDevice→CAN,PowerSupply→SCPI…)</td></tr>
|
<tr><td>Transport</td><td>Tcp/Serial/Visa/SDK/Can</td></tr>
|
<tr><td>Endpoint</td><td>按 Transport 校验格式并更新提示(CAN:总线:节点)</td></tr>
|
<tr><td>通道列表</td><td>ChannelId 唯一,能力/电压/电流/SessionKey</td></tr>
|
<tr><td rowspan="5">执行位置<br>(V3.0 §05)</td><td>ExecutionMode</td><td>单选 <span class="tag t-local">Local</span>(默认)/<span class="tag t-broker">Broker</span>;与仲裁域<b>双向联动</b></td></tr>
|
<tr><td>AuthorityId</td><td>必填下拉,候选来自 <code>V3AuthorityCatalog</code>(appSettings <code>V3.AuthorityIds</code>,未配置回退 <code>local-main;broker-b01</code>)</td></tr>
|
<tr><td>BrokerId</td><td>仅 Broker 显示;候选 <code>V3.BrokerIds</code>;门禁要求 BrokerId==AuthorityId</td></tr>
|
<tr><td>OwnerProcessId</td><td>只读=当前进程 PID(MVP 占位);<b>不入配置哈希</b>(运行期信息)</td></tr>
|
<tr><td>PhysicalSessionKey</td><td>仅 Broker;候选为端点规范化的写死字符串:<code>can/{bus}/main</code>、<code>tcp/{ip:port}/main</code>、<code>serial/{COM}/main</code>、<code>{deviceId}/main</code>、已存通道 SessionKey</td></tr>
|
<tr><td>访问策略</td><td>AccessPolicy</td><td><b>只读派生</b>不落库:Local→"独占直连·本进程持有物理会话";Broker→"共享仲裁·Broker SessionGate 代理"</td></tr>
|
</table>
|
<p class="small">联动规则:选 Broker → 仲裁域自动切到首个 Broker 候选;选 local- 域 → 自动切回 Local。XAML 初始化期事件加控件空引用守卫。</p>
|
</div>
|
<div class="card">
|
<h3>3.2 数据模型与持久化</h3>
|
<ul>
|
<li><code>DeviceDefinition</code> 表 13 列 = 原 8 列 + 执行位置 5 列;旧库经 <code>PRAGMA table_info</code> 检查后 <code>ALTER TABLE ADD COLUMN</code> 迁移,存量数据默认 <code>Local/空AuthorityId</code>。</li>
|
<li>实体默认值 <code>ExecutionMode=Local</code>;读取端 <code>HasColumn</code> 容错缺列。</li>
|
<li>批量生成(BatchGenerateModal)走同一 <code>SaveDevice</code>,新列取默认。</li>
|
</ul>
|
<h3>3.3 版本状态机(发布工作流 §05)</h3>
|
<pre class="seq">
|
Draft ──保存草稿──► (重算 hash 落库)
|
Draft ──发布(PublishVersion)──► ① ValidatePublishGate() 拦截
|
② 通过→重算 hash→status=Published
|
Published ──应用(Apply)──► Effective(旧 Effective 退回 Published)
|
</pre>
|
<h3>3.4 发布门禁校验项(ValidatePublishGate)</h3>
|
<ol>
|
<li>AuthorityId 必填且在配置枚举内(存量空值 → 提示"旧数据请重新保存")</li>
|
<li>Local 设备的域必须 <code>local-</code> 前缀</li>
|
<li>Broker 设备:BrokerId 必填、在枚举内、且与 AuthorityId 一致(域一致)</li>
|
<li>Broker 设备:PhysicalSessionKey 必填</li>
|
<li>同一 PhysicalSessionKey 只允许一个宿主域("同一会话只能有一个宿主")</li>
|
</ol>
|
<div class="note">任一失败 → 弹窗列出错误(最多显示 8 条)并阻止发布;冲突检测(IP/Slot/越界)仍走 <code>DetectConflicts</code>。</div>
|
</div>
|
</div>
|
|
<!-- ============================================================ -->
|
<h2 id="s4">4 驱动分派与配置哈希(INV-04)</h2>
|
<div class="card">
|
<pre class="seq">
|
注册页保存设备(含执行位置) ──► data.db
|
│
|
Bootstrapper 启动扫描 ▼
|
ExecutionMode='Local' ──► DriverFactory.CreateDriver ──► 直驱(本进程物理会话)
|
ExecutionMode='Broker' ──► <b>BrokeredCanDriver</b>(deviceId, aging.{BrokerId}.v1, hash₀)
|
│ hash₀ = DeviceConfigHash.Compute(data.db)
|
│ 规则与 DeviceConfigModel.ComputeConfigHash 完全一致:
|
│ DEVICE(id|kind|driver|transport|endpoint|<b>mode|authority|broker|sessionKey</b>)
|
│ + CHANNEL + SLOT 三段拼接 SHA256;OwnerProcessId 不参与
|
▼ 每次 RPC 携带
|
宿主出队时与自己重算的生效 hash 对账 → 不一致 RejectedConfigMismatch
|
</pre>
|
<ul>
|
<li><b>分派点在驱动层</b>:<code>DeviceService</code> 不感知 Local/Broker——它只从 <code>DeviceManager.GetDriver(deviceId)</code> 取驱动。上层(自动化门面、调试面板)零改动。</li>
|
<li>配置哈希 = 快照指纹:Broker 用 <code>DeviceConfigHash.Compute</code>(与注册页同算法,缺表跳过空段)每 2s 惰性重算。</li>
|
</ul>
|
</div>
|
|
<!-- ============================================================ -->
|
<h2 id="s5">5 调试页选设备时序(OnDeviceSelected)</h2>
|
<div class="card">
|
<pre class="seq">
|
设备下拉变更
|
1. LoadDeviceInfo(deviceId) ← 读 data.db:Kind/Endpoint/EffectiveVersion
|
2. OpenSession(deviceId) ← DebugSessionManager
|
a. GetActiveSessionByDevice → 命中且<b>在本进程内存</b> → 复用返回
|
b. 命中但仅存在于 debug.db(进程重启遗留死会话)→ <u>强制回收为 Closed</u> 再新建
|
c. 新建 session(State=ReadOnly,写 debug_session 表 + 入内存缓存)
|
3. DeviceManager.ConnectAsync(deviceId)
|
<span class="tag t-local">Local</span> 直驱 ConnectAsync:注册总线节点/建立 TCP…
|
<span class="tag t-broker">Broker</span> BrokeredCanDriver → RPC CONNECT(<b>无租约也允许</b>:仅登记宿主物理会话,无总线输出)
|
异常→仅连接状态显示"连接失败",不抛出
|
4. UpdateButtonStates() ← "进入维护"仅 ReadOnly 时可用
|
5. LoadPanel(kind)
|
CAN 面板:ExistingDriver = DeviceManager.GetDriver(deviceId)
|
├─ 非 null(Broker 设备)→ 面板复用该驱动,<b>禁止本地直驱</b>(不 Dispose 外部驱动)
|
└─ null(未注册)→ 降级本地直驱(模拟器语义,owns=true)
|
面板连接后:DispatcherTimer 100ms 轮询启动(UpdateStats + 代理驱动邮箱抽取)
|
顶部"连接状态"与面板内"状态"为两个指标:前者=DeviceManager 通路,后者=面板实际使用的驱动
|
</pre>
|
</div>
|
|
<!-- ============================================================ -->
|
<h2 id="s6">6 维护模式状态机与租约时序</h2>
|
<div class="grid2">
|
<div class="card">
|
<h3>6.1 会话状态机</h3>
|
<pre class="seq">
|
RequestMaintenance ✔ ActivateMaintenance
|
ReadOnly ────────────────────► AcquiringMaintenance ────────────► <b>MaintenanceActive</b>
|
▲ │失败(占用/超时) │ CloseSession
|
│回收死会话(Closed) ▼ ▼
|
└──────────────────────── 保持 ReadOnly Closing → Closed
|
(释放维护租约)
|
</pre>
|
<h3>6.2 进入维护(含跨进程仲裁)</h3>
|
<pre class="seq">
|
"进入维护" → RequestMaintenance(sessionId)
|
1. 会话在内存缓存?不在→debug.db 恢复兜底;仍无→拒绝
|
2. State==ReadOnly?否则拒绝(按钮已按此置灰)
|
3. <b>取/建会话→令牌缓存 _sessionOwnerTokens[sessionId]</b>
|
(ResourceOwnerToken.TokenId 是随机 GUID,<u>必须复用同一实例</u>,
|
否则 LayeredResourceManager 按 TokenId 索引的 grants 释放时找不回)
|
4. ResourceManager.ReserveAsync(token, key=deviceId)
|
→ DbLeaseStore.Acquire(arb.db BEGIN IMMEDIATE 单事务)
|
├ Acquired/Reused → Active 租约(TTL 300s,Generation+1)
|
├ ConflictHeld → 抛 ResourceConflictException → 拒绝(提示含占用者/机器/PID)
|
└ RecoveryBlocked → 拒绝(过期待回收,见 §8)
|
5. State=AcquiringMaintenance → ActivateMaintenance → MaintenanceActive
|
6. LoadPanel 重建 → 面板 IsMaintenanceMode=true(写控件解锁)
|
</pre>
|
</div>
|
<div class="card">
|
<h3>6.3 退出维护</h3>
|
<pre class="seq">
|
"退出维护" → CloseSession
|
1. State=MaintenanceActive?→ Closing
|
2. ReleaseReservationAsync(<b>复用进入时的同一令牌</b>)
|
→ DbLeaseStore.Release → Phase=0(Free),lease_history 记 Released
|
3. Closed + closed_at 落库、移出内存缓存、清令牌缓存
|
<b>校验点</b>:arb.db resource_state 应变回 Free —— 泄漏租约曾导致
|
"第二次进入维护永久失败",根因与修复见 §8/§10
|
</pre>
|
<h3>6.4 维护与写权限矩阵(宿主侧执行语义)</h3>
|
<table>
|
<tr><th>操作</th><th>租约要求</th><th>原因</th></tr>
|
<tr><td>CONNECT / DISCONNECT</td><td><i>豁免</i></td><td>仅登记/释放宿主物理会话,无总线输出</td></tr>
|
<tr><td>RECV / RECV_ALL / LOG / STATUS</td><td><i>豁免</i></td><td>纯读邮箱/状态,被动监控支持调试页只读监听</td></tr>
|
<tr><td>SEND(帧发送)</td><td><b>必须 Active</b></td><td>总线写,INV-01 禁止无租约直接 I/O</td></tr>
|
<tr><td>AUTO_RESPONSE / FILTER / PROMISCUOUS</td><td><b>必须 Active</b></td><td>有副作用的 QUERY,改变节点行为</td></tr>
|
</table>
|
<div class="fixed">✅ 因此正确操作顺序即您指出的:<b>先进入维护模式 → 才能打开自动响应/发送</b>;只读"看帧"不需要维护。</div>
|
</div>
|
</div>
|
|
<!-- ============================================================ -->
|
<h2 id="s7">7 报文收发全链路时序(Broker 设备)</h2>
|
<div class="card">
|
<h3>7.1 发送(维护模式下,CAN-003 → 总线)</h3>
|
<pre class="seq">
|
面板"发送" → BrokeredCanDriver.SendCommandAsync("SEND:0x123:55 66")
|
└─ BrokerClientChannel.EnsureConnectedAsync ← 惰性:管道断开自动重连+重握手
|
└─ <b>Hello</b> → 宿主回 {ServerEpoch, ConfigHash, BrokerId, ProtocolVersion}
|
协议主版本不符 → 断开拒绝
|
└─ <b>SubmitTask</b>(帧=4字节大端长度+UTF8 JSON 封套;RequestId 关联、deadline 超时)
|
Body={DeviceId, OpCode=SEND, Payload, ResourceKey=deviceId, ClientInstanceId}
|
▼ ─────────── DeviceBroker.exe ───────────
|
①白名单+域内设备过滤 非本域/未知op → Failed
|
②<b>configHash 对账</b>(INV-04) 不一致 → RejectedConfigMismatch("先 Apply 再 I/O")
|
③<b>栅栏再校验</b>(INV-01/03) RecoveryRequired→拒绝;无 Active 租约→RejectedStaleLease
|
④<b>SessionGate</b>(按 PhysicalSessionKey 串行原子片段)
|
队列等待 5s 超时 → Failed RESOURCE_BUSY(确定未执行,可安全重试)
|
⑤CanDriverAdapter.SendFrame(宿主进程物理/模拟总线)
|
⑥ResultBody{State, Readback, ServerEpoch} 同管道返回
|
▼ ─────────── 客户端 ───────────
|
Succeeded→返回设备响应串 | RejectedStaleLease→抛 LEASE_STALE
|
ConfigMismatch→抛异常 | Unknown(超时/片段中异常)→<u>TimeoutException 禁止假定未执行(INV-06)</u>
|
</pre>
|
<h3>7.2 接收(总线帧 → 面板显示)</h3>
|
<pre class="seq">
|
总线广播 → 宿主内 node(CAN-003) 收帧 → RxCount++,帧入 _recvBuffer + 节点邮箱
|
客户端面板 DispatcherTimer(100ms) 每 10 tick(≈1s):
|
DrainBrokerMailbox → RPC QUERY <b>"RECV_ALL"</b>(只读,<u>无需租约</u>)
|
→ 解析 "RECV_ALL:n:id:data[:ts]|…" → AddReceivedFrame(RX, 来源=<b>BUS@broker</b>)
|
RPC 失败(宿主不可达等)静默跳过,下一秒重试,不弹错误
|
(Local 直驱设备无需轮询:DriverFrameReceived 事件即时显示)
|
对端模拟:真实硬件同一总线天然互通;模拟器场景用<b>同宿主的 CAN-004</b> 作对端验证。
|
</pre>
|
<div class="fixed">✅ <b>修复记录</b>:① 代理驱动无帧事件且面板从不查邮箱 → "只有发送没有接收",现增加 RECV_ALL 只读轮询;② 宿主 <code>HandleRecvAll</code> 曾把同一帧经 buffer+邮箱双路上报(RX 重复行),已改为 buffer 优先、邮箱仅兜底。</div>
|
</div>
|
|
<!-- ============================================================ -->
|
<h2 id="s8">8 异常与自愈机制</h2>
|
<div class="card">
|
<table>
|
<tr><th>场景</th><th>机制</th><th>表现/去向</th></tr>
|
<tr><td>进程异常退出(崩溃/被杀)</td><td>租约不再续期 → TTL(300s) 过期 → <b>RecoveryRequired</b>(INV-03 禁止自动移交)</td><td>下次 Acquire 返回 RecoveryBlocked → "无法进入维护"</td></tr>
|
<tr><td>宿主/主程序<b>重启</b></td><td><b>ReconcileAfterBoot</b>:SweepExpired(过期→待回收+历史)→ 逐个 ApproveRecovery(登记读回证据进 recovery_checkpoint,Phase=Free,Generation 不回退)</td><td>启动即自愈;证据可在 arb.db 审计</td></tr>
|
<tr><td>调试页维护租约泄漏</td><td>根因:进出维护各自 new Token(TokenId 随机 GUID)→ 释放按 TokenId 查找落空。修复:会话→令牌实例缓存复用</td><td>退出维护后 resource_state 必回 Free</td></tr>
|
<tr><td>debug.db 死会话残留</td><td>OpenSession 检出"DB 有、内存无"的未关闭会话 → 回收 Closed 再新建</td><td>不再出现"会话不存在"型拒绝</td></tr>
|
<tr><td>租约过期后使用(作用域租约)</td><td>AutoDeviceLease 心跳 8s Renew;连续 2 次失败 → LeaseLost 事件 + 后续调用抛 <b>LEASE_STALE</b>(熔断)</td><td>自动化步骤快速失败而非静默半执行</td></tr>
|
<tr><td>RPC 超时/断线</td><td>响应关联超时 → State=<b>Unknown</b>(INV-06);读循环断线感知 → 下次调用惰性重连重握手</td><td>面板轮询静默重试;显式 I/O 抛异常禁止重试假定未执行</td></tr>
|
<tr><td>配置版本错位(客户端快照旧)</td><td>宿主每次出队对账 configHash → RejectedConfigMismatch</td><td>强制"先 Apply 再 I/O"</td></tr>
|
</table>
|
</div>
|
|
<!-- ============================================================ -->
|
<h2 id="s9">9 关键代码索引</h2>
|
<div class="card">
|
<table>
|
<tr><th>职责</th><th>文件</th><th>关键成员</th></tr>
|
<tr><td>启动装配</td><td>Plugin.IntegrationTest/MainWindow.xaml.cs</td><td>InitializePlugins / SeedPresetCanDevices / InitializeAutoServices / TryStartDeviceBroker</td></tr>
|
<tr><td>注册 UI/联动/校验</td><td>Plugin.DeviceConfig/DeviceEditModal.xaml(.cs)</td><td>Open / ExecMode_Changed / CbAuthority_SelectionChanged / RefreshSessionKeyCandidates / SaveBtn_Click</td></tr>
|
<tr><td>候选枚举配置</td><td>Plugin.DeviceConfig/V3AuthorityCatalog.cs + Plugin.IntegrationTest/App.config</td><td>AuthorityIds / BrokerIds / LocalAuthorityId / IsLocalAuthority</td></tr>
|
<tr><td>发布门禁/哈希/迁移</td><td>Plugin.DeviceConfig/Model/DeviceConfigModel.cs</td><td>MigrateExecutionColumns / ValidatePublishGate / ComputeConfigHash / SaveDevice</td></tr>
|
<tr><td>实体</td><td>Plugin.DeviceConfig/Entity/DeviceDefinition.cs</td><td>ExecutionModeKind / AuthorityId / BrokerId / PhysicalSessionKey / AccessPolicy(派生)</td></tr>
|
<tr><td>服务装配</td><td>Aging.Infrastructure/InfrastructureBootstrapper.cs</td><td>Initialize / RegisterBrokeredDrivers</td></tr>
|
<tr><td>跨进程租约</td><td>Aging.Infrastructure/Core/DbLeaseStore.cs · LayeredResourceManager.cs</td><td>Acquire/Renew/Release/PhaseOf/SweepExpired/ApproveRecovery/<b>ReconcileAfterBoot</b></td></tr>
|
<tr><td>维护会话状态机</td><td>Plugin.DeviceDebug/Model/DebugSessionManager.cs</td><td>OpenSession(死会话回收) / RequestMaintenance(令牌复用) / ActivateMaintenance / CloseSession</td></tr>
|
<tr><td>调试页时序</td><td>Plugin.DeviceDebug/DeviceDebug.xaml.cs · Panels/CanDebugPanel.xaml.cs</td><td>OnDeviceSelected / LoadPanel(ExistingDriver 注入) / DoConnect / PollTimer_Tick / DrainBrokerMailbox</td></tr>
|
<tr><td>Broker 客户端</td><td>Aging.Infrastructure/Broker/BrokerClientChannel.cs · Drivers/BrokeredCanDriver.cs</td><td>EnsureConnectedAsync(Hello) / SendAsync(关联+超时) / SubmitAsync / ThrowForState</td></tr>
|
<tr><td>Broker 宿主</td><td>DeviceBroker/BrokerServer.cs · Program.cs</td><td>LoadDevices / ExecuteOneShotAsync(三重关卡) / SessionGate / RunFragmentAsync / ProbeReadback</td></tr>
|
<tr><td>线协议/契约</td><td>Aging.ResourceContracts/BrokerIpc.cs · IpcEnvelope.cs · DeviceOperationTypes.cs · LeaseTypes.cs</td><td>帧编解码 / SubmitBody·ResultBody / LeaseGrant·AcquireRequest</td></tr>
|
</table>
|
</div>
|
|
<!-- ============================================================ -->
|
<h2 id="s10">10 已知限制与简化(P2 计划项)</h2>
|
<div class="card">
|
<ul>
|
<li><b>owner↔clientInstanceId 弱对齐</b>:同机 arb.db 栅栏只做存在性/相位检查,未校验申请者身份与请求连接一一对应(待签名凭据/epoch token)。</li>
|
<li><b>模拟器跨进程总线</b>:CanSimulator 进程内实例互不相通,跨进程收发仅同宿主设备间可达;真实硬件无此限制。</li>
|
<li><b>推迟能力</b>:Draining 自动让渡、周期任务/订阅/ResultOutbox、父设备 Permit 多通道仲裁、Prepare/Activate 两段式跨域预留、发布预览差异。</li>
|
<li><b>UI 待办</b>:App.Success/App.Warn 标题未本地化;退出维护后设备选择器被重置;临时诊断日志(brokerrx_diag.log)待验证通过后撤除;RecoveryRequired 尚无人工审批 UI(当前由启动对账代偿)。</li>
|
<li><b>BrokerId/OwnerProcessId 语义</b>:OwnerProcessId 为 MVP 占位(当前进程 PID),Broker 常驻进程应为其宿主 PID 且随宿主重启更新——不入哈希的设计已预留。</li>
|
</ul>
|
<p class="small">本文档与代码实况同步于 2026-11-09;实现变更请同步更新本文件。</p>
|
</div>
|
|
</div></body>
|
</html>
|