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
202 lines
6.1 KiB
Go
202 lines
6.1 KiB
Go
package proc
|
||
|
||
import (
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
"sync"
|
||
|
||
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||
)
|
||
|
||
// Host 持有**被全部子进程插件共享的一块 StageContext 段**,是共享内存数据面的
|
||
// 所有权中心(§3.3/§3.4)。
|
||
//
|
||
// ❗ 为什么必须共享一块段(这是一个容易走错的关键点):
|
||
// 若每个插件各持一块段,则「内核 ctx → 段 → 插件改 → 回读 ctx」在多插件下退化成
|
||
// 副本模型——两个插件各写各的段、各自回读,最后回读者覆盖前者,
|
||
// lost update 原样复现(§8.4 实测 35.8~36.8%)。
|
||
// 实验 8 的做法是 5 个 worker 进程 mmap **同一个 memfd**,本实现与之一致。
|
||
//
|
||
// 生命周期:Host 由 registry 创建一次,随内核存活;每个插件 spawn 时经
|
||
// ExtraFiles 拿到同一 memfd(fd 3),mmap 后即看到同一份物理页。
|
||
type Host struct {
|
||
memfd *os.File
|
||
data []byte
|
||
seg *Segment
|
||
shmSize int
|
||
|
||
// locks 被全部插件的 coreHandler 共享——同阶段并发扇出的插件在此排队,
|
||
// 语义等价于内置插件共享 *StageContext 的 sync.RWMutex(§0.2 第 1 条)。
|
||
locks *lockRegistry
|
||
|
||
// stageMu 串行化「整次 stage 执行」对共享段的独占。
|
||
//
|
||
// 必要性:内核可能在不同路径并发触发 RunStage(如 emitResponse 的
|
||
// before_output 与主循环的其他阶段)。段只有一份,两次 stage 交叠会互相污染。
|
||
// 由首个进入的插件加锁、最后离开的插件解锁;RunStage 的 wg.Wait() 保证
|
||
// 每个 handler 的 defer 必然执行,故 inflight 必然归零,不会死锁。
|
||
stageMu sync.Mutex
|
||
|
||
coordMu sync.Mutex
|
||
coord *stageCoordinator
|
||
}
|
||
|
||
// NewHost 创建共享段(平台层 allocShm + 布局初始化)。
|
||
//
|
||
// 段的分配按平台分开(shmalloc_*.go):Linux 用 memfd,macOS 用
|
||
// 立即 unlink 的临时文件(无 memfd_create),其余平台明确报错。
|
||
// 两者语义一致:无文件名残留,fd 可经 ExtraFiles 传给子进程,
|
||
// 子进程 mmap 同一 inode——「全部插件共享一块段」的前提得以成立。
|
||
// 实验 2 已验证父子 mmap 到不同虚拟地址时相对偏移仍正确解引用。
|
||
func NewHost() (*Host, error) {
|
||
memfd, data, err := allocShm(shmDefaultSize)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
seg, err := NewSegment(data)
|
||
if err != nil {
|
||
freeShm(memfd, data)
|
||
return nil, err
|
||
}
|
||
|
||
return &Host{
|
||
memfd: memfd,
|
||
data: data,
|
||
seg: seg,
|
||
shmSize: shmDefaultSize,
|
||
locks: &lockRegistry{},
|
||
}, nil
|
||
}
|
||
|
||
// shmDefaultSize 是共享 StageContext 段的大小。
|
||
//
|
||
// 取 256KB:StageContext 全字段 JSON 化后典型 < 4KB(工具结果中位 93B,§2.5),
|
||
// append-only 中间垃圾由 stage 结束时 Compact 回收,256KB 给足余量。
|
||
// 全部插件共享一块,总开销恒定,不随插件数增长。
|
||
const shmDefaultSize = 256 * 1024
|
||
|
||
// Close 释放共享段。
|
||
func (h *Host) Close() error {
|
||
data, f := h.data, h.memfd
|
||
h.data, h.memfd = nil, nil
|
||
return freeShm(f, data)
|
||
}
|
||
|
||
// beginStage 由插件 handler 进入时调用。
|
||
//
|
||
// 首个进入者:获取 stageMu(独占共享段)→ 把内核 StageContext 写入段。
|
||
// 后续进入者:仅递增 inflight。
|
||
func (h *Host) beginStage(sc *pubsdk.StageContext) (*stageCoordinator, error) {
|
||
h.coordMu.Lock()
|
||
first := h.coord == nil
|
||
if first {
|
||
// 独占共享段直到本次 stage 全部插件离开
|
||
h.coordMu.Unlock()
|
||
h.stageMu.Lock()
|
||
h.coordMu.Lock()
|
||
// 双检:等锁期间可能已有其他插件建好协调器(它们会先拿到 stageMu)
|
||
if h.coord != nil {
|
||
first = false
|
||
h.stageMu.Unlock()
|
||
} else {
|
||
h.coord = newStageCoordinator(h.seg)
|
||
}
|
||
}
|
||
coord := h.coord
|
||
h.coordMu.Unlock()
|
||
|
||
if err := coord.enter(sc, first); err != nil {
|
||
if first {
|
||
h.coordMu.Lock()
|
||
h.coord = nil
|
||
h.coordMu.Unlock()
|
||
h.stageMu.Unlock()
|
||
}
|
||
return nil, err
|
||
}
|
||
h.locks.bind(coord.lock)
|
||
return coord, nil
|
||
}
|
||
|
||
// endStage 由插件 handler 返回时调用。
|
||
// 最后离开者:把共享段结果读回内核 StageContext → 压实 arena → 释放 stageMu。
|
||
func (h *Host) endStage(coord *stageCoordinator) error {
|
||
last, err := coord.leave()
|
||
if !last {
|
||
return err
|
||
}
|
||
h.coordMu.Lock()
|
||
h.coord = nil
|
||
h.coordMu.Unlock()
|
||
h.stageMu.Unlock()
|
||
return err
|
||
}
|
||
|
||
// ForceReleaseLock 在插件进程崩溃时释放其可能持有的 stage 锁(实验 9 自愈机制)。
|
||
func (h *Host) ForceReleaseLock(plugin string) bool {
|
||
return h.locks.forceRelease(plugin)
|
||
}
|
||
|
||
// Segment 暴露共享段(供诊断与测试)。
|
||
func (h *Host) Segment() *Segment { return h.seg }
|
||
|
||
// stageCoordinator 跟踪一次 stage 执行中参与插件的进出。
|
||
type stageCoordinator struct {
|
||
seg *Segment
|
||
lock *stageLock
|
||
|
||
mu sync.Mutex
|
||
inflight int
|
||
written bool
|
||
ctxRef *pubsdk.StageContext
|
||
}
|
||
|
||
func newStageCoordinator(seg *Segment) *stageCoordinator {
|
||
return &stageCoordinator{seg: seg, lock: newStageLock()}
|
||
}
|
||
|
||
// enter 登记一个插件进入本次 stage;first 为真时把内核状态写入共享段。
|
||
func (c *stageCoordinator) enter(sc *pubsdk.StageContext, first bool) error {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
c.inflight++
|
||
if !first || c.written {
|
||
return nil
|
||
}
|
||
c.ctxRef = sc
|
||
if err := c.seg.WriteAll(sc); err != nil {
|
||
c.inflight--
|
||
return fmt.Errorf("写入共享段: %w", err)
|
||
}
|
||
c.written = true
|
||
return nil
|
||
}
|
||
|
||
// leave 登记一个插件离开;返回是否为最后一个离开者。
|
||
//
|
||
// 最后离开者负责把共享段结果读回内核 StageContext,并压实 arena
|
||
// (此时无插件持锁,满足 §3.3 的压实前提)。
|
||
func (c *stageCoordinator) leave() (last bool, err error) {
|
||
c.mu.Lock()
|
||
c.inflight--
|
||
last = c.inflight == 0
|
||
sc := c.ctxRef
|
||
written := c.written
|
||
c.mu.Unlock()
|
||
|
||
if !last || !written || sc == nil {
|
||
return last, nil
|
||
}
|
||
if rErr := c.seg.ReadInto(sc); rErr != nil {
|
||
return last, fmt.Errorf("回读共享段: %w", rErr)
|
||
}
|
||
if reclaimed := c.seg.Compact(); reclaimed > 0 {
|
||
log.Printf("[proc] stage 结束,arena 压实回收 %d 字节", reclaimed)
|
||
}
|
||
return last, nil
|
||
}
|
||
|
||
// ShmSize 返回共享段大小(供诊断/日志)。
|
||
func (h *Host) ShmSize() int { return h.shmSize }
|