mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-05 08:47:27 +00:00
fix(plugin): 修两处持锁自死锁(CI 双红真因)+ tools_ready 事件声明
## 死锁(这是 CI 连续两次红的真因) `StopAll` 与 `StopPlugin` 都在持有 `r.mu` **写锁**时调用 `RemoveBuiltinSkill` → `skillDirOf` → `r.mu.RLock()` ⇒ **自死锁**。 后果不是报错而是**卡死**: - internal/plugins 卡 300s 超时(TestCapability_RestrictedPluginStillLoads) - internal/plugin 卡 840s 超时(TestStopPluginDoesNotTouchPersistentState) - CI 的 `go test ./...` 与 `go test -race` 双双红 修法:拆出**不加锁**的内部版本,持锁路径只走它: - `skillDirFrom(dataDir, name)`(纯函数) - `removeBuiltinSkillLocked(pluginName, p, dataDir)`(显式传 dataDir, 避免"看起来没取锁、一改就取锁"的隐患) - `announceSkillRemovedLocked`(持锁下直读 r.evBus) 修后:internal/plugins 300s → 12s,internal/plugin 840s → 0.7s。 ## 防回归判据 `TestStopAllAndStopPluginDoNotDeadlock`:死锁**不返回**,普通断言抓不到, 所以用**超时**当判据(5 秒未返回即失败),并指明应改用 Locked 版本。 ## tools_ready 事件声明 新增的 tools_ready 触发 `TestEveryPublishedEventHasAnOutwardPath` (该判据防"事件发了、客户端收不到"的静默失效)。 tools_ready 是**内核内部**就绪信号(plugin-host → seq 等订阅者), 不是对话流程事件,前端无需感知 ⇒ 在 allowed 里显式声明理由。 ## 验证 go build ./... / go vet ./... / go test -p 1 ./... / -race 全通过。
This commit is contained in:
@ -7,6 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/events"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/supervisor"
|
||||
@ -182,6 +183,17 @@ func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
ks.PluginsReady = true
|
||||
log.Printf("[pluginhost] kernel fully initialized -> loaded remaining plugins (%d built-in)",
|
||||
len(noManifest))
|
||||
|
||||
// ★ 发布 tools_ready —— "工具已全部注册完毕"的唯一信号。
|
||||
//
|
||||
// 语义只到"工具可调",**不涉及 agent 是否在跑**:
|
||||
// 依赖完整内核做事的订阅者(seq 开机序列)此刻即可开始,
|
||||
// 返回值进队列、等 agent 起来后处理即可。
|
||||
//
|
||||
// 位置即语义:放在 PluginsReady=true 之后、与内置插件加载同一次
|
||||
// once 里 —— 早了(如内核装配完成处)还有内置插件未加载,
|
||||
// 那时工具不全;晚了则订阅者错过这次进程唯一的就绪时刻。
|
||||
publishToolsReady(ks)
|
||||
})
|
||||
}
|
||||
// 订阅 + 立即评估一次(若 bus 已 complete,比如非生产路径的退化 bus)
|
||||
@ -350,3 +362,29 @@ func planCatalog(ks *kernelboot.KernelState) *PluginCatalog {
|
||||
c, _ := ks.PluginCatalog.(*PluginCatalog)
|
||||
return c
|
||||
}
|
||||
|
||||
// publishToolsReady 广播 tools_ready(全部插件加载完成、工具已注册)。
|
||||
//
|
||||
// 失败只记日志、不阻断:系统本身仍可正常接受输入,只是依赖本事件的
|
||||
// 订阅者(如 seq 的开机序列)不会自动执行。**必须可见** ——
|
||||
// 静默失败会让"为什么开机序列没跑"完全无从查起。
|
||||
func publishToolsReady(ks *kernelboot.KernelState) {
|
||||
if ks == nil || ks.EventBus == nil {
|
||||
log.Printf("[pluginhost] tools_ready NOT published: EventBus is nil " +
|
||||
"(subscribers such as seq boot sequences will not run)")
|
||||
return
|
||||
}
|
||||
var loaded []string
|
||||
if ks.PluginRegistry != nil {
|
||||
loaded = ks.PluginRegistry.ListLoadedPlugins()
|
||||
}
|
||||
ks.EventBus.Publish(&events.Event{
|
||||
Type: events.EventToolsReady,
|
||||
Source: "plugin-host",
|
||||
Payload: map[string]interface{}{
|
||||
"plugins": loaded,
|
||||
"plugin_count": len(loaded),
|
||||
},
|
||||
})
|
||||
log.Printf("[pluginhost] tools_ready published (%d plugins)", len(loaded))
|
||||
}
|
||||
|
||||
50
internal/kernelmod/pluginhost/toolsready_test.go
Normal file
50
internal/kernelmod/pluginhost/toolsready_test.go
Normal file
@ -0,0 +1,50 @@
|
||||
package pluginhost
|
||||
|
||||
// tools_ready 的发布判据。
|
||||
//
|
||||
// 语义边界(用户裁定):seq 自动运行**不依赖 agent** —— 它只是一段工具调用
|
||||
// 序列,返回值排队等 agent 起来处理即可。所以事件的正确时机是
|
||||
// "全部插件加载完、工具都注册了"(ks.PluginsReady = true 处),
|
||||
// 而不是 agent.Start() 之后。
|
||||
//
|
||||
// 放在 kernelmod 而不是 main:时机语义属于内核;外包给编排层会让
|
||||
// 换个入口(e2e / 测试 / 别的发行版 main)就不一致。
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/events"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
)
|
||||
|
||||
// 没有 EventBus 时必须**响亮报错而不是静默返回** ——
|
||||
// 静默失败会让"为什么开机序列没跑"完全无从查起。
|
||||
func TestPublishToolsReadyWithoutBusDoesNotPanic(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("publishToolsReady 在 nil EventBus 下不能 panic:%v", r)
|
||||
}
|
||||
}()
|
||||
publishToolsReady(nil)
|
||||
publishToolsReady(&kernelboot.KernelState{})
|
||||
}
|
||||
|
||||
// 有 EventBus 时必须真的发布一条 tools_ready,且带 plugin_count。
|
||||
func TestPublishToolsReadyPublishes(t *testing.T) {
|
||||
bus := events.NewBus()
|
||||
var got []*events.Event
|
||||
bus.Subscribe(events.EventToolsReady, func(e *events.Event) { got = append(got, e) })
|
||||
|
||||
ks := &kernelboot.KernelState{EventBus: bus}
|
||||
publishToolsReady(ks)
|
||||
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("应发布恰好一条 tools_ready,实际 %d 条", len(got))
|
||||
}
|
||||
if got[0].Source != "plugin-host" {
|
||||
t.Fatalf("Source 应为 plugin-host,实际 %q", got[0].Source)
|
||||
}
|
||||
if _, ok := got[0].Payload["plugin_count"]; !ok {
|
||||
t.Fatal("payload 应含 plugin_count")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user