JotPluggler 布局会话:前态入栈与草稿遮蔽

作者:

在

结论:JotPluggler 的布局不只是视觉数据——曲线可携带 custom_python,因此加载布局不应自动授予代码执行、文件、网络或 CAN 权限。撤销栈入栈的是修改前快照,由 reset(A) -> 编辑成 B -> push(A) 可推导历史为 [A,A],此时 Undo 得到 A、Redo 也只能得到 A,不能恢复 B。草稿按文件名 stem 放在统一 .jotpluggler_autosave/ 目录,不同目录同名布局可能串用同一草稿。以下为静态源码分析,未编译、未运行 GUI、未验证断电恢复。

布局模型与加载

布局以 JSON 描述 tabs 与工作区递归 split 树,叶节点带 curves 时形成 pane。加载路径先解析文本再建模型,二者都不构成信任验证。

组件 / 函数 源码行为 需要注意
resolve_layout_path() 直接 JSON 路径优先;XML 路径有同名 JSON 时改用 JSON;否则按名称查内置 layouts/ 路径解析不是信任验证;外部布局中的代码见下文
parse_layout() 解析 tabs、current_tab_index(兼容 currentTabIndex);无 tab 抛错,当前页索引夹到有效范围 未在该入口做布局大小、递归深度或来源签名限制
parse_workspace_node() / parse_tab() 递归把带 curves 的叶节点加入 pane 数组,内部节点保存横/纵分割与尺寸 非法节点可能形成空工作区;未做畸形 JSON 压测
parse_dock_area() / parse_curve() 恢复图表、地图、缩略图、相机类型及视窗范围;曲线可携带变换参数与 custom_python 布局并非纯视觉数据
run() / reload_layout() 启动及切换布局时,若对应草稿存在则优先加载草稿,再重置 UI 与撤销状态 同名草稿可以遮蔽用户选定的正式布局

解析入口并未限制布局大小、递归深度或来源签名,也未做畸形 JSON 压测;resolve_layout_path() 只是定位文件,本身不构成信任验证。启动与切换布局时,若对应草稿存在会优先加载草稿,再重置 UI 与撤销状态,因此同名草稿可以遮蔽用户选定的正式布局。

编辑链与 Dock 树

编辑入口围绕 pane 树与 tab 两组操作。split_pane_node() 把叶 pane 改成两个子节点,或在方向相同的父节点中插入新 pane;remove_pane_node() 删除 pane 后整理尺寸与索引。split_pane() 与 close_pane() 是上层入口,apply_pane_menu_action() 处理分割、重置视窗、清空及关闭,apply_pane_drop_action() 处理浏览器曲线、已有曲线和地图/相机等特殊 pane 的拖放。修改后经 commit_tab_layout_change() 更新 pane UI 状态、记录历史并写草稿。

tab 侧由 draw_inline_tab_editor() 提交重命名,另一分支新建、复制和关闭 tab,并调用同一历史/草稿机制。build_dock_tree() 递归把布局树映射为 ImGui Dock 节点,ensure_dockspace() 删除旧节点后按树重建。

这里要区分两种所有权:模型树是持久化状态,ImGui Dock 节点是运行时投影,不应把二者混成插件的同一所有权对象。

撤销、草稿与正式保存

撤销栈保存最多 50 个快照。UndoStack 在加载/重置时把当前布局放入;编辑路径在修改完成后调用 push(before_layout),入栈的是修改前快照而不是当前结果。apply_undo()/apply_redo() 从历史取布局,再由 restore_undo_layout() 同步 UI 并保存草稿。由 reset(A) -> 编辑成 B -> push(A) 可直接推导历史为 [A,A];这是源码逻辑结论,尚未通过 GUI 测试。

撤销与草稿之间存在联动:加载或重置时 UndoStack.reset() 会把当前布局放入历史;每次编辑入栈的是修改前快照;Undo/Redo 从历史取布局后由 restore_undo_layout() 同步 UI 并写草稿。另一方面,mark_layout_dirty() 直接触发 autosave_layout(),记录当前 tab、序列化全布局并置 layout_dirty,因此草稿在用户显式保存之前就已经落盘。重构时应先补状态历史单测,再规定「当前状态入栈」或「前态/后态命令」的统一语义。

