plugin: 子进程通道接通 registry(proc 通道端到端可运行)

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
This commit is contained in:
JianFeeeee
2026-09-02 13:03:57 +08:00
parent 82dcc86173
commit 11c1bbcebb
9 changed files with 881 additions and 50 deletions

View File

@ -4,40 +4,128 @@ 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 加载子进程插件(plugin.bin)——外部插件多进程化的加载入口。
// 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
//
// 当前状态:**分派桩位**。共享内存数据面与锁仲裁已在 internal/plugin/proc/ 落地
// 并通过 16 项测试(含 -race),进程管理与 RPC 编解码为 Part 2 内容。
//
// 返回 nil,nil 表示目录中没有 plugin.bin(交由后续探测通道)。
// 找到二进制但通道未就绪时返回明确错误——不静默回退到 cabi,
// 否则"已迁移插件跑回旧通道"极难排查。
func tryLoadProc(dir, name string, config map[string]interface{}) (sdk.Plugin, error) {
// 实施计划: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, nil
return "", nil
}
return nil, fmt.Errorf("proc plugin %s: 检查 %s: %w", name, binEntry, err)
return "", fmt.Errorf("proc plugin %s: 检查 %s: %w", name, binEntry, err)
}
if st.IsDir() {
return nil, fmt.Errorf("proc plugin %s: %s 是目录,不是可执行文件", name, binEntry)
return "", fmt.Errorf("proc plugin %s: %s 是目录,不是可执行文件", name, binEntry)
}
if st.Mode()&0o111 == 0 {
return nil, fmt.Errorf("proc plugin %s: %s 缺少可执行权限(chmod +x)", name, binEntry)
// 常见于经 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
}
return nil, fmt.Errorf("proc plugin %s: 子进程通道尚未实现(Part 2)——"+
"共享内存数据面已就绪(internal/plugin/proc),"+
"如需运行请把 plugin.json 的 entry 改回 %s 走 C ABI 通道", name, soEntry)
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(),
})
}