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

183 lines
6.0 KiB
Python

"""文本排版: CJK 宽度感知的对齐 / 表格 / 横线 / 整块输出.
[为什么单独一个文件]
面向老板的界面一律中文, 而且要对齐成表.中文是**双宽**字符, 直接 len() 排出来一定是歪的:
len("状态") == 2, 但屏幕上占 4 列, 混着 ASCII 列就会越往后越乱.
内核和引导器都要打这种表, 所以抽出来只此一份实现, 不复制 (设计 02 §7 "一套职责一个文件").
[依赖]
纯 stdlib (unicodedata).引导器是零第三方依赖的, 这个文件不能破例.
[宽度规则]
* unicodedata.combining(c) 非 0 -> 组合符 (声调符之类), 占 0 列
* east_asian_width(c) 是 W / F -> 宽 / 全角, 占 2 列
* 其余 (含 A 歧义 / H 半角 / Na) -> 占 1 列
歧义字符按 1 列算是有意的: Linux 终端默认窄渲染, 按 2 算会多留空格.
[约定]
表格类函数**返回 list[str] 而不是直接打印**, 这样调用方能自己拼标题 / 汇总行,
也能在 --json 分支里复用同一份宽度计算.
"""
from __future__ import annotations
import sys
import unicodedata
# 对齐参数的字面量.不用第三方枚举 (零依赖), 也不要用 True/False 这种看不懂的写法.
: str = "左"
: str = "右"
def 字符宽(字符: str) -> int:
"""单个字符在终端里占几列.
参数:
字符: 长度为 1 的字符串.传更长的串不会报错, 但结果没意义 (只按第一个属性判).
返回:
0 (组合符) / 1 (半角) / 2 (全角或宽).
说明:
只看 east_asian_width 的 W / F 两类.A (Ambiguous, 如全角破折号,某些符号)
故意按 1 列算, 因为 Linux 终端默认是窄渲染, 按 2 算会跟实际屏幕对不上.
"""
if unicodedata.combining(字符):
return 0
if unicodedata.east_asian_width(字符) in ("W", "F"):
return 2
return 1
def 显示宽度(文本: str) -> int:
"""整串在终端里占几列 (逐字符累加, 不是 len()).
参数:
文本: 任意字符串, 可以混中英文.
返回:
占用的列数.空串返回 0.
"""
= 0
for 字符 in 文本:
+= 字符宽(字符)
return
def 截断(文本: str, : int) -> str:
"""超宽就截断, 并在末尾补一个省略号 (省略号本身按 1 列算).
参数:
文本: 原串.
宽: 允许的最大列数.
返回:
截断后的串; 没超宽则原样返回.
宽 <= 0 返回空串.
说明:
截断点按"列"算而不是按"字符"算, 所以不会把一个全角字符砍成半截.
预留 1 列给省略号: 条件是 用 + 宽2 > 宽 - 1.
"""
if <= 0:
return ""
if 显示宽度(文本) <= :
return 文本
: list[str] = []
= 0
for 字符 in 文本:
宽2 = 字符宽(字符)
if + 宽2 > - 1:
break
.append(字符)
+= 宽2
return "".join() + "…"
def 填充(文本: str, : int, 对齐: str = ) -> str:
"""把一串补空格到指定列数 (先截断再补, 所以结果一定不超宽).
参数:
文本: 原串.
宽: 目标列数.
对齐: 左 (默认, 末尾补空格) 或 右 (开头补空格, 数字列好看).
返回:
补齐后的串, len() 可能不等于 宽, 但 显示宽度() 一定 <= 宽.
"""
文本 = 截断(文本, )
= - 显示宽度(文本)
if < 0:
= 0
if 对齐 == :
return " " * + 文本
return 文本 + " " *
def 横线(: int, 字符: str = "─") -> str:
"""画一条分隔线 (表头下面那条).
参数:
宽: 列数 (表格里传某一列的列宽).
字符: 线用的字符, 默认 U+2500 制表符横线 (终端里一定等宽).
返回:
重复 宽 次的字符串; 宽 <= 0 返回空串.
"""
if <= 0:
return ""
return 字符 *
def 表格(
表头: list[str],
行表: list[list[str]],
对齐表: list[str] | None = None,
间隔: str = " ",
) -> list[str]:
"""渲染一张对齐的中文表格, 返回各行文本 (不打印).
参数:
表头: 列名列表, 长度即列数.
行表: 每行是一个字符串列表; 某行短于列数时, 缺的列按空串处理 (不报错).
对齐表: 每列的对齐方式 (左 / 右); 缺省全左.
间隔: 列与列之间塞几个空格, 默认 2 个 (中文表看着不挤).
返回:
行文本列表: [表头, 横线, 数据行...].调用方自己决定要不要再加汇总行.
说明:
列宽 = 该列表头与所有数据行里最宽的那个 (显示宽度), 全表一次性算好,
所以中途插入更长的行不会把别的行挤歪.
"""
列数 = len(表头)
宽表: list[int] = []
for in range(列数):
= 显示宽度(表头[])
for in 行表:
if < len():
= max(, 显示宽度([]))
宽表.append()
对表 = 对齐表 if 对齐表 is not None else [] * 列数
: list[str] = []
.append(间隔.join(填充(表头[], 宽表[]) for in range(列数)))
.append(间隔.join(横线(宽表[]) for in range(列数)))
for in 行表:
: list[str] = []
for in range(列数):
= [] if < len() else ""
= 对表[] if < len(对表) else
.append(填充(, 宽表[], ))
.append(间隔.join())
return
def 打印(: list[str]) -> None:
"""整块输出一行行文本.
参数:
行: 要输出的每行内容 (通常直接用 表格() 的返回值 + 自己加的汇总行).
返回:
None.
说明:
一次 write 写完再 flush, 不逐行 print: 逐行打印时终端会一行行挤出来,
管道重定向 (老板要 `> 文件`) 时也不会被行缓冲切得七零八落.
"""
sys.stdout.write("\n".join() + "\n")
sys.stdout.flush()