wenxiangchao
4 天以前 559890c482f5973f06aafdfec9550fee5ba3c7f2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>多进程/多线程场景下的仪器资源独占与共享方案</title>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<style>
    * { box-sizing: border-box; }
    body {
        font-family: -apple-system, "Segoe UI", "Microsoft YaHei", "PingFang SC", sans-serif;
        max-width: 1240px; margin: 0 auto; padding: 32px 24px 80px;
        background: #FAFAFA; color: #212121; line-height: 1.75;
    }
    h1 { font-size: 29px; color: #1565C0; border-bottom: 3px solid #1976D2; padding-bottom: 12px; }
    h2 { font-size: 23px; color: #0D47A1; margin-top: 42px; padding-left: 12px; border-left: 4px solid #FF9800; }
    h3 { font-size: 17px; color: #37474F; margin-top: 26px; }
    h4 { font-size: 15px; color: #455A64; margin-top: 18px; }
    .subtitle { color: #757575; font-size: 14px; margin-bottom: 24px; }
    .card { background: white; border-radius: 8px; padding: 18px 22px; margin: 16px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
    .highlight { background: #FFF8E1; border-left: 4px solid #FFC107; padding: 12px 16px; margin: 12px 0; border-radius: 4px; }
    .info { background: #E3F2FD; border-left: 4px solid #2196F3; padding: 12px 16px; margin: 12px 0; border-radius: 4px; }
    .success { background: #E8F5E9; border-left: 4px solid #4CAF50; padding: 12px 16px; margin: 12px 0; border-radius: 4px; }
    .warn { background: #FBE9E7; border-left: 4px solid #FF5722; padding: 12px 16px; margin: 12px 0; border-radius: 4px; }
    table { width: 100%; border-collapse: collapse; margin: 16px 0; background: white; border-radius: 6px; overflow: hidden; box-shadow: 0 1px 4px rgba(0,0,0,0.06); font-size: 13.5px; }
    th { background: #1976D2; color: white; padding: 9px 12px; text-align: left; font-weight: 600; }
    td { padding: 8px 12px; border-bottom: 1px solid #EEEEEE; vertical-align: top; }
    tr:nth-child(even) td { background: #FAFAFA; }
    tr:hover td { background: #E3F2FD; }
    code { font-family: "Consolas", "Monaco", monospace; background: #F5F5F5; color: #C62828; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
    pre { background: #263238; color: #ECEFF1; padding: 16px 20px; border-radius: 6px; overflow-x: auto; font-family: "Consolas", monospace; font-size: 13px; line-height: 1.55; }
    pre code { background: transparent; color: inherit; padding: 0; font-size: 13px; }
    .mermaid { background: white; padding: 20px; border-radius: 8px; margin: 20px 0; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.06); overflow-x: auto; }
    ul { padding-left: 22px; } li { margin: 4px 0; }
    .toc { background: #F5F5F5; padding: 16px 24px; border-radius: 6px; margin: 20px 0; }
    .toc a { color: #1976D2; text-decoration: none; display: block; padding: 3px 0; }
    .toc a:hover { text-decoration: underline; }
    .kwd { color: #FFCC80; } .str { color: #C5E1A5; } .cmt { color: #546E7A; } .fn { color: #82AAFF; }
    .good { color: #2E7D32; font-weight: 600; } .bad { color: #C62828; font-weight: 600; } .mid { color: #EF6C00; font-weight: 600; }
    footer { margin-top: 60px; padding-top: 20px; border-top: 1px solid #E0E0E0; color: #757575; font-size: 12px; text-align: center; }
    .path { font-family: Consolas, monospace; font-size: 12.5px; background: #ECEFF1; color: #37474F; padding: 1px 6px; border-radius: 3px; }
</style>
</head>
<body>
 
<h1>多进程 / 多线程场景下的仪器资源独占与共享方案</h1>
<div class="subtitle">TestPlatform Aging Software · SQLite 设备注册 + Aging.Infrastructure 租约 + OpenTAP 序列执行器 三栈融合设计 · 2026-11-03</div>
 
<div class="toc">
    <strong>目录</strong>
    <a href="#s1">1. 现状盘点与问题定义</a>
    <a href="#s2">2. 两类并发场景的严格区分</a>
    <a href="#s3">3. 五个候选方案逐一评估与横向对比</a>
    <a href="#s4">4. 推荐目标架构:分层混合(租约表 + 仲裁规则 + 按需 Broker)</a>
    <a href="#s5">5. 租约完整生命周期:申请 / 续期 / 释放 / 超时抢占</a>
    <a href="#s6">6. 全自动 vs 手动维护的互斥仲裁规则</a>
    <a href="#s7">7. 异常场景处理:死锁 / 孤儿锁 / 时钟 / 幂等</a>
    <a href="#s8">8. 独占 or 分时复用:设备分组与 Slot 绑定策略</a>
    <a href="#s9">9. 本仓库改造清单(具体文件路径 + 接口签名 + DDL)</a>
    <a href="#s10">10. 测试要点:单元 / 多进程集成 / 崩溃演练</a>
    <a href="#s11">11. 分阶段实施计划与工作量估算</a>
    <a href="#s12">12. 风险、反模式与不推荐做法</a>
    <a href="#s13">13. 结论</a>
</div>
 
<!-- ================================================================= -->
<h2 id="s1">1. 现状盘点与问题定义</h2>
 
<div class="card">
<strong>先说结论:你们的架构已经"半只脚"踩在正确方案上。</strong>
<code>ResourceOwnerToken</code>(含 Generation 世代号)、<code>reservation_ref</code> 字段、<code>IAutoDeviceLease</code> 作用域——这些概念与工业界标准的"租约(Lease)+ 栅栏令牌(Fencing Token)"模型一一对应。唯一的问题是:<strong>整套机制活在内存里</strong>。
</div>
 
<h3>1.1 现状代码事实核查</h3>
<table>
    <tr><th>组件</th><th>文件</th><th>现状</th><th>跨进程能力</th></tr>
    <tr>
        <td>ResourceManager</td>
        <td><span class="path">Aging.Infrastructure/Core/ResourceManager.cs</span></td>
        <td><code>ConcurrentDictionary</code> 存预约 + <code>SemaphoreSlim(1,1)</code> 每资源细锁;Generation 全局自增</td>
        <td class="bad">❌ 纯进程内存,其他进程完全不可见</td>
    </tr>
    <tr>
        <td>ResourceOwnerToken</td>
        <td><span class="path">Contracts/ResourceOwnerToken.cs</span></td>
        <td>TokenId + OwnerType(Production/Maintenance) + Generation + IssuedAt/ExpiresAt</td>
        <td class="mid">⚠️ 结构可序列化,但目前没有跨进程校验路径</td>
    </tr>
    <tr>
        <td>AutoDeviceFacade / Lease</td>
        <td><span class="path">Services/AutoDeviceFacade.cs</span></td>
        <td>Reserve → DeviceService 分派 → finally Release;Lease 作用域复用</td>
        <td class="bad">❌ 依赖内存版 ResourceManager</td>
    </tr>
    <tr>
        <td>DeviceService</td>
        <td><span class="path">Services/DeviceService.cs</span></td>
        <td>执行前 <code>ValidateOwner</code>(内存查表)→ <code>AcquireOperationAsync</code> → driver 下发</td>
        <td class="bad">❌ 校验仅本进程有效</td>
    </tr>
    <tr>
        <td>DebugSessionManager</td>
        <td><span class="path">Plugin.DeviceDebug/Model/DebugSessionManager.cs</span></td>
        <td>RequestMaintenance 已捕获 <code>ResourceConflictException</code>,并把 <code>reservation.ReservationId</code> 写入 <code>session.ReservationRef</code> 持久化</td>
        <td class="mid">⚠️ 已预留挂接点,差一个"活的"仲裁源</td>
    </tr>
    <tr>
        <td>debug_session 表</td>
        <td><span class="path">Plugin.DeviceDebug/Model/DeviceDebugModel.cs</span></td>
        <td>有 <code>reservation_ref TEXT</code>、<code>state</code>、<code>closed_at</code>,无心跳/过期列</td>
        <td class="mid">⚠️ 可扩展</td>
    </tr>
    <tr>
        <td>IPC 设施</td>
        <td><span class="path">ConnectionUtil/PipeServer.cs · PipeClient.cs</span></td>
        <td>命名管道(Message 模式)已实现:多实例服务端、连接事件、请求-响应 SendMessage(带超时)</td>
        <td class="good">✅ 可直接作为 Device Broker 的传输层,无需新造轮子</td>
    </tr>
    <tr>
        <td>OpenTAP SynchronizationManager</td>
        <td>OpenTAP 9.31 引擎</td>
        <td>进程内 <code>RequestLock(resource, timeout)</code> 按仪器互斥</td>
        <td class="bad">❌ 官方文档明确:仅进程内</td>
    </tr>
</table>
 
<h3>1.2 问题本质拆解</h3>
<div class="highlight">
"多工位共用一台仪器"其实包含 <strong>三个不同层面</strong>的问题,必须分开解决,混在一起谈必然方案走形:
<ol>
<li><strong>仲裁(Arbitration)</strong>:谁<em>有权</em>在这台仪器上发命令?→ 需要跨进程可见、可持久化、崩溃可回收的<strong>租约状态</strong>。</li>
<li><strong>连接独占(Connection)</strong>:物理句柄(COM 口、USB、CAN 卡)同一时刻只能被一个进程 <code>open</code>;网口仪器则允许同机多连接但需要"逻辑独占"语义。→ 需要把<strong>连接持有者与租约持有者绑定</strong>,必要时引入代理进程。</li>
<li><strong>命令串行化(Serialization)</strong>:分时复用型设备(如共享温箱),允许多工位排队下发原子命令片段。→ 需要<strong>队列/栅栏令牌</strong>,而非独占租约。</li>
</ol>
</div>
 
<!-- ================================================================= -->
<h2 id="s2">2. 两类并发场景的严格区分</h2>
 
<div class="mermaid">
graph TB
    subgraph 场景A["场景 A:同进程多线程(单 HMI 主程序内)"]
        A1["OpenTAP 并行步骤 / MultiSite 调度线程"] --- A2["WPF UI 线程(手动调试)"]
        A3["ResultProxy 落库线程"]
    end
    subgraph 场景B["场景 B:跨进程 / 跨机器"]
        B1["工位进程1 tap.exe/Station1.exe"]
        B2["工位进程2 Station2.exe"]
        B3["HMI 主程序(维护工程师)"]
        B4["其他机器上的执行节点<br/>(多机共测一台仪器)"]
    end
    subgraph 解决手段["分别对应的手段"]
        S1["OpenTAP SynchronizationManager<br/>+ 现有内存版 ResourceManager<br/>(保持不变 ✅)"]
        S2["持久化租约表(DB Lease)<br/>+ Fencing Generation<br/>+ 心跳 TTL + 仲裁策略<br/>(本次新建 ⭐)"]
    end
    场景A --> S1
    场景B --> S2
</div>
 
<table>
    <tr><th>维度</th><th>场景 A:进程内多线程</th><th>场景 B:跨进程/跨机器</th></tr>
    <tr><td>典型触发</td><td>TestPlan 中 Parallel 步骤 / MultiSite 同时跑不同分支</td><td>多工位独立进程、HMI 与自动测试并行、CI 节点</td></tr>
    <tr><td>互斥原语</td><td>SemaphoreSlim / lock / SynchronizationManager.RequestLock</td><td>OS 命名内核对象 / DB 租约 / Broker 队列</td></tr>
    <tr><td>崩溃表现</td><td>进程死 → 锁随进程消失(内核回收)</td><td class="bad">持有者死亡后锁<strong>残留</strong>,必须靠 TTL/看门狗回收</td></tr>
    <tr><td>可见性</td><td>天然可见(同一堆)</td><td>必须外部化状态(DB / Broker / 共享内存)</td></tr>
    <tr><td>你们的现状</td><td class="good">已解决(ResourceManager 内存版即是干这个的)</td><td class="bad">完全未解决——本次方案的主战场</td></tr>
</table>
 
<div class="info"><strong>设计原则:场景 A 用轻量内存锁(快),场景 B 用持久化租约(可靠),两层复合成一个 <code>LayeredResourceManager</code>,对上层 <code>IAutoDeviceFacade</code> 完全透明。</strong></div>
 
<!-- ================================================================= -->
<h2 id="s3">3. 五个候选方案逐一评估与横向对比</h2>
 
<h3>方案 ①:全部收敛到单进程 + OpenTAP 内置同步</h3>
<div class="card">
把所有工位执行体都塞进一个主进程(一个 TestPlan 跑多分支),完全依赖 <code>SynchronizationManager</code>。
<br><br>
<span class="mid">评估:</span>物理上最省事,<strong>但违反老化测试的独立故障域原则</strong>——一个工位 <code>StackOverflow</code>/原生驱动崩溃拖死全部 N 个工位;老化工龄动辄几十小时,进程级隔离是刚需。内存泄漏在多进程下可以按工位轮换回收,单进程无解。
<br><strong>结论:仅作为兜底简化形态(单工位小型机台可用),不作为多工位目标架构。</strong>
</div>
 
<h3>方案 ②:命名内核对象(Named Mutex / Named Semaphore)</h3>
<div class="card">
<code>new Mutex(false, "Global\\Lease_CAN-001")</code>,跨进程抢内核对象。
<br><br>
<strong>优点:</strong>实现成本≈0;进程死亡时内核自动释放 Abandoned(同机可靠)。
<br><strong>致命缺陷:</strong>
<ul>
<li><strong>无语义状态</strong>:Mutex 只知道"谁拿着",不知道"拿去干什么、什么时候该还"——<code>AbandonedMutexException</code> 时上一个持有者<em>操作到哪一步了</em>完全不可知,对"人在接线/输出已打开"这类安全状态是盲区;</li>
<li><strong>无优先级仲裁</strong>:维护模式抢占生产、优雅交接(handover)都表达不了;</li>
<li><strong>无审计</strong>:谁在什么时间持有了多久,内核对象不记录,与你们 <code>debug_operation</code> 审计体系脱节;</li>
<li><strong>不跨机器</strong>;<code>Global\</code> 命名在 Windows 服务/会话隔离下还有权限坑。</li>
</ul>
<strong>结论:不能单独成立;可以作为方案③的"同机加速信号"做混合(见 §4 分层设计)。不推荐用 NamedEvent 轮询模拟租约(竞态多、调试地狱)。</strong>
</div>
 
<h3>方案 ③:数据库租约表(Lease + TTL + 心跳 + 崩溃回收)⭐ 推荐主体</h3>
<div class="card">
把租约状态持久化到 <code>data.db</code> 新表 <code>device_lease</code>,用 <code>BEGIN IMMEDIATE</code> 事务实现跨进程原子的"比较并换入"(CAS),TTL 过期即被视为崩溃可回收,Generation 作栅栏令牌防"僵尸持有者"。
<br><br>
<strong>与现有 IResourceManager 的衔接:</strong>接口签名<strong>一个都不改</strong>。新增 <code>DbLeaseStore</code> 内部类,<code>ResourceManager</code> 升级为组合模式:内存字典继续做本进程快速路径与信号量互斥,预约/校验同时读写 DB 表。上层 <code>AutoDeviceFacade / DeviceService / DebugSessionManager / IAutoDeviceLease</code> 零改动,只换 Bootstrapper 里的注册实例。
<br><strong>限制:</strong>SQLite 是单机文件库——多机共享需把该表迁到服务端数据库(或走方案④ Broker 代理),且 SQLite 写并发要靠 WAL + 短事务,租约操作设计成毫秒级点查/点写,实测支撑每工位秒级命令流毫无压力。
<strong>结论:跨进程仲裁的最佳性价比,作为主体方案。</strong>
</div>
 
<h3>方案 ④:独立设备代理进程(Device Broker + IPC)</h3>
<div class="card">
一台机器上跑一个 <code>DeviceBroker.exe</code>,<strong>所有物理连接(COM/USB/CAN 卡句柄、SCPI TCP socket)只由它持有</strong>;各工位进程通过命名管道把命令转发给 Broker 排队执行。仓库现成的 <span class="path">ConnectionUtil/PipeServer·PipeClient</span> 即可做传输层。
<br><br>
<strong>解决的核心痛点(租约表解决不了的):</strong>
<ul>
<li><strong>句柄独占</strong>:串口/USB 不允许第二个进程 <code>CreateFile</code>。若仅靠租约,每次租约易手都要 close→open 一次,USB 重枚举可达秒级且丢缓冲;Broker 让连接<strong>永不换手</strong>,切换的只是"逻辑所有权";</li>
<li><strong>多机共享</strong>:A 机插着 CAN 卡,B/C 机的测试也能经 TCP/管道让 Broker 代发——Broker 天然是跨机网关;</li>
<li><strong>命令级串行化</strong>:分时复用设备(共享温箱)在 Broker 内是单队列,比租约抢占高效得多。</li>
</ul>
<strong>代价</strong>:多一个常驻进程要运维(看门狗、升级、日志);所有 I/O 多一跳 IPC(~毫秒级,老化场景可忽略);开发量最大。
<strong>结论:串口/USB/CAN 卡这类"单机物理独占"设备的<strong>终局方案</strong>;但建议 Phase 3 再上,Phase 1 用租约表 + "租约持有期内保持连接"即可满足大部分场景。</strong>
</div>
 
<h3>方案 ⑤:直接复用 OpenTAP 生态能力</h3>
<div class="card">
逐项核查 OpenTAP 现成能力能否覆盖跨进程需求(详细分析见姊妹篇 <a href="./OpenTAP_Instrument_Package_Analysis.html">《OpenTAP Instrument + 插件包机制分析》§9</a>):
<table>
    <tr><th>OpenTAP 能力</th><th>是什么</th><th>能否解决跨进程仲裁</th></tr>
    <tr><td><code>Instrument : Resource</code> 资源模型</td><td>仪器生命周期托管 + 编辑器绑定</td><td class="bad">❌ 实例生命周期仅进程内;不感知其他进程</td></tr>
    <tr><td><code>SynchronizationManager</code></td><td>进程内按仪器互斥锁</td><td class="bad">❌ 设计目标就是单进程</td></tr>
    <tr><td>ResultProxy / 远程执行(<code>tap run --remote</code>、Session.Local)</td><td>把一个 TestPlan 切分到远端执行节点,结果回流</td><td class="mid">⚠️ 解决"执行分布",不解决"两台机抢一台仪器"——每个远端 session 各自还是单进程视野</td></tr>
    <tr><td>BenchStore / ResourceSettings(9.23+)</td><td>台架资源配置持久化(XML)</td><td class="mid">⚠️ 配置可以共享,<strong>仲裁状态不行</strong>(无 TTL/心跳概念)</td></tr>
    <tr><td>私有包仓库分发</td><td>驱动以 <code>.opentap</code> 包安装</td><td class="good">✅ 解决驱动分发(与并发仲裁正交,值得并行推进)</td></tr>
    <tr><td><code>IResourcePlan</code>/ResourceConflicts(9.20+)</td><td>资源规划/冲突<strong>静态检查</strong>(编辑器里提示"两个序列用了同一仪器")</td><td class="mid">⚠️ 只能提前发现冲突,运行时多进程并发仍不仲裁</td></tr>
</table>
<strong>结论:OpenTAP 生态没有任何一块能直接替代跨进程运行时仲裁——这是开源测试框架的普遍空白,也正是你们 IResourceManager 体系的战略价值。能复用的是:分发(包机制)、配置模型(BenchStore 思路)、以及把最终驱动做成 Instrument 插件的长期形态。</strong>
</div>
 
<h3>横向对比总表</h3>
<table>
    <tr><th>维度</th><th>①单进程收敛</th><th>②Named Mutex</th><th>③DB 租约表 ⭐</th><th>④Device Broker</th><th>⑤OpenTAP 现成</th></tr>
    <tr><td>并发安全(跨进程)</td><td class="bad">伪命题(无多进程)</td><td class="mid">互斥可以,但无状态语义</td><td class="good">强一致(DB 事务 CAS)</td><td class="good">单点串行化,最强</td><td class="bad">不支持</td></tr>
    <tr><td>崩溃残留处理</td><td class="good">进程死=锁消失</td><td class="mid">Abandoned 可感知但盲接</td><td class="good">TTL 自动过期 + 回收</td><td class="mid">Broker 死=全停(需看门狗)</td><td>—</td></tr>
    <tr><td>优先级/抢占/优雅交接</td><td class="mid">代码可实现</td><td class="bad">表达不了</td><td class="good">状态列+grace 字段原生支持</td><td class="good">队列策略任意定制</td><td class="bad">无</td></tr>
    <tr><td>审计可追溯</td><td class="bad">无</td><td class="bad">无</td><td class="good">表即历史(可挂 debug_operation)</td><td class="good">Broker 全权日志</td><td class="mid">ResultProxy 有结果无仲裁</td></tr>
    <tr><td>性能开销</td><td class="good">纳秒级</td><td class="good">微秒级</td><td class="mid">毫秒级(WAL 点写)</td><td class="mid">毫秒级(IPC 一跳)</td><td>—</td></tr>
    <tr><td>实现成本</td><td class="good">0(重构架构为负)</td><td class="good">0.5 人日</td><td class="good">3~4 人日</td><td class="mid">5~8 人日</td><td class="good">已内建/不适用</td></tr>
    <tr><td>可扩展到多机</td><td class="bad">否</td><td class="bad">否</td><td class="mid">需换服务端 DB</td><td class="good">是(Broker=网关)</td><td class="mid">部分</td></tr>
    <tr><td>物理句柄独占问题</td><td class="good">天然规避</td><td class="bad">不解决(close/open 抖动)</td><td class="mid">租约期内保连接可缓解</td><td class="good">根治(唯一持有者)</td><td class="bad">不解决</td></tr>
    <tr><td>调试难度</td><td class="good">低</td><td class="mid">内核对象黑盒</td><td class="good">低(一张表全透明,SELECT 即现场)</td><td class="mid">需双进程联调</td><td>—</td></tr>
    <tr><td>与现有代码衔接</td><td class="mid">大改</td><td class="mid">需新增旁路</td><td class="good">IResourceManager 实现层替换,上层零改</td><td class="mid">Driver 层拆分转发端/宿主端</td><td class="good">正交复用</td></tr>
</table>
 
<!-- ================================================================= -->
<h2 id="s4">4. 推荐目标架构:分层混合(租约表 + 仲裁规则 + 按需 Broker)</h2>
 
<div class="mermaid">
graph TB
    subgraph L4["上层调用(保持不变)"]
        TS["OpenTAP TestStep<br/>(全自动 · IAutoDeviceLease 作用域)"]
        DBG["DeviceDebug UI<br/>(手动维护 · DebugSessionManager)"]
    end
    subgraph L3["门面层 Aging.Infrastructure/Services"]
        FAC["AutoDeviceFacade<br/>(租约申请/心跳/栅栏校验)"]
        SVC["DeviceService<br/>(命令分派 + owner 校验升级为跨进程)"]
    end
    subgraph L2["仲裁层(本次核心改造)"]
        LRM["LayeredResourceManager : IResourceManager"]
        MEM["进程内层:SemaphoreSlim + 内存缓存<br/>(复用现有 ResourceManager 逻辑)"]
        DBL["跨进程层:DbLeaseStore<br/>device_lease 表 · BEGIN IMMEDIATE CAS"]
        POL["仲裁策略引擎:优先级/抢占/grace"]
        LRM --> MEM & DBL & POL
    end
    subgraph L1["物理连接层(Phase 3 按需)"]
        OWN["本进程直连:网口仪器(SCPI/TCP/CAN总线模拟)<br/>租约=逻辑独占,句柄随租约开关"]
        BRK["DeviceBroker.exe:串口/USB/CAN卡唯一持有者<br/>复用 ConnectionUtil.PipeServer/PipeClient"]
    end
    TS & DBG --> FAC --> SVC --> LRM
    DBL --> SQLITE[("data.db<br/>device_lease 表<br/>WAL 模式")]
    SVC --> OWN & BRK
</div>
 
<h3>4.1 分层职责与决策规则</h3>
<table>
    <tr><th>层</th><th>解决什么</th><th>何时启用</th></tr>
    <tr><td>内存层(已有)</td><td>同进程多线程细锁、快速失败</td><td class="good">始终(零成本)</td></tr>
    <tr><td>DB 租约层(新增)</td><td>跨进程运行时仲裁、优先级抢占、审计、崩溃回收</td><td class="good">Phase 1 —— 主体投入</td></tr>
    <tr><td>Broker 层(新增)</td><td>物理句柄独占、多机共享、命令队列</td><td class="mid">Phase 3 —— 仅当出现:① 串口/USB 重开成本不可接受 ② 多机共测一台机上的设备 ③ 分时复用设备要排队</td></tr>
    <tr><td>OpenTAP 生态</td><td>驱动分发(包)、配置共享(Bench 思路)</td><td class="mid">与 Phase 1/3 正交并行推进</td></tr>
</table>
 
<!-- ================================================================= -->
<h2 id="s5">5. 租约完整生命周期:申请 / 续期 / 释放 / 超时抢占</h2>
 
<h3>5.1 租约状态机</h3>
<div class="mermaid">
stateDiagram-v2
    [*] --> Free: device_lease 无行/已回收
    Free --> Active: Acquire(事务CAS成功<br/>generation++ 写入 owner)
    Active --> Active: Heartbeat 续期<br/>(extends expires_at)
    Active --> PendingHandover: 更高优先级申请到达<br/>(Maintenance 请求,写 grace 截止)
    PendingHandover --> Released: 持有者优雅让渡<br/>(完成当前 TestStep 后 Release)
    PendingHandover --> ForceReleased: grace 超时<br/>(请求者可抢占 generation++)
    Active --> Expired: 心跳断(进程崩溃/断网)<br/>expires_at &lt; DB now
    Expired --> Free: 回收(任何 Acquire 顺带 DELETE<br/>+ 后台 Sweep 兜底)
    Active --> Released: 正常 Dispose/ReleaseReservation
    Released --> Free
</div>
 
<h3>5.2 申请 + 抢占 + 栅栏拒绝时序</h3>
<div class="mermaid">
sequenceDiagram
    autonumber
    participant P1 as 工位进程1<br/>(Production 租约持有者)
    participant DB as device_lease 表<br/>(SQLite WAL)
    participant HMI as 工位进程2<br/>(Maintenance 请求者)
    participant DRV as DeviceService/驱动
 
    Note over P1,DB: —— 正常持租 + 心跳 ——
    P1->>DB: Acquire CAN-001(BEGIN IMMEDIATE→无冲突→INSERT gen=41)
    DB-->>P1: lease(tokenA, gen41, expires=now+30s)
    loop 每 10s(TTL/3)
        P1->>DB: Renew WHERE token=tokenA AND gen=41
        DB-->>P1: 影响行数=1 ✅ expires 顺延
    end
    P1->>DRV: ExecuteCommandAsync("OUTP ON") [携带 gen41]
    DRV->>DB: FenceCheck(gen41) → 通过 ✅
    DRV-->>P1: OK
 
    Note over HMI,DB: —— 维护模式抢占 ——
    HMI->>DB: RequestMaintenance(CAN-001)
    DB-->>HMI: 已占用 → 写 state=PendingHandover,<br/>grace_until=now+30s, 返回"等待让渡"
    P1->>DB: (下一次心跳发现 state=PendingHandover)
    P1->>P1: 完成当前 TestStep → 主动 Release
    P1->>DB: Release(tokenA) → DELETE 行
    HMI->>DB: 轮询/通知后重试 Acquire → 成功(gen=42, Maintenance)
    HMI->>HMI: DebugSessionManager 进入 MaintenanceActive
 
    Note over P1,DRV: —— 僵尸持有者被栅栏拦截 ——
    P1->>P1: (崩溃复活/网络分区后仍以为自己持有 gen41)
    P1->>DRV: ExecuteCommandAsync(旧 tokenA gen41)
    DRV->>DB: FenceCheck: 表内 gen=42 ≠ 41
    DRV-->>P1: ❌ LEASE_STALE → 本地熔断,会话转 RecoveryRequired
</div>
 
<h3>5.3 核心 SQL(原子申请,一次事务完成回收+检查+写入)</h3>
<pre><code>BEGIN IMMEDIATE;                        <span class="cmt">-- 拿写锁:跨进程串行化入口(SQLite WAL 下读写不互斥,写写排队)</span>
 
<span class="cmt">-- ① 顺带回收所有已过期租约(懒回收,无需独立守护也能自愈)</span>
DELETE FROM device_lease
 WHERE resource_key = @rk AND expires_at &lt; datetime('now');
 
<span class="cmt">-- ② 检查当前有效占用</span>
SELECT owner_id, owner_type, state, grace_until INTO @cur FROM device_lease WHERE resource_key = @rk;
 
<span class="cmt">-- ③ 无占用 → 插入新租约(generation 取全局序)</span>
INSERT INTO device_lease(resource_key, owner_id, owner_type, token_id, generation,
                         machine_name, host_process_id, state, acquired_at, heartbeat_at, expires_at)
VALUES(@rk, @owner, @otype, @token, @gen, @machine, @pid, 0,
        datetime('now'), datetime('now'), datetime('now', '+30 seconds'));
 
<span class="cmt">-- ④ 有占用且请求者是 Maintenance → 不直接抢,标记让渡窗口</span>
UPDATE device_lease SET state = 1, grace_until = datetime('now','+30 seconds')
 WHERE resource_key = @rk AND state = 0;
 
COMMIT;</code></pre>
 
<h3>5.4 续期(心跳)与失效判定</h3>
<pre><code><span class="cmt">// AutoDeviceLease 内部:TTL/3 周期定时器(Phase 1 即实现)</span>
<span class="kwd">var</span> affected = db.<span class="fn">ExecuteNonQuery</span>(<span class="str">@"
    UPDATE device_lease
       SET heartbeat_at = datetime('now'),
           expires_at   = datetime('now', '+30 seconds'),
           state        = CASE WHEN state = 1 THEN state ELSE 0 END   <span class="cmt">-- 保留让渡标记</span>
     WHERE resource_key = @rk AND token_id = @token AND generation = @gen"</span>);
 
<span class="kwd">if</span> (affected == 0) {
    <span class="cmt">// 行不在了或 generation 已被超越 ⇒ 我已被抢占/回收 —— 本地立即熔断:</span>
    <span class="cmt">// 1) 停止下发任何命令  2) 释放物理连接  3) 会话标记 RecoveryRequired  4) 上报一次告警</span>
    <span class="fn">OnLeaseLost</span>();
}
<span class="kwd">if</span> (db.<span class="fn">ReadState</span>() == PendingHandover &amp;&amp; InStepBoundary) {
    <span class="fn">Dispose</span>();   <span class="cmt">// 优雅让渡:只在 TestStep 边界释放,绝不在一条命令中间放手</span>
}</code></pre>
 
<div class="highlight"><strong>三条铁律:</strong>① 所有时间戳一律用 <code>datetime('now')</code>(DB 时钟),客户端时间仅作参考列;② 每条命令下发前必须 <code>FenceCheck</code>(一行点查,成本≈1ms);③ 让渡/释放只发生在 TestStep 边界,保证设备不处于"半条指令"状态。</div>
 
<!-- ================================================================= -->
<h2 id="s6">6. 全自动 vs 手动维护的互斥仲裁规则</h2>
 
<p>目标场景:工程师正打开机柜接 CAN 线(维护模式),全自动程序绝不能突然下发 <code>OUTP ON</code>。规则明确定义如下(策略引擎集中实现于新增 <span class="path">Aging.Infrastructure/Core/LeaseArbiter.cs</span>):</p>
 
<h3>6.1 优先级矩阵(行=当前持有者,列=新请求者)</h3>
<table>
    <tr><th></th><th>请求:Production(全自动)</th><th>请求:Maintenance(手动维护)</th><th>请求:ReadOnly(监控/看数据)</th></tr>
    <tr>
        <td><strong>持有:Production</strong></td>
        <td class="mid">排队(FIFO 或按工位优先级,带超时)</td>
        <td class="mid"><strong>让渡流程</strong>:PendingHandover + grace 30s;超时未让渡 → 允许强占 + 旧持有者栅栏失效</td>
        <td class="good">放行(不建租约,走旁路只读)</td>
    </tr>
    <tr>
        <td><strong>持有:Maintenance</strong></td>
        <td class="bad"><strong>直接拒绝</strong>(ResourceConflictException,"有人在维护";UI 弹窗告知工程师)</td>
        <td class="bad">拒绝(第二个人也不许碰——一台设备同时只允许一个维护会话,复用现有 OpenSession 重复检查逻辑)</td>
        <td class="good">放行</td>
    </tr>
    <tr>
        <td><strong>持有:ReadOnly</strong></td>
        <td class="good">立即放行(读锁不构成障碍)</td>
        <td class="good">立即放行</td>
        <td class="good">放行</td>
    </tr>
</table>
 
<div class="info"><strong>与现有代码的挂接点:</strong><span class="path">DebugSessionManager.RequestMaintenance()</span> 已经做了 <code>ReserveAsync</code> + 捕获 <code>ResourceConflictException</code> → 只需把内存版换成 Layered 版即自动获得跨进程拒绝;<span class="path">ValidateCommand()</span> 已有"MaintenanceActive + ValidateOwner"双重检查,把 <code>ValidateOwner</code> 改为查 DB 栅栏,就得到<strong>"人在接线时程序连命令都发不出"</strong>的硬保证——即使自动程序逻辑有 bug。</div>
 
<h3>6.2 维护模式退出与自动流程恢复</h3>
<ul>
<li>工程师退出维护(<span class="path">ExitMaintenanceBtn_Click → CloseSession</span>):释放行 + <code>UPDATE debug_session SET state=Closed</code>(现有逻辑保持);</li>
<li>自动工位对 PendingHandover 的感知延迟 ≤ 心跳周期(10s),老化长步骤(小时级)不适用抢占——对 <code>grace</code> 内未完成的步骤,策略改为<strong>"拒绝进入,已运行不打断"</strong>(可配置:安全类工位选择强占+设备紧急安全态;产能类工位选择排队等待);</li>
<li>HMI 侧建议:设备被自动占用时,维护申请显示"占用者:Station-3 · 已运行 01:23:45 · 预计剩余 02:10"(数据全在 <code>device_lease</code> + <code>debug_session</code>,一条 JOIN 即可)。</li>
</ul>
 
<h3>6.3 审计闭环</h3>
<ul>
<li><code>device_lease</code> 表本身就是"占用历史"(Release 时改写 <code>released_at</code> 再保留 7 天,或双表:当前 + 归档);</li>
<li>每条经 <code>DeviceService</code> 的命令继续写 <code>debug_operation</code>,新增两列 <code>owner_type, lease_generation</code>,即可回答"这条 OUTP ON 是哪个工位、哪一代持有者发的"——事故追溯闭环。</li>
</ul>
 
<!-- ================================================================= -->
<h2 id="s7">7. 异常场景处理:死锁 / 孤儿锁 / 时钟 / 幂等</h2>
 
<table>
    <tr><th>异常场景</th><th>成因</th><th>对策</th><th>机制落点</th></tr>
    <tr>
        <td><strong>多设备死锁</strong>(工位A锁{电源}等{CAN},B反序)</td>
        <td>组申请加锁顺序不一致</td>
        <td class="good">① 全局强制按 <code>resource_key</code> 字典序申请(AcquireGroupAsync 内部排序)<br>② 事务内"全有或全无":任一冲突则整体回滚重试(<strong>wait-die</strong>:新请求者退避重试,老持有者不被饿死)<br>③ 组申请总超时(默认 60s)</td>
        <td>AutoDeviceFacade 新增 <code>AcquireGroupAsync</code></td>
    </tr>
    <tr>
        <td><strong>进程崩溃 → 孤儿锁</strong></td>
        <td>kill / BSOD / 断电,Release 没跑</td>
        <td class="good">① TTL 30s 自动过期(任何 Acquire 顺带 DELETE 过期行 = 懒回收)<br>② 同机看门狗:lease 记录 <code>machine_name + host_process_id</code>,本机新进程启动时枚举存活 PID 提前回收(不用等 TTL)<br>③ HMI"强制释放"按钮(需 ADMIN 角色,写审计 note="force_release")</td>
        <td>device_lease 列 + Bootstrapper 启动自检</td>
    </tr>
    <tr>
        <td><strong>僵尸持有者</strong>(网络分区/GC停顿后复活继续发命令)</td>
        <td>进程活着但被抢占过</td>
        <td class="good">栅栏令牌:命令必须携带 <code>(token_id, generation)</code>,DB 中 generation 已变 → 驱动入口直接拒绝(§5.2 时序 15-17 步)。<strong>这是比 TTL 更根本的安全网</strong>——TTL 管"谁能拿",栅栏管"拿过的人过期后不能碰"。</td>
        <td>DeviceService.ExecuteCommandAsync 入口 FenceCheck</td>
    </tr>
    <tr>
        <td><strong>时钟不一致</strong>(多机 NTP 漂移)</td>
        <td>expires 用本地时间比较会误判</td>
        <td class="good">① 全部时间判定用 DB 侧时钟(<code>datetime('now')</code>),客户端只传相对 TTL("+30 seconds"),从不传绝对时间<br>② Phase 3 Broker 化后统一用 Broker 时钟<br>③ 多机 + 共享 DB 场景要求 NTP(运维项,误差&lt;1s 即可)</td>
        <td>SQL 写法约束(§5.3 全部合规)</td>
    </tr>
    <tr>
        <td><strong>心跳线程被 UI 卡顿饿死</strong></td>
        <td>WPF 主线程阻塞 / 进程假死</td>
        <td class="good">心跳用<strong>独立后台线程</strong>(非 UI Dispatcher),且心跳失败连续 2 次即本地熔断——宁可误停不可漏停</td>
        <td>AutoDeviceLease 内 Timer 实现</td>
    </tr>
    <tr>
        <td><strong>重试不幂等</strong>(超时后重发"OUTP ON"是安全的,重发"计数器+1"不安全)</td>
        <td>网络/DB 抖动自动重试</td>
        <td class="good">① 命令分级:设备层可安全重发(设置型 SET xx=yy ✅ 幂等;累加/触发型 ⚠️ 禁止自动重试,上抛)<br>② <code>debug_operation.operation_id</code> 已是客户端生成 GUID → 作为幂等键 INSERT OR IGNORE,重复到达不二次执行<br>③ 老化场景的 SCPI/CAN 写命令多为设置型,天然幂等友好,但必须在驱动接口上用 <code>CommandSemantics.Idempotent</code> 显式声明</td>
        <td>DeviceCommandResult + 驱动装饰器</td>
    </tr>
    <tr>
        <td><strong>SQLite 写竞争</strong>(N 个工位 × 心跳+申请集中写一行区域)</td>
        <td>写锁串行化</td>
        <td class="good">WAL 模式 + busy_timeout=5000 + 事务只包住 2~3 条点写(&lt;5ms);老化命令节奏(秒级)下并发写压力可忽略。真到高频(&gt;100 写/秒)再迁服务端 DB</td>
        <td>连接串 PRAGMA(见 §9 DbLeaseStore)</td>
    </tr>
    <tr>
        <td><strong>抢占风暴</strong>(两个高优请求同时到达)</td>
        <td>并发 PendingHandover 标记</td>
        <td class="good">仲裁在 <code>BEGIN IMMEDIATE</code> 事务内串行判定;第二个维护请求会看到"已有维护排队"而被告知排队位置,不产生双 grace 竞争</td>
        <td>事务天然覆盖</td>
    </tr>
</table>
 
<!-- ================================================================= -->
<h2 id="s8">8. 独占 or 分时复用:设备分组与 Slot 绑定策略</h2>
 
<h3>8.1 设备访问模式(新增列,注册页可配)</h3>
<table>
    <tr><th>模式</th><th>语义</th><th>租约形态</th><th>典型设备(本项目)</th></tr>
    <tr>
        <td><code>Exclusive</code> 独占</td>
        <td>一个工位全程持有,测试期间别人碰不到</td>
        <td>整步骤级长租约(TTL 靠心跳维持数小时)</td>
        <td>工位专用电源、DUT 专用 CAN 卡通道</td>
    </tr>
    <tr>
        <td><code>TimeShared</code> 分时串行</td>
        <td>多条命令排队过设备,每条原子</td>
        <td>命令级短租约(秒级)或 Broker 队列</td>
        <td>共享 DAQ 读数、公用万用表</td>
    </tr>
    <tr>
        <td><code>SharedRead</code> 共享读+独占写</td>
        <td>监控随便读(旁路),写必须抢租约</td>
        <td>读不走仲裁;写=Exclusive 租约</td>
        <td>温箱(状态人人可看,设温必须独占)、CAN 总线监听</td>
    </tr>
</table>
 
<h3>8.2 决策规则(新增 <code>DeviceDefinition.access_mode</code> 列,注册页下拉选择)</h3>
<div class="card">
<b>一问:</b>"两个工位<strong>同时</strong>用这台设备,物理上/协议上会不会互相干扰?"<br>
<b>会</b>(串口指令交叉、电源被改压)→ <code>Exclusive</code>;<br>
<b>不会但语义会乱</b>(万用表 A 读数是 B 触发下产生的)→ <code>TimeShared</code>(串行队列);<br>
<b>读无害、写有害</b>(温箱状态广播)→ <code>SharedRead</code>。<br>
<b>判据来源:</b>设备传输层类型 + 协议是否支持会话隔离。CAN 卡默认 Exclusive(通道级可细分),TCP SCPI 看仪器连接数限制(多数 DMM 只允许 1 个 socket → 实质 Exclusive)。
</div>
 
<h3>8.3 工位 ↔ 设备分组:绑定到 Slot 体系</h3>
<div class="mermaid">
graph LR
    subgraph 台架["物理台架"]
        S1["Slot-A(工位1)"] --- S2["Slot-B(工位2)"]
        P1["PS-001 专用电源"] --- S1
        C1["CAN-001 卡 CH0"] --- S1
        C2["CAN-002 卡 CH1"] --- S2
        T1["CHAMBER-01 共享温箱"] --- S1 & S2
    end
    subgraph 数据["data.db 映射(已有表 + 新列)"]
        M1["SlotDeviceMapping<br/>SlotId ↔ DeviceId+ChannelId"]
        G1["新增 device_group:<br/>group_key → {resource_keys}<br/>access_mode 按设备取"]
    end
</div>
 
<ul>
<li><strong>复用现有 <span class="path">SlotDeviceMapping</span> 表</strong>:工位身份 = <code>SlotId</code>。工位进程启动参数带 SlotId → 组租约自动展开为该 Slot 绑定的全部设备 resource_key 列表(按字典序申请);</li>
<li><strong>组申请原子化</strong>:新增 <code>AcquireGroupAsync(groupKey, ...)</code>:查 <code>device_group</code>(或即时 JOIN SlotDeviceMapping)→ 排序 → 单事务逐台 INSERT,任一冲突整体回滚 → 避免"占了一半"的半死状态;</li>
<li><strong>共享设备不进组</strong>:<code>CHAMBER-01</code> 这类跨 Slot 设备在 <code>DeviceDefinition.access_mode</code> 标 TimeShared/SharedRead,组申请时跳过,命令执行时才抢短租约;</li>
<li><strong>CAN 多工位共卡</strong>:resource key 细化到 <code>CAN_CARD01:CH2</code>——<code>ResourceKey</code> 本来就支持 Channel(见 <span class="path">Contracts/ResourceKey.cs</span>),CAN 帧 ID 级再细分可用 <code>resource_type='CAN_FRAME'</code> + channel=帧段(Broker 阶段实现命令级串行化时够用)。</li>
</ul>
 
<!-- ================================================================= -->
<h2 id="s9">9. 本仓库改造清单(具体到文件)</h2>
 
<h3>9.1 数据库 DDL(新增到 <span class="path">Plugin.DeviceConfig/Model/DeviceConfigModel.cs</span> InitDatabase 的 sqls 列表)</h3>
<pre><code><span class="cmt">-- ① 跨进程租约表(核心新表)</span>
CREATE TABLE IF NOT EXISTS device_lease (
    resource_key     TEXT PRIMARY KEY,        <span class="cmt">-- 'CAN-001:Generic' 与 ResourceKey.ToString() 一致</span>
    owner_id         TEXT NOT NULL,           <span class="cmt">-- 'Station3.Run20261103' 工位可读标识</span>
    owner_type       INTEGER NOT NULL,        <span class="cmt">-- 0=Production 1=Maintenance 2=ReadOnly</span>
    token_id         TEXT NOT NULL,           <span class="cmt">-- ResourceOwnerToken.TokenId</span>
    generation       INTEGER NOT NULL,        <span class="cmt">-- 栅栏令牌:每次成功申请 +1(全局取 max+1)</span>
    machine_name     TEXT NOT NULL DEFAULT '',
    host_process_id  INTEGER NOT NULL DEFAULT 0,
    slot_id          TEXT,                    <span class="cmt">-- 关联工位 Slot(组审计用)</span>
    state            INTEGER NOT NULL DEFAULT 0,  <span class="cmt">-- 0=Active 1=PendingHandover</span>
    acquired_at      TEXT NOT NULL,
    heartbeat_at     TEXT NOT NULL,
    expires_at       TEXT NOT NULL,           <span class="cmt">-- 全部由 DB datetime('now') 生成</span>
    grace_until      TEXT,
    note             TEXT DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_lease_expire ON device_lease(expires_at);
 
<span class="cmt">-- ② 设备访问模式(现有表加列,老库幂等迁移)</span>
ALTER TABLE DeviceDefinition ADD COLUMN access_mode INTEGER NOT NULL DEFAULT 0;
<span class="cmt">--    0=Exclusive 1=TimeShared 2=SharedRead</span>
 
<span class="cmt">-- ③ 工位设备组(组租约的展开依据;也可不建表,运行时 JOIN SlotDeviceMapping)</span>
CREATE TABLE IF NOT EXISTS device_group (
    group_key   TEXT PRIMARY KEY,     <span class="cmt">-- = SlotId 或自定义</span>
    resource_key TEXT NOT NULL,
    order_hint  INTEGER DEFAULT 0,
    PRIMARY KEY (group_key, resource_key)
);
 
<span class="cmt">-- ④ debug_session 加 3 列(会话↔租约联动可视化)</span>
ALTER TABLE debug_session ADD COLUMN lease_generation INTEGER;
ALTER TABLE debug_session ADD COLUMN lease_owner_type INTEGER;
ALTER TABLE debug_session ADD COLUMN heartbeat_at TEXT;
 
<span class="cmt">-- ⑤ debug_operation 加 2 列(命令级追溯)</span>
ALTER TABLE debug_operation ADD COLUMN lease_generation INTEGER;
ALTER TABLE debug_operation ADD COLUMN idempotency_key TEXT;   <span class="cmt">-- 幂等重试去重</span>
CREATE UNIQUE INDEX IF NOT EXISTS idx_op_dedup ON debug_operation(idempotency_key) WHERE idempotency_key != '';</code></pre>
 
<h3>9.2 接口与实现改动</h3>
<table>
    <tr><th>文件</th><th>改动类型</th><th>内容</th></tr>
    <tr>
        <td><span class="path">Aging.Infrastructure/Contracts/IResourceManager.cs</span></td>
        <td>扩展(向后兼容)</td>
        <td>新增 3 个方法,默认实现委托现有语义:<br>
        <code>Task&lt;bool&gt; RenewAsync(ResourceOwnerToken, ResourceKey, int ttlSeconds, CancellationToken)</code><br>
        <code>(bool valid, string? holderInfo, int? graceSeconds) ProbeConflict(ResourceKey)</code> <span class="cmt">// UI 显示"被谁占用"</span><br>
        <code>Task&lt;GroupLeaseHandle&gt; AcquireGroupAsync(string groupKey, ResourceOwnerToken owner, CancellationToken ct)</code></td>
    </tr>
    <tr>
        <td><span class="path">Aging.Infrastructure/Core/DbLeaseStore.cs</span> ★新建</td>
        <td>新增</td>
        <td>§5.3/§5.4 的 SQL 封装;WAL/busy_timeout PRAGMA;懒回收;同机 PID 看门狗。约 250 行</td>
    </tr>
    <tr>
        <td><span class="path">Aging.Infrastructure/Core/ResourceManager.cs</span></td>
        <td>改造为组合</td>
        <td>保留内存字典 + SemaphoreSlim(快速路径);预约/校验转发 <code>DbLeaseStore</code>;离线模式(DB 不可达)降级为纯内存 + 每 5s 重连并在重连后栅栏自检</td>
    </tr>
    <tr>
        <td><span class="path">Aging.Infrastructure/Core/LeaseArbiter.cs</span> ★新建</td>
        <td>新增</td>
        <td>§6.1 优先级矩阵的策略引擎:让渡/拒绝/排队判定,策略参数(grace、抢占开关)进配置</td>
    </tr>
    <tr>
        <td><span class="path">Aging.Infrastructure/Services/AutoDeviceFacade.cs</span></td>
        <td>增强</td>
        <td>① <code>ReserveCoreAsync</code> 的 owner 标识从随机 Guid 改为可配置 <code>StationGlobals.StationId</code>(多机/多工位可读性);② <code>AutoDeviceLease</code> 构造函数启动后台心跳 Timer(TTL/3),Dispose 停止;③ 捕获 PendingHandover → <code>TryYieldAtBoundary()</code></td>
    </tr>
    <tr>
        <td><span class="path">Aging.Infrastructure/Services/DeviceService.cs</span></td>
        <td>增强</td>
        <td><code>ValidateOwner</code> 升级为 DB 栅栏校验(查 generation 匹配);不匹配返回 <code>ErrorCode="LEASE_STALE"</code> 结构化失败,不抛裸异常</td>
    </tr>
    <tr>
        <td><span class="path">Aging.Infrastructure/Drivers/DriverFactory.cs</span> + 各驱动</td>
        <td>Phase 3</td>
        <td>新增 <code>transport=="CAN-REMOTE"</code> 等代理型驱动(把命令序列化为 Broker 请求);现有直连驱动不动。<code>IBrokeredTransport</code> 接口定义于 Contracts</td>
    </tr>
    <tr>
        <td><span class="path">Plugin.DeviceDebug/Model/DebugSessionManager.cs</span></td>
        <td>微调</td>
        <td><code>RequestMaintenance</code> 处理 <code>PendingHandover</code>(转异步等待 + UI 回调"等待工位让渡,剩余 X 秒");冲突异常 message 已含占用者 → 直接给 ToastHelper</td>
    </tr>
    <tr>
        <td><span class="path">Plugin.DeviceDebug/Model/DeviceDebugModel.cs</span></td>
        <td>微调</td>
        <td>debug_session 新增列写入;启动时 <code>SELECT ... WHERE state NOT IN (Closed) AND heartbeat过期</code> 做孤儿会话回收(置 Faulted)</td>
    </tr>
    <tr>
        <td><span class="path">Plugin.DeviceConfig/Model/DeviceConfigModel.cs</span></td>
        <td>DDL</td>
        <td>§9.1 的建表/加列(幂等,IF NOT EXISTS / try-catch ALTER);<span class="path">DeviceEditModal.xaml(.cs)</span> 增加 access_mode 下拉</td>
    </tr>
    <tr>
        <td><span class="path">Aging.Infrastructure/InfrastructureBootstrapper.cs</span></td>
        <td>配置点</td>
        <td>注册 <code>LayeredResourceManager</code>(带 dbPath)替代纯内存版;读 <code>StationId</code>;可选注册 BrokerClient 传输</td>
    </tr>
    <tr>
        <td>★新建 <span class="path">DeviceBroker/</span> 控制台工程(Phase 3)</span></td>
        <td>新增</td>
        <td>引用 ConnectionUtil 现成 PipeServer;协议:<code>{op, deviceId, channel, payload, token, gen, idemKey}</code>;内部按设备队列 + 复用 DriverFactory 创建真实驱动;崩溃看门狗(父进程句柄失联自动退出交 HMI 拉起)</td>
    </tr>
</table>
 
<h3>9.3 关键接口签名(Phase 1 核心代码示例)</h3>
<pre><code><span class="cmt">// DbLeaseStore —— 原子申请(对应 §5.3 SQL)</span>
<span class="kwd">public sealed class</span> DbLeaseStore
{
    <span class="kwd">public</span> DbLeaseStore(<span class="kwd">string</span> dataDbPath) { <span class="cmt">/* PRAGMA journal_mode=WAL; busy_timeout=5000 */</span> }
 
    <span class="kwd">public enum</span> AcquireResult { Acquired, Renewed, QueuedHandover, RejectedMaintenanceHeld, RejectedByProductionHeld }
 
    <span class="kwd">public</span> AcquireResult TryAcquire(<span class="kwd">string</span> resourceKey, ResourceOwnerToken owner, OwnerType type,
                                <span class="kwd">string</span> machine, <span class="kwd">int</span> pid, <span class="kwd">string</span>? slotId, <span class="kwd">int</span> ttlSeconds);
    <span class="kwd">public bool</span>  Renew(<span class="kwd">string</span> resourceKey, <span class="kwd">string</span> tokenId, <span class="kwd">long</span> generation, <span class="kwd">int</span> ttlSeconds);
    <span class="kwd">public bool</span>  Release(<span class="kwd">string</span> resourceKey, <span class="kwd">string</span> tokenId);
    <span class="kwd">public long</span>  CurrentGeneration(<span class="kwd">string</span> resourceKey);          <span class="cmt">// FenceCheck 用</span>
    <span class="kwd">public int</span>   SweepExpired();                                   <span class="cmt">// 懒回收兜底</span>
    <span class="kwd">public</span> (bool valid, <span class="kwd">string</span>? holder, <span class="kwd">int</span>? graceSec) ProbeConflict(<span class="kwd">string</span> resourceKey);
}
 
<span class="cmt">// AutoDeviceLease 心跳(Phase 1)——独立线程,UI 卡顿不影响</span>
_beacon = <span class="kwd">new</span> Timer(_ =&gt; {
    <span class="kwd">try</span> {
        <span class="kwd">if</span> (!_store.Renew(DeviceId, _owner.TokenId, _owner.Generation, _ttl))
            { LeaseLost?.Invoke(<span class="kwd">this</span>, EventArgs.Empty); <span class="kwd">return</span>; }
        <span class="kwd">if</span> (_store.IsPendingHandover(DeviceId)) _handoverTcs.TrySetResult(<span class="kwd">true</span>);
        _failCount = 0;
    } <span class="kwd">catch</span> { <span class="kwd">if</span> (++_failCount &gt;= 2) LeaseLost?.Invoke(<span class="kwd">this</span>, EventArgs.Empty); }
}, <span class="kwd">null</span>, _ttlMs/3, _ttlMs/3);
 
<span class="cmt">// TestStep 基类在步骤边界让渡(集成测试演示用法)</span>
<span class="kwd">if</span> (lease.HandoverRequested) lease.Dispose();   <span class="cmt">// 步骤结束点检查,绝不在命令中途放手</span></code></pre>
 
<!-- ================================================================= -->
<h2 id="s10">10. 测试要点</h2>
 
<h3>10.1 单元(<span class="path">HMI.UnitTest</span> 或新建 xUnit)</h3>
<table>
    <tr><th>用例</th><th>验证点</th></tr>
    <tr><td>Acquire 并发 20 线程同 key</td><td>恰好 1 个 Acquired,其余 Rejected/Renewed(同 token);generation 严格 +1 无跳变竞争</td></tr>
    <tr><td>Renew 用旧 generation</td><td>返回 false(栅栏语义)</td></tr>
    <tr><td>TTL 过期 → 直接 Acquire 过期持有者的 key</td><td>懒回收生效:先 DELETE 过期行再插入成功</td></tr>
    <tr><td>仲裁矩阵全遍历(3×3 持有×请求)</td><td>§6.1 每个格子一个断言</td></tr>
    <tr><td>组申请部分冲突</td><td>全回滚,不留下"占了一半"的行</td></tr>
</table>
 
<h3>10.2 多进程集成(利用 <span class="path">Plugin.IntegrationTest</span> 扩展,重点)</h3>
<div class="card">
在"全自动测试" Tab 增加 <strong>【多进程压力用例】</strong>按钮:
<pre><code><span class="cmt">// 伪代码:主进程 fork N 个自身 --station-worker 模式的子进程</span>
<span class="kwd">for</span> (<span class="kwd">int</span> s = 0; s &lt; 4; s++)
    Process.Start(exe, $<span class="str">"--worker {s} --device CAN-001 --loops 200"</span>);
 
<span class="cmt">// 子 worker 逻辑:循环 [TryAcquire → 发一帧 → 校验 → Dispose],统计成功轮转次数</span>
<span class="cmt">// 主进程断言:① 任意时刻 device_lease 只有 1 行有效 ② 所有帧的 generation 单调 ③ 无双写</span>
<span class="cmt">//           ④ 中途 Kill(随机 worker) → 其余 worker ≤ TTL+5s 内接管,测试继续收敛为 PASS</span></code></pre>
额外两个杀手级用例:
<ul>
<li><strong>崩溃孤儿锁</strong>:<code>Process.Kill</code> 持有者(不跑 Dispose)→ 断言 ≤35s 后被其他工位自动接管;<code>host_process_id</code> 同机看门狗路径则 ≤2s 回收;</li>
<li><strong>维护抢占演练</strong>:worker A 长循环持租 → UI 发起 RequestMaintenance → 断言 grace 内让渡、Maintenance 获得、A 重申请被拒(RejectionMessage 含"维护");期间 A 若违规硬发命令 → 驱动返回 <code>LEASE_STALE</code>。</li>
</ul>
</div>
 
<!-- ================================================================= -->
<h2 id="s11">11. 分阶段实施计划与工作量</h2>
 
<table>
    <tr><th>阶段</th><th>交付物</th><th>解决的痛点</th><th>工作量</th></tr>
    <tr>
        <td><strong>Phase 1<br>跨进程租约</strong></td>
        <td>device_lease DDL;<code>DbLeaseStore</code>;<code>ResourceManager</code> 组合化;心跳 + 栅栏 + 懒回收;Bootstrapper 切换;多进程集成测试</td>
        <td class="good">多进程互斥仲裁 ✅ 崩溃自愈 ✅ 审计可追溯 ✅ 上层零改动 ✅</td>
        <td><strong>3~4 人日</strong></td>
    </tr>
    <tr>
        <td><strong>Phase 2<br>仲裁规则 & UI</strong></td>
        <td><code>LeaseArbiter</code> 优先级矩阵;维护抢占/让渡流程;<span class="path">DebugSessionManager</span> PendingHandover;HMI"占用者信息/强制释放";access_mode 注册页 + 组租约 <code>AcquireGroupAsync</code></td>
        <td class="good">"人在接线程序不能动"硬保证 ✅ 工位分组 ✅</td>
        <td><strong>2~3 人日</strong></td>
    </tr>
    <tr>
        <td><strong>Phase 3<br>Device Broker(按需触发)</strong></td>
        <td><span class="path">DeviceBroker</span> 工程(复用 ConnectionUtil 管道);代理型驱动 <code>*BrokeredDriver</code>;多机接入;命令队列 + TimeShared 策略落地</td>
        <td class="good">串口/USB 句柄独占根治 ✅ 多机共享 ✅ 分时命令串行化 ✅</td>
        <td><strong>5~8 人日</strong></td>
    </tr>
    <tr>
        <td><strong>Phase 4<br>演进(可选)</strong></td>
        <td>驱动迁移为 OpenTAP Instrument 插件包(.opentap);Bench 配置与 DeviceConfig 单源化;HMI 内嵌驱动管理(Image API)</td>
        <td class="good">厂商驱动生态复用、分发运维</td>
        <td>另评估</td>
    </tr>
</table>
 
<div class="success"><strong>触发 Phase 3 的判断标准(避免过度设计):</strong>出现以下任一情况才立项——①串口/USB 设备租约易手频率高到 close/open 抖动成为瓶颈;②确有 2 台以上电脑需要操作同一台物理仪器;③出现 ≥1 台 TimeShared 命令级串行设备有并行排队需求。否则 Phase 1+2 已闭环主要风险。</div>
 
<!-- ================================================================= -->
<h2 id="s12">12. 风险、反模式与不推荐做法</h2>
 
<table>
    <tr><th>反模式</th><th>后果</th><th>正确做法</th></tr>
    <tr><td class="bad">多进程各自 open 同一 COM 口/CAN 卡句柄"碰运气"</td><td>WriteFile 交错、数据撕裂、蓝屏级未定义行为</td><td>要么租约保证单持有者(Phase 1),要么 Broker 唯一持有(Phase 3)。<strong>物理层不允许并发是硬约束,任何软件方案不能挑战</strong></td></tr>
    <tr><td class="bad">仅用 Named Mutex 当租约</td><td>崩溃后接管方对设备状态全盲(输出可能还开着)</td><td>Mutex 只配做同机快速信号,状态与交接语义必须在 DB 表(§3 方案②评估)</td></tr>
    <tr><td class="bad">网络共享盘(SMB)上放 data.db 供多机写</td><td>SQLite 锁在 SMB 语义不可靠 → 双持有者 = 真实炸机风险</td><td>多机场景:Phase 3 Broker(每台物理机一份 DB)或租约表迁到服务端 SQL</td></tr>
    <tr><td class="bad">客户端自己算 expires_at 传绝对时间</td><td>时钟漂移导致提前/延迟回收</td><td>只传相对 TTL,DB 侧 <code>datetime('now')</code> 计算(§7)</td></tr>
    <tr><td class="bad">TTL 设 5 分钟"求稳"</td><td>崩溃后其他工位干等 5 分钟</td><td>TTL 30s + 心跳 10s;同机 PID 看门狗秒级兜底</td></tr>
    <tr><td class="bad">抢占发生在一条多字节 CAN 事务中间</td><td>设备卡在半包状态</td><td>让渡点严格对齐 TestStep 边界/单条命令完成(§5.4 铁律③)</td></tr>
    <tr><td class="bad">维护模式不做栅栏校验"反正有 UI 弹窗"</td><td>UI 弹窗可被忽略,程序 bug 绕不过</td><td>驱动入口 FenceCheck 是物理闸门(§7 僵尸行),弹窗只是体验层</td></tr>
    <tr><td class="bad">心跳挂在 UI Dispatcher</td><td>WPF 主线程卡顿→假死误判被抢占</td><td>独立后台线程(§9.3 代码已合规)</td></tr>
</table>
 
<h3>已知局限(诚实声明)</h3>
<ul>
<li><strong>SQLite 单写者</strong>:租约写 QPS 上限约百级;老化节拍(秒级)远低于此,但高频遥测不要走租约表(走结果通路)。</li>
<li><strong>Phase 1 后多机仍受限</strong>:单机内多进程完全解决;多机需要把 <code>device_lease</code> 所在库放到服务器(表结构不变,<code>DbLeaseStore</code> 换连接工厂即可——这是 Phase 3 前的最小台阶)。</li>
<li><strong>Broker 单点</strong>:Phase 3 引入看门狗拉起;Broker 重启期间命令短暂不可用(设备本身不受影响)。</li>
</ul>
 
<!-- ================================================================= -->
<h2 id="s13">13. 结论</h2>
 
<div class="success">
<ol>
<li><strong>两类场景分别治理</strong>:进程内多线程继续用现有内存锁 + OpenTAP 同步;跨进程才是真正缺口,不要用一个机制硬吃两个场景。</li>
<li><strong>主体方案 = SQLite 租约表(TTL+心跳+generation 栅栏+懒回收)</strong>,作为 <code>IResourceManager</code> 的跨进程实现,上层 <code>IAutoDeviceFacade</code>/<code>AutoDeviceLease</code>/<code>DebugSessionManager</code> 接口签名与业务代码<strong>零改动</strong>(Phase 1,3~4 人日)。</li>
<li><strong>安全硬保证靠栅栏,不靠礼貌</strong>:每条命令驱动入口校验 generation,实现"人在接线时程序物理发不出指令"(§6 信息+§7 僵尸行)。抢占/让渡只在 TestStep 边界,grace 默认 30s。</li>
<li><strong>Device Broker(Phase 3)仅在触发条件出现时立项</strong>:串口/USB 句柄抖动、多机共测、分时排队——三者任一。传输层直接复用仓库现成的 <code>ConnectionUtil.PipeServer/PipeClient</code>,不必引入 WCF/gRPC。</li>
<li><strong>OpenTAP 生态解决的是"分发与形态"而非"运行时仲裁"</strong>——跨进程仲裁是它的空白,你们的 IResourceManager + 租约表恰好补位,这是本项目差异化资产;长期把物理仪器包装成 OpenTAP Instrument 插件包与租约体系并行不悖。</li>
<li><strong>独占 vs 分时用 <code>access_mode</code> 显式建模</strong>(注册页可选),工位分组复用 <code>SlotDeviceMapping</code> + 组租约按 key 字典序原子申请,杜绝死锁。</li>
<li><strong>崩溃残留是唯一真正难的问题,TTL+看门狗+懒回收三层冗余解决</strong>;测试用 Kill 进程演练接管收敛(§10.2),上线前必须过。</li>
</ol>
</div>
 
<footer>
    多进程/多线程仪器资源独占与共享方案 · TapProject Aging Infrastructure 架构设计 · 2026-11-03<br>
    关联文档:<a href="./OpenTAP_Instrument_Package_Analysis.html">OpenTAP Instrument + 插件包机制分析</a> ·
    <a href="./DeviceConfig_DeviceDebug_Architecture.html">仪器注册与硬件调试调用关系</a>
</footer>
 
<script>
    mermaid.initialize({ 
        startOnLoad: true, theme: 'default', securityLevel: 'loose',
        fontFamily: 'Segoe UI, Microsoft YaHei, sans-serif',
        flowchart: { curve: 'basis', padding: 16 },
        sequence: { showSequenceNumbers: false, actorMargin: 50 }
    });
</script>
 
</body>
</html>