mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 09:58:06 +00:00
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/ 为空
This commit is contained in:
232
internal/plugin/proc/testdata/appendplugin.go
vendored
Normal file
232
internal/plugin/proc/testdata/appendplugin.go
vendored
Normal file
@ -0,0 +1,232 @@
|
||||
//go:build ignore
|
||||
|
||||
// appendplugin 在 stage 中把自己的标记追加到 FinalText(读-改-写)。
|
||||
//
|
||||
// 用于跨进程 lost update 验证:多个此类插件并发处理同一 stage,
|
||||
// 若全部标记都保留 ⇒ 无丢失;若少了 ⇒ 出现 lost update。
|
||||
//
|
||||
// 这是实验 8(5 进程 × 300 轮零丢失)在真实 RPC + 真实内核 RunStage
|
||||
// 下的复刻——机制单测已过,这里验证集成后同样成立。
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type request struct {
|
||||
ID uint64 `json:"id,omitempty"`
|
||||
Method string `json:"method"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type response struct {
|
||||
ID uint64 `json:"id"`
|
||||
Result interface{} `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
out = bufio.NewWriter(os.Stdout)
|
||||
writeMu sync.Mutex
|
||||
|
||||
nextID uint64
|
||||
pendMu sync.Mutex
|
||||
pending = map[uint64]chan json.RawMessage{}
|
||||
|
||||
shm []byte
|
||||
tag string
|
||||
)
|
||||
|
||||
func send(v interface{}) {
|
||||
b, _ := json.Marshal(v)
|
||||
writeMu.Lock()
|
||||
out.Write(b)
|
||||
out.WriteByte('\n')
|
||||
out.Flush()
|
||||
writeMu.Unlock()
|
||||
}
|
||||
|
||||
func callKernel(method string, params interface{}) (json.RawMessage, bool) {
|
||||
pendMu.Lock()
|
||||
nextID++
|
||||
id := nextID
|
||||
ch := make(chan json.RawMessage, 1)
|
||||
pending[id] = ch
|
||||
pendMu.Unlock()
|
||||
|
||||
var raw json.RawMessage
|
||||
if params != nil {
|
||||
b, _ := json.Marshal(params)
|
||||
raw = b
|
||||
}
|
||||
send(request{ID: id, Method: method, Params: raw})
|
||||
r, ok := <-ch
|
||||
return r, ok
|
||||
}
|
||||
|
||||
const (
|
||||
headerSize = 64
|
||||
stageFieldCount = 18
|
||||
sliceSize = 8
|
||||
flagCount = 8
|
||||
|
||||
offArenaBase = 8
|
||||
offArenaCap = 12
|
||||
offArenaUsed = 16
|
||||
offCtxBase = 20
|
||||
offSeq = 24
|
||||
|
||||
fFinalText = 5 // 与 proc/shmcodec.go 的 stageField 枚举顺序一致
|
||||
)
|
||||
|
||||
func desc(field int) (uint32, uint32) {
|
||||
cb := binary.LittleEndian.Uint32(shm[offCtxBase:])
|
||||
o := cb + uint32(field*sliceSize)
|
||||
return binary.LittleEndian.Uint32(shm[o:]), binary.LittleEndian.Uint32(shm[o+4:])
|
||||
}
|
||||
|
||||
func setDesc(field int, off, ln uint32) {
|
||||
cb := binary.LittleEndian.Uint32(shm[offCtxBase:])
|
||||
o := cb + uint32(field*sliceSize)
|
||||
binary.LittleEndian.PutUint32(shm[o:], off)
|
||||
binary.LittleEndian.PutUint32(shm[o+4:], ln)
|
||||
}
|
||||
|
||||
func readFinalText() string {
|
||||
off, ln := desc(fFinalText)
|
||||
if off == 0 && ln == 0 {
|
||||
return ""
|
||||
}
|
||||
if ln == 0 {
|
||||
return ""
|
||||
}
|
||||
base := binary.LittleEndian.Uint32(shm[offArenaBase:])
|
||||
return string(shm[base+off : base+off+ln])
|
||||
}
|
||||
|
||||
func writeFinalText(s string) error {
|
||||
used := binary.LittleEndian.Uint32(shm[offArenaUsed:])
|
||||
if used == 0 {
|
||||
used = 1
|
||||
}
|
||||
cap_ := binary.LittleEndian.Uint32(shm[offArenaCap:])
|
||||
if used+uint32(len(s)) > cap_ {
|
||||
return fmt.Errorf("arena 空间不足")
|
||||
}
|
||||
base := binary.LittleEndian.Uint32(shm[offArenaBase:])
|
||||
copy(shm[base+used:], []byte(s))
|
||||
binary.LittleEndian.PutUint32(shm[offArenaUsed:], used+uint32(len(s)))
|
||||
setDesc(fFinalText, used, uint32(len(s)))
|
||||
// 世代号自增
|
||||
v := binary.LittleEndian.Uint64(shm[offSeq:])
|
||||
binary.LittleEndian.PutUint64(shm[offSeq:], v+1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
tag = os.Getenv("PLUGIN_TAG")
|
||||
if tag == "" {
|
||||
tag = "?"
|
||||
}
|
||||
|
||||
in := bufio.NewScanner(bufio.NewReader(os.Stdin))
|
||||
in.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
|
||||
for in.Scan() {
|
||||
line := make([]byte, len(in.Bytes()))
|
||||
copy(line, in.Bytes())
|
||||
|
||||
var probe struct {
|
||||
ID uint64 `json:"id"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
if json.Unmarshal(line, &probe) != nil {
|
||||
continue
|
||||
}
|
||||
if probe.Method == "" {
|
||||
var resp struct {
|
||||
ID uint64 `json:"id"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
}
|
||||
json.Unmarshal(line, &resp)
|
||||
pendMu.Lock()
|
||||
ch, ok := pending[resp.ID]
|
||||
delete(pending, resp.ID)
|
||||
pendMu.Unlock()
|
||||
if ok {
|
||||
ch <- resp.Result
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var req request
|
||||
json.Unmarshal(line, &req)
|
||||
|
||||
switch req.Method {
|
||||
case "handshake":
|
||||
var hp struct {
|
||||
ShmSize int `json:"shm_size"`
|
||||
}
|
||||
json.Unmarshal(req.Params, &hp)
|
||||
if hp.ShmSize > 0 {
|
||||
m, err := syscall.Mmap(3, 0, hp.ShmSize,
|
||||
syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
|
||||
if err != nil {
|
||||
send(response{ID: req.ID, Error: fmt.Sprintf("mmap: %v", err)})
|
||||
continue
|
||||
}
|
||||
shm = m
|
||||
}
|
||||
send(response{ID: req.ID, Result: map[string]interface{}{
|
||||
"protocol": 1, "sdk_version": "test",
|
||||
"plugin_name": "append-" + tag, "pid": os.Getpid(),
|
||||
}})
|
||||
|
||||
case "plugin.init":
|
||||
send(response{ID: req.ID})
|
||||
|
||||
case "plugin.start":
|
||||
go func(id uint64) {
|
||||
callKernel("stage.register", map[string]interface{}{
|
||||
"stage": "after_toolcall",
|
||||
"scope": "global",
|
||||
})
|
||||
send(response{ID: id})
|
||||
}(req.ID)
|
||||
|
||||
case "plugin.stop":
|
||||
send(response{ID: req.ID})
|
||||
out.Flush()
|
||||
os.Exit(0)
|
||||
|
||||
case "stage.invoke":
|
||||
go func(id uint64) {
|
||||
if shm == nil {
|
||||
send(response{ID: id, Error: "共享段未挂载"})
|
||||
return
|
||||
}
|
||||
// 拿锁 → 读 → 追加自己的标记 → 写回 → 放锁
|
||||
callKernel("stage.lock", nil)
|
||||
cur := readFinalText()
|
||||
err := writeFinalText(cur + tag)
|
||||
callKernel("stage.unlock", nil)
|
||||
if err != nil {
|
||||
send(response{ID: id, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
send(response{ID: id, Result: map[string]interface{}{"dirty_fields": 1}})
|
||||
}(req.ID)
|
||||
|
||||
default:
|
||||
if req.ID != 0 {
|
||||
send(response{ID: req.ID})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user