Files
Leo edc9916b2f 修好:Mac 自我更新斷七代(dmg 餵給解 zip 的 ditto)+殘留 staged 旗標騙人重啟
症狀一:manifest.daemon.mac.file 出貨格式從 zip 換成 dmg(build-dmg.sh),
但 applyStagedAndRestart 只認 zip(ditto -x -k)⇒ ditto: Couldn't read PKZip
signature,Mac「檢查更新」從 v0.18.5 斷到 v0.18.22(leo 真機截圖實測)。
症狀二:手動裝好新版後仍顯示「已下載完成,重新啟動就會套用」——staged.json
判準是「有沒有下載過」而非「目前版本 vs 最新版本」。

- extractAppFrom 依副檔名分流 .dmg(hdiutil attach -mountpoint)/.zip;
  Windows 補上真正的自我更新(rename 正在跑的 exe → 搬新版 → 原路徑重啟,
  併檔後 Windows 只有單一 exe,「開資料夾」是併檔前的殘留假設)。
- CheckUpdate 加 stagedDecision 純函式:已追上或 staged 版本過期就清掉
  殘留(連磁碟檔案一起清,不只清記憶體)。
- 新增 selfupdate_test.go:用真的 hdiutil 造 dmg 驗證解壓(不是 mock)。

Mac 端到端已用真的線上 manifest/真 v0.18.22 dmg 驗證整條鏈路(含用舊寫法
對同一份真檔案重現原始錯誤,證明修法對症)。Windows 只驗到 GOOS=windows
交叉編譯成功,真機行為待 leo 用 VMware Fusion 驗證——詳見 status.md。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 02:58:01 +08:00

110 lines
4.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
// update_api.go — 「版本與更新」頁的後端(t193)
//
// 🔴 leo 2026-08-04:「檢查更新連到 docs 的檢查更新,**不正確**,
// 是**直接在這裡進行檢查更新**,應該是跳到一個頁面顯示**現在版本與最新版本**,
// 然後**下載可以進行安裝**」
//
// ⇒ 三段式,全部在 App 內完成,不把人丟去網頁:
// ① CheckUpdate 查最新版 → 前端顯示「現在版本 vs 最新版本」
// ② DownloadUpdate 下載+sha256 校驗 → 備妥
// ③ ApplyUpdate 覆蓋正在跑的 .app 並重啟
import (
"os"
"runtime"
"strings"
)
// UpdateInfo 是「版本與更新」頁要顯示的一切。
type UpdateInfo struct {
Current string `json:"current"`
Latest string `json:"latest"`
Available bool `json:"available"`
Notes string `json:"notes,omitempty"`
Staged bool `json:"staged"` // 已下載備妥、等重啟套用
Err string `json:"err,omitempty"` // 誠實回報,不假裝成功
}
func (a *App) CheckUpdate() UpdateInfo {
info := UpdateInfo{Current: version}
rel, err := fetchLatestRelease()
if err != nil {
info.Err = "暫時連不上更新伺服器:" + err.Error()
return info
}
info.Latest = rel.Version
info.Notes = rel.Notes
info.Available = newerThanCurrent(rel.Version)
// 🔴 2026-08-08(leo 真機實測):手動裝好新版後按「檢查更新」,
// 仍顯示「新版已下載完成,重新啟動就會套用」——因為背景/上次曾自動下載備妥過
// 同一版,使用者卻是自己手動裝的,staged.json 沒人清,殘留狀態一直騙下去。
// 判準要是「當前版本 vs 最新版本」,不是「有沒有下載過」——見 stagedDecision。
if s := loadStaged(); s.Ready {
report, clear := stagedDecision(info.Available, s.Version, rel.Version)
if clear {
clearStaleStaged()
}
info.Staged = report
}
return info
}
// stagedDecision 決定「已備妥」的殘留旗標該回報成「已就緒,可重啟套用」還是該被清掉。
// 抽成純函式(無 IO)方便單元測試涵蓋 08-08 那個真機撞到的殘留旗標病,不必真的碰磁碟/網路。
//
// available=目前版本是否落後 latestnewerThanCurrent 的結果)
// stagedVersionlatestVersion=已備妥的版本 vs 這次查到的最新版本
//
// 只有「目前版本落後、且備妥的正是這次的最新版」才回報 Staged=true
// 其餘情況(已經追上/備妥的是舊版)都是殘留,要清掉不能再誤導使用者。
func stagedDecision(available bool, stagedVersion, latestVersion string) (report bool, clear bool) {
if !available || stagedVersion != latestVersion {
return false, true
}
return true, false
}
// clearStaleStaged 清掉「已備妥但已經不需要套用」的殘留(暫存檔+標記),
// 不只是記憶體裡的 currentStaged——否則下次啟動 loadStaged() 又會撿回同一個誤導。
func clearStaleStaged() {
s := loadStaged()
if s.ZipPath != "" {
_ = os.Remove(s.ZipPath)
}
_ = os.Remove(stagedMarkerPath())
currentStaged = stagedUpdate{}
}
func (a *App) DownloadUpdate() UpdateInfo {
info := UpdateInfo{Current: version}
rel, err := fetchLatestRelease()
if err != nil {
info.Err = "暫時連不上更新伺服器:" + err.Error()
return info
}
info.Latest, info.Notes = rel.Version, rel.Notes
info.Available = newerThanCurrent(rel.Version)
zip, err := downloadUpdate(rel)
if err != nil {
// 誠實:下載/校驗失敗就說失敗,不留下半個檔假裝好了
info.Err = "下載失敗:" + err.Error()
return info
}
currentStaged = stagedUpdate{Version: rel.Version, ZipPath: zip, Ready: true}
saveStaged(currentStaged)
info.Staged = true
return info
}
func (a *App) ApplyUpdate() error {
if err := applyStagedAndRestart(); err != nil {
return err
}
return nil
}
// isWindows 供 selfupdate.go 使用(原本在 arcrun-tray 的 main.go)。
func isWindows() bool { return strings.EqualFold(runtime.GOOS, "windows") }