启动辅助界面:有限 Markdown 方言、进度管道与退出码协议

作者:

在

结论:启动辅助界面的三块内容各有明确的契约边界:发布说明走的是一个「有限 Markdown 方言」,只把 === 后的行包成 <h1>、* 行转成列表,末尾只替换 & 与双引号,不转义 < 与 >;进度窗口通过子进程标准输入按行传文本或百分比,空行中断本帧读取而 EOF 不会主动退出;错误窗口的宿主 wait_for_exit() 在成功创建子进程后只接受退出码 1,而 PC 正常关闭路径的退出码预期是 0,可能造成无限等待。以下为固定源码快照 c8fb906 与无 GUI 探针的静态结构分析,未启动构建、注册、更新、raylib 窗口或车载设备。

发布说明的有限 Markdown 方言

parse_markdown() 按换行拆分:后一行以 === 开头时把当前行包为 <h1>,以 * 开头的行转为 <ul>/<li>,并根据前导空格和 tab_length 粗略处理嵌套;其他非空行原样输出。它不是通用 Markdown 解析器,不支持标题等级、代码块、链接和转义规则的完整语义。输入以 ===anything 开头也满足标题分隔条件,非规范缩进可能与内部 list_level 不一致。

末尾统一把 & 变为 &amp;、双引号变为 &quot;,不转义 < 与 >。无设备探针中,输入 <b>note</b> & x 得到 <b>note</b> &amp; x。下游 HtmlRenderer.parse_html_content() 是 raylib 文本渲染器,只识别有限标签,不是浏览器执行环境;但原始 <b> 等受支持标签仍能改变显示结构,不能把这函数当作对任意来源文本的 HTML 净化器。

updated.parse_release_notes() 从 RELEASES.md 取第一个双换行前的最新说明,UTF-8 解码后调用上述函数;解析异常则回退原始字节加换行,文件缺失则返回空字节。结果写入更新参数后,由行车外告警页 ReleaseNotesDialog.refresh() 或软件设置项显示。现有 test_markdown.py 只遍历发布说明段并检查转换结果非空,没有验证标签转义、嵌套列表或异常输入。

构建/注册进度窗口

宿主 common.Spinner 通过 Popen(["./spinner.py"], stdin=PIPE, cwd=system/ui) 启动独立 UI,启动 OSError 只留下 None。update() 向标准输入写入 UTF-8 文本加换行再 flush,它只在 flush 周围捕获 BrokenPipeError,写入本身若抛错会向调用方传播。update_progress(cur,total) 计算并四舍五入百分比,宿主端不检查 total=0 或范围。

契约点 源码行为 探针确认
进度写管道 update() 写 UTF-8 文本加换行,只在 flush 周围捕获 BrokenPipeError 1/4 写为 25n;零分母抛错;flush 断管被吞、write 断管传播。
数字解析 子进程 set_text() 对 str.isdigit() 为真的文本尝试 int() 并裁到 0..100,其余换行为文字 -1 是文字;某些 Unicode 数字如 ² 通过 isdigit() 却在 int() 抛 ValueError,中断当前 UI 主循环。
帧读取 _read_stdin() 用零等待 select 读取就绪行并 strip() 两端空白,读到空行或 EOF 就结束本轮 空行之后尚存输入留待下一帧;main() 每帧只取本次读到的最后一行,EOF 本身不会主动关闭窗口。
子进程回收 Spinner.close() 发 kill(),再 communicate(timeout=2) 回收 与 TextWindow 的终止策略不同。

manager.build() 用它显示 scons 构建进度,失败时关闭 spinner 并打开错误窗口;registration.register() 在需要在线注册时显示阶段文字。二者的进度窗口只是呈现,不是构建/注册完成的证明;真正结果仍由子进程返回码或注册 API 决定。

错误窗口的等待与退出码

场景 子进程退出码 宿主 wait_for_exit() 行为
创建失败 — text_proc is None,直接返回,不会无限循环
PC 正常关闭 预期 0(源码无显式 sys.exit(1)) 只接受 1,状态 0 每 0.1 秒继续轮询,无 deadline
设备 Reboot 路径 源文档未给出显式 sys.exit(1) 同上,可能无限等待(静态推论,未 GUI 实测)

