Skip to content

fix(updater): keep a downloaded update across checks and restarts - #2949

Merged
openai0229 merged 3 commits into
mainfrom
feat/desktop-update-resume
Sep 21, 2026
Merged

openai0229 merged 3 commits into
mainfrom
feat/desktop-update-resume

Conversation

@openai0229

@openai0229 openai0229 commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

问题

下载完成后更新装不上:每次 appCheckUpdate() 都会开启新的审计操作并把 preparedTransaction / preparedManifest 清空,刚下载并暂存的包当场被遗忘,点"重启安装"时后端已经没有可交给 helper 的更新;前端同样在每次检查响应里无条件覆盖状态。

具体缺陷(本次一次性修掉):

编号 缺陷 影响
F3 检查更新清空已下载/进行中的更新(后端 beginAuditOperation,前端 action.ts) 下载白做,装不上
F4 triggerDownload 不幂等 重复点击重复下载同一个包
F5 前端无 loading / 无重入保护 / toast 重复 / Updated 被当成"没有新版本" 连点 6 次 = 6 次串行检查 + 6 条提示
F6 检查响应覆盖进行中状态 下载/安装进度被抹掉
F8 没有缓存复用、没有暂存复用、没有持久化 prepared 状态 重启后需重新下载,离线无法安装

改动

后端

  • 检查前先复用 prepared 更新:磁盘上记住的签名 manifest 用内置公钥重新验签,缓存包按 manifest 的 size + SHA-256 校验,暂存目录重新校验后才提供安装(DISCOVERY/PREPARED_UPDATE_RESTORED),完全跳过 discovery,全程不需要网络。
  • release epoch 不再前进的记录视为"已用掉",静默清理(PREPARED_UPDATE_CONSUMED);验签失败或包被改动则丢弃并重新下载(PREPARED_UPDATE_DISCARDED)。
  • 新增 prepared-update.json 持久化(PreparedUpdateStore,原子写、损坏即视为无);暂存目录用 .source-sha256 标记来源包,重启后不重复解包。
  • triggerDownload 幂等:已 prepared 时直接返回成功并补发一次完成进度;检查失败不再丢弃已发现的更新;缓存命中记 DOWNLOADING/CACHE_HIT,缓存不匹配才删除重下。
  • 检查结果新增第三态 READY_TO_INSTALL(UpdatedStatus.Updated),客户端据此显示"重启安装"而不是"没有可用新版本"。

前端

  • 多个触发共享同一个进行中的检查(点 6 次只发 1 次请求),按钮显示 loading,检查期间忽略第二次点击,每次检查只弹一条提示。
  • 检查响应不再覆盖进行中/已完成状态:只有"新发现的更高版本"能替换已下载待安装的状态。

文档:script/package/README-updates.md 增加下载/暂存/恢复说明,并修正 LaunchAgent 标签为 per-product(com.chat2db.updater.<product>)。

验证

  • chat2db-community-updater:112 tests,0 失败(新增 PreparedUpdateStoreTest 7 个、FullPackageStagerTest 5 个复用用例)。
  • chat2db-community-jcef:137 tests,0 失败(新增 6 个:缓存命中不发起下载、重复下载不重下、检查不丢 prepared、重启后离线恢复、篡改缓存被丢弃并重下、已安装记录被静默清理)。
  • 前端 yarn test:hot-update、yarn test:update-check-schedule 通过,改动文件 eslint 0 warning。

未包含 / 待办

  • 真机验证(本机 5.3.7-beta.4 → 下一个 beta)尚未执行。
  • 稳定通道索引 404 的容忍与三态失败提示不在本次范围内。

本机真机测试期间发现并补修的两个问题(同一 PR)

  1. beta 通道的 prepared 更新会在下次启动被判不可信:恢复时用 STABLE 通道去验签,而 beta 包的 manifest 里 channel=BETA,于是验签通过、通道校验失败 → 已下载的 beta 更新被丢弃。现在按 manifest 自己的通道校验(通道字段在签名覆盖范围内,不可伪造),并补了回归测试(去掉修复该测试即红)。
  2. 检查失败被显示成"没有可用的新版本":appCheckUpdate 的 catch 无论超时、连不上还是 404 都返回 notAvailable。现在分两类:
    • 索引/ manifest 未发布(HTTP 404)→ 仍算"没有新版本"(稳定通道还没发 release-index.json 时不该报错)
    • 其它失败(连接超时、5xx、验签失败)→ 新增 CHECK_FAILED 态 → handler 映射 updateFailed → 前端提示"检查更新失败,请检查网络后重试"(5 种语言已补,i18n 校验通过)

