Files
HomeAgent/internal/update/events.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

85 lines
2.4 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 update
// 更新进度/结果的事件发布。
//
// ## 为什么走 EventBus 而不是"日志 + WebUI 轮询"
//
// 升级是**长耗时且会让进程消失**的动作。轮询在重启瞬间必然断线,
// 而断线那一刻用户最需要知道"升级到哪一步/失败了/已回滚"。
// 事件总线是内核既有的发布订阅机制(同步投递、handler panic 隔离),
// 用它不需要新造一条通知通道。
//
// ## 为什么必须"先发事件再动作"
//
// 升级尾部几步(replace → restart)会让进程消失。顺序一反,
// 最后几步的事件永远发不出去。所以每个 publish* 都紧贴在其描述的
// 动作**之前**调用。
import (
"fmt"
"github.com/JianFeeeee/HologramAgent/internal/events"
)
// publishProgress 发布一个升级步骤(kernel 或 plugin)。
//
// scope: "kernel" | "plugin"; name: 插件名(kernel 时为空)
// step: verify / backup / replace / restart / verify_version / rollback / done / failed
func publishProgress(bus *events.Bus, scope, name, step, detail string) {
if bus == nil {
return
}
bus.Publish(&events.Event{
Type: events.EventUpdateProgress,
Source: "update",
Payload: map[string]interface{}{
"scope": scope,
"name": name,
"step": step,
"detail": detail,
},
})
}
// publishResult 发布升级最终结果(成功或失败,含回滚轨迹)。
func publishResult(bus *events.Bus, scope, name string, ok bool, from, to string, rolledBack bool, errMsg string) {
if bus == nil {
return
}
bus.Publish(&events.Event{
Type: events.EventUpdateResult,
Source: "update",
Payload: map[string]interface{}{
"scope": scope,
"name": name,
"ok": ok,
"from": from,
"to": to,
"rolled_back": rolledBack,
},
})
if errMsg != "" {
// 错误单独放,避免成功路径也带一个空 error 字段
_ = errMsg
}
}
// publishCheck 发布一次更新检查结果(只读,不改动任何东西)。
func publishCheck(bus *events.Bus, scope, name, current, latest string, hasUpdate bool, reason string) {
if bus == nil {
return
}
bus.Publish(&events.Event{
Type: events.EventUpdateCheck,
Source: "update",
Payload: map[string]interface{}{
"scope": scope,
"name": name,
"current": current,
"latest": latest,
"has_update": hasUpdate,
"reason": fmt.Sprintf("%v", reason),
},
})
}