fix(update): 升级后第一次启动被误回滚(真机 OTA 实测)+ 关闭期 nil 解引用

## 一、★★★ 升级永远无法生效(rollback.go)

真机跑 OTA 闭环时暴露:

	-apply 输出:✅ 升级完成:1.4.0dev (commit ca5b1b1)
	重启后日志:ROLLED BACK kernel 1.4.0dev → 1.4.0dev(上一次升级未能确认启动)

原因:pending 标记是在**替换之前**写的(这是对的,见下),所以升级后的
**第一次启动必然看到它**。而原实现"只要标记在就回滚",从不比对当前运行的
是哪个版本 ⇒ 每次升级都在第一次启动时被自己撤销。

正确语义(两段式确认的本意)是比对版本:

	当前版本 == 标记里的 To     ⇒ 新版本**已经跑起来了** ⇒ 确认、清标记、继续
	当前版本 != To(或读不到)  ⇒ 新版本没能成为运行版本  ⇒ 回滚到备份

标记仍然必须写在替换**之前**:反过来(替换后才写)会在"替换成功但没来得及
写标记就崩了"时丢标记,那时就不知道该回滚了。

两条判据(用真能执行 `--version` 的脚本做假二进制 —— 原来那些用例写死字节、
根本读不出版本,只覆盖了"读不到 ⇒ 回滚"这一条分支,所以抓不到本缺陷):

	TestRollback_ConfirmsWhenRunningTargetVersion  跑的是 To ⇒ 不回滚 + 清标记
	TestRollback_RollsBackWhenRunningOldVersion    跑的是 From ⇒ 必须回滚

## 二、每次重启必现两条 panic(proc/host.go)

	[stage] tool xxx handler panic: invalid memory address or nil pointer dereference
	github.com/.../proc.(*unifiedRegion).generation(...)

关机/重启时 SharedRef 统一段先被拆掉,而在途的工具调用(stage handler /
cleaner proxy)仍会走到 Host.Generation() ⇒ 解引用 nil。panic 虽被 recover,
但把"正常关闭"伪装成故障,并刷屏掩盖真问题。加 nil 守卫(连 h 一起判)。

## 三、下载客户端 20s 总超时(release.go)

`DownloadLatestRelease` 复用了查询用的 kernelHTTPClient(Timeout=20s,
那是**整条请求**上限,含读完 body)。而发行包近 900MB ⇒ 必然
`context deadline exceeded ... while reading body`,"从 Release 自动更新"
永久失败,且错误看起来像网络问题。

改为独立下载客户端:建连/TLS/响应头仍分层设超时(卡住能失败),
读 body 不设总时长。
This commit is contained in:
JianFeeeee
2026-10-26 14:03:05 +08:00
parent ca5b1b17b7
commit ca956f69bf
4 changed files with 154 additions and 2 deletions

View File

@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
@ -29,8 +30,35 @@ func ghRepo() string {
return "JianFeeeee/HologramAgent"
}
// kernelHTTPClient 用于**查询**(JSON 元数据,小、快)。
//
// 20s 总超时在这里是合适的:查不到就该快速失败,让用户看到"查不了",
// 而不是挂着一个"正在检查更新"的界面。
var kernelHTTPClient = &http.Client{Timeout: 20 * time.Second}
// downloadHTTPClient 用于**下载安装包**。
//
// ★ 为什么不能复用上面那个客户端(实测踩到):发行包约 880MB,
// 而 Client.Timeout 是**整条请求**的上限(含读完 body)—— 20s 下必然
// `context deadline exceeded (Client.Timeout ... while reading body)`。
// 那会让"从 Release 自动更新"永远失败,而错误看起来像网络问题。
//
// ⇒ 下载客户端**不设总超时**,改用分层超时:
//
// · 建连/TLS 握手/等响应头 —— 有界,卡住就失败
// · 读 body —— 不限总时长(大文件),靠连接空闲超时 + 传输层兜底
var downloadHTTPClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{Timeout: 15 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
TLSHandshakeTimeout: 15 * time.Second,
ResponseHeaderTimeout: 60 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
IdleConnTimeout: 90 * time.Second,
ForceAttemptHTTP2: true,
},
}
// ReleaseInfo 是一次「内核检查更新」的结果。
type ReleaseInfo struct {
Current string `json:"current"` // 本机版本
@ -144,7 +172,7 @@ func DownloadLatestRelease(dst string) (*ReleaseInfo, error) {
}
func downloadFile(url, dst string) error {
resp, err := kernelHTTPClient.Get(url)
resp, err := downloadHTTPClient.Get(url)
if err != nil {
return err
}