mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-23 10:28:06 +00:00
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/ 为空
This commit is contained in:
52
internal/plugin/proc/testdata/badprotoplugin.go
vendored
Normal file
52
internal/plugin/proc/testdata/badprotoplugin.go
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
//go:build ignore
|
||||
|
||||
// badprotoplugin 上报错误的协议版本,验证内核显式拒绝而非半兼容运行。
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
in := bufio.NewScanner(bufio.NewReader(os.Stdin))
|
||||
out := bufio.NewWriter(os.Stdout)
|
||||
send := func(v interface{}) {
|
||||
b, _ := json.Marshal(v)
|
||||
out.Write(b)
|
||||
out.WriteByte('\n')
|
||||
out.Flush()
|
||||
}
|
||||
|
||||
for in.Scan() {
|
||||
var req request
|
||||
if err := json.Unmarshal(in.Bytes(), &req); err != nil {
|
||||
continue
|
||||
}
|
||||
if req.Method == "handshake" {
|
||||
send(response{ID: req.ID, Result: map[string]interface{}{
|
||||
"protocol": 999, // 故意不匹配
|
||||
"sdk_version": "ancient",
|
||||
"plugin_name": "badproto",
|
||||
"pid": os.Getpid(),
|
||||
}})
|
||||
continue
|
||||
}
|
||||
if req.ID != 0 {
|
||||
send(response{ID: req.ID})
|
||||
}
|
||||
}
|
||||
}
|
||||
120
internal/plugin/proc/testdata/callbackplugin.go
vendored
Normal file
120
internal/plugin/proc/testdata/callbackplugin.go
vendored
Normal file
@ -0,0 +1,120 @@
|
||||
//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})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
54
internal/plugin/proc/testdata/crashplugin.go
vendored
Normal file
54
internal/plugin/proc/testdata/crashplugin.go
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
//go:build ignore
|
||||
|
||||
// crashplugin 在收到 boom 工具调用时 panic,用于验证崩溃隔离:
|
||||
// 子进程死亡不应带崩 homed,且内核须能感知退出(供 recordCrash 使用)。
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
in := bufio.NewScanner(bufio.NewReader(os.Stdin))
|
||||
out := bufio.NewWriter(os.Stdout)
|
||||
send := func(v interface{}) {
|
||||
b, _ := json.Marshal(v)
|
||||
out.Write(b)
|
||||
out.WriteByte('\n')
|
||||
out.Flush()
|
||||
}
|
||||
|
||||
for in.Scan() {
|
||||
var req request
|
||||
if err := json.Unmarshal(in.Bytes(), &req); err != nil {
|
||||
continue
|
||||
}
|
||||
switch req.Method {
|
||||
case "handshake":
|
||||
send(response{ID: req.ID, Result: map[string]interface{}{
|
||||
"protocol": 1, "sdk_version": "test", "plugin_name": "crash", "pid": os.Getpid(),
|
||||
}})
|
||||
case "tool.invoke":
|
||||
// 模拟插件 bug:直接 panic,进程带非零码退出
|
||||
panic("插件内部 panic:用于验证崩溃隔离")
|
||||
default:
|
||||
if req.ID != 0 {
|
||||
send(response{ID: req.ID})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
76
internal/plugin/proc/testdata/echoplugin.go
vendored
Normal file
76
internal/plugin/proc/testdata/echoplugin.go
vendored
Normal file
@ -0,0 +1,76 @@
|
||||
//go:build ignore
|
||||
|
||||
// echoplugin 是测试用的最简子进程插件:实现握手 + 回显工具。
|
||||
// 不 import 公开 SDK——只验证 proc 包的 RPC 机制本身。
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
in := bufio.NewScanner(bufio.NewReader(os.Stdin))
|
||||
in.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
out := bufio.NewWriter(os.Stdout)
|
||||
|
||||
send := func(v interface{}) {
|
||||
b, _ := json.Marshal(v)
|
||||
out.Write(b)
|
||||
out.WriteByte('\n')
|
||||
out.Flush()
|
||||
}
|
||||
|
||||
for in.Scan() {
|
||||
var req request
|
||||
if err := json.Unmarshal(in.Bytes(), &req); err != nil {
|
||||
continue
|
||||
}
|
||||
switch req.Method {
|
||||
case "handshake":
|
||||
send(response{ID: req.ID, Result: map[string]interface{}{
|
||||
"protocol": 1,
|
||||
"sdk_version": "test",
|
||||
"plugin_name": "echo",
|
||||
"pid": os.Getpid(),
|
||||
}})
|
||||
case "plugin.stop":
|
||||
send(response{ID: req.ID})
|
||||
out.Flush()
|
||||
os.Exit(0)
|
||||
case "tool.invoke":
|
||||
var p struct {
|
||||
Name string `json:"name"`
|
||||
Args map[string]interface{} `json:"args"`
|
||||
}
|
||||
json.Unmarshal(req.Params, &p)
|
||||
switch p.Name {
|
||||
case "fail_tool":
|
||||
send(response{ID: req.ID, Error: "故意失败:用于验证错误上报"})
|
||||
default:
|
||||
text, _ := p.Args["text"].(string)
|
||||
send(response{ID: req.ID, Result: map[string]interface{}{"result": text}})
|
||||
}
|
||||
case "event.deliver":
|
||||
// 通知:不回应答
|
||||
default:
|
||||
if req.ID != 0 {
|
||||
send(response{ID: req.ID, Error: fmt.Sprintf("未实现 %s", req.Method)})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
56
internal/plugin/proc/testdata/hangplugin.go
vendored
Normal file
56
internal/plugin/proc/testdata/hangplugin.go
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
//go:build ignore
|
||||
|
||||
// hangplugin 收到工具调用后永久阻塞,用于验证:
|
||||
// 1. 调用方能凭 context 超时返回(不被拖死)
|
||||
// 2. Kill 能真正回收资源(对比 cgo 超时后 OS 线程永久泄漏,§9.3)
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
in := bufio.NewScanner(bufio.NewReader(os.Stdin))
|
||||
out := bufio.NewWriter(os.Stdout)
|
||||
send := func(v interface{}) {
|
||||
b, _ := json.Marshal(v)
|
||||
out.Write(b)
|
||||
out.WriteByte('\n')
|
||||
out.Flush()
|
||||
}
|
||||
|
||||
for in.Scan() {
|
||||
var req request
|
||||
if err := json.Unmarshal(in.Bytes(), &req); err != nil {
|
||||
continue
|
||||
}
|
||||
switch req.Method {
|
||||
case "handshake":
|
||||
send(response{ID: req.ID, Result: map[string]interface{}{
|
||||
"protocol": 1, "sdk_version": "test", "plugin_name": "hang", "pid": os.Getpid(),
|
||||
}})
|
||||
case "tool.invoke":
|
||||
// 永久卡住,永不回应答
|
||||
time.Sleep(10 * time.Minute)
|
||||
default:
|
||||
if req.ID != 0 {
|
||||
send(response{ID: req.ID})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user