Files
lou 0827b2399c 内核框架 v0.1 首次提交: 引导器 / 内核 / 驱动 + 日志系统
引导器 UEFI.boot.py (纯 stdlib, 内核的管家)
  体检 6 项 (解释器/venv/包/驱动目录/PG) / venv 重建 / 包台账 / 内核进程启停记账 / 参数原样透传
  内核日志 / 引导器日志 / 内核 stdio 三条道的查看入口都归它

内核 内核/ (常驻调度 = 甲)
  命令消费 (PG commands + LISTEN/NOTIFY) / 驱动调用仲裁六条 (越权·成环·无人提供·按需拉起·同锁排队·超时收权)
  依赖链巡检 10s / 心跳 300s / 独一份调度锁 (pg_try_advisory_lock 0x65666901)
  扫描: 9 条校验 + 契约匹配 + 拓扑排序; 状态: 快照原子写 + 断电收尸判定
  db.py 是唯一碰 SQL 的文件 (drivers/driver_state/events/scans/commands/calls/kernel_env/kernel_runs)

日志系统 (2026-09-16 完整化, 设计/04-日志系统.md)
  三条道一个文件一种内容: 内核.log (结构化行) / 内核.out.log (命令输出 + 崩溃原文) / 引导器.log (引导器动作)
  驱动日志每轮启动前插分隔头; 门槛 log_level / 轮转 log_max_mb + log_keep / -n · -f · --级别 · -g · --json · --全部
  实现 内核/日志.py (内核与引导器共用一份); 修掉"命令输出混进日志文件" (实证存档 归档/20260916-日志系统重做前/)

驱动样板 (也是写驱动的示范): Json解码 (oneshot) / 样板常驻 (provides 样板:心跳) / 样例消费器 (needs 只发契约名)

代码标点统一为 ASCII (保留界面用的框线 ─│◄▶ 与表格占位符 —); 已用 AST 等价对比证明逻辑零改动

自测 (真机, 不 mock): 进程 (真起进程真收子树) / 内核 58 项 / 配置 / db 182 项 / 日志 86 项
验收: 试跑引导器.py PASS 11 / FAIL 0 / 残留无; uvx pyright 与 uvx basedpyright 均 0 errors 0 warnings
2026-09-16 21:03:48 +08:00

