fix(desktop): atomically swap rollback bundles - #5651
huangruiteng merged 1 commit into
Conversation
|
Hi @Duang777, the DCO If the log confirms a missing |
eaab7f2 to
974b22d
Compare
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: model_agent; gpt-6.1-sol; OpenAI; runtime_reported; reasoning_effort=xhigh
动机
APPROVE — 未发现当前 head 的阻塞问题。 需要在 macOS 上恢复上一版本的桌面 App 用户。 以前回退要先移走当前 App,再移入备份,进程在两步之间被杀死会留下空安装路径;现在一次交换提交后,安装路径仍有可运行的备份,旧 App 保存在临时恢复目录。 独立强杀对照在固定 base 复现安装路径消失,在当前 head 验证替换 App 签名有效、旧 App 与恢复记录保留;损坏备份和恢复记录失败仍拒绝覆盖。 本次只修 macOS 回退的进程终止窗口,不覆盖断电持久性、自动更新插件自身的替换流程、备份轮换、Windows 行为或实际发布安装升级。
改动思路
head 974b22d461ce0aa5bd2c54747cb9619ec5345081,固定 merge base 053a8c246d16f0bbf88491905038295a5c0843e1。原 error compensation 只能处理第二次 rename 返回错误,进程被杀时根本没有机会恢复。更小的修复是在现有 replace_bundle owner 用 macOS 原生交换完成唯一提交;继续添加补偿或另造多阶段恢复状态,都不能比这次有界替换更直接。
生产调用仍是既有固定 rollback action → 当前执行文件确定 App → 校验备份副本 → 记录既有 pending journal → 替换 → 既有 restart/continuation。临时目录建在安装路径旁边,同卷。交换后旧 App 已安全位于 candidate,随后 candidate→failed 只是命名:即使进程此时死亡或命名失败,安装路径仍有替换 App,原始 App 没有被删掉。没有新设置、权限、CLI 参数、Goal 状态或第二控制面 owner。
具体改动
spec_ref:apps/desktop/loopx-control-plane/README.md;spec_revision:053a8c246d16f0bbf88491905038295a5c0843e1。已先读原文:Updates And Recovery 的签名备份/既有回退/Goal 数据边界、Runtime Model 的 native App owner、Validation 的原生 cargo/build 验证,在本次有界修复均 implemented。强杀连续性另外由独立复现的实际缺陷建立,未把作者声明当成已验收规范。
关键代码讲解
update_backup.rs:204 restore_verified_backup仍从可信当前执行路径定位 target;真实 ditto 复制和 codesign 校验先于 journal 与 commit。staging 在交换前保留,失败保留源与目标的责任没有转给新状态机制。:227 replace_bundle在 macOS 用rustix::fs::renameat_with(... EXCHANGE),失败返回既有 rollback_failed,两个路径都未先移空;不回退到有同样死亡窗口的两次 rename。非 macOS 原代码保留,rollback availability 仍只支持 macOS。:372 bundle_swap_keeps_install_path_after_process_kill启动、等待、force-kill 并 reap 真正的子进程,再检查路径。正常保留原 App 与 candidate 消失失败场景也覆盖;旧 second-rename failure 测试重命名为 atomic exchange failure。
完整 diff 为3文件 +134/-14,大部分 Rust 增量是 cfg(test) 强杀/正常回归覆盖;Cargo 只把已有锁定的 rustix 加成 macOS direct dependency,没有版本升级或框架。OS 效果归现有 Rust App,通用 TS/Goal authority 不受改动。#5647 的备份轮换和 updater 插件自动安装是其他事务,不能把此修复泛化过去。
对主干的风险
我在独立本地 source copy 加同一组3项 oracle,只在第一次原生文件操作返回后插入 test-only pause,不改替换逻辑,编译整个现有 crate。调用实际 restore_verified_backup,使用真实 macOS ditto、ad-hoc codesign、文件交换和 SIGKILL;恢复记录 hook 写真实文件。固定 base 2通过/1失败,失败为明确的 installed-path continuity oracle:进程死亡后 target 不存在。当前 head 3通过:target 是签名有效的替换、旧 App 在 preserved staging、恢复记录和备份源仍在。初次 copy harness 缺 repository-relative include 路径,已恢复同一源码布局后才得到有效结果;未用编译失败充当回归。
其余独立路径:损坏 sealed resource 先拒绝,journal hook 失败先拒绝,之后授权恢复可继续;连续两次恢复保持已记录结果与签名;同目录 peer App 和后建 future App 保持不变。原生 full crate 还覆盖 missing candidate、damaged target、安全重启和固定 action 规则。它们没有真实替换本人安装的 App;这不证明完整 owner AppHandle journal adoption 或 packaged UI 操作已验收。
本轮 cargo test --locked 当前完整 base83/head85通过,两项需实际 installed signed App 的测试和一项 released signed archive 测试仍 ignored;这次决定性签名/复制/交换由独立真实 ad-hoc signed fixture 另外验证。head clippy、两端 cargo fmt、changed rustfmt、12项 Python Desktop 测试、DCO/diff 检查通过。完整 runtime snapshot 在两端按 exact source 准备,包含原前端 build;未声称实际安装/UI 运行。
初次 head 大套件有 84通过/1失败,是未改服务 fixture5秒内未达 Matching;保留这个观察及未证实根因。后续完整 B/H 和单独同源服务测试通过;服务源/依赖和 socket 路径未由此 PR 改动,且新增 filesystem 不调用 socket,因此已有当前证据充分覆盖本 PR 改动,未称其修好了服务间歇问题,也未只展示一次绿色结果。初次 base 缺 runtime 资源的 build 失败也保留,按原生 snapshot 恢复环境后重跑。按配置不查询/等待/轮询 CI。
语义 triage 覆盖完整 diff;这里没有新增 shared enum/state vocabulary,复用已有 native owner。advisory 首次 cwd 错误已保留,改到 root 后得到0 supported candidates;工具不支持 Rust,0不等于证明无语义。未调整任何预算、扫描根或必需断言。Future-facing pass 已应用:删掉 macOS 脆弱补偿路径,复用一个原生效果边界;没有必要再加恢复 schema 或无用抽象。
我的整体评价
这次 named defect 的有界 goal_achieved,long_horizon 和 user_experience 都 improved:它减少回退死亡后需要人工救安装路径的情况,已有操作和授权步骤保持。正向判断有同输入、固定源码、实际签名后端和强杀前后对照支持;频率、常规耗时、token、长期吞吐、断电耐久性都没有实测,不能宣称普遍提速。
兼容性保留既有 pending journal/backup 数据和非 macOS 分支;没新增 request decoder。风险主要是 scoped macOS 文件系统效果和未测的真实发布/installed owner journey,不是 quota/agent 权限。当前无阻塞发现,历史服务间歇诊断独立保留;这份 APPROVE 不授予 runtime/control-plane 自合并权限。
English verdict: APPROVE — 974b22d461ce0aa5bd2c54747cb9619ec5345081 removes the reproduced macOS rollback process-death gap in the existing owner. An independent actual signed-copy/journal/restore probe fails install continuity at immutable base and passes all3cases at head, using real ditto/codesign/filesystem and SIGKILL. Current full cargo B83/H85, head clippy, both fmt and12Desktop tests pass; initial unrelated service timeout and preparation failures are preserved. No power-loss, live installed UI/journal adoption, normal-path speedup or merge authority is claimed.
The previous two-rename replacement left the installed App path empty when the process died before the second rename, bypassing in-process compensation. Use macOS RENAME_SWAP so the install path stays occupied, and cover the original crash window with a child-process kill regression. Signed-off-by: Duang777 <[email protected]>
974b22d to
90b1be0
Compare
Goal And Delivered Outcome
Outcome basis / optional anchor: Reproduced process-death window in the macOS rollback bundle replacement. Related to the adjacent backup-rotation repair in fix(desktop): recover interrupted backup rotations #5647, but independently reviewable.
Goal/source and gap:
replace_bundle()renamed the installed App away before moving the verified rollback candidate into its path. The error branch restored the original, but process termination between the two calls bypassed that compensation and left the install path absent.Observable before → after, with the validation row that proves it: before the fix, a child-process kill after the first filesystem operation left
installed.appmissing; after the fix, the same kill point leaves the verified replacement at the install path and the displaced App in preserved staging.Issue/task and intended base: Self-contained reproduced defect; base
main.Author Declaration
Implemented against
replace_bundlebundle_swap_keeps_install_path_after_process_killreplace_bundlerestore_verified_backup_keeps_target_when_atomic_exchange_failsreplace_bundlesuccessful_replacement_preserves_the_originalRENAME_SWAPon macOS throughrustix; unsupported exchange fails before mutating either path. The existing signature verification still runs before the swap.Scope And Continuation
Validation
122682d560dd980721b45d951d79af0b70b46a19regression_paritypassedintegrationpassedunitpassedcargo test --locked: 82 passed, 2 ignored; signed-archive integration target: 1 ignored.unitpassedpython3 -m unittest discover -s tests/desktop -p 'test_*.py': 12 passed.staticpassedcargo clippy --all-targets --locked -- -D warnings.staticpassedstaticpassedloopx canary premerge --from-git-diff --git-diff-base upstream/main: 4/4 selected canaries passed for three changed files.staticfailedcargo fmt --all -- --checkreports existing formatting drift in unchangedmaintenance.rs; the changed Rust file passes its direct rustfmt check.integrationnot_runSee validation disclosure guidance.
Frontend / Visual Evidence
Type of Change
LoopX Area
Technical Direction
Shared-authority RFC fixture impact
N/A
Boundary Checklist
.loopx/,.codex/goals/, and liveACTIVE_GOAL_STATE.md).none.Signed-off-bytrailer (git commit -s).