Files
HomeAgent/internal/plugin/proc/host.go
dev 82dcc86173 feat(proc): Plugin 加载器 + 51 method handler + RunStage 接共享段(Part 2 完成 / Part 4 闭环)
corehandler.go —— cabi/loader.go 51 个 case 体的整块平移(§3.2):
- 参数从「s1/s2/s3 + i1/i2 五个固定槽」改为结构化 JSON,语义不变
- CoreSDK 接口刻意只含外部插件应得能力:无 Selftest/Supervisor/Tracker/
  Status/Adapter/Config/Tool/Indexer/OutputChan/Publish
  → 权限梯度从「C ABI 表达能力的意外产物」变成「显式声明并强制的策略」(§3.8)
- 事件订阅(case 23/24)与 SetToolBlocks 明确返回未实现,不再像 C ABI 那样静默成功
  (静默成功后收不到事件比报错更难排查)
- ToolDef.Cleaner / ChannelDef.Cleaner 是函数,跨进程置 nil(§3.5 回调型资源)

host.go —— 共享段所有权中心:
-  全部子进程插件共享**同一块 memfd**。若每插件一段,
  「内核 ctx → 段 → 插件改 → 回读 ctx」在多插件下退化成副本模型,
  最后回读者覆盖前者,§8.4 的 35.8~36.8% lost update 原样复现
- stageMu 串行化整次 stage 对段的独占(内核可能并发触发 RunStage)
- 首个进入者写入段,最后离开者回读 + Compact(此时无插件持锁,满足 §3.3 前提)

stage.go —— RunStage 接线(风险 3.4 落点):
- 并发扇出保留(§0.2 第 1 条:并发扇出是原始设计,不是缺陷)
- 插件失败时 ForceReleaseLock,避免后续插件死锁(实验 9,无需 robust mutex)

plugin.go —— registry 可加载的插件实体:
- Start: spawn(fd 3 传共享段)→ 握手 → plugin.init → plugin.start
- Close: **真 kill + wait**,对比 cabi 的 Close 只做 dlclose 而后者是 no-op(§1.1)
- invokeOutput **同步等真实结果**,失败上报 error —— §9.4 根治

验证(34 项测试全绿,含 -race,全部用真实子进程):
- 单插件 stage 读改写经共享段回到内核 StageContext
- sanitizer(改写) + weather(只读) 并发:清洗结果不被覆盖(现网场景)
- **5 个独立进程并发 append 同一 FinalText:5 个标记全部保留,零丢失零撕裂**
  (实验 8 在真实 RPC + 真实 RunStage 下的复刻)
- 工具注册可调用 / 输出通道真实失败上报 / start 期间反向调用
- 未知 method 与未实现能力被拒绝 / stage 外加锁被拒绝

接口冻结: git diff third_party/homeagent-sdk/sdk/ 为空
2026-09-02 11:34:33 +08:00

216 lines
6.3 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 proc
import (
"fmt"
"log"
"os"
"sync"
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
"golang.org/x/sys/unix"
)
// 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 拿到同一 memfdfd 3mmap 后即看到同一份物理页。
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 创建共享段memfd + mmap + 布局初始化)。
//
// 用 memfd 而非 /dev/shm 文件:无需文件名、不残留(进程退出即回收)、
// 可经 ExtraFiles 传给子进程。实验 2 已验证父子 mmap 到不同虚拟地址时
// 相对偏移仍正确解引用。
func NewHost() (*Host, error) {
fd, err := unix.MemfdCreate("hastagectx", unix.MFD_CLOEXEC)
if err != nil {
return nil, fmt.Errorf("proc: 创建共享段 memfd: %w", err)
}
if err := unix.Ftruncate(fd, int64(shmDefaultSize)); err != nil {
unix.Close(fd)
return nil, fmt.Errorf("proc: 共享段 ftruncate: %w", err)
}
data, err := unix.Mmap(fd, 0, shmDefaultSize,
unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
if err != nil {
unix.Close(fd)
return nil, fmt.Errorf("proc: 共享段 mmap: %w", err)
}
seg, err := NewSegment(data)
if err != nil {
unix.Munmap(data)
unix.Close(fd)
return nil, err
}
return &Host{
memfd: os.NewFile(uintptr(fd), "hastagectx"),
data: data,
seg: seg,
shmSize: shmDefaultSize,
locks: &lockRegistry{},
}, nil
}
// shmDefaultSize 是共享 StageContext 段的大小。
//
// 取 256KBStageContext 全字段 JSON 化后典型 < 4KB工具结果中位 93B§2.5
// append-only 中间垃圾由 stage 结束时 Compact 回收256KB 给足余量。
// 全部插件共享一块,总开销恒定,不随插件数增长。
const shmDefaultSize = 256 * 1024
// Close 释放共享段。
func (h *Host) Close() error {
if h.data != nil {
unix.Munmap(h.data)
h.data = nil
}
if h.memfd != nil {
err := h.memfd.Close()
h.memfd = nil
return err
}
return nil
}
// 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 登记一个插件进入本次 stagefirst 为真时把内核状态写入共享段。
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
}