346 lines
20 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""内核项目设计图生成器 —— 按老板标准格式(椭圆起止/菱形判断/红虚线失败分支/平行四边形输入输出)。
生成 5 张图到本目录:
00-总览.drawio 四层架构 + PG 内存总线
01-引导器-启动时序.drawio 引导器三职责的启动主链
02-内核-总调度.drawio 扫描 → 契约匹配 → 排序 → 就绪 → 拉起
03-内核调用-时序.drawio 主驱动 / PG / 内核 / 副驱动 四方时序(零通道)
04-驱动-生命周期.drawio 驱动状态机 invalid→stopped→starting→running→…
边元组可带第 6 项 waypoints=[(x,y),...](绝对坐标),用于把回环/跨列线走到空白带。
"""
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
OUT = Path(__file__).resolve().parent
def esc(s: str) -> str:
return (s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
.replace('"', '&quot;').replace('\n', '&#10;'))
VS = {
'box': 'rounded=0;whiteSpace=wrap;html=1;',
'start': 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;',
'io': 'shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;',
'sub': 'shape=mxgraph.flowchart.predefined_process;whiteSpace=wrap;html=1;',
'diamond': 'rhombus;whiteSpace=wrap;html=1;',
'db': 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#dae8fc;strokeColor=#6c8ebf;',
'note': 'shape=note;whiteSpace=wrap;html=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;align=left;spacingLeft=6;',
'hdr': 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;',
'cont': 'swimlane;html=1;startSize=30;fillColor=#f5f5f5;strokeColor=#666666;fontStyle=1;',
}
def ED(ex, ey, nx, ny, dashed=False, color='#000000', bidir=False):
s = ('edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;'
'horizontal=1;labelBackgroundColor=#ffffff;'
f'exitX={ex};exitY={ey};exitDx=0;exitDy=0;entryX={nx};entryY={ny};entryDx=0;entryDy=0;')
if bidir:
s += 'startArrow=classic;endArrow=classic;'
if dashed or color != '#000000':
s += f'dashed=1;strokeColor={color};fontColor={color};'
return s
def V(**k): # 竖直向下:底出口 → 顶入口
return ED(0.5, 1, 0.5, 0, **k)
def R(**k): # 向右:右出口 → 左入口
return ED(1, 0.5, 0, 0.5, **k)
def L(**k): # 向左:左出口 → 右入口
return ED(0, 0.5, 1, 0.5, **k)
RED = {'dashed': True, 'color': '#cc0000'}
GRAY = {'dashed': True, 'color': '#888888'}
def doc(page_w, page_h, hdr, hdr_box, nodes, edges, containers=()):
"""hdr_box=(x,y,w,h);nodes=(id,text,kind,x,y,w,h,parent);containers=(id,title,x,y,w,h)"""
hx, hy, hw, hh = hdr_box
L = ['<mxfile host="app.diagrams.net">',
' <diagram name="第 1 页" id="d1">',
f' <mxGraphModel dx="900" dy="600" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="{page_w}" pageHeight="{page_h}" math="0" shadow="0">',
' <root>',
' <mxCell id="0" />',
' <mxCell id="1" parent="0" />',
f' <mxCell id="hdr" parent="1" value="{esc(hdr)}" style="{VS["hdr"]}" vertex="1">',
f' <mxGeometry height="{hh}" width="{hw}" x="{hx}" y="{hy}" as="geometry" />',
' </mxCell>']
for cid, title, x, y, w, h in containers:
L.append(f' <mxCell id="{cid}" parent="1" value="{esc(title)}" style="{VS["cont"]}" vertex="1">')
L.append(f' <mxGeometry height="{h}" width="{w}" x="{x}" y="{y}" as="geometry" />')
L.append(' </mxCell>')
for nid, text, kind, x, y, w, h, parent in nodes:
L.append(f' <mxCell id="{nid}" parent="{parent}" value="{esc(text)}" style="{VS[kind]}" vertex="1">')
L.append(f' <mxGeometry height="{h}" width="{w}" x="{x}" y="{y}" as="geometry" />')
L.append(' </mxCell>')
for rec in edges:
eid, src, dst, style, label = rec[0], rec[1], rec[2], rec[3], rec[4]
pts = rec[5] if len(rec) > 5 else None
v = f' value="{esc(label)}"' if label else ''
L.append(f' <mxCell id="{eid}" edge="1" parent="1" source="{src}" target="{dst}" style="{style}"{v}>')
if pts:
L.append(' <mxGeometry relative="1" as="geometry">')
L.append(' <Array as="points">')
for px, py in pts:
L.append(f' <mxPoint x="{px}" y="{py}" />')
L.append(' </Array>')
L.append(' </mxGeometry>')
else:
L.append(' <mxGeometry relative="1" as="geometry" />')
L.append(' </mxCell>')
L += [' </root>', ' </mxGraphModel>', ' </diagram>', '</mxfile>']
return '\n'.join(L)
# ───────────────────────── 00 总览 ─────────────────────────
def g00():
nodes = [
('cli', 'UEFI.boot.py <子命令>\n列表 / 启动 / 停止 / 状态 / 日志', 'box', 20, 44, 480, 50, 'C1'),
('out', '答案与日志直接打终端(纯 CLI,没有 TUI)', 'box', 540, 44, 480, 50, 'C1'),
('env', '管内核环境\n解释器 / venv 健康 / 路径', 'box', 20, 46, 320, 68, 'C2'),
('pkg', '管包状态\n缺什么 / 版本不符 / 索引源', 'box', 360, 46, 320, 68, 'C2'),
('kpr', '管内核进程\n拉起 / 判活 / 日志 / 记账', 'box', 700, 46, 320, 68, 'C2'),
('kcmd', '命令循环\ncommands + LISTEN/NOTIFY', 'box', 20, 46, 240, 70, 'C3'),
('kmatch', '契约匹配与排序\nprovides ↔ needs 拓扑', 'box', 280, 46, 240, 70, 'C3'),
('karb', '转发仲裁\n锁 / 超时 / 越权 / 成环', 'box', 540, 46, 240, 70, 'C3'),
('kproc', '驱动进程与状态\n/proc 判活 · 快照', 'box', 800, 46, 220, 70, 'C3'),
('r1', '主驱动 resident\n常驻等活 · LISTEN driver_<名>', 'box', 15, 46, 270, 70, 'C4'),
('r2', '副驱动 oneshot\n按需拉起 · 跑完收尸', 'box', 315, 46, 270, 70, 'C4'),
('skill', 'web.json = {html, css, js}\n由驱动解释执行(样本:Json解码)', 'box', 20, 46, 340, 80, 'C6'),
('t1', 'drivers', 'db', 20, 46, 190, 46, 'C5'),
('t2', 'driver_state', 'db', 230, 46, 190, 46, 'C5'),
('t3', 'events', 'db', 440, 46, 190, 46, 'C5'),
('t4', 'calls', 'db', 650, 46, 190, 46, 'C5'),
('t5', 'commands', 'db', 860, 46, 160, 46, 'C5'),
('t6', 'scans', 'db', 20, 108, 190, 46, 'C5'),
('t7', 'kernel_env', 'db', 230, 108, 190, 46, 'C5'),
('t8', 'kernel_runs', 'db', 440, 108, 190, 46, 'C5'),
('t9', '契约数据表(驱动产出 / 取用,表名由内核约定并注入)', 'db', 650, 108, 370, 46, 'C5'),
('sum', '一句话:引导器管内核(环境 / 包 / 进程)→ 内核管驱动(总调度 / 转发 / 反打架)→ 驱动互不认识、零通道。\n'
'所有状态与请求都落在 PostgreSQL 里 —— 零 socket、零自造协议。',
'note', 60, 1000, 1040, 80, '1'),
]
edges = [
('e1', 'C1', 'C2', V(), '命令 + 参数'),
('e2', 'C2', 'C3', V(), '启动并看住内核进程'),
('e3', 'C3', 'C4', ED(0.5, 1, 0.5, 0, bidir=True), '启停 / 转发 / 判活'),
('e4', 'C3', 'C5', ED(0.5, 1, 0.5, 0, bidir=True), '',
[(1120, 570), (1120, 770)]),
('e5', 'C4', 'C6', R(**GRAY), 'v0.2'),
('e6', 'C6', 'C5', ED(0.5, 1, 0.85, 0, **GRAY), 'v0.2 加载'),
('e7', 'C4', 'C5', ED(0.35, 1, 0.35, 0, **GRAY), '只写 calls / events,读契约表'),
]
containers = [
('C1', '用户 / CLI(人敲命令)', 60, 100, 1040, 110),
('C2', '① 引导器 UEFI.boot.py(短命 CLI · 零第三方依赖)', 60, 240, 1040, 140),
('C3', '② 内核(常驻 · 总调度 · 纯 CLI 命令 + PG 当内存)', 60, 410, 1040, 160),
('C4', '③ 驱动(互不认识 · 零通道 · 只声明「我要什么 / 我产出什么」)', 60, 600, 600, 140),
('C6', '④ 程序 / Skill(声明式 · v0.2 占位)', 720, 600, 380, 140),
('C5', 'PostgreSQL efi_kernel = 内存 / 总线(唯一通道)', 60, 770, 1040, 180),
]
return doc(1160, 1120, '内核项目 总览 / 用户级虚拟操作系统框架\n'
'四层:引导器(UEFI) → 内核 → 驱动 → 程序(Skill)',
(100, 20, 960, 54), nodes, edges, containers)
# ───────────────────────── 01 引导器 ─────────────────────────
def g01():
nodes = [
('st', '开始:python3 UEFI.boot.py <子命令>', 'start', 253, 60, 320, 50, '1'),
('n1', '读 环境.efi.json(唯一一份配置)', 'box', 283, 138, 260, 50, '1'),
('n2', '环境体检(6 项 · 纯 stdlib)\n解释器 / venv 健康 / 包 / 驱动目录 / PG', 'sub', 233, 216, 360, 62, '1'),
('d1', '体检通过 ?', 'diamond', 308, 306, 210, 80, '1'),
('f1', '打印原因 + 修复命令\n退出 1(不自动改)', 'box', 570, 314, 230, 64, '1'),
('n3', '写 环境状态.efi.json(快照,原子替换)', 'box', 253, 416, 320, 50, '1'),
('n4', '写 PG kernel_env\n(连不上 → WARN 降级,不算失败)', 'box', 263, 494, 300, 58, '1'),
('d2', '内核在跑 ?', 'diamond', 308, 580, 210, 80, '1'),
('n5', '拉起内核进程\n(常驻调度器)', 'box', 570, 588, 230, 64, '1'),
('n6', '把子命令交给内核(argv 透传)→ 等结果', 'box', 253, 690, 320, 50, '1'),
('n7', '打印内核输出 + 记 kernel_runs\n(起止 / 耗时 / 退出码)', 'box', 253, 768, 320, 58, '1'),
('en', '结束(不管子进程:驱动是独立进程组)', 'start', 253, 856, 320, 50, '1'),
('tip', '引导器自己的命令(自检 / 环境 / 包)PG 连不上只降级;\n透传给内核的命令不降级 —— 内核的内存就是 PG。',
'note', 60, 950, 700, 66, '1'),
]
edges = [
('e1', 'st', 'n1', V(), ''),
('e2', 'n1', 'n2', V(), ''),
('e3', 'n2', 'd1', V(), ''),
('e4', 'd1', 'n3', V(), '是'),
('e5', 'd1', 'f1', R(**RED), '否'),
('e6', 'n3', 'n4', V(), ''),
('e7', 'n4', 'd2', V(), ''),
('e8', 'd2', 'n6', ED(0.5, 1, 0.35, 0), '是'),
('e9', 'd2', 'n5', R(), '否'),
('e10', 'n5', 'n6', ED(0.5, 1, 0.75, 0), ''),
('e11', 'n6', 'n7', V(), ''),
('e12', 'n7', 'en', V(), ''),
]
return doc(827, 1060, '模块:引导器 UEFI.boot.py(内核的管家)\n'
'输入: 子命令 | 输出: 内核结果 + 环境状态快照 + 台账',
(93, 20, 640, 54), nodes, edges)
# ───────────────────────── 02 内核 总调度 ─────────────────────────
def g02():
nodes = [
('st', '开始:内核 boot / 收到「扫描」命令', 'start', 253, 60, 320, 50, '1'),
('n1', '扫 驱动/:认根目录的 配置.efi.json', 'sub', 253, 138, 320, 50, '1'),
('n2', '读配置 + 9 条校验\n(入口越界 / 重名 / 版本 / venv…)', 'box', 253, 216, 320, 58, '1'),
('n3', '建契约图:provides ↔ needs\n只认契约名,不认驱动名', 'io', 253, 302, 320, 58, '1'),
('d1', '契约齐全且无环 ?', 'diamond', 308, 392, 210, 80, '1'),
('f1', '相关驱动标 invalid\n(契约无人提供 / 成环)\n不影响其他驱动', 'box', 570, 392, 230, 80, '1'),
('n4', '拓扑排序 → 得到启动顺序', 'box', 283, 502, 260, 50, '1'),
('n5', '按序就绪等待:上游起来才放行下游', 'box', 253, 580, 320, 50, '1'),
('n6', '拉起驱动进程(resident 常驻 / oneshot 按需)', 'box', 253, 658, 320, 50, '1'),
('n7', '写 PG driver_state + 运行.efi.json 快照', 'box', 253, 736, 320, 50, '1'),
('n8', '进常驻调度环:听 commands / calls', 'box', 253, 814, 320, 50, '1'),
('en', '本轮调度结束(内核进程不退出)', 'start', 253, 892, 320, 50, '1'),
('f2', '上游崩 → 下游标「依赖失效」\n停上游 → 下游级联停', 'box', 600, 658, 200, 76, '1'),
('f3', '请求越权 / 调用成环 → denied\n超时 → timeout + 释放锁', 'box', 600, 814, 200, 76, '1'),
]
edges = [
('e1', 'st', 'n1', V(), ''),
('e2', 'n1', 'n2', V(), ''),
('e3', 'n2', 'n3', V(), ''),
('e4', 'n3', 'd1', V(), ''),
('e5', 'd1', 'n4', V(), '是'),
('e6', 'd1', 'f1', R(**RED), '否'),
('e7', 'n4', 'n5', V(), ''),
('e8', 'n5', 'n6', V(), ''),
('e9', 'n6', 'n7', V(), ''),
('e10', 'n7', 'n8', V(), ''),
('e11', 'n8', 'en', V(), ''),
('e12', 'n6', 'f2', R(**RED), '崩溃 / 级联'),
('e13', 'n8', 'f3', R(**RED), '反打架'),
]
return doc(827, 1000, '模块:内核 · 总调度(启动期)\n'
'输入: 驱动目录 | 输出: 启动顺序 + driver_state + 常驻调度环',
(93, 20, 640, 54), nodes, edges)
# ───────────────────────── 03 内核调用 时序 ─────────────────────────
def g03():
nodes = [
('a1', '① 写请求\ncalls: want=契约名', 'box', 20, 60, 230, 64, 'CA'),
('a2', '⑩ 读回数据\n(从 PG 契约表取)', 'box', 20, 890, 230, 70, 'CA'),
('p1', '② 请求入库\nstate = pending', 'box', 20, 60, 230, 64, 'CP'),
('p2', '③ NOTIFY 唤醒内核', 'box', 20, 150, 230, 50, 'CP'),
('p3', '⑦ 产出入库(契约表)', 'box', 20, 620, 230, 58, 'CP'),
('p4', '⑨ result 回填 + 释放锁', 'box', 20, 800, 230, 58, 'CP'),
('p5', '⑩ NOTIFY 请求方(或轮询)', 'box', 20, 890, 230, 58, 'CP'),
('k1', '④ 校验:谁提供 / 权限 / 锁 /\n超时 / 调用链成环', 'box', 20, 150, 260, 70, 'CK'),
('k2', '⑤ 已被占用 → 排队 waiting', 'box', 20, 270, 260, 50, 'CK'),
('k3', '⑥ 匹配提供方 → 转发', 'box', 20, 366, 260, 58, 'CK'),
('k4', '⑧ LISTEN 到完成 → 写 result', 'box', 20, 720, 260, 58, 'CK'),
('b1', '执行\nresident 收转发 / oneshot 被拉起', 'box', 20, 366, 230, 70, 'CB'),
('b2', '产出写 PG(不回传)', 'box', 20, 620, 230, 58, 'CB'),
('note', '防打架六条:① 同一 lock_key 串行化(先到先执行,后面排队)② 调用链成环 → denied ③ 提供方没起 → 先起 / 按需拉起\n'
'④ deadline 超时 → 内核收权并释放锁 ⑤ 只能要自己 needs 里声明过的契约 ⑥ 权限分家:驱动只写 calls / events,\n'
'commands 只有 CLI 和引导器能写,drivers / driver_state 只有内核能写\n'
'★ 左列(主驱动)与右列(副驱动)之间没有任何连线 —— 这就是「打架无从谈起」的物理保证。',
'note', 60, 1150, 1170, 150, '1'),
]
edges = [
('e1', 'a1', 'p1', R(), '① INSERT'),
('e2', 'p1', 'p2', V(), ''),
('e3', 'p2', 'k1', R(), '③ NOTIFY'),
('e4', 'k1', 'k2', V(), '否:已被占用'),
('e5', 'k2', 'k3', V(), '轮到自己'),
('e6', 'k3', 'b1', R(), '⑥ 转发(提供方由内核匹配)'),
('e7', 'b1', 'b2', V(), ''),
('e8', 'b2', 'p3', L(), '⑦ 写契约表'),
('e9', 'p3', 'k4', R(), '⑧ 内核 LISTEN 到完成'),
('e10', 'k4', 'p4', ED(0.5, 1, 0.5, 0), '⑨ 写 result + 释放锁'),
('e11', 'p4', 'p5', V(), ''),
('e12', 'p5', 'a2', L(), '⑩ 通知 done'),
]
containers = [
('CA', '主驱动(请求方 · 不认识对面是谁)', 60, 110, 270, 1000),
('CP', 'PostgreSQL efi_kernel(总线)', 350, 110, 270, 1000),
('CK', '内核(常驻调度器 · 唯一仲裁者)', 640, 110, 300, 1000),
('CB', '副驱动(提供方)', 960, 110, 270, 1000),
]
return doc(1320, 1330, '模块:内核调用时序(驱动 → 内核 → 驱动,数据经 PG)\n'
'输入: calls 请求 | 输出: result + 契约表产出(驱动之间零通道)',
(340, 20, 640, 54), nodes, edges, containers)
# ───────────────────────── 04 驱动 生命周期 ─────────────────────────
def g04():
nodes = [
('st', '开始:内核扫描驱动', 'start', 253, 60, 320, 50, '1'),
('d0', '配置合法 ?', 'diamond', 308, 138, 210, 80, '1'),
('f0', 'invalid\n拒载(不影响其他驱动)', 'box', 590, 146, 220, 64, '1'),
('n1', 'stopped(没在跑,正常态)', 'box', 283, 250, 260, 50, '1'),
('n2', 'starting(已 spawn,未确认存活)', 'box', 263, 328, 300, 50, '1'),
('d1', '探活通过 ?', 'diamond', 308, 406, 210, 80, '1'),
('f1', 'failed\n入口不存在 / 秒退(记 exit_code)', 'box', 580, 414, 230, 76, '1'),
('n3', 'running(/proc cmdline 校验通过才算)', 'box', 243, 514, 340, 56, '1'),
('n4', 'crashed(断电收尸)\nPG 说 running 但 /proc 不在', 'box', 610, 514, 200, 76, '1'),
('d2', '还在跑 ?', 'diamond', 308, 604, 210, 80, '1'),
('n5', 'exited(进程没了,记 exit_code)', 'box', 263, 710, 300, 50, '1'),
('d3', 'restart = on-failure ?', 'diamond', 308, 788, 210, 80, '1'),
('n6', 'stopped(收尾完成)', 'box', 283, 894, 260, 50, '1'),
('en', '结束:状态写 PG driver_state + 运行.efi.json 快照', 'start', 213, 972, 400, 50, '1'),
('tip', 'resident = 常驻等活;oneshot = 按需拉起、跑完收尸。\n两种状态机完全相同,区别只在「谁触发启动」。',
'note', 60, 1052, 700, 66, '1'),
]
edges = [
('e1', 'st', 'd0', V(), ''),
('e2', 'd0', 'n1', V(), '是'),
('e3', 'd0', 'f0', R(**RED), '否'),
('e4', 'n1', 'n2', V(), '启动'),
('e5', 'n2', 'd1', V(), ''),
('e6', 'd1', 'n3', V(), '是'),
('e7', 'd1', 'f1', R(**RED), '否'),
('e8', 'n3', 'd2', V(), ''),
('e9', 'n3', 'n4', R(**RED), '断电 / 被杀'),
('e10', 'd2', 'n3', ED(0, 0.5, 0, 0.5), '是(持续判活)', [(110, 644), (110, 542)]),
('e11', 'd2', 'n5', V(), '否'),
('e12', 'n5', 'd3', V(), ''),
('e13', 'd3', 'n2', ED(0, 0.5, 0, 0.5, **RED), '是 → 重拉,并按契约恢复下游', [(190, 828), (190, 353)]),
('e14', 'd3', 'n6', V(), '否'),
('e15', 'n6', 'en', V(), ''),
('e16', 'n4', 'n6', V(), '收尸完成'),
]
return doc(827, 1150, '模块:驱动生命周期(状态机)\n'
'输入: 扫描结果 / 启停命令 | 输出: driver_state + 运行.efi.json',
(93, 20, 640, 54), nodes, edges)
def validate(path: Path) -> None:
t = ET.parse(path)
cells = t.getroot().findall('.//mxCell')
ids = {c.get('id') for c in cells}
bad = [(c.get('id'), c.get('source'), c.get('target')) for c in cells
if c.get('edge') == '1' and (c.get('source') not in ids or c.get('target') not in ids)]
nv = len([c for c in cells if c.get('vertex') == '1'])
ne = len([c for c in cells if c.get('edge') == '1'])
print(f'{path.name}: {nv} 节点 {ne} 连线 断链={"无" if not bad else bad}')
assert not bad, bad
if __name__ == '__main__':
files = {
'00-总览.drawio': g00(),
'01-引导器-启动时序.drawio': g01(),
'02-内核-总调度.drawio': g02(),
'03-内核调用-时序.drawio': g03(),
'04-驱动-生命周期.drawio': g04(),
}
for name, xml in files.items():
p = OUT / name
p.write_text(xml, encoding='utf-8')
validate(p)
print('全部生成完毕 ->', OUT)
sys.exit(0)