Repository navigation
fix(upgrade): wait for previous App exit before relaunch - #8067
Conversation
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: model_agent; OpenAI Codex; OpenAI; self_reported(host execution attribution unavailable,未声称精确模型构建或人工独立评审)。
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
动机 / Motivation
APPROVE,未发现此 diff 的阻断项。通过正常 upgrade 更新并重启已安装 LoopX App 的用户。旧版正常 Quit 返回后原进程仍存活,新实例提前启动后丢失升级请求;新版等待目标 App 正常退出后才启动,退出超时就不启动替代进程。真实隔离 macOS AppKit 的独立旧 PID 断言证明等待与超时行为;25 项升级检查通过,未把 fixture 当整机升级完成。本 PR 不修改签名、降级、模型、原业务或消息消费责任,不强杀生产进程,也不生成假的原生回执。真实已安装 App 的正常升级与原生成功回执尚待部署独验;父级管家稳定性与历史消息未整体完成。
这不是从代码猜测的等待需求:正常升级的系统日志已显示新实例先结束、旧实例随后退出;同一真实 AppKit 异步退出条件在不可变基线中再次触发“旧 PID 仍活着就启动替代进程”的独立断言。原有 subprocess mock 会抹掉这个时间窗口,因此只靠原单测全绿不足以证明安全。
改动思路 / Approach
在原 Quit 与原 LaunchServices 启动之间等待正常生命周期完成,是最小责任层修复;不需要第二个进程管理器或持久状态。当前边界只增加已有 Quit 操作内的有界退出等待,启动参数、原生 updater、runtime pin、成功与失败回执判定均保留。
仍由原 AppleScript 向预选 App path 发出正常 Quit,等待放在相同的 subprocess timeout 内,每次观察当前应用是否还在运行。原进程未退出就不执行 open;超时将异常交回既有升级失败处理。没有增加 PID 数据库、轮询 daemon、强杀或另一个安装 owner,也没有依靠固定 sleep 宣称 App 已关闭。原 request/channel 和 runtime pin 继续传给同一个 LaunchServices 启动,随后仍由 native updater 核验签名及成功回执。
具体改动 / Changes
关键代码讲解
_restart_app将原来的 Quit→立即 open 改成 Quit→实际退出→原 open。退出等待复用既有min(timeout_seconds, 30)边界,路径仍通过 argv 传入 AppleScript,原LOOPX_BINpin 与原生升级参数保持,没有字符串插入路径或更改源选择。_upgrade_desktop仍是调用者与回执消费者,未把退出完成升级为成功。它继续要求匹配 request/channel、App 版本、bundled revision、真实活跃 PID 和 runtime identity;没有收到原生终态就保持失败/timeout。此处复核用于确认较小的时序修复没有绕过升级责任层。
产品 +6/-2 行,既有测试模块 +80 行。新增测试显式启用真实隔离 AppKit 进程,其 terminateLater 使 Quit 与退出分开;replacement launcher 只由独立 PID oracle 接管,避免开启第二个应用。成功与超时两臂都运行,没有 mock 正常 Quit 或进程退出。没有单独规定此时序的已接受书面规范,本 PR 自包含缺陷请求为本次修复依据;正常退出、超时不启动、原 pin/receipt 规则三项均有当前实现和检查对应。
对主干的风险 / Risks
主要风险是等待让正常升级挂住,或提前把原 App 杀掉。当前实现保留原超时并只走正常 Quit,超时测试证明 replacement launcher 完全未被调用;退出成功测试在实际 OS PID 已消失后才允许 launch。不可变基线 6a92640346051b79d08fb369354180b613c8fa46 加载同一原生 fixture 时失败,候选 native-enabled 整套 25 项通过。原 23 项 portable 升级、runtime pin、缺失/歧义 App、deferred service 和 native receipt 判定在基线通过,候选也覆盖并通过,输入和持久契约没有迁移。
标准 premerge 的 6 项直接检查及 7 项 catalog 检查通过,包括 install-local、packaged install 和 update smoke。另有一项 CLI output budget FAIL:基线和候选同一命令都缺 interaction_contract、scheduler_hint、selected_todo。已在未修改的 6a92640346051b79d08fb369354180b613c8fa46 独立复现相同失败身份与细节,原生 Quit helper 不参与这条只读控制面输出;未删除断言、放宽预算、选择性重跑成绿色或声称标准 gate 通过。根据当前 review policy,以独立的受影响安装/生命周期检查建立此 diff 的必要验证,保留该既有失败单列继续处理。Ruff、diff hygiene、RFC bilingual 和两文件公开扫描通过。此 Goal 不等待 CI,未查询/轮询 CI。
我的整体评价 / Verdict
在原 Quit 与原 LaunchServices 启动之间等待正常生命周期完成,是最小责任层修复;不需要第二个进程管理器或持久状态。当前边界只增加已有 Quit 操作内的有界退出等待,启动参数、原生 updater、runtime pin、成功与失败回执判定均保留。long_horizon 与 user_experience 均为 improved:原升级请求不会因异步旧实例抢占而无效等待,不增加用户授权或操作回合,超时仍明确失败。已检查 support_control_upgrade.py 与现有桌面 updater 调用路径,没有渲染、文案或交互结构变更;这是同一正常生命周期的时序修复。未验证范围:真实安装后的 native updater/渲染未由本次隔离 fixture 证明;本 PR 没有展示层变化。
保留现有兼容面、原生签名和正常回执判断是合理的,无新增版本分支或状态;生产改动与已证实问题相称。语义检查停在局部 process IO,未创建共享 Core vocabulary。认可这个可回滚增量;合并后必须通过正常 owner 做实际 native 升级/readback,原 CLI budget 失败和父级其余工作继续保留。
English verdict: APPROVE — head c69b4f97d67dbcb9ec6378738012977a2f3f7aa9 waits for actual native App termination before relaunch and preserves timeout/pin/receipt ownership. Native base/head regression, 25 upgrade tests and affected installation checks pass; the independently reproduced pre-existing CLI output failure remains disclosed.
Goal And Delivered Outcome
正常
loopx upgrade的 App 重启存在竞态:正常 Quit 返回后,旧 App 仍可能存活。此时立即启动的新进程会被单实例机制结束,旧进程随后退出,原升级请求得不到原生回执,App 也未保留运行。本变更在同一个正常 Quit 操作内等待目标 App 实际退出,再执行原 LaunchServices 启动。保持已有超时;退出未完成时不启动替代进程,也不伪造升级成功。
main,6a92640346051b79d08fb369354180b613c8fa46。Author Declaration
_restart_app_restart_app_upgrade_desktopScope And Continuation
仅改变正常 Quit 与原启动之间的时序,仍通过相同路径/argv 选择 App,保留正常退出和既有 native updater。没有 force kill 生产 App、新消费者、权限、receipt schema、签名或降级规则。实际已安装 App 的升级/readback 在正常部署阶段独立验证,fixture 不是整机升级成功证明。
Validation
c69b4f97d67dbcb9ec6378738012977a2f3f7aa9。LOOPX_TEST_NATIVE_APP_RESTART=1 python -m pytest -q tests/test_self_upgrade.py:25 项通过,包含 macOS AppKit 的实际异步退出与超时两种情况CLI output budget 的同一命令在不可变基线
6a92640346051b79d08fb369354180b613c8fa46和候选均失败:interaction_contract、scheduler_hint、selected_todo缺失。此 PR 的正常 App 退出 helper 不参与这些只读控制面输出,相关文件/调用链未变化。保留原失败;本次变更的必要安装验证(install-local、packaged-install、update smoke)、原生进程退出/超时及全部升级单测独立通过,没有修改预算或删断言。原生测试显式启用,仅创建独立 AppKit fixture,不使用 LoopX 模型、账户或服务。真实 Quit 与进程退出未 mock;replacement launcher 由只读 oracle 接管以检查旧 PID,并避免启动第二个真实应用。未启用此 macOS 测试时明确 skip,不冒称覆盖。
Frontend / Visual Evidence
UI impact: none;未改变渲染、文案或操作流程。此变更处理同一原生生命周期内的时序,故 first viewport N/A。
Type / Area
Shared-authority RFC fixture impact
N/A;无 Core policy、存储路由、授权或 receipt schema 变化。
Boundary Checklist