mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-05 16:57:32 +00:00
## 范围
OTA 只处理 homed / depgraph.bin / waiter 三个文件,**不升级插件**。
插件与内核的兼容性改由「自动禁用 + 写明原因 + 界面展示」处理。
## 1. homed --version(OTA 的前提)
新增该 flag,输出单行 key=value:
version=1.4.0dev commit=c9a32a9 build_time=... sdk=1.3.0
★ 为什么必须有:升级前要能**问一个二进制它是什么版本**。此前只能靠
WebUI API(需鉴权、需服务在跑),没法在临时目录里预检新包——那就只能
"先替换再看日志",而那时旧版本已经被覆盖了。
## 2. cmd/homed-upgrade:默认只探测
homed-upgrade -pkg xxx.tar.gz # 探测
homed-upgrade -pkg xxx.tar.gz -apply # 执行
探测项:当前版本 / 包内版本 / 版本比较结论 / depgraph 是否同批 /
**插件兼容性预判**(换内核后会有几个插件被自动禁用)。
版本比较区分四种情况,都给出可读理由(用户才敢按 -apply):
· 可升级 / **降级**(明确警告)/ 完全相同(无需升级)/
**同版本不同 commit**(同版本重发——最容易漏掉的一种)
`-apply` 走:预检 → 备份(只备本次会动的文件,不整目录拷)→ 原子替换
(写 .tmp 再 rename)→ 重启 → 版本复核 → 失败自动回滚。
## 3. 兼容性兜底:第一次升级必然撞到的坑
实测踩到:线上内核是加 --version **之前**的版本,第一次探测必然读不到。
此时不能失败退出(否则所有用户第一次升级都要人工介入)。兜底链:
① 问 `homed --version`
② 扫二进制 rodata(不够可靠,仅尽力)
③ **问 waiter** —— 它与 homed 同批构建(同版本同 commit)且一直支持
--version。实测这条成功识别出当前是 1.4.0/98850e1。
④ 都失败 → 版本比较降级为"未知",**不阻断**其余预检。
## 4. 不兼容插件自动禁用(替代"升级插件")
内核换版本后,用旧 SDK 编的插件握手会判主版本不匹配。此前只是
`log.Printf` 后返回 false:每次重启重试一遍失败一遍、日志反复刷,
用户在界面上只看到"插件不见了"。
改为:持久禁用 + 写明原因(`maybeAutoDisableIncompatible`)。
`disabled_plugins` 表新增 reason 列(带 ALTER 迁移,旧库补列),
`sdk.DisabledPluginInfo` 同步加 Reason,随既有 settings API 到前端。
★ **只认"主版本不匹配"这一类**:端口被占、配置缺失、插件崩溃都是
可能自愈的临时/环境问题。把它们也自动禁用,等于把可修复的故障变成
"永不加载",比反复重试糟糕得多。
判据:
· cmd/homed-upgrade/main_test.go — 5 种版本比较结论 + 未知不阻断 +
semver 解析(1.4.0dev 不得被当 1.4.0 比大小)+ waiter 输出解析
(日期不得被当版本号)
· internal/plugin/autodisable_test.go — 认得出主版本不匹配(且 reason
必须带两端版本号)/ 四类临时故障**不得**被误判为版本不兼容
107 lines
3.8 KiB
Go
107 lines
3.8 KiB
Go
package main
|
||
|
||
import "testing"
|
||
|
||
// ── 版本比较的判据 ────────────────────────────────────────────
|
||
//
|
||
// 这些是 OTA 敢不敢按 -apply 的依据。判错一次就可能把新版换成旧版,
|
||
// 或让"内容其实没变"被当成升级成功。
|
||
|
||
func TestCompareVersions(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
cur *binVersion
|
||
nv *binVersion
|
||
wantSub string // 结论文案必须包含的片段
|
||
}{
|
||
{
|
||
name: "正常升级",
|
||
cur: &binVersion{Version: "1.3.0", Commit: "aaa"},
|
||
nv: &binVersion{Version: "1.4.0", Commit: "bbb"},
|
||
wantSub: "可升级",
|
||
},
|
||
{
|
||
name: "降级必须显式警告",
|
||
cur: &binVersion{Version: "1.4.0", Commit: "aaa"},
|
||
nv: &binVersion{Version: "1.3.0", Commit: "bbb"},
|
||
wantSub: "降级",
|
||
},
|
||
{
|
||
name: "同版本同 commit = 无需升级",
|
||
cur: &binVersion{Version: "1.4.0", Commit: "aaa"},
|
||
nv: &binVersion{Version: "1.4.0", Commit: "aaa"},
|
||
wantSub: "完全相同",
|
||
},
|
||
{
|
||
// ★ 这条最容易被忽略:版本号没变但内容变了(同版本重发)。
|
||
// 不报出来会让用户以为"升级了其实没升"或反之。
|
||
name: "同版本不同 commit 必须点明",
|
||
cur: &binVersion{Version: "1.4.0", Commit: "aaa"},
|
||
nv: &binVersion{Version: "1.4.0", Commit: "bbb"},
|
||
wantSub: "同版本重发",
|
||
},
|
||
{
|
||
// 非标准形态(1.4.0dev):走字符串回退,不能说"降级"
|
||
name: "非标准版本号走字符串比较",
|
||
cur: &binVersion{Version: "1.4.0", Commit: "aaa"},
|
||
nv: &binVersion{Version: "1.4.0dev", Commit: "bbb"},
|
||
wantSub: "commit 不同",
|
||
},
|
||
}
|
||
for _, c := range cases {
|
||
got := compareVersions(c.cur, c.nv)
|
||
if !containsStr(got, c.wantSub) {
|
||
t.Errorf("%s: 结论应含 %q,实际 %q", c.name, c.wantSub, got)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 判据:读不到当前版本时**不能**阻断(第一次升级必然撞到)。
|
||
//
|
||
// 这是实测踩到的:线上内核是加 --version 之前的版本,第一次探测必然失败。
|
||
// 若此时直接退出,所有用户第一次升级都要人工介入。
|
||
func TestCompareVersions_UnknownCurrentDoesNotBlock(t *testing.T) {
|
||
got := compareVersions(nil, &binVersion{Version: "1.4.0", Commit: "bbb"})
|
||
if containsStr(got, "❌") {
|
||
t.Errorf("当前版本未知不应判失败,实际: %s", got)
|
||
}
|
||
if !containsStr(got, "跳过比较") {
|
||
t.Errorf("应说明跳过比较,实际: %s", got)
|
||
}
|
||
}
|
||
|
||
// ── 版本解析 ─────────────────────────────────────────────────
|
||
|
||
func TestParseSemver(t *testing.T) {
|
||
if v := parseSemver("1.4.0"); v == nil || v.major != 1 || v.minor != 4 || v.patch != 0 {
|
||
t.Errorf("1.4.0 解析错: %+v", v)
|
||
}
|
||
// dev 后缀不算正式语义版本(否则 1.4.0dev 会被当成 1.4.0 比大小)
|
||
if v := parseSemver("1.4.0dev"); v != nil {
|
||
t.Errorf("1.4.0dev 应回退字符串比较,实际解析成 %+v", v)
|
||
}
|
||
}
|
||
|
||
// ── waiter 兜底解析 ──────────────────────────────────────────
|
||
|
||
func TestFirstSemverLike(t *testing.T) {
|
||
// waiter 的真实输出形态
|
||
line := "waiter 1.4.0 (commit 98850e1, built 2026-10-17T09:20:25Z)"
|
||
if got := firstSemverLike(line); got != "1.4.0" {
|
||
t.Errorf("应从 waiter 输出取到 1.4.0,实际 %q", got)
|
||
}
|
||
// 不能把日期当版本号
|
||
if got := firstSemverLike("built 2026-10-17"); got != "" {
|
||
t.Errorf("日期不应被识别为版本号,实际 %q", got)
|
||
}
|
||
}
|
||
|
||
func containsStr(s, sub string) bool {
|
||
for i := 0; i+len(sub) <= len(s); i++ {
|
||
if s[i:i+len(sub)] == sub {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|