Repository navigation
feat(ostool-server): control axloader boot and OTA through device HTTP - #198
Conversation
There was a problem hiding this comment.
审查结论:COMMENT
没有阻塞性缺陷;CI 全绿、v2/v3 兼容路径未被破坏。但有 1 项恢复路径问题与若干文档/测试补充建议,详见下。
本 PR 做了什么
- 新增 v5 设备协议(
DEVICE_PROTOCOL_VERSION = 5):装载器在同一网卡广播 MAC/架构/启动代次/HTTP 端口,ostool-server 反向GET /api/v1/status核对 MAC、代次、架构后才按板卡 TOML 的 MAC 绑定下发;device::reconcile/push_boot/push_file从会话存储重新核对长度与 SHA-256,把内核与可选 initramfs 直接推给设备的启动事务接口再请求交接,不再向 v5 装载器发送文件 URL。 - 新增独立于 Session 的持久 OTA 镜像库与任务(
ostool-server/src/ota.rs、api/ota.rs);v4 轮询可收到update/confirm_update,PROTOCOL_VERSION由 3 提到 4 并新增PREVIOUS_PROTOCOL_VERSION = 3继续兼容 v2/v3;LoaderPollResponse增加两个变体,LoaderPollRequest增加可选ota字段。 - 管理面:镜像上传/指派/取消、
otaSSE 主题、BoardEditor 内的BoardOta面板;文档重写与本地隔离脚本ostool-server/scripts/test-axloader-local.py。
影响面与是否隔离
- v2/v3 的发现/offer、poll、状态上报、启动契约保持不变,
offer/accept_poll/accept_status只是把可接受版本集合扩成 2|3|4,v2 仍只接收无initramfs/cmdline的启动;ostool CLI 不构造LoaderPollRequest(只在ostool/src/board/client.rs拼 loader-status URL),因此协议常量变更不影响 CLI。 - 公开契约变化:
httpboot-protocol0.3.0→0.4.0 且PROTOCOL_VERSION3→4,v4 poll 必须携带ota,否则ota_state_missing(见残余风险)。 LoaderRegistry::prune()增加了 v5 冲突分支,但保留了原先基于 superseded report 的冲突语义(current_registration_id在registrations中时行为与基线一致)。- 关键耦合:v4 poll 与 v5 reconcile 的启动下发现在都要先经过 OTA 决策(
Decision::Wait会直接 return),因此 OTA 状态机会影响既有启动路径 —— 见 F-001。 - 未发现锁序问题:
queue/decide/report统一按 observations→jobs 取锁;decide/report的持久化只在阶段真正变化时写入;put_image的 PE 偏移校验有checked_add(94)边界,无越界读。
验证
- CI(head 67ddda2):
check (stable, x86_64-unknown-linux-gnu)两条均 completed/success,覆盖cargo fmt --all -- --check、clippy、build、cargo test、webui 单测/build/Playwright e2e 与cargo publish --workspace --dry-run --locked;check run 上 4 条ENOENT .../target/tests/target注解指向 action 自身步骤,与本 PR 改动无关,没有由本 PR 引起的 CI 失败。 - 本地:本审查环境没有 Rust 工具链(
cargo/rustc不存在)与 pnpm,无法复现 PR 描述的cargo fmt/clippy/test与前端命令,因此没有本地运行结果,验证结论以 CI 为准。 - 阅读范围:23 个变更文件全部通读,重点核对 ota.rs / device.rs / api/ota.rs / loader.rs / api/router.rs 与既有 v2-v4 路径的交互、锁序、PE 校验边界与路径拼装。
既有意见
reviews=0、issue comments=0、review comments=0,没有需要跟踪的历史意见或未解决讨论。
建议处理(均非阻塞)
- F-001(advisory)
ostool-server/src/ota.rs:251:取消只允许Queued,而任何非终态任务都会阻断该板卡的启动下发,DELETE与重新指派都不可用 —— 见内联评论。 - F-002(advisory)
README.md:18/README.en.md:18:两处仍把docs/axloader-network-control.md描述为“v3 宿主启动制品及 v2 兼容、持久 MAC 绑定、Web 管理和内建 QEMU 虚拟板设计”,但该文档已被本 PR 重写(标题改为“axloader 网络控制与本地验证”,删除了内建 QEMU 虚拟板/CLI 行为/HTTP 接口小节),中英描述都不再成立,也未提及 v4/v5。AGENTS.md 要求用户可见工作流变化时同步文档并保持中英一致(同作者 PR #194 就是这么做的),建议一并更新。 - F-003(advisory)
docs/api.md:673:新增链接的锚点#装载器升级接口v4在新文档中不存在,本 PR 新增的 loader-images / loader-updates / loader-updates/{id}/image / loaders/ota-status 端点也没有文档 —— 见内联评论。 - F-004(advisory)
webui/src/pages/BoardOta.tsx:35:把file.name当版本标签,含空格/中文的文件名会被服务端 400 拒绝 —— 见内联评论。 - F-005(advisory,测试缺口) 新增的
device.rs(reconcile/push_boot/push_file,338 行)没有仓库内自动化测试,唯一端到端证据是需要 TGOS 工作树 + OVMF + KVM + mtools 的手工脚本,CI 无法执行;router.rs 新增用例只覆盖服务端 poll/update/download/report。建议为推送与确认时序补一个本地假设备用例(hyper/axum 假设备 + 临时 OtaStore 目录),覆盖 epoch 不匹配、摘要不符、复位重推、确认校验失败等分支。 - F-006(advisory)
device.rs:70:v5 状态里的ota是Option,缺失时服务端静默跳过启动推送(既不下发 OTA 也不下发启动,且没有任何日志)。建议记一次明确 warn 或按空状态继续走启动分支,并在文档中把ota写成 v5 必需字段。
残余风险(不阻塞)
- v5 广播与设备 HTTP 无认证:任何能发 UDP 广播的主机都能让服务端对
peer.ip():http_port发GET /api/v1/status,并在有排队任务时把镜像推给它。与文档声明的“可信隔离实验网”一致,但上线前需要网络隔离或后续认证。 PROTOCOL_VERSION3→4 且 v4 poll 必须带ota:对 0.4.0 crate 的其他消费方是隐性不兼容(不带ota的 v4 设备会被ota_state_missing拒绝);TGOS 目前锁 0.3.0(版本 3),不受影响。loader_network.bind_addr若被配置为 IPv6([::]:2998),format!("http://{}:{}", peer.ip(), port)生成的 URL 不带方括号,reconcile 会持续失败;默认值0.0.0.0:2998无此问题。
Powered by deepseek-flash
There was a problem hiding this comment.
审查结论:COMMENT(无阻塞问题)
本 PR 做了什么
- 新增 v5 设备协议(
DEVICE_PROTOCOL_VERSION = 5):装载器在同一 UEFI 网卡广播 MAC/架构/启动代次/HTTP 端口,loader::serve_udp_discovery()调用device::reconcile(),先反向GET /api/v1/status核对 MAC、启动代次与架构,再按板卡 TOML 的 MAC 绑定决策;device::push_boot/push_file从会话存储重新核对长度与 SHA-256 后经设备 HTTP 推送,不再给 v5 装载器文件 URL。 - 新增独立于 Session 的持久 OTA 镜像库与任务(
ostool-server/src/ota.rs、api/ota.rs):v4 轮询可收到update/confirm_update,PROTOCOL_VERSION3→4 并新增PREVIOUS_PROTOCOL_VERSION = 3继续兼容 v2/v3,httpboot-protocol0.3.0→0.4.0。 - 管理面:镜像上传/指派/取消、
otaSSE 主题、BoardEditor 内的BoardOta面板;文档重写(含端点与状态码表)与本地隔离脚本ostool-server/scripts/test-axloader-local.py。
影响面与是否隔离
- v2/v3 未被破坏:
offer/accept_poll/accept_status只把可接受版本扩成 2|3|4;LoaderPollRequest.ota带serde(default)+skip_serializing_if,旧 v3 JSON 仍可解析;v2 仍只在无initramfs/cmdline时接收启动;prune()的 v5 冲突分支仅在current_registration_id不在registrations时生效,v2-v4 冲突语义与基线一致。ostoolCLI 不构造LoaderPollRequest,协议常量变更不影响 CLI。 - 有意的契约变化(已文档化):0.4.0 的 v4 poll 必须携带
ota,否则得到ota_state_missing;LoaderPollResponse新增两个变体。TGOS 目前锁 0.3.0(版本 3),不受影响。 - 关键耦合:v4 poll 与 v5 reconcile 的启动下发现在都先经过 OTA 决策,
Decision::Wait会直接返回,OTA 状态机已成为启动路径的一部分(见下方 F-001 与残余风险)。锁序一致(observations→jobs,queue/decide/report/cancel均相同),未发现死锁、越界读(put_image的 PE 偏移有checked_add(94)边界)或 RAII 清理缺口。
验证
- CI(当前 head c06bfcb):两条
check (stable, x86_64-unknown-linux-gnu)均 completed/success、annotation 为 0,覆盖cargo fmt --all -- --check、clippy、build、cargo test、webui 单测/build/Playwright e2e 与cargo publish --workspace --dry-run --locked。没有由本 PR 引起的失败。 - 本地:本环境缺 C 工具链与 pnpm,
cargo test/clippy无法编译(linker cc not found),前端命令未运行。已成功运行:cargo fmt --all -- --check(通过)、cargo metadata --locked(退出码 0,说明本 PR 的 Cargo.lock 与清单一致)。 - 阅读范围:27 个变更文件全部核对,重点复核
ota.rs状态机与持久化、device.rs推送/确认、api/ota.rs契约、loader.rs的 v5 分发与 prune。
既有意见
之前 Mai 审查(head 67ddda2)的 6 条意见均非阻塞,逐条核实已在 c06bfcb 修复:取消任意非终态任务并清除观察记录(cancel() + active_update_can_be_cancelled_and_replaced_after_restart,UI 对所有非终态显示取消入口);README 中英描述已更新;锚点修正并补齐 loader-images/loader-updates/DELETE/image/ota-status 状态码表;前端 sanitizeImageVersion + Vitest 覆盖空格/中文/空标签/96 字节;新增两个 axum 假设备测试覆盖 epoch 变化重推、摘要拒绝重试、确认摘要不匹配、无 ota 时的启动推送;无 OTA 状态改为告警并继续启动推送。作者 3 条回复与代码一致,无需继续跟踪。
未解决项(均为 advisory,见内联评论)
- F-001
ostool-server/src/device.rs:219:push_boot()只用 create 建立事务,未使用协议中定义的DELETE /api/v1/boot/jobs/{id};同一设备启动代次内旧清单仍在时,新boot_id的清单会持续 409,启动推送无法送达。 - F-002
ostool-server/src/api/ota.rs:54:Content-Length: 0与超限共用 413EFI image exceeds 32 MiB,与 docs/api.md 的 400/413 契约不一致。 - F-003(body-only) 持久镜像库只增不减:无删除端点也无回收策略,长期运行会让
data_dir无限增长(每个 ≤32 MiB),GET /api/v1/admin/loader-images还会把全部镜像返回到管理页。
残余风险(不阻塞)
- v5 广播与设备 HTTP 无认证:任何能发 UDP 广播并可指定端口的主机都能让服务端对
peer.ip():http_port发GET /api/v1/status,并在有排队任务时把镜像推给它。与文档声明的“可信隔离实验网”一致,上线前需网络隔离或后续认证。 - 待试槽只允许在板卡空闲时确认:若在设备进入待试槽后、下一次广播前启动 Session,服务端会因
board_idle=false持续返回Wait,该 Session 的启动推送被暂缓,直到设备按自身 A/B 规则回滚。文档已说明该语义,但缺少自动化覆盖。 - 端到端证据仍依赖
ostool-server/scripts/test-axloader-local.py(需 TGOS 工作树 + OVMF + KVM + mtools),CI 不执行;文档示例中的--tgos /home/zhourui/.codex/worktrees/9a24/tgoskits-dev是开发者本机绝对路径,建议改为占位路径以便复现。 - 相似 PR:仓库当前仅本 PR 与 #196(FIT 生成)处于打开状态,无重复或重叠工作。
Powered by deepseek-flash
|
本轮审查中的 body-only F-003 已在
文档中的本机绝对 TGOS 路径也已替换为 |
There was a problem hiding this comment.
审查结论:COMMENT(无阻塞问题)
本 PR 做了什么
- v5 设备协议(
DEVICE_PROTOCOL_VERSION = 5):装载器在同一网卡广播 MAC、架构、启动代次与设备 HTTP 端口,loader::serve_udp_discovery()把 v5 广播交给device::reconcile(),先反向GET /api/v1/status核对 MAC、代次、架构(LoaderRegistry::accept_announcement),再从板卡 TOML 按 MAC 查绑定;device::push_boot/push_file从会话存储重新核对长度与 SHA-256,经设备 HTTP 提交启动事务并请求交接,不再向 v5 装载器发送文件下载 URL。 - 独立于 Session 的持久 OTA 镜像库与任务(
ostool-server/src/ota.rs、api/ota.rs):镜像上传/枚举/删除、按板卡指派/查询/取消、旧 v4 下载与阶段上报、otaSSE 主题、BoardEditor 内的BoardOta面板。 httpboot-protocol0.3.0→0.4.0(PROTOCOL_VERSION3→4,新增PREVIOUS_PROTOCOL_VERSION = 3、DEVICE_PROTOCOL_VERSION = 5与 v5 消息类型)、文档重写与本地隔离脚本ostool-server/scripts/test-axloader-local.py。
影响面与隔离性
- v2/v3 旧路径未被破坏:
offer/accept_poll/accept_status只把可接受版本集合扩成 2|3|4;LoaderPollRequest.ota带serde(default)+skip_serializing_if,旧 JSON 仍可解析;v2 仍只在无initramfs/cmdline时接收启动。prune()新增的 v5 冲突分支只在current_registration_id不在registrations且记录仍在线时生效,v2-v4 冲突语义与基线一致;v5 重复 MAC 冲突会一直保持,直到记录中那个 IP 的实例离线超过 10 秒后由新地址接管,未发现状态泄漏。 - 有意的契约变化:0.4.0 的 v4 poll 必须携带
ota,否则得到ota_state_missing;LoaderPollResponse新增Update/ConfirmUpdate。仓库内只有 ostool-server 使用这些常量,ostoolCLI 不构造LoaderPollRequest,不受影响。 - 关键耦合:v4 poll 与 v5 reconcile 的启动下发现在都先经过 OTA 决策,
Decision::Wait会让 poll 回bound_idle、让 reconcile 直接结束本轮,OTA 状态机已成为既有启动路径的一部分(文档已说明“待试未确认时设备自身拒绝内核启动”的语义)。 - 锁序核对:
queue/decide/report/cancel统一 observations→jobs,delete_image为 jobs→image_io,queue_job为 boards→runtimes(与既有create_session_inner/sync_board_runtime_states相同),未发现锁序反转、持锁 await 或 std 锁跨 await;put_image的 PE 偏移有checked_add(94)边界,未发现越界读。
验证
- CI(head 678b227):两条
check (stable, x86_64-unknown-linux-gnu)均 completed/success、annotation 0,覆盖cargo fmt --all -- --check、clippy、build、cargo test、webui 单测/build/Playwright e2e 与cargo publish --workspace --dry-run --locked;没有由本 PR 引起的失败。 - 本地:本审查环境没有 Rust 工具链(
cargo/rustc均不存在)与 pnpm,无法复现 PR 描述的cargo fmt/clippy/test与前端命令,验证结论以 CI 为准。已执行:review_pr_helper.py prepare-review(HEAD/工作树校验通过)与git diff --unified=0的行级可评论性核对(用于下方内联评论)。 - 阅读范围:29 个变更文件全部核对,重点复核
ota.rs状态机与持久化、device.rs推送/确认/替换、api/ota.rs契约、loader.rs的 v5 分发与 prune、webui 的BoardOta/client/types/events。
既有意见
上一轮 Mai 审查(head c06bfcb,job 34d006fc)的 3 条意见已在 678b227 逐条修复并复核:push_boot() 现在读取起始状态中的旧 boot_id,首次 create 返回 409 时携带同一 X-Boot-Epoch 删除旧事务、要求 204 后只重试一次,假设备用例覆盖“旧清单替换”“上传摘要拒绝后重试”“删除后二次仍 409 时停止”;Content-Length: 0 返回 400 empty EFI image、仅超限返回 413,路由契约测试同时断言 code/message;新增镜像删除接口与页面入口,覆盖“非终态引用返回 409、终态引用可删且保留摘要/长度/版本”。更早一轮(job 2f97351a)的取消任意非终态任务、锚点与端点补齐、文件名清洗、README 中英一致意见同样已落实。作者回复与代码一致,无需继续跟踪。
未解决项(均 advisory,非阻塞)
- F-001
ostool-server/src/ota.rs:438:重复上报同一阶段(staged/succeeded/failed)被 409 拒绝,且与相邻“终态同阶段可再报”的判断自相矛盾 —— 见内联评论。 - F-002
ostool-server/src/device.rs:94:设备拒绝 OTA 上传时任务会永久停在downloading且从不写job.error;另 231-233 行要求旧事务删除必须返回 204,404 会让整轮推送放弃 —— 见内联评论。 - F-003
ostool-server/webui/src/pages/BoardOta.tsx:44:上传成功后未清空 file input,同一文件无法再次上传 —— 见内联评论。 - F-004(body-only)
docs/axloader-network-control.md:重写删除了“内建 QEMU 虚拟板”整节,[virtual_qemu]配置键(ovmf_code/tap_pool/network_namespace/bridge/memory_mib等)与虚拟板验收流程在仓库文档中已不可查,而该功能与代码本 PR 未改动(src/virtual_qemu.rs、/api/v1/admin/virtual-devices与virtual-lab子命令仍在,docs/api.md:489-491 仍宣传virtual-lab up/status/down),README 也不再指向虚拟板设计。建议补回该配置参考(或移入 docs/api.md 的virtual-lab小节旁),并保持中英一致。
残余风险(不阻塞)
- v5 广播、设备 HTTP 与
POST /api/v1/loaders/ota-status均无认证:能发广播或能取得registration_id的主机可让服务端对peer.ip():http_port发起状态查询与镜像推送,也可自行上报阶段与结果。与文档声明的“可信隔离实验网”一致,上线前需要网络隔离或后续认证。 - 0.4.0 的 v4 poll 必须携带
ota,对升级到 0.4.0 的其他消费方是隐性不兼容(缺字段即ota_state_missing,且不带retry_after_ms);TGOS 锁 0.3.0,暂不受影响。 - v5 设备侧(TGOS/axloader)请求类型目前两仓各自定义,一致性只由需要 TGOS 工作树 + OVMF + KVM + mtools 的手工脚本
ostool-server/scripts/test-axloader-local.py证明,CI 不执行;服务端侧已有 axum 假设备与路由契约测试覆盖。 - 管理端
ota主题在admin_topic失败时下发{"error": ...},而前端按LoaderOtaResources使用(ota?.jobs.find),与其它主题一样缺少防御性处理(既有模式,非本 PR 引入)。 - 相似 PR:仓库当前仅本 PR 与 #196(FIT 生成)处于打开状态,无重叠或重复工作。
Powered by deepseek-flash
There was a problem hiding this comment.
审查范围与结论
本 PR(head 097d373f,base 04170c34)为 axloader 引入协议 v5:设备通过 UDP 2998 单向广播自身 HTTP 入口,ostool-server 反向调用设备接口完成启动交接(POST /api/v1/boot/jobs + PUT …/{id}/kernel|initramfs + POST …/{id}/start)与 OTA 下发(PUT /api/v1/ota/image、POST /api/v1/ota/confirm),并新增独立于 Session 的持久 OTA 镜像库与任务(ostool-server/src/ota.rs、api/ota.rs、管理页 BoardOta.tsx)。旧 v2/v3/v4 的发现、poll、状态上报与下载 URL 路径保留;httpboot-protocol 新增 DEVICE_PROTOCOL_VERSION = 5 与 PREVIOUS_PROTOCOL_VERSION = 3,LoaderPollRequest 增加可选 ota(v4 缺失时返回 ota_state_missing)。最后一个提交把 v5 清单构造抽成 v5_boot_manifest,固定把兼容入口 httpboot_entry 转换为 __x86_64_efi_pe_entry(cmdline/initramfs 仍按可选字段原样保留),并让 BootPayloadConfig 序列化时省略未配置字段。
结论:未发现阻塞性问题;但有 3 项上一轮已提出、本 head 仍未处理的非阻塞问题,因此以 COMMENT 提交(详见下方内联评论)。
契约与影响面
- 新增管理端与设备端接口:
GET|POST /api/v1/admin/loader-images、DELETE …/{sha256}、GET|POST /api/v1/admin/boards/{board_id}/loader-updates、DELETE …/{update_id}、GET /api/v1/loader-updates/{update_id}/image、POST /api/v1/loaders/ota-status。docs/api.md已补齐成功/错误状态码契约(空镜像 400、超限 413、非终态引用 409、终态下载 404),与代码一致。 - 旧装载器兼容:
loader.rs/router.rs对 v2/v3/v4 的 Offer/poll/status 判定改为LEGACY | PREVIOUS | PROTOCOL_VERSION,v2/v3/v4 poll 响应仍返回原entry_symbol与可选字段(api/router.rs测试同时覆盖 v3 与 v2)。PROTOCOL_VERSION3→4 只影响本 PR 同时引入的 v4 OTA 语义,未发现对既有 v4 客户端的破坏(base 中不存在 v4 客户端)。 - 共享状态与并发:OTA 任务持久化在
jobs.json(状态)与devices.json(观测),镜像在images/,锁序统一为observations → jobs → image_io;delete_image为jobs → image_io,未发现锁序反转或死锁路径,生产路径无unwrap/panic(ota.rs两处unwrap均由前置get保证)。管理路由沿用既有notify_admin_changes层并额外invalidate(["ota"])。 - 前端新增
otaSSE 集合,BoardEditor仅在板卡绑定 MAC 后挂载BoardOta;sanitizeImageVersion与服务端is_ascii_graphic/96 字节校验一致。
CI 与本地验证
- CI:head
097d373f的两个check (stable, x86_64-unknown-linux-gnu)check-run 均为completed/success(06:39/06:40 UTC),覆盖cargo fmt --check、clippy、build、cargo test与 webuipnpm test/build。 - 本审查容器内没有 Rust 工具链(
cargo/rustc不存在)也没有pnpm,因此未能本地重跑 fmt/clippy/cargo test与前端测试;上述结论基于 CI 结果与静态阅读。 - 已执行:
prepare-review校验(head 与工作树一致、干净)、git rev-parse HEAD=097d373f…,并用脚本核对本 PR 涉及的跨文件文档锚点均可解析。
上一轮审查评论
上一轮(针对 678b2278)的 6 项评论中:取消任意非终态任务、docs/api.md 失效锚点与端点表、前端版本标签清洗已在 c06bfcb 修复;push_boot 的 409 旧事务替换、空 EFI 上传的 400/413 区分已在 678b227 修复,本轮复核确认落码且与文档一致。剩余 3 项(ota.rs 阶段上报、device.rs 拒绝处理、BoardOta.tsx 文件输入)在 097d373f 上仍成立且无回复,本轮继续以内联评论提出。
未解决的非阻塞问题
ostool-server/src/ota.rs:438— 同阶段重复上报被判invalid OTA phase transition(映射 409),而第 433-436 行的守卫显式允许终态同阶段重发,该分支不可达。ostool-server/src/device.rs:96— 设备拒绝 OTA 上传时错误只进log::warn!:任务停在Downloading、job.error为空、每次广播重复上传整份镜像;镜像库仅接受 AMD64 PE,而指派不校验设备架构,AArch64 板卡是必然失败组合。ostool-server/webui/src/pages/BoardOta.tsx:44— 非受控<input type="file">上传成功后未清空 value,再选同一文件不触发change,上传按钮持续禁用。
残余风险(不阻塞,建议后续单独跟踪)
- v5 发现与 HTTP 控制无认证、MAC 仅作绑定键:在设备离线超过
ONLINE_TTL的窗口内,同网段伪造广播可令服务器把 Session 内核/initramfs 与 OTA 镜像上传到伪造端点;文档已声明本阶段仅用于可信隔离实验网,实体网络上线前需单独验收(PR 描述亦如此声明)。 GET /api/v1/loader-updates/{update_id}/image为公开非管理端点,仅凭不可猜测的update_id即可下载镜像字节,未校验请求方 MAC。put_image在持有std::sync::Mutex时同步写至多 32 MiB 并fsync,delete_image在持有jobs异步锁时做阻塞文件操作,并发上传/删除期间会占用 tokio worker;建议改spawn_blocking或缩小锁范围。ostool-server/scripts/test-axloader-local.py需要本地 TGOS 产物与 OVMF/QEMU,本容器无法执行;该脚本与device.rs的 loopback 假设备测试仅能以 CI 与作者记录的手工验证为证据。
Powered by deepseek-flash
There was a problem hiding this comment.
审查结论:COMMENT(无阻塞问题)
本 PR 做了什么
- 引入 v5 设备协议(
DEVICE_PROTOCOL_VERSION = 5):装载器在同一 UEFI 网卡广播 MAC、架构、启动代次与设备 HTTP 端口,loader::serve_udp_discovery()把 v5 广播交给device::reconcile(),先反向GET /api/v1/status核对 MAC、启动代次与架构(LoaderRegistry::accept_announcement),再按板卡 TOML 的 MAC 绑定决策;device::push_boot/push_file从会话存储重新核对长度与 SHA-256 后把内核与可选 initramfs 推送到设备的启动事务接口并请求交接,不再向 v5 装载器下发文件下载 URL。 - 新增独立于 Session 的持久 OTA 镜像库与任务(
ostool-server/src/ota.rs、api/ota.rs):镜像上传/枚举/删除、按板卡指派/查询/取消、旧 v4 下载与阶段上报、otaSSE 主题、BoardEditor 内的 BoardOta 面板。httpboot-protocol0.3.0→0.4.0(PROTOCOL_VERSION3→4,新增PREVIOUS_PROTOCOL_VERSION = 3、DEVICE_PROTOCOL_VERSION与 v5 消息类型),LoaderPollRequest增加可选ota。 - 文档重写,并新增本地隔离脚本
ostool-server/scripts/test-axloader-local.py(临时目录中的真实 FAT 磁盘、OVMF VARS、hostfwd与广播帧转交)。
影响面
- v2/v3 旧路径未被破坏:
offer/accept_poll/accept_status只是把可接受版本集合扩成 2|3|4;LoaderPollRequest.ota带serde(default)+skip_serializing_if,旧 JSON 仍可解析;LoaderPollResponse是tag = "state"的内部标签枚举,新增Update/ConfirmUpdate不会影响旧装载器;prune()的 v5 冲突分支只在current_registration_id不在registrations且记录仍在线时生效,v2-v4 冲突语义与基线一致。ostoolCLI 不构造LoaderPollRequest,协议常量变更不影响它。 - 有意的契约变化(已文档化):0.4.0 的 v4 poll 必须携带
ota,否则得到ota_state_missing;TGOS 目前锁 0.3.0(版本 3),不受影响。 - 关键耦合:v4 poll 与 v5 reconcile 的启动下发都要先经过 OTA 决策,
Decision::Wait会直接跳过本轮启动推送(见 F-002)。 - 并发与边界核对:
queue/decide/report/cancel统一按 observations→jobs 取锁,queue_job为 boards→ota,delete_image为 jobs→image_io,未发现锁序反转、持 std 锁跨 await 或生产路径 panic/unwrap;put_image的 PE 解析用checked_add(94)限定边界,pe_offset+94覆盖了所有硬编码偏移,未发现越界读。
验证
- CI(当前 head
b5b3f2e):两条check (stable, x86_64-unknown-linux-gnu)均 completed/success、annotations 0(07:04–07:11 UTC),覆盖cargo fmt --all -- --check、clippy、build、cargo test、webuipnpm test/build、Playwright e2e 与cargo publish --workspace --dry-run --locked;没有由本 PR 引起的失败。补充一点上下文:工作流的 clippy 步骤是cargo clippy --target ... --all-features,没有-D warnings,所以 lint 回归不会让 CI 失败,PR 描述里的-D warnings只来自本地运行。 - 本地:本审查容器没有 Rust 工具链(
cargo/rustc不存在)也没有pnpm,未能重跑 fmt/clippy/test 与前端命令,验证结论以 CI 为准。已执行:review_pr_helper.py prepare-review(HEAD 与工作树校验通过,HEAD =b5b3f2e)、git diff --unified=0 origin/main...HEAD(逐条核对下方内联评论的行级可评论性)以及全部引用锚点的复核。
既有意见
上一轮(job 30c24a05,head 097d373)的 3 条意见已在 b5b3f2e 逐条落实,本轮复核确认:
report()同阶段重复上报被判 409 → 现在job.phase == phase时按幂等请求处理,只在错误文本变化时持久更新(ota.rs:437-446),新增duplicate_phase_reports_are_idempotent覆盖 downloading/staged/failed/succeeded。- 设备拒绝镜像只留 warn、任务长期停在
downloading→ 新增record_delivery_failure(ota.rs:481-511)持久记录设备错误与投递次数,连续 3 次进入Failed并停止重发,device.rs的假设备用例断言错误文本与计数;删除旧启动事务时把 404 视为“已不存在”(device.rs:246),用例覆盖替换成功、摘要拒绝后重试、二次仍 409 时停止。 - 上传成功后未清空 file input →
BoardOta.tsx通过 ref 同时清空 DOM value 与状态,BoardOta.test.tsx覆盖同一文件二次选择并上传。
更早两轮(job 92d29d28 / job 34d006fc、job 2f97351a)的其余意见(取消任意非终态任务并清理观察记录、docs/api.md 失效锚点与端点状态码表、EFI 文件名清洗、README 中英一致、Content-Length: 0 → 400 empty EFI image、镜像删除接口与终态审计字段)同样已在 c06bfcb/678b227 落码。作者回复与代码一致,无需继续跟踪。
本轮提出的非阻塞问题(均为 advisory)
- F-001(内联
ostool-server/src/api/ota.rs:119):指派架构门槛把boot_arch省略的合法 x86_64 HTTP Boot 板卡一并拒绝,消息也会误导。 - F-002(内联
ostool-server/src/ota.rs:383):待试确认以board_idle为前提,与“待试槽未确认前设备拒绝启动内核”叠加,会让随后启动的 Session 一直等不到启动推送。 - F-003(body-only,
docs/axloader-network-control.md+docs/api.md):重写删除了“内建 QEMU 虚拟板”整节与[virtual_qemu]配置参考,而虚拟板代码与virtual-lab子命令仍在,文档不再可查。 - F-004(内联
ostool-server/src/ota.rs:207):镜像库的阻塞文件 I/O(≤32 MiB 写盘 + fsync、read_dir/read、remove_file+ fsync)直接跑在 async handler 内。
残余风险(不阻塞,建议后续单独跟踪)
- v5 广播、设备 HTTP 与
POST /api/v1/loaders/ota-status均无认证,MAC 仅作绑定键:能发 UDP 广播的主机可让服务端对peer.ip():http_port发起状态查询与镜像/内核推送,也能自行上报阶段与结果。与文档声明的“可信隔离实验网”一致,实体网络上线前需要网络隔离或后续认证。 device::push_boot/push_file用硬编码前缀/boot/sessions/{session_id}/从公开 URL 反解会话内相对路径;http_boot.public_base_url若带路径前缀(append_url_path会保留既有路径段)会导致strip_prefix失败,v5 启动推送每轮只留一条 warn。默认配置(public_base_url为空)不受影响。- 设备状态缺少
ota(无 OTA 持久区)时,每轮广播都会写一条log::warn!,长期运行可能刷日志,建议降级为限频日志。 - 端到端证据仍依赖
ostool-server/scripts/test-axloader-local.py(需 TGOS 工作树 + OVMF + KVM + mtools),CI 不执行;服务端侧已有 loopback 假设备与路由契约测试覆盖。 - 相似 PR:本 PR 之外仓库只有 #196(FIT 生成)处于打开状态,无重叠或重复工作。
Powered by deepseek-flash
|
已在 3621fe8 处理本轮 body-only F-003:恢复 |
There was a problem hiding this comment.
变更概述
PR #198 在保留 axloader v2/v3/v4(UDP Offer + /api/v1/loaders/poll + 状态上报 + 下载 URL)的前提下,新增 v5「设备自持 HTTP 入口」控制路径:
httpboot-protocol0.3.0 → 0.4.0:PROTOCOL_VERSION = 4、新增DEVICE_PROTOCOL_VERSION = 5与PREVIOUS_PROTOCOL_VERSION = 3,以及LoaderAnnouncement/LoaderDeviceStatus/DeviceBootJob/LoaderOtaState等类型。ostool-server:新增device::reconcile()(广播 → 真实 HTTPGET /api/v1/status核对 MAC/代次/架构 → 按板卡 TOML 的 MAC 绑定 → 反向推送启动事务或 OTA)、ota::OtaStore(镜像库 + 持久升级任务 + 观察记录)、api/ota.rs(管理端镜像/任务接口、v4loader-updates/{id}/image、loaders/ota-status)与otaSSE 主题;管理页新增BoardOta面板与镜像版本清洗。ostool:BootPayloadConfig的cmdline/initramfs增加skip_serializing_if,未配置字段不再序列化。
既有契约与耦合面
- 兼容性:v2/v3 装载器仍被
offer/accept_poll/accept_status接受(PREVIOUS_PROTOCOL_VERSION已加入白名单);v4 poll 新增「必须携带ota状态」的要求(缺失返回ota_state_missing),LoaderPollRequest.ota带 serde default + skip_serializing_if,JSON 契约向后兼容;LoaderPollResponse新增的Update/ConfirmUpdate只会发给 v4 装载器。仓库内 5 处LoaderPollRequest构造点(含测试)均已补齐ota字段。 - Session/租约:v5 推送复用现有 Session、板卡租约与 loader 状态;串口/WebSocket 生命周期未改动。新增 OTA 持久状态独立于 Session(
loader-ota/jobs.json、devices.json、images/),服务端重启后可恢复任务。 - 锁序:
OtaStore统一为 image catalog → observations → jobs(queue/delete_image/decide/report/cancel一致),未发现锁序反转;镜像写入、摘要校验、删除与目录 fsync 已移入spawn_blocking,且删除前释放jobs锁。 - SSE/前端:新增
ota主题,admin_topic("ota")只读内存缓存;events.ts/types/api.ts/docs/admin-ui.md集合键已同步。
CI 与本地验证
- head
3621fe8上两个 check run(check (stable, x86_64-unknown-linux-gnu))均为 success(2026-09-29T07:53Z 完成)。该 workflow 覆盖cargo fmt --all -- --check、clippy、build、cargo test、pnpm test、pnpm build、Playwright e2e 与cargo publish --workspace --dry-run --locked,即本 PR 的 Rust 与 web UI 变更均已通过 CI 验证。 - 本次 review 环境缺少 cargo/rustc 与 pnpm(
cargo: not found、pnpm: not found,仅有 Node 24 且无法联网安装依赖),因此本地未能复现 fmt/clippy/test;未发现由本 PR 引起的 CI 失败。 - PR 作者声明的隔离联调(
ostool-server/scripts/test-axloader-local.py:真实 QEMU + 真实 ostool-server + 真实 FAT 磁盘)需要 KVM、OVMF 与 TGOS 产物,本环境无法复现,属未独立验证部分。
既往评审意见核对
此前 19 条 review comment 中的可执行意见已在 head 上逐条核对,均已在 b5b3f2e / 3621fe8 解决:任意非终态任务可取消并清除观察记录;boot_arch 省略按 x86_64 处理且显式非 x86_64 仍拒绝,管理页仅显示受支持面板;匹配的 server 待试槽在活动 Session 下也可确认;镜像 I/O 移入 spawn_blocking、元数据启动时缓存、统一锁序;同阶段回报幂等;Content-Length: 0 返回 400、超 32 MiB 返回 413;删除旧启动事务 404 视为已消失;v5 投递失败计数 3 次后转 failed;webui 同一文件可重复上传。这些意见技术上均合理,当前 head 已不再存在对应问题。
遗留问题与测试缺口
无阻塞问题。本次提出 3 条 advisory(见行内评论):活动 Session 内按广播重复整批重推启动事务并回退会话 loader 状态(device.rs:162);未做 MAC/绑定校验即按广播来源与 http_port 发起 HTTP 调用(device.rs:33);jobs.json/devices.json 解析失败会阻止服务端启动(ota.rs:101)。另有两点残余风险/测试缺口:v5 广播在真实二层网络的可达性与反向路由(PR 已声明为上线前单独验收),以及 reconcile 任务异常结束时 in_progress MAC 集合的释放(当前仅在任务正常返回时移除,最长受 300 s HTTP 超时约束)。
同 PR 检索:repo:drivercraft/ostool type:pr axloader OTA 与 repo:drivercraft/ostool type:pr loader-images 均只命中本 PR #198,未发现重叠或重复 PR。
结论:CI 通过、协议与持久状态契约向后兼容、既往评审项已闭环,仅剩上述非阻塞加固建议,可以合并。
Powered by deepseek-flash
| peer.ip() | ||
| ); | ||
| } | ||
| if runtime.lease_state != BoardLeaseState::Using { |
There was a problem hiding this comment.
板卡处于 Using 且已发布启动命令时,每次收到 v5 广播都会重推整套启动事务:本函数只检查 runtime.lease_state 与 session.boot_command(),完全没有使用起始 GET /api/v1/status 已经拿到的 observed.boot(boot_id/phase/kernel_received)。设备在接收过程中会继续广播(docs/axloader-network-control.md §2.1 已说明),而 loader::serve_udp_discovery 只在单次 reconcile 进行中对同一 MAC 去重,因此上一次上传一结束就会开始下一次完整上传:重复 POST /api/v1/boot/jobs + PUT kernel + PUT initramfs + start,内核与归档各可达 256 MiB(MAX_HTTP_BOOT_INITRAMFS_BYTES),单次多余流量约 512 MiB。同时 push_boot() 在上传前调用 session.update_loader_status(..., LoaderStatusPhase::Accepted),而 SessionState::update_loader_status 只校验 boot_id 并直接覆盖 loader.status,同一 boot_id 的会话状态会从 ready_to_handoff 回退到 accepted,GET /api/v1/sessions/{id}/loader-status 的观察者与 ostool HTTP Boot runner 日志都会看到这次回退(runner 只在 failed 时结束,因此只是噪声而非失败)。建议在推送前判断设备是否已持有本次事务:observed.boot.boot_id == command.boot_id 且阶段已完成文件接收/交接(如 ready_to_handoff)时直接跳过,只在启动代次变化(复位)、设备无事务或上报失败阶段时重推,并在假设备测试中覆盖“同一代次连续两次广播只上传一次文件”。
| "unsupported device protocol" | ||
| ); | ||
| ensure!(announcement.http_port > 0, "missing device HTTP port"); | ||
| let device_addr = SocketAddr::new(peer.ip(), announcement.http_port); |
There was a problem hiding this comment.
这里用 peer.ip() 与广播自带的 http_port 拼出设备地址后,立刻对任意来源执行 GET {endpoint}/api/v1/status,而 MAC/板卡绑定校验(board_id_for_mac,第 47-62 行)发生在其后,协议本身也没有认证(docs/axloader-network-control.md §1.1 已写明限定可信隔离实验网)。因此同一二层网络中任意主机(UDP 源地址可伪造、http_port 完全可控)都能让 ostool-server 向任意 IP:端口发起请求并解析响应;若再伪造出已绑定板卡的 MAC,服务端还会把内核/initramfs/EFI 镜像 POST/PUT 到该地址,等于一个未认证的盲 SSRF 通道与镜像外发路径。建议在发起任何 HTTP 调用之前先按 announcement.mac_address 找到未禁用且 boot.kind == httpboot 的绑定板卡,无绑定则直接返回;也可考虑把设备端口固定为协议常量 2999,如确实需要非默认端口(本地脚本会把它改写成 hostfwd 端口)则用显式配置放行。若这是有意保留的行为,请在文档中明确写出该信任边界。
| pub fn open(data_dir: &Path) -> anyhow::Result<Self> { | ||
| let root = data_dir.join("loader-ota"); | ||
| fs::create_dir_all(root.join("images"))?; | ||
| let jobs = match fs::read(root.join("jobs.json")) { |
There was a problem hiding this comment.
jobs.json 或 devices.json 解析失败时 OtaStore::open 直接返回错误,而 build_app_state 里的 OtaStore::open(&config.data_dir)?(ostool-server/src/state.rs)会让整个服务端启动失败。这与同一文件里镜像元数据的容错处理不一致(read_images 只 log::warn! 并跳过不一致条目,PR 说明也写了“异常元数据隔离”),也与板卡配置的既有做法不一致(不兼容板卡 TOML 会被移入 quarantine/ 后继续启动,见 board_store/fs.rs)。结果是运维手工编辑或磁盘损坏导致的一个状态文件异常,会让串口、板卡管理和 SSE 管理台全部无法启动,只能人工删文件恢复。建议与镜像/板卡保持一致:解析失败时记录告警并把该文件(或整个 loader-ota 目录)重命名隔离,然后以空任务表继续启动;并补一个“启动时 jobs.json 已损坏仍能启动”的单元测试。
问题
旧版 axloader 依赖发现、轮询和下载:直连控制与 ostool-server 指派走不同路径,设备无法在没有服务端时用同一套接口完成内核启动。已有 OTA 镜像库和持久任务还需要接入设备拥有 HTTP 入口的协议,同时保持旧装载器可用。
改动与依据
BootPayloadConfig { cmdline, initramfs }。两个字段独立可选,序列化时省略未配置字段,空 cmdline 保持兼容。v5 清单完整保留 cmdline 和 initramfs 元数据,并把兼容入口httpboot_entry转换为__x86_64_efi_pe_entry;v2/v3/v4 继续收到原入口和字段。hostfwd和广播帧转交。Session 使用真实 ArceOS EFI 内核、可选 cmdline 和 initramfs,服务端必须真正调用 QEMU 客户机 HTTP 接口。审查反馈修复
SocketAddr生成,正确处理 IPv6 地址。boot_id时,服务端在创建返回409后删除状态中观察到的旧事务并只重试一次;删除返回404也视为事务已经消失。假设备测试覆盖替换成功、上传摘要拒绝后的重试,以及第二次创建仍冲突时停止。downloading、staged、failed、succeeded同阶段回报可幂等重试;终态重复回报不再依赖服务进程内观察记录。failed并停止重发;管理指派只接受配置为 x86_64 UEFI HTTP 的板卡,避免把 AMD64 PE 指派给 AArch64。400 empty EFI image,仅超过 32 MiB 返回413,契约测试同时核对错误码和消息。409,终态任务保留摘要、长度和版本审计字段。上传、指派与删除按统一锁序串行,删除可重试清理中断后遗留的 EFI 文件;终态任务的旧 v4 下载返回404。boot_arch省略时与现有客户端一致按 x86_64 处理;AArch64 和非 HTTP Boot 板卡不再显示 OTA 面板,后端仍显式拒绝非 x86_64 指派。spawn_blocking,删除检查在阻塞段前释放任务锁;元数据启动时载入缓存并只在增删后更新,SSE 快照不再逐次扫描目录。异常元数据不会阻止服务启动,部分删除失败也不会留下可继续指派的缓存条目。[virtual_qemu]字段、网络前提、绑定约束和六步验收流程,并在 API 文档与中英文 README 恢复入口。验证
当前精确提交
3621fe8a83f372c28ef9764edeba6821ab11c27b:cargo fmt --all -- --checkcargo clippy --target x86_64-unknown-linux-gnu --all-features -- -D warningscargo build --target x86_64-unknown-linux-gnu --all-featurescargo test --target x86_64-unknown-linux-gnu -- --nocapture:workspace 全部通过,ostool-server 164 项通过。pnpm --dir ostool-server/webui test:26 项通过。pnpm --dir ostool-server/webui build:通过。python3 ostool-server/scripts/test-axloader-local.py --tgos /path/to/tgoskits-dev --server-bin target/x86_64-unknown-linux-gnu/debug/ostool-server:真实服务端发现 QEMU 装载器并推送真实内核、cmdline 与 initramfs,内核输出三个预期 marker;服务端重启后恢复 OTA 任务、确认新槽,再次启动仍使用该槽。结果记录位于本地隔离目录/tmp/axloader-v5-local-k7a1_zy3/result.json。本 PR 只更新代码与本地测试,不部署到 runner,也不刷写实体设备。接口当前仅用于可信隔离实验网;实体网络的广播可达性与反向路由仍需上线前验证。对应 TGOS 固件改动见 rcore-os/tgoskits#2535。