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

232 lines
12 KiB
Python

#!/usr/bin/env python3
"""日志库真机自测: 真写文件, 真轮转, 真起子进程跟日志 -- 不 mock.
用法: .venv/bin/python 内核/自测日志.py
覆盖: 级别门槛 / 行解析与猜级别 / 写(门槛过滤) / 尾读(级别+关键词过滤) / 轮转(真造超限文件) /
实时跟(真起一个子进程跟在后面, 追加两条看它吐哪条) / 选项解析 / 启动分隔头(真起进程).
隔离: 所有测试文件都在 tempfile.mkdtemp() 的临时目录里, 跑完整个删掉 --
生产日志 (内核/logs/*.log, 驱动/*/logs/*.log) 一个字节都不碰 (开头结尾各拍一次快照核对).
"""
from __future__ import annotations
import shutil
import subprocess
import sys
import tempfile
import time
from pathlib import Path
项目根 = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(项目根 / "内核"))
import 日志 # noqa: E402
import 进程 # noqa: E402
工作 = Path(tempfile.mkdtemp(prefix="_自测日志_"))
生产日志目录 = 项目根 / "内核" / "logs"
失败 = 0
def 核对(名称: str, 实际: object, 期望: object) -> None:
"""一项核对 (照 自测进程.py 的口径: 打实际值, 不对才追加期望值)."""
global 失败
ok = 实际 == 期望
if not ok:
失败 += 1
print(f"[{'OK ' if ok else 'FAIL'}] {名称}: {实际!r}" + ("" if ok else f" (期望 {期望!r})"))
def 快照(目录: Path) -> dict[str, int]:
"""目录里 .log 文件的 名字->大小 (核对"自测没碰生产日志")."""
if not 目录.exists():
return {}
return {.name: .stat().st_size for in sorted(目录.glob("*.log"))}
def 造满(路径: Path, 标记: str, 行数: int = 400) -> int:
"""造一个"全是标记行"的大文件 (轮转测试用: 之后靠标记认得出这份是哪一轮的)."""
路径.write_text("".join(f"{标记}{} 行 填充填充填充填充\n" for in range(行数)), encoding="utf-8")
return 路径.stat().st_size
生产快照 = 快照(生产日志目录)
print(f"工作目录: {工作}")
print(f"生产日志快照: {生产快照}")
print("== 1) 级别与门槛 (纯逻辑) ==")
核对("规范化 info -> INFO", 日志.规范化级别("info"), "INFO")
核对("规范化 认不出来 -> INFO", 日志.规范化级别("瞎写的"), "INFO")
核对("规范化 空 -> INFO", 日志.规范化级别(None), "INFO")
核对("级别表顺序", 日志.级别表, ("DEBUG", "INFO", "WARN", "ERROR"))
核对("WARN 比 INFO 严重", 日志.级别序号("WARN") > 日志.级别序号("INFO"), True)
核对("ERROR 过 WARN 门槛", 日志.达标("ERROR", "WARN"), True)
核对("INFO 过不了 WARN 门槛", 日志.达标("INFO", "WARN"), False)
核对("空门槛全过", 日志.达标("DEBUG", ""), True)
print("== 2) 行解析与猜级别 ==")
样本 = 日志.格式化("2026-09-16T20:00:00+08:00", "WARN", "内核", "测试 一条")
核对("格式化", 样本, "2026-09-16T20:00:00+08:00 WARN [内核] 测试 一条")
字段 = 日志.解析行(样本)
核对("解析 级别", (字段 or {}).get("级别"), "WARN")
核对("解析 来源", (字段 or {}).get("来源"), "内核")
核对("解析 消息", (字段 or {}).get("消息"), "测试 一条")
核对("解析 时刻", (字段 or {}).get("时刻"), "2026-09-16T20:00:00+08:00")
核对("裸行解析给 None", 日志.解析行("假内核: 心跳"), None)
核对("猜级别 裸行 Traceback", 日志.猜级别("Traceback (most recent call last):"), "ERROR")
核对("猜级别 裸行 [WARN]", 日志.猜级别("[WARN] 快没电了"), "WARN")
核对("猜级别 裸行 普通输出", 日志.猜级别("假内核: 心跳"), "INFO")
核对("猜级别 结构化行用真字段", 日志.猜级别(样本), "WARN")
核对("命中 关键词在", 日志.命中(样本, "", "测试"), True)
核对("命中 级别不够", 日志.命中(样本, "ERROR", ""), False)
核对("命中 关键词不在", 日志.命中(样本, "", "没有这段"), False)
print("== 3) 记(): 真写文件 + 门槛过滤 ==")
一个 = 工作 / "一个.log"
核对("写 INFO 返回 True", 日志.(一个, "INFO", "内核", "第一条", 控制台=False), True)
核对("DEBUG 被门槛挡 (返回 False)", 日志.(一个, "DEBUG", "内核", "被挡掉的", 门槛="INFO"), False)
核对("ERROR 过门槛", 日志.(一个, "ERROR", "内核", "真错误", 门槛="INFO"), True)
核对("写到子目录也能建", 日志.(工作 / "子" / "目录.log", "INFO", "内核", "x"), True)
行表 = 日志.读全部(一个)
核对("文件里 2 行 (DEBUG 没进去)", len(行表), 2)
核对("第一行内容", (日志.解析行(行表[0]) or {}).get("消息"), "第一条")
核对("第二行级别", (日志.解析行(行表[1]) or {}).get("来源"), "内核")
核对("门槛空 = 全记", 日志.(一个, "DEBUG", "内核", "门槛空", 门槛=""), True)
核对("大小文本 5MB", 日志.大小文本(5 * 1024 * 1024), "5.0MB")
print("== 4) 尾读: 行数 / 级别 / 关键词 ==")
多条 = 工作 / "多条.log"
for in range(12):
= "ERROR" if in (0, 5) else "INFO"
日志.(多条, , "内核", f"第{}条 标记{'甲' if % 2 else '乙'}", 门槛="")
核对("尾 5 行", len(日志.(多条, 5)), 5)
核对("尾 5 行是最后 5 条", (日志.解析行(日志.(多条, 5)[0]) or {}).get("消息"), "第7条 标记甲")
核对("级别 ERROR 只 2 条", len(日志.(多条, 100, 级别="ERROR")), 2)
核对("关键词 甲 6 条", len(日志.(多条, 100, 关键词="甲")), 6)
核对("级别+关键词 同时筛", len(日志.(多条, 100, 级别="ERROR", 关键词="乙")), 1)
核对("尾 0 行给空表", 日志.(多条, 0), [])
核对("尾 文件不存在给空表", 日志.(工作 / "没有这个.log", 5), [])
核对("行数比文件大 = 全给", len(日志.(多条, 999)), 12)
print("== 5) 轮转 (真造超限文件) ==")
= 工作 / "大.log"
尺寸 = 造满(, "第一批")
核对("不到上限不轮转", 日志.轮转(, 上限字节=尺寸 * 2, 保留份数=2), False)
核对("超上限就轮转", 日志.轮转(, 上限字节=尺寸 // 2, 保留份数=2), True)
核对("原文件已改名 (.1)", 日志.带序路径(, 1).exists(), True)
核对("原文件不在了 (被改名搬走)", .exists(), False)
核对(".1 里是第一批", "第一批" in 日志.带序路径(, 1).read_text(encoding="utf-8"), True)
造满(, "第二批")
核对("第二轮轮转", 日志.轮转(, 上限字节=尺寸 // 2, 保留份数=2), True)
造满(, "第三批")
核对("第三轮轮转", 日志.轮转(, 上限字节=尺寸 // 2, 保留份数=2), True)
核对("名单 老->新 (第三次轮转后原文件也不在了)", [.name for in 日志.轮转名单()], ["大.log.2", "大.log.1"])
核对(".2 是第二批 (第三批把第一批挤掉了)", "第二批" in 日志.带序路径(, 2).read_text(encoding="utf-8"), True)
核对(".1 是第三批", "第三批" in 日志.带序路径(, 1).read_text(encoding="utf-8"), True)
核对("保留份数=0 = 不轮转", 日志.轮转(, 上限字节=1, 保留份数=0), False)
核对("上限=0 = 不轮转", 日志.轮转(, 上限字节=0, 保留份数=3), False)
核对("尾读含历史份 (最早那批已按保留份数挤掉, 剩 .2 + .1)", len(日志.(, 9999)), 800)
核对("尾读不含历史份 -> 原文件不在给 0", len(日志.(, 9999, 含轮转=False)), 0)
造满(, "第四批")
核对("尾读不带历史份 = 只看当前文件", len(日志.(, 9999, 含轮转=False)), 400)
核对("尾读带历史份 = 当前 + 两份历史", len(日志.(, 9999)), 1200)
print("== 6) 实时跟 (-f): 真起子进程跟在后面 ==")
跟文件 = 工作 / "跟.log"
跟文件.write_text("开头一条\n", encoding="utf-8")
代码 = (
"import sys; sys.path.insert(0, "
+ repr(str(项目根 / "内核"))
+ "); from pathlib import Path; import 日志; sys.exit(日志.跟(Path("
+ repr(str(跟文件))
+ "), 级别='ERROR', 初始行=10))"
)
= subprocess.Popen(
[sys.executable, "-c", 代码],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
time.sleep(1.2)
日志.(跟文件, "INFO", "内核", "这条不该跟出来", 门槛="")
日志.(跟文件, "ERROR", "内核", "这条该跟出来", 门槛="")
time.sleep(1.5)
.terminate()
try:
跟出, _ = .communicate(timeout=5)
except subprocess.TimeoutExpired:
.kill()
跟出, _ = .communicate(timeout=5)
核对("跟: 吐出了 ERROR 那条", "这条该跟出来" in 跟出, True)
核对("跟: 过滤掉了 INFO 那条", "这条不该跟出来" in 跟出, False)
核对("跟: 初始的裸行 (猜 INFO) 也被级别挡住", "开头一条" in 跟出, False)
核对("跟: 子进程已收工", .poll() is not None, True)
print("== 7) 选项解析 (内核与引导器共用一份) ==")
, 问题 = 日志.解析选项(["样板常驻", "-n", "5", "--级别", "warn", "-g", "心跳", "--json"])
核对("名", ., "样板常驻")
核对("行数", .行数, 5)
核对("级别自动大写", .级别, "WARN")
核对("关键词", .关键词, "心跳")
核对("json", .json输出, True)
核对("没问题", 问题, "")
核对("默认行数可传", 日志.解析选项([], 77)[0].行数, 77)
核对("--全部", 日志.解析选项(["--全部"])[0].全部, True)
核对("--内核", 日志.解析选项(["--内核"])[0].内核, True)
核对("--引导器", 日志.解析选项(["--引导器"])[0].引导器, True)
核对("--输出", 日志.解析选项(["--输出"])[0].输出, True)
核对("-f", 日志.解析选项(["-f"])[0].跟随, True)
核对("坏级别报错", 日志.解析选项(["--级别", "瞎写"])[1], "--级别 只认 DEBUG/INFO/WARN/ERROR, 收到 '瞎写'")
核对("不认识的参数报错", 日志.解析选项(["--瞎写"])[1], "不认识的参数: --瞎写")
核对("缺值报错", 日志.解析选项(["-n"])[1], "-n 后面要跟一个数字")
核对("行数写错报错", 日志.解析选项(["-n", "五"])[1], "-n 后面要跟一个数字, 收到 '五'")
print("== 8) JSON Lines 输出 ==")
多行 = 日志.转json行([样本, "假内核: 心跳"])
核对("两行 jsonl", len(多行.strip().splitlines()), 2)
核对("结构化行带字段", '"级别": "WARN"' in 多行, True)
核对("裸行给空来源", '"来源": ""' in 多行, True)
核对("裸行级别靠猜", '"级别": "INFO"' in 多行, True)
print("== 9) 启动分隔头 (真起进程, 走 进程.启动) ==")
假驱动 = 工作 / "说一句.py"
假驱动.write_text("print('我起来了')\n", encoding="utf-8")
分隔日志 = 工作 / "分隔.log"
结果 = 进程.启动(
[sys.executable, str(假驱动)],
cwd=工作,
日志=分隔日志,
env=None,
入口=假驱动,
探活秒=0.6,
分隔="启动 假驱动 说一句.py",
)
文本 = 分隔日志.read_text(encoding="utf-8")
核对("分隔头写进日志了", "==== 启动 假驱动 说一句.py" in 文本, True)
核对("进程自己的输出也在", "我起来了" in 文本, True)
核对("分隔头在进程输出之前", 文本.index("====") < 文本.index("我起来了"), True)
核对("秒退仍如实报 (这驱动就是跑完就退)", 结果.ok, False)
分隔日志2 = 工作 / "不分隔.log"
_ = 进程.启动([sys.executable, "-c", "print('没分隔')"], cwd=工作, 日志=分隔日志2, 入口=工作 / "x.py")
核对("不传分隔就不写分隔头", "====" in 分隔日志2.read_text(encoding="utf-8"), False)
print("== 10) 清场与残留终检 ==")
shutil.rmtree(工作, ignore_errors=True)
核对("临时工作目录已删干净", 工作.exists(), False)
核对("生产日志一个字节都没动", 快照(生产日志目录), 生产快照)
: list[int] = []
for in Path("/proc").glob("[0-9]*"):
try:
命令行 = ( / "cmdline").read_bytes().decode("utf-8", "replace")
except OSError:
continue # 进程刚没了 / 读不到, 跳过
if "日志.跟(" in 命令行 and str(工作) in 命令行:
.append(int(.name))
核对("没有跟日志的自测残留进程", , [])
print()
print(f"结论: {'全过' if 失败 == 0 else f'{失败} 项没过'}")
sys.exit(0 if 失败 == 0 else 1)