宿主 common.TextWindow 以错误文本为进程参数启动 system/ui/text.py,失败时将 text_proc 设为 None。get_status() 调用 poll() 返回退出码或 None;wait_for_exit() 在成功创建子进程后只接受退出码 1,状态 0 或其他值每 0.1 秒继续轮询,没有 deadline。创建失败后 text_proc is None,则直接返回,不会无限循环;close() 只发 terminate()、置空引用,没有 wait() 确认回收。

UI 子进程 system/ui/text.py 在 PC 上显示 Exit,在设备上显示 Reboot;按钮先调用 gui_app.request_close(),设备上再调用 HARDWARE.reboot()。GuiApplication.render() 在关闭请求后自然结束循环,text.py 的 __main__ 没有显式 sys.exit(1)。因此在普通 PC 正常关闭、脚本正常返回的源码路径上,子进程预期退出码是 0,而宿主只接受 1,可能无限等待;这是跨进程静态推论,尚未用 GUI 实测。

system/ui/text.py 自有的 wrap_text() 不调用通用 lib.wrap_text():它按段落拆开、保留中间/尾部空段,但丢掉前导空段;若首个词本身超宽,会先插入空字符串行,随后仍保留完整超宽词,不执行词内断行。探针确认 abcdefgh 在模拟宽度 4 下输出 ["", "abcdefgh"],a b 在宽度 1 下还可能保留行尾空格。错误追踪中的长路径或不可断词因此可能越过文本视口。

插件化与验证要求

源文档给出的是建议而非已实现能力:更新或插件提供的说明文本须明确来源和信任级别,如需富文本应用结构化、白名单标签生成,不能依赖 parse_markdown() 的 &/引号替换完成净化;进度 UI 的输入应是受限状态对象(阶段、0..100 进度、错误码),而不是让任意插件直接写入引导进程标准输入;启动/故障窗口应受进程监督,退出码、存活检测、最大等待时长、终止后的 wait() 与无 UI 回退日志都需有契约;故障窗口不得阻断安全关机或设备恢复。

已用无设备探针覆盖宿主子进程正常 0/异常 1/无法启动及进度管道 write/flush 断开,另覆盖进度 UI 输入/帧选择和错误 UI 换行/按钮回调;卡住进程的真实资源回收、UI 渲染与设备 Reboot 动作仍需台架验证。

常见问题

parse_markdown() 会转义 HTML 吗?

只替换 & 与双引号,不转义尖括号。探针中一个 b 标签加上 & 与 x 后,b 标签仍原样保留,& 则被编码。它不能当作对任意来源文本的 HTML 净化器。

进度条输入一个负数会怎样?

子进程 set_text() 对 str.isdigit() 为真的文本才尝试 int(),-1 因不是 isdigit() 而被当作文本;而某些 Unicode 数字如 ² 通过 isdigit() 却在 int() 抛 ValueError,会中断当前 UI 主循环。

为什么错误窗口可能一直不退出?

宿主 wait_for_exit() 在成功创建子进程后只接受退出码 1,而 PC 正常关闭路径的 text.py 退出码预期是 0,状态 0 会被每 0.1 秒继续轮询且没有 deadline,因此可能无限等待;这是静态推论,尚未用 GUI 实测。

进度窗口显示完成就代表任务完成了吗?

不代表。进度窗口只是呈现,真正结果仍由子进程返回码或注册 API 决定;进度消息也不是可靠队列或确认协议。

延伸阅读

参考资料

  • 项目 02 —— openpilot-research/BOOTSTRAP_UI_MARKDOWN_PROCESS_CONTRACT_DEEP_DIVE.md(Markdown 方言、进度管道、退出码协议与探针结论)。
  • 项目 02 —— openpilot-research/ 同目录富文本与更新流程专题(作为相邻边界参考)。

说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

更新记录

首次发布 2026-10-11 最近核对:2026-10-11

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注