测试与 CI

  • chat2db-community-updater:112 tests,chat2db-community-jcef:141 tests,0 失败
  • 前端:test:hot-update、test:update-check-schedule、test:i18n 通过,改动文件 eslint 0 warning
  • Community CI:前两个 commit 已 success,最新 commit 运行中;AI PR reviewer / QQ 通知为既有失败,与本改动无关

真机验证进度(本机 macOS,隔离的 dev 实例)

  • 已验证:检查更新 → 发现 5.3.7-beta.4 → 下载 388.9MB → 校验 sha256 → 解包暂存 → 状态变"新版本已准备就绪";代理未配时正确走 CHECK_FAILED 分支
  • 待验证:退出重开不重新下载(PREPARED_UPDATE_RESTORED)、点击安装后 helper 切换 + 自动重启到 beta.4(首次尝试因测试包内 helper jar 取错而 30s ACK 超时并失败,属测试环境问题,已修正后重试)

A downloaded update could not be installed. Every update check started a new
audit operation and cleared the prepared transaction and manifest, so the
package that had just been downloaded was forgotten, and the install step had
nothing left to hand to the helper. The frontend cleared the state in the same
way: every check response overwrote the update state, and several clicks on
"Check for updates" ran one serialized check each and showed one message each.

Backend:
- Reuse a prepared update before discovery runs: a remembered signed manifest
  is verified again with the bundled key, the cached package is checked against
  the size and SHA-256 it names, and the staged package is re-validated before
  it is offered for installation (PREPARED_UPDATE_RESTORED). A remembered
  update whose release epoch no longer advances the installed one is spent and
  dropped quietly (PREPARED_UPDATE_CONSUMED); anything that no longer verifies
  is discarded (PREPARED_UPDATE_DISCARDED) and downloaded again.
- Remember the prepared manifest and transaction in prepared-update.json, and
  keep the staging directory that was produced from exactly one package, so a
  restart neither downloads nor unpacks the same payload again.
- Make triggerDownload idempotent: a repeated request for a prepared update
  reports success and the completed progress instead of downloading again, and
  a check can no longer discard the discovered update or a running download.
- Report "ready to install" as a third check result so the client can offer the
  install action instead of "no update available".

Frontend:
- Share one in-flight check between overlapping triggers, ignore a second click
  while the check runs, and show the check button as loading.
- Never let a check response erase an update that is downloading, installing or
  already downloaded: only a newer discovered release may replace it.

Tests cover the persisted prepared update, staging reuse, the cache hit without
a download, the redownload after a changed package, the offline restore after a
restart, and the frontend state protection and request de-duplication.
@openai0229 openai0229 moved this to In Progress in Chat2DB Community Sep 21, 2026
@openai0229
openai0229 marked this pull request as ready for review September 21, 2026 05:41
@openai0229
openai0229 requested a review from a team as a code owner September 21, 2026 05:41
@openai0229 openai0229 moved this from In Progress to In Review in Chat2DB Community Sep 21, 2026
A prepared update that came from the beta channel failed verification in the
next session: the remembered manifest was checked against the stable channel,
so the manifest was declared untrusted and the downloaded beta update was
discarded instead of being offered for installation.

Verify the remembered manifest against the channel it names, exactly like the
discovery verifies a manifest against the channel it queried. The manifest is
covered by the signature, so its channel cannot be forged.
…lable"

A check that could not reach the update source was reported exactly like a
check that found no newer release, so a user with a broken network or a blocked
download host was told "no new version available" and had no way to tell the
difference.

Report the two cases separately:
- A release index or manifest that the source does not publish (HTTP 404) keeps
  meaning "no update available", because a channel that has not published its
  index yet is not an error.
- Any other failure (connect timeout, server error, verification failure) is
  reported as a failed check.

The check result carries a new CHECK_FAILED state, the JCEF handler maps it to
the updateFailed status, and the client already renders that status as an error
message. The new client message says the check failed and suggests checking the
network instead of claiming that no new version exists.
@openai0229
openai0229 merged commit 50d646d into main Sep 21, 2026
18 of 21 checks passed
@openai0229
openai0229 deleted the feat/desktop-update-resume branch September 21, 2026 06:39
@openai0229 openai0229 moved this from In Review to Done in Chat2DB Community Sep 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant