Files
HomeAgent/internal/plugin/proc/testdata/callbackplugin.go
dev d62430a71b feat(proc): 子进程通道 —— RPC 协议 + 进程管理(Part 2 核心)
协议面(protocol.go,§3.2 method id 平移为 method 名):
- NDJSON 帧,双向复用同一对 stdio;ID>0 需应答,ID==0 为通知(post-and-forget)
- 51 个 C ABI method id 全部平移为可读 method 名并标注原编号对照
  编号本身扔掉——加能力不用改两边常量表,不再有 47 夹在 7 和 8 之间的痕迹
- case 25(CORE_FREE_STRING) 无对应 method:进程模型下各自 GC,概念消失
- case 23/24(事件订阅) 与 io.setToolBlocks 今日均为空实现「给不了」,
  子进程下首次真正可给(§3.8 能力对齐)
- 新增 stage.lock/stage.unlock(C ABI 下不存在跨进程锁概念)
- StageInvokeParams 不含 StageContext 数据本身——数据在共享段,只带 stage 名 + seq

进程面(process.go,§2.3 保留现有生命周期机制):
- Spawn: 启动 + 握手(协议版本不匹配显式拒绝,不半兼容运行)
- readLoop: NDJSON 分派应答/插件反向请求,1MB 单帧上限(大 payload 走 arena)
- CallContext: ctx 取消时立即返回**且清理 pending 条目**
  对比 cgo:超时只让调用方返回,goroutine 永久卡在 C 调用里(现网泄漏 26 次)
- Notify: ID=0 不占 pending 表,满足约束 B(流式逐 token 发布不得等待消费者)
- markExited: EOF/退出 → 唤醒全部在途调用 → onExit 回调
  这是「把 panic 捕获换成进程退出检测」的落点,plugin_health 逻辑完全复用
- Stop: plugin.stop → 宽限期 → 超时 Kill;Kill 后 OS 回收全部资源,零泄漏
- serveRequest 带 panic 隔离:内核 handler panic 不带崩 readLoop

验证(10 项,真实子进程而非 mock,含 -race):
- 握手/工具调用/错误上报(插件失败调用方收到 error,非假成功)
- 插件反向调用内核(tool.register + settings.get 双向往返)
- **崩溃隔离**:插件 panic → 子进程 exit 2,内核存活、收到 onExit、在途调用不挂死
- 优雅停止 / **Kill 卡死插件**(ctx 超时返回 + pending 清零 + 资源回收)
- 通知不等应答(100 条 < 1s)/ 50 并发调用应答不串 / 协议版本不匹配拒绝

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

121 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.

//go:build ignore
// callbackplugin 验证插件 → 内核的反向调用51 个 core.* method 的机制)。
package main
import (
"bufio"
"encoding/json"
"os"
"sync"
)
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
pending = map[uint64]chan json.RawMessage{}
pendMu sync.Mutex
)
func send(v interface{}) {
b, _ := json.Marshal(v)
writeMu.Lock()
out.Write(b)
out.WriteByte('\n')
out.Flush()
writeMu.Unlock()
}
// callKernel 反向调用内核并等待应答。
func callKernel(method string, params interface{}) json.RawMessage {
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})
return <-ch
}
func main() {
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 err := json.Unmarshal(line, &probe); err != 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":
send(response{ID: req.ID, Result: map[string]interface{}{
"protocol": 1,
"sdk_version": "test",
"plugin_name": "cb",
"pid": os.Getpid(),
}})
case "plugin.start":
// 在独立 goroutine 里回调,避免阻塞读循环
go func(id uint64) {
callKernel("tool.register", map[string]interface{}{"name": "cb_tool"})
callKernel("settings.get", map[string]interface{}{"key": "some_key"})
send(response{ID: id})
}(req.ID)
case "plugin.stop":
send(response{ID: req.ID})
out.Flush()
os.Exit(0)
default:
if req.ID != 0 {
send(response{ID: req.ID})
}
}
}
}