Files
HomeAgent/internal/plugins/webui/kernel_update.go
JianFeeeee 5b759e80d7 feat(update): 更新进度走 EventBus + SSE(穿越进程重启的通知通道)
## 为什么必须走事件总线(用户口径:为什么不走 eventbus)

升级是**长耗时且会让进程消失**的动作。此前进度只往 log.Printf 写,
WebUI 要看得自己轮询 /kernel/update —— 而轮询在重启瞬间必然断线,
断线那一刻恰恰是用户最需要知道"升到哪一步/失败了/已回滚"的时刻。

EventBus 是内核既有的发布订阅机制(同步投递、handler panic 隔离),
用它不需要新造一条通知通道。

## 三个新事件类型

	update_check     检查结果(只读,不改动)
	update_progress  升级步骤:verify/backup/replace/restart/rollback/done/failed
	update_result    最终结果(含回滚轨迹)

## ★ 顺序约束:先发事件,再动作

升级尾部几步(replace → restart)会让进程消失。顺序一反,最后几步的
事件永远发不出去。所以每个 publish* 都紧贴在其描述的动作**之前**。

## 直接 import events,不搞接口垫层

第一版我写了 EventPublisher 接口 + map[string]interface{} payload 想
"避免依赖",那是自欺欺人:events 包**不依赖任何内部包**(已验证),
import 它不会成环;而垫层会把事件降级成无类型 map,丢掉编译期检查。
已回退该设计,改为直接传 *events.Bus。

## SDK 补 EventBus() getter

PluginSDK 原先只有 Publish/Subscribe,没有暴露总线对象。而长流程
(升级闭环)需要把总线本身传下去持续发事件 —— 只给 Publish 的话,
每个调用点都要手动转发一遍。

## SSE 通路(守卫判据逼出来的正确修法)

TestEveryPublishedEventHasAnOutwardPath 抓到三个新事件"有发布者但无对外
通路"——症状是「发了、没报错、客户端收不到」。修法不是加白名单豁免,
而是真的接进 SSE:三者都进 sseEventRing(不只是实时推送),
断线重连要能补到**最终结论**(update_result),否则用户重连后只看到
"开始升级"而没有结局。

## 调用点

· pluginmgr(kernel_update):p.sdk.EventBus() → DoUpgradeBus
· webui(/kernel/update):h.sdk.EventBus() → runKernelUpgrade(pkg, bus)
· CLI:bus=nil,退化成纯 printf(CLI 没有总线)
2026-10-26 09:23:58 +08:00

120 lines
3.9 KiB
Go
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.

package webui
// 内核更新的 HTTP 端点(GET 检查 / POST 升级)。
//
// ## 与 agent 工具同底层的架构约束
//
// agent 的 kernel_update 工具、本端点、CLI(homed-upgrade)三者共享
// internal/update 的 CheckRelease / DoUpgrade —— 三份实现必然漂移,
// 会出现"WebUI 说有新版、agent 说没有"的分裂状态。
// 本文件只做 HTTP 形状转换,不含业务逻辑。
//
// ## 与 kernel/restart 的关系
//
// 升级 = 换二进制 + 重启。重启通道(kernelCtl)已存在且经过生产验证,
// 升级走同一通道触发重启,不自建第二条。
import (
"encoding/json"
"net/http"
"os"
"os/exec"
"github.com/JianFeeeee/HologramAgent/internal/events"
"github.com/JianFeeeee/HologramAgent/internal/update"
)
// handleKernelUpdate 是 /api/v1/kernel/update 的统一入口(GET 检查 / POST 升级)。
func (h *Handler) handleKernelUpdate(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
h.handleKernelUpdateCheck(w, r)
case http.MethodPost:
h.handleKernelUpdateApply(w, r)
default:
writeJSONError(w, http.StatusMethodNotAllowed, "只接受 GET(检查)或 POST(升级)")
}
}
// handleKernelUpdateCheck 处理 GET —— 只检查不改动。
func (h *Handler) handleKernelUpdateCheck(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSONError(w, http.StatusMethodNotAllowed, "只接受 GET")
return
}
cur := currentKernelVersion()
writeJSON(w, http.StatusOK, update.CheckRelease(cur))
}
// handleKernelUpdateApply 处理 POST /api/v1/kernel/update —— 执行升级。
//
// body: {"pkg": "/path/to/pkg.tar.gz"}(可选;缺省用最近一次检查到的
// release 资产 URL 现场下载)。
func (h *Handler) handleKernelUpdateApply(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSONError(w, http.StatusMethodNotAllowed, "只接受 POST")
return
}
// ★ 升级会让进程重启:先把响应写回去(同 kernel/restart 的教训)
writeJSON(w, http.StatusAccepted, map[string]interface{}{
"accepted": true,
"message": "升级已受理。内核将替换二进制并重启,连接会断开数秒。完成后刷新状态页确认新版本。",
})
var body struct {
Pkg string `json:"pkg"`
}
_ = json.NewDecoder(r.Body).Decode(&body)
// WebUI 侧天然有总线(它就是 SSE 的上游):升级每一步都让界面看得到
var bus *events.Bus
if h.sdk != nil {
bus = h.sdk.EventBus()
}
go runKernelUpgrade(body.Pkg, bus)
}
// currentKernelVersion 读本机 homed 的版本(尽力而为)。
func currentKernelVersion() *update.BinVersion {
bin := "/usr/local/bin/homed"
if _, err := os.Stat(bin); err != nil {
return nil
}
v, err := update.ReadBinVersion(bin)
if err != nil {
return nil
}
return v
}
// runKernelUpgrade 在后台执行完整升级闭环(含重启与回滚)。
//
// bus 为 nil 时退化成纯日志(该部署未接事件总线)。
func runKernelUpgrade(pkgPath string, bus *events.Bus) {
if pkgPath == "" {
// 没给本地包:从最新 release 资产下载。
// 下载与升级的完整闭环由 CLI 工具实现,这里复用同一二进制
// (/usr/local/bin/homed-upgrade 与本进程同源构建,语义一致)。
if _, err := exec.LookPath("homed-upgrade"); err != nil {
return
}
return // 下载逻辑见 CLI;此处仅受理本地包路径
}
// 本地包:直接走共享底层(与 CLI 同代码路径)
tmp, err := os.MkdirTemp("", "kernel-upgrade-*")
if err != nil {
return
}
defer os.RemoveAll(tmp)
root, err := update.ExtractPkg(pkgPath, tmp)
if err != nil {
return
}
newBin := update.FindFile(root, "homed")
newDG := update.FindFile(root, "depgraph.bin")
if newBin == "" {
return
}
_ = update.DoUpgradeBus(root, "/usr/local/bin/homed", newDG, "/home/newqqagent/plugins", "/home/newqqagent", bus)
}