Files
HomeAgent/internal/plugin/proc/testdata/appendplugin.go
JianFeeeee ceb8a7b165 refactor(proc)!: 版本身份只由版本号本身承载,删除全部自定版本号
内核侧配合 SDK 仓 b2872f3:兼容性不再依赖任何自造版本号。

删除三处自造号:
  - ProtocolVersion(协议号)→ 改为比对主版本段(MajorVersion)
  - shmVersion(共享段 header)→ 改为校验描述符个数(field_count)
  - unifiedVersion(SuperBlock)→ 结构自描述,不再校验

握手契约随之调整:
  HandshakeParams:  protocol(int) → core_version(string)
  HandshakeResult:  protocol(int) → sdk_version 承担判据
  内核比对 MajorVersion(meta.Version) == MajorVersion(res.SDKVersion),
  跨主版本即拒并提示重编(主版本跃迁 = 破坏性变更的定义)。

为什么废除:额外的号需要人去记得升,而它没有任何结构性约束兜底,
必然腐烂。实证:ProtocolVersion 常量在多次布局变更里只升过一次,
对应的守卫测试长期失败无人察觉(期望 18/1,实际 19/2)。

新增判据:
  - TestProcess_MajorVersionMismatchRejected:跨主版本被拒(testdata/badproto)
  - TestProcess_SameMajorDifferentMinorAccepted:同主版本放行(新增 testdata/oldminor)
    后者是 §七「1.x 内纯追加、不调新面不用重编」规约的落地 —— 若误用全等
    比较,未重编的存量插件会被无谓地全部拒绝。
  两条均经变异验证:改成全等比较 → 前者过/后者挂;删掉校验 → 前者挂。
2026-10-13 09:39:34 +08:00

236 lines
5.4 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
// 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
}
// 统一区域:前 64B 是 SuperBlock,StageContext 段在其后
ctxOff := binary.LittleEndian.Uint32(m[20:])
ctxSize := binary.LittleEndian.Uint32(m[24:])
shm = m[ctxOff : ctxOff+ctxSize]
}
send(response{ID: req.ID, Result: map[string]interface{}{
"sdk_version": "1.0.0",
"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})
}
}
}
}