Files
HomeAgent/docs/zh/experiments/plugin-arch/19-migration-verify/switch-production.py
JianFeeeee 62bdfa2b54 meta: 内核版本升到 1.0.0;生产切换脚本改走 hmap 正规通道(Part 6.5)
## 版本号

1.0.0:外部插件从 C ABI 动态库迁到子进程 + 共享内存。首个不再加载
`.so`/`.dll` 的版本,与 0.9.x 不兼容(存量插件必须用新版 plugindev 重编)。
SDKCompatibleVersion 同步升 1.0.0。

同时删掉 ABIVersion / CABINum / 51 个 Core<Method> 整数 ID —— 随 Part 6.2
删 internal/plugin/cabi/ 就已无使用者(grep 确认只剩定义处)。留着会让人
以为 C 层协商还在生效,或以为加 method 要同步维护那张整数表。

⚠️ 注意 Makefile 的 `VERSION ?= $(shell git describe --tags --dirty)`:
实际注入值来自 git tag,meta.go 里的默认值只在不带 ldflags 时生效。
make build 当前注入 v0.9.1-56-g2572688-dirty。要让 1.0.0 真正生效需打
v1.0.0 tag 或显式传 VERSION=1.0.0。

## 生产切换脚本重写

第一版是手工拷 plugin.bin + 手改 plugin.json 的 entry —— 那等于**重新实现
了一遍 hmap 解包逻辑,且实现得更差**。漏掉的东西:

  platforms 字段          hmap 内的 plugin.json 本来就写对了
  平台二进制选择          我硬编码 _linux_amd64,正规路径用 platformBinary()
  overwrite 语义          StopAndUnload 停旧实例但**保留配置表**
  失败回滚                os.Rename 备份旧目录,解包失败自动恢复
  校验                    validatePackage 查 manifest + 各平台二进制齐全

配置保留那条尤其关键:生产 17 个插件都有配置(qq 账号、weather 默认城市、
browser profile 路径)。我的脚本恰好没碰配置表所以侥幸不丢,但那是运气
不是设计。

改为 POST 到 pluginmgr 的 HTTP 端点(127.0.0.1:9876/plugins),
传 {path, overwrite:true} 走 installFromPath → installFromData。

保留的一个设计:**先全部校验再动手**。任一插件缺 hmap 就整批中止——
新 homed 不认 .so,「一半装了一半没装」的中间态最难排查。

## 生产切换已执行

顺序(先换二进制再装包,而非反过来):
  1. systemctl stop homeagent
  2. 换 /usr/local/bin/homed
  3. 起服务 —— 15 个 .so 插件报可操作错误被跳过,homed 本体与 16 个内置正常
  4. 逐个 POST 装 17 个 hmap(overwrite=true)
  5. 待重启核对

第 3 步顺带在真实二进制上验证了 Part 6.2 的可操作错误:
  [plugin] dynamic weather: 检测到旧 C ABI 产物(plugin.so/.dll/.dylib)。
  外部插件已改为子进程模式,请用新版 plugindev 重编产出 plugin.bin
  (业务代码无需修改)
不崩溃,只跳过。若反过来先装包,旧 homed 的 StopAndUnload 会停掉 qq
消息通道且无法重载 .bin,会卡在「插件全挂」的状态。

结果:17/17 成功,全部 config_kept=true;0 个残留 .so;17 个 plugin.bin
均有执行位;17 个 manifest 的 entry 均为 plugin.bin;无 .bak 残留。
bundle 包正确挑了当前平台(weather 目录只留 8.7MB 的 linux/amd64 那份)。

备份:/home/newqqagent-migration-backup-20260902-214812
(plugins 全目录 + homed.old + homeagent.service,162MB)

验证:go build ./... 通过;go test ./... 全仓无失败。

Ref: docs/zh/plugin-migration-plan.md Part 6.5
2026-09-02 22:40:47 +08:00

133 lines
4.1 KiB
Python
Executable File
Raw 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.