阶段 行为 边界
标记脏状态 mark_layout_dirty() 直接调用 autosave_layout(),记录当前 tab、序列化全布局 修改即写草稿,不等用户保存
草稿路径 autosave_path_for_layout() 按文件名 stem 清洗后放入统一 .jotpluggler_autosave/ 不同目录同名布局、甚至清洗后同名 stem 会映射到同一草稿文件(静态推断)
正式保存 save_layout_json() 写 tab、分割树、pane 视窗和非运行时曲线;curve_to_json() 保留 Python 代码 保留代码意味着布局文件不是纯数据
写入方式 write_file_or_throw() 使用 O_TRUNC 直接写目标文件 未见临时文件、原子 rename 或 fsync,不能保证崩溃/断电后一致性
保存与重置 save_layout() 写正式路径后清草稿与 dirty;reset_layout() 删草稿并重载正式布局 草稿损坏时优先解析草稿的路径会报错,代码未见自动回退正式布局

对自制硬件与插件架构的约束

布局可作为可视化配置插件,但其 custom_python 必须拆成独立的代码执行能力:加载布局不应自动授予文件、网络、Params 或 CAN TX 权限。车端部署建议把此类工具与控制进程隔离,默认只读日志/回放,外部布局先做来源确认、版本/schema 校验和执行授权。

草稿键应基于规范化布局身份(例如完整路径哈希或稳定 UUID),并对正式文件与草稿采用临时文件、校验、原子替换及可回退恢复流程。插件 API 应分开 LayoutModel、PaneRenderer、DataSeriesProvider 与 ExecutableTransform;渲染插件只接收经校验的数据视图,不直接获得设备总线或安全配置写权限。这是重构建议,并非现有实现。

同目录测试只检查 -h 输出,不能覆盖布局解析、拖放、撤销重做、草稿碰撞、损坏恢复或断电一致性。本篇未执行测试;绘图、地图、视频/缩略图、日志浏览器与导出仍待分别拆解。

常见问题

加载一个布局就会执行里面的代码吗?

布局解析会保留 custom_python,路线加载还会刷新自定义曲线;但按边界设计,加载布局不应自动授予文件、网络、Params 或 CAN TX 权限,代码执行应作为单独能力另行授权。

Undo 之后再 Redo,能回到被撤销的状态吗?

不能一概而论。由 reset(A) -> 编辑成 B -> push(A) 可推导历史为 [A,A]:Undo 得到 A,Redo 也只能得到 A,无法恢复 B。这是源码逻辑结论,尚未通过 GUI 测试。

为什么不同布局会串用同一个草稿?

因为草稿路径由 autosave_path_for_layout() 按文件名 stem 清洗后放在统一目录;不同目录中的同名布局,甚至清洗后同名的 stem,会映射到同一个草稿文件,存在跨布局串用风险(静态推断)。

保存布局是原子写吗?

不是。write_file_or_throw() 使用 O_TRUNC 直接写目标文件,此路径未见临时文件、原子 rename 或 fsync,因此不能据此保证崩溃/断电后的一致性。

延伸阅读

参考资料

  • 项目 02 —— openpilot-research/JOTPLUGGLER_LAYOUT_SESSION_DEEP_DIVE.md(布局解析、编辑链、撤销栈与草稿持久化)
  • 项目 02 —— openpilot-research/JOTPLUGGLER_DATA_PIPELINES_DEEP_DIVE.md(布局依赖的数据管线,作为本文的补充阅读)
  • 项目 02 —— openpilot-research/JOTPLUGGLER_EXTRACTION_CUSTOM_CODE_DEEP_DIVE.md(自定义代码的执行链,支撑本文的代码能力拆分建议)

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

更新记录

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

评论

发表回复

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