mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
Part 3 收尾。tryLoadProc 从桩位变成真实加载路径,plugin.bin 插件现在 经 registry 完整跑起来:spawn → 握手(共享段 fd 3)→ init/start → 反向注册 → 工具调用 → stage 共享内存读改写。 registry 侧: - Registry 持有 procHost(惰性创建,**全部 .bin 插件共用一块段**)。 每插件一段会让「内核 ctx → 段 → 插件改 → 回读 ctx」在多插件下退化成 副本模型,lost update 原样复现(§8.4 实测 35.8~36.8%)。 - tryDynamic 分派到 Registry.loadProc;tryLoadProc 退为纯静态校验 (构造需要 Host,只有 Registry 有)。 - StopAll 在锁外释放共享段:插件还持有映射时拆段,它们下一次访问就是 SIGBUS;且持锁调用会与 onProcCrash 回调产生锁序风险。 - onProcCrash 把子进程退出转成 EventSystem 事件,不在回调里直接重载 (重载需 registry 锁,而回调可能来自持锁路径的 goroutine)。 proc_core.go —— 权限梯度的类型系统落点(§3.8): - procCore 用**命名字段**持有 *isdk.PluginSDK,不是嵌入。嵌入会提升全部 方法,外部插件就能经类型断言拿到 Supervisor/Tracker/Adapter/Indexer/ Status/Selftest。命名字段下只有显式写出的方法存在——权限梯度从 「C ABI 表达能力的意外产物」变成显式声明并强制的策略。 - 能力访问器把内部超集接口收窄到公开面(isdk.KnowledgeAPI 内嵌 pubsdk.KnowledgeAPI 再加 Stats/Remove,isdk.MemoryAPI 加 GraphData, isdk.LLMAPI 加 Chat/ReloadFromConfig);nil 保护避免类型化 nil 让 corehandler 的判空失效。 - procPluginAdapter 转接 Start(*isdk.PluginSDK) → Start(proc.CoreSDK), Close 对 closeDynamic 可见故重载能真 kill 子进程(对比 dlclose 对 Go c-shared 是 no-op,§1.1)。 共享段分配按平台拆分(原先 host.go 直接调 unix.MemfdCreate,darwin/windows 交叉编译失败):Linux memfd;macOS 立即 unlink 的临时文件(无 memfd_create, 但语义一致:无残留、fd 可经 ExtraFiles 传递、子进程 mmap 同一 inode); 其余平台明确报错而非静默降级成「无共享段」——那会让 stage 静默失去数据面。 测试 +13 项: - e2e_template_test.go 用**真实 plugindev 模板**(而非 testdata 手写假插件) 编译插件跑全链路,验证「模板 ↔ 内核」协议/布局真的对齐,不只是内核自己 跟自己对齐。含 lifecycle.autoRestart 上报、工具调用、stage 读改写、 FinalText 回传(C ABI 下 after_toolcall 看不到此字段,§8.3 10→16)、 只读插件不覆盖改写插件。 - proc_load_test.go 验证 Host 唯一性/惰性、chmod +x 错误提示、 Close 可见性,以及 procCore 不暴露内核内部机制的断言。 验证:go build ./... 通过;go test -race ./internal/plugin/... 全绿; 全仓 go test 无新增失败;git diff third_party/homeagent-sdk/sdk/ 为空。 既有告警 cabi/loader.go:156 unsafe.Pointer 非本次引入。 Ref: docs/zh/架构迁移评估.md §3.3/§3.4/§3.8、docs/zh/plugin-migration-plan.md Part 3
132 lines
4.3 KiB
Go
132 lines
4.3 KiB
Go
//go:build linux || darwin
|
||
|
||
package plugin
|
||
|
||
import (
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
"path/filepath"
|
||
"time"
|
||
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin/proc"
|
||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||
)
|
||
|
||
// tryLoadProc 只做静态校验(供双通道探测与测试),不构造插件实体。
|
||
//
|
||
// 真正加载走 Registry.loadProc:子进程插件需要共享段 Host,
|
||
// 而 Host 必须是**全部 .bin 插件共用的那一个**,只能由 Registry 持有。
|
||
//
|
||
// 返回 nil,nil 表示目录中没有 plugin.bin(交由后续探测通道);
|
||
// 找到二进制但不可用时返回明确错误——不静默回退到 cabi。
|
||
func tryLoadProc(dir, name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||
_, err := validateProcBinary(dir, name)
|
||
return nil, err
|
||
}
|
||
|
||
// 子进程插件加载(plugin.bin)——外部插件多进程化的加载入口。
|
||
//
|
||
// 设计依据:docs/zh/架构迁移评估.md §3(stdio JSON-RPC 控制面 + shm 数据面 + eventfd 通知面)
|
||
// 实施计划:docs/zh/plugin-migration-plan.md Part 2/3
|
||
|
||
// validateProcBinary 校验 plugin.bin 是否存在且可执行。
|
||
// 返回 ("", nil) 表示该目录不是 proc 插件。
|
||
func validateProcBinary(dir, name string) (string, error) {
|
||
binPath := filepath.Join(dir, binEntry)
|
||
st, err := os.Stat(binPath)
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
return "", nil
|
||
}
|
||
return "", fmt.Errorf("proc plugin %s: 检查 %s: %w", name, binEntry, err)
|
||
}
|
||
if st.IsDir() {
|
||
return "", fmt.Errorf("proc plugin %s: %s 是目录,不是可执行文件", name, binEntry)
|
||
}
|
||
if st.Mode()&0o111 == 0 {
|
||
// 常见于经 zip/hmap 分发丢失权限位——给出可直接执行的修复指令
|
||
return "", fmt.Errorf("proc plugin %s: %s 缺少可执行权限(chmod +x %s)",
|
||
name, binEntry, binPath)
|
||
}
|
||
return binPath, nil
|
||
}
|
||
|
||
// loadProc 构造子进程插件实体(不 spawn)。
|
||
//
|
||
// 共享段 Host 在此惰性创建:**全部 .bin 插件共用一块段**。
|
||
// 若每插件一段,多插件同阶段并发时会退化成副本模型,
|
||
// lost update 原样复现(§8.4 实测 35.8~36.8%)。
|
||
func (r *Registry) loadProc(dir, name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||
binPath, err := validateProcBinary(dir, name)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if binPath == "" {
|
||
return nil, nil
|
||
}
|
||
|
||
host, err := r.ensureProcHost()
|
||
if err != nil {
|
||
return nil, fmt.Errorf("proc plugin %s: %w", name, err)
|
||
}
|
||
|
||
return procPluginAdapter{Plugin: proc.New(name, binPath, dir, config, host, r.onProcCrash)}, nil
|
||
}
|
||
|
||
// ensureProcHost 惰性创建共享段 Host(全进程唯一)。
|
||
func (r *Registry) ensureProcHost() (*proc.Host, error) {
|
||
r.procHostMu.Lock()
|
||
defer r.procHostMu.Unlock()
|
||
if r.procHost != nil {
|
||
return r.procHost, nil
|
||
}
|
||
host, err := proc.NewHost()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
r.procHost = host
|
||
log.Printf("[plugin] 共享段已创建(全部子进程插件共用一块,%d KB)", host.ShmSize()/1024)
|
||
return host, nil
|
||
}
|
||
|
||
// closeProcHost 释放共享段(仅在内核关停时调用)。
|
||
func (r *Registry) closeProcHost() {
|
||
r.procHostMu.Lock()
|
||
defer r.procHostMu.Unlock()
|
||
if r.procHost == nil {
|
||
return
|
||
}
|
||
if err := r.procHost.Close(); err != nil {
|
||
log.Printf("[plugin] 关闭共享段: %v", err)
|
||
}
|
||
r.procHost = nil
|
||
}
|
||
|
||
// onProcCrash 在子进程插件异常退出时回调。
|
||
//
|
||
// **崩溃隔离**:子进程死亡只影响自己,homed 继续服务——对比 C ABI 下
|
||
// 插件 panic 直接带崩整个进程(§1.2,现网已发生)。
|
||
//
|
||
// 崩溃计数/冷却/自愈复用既有 plugin_health(§2.3),本函数只负责把
|
||
// 进程退出这一事实转成事件通知;具体重载策略由 agent 侧决定。
|
||
func (r *Registry) onProcCrash(name string, err error) {
|
||
log.Printf("[plugin] 子进程插件 %s 异常退出: %v(homed 未受影响)", name, err)
|
||
if r.evBus == nil {
|
||
return
|
||
}
|
||
// 不在此处直接重载:重载需要 registry 锁,而本回调可能在
|
||
// 持锁路径的 goroutine 中触发,直接调用会死锁。
|
||
r.evBus.Publish(&events.Event{
|
||
Type: events.EventSystem,
|
||
Source: "plugin",
|
||
Payload: map[string]interface{}{
|
||
"event": "plugin_crashed",
|
||
"plugin": name,
|
||
"error": err.Error(),
|
||
},
|
||
Timestamp: time.Now().Unix(),
|
||
})
|
||
}
|