#!/usr/bin/env python3
"""生产切换:经 pluginmgr 正规通道安装 17 个 hmapPart 6.5)。
与手工拷贝方案的区别 —— 这里复用内核自己的安装逻辑:
validatePackage 校验 manifest + 平台二进制齐全
StopAndUnload 停旧实例但**保留配置表**
os.Rename 备份 解包失败自动回滚到旧版本
platformBinary() 按 runtime 挑当前平台那份,重命名为 plugin.bin
chmod 0755 补执行位
手工拷贝会重新实现这一套,且必然实现得更差(第一版就漏了 platforms 字段
与配置保留语义)。
用法:
switch-production.py 演练
switch-production.py --apply 实际安装
"""
import json
import os
import sys
import urllib.error
import urllib.request
PROD_PLUGINS = "/home/newqqagent/plugins"
SDK_EXAMPLE = "/home/program/TrueAgent/third_party/homeagent-sdk/example"
PLUGINMGR = "http://127.0.0.1:9876/plugins"
def find_hmap(name):
"""找插件的 hmap 包。
bundle:true -> <snake>_bundle.hmap含多平台二进制
bundle:false -> <snake>_<goos>_<goarch>.hmapqq 是这种)
"""
dist = os.path.join(SDK_EXAMPLE, name, "dist")
if not os.path.isdir(dist):
return None
cands = [f for f in os.listdir(dist) if f.endswith(".hmap")]
if not cands:
return None
for c in cands:
if c.endswith("_bundle.hmap"):
return os.path.join(dist, c)
return os.path.join(dist, sorted(cands)[0])
def install(path):
"""POST 到 pluginmgr。overwrite=true 走原地更新分支,保留配置表。"""
body = json.dumps({"path": path, "overwrite": True}).encode()
req = urllib.request.Request(
PLUGINMGR, data=body,
headers={"Content-Type": "application/json"},
method="POST")
try:
with urllib.request.urlopen(req, timeout=180) as resp:
return json.loads(resp.read().decode()), None
except urllib.error.HTTPError as e:
return None, "HTTP %d: %s" % (e.code, e.read().decode()[:300])
except Exception as e:
return None, str(e)
def main():
apply = "--apply" in sys.argv
targets = sorted(
d for d in os.listdir(PROD_PLUGINS)
if os.path.isfile(os.path.join(PROD_PLUGINS, d, "plugin.so"))
or os.path.isfile(os.path.join(PROD_PLUGINS, d, "plugin.bin"))
)
print("生产外部插件: %d" % len(targets))
# 先全部校验,任一缺包就整批中止。
# 理由:新 homed 不认 .so「一半装了一半没装」的中间态最难排查。
plan = []
missing = []
for name in targets:
h = find_hmap(name)
if h is None:
missing.append(name)
else:
plan.append((name, h))
if missing:
print("\n✗ 中止:以下插件缺 hmap 包:")
for m in missing:
print(" " + m)
print("\n先跑 rebuild-plugins.sh 重编。")
return 1
print("✓ 全部 %d 个 hmap 就位\n" % len(plan))
for name, h in plan:
print(" %-16s %-44s %6d KB" % (
name, os.path.basename(h), os.path.getsize(h) // 1024))
if not apply:
print("\n[演练] 加 --apply 才实际安装")
return 0
print("\n经 pluginmgr 安装overwrite=true保留配置...")
ok = 0
failed = []
for name, h in plan:
result, err = install(h)
if err:
print("%-16s %s" % (name, err))
failed.append(name)
continue
if "error" in result:
print("%-16s %s: %s" % (
name, result["error"], result.get("details", "")))
failed.append(name)
continue
print("%-16s %-12s v%s -> v%s config_kept=%s" % (
name,
result.get("action", "?"),
result.get("previous_version", "?"),
result.get("version", "?"),
result.get("config_kept", False)))
ok += 1
print("\n成功 %d / 失败 %d" % (ok, len(failed)))
if failed:
print("失败: " + " ".join(failed))
return 1
return 0
if __name__ == "__main__":
sys.exit(main())