Files
HomeAgent/internal/plugin/dynamic.go
dev 610e9d0bbb feat(plugin): entry 双通道分派 + 共享内存 stage 数据面(Part 1 + Part 4 核心)
Part 1 加载分派骨架(迁移可逐插件推进、随时回退的前提):
- dynamic.go: 新增 binEntry/skillEntry 常量 + entryKind 枚举 + classifyEntry/detectEntryKind
  manifest entry 优先级最高(改回 plugin.so 即回退 cabi);无 manifest 时按目录探测,.bin 优先
- registry.go: tryDynamic 按 entry 分派 proc/cabi 双通道;
  entry 声明 .bin 但二进制缺失时报明确错误,不静默回退(否则'已迁移插件跑回旧通道'极难排查)
- registry.go: pluginEntryHash 候选顺序与 detectEntryKind 对齐(.bin 优先),
  否则增量重载会用错文件算 hash
- dynamic_proc_{unix,windows}.go: tryLoadProc 桩位(权限/类型校验已实现,进程管理属 Part 2)

Part 4 共享内存数据面(迁移评估 §3.3/§3.4/§3.7,最关键一环):
- proc/shm.go: 段布局(Header + ShmStageCtx 描述符数组 + append-only arena)
  相对偏移设计——各进程 mmap 到不同虚拟地址仍能正确解引用
  arena 用尽显式报错而非静默截断(§4.4 风险登记);Compact() 回收 append-only 垃圾
- proc/shmcodec.go: StageContext 16 字段跨进程编解码
  字段级描述符消除 lost update:只改 FinalText 的插件不触碰 ToolResults 描述符
  WriteDirty 只写脏字段——只读插件零写入,不可能覆盖他人改写
  Snapshot 存序列化字符串(切片共享底层数组的坑,C ABI 侧修 11.3 时已踩过)
  Extra 4 键提升为具名字段;Response 用标志位表达 nil vs 空串
- proc/lock.go: 锁仲裁回归内核(§3.7 已裁定,零 cgo)
  ForceRelease 实现实验 9 的崩溃自愈——排除 robust pthread_mutex 必要性
  重复加锁显式拒绝(否则死锁 30s);等待超时有补偿 goroutine 防锁泄漏

验证:
- proc 包 16 项测试全绿(含 -race):全字段往返/只读零写回/原地改切片识别/
  现网 sanitizer+weather 场景/5插件×40轮并发零丢失/arena 耗尽报错/压实不破坏字段/
  锁互斥·串扰拒绝·崩溃自愈·临界区串行化
- entry 分派 9 项测试全绿;go build ./... exit 0;接口冻结 git diff sdk/ 为空
2026-09-02 10:41:18 +08:00

101 lines
2.5 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 plugin
import (
"encoding/json"
"os"
"path/filepath"
)
const (
soEntry = "plugin.so"
dllEntry = "plugin.dll"
binEntry = "plugin.bin" // 子进程插件(纯 Go 二进制stdio JSON-RPC
luaEntry = "main.lua"
skillEntry = "SKILL.md"
metaEntry = "plugin.json"
)
// entryKind 描述插件入口归属的加载通道。
// 外部插件多进程化期间 .so/.dllcabi与 .binproc**双通道共存**
// 按 plugin.json 的 entry 字段分派,使迁移可逐插件推进、随时回退。
type entryKind int
const (
entryUnknown entryKind = iota
entryCABI // plugin.so / plugin.dll / plugin.dylib —— C ABI 动态库
entryProc // plugin.bin —— 子进程 + stdio JSON-RPC
entryLua // main.lua
entrySkill // SKILL.md
)
func (k entryKind) String() string {
switch k {
case entryCABI:
return "cabi"
case entryProc:
return "proc"
case entryLua:
return "lua"
case entrySkill:
return "skill"
}
return "unknown"
}
// classifyEntry 把 manifest 的 entry 字段映射到加载通道。
// entry 为空时返回 entryUnknown由调用方回退到目录探测兼容无 manifest 的旧插件)。
func classifyEntry(entry string) entryKind {
switch entry {
case soEntry, dllEntry, "plugin.dylib":
return entryCABI
case binEntry:
return entryProc
case luaEntry:
return entryLua
case skillEntry:
return entrySkill
}
return entryUnknown
}
// detectEntryKind 先读 manifest 的 entry读不到则按目录内存在的入口文件推断。
// 推断顺序:.bin 优先于 .so——迁移期间同一插件目录可能两个产物共存升级未清理
// 此时应走新通道manifest 显式声明优先级最高。
func detectEntryKind(plgDir string) entryKind {
if mft := readManifest(plgDir); mft != nil {
if k := classifyEntry(mft.Entry); k != entryUnknown {
return k
}
}
for _, probe := range []struct {
file string
kind entryKind
}{
{binEntry, entryProc},
{soEntry, entryCABI},
{"plugin.dylib", entryCABI},
{dllEntry, entryCABI},
{luaEntry, entryLua},
{skillEntry, entrySkill},
} {
if st, err := os.Stat(filepath.Join(plgDir, probe.file)); err == nil && !st.IsDir() {
return probe.kind
}
}
return entryUnknown
}
func readManifest(dir string) *PluginManifest {
data, err := os.ReadFile(filepath.Join(dir, metaEntry))
if err != nil {
return nil
}
var m PluginManifest
if err := json.Unmarshal(data, &m); err != nil {
return nil
}
return &m
}
var _ = json.Marshal