diff --git a/README.md b/README.md index 9cf2b9f..bbc15c5 100644 --- a/README.md +++ b/README.md @@ -32,14 +32,14 @@ HomeAgent 插件开发 SDK,用于构建与 HomeAgent 平台交互的智能插 **1.0.x 插件升到 1.1.x:不需要改代码,也不需要重编。** 1.1.0 的新增全部是 「插件调用、内核实现」方向,不调就不受影响(已用 SDK 0.9.2 编的旧 `plugin.bin` -实测验证:在新内核上直接建链通过,因为握手校验的是 `ProtocolVersion`、不是 SDK 版本)。 +实测验证:在新内核上直接建链通过,因为握手校验的是主版本号、不是 SDK 全等版本)。 想用新字段时重编即可。 -**1.1.x 插件升到 1.2.x:接口纯追加,但必须重编。** 公开接口没有签名变更(新增 -`InjectOptions` 与六个 `*Opts` 变体、`ChannelDef.ContextPolicy`),不调新能力就不受影响; -但内核的**插件运行协议升到了 2**(统一共享内存区的 fd3 布局改变,**不支持滚动升级**), -所以 `plugin.bin` 必须用配套的 `hmapdev` 重编后与内核**同批**安装——否则握手时协议版本 -不匹配会被拒绝(错误信息会明确提示用配套 hmapdev 重编,不会静默降级)。 +**1.1.x 插件升到 1.2.x:接口纯追加;若内核共享区布局改了则需重编。** 公开接口没有签名变更 +(新增 `InjectOptions` 与六个 `*Opts` 变体、`ChannelDef.ContextPolicy`),不调新能力就不受影响。 +1.2.0 同时把 fd3 换成了统一共享区(SuperBlock + StageContext + EvtRing), +这类**布局**变化由握手时的结构判据拦下(统一区魔数、`field_count` 描述符个数), +而不是靠某个版本号——它们都是二进制里的派生事实,不会「忘同步」。 ## 1.3.0 新增:注入优先级与动态输出通道 @@ -624,9 +624,9 @@ cd tools/vscode-hmapdev && npm install && npm run compile # 然后在 VSCode | [sanitizer](example/sanitizer) | Go | 内容清洗/安全过滤 | **发版时附带预编译示例产物**:SDK 的 release 除 5 平台 `hmapdev` 外,还包含各示例插件的 -`.hmap` 与 `SHA256SUMS`/`MANIFEST.txt`。原因是插件二进制与内核**协议绑定**(`ProtocolVersion` -+ 共享内存区魔数),只发工具链不发示例产物,很容易拿旧产物去装而握手失败——那看起来像 -「插件坏了」而不是「版本不配套」。 +`.hmap` 与 `SHA256SUMS`/`MANIFEST.txt`。原因是插件二进制与内核在**布局上绑定**(主版本号 ++ 统一共享区魔数 + `field_count`),只发工具链不发示例产物,很容易拿旧产物去装而握手失败 +——那看起来像「插件坏了」而不是「版本不配套」。 ## Remote Device SDK diff --git a/README_EN.md b/README_EN.md index 53f0f81..7ed45c5 100644 --- a/README_EN.md +++ b/README_EN.md @@ -52,7 +52,7 @@ has been withdrawn — any SDK tag with a non-zero patch position is wrong.) **Upgrading a 1.0.x plugin to 1.1.x: no code changes, no rebuild.** Everything added in 1.1.0 is in the "plugin calls, kernel implements" direction, so not calling it means not being affected (verified with an old `plugin.bin` built against SDK 0.9.2: it handshakes fine on the new kernel, -because the handshake validates `ProtocolVersion`, not the SDK version). Rebuild only when you want +because the handshake validates the **major** version, not the exact SDK version). Rebuild only when you want the new fields. **Upgrading a 1.1.x plugin to 1.2.x: the interface is purely additive, but a rebuild is required.** @@ -526,8 +526,8 @@ Internal plugins (platform built-in) have full SDK access including SocialAPI wr **Prebuilt example artifacts ship with every release**: besides the 5-platform `hmapdev`, an SDK release contains the example plugins' `.hmap` files plus `SHA256SUMS`/`MANIFEST.txt`. The reason is -that plugin binaries are **protocol-bound** to the kernel (`ProtocolVersion` + the shared-memory -magic), so shipping the toolchain without matching artifacts invites installing an old artifact — +that plugin binaries are **layout-bound** to the kernel (major version + unified-region magic + +`field_count`), so shipping the toolchain without matching artifacts invites installing an old artifact — which fails the handshake and looks like "the plugin is broken" rather than "the versions don't match". diff --git a/docs/versions.md b/docs/versions.md index b18bc8b..8308b18 100644 --- a/docs/versions.md +++ b/docs/versions.md @@ -32,17 +32,33 @@ 想用新字段的重编即可。 !!! tip "什么时候必须重编" - 只有两种情况:① 内核跨了中版本(如 1.1 → 1.2)且你用了新 API; - ② 内核的 RPC 协议版本变了(`.hmap` 里的 `protocol` 字段与内核不匹配)。 - 后者的错配**不会静默失效** —— 握手时会显式拦下。 + 只有一种情况:**内核跨了主版本**(如 1.x → 2.x)。 + 主版本跃迁是破坏性变更的定义,此后的帧格式与共享段布局都不兼容, + 必须用配套 `hmapdev` 重编。 -## RPC 协议版本 + **中版本跃迁(如 1.1 → 1.2)不需要重编**:1.x 内的新增全部是 + 「插件调用、内核实现」,不调新面就不受影响。 -插件包里带 `protocol` 字段,必须等于内核的 `ProtocolVersion`(当前 **2**)。 +## 协议兼容性:只认主版本号 -协议 v2 引入了调用帧(tool / cleaner / output)与 `blocks_ref` 媒体块。 -v1 插件遇上 v2 内核会拿到空参数,反过来 v2 插件发 `blocks_ref` 会被 v1 内核静默忽略 —— -**两边错配都不报错、只是静默失效**,所以协议版本在握手上显式校验。 +**没有独立的协议版本号。** 兼容性由双方版本号的**主版本段**表态: +内核的 `meta.Version` 与插件的 `sdk_version`(插件包里的 `sdk_version` 字段, +即编译时链接的 SDK 版本)。 + +| 内核 | 插件 SDK | 结果 | +|---|---|---| +| 1.4.0 | 1.0.0 / 1.1.0 / 1.3.0 | ✅ 建链(同主版本) | +| 1.4.0 | 2.0.0 | ❌ 拒绝(跨主版本) | + +**为什么不设独立的协议号**:语义版本变动频繁(修 bug、加字段),如果每个 +这类改动都要人去升一个额外的号,那个号必然会被漏升而腐烂(历史实证: +曾有一个 `ProtocolVersion` 常量,多次布局变更里只升过一次,而对应的守卫 +测试长期处于失败状态、无人察觉)。让兼容性只挂在**始终会被发的那个号**上, +就没有「忘同步」这个失效模式。 + +跨主版本错配**不会静默失效**——握手时会显式拦下,并提示用配套 `hmapdev` 重编。 +此外握手还会比对 `field_count`(StageContext 描述符个数):它决定标志位基址, +不一致会让插件读到错位字段,因此也在握手上显式拒绝。 ## 怎么确认自己在用什么 diff --git a/meta/meta.go b/meta/meta.go index c6553c6..59b2137 100644 --- a/meta/meta.go +++ b/meta/meta.go @@ -103,15 +103,15 @@ func FullVersion() string { return SDKName + " v" + Version + " (" + Commit + ")" } -// ---- 协议版本 ---- +// ---- 协议兼容性 ---- // -// 子进程 RPC 的协议版本是一个独立的小整数,与 SDK/内核语义版本解耦: -// 语义版本变动频繁(修 bug、加字段),而 wire 协议只在**帧格式或握手语义** -// 变化时才升。当前值见核心仓 internal/plugin/proc/protocol.go 的 ProtocolVersion。 +// **不自定协议号**:兼容性只认双方版本号的**主版本段**。 +// 主版本相同即兼容(1.x 内全是纯追加);跨主版本则是破坏性变更 +// (如 1.0.0 从 C ABI 改子进程),握手时显式拒绝并提示重编。 // // C ABI 时代的 ABIVersion / CABINum / 51 个 Core 整数 ID 已随 // Part 6.2 删除 internal/plugin/cabi/ 一并退场: // - 整数 method id 平移为 method 名字符串(proc/protocol.go 的 Method* 常量) -// - 版本协商改为握手帧里的 protocol 字段 +// - 版本协商改为握手帧里的 core_version / sdk_version 主版本比对 // // 保留那些常量只会让人以为它们还在生效。 diff --git a/tools/hmapdev/proc_runtime_test.go b/tools/hmapdev/proc_runtime_test.go index b7f0632..94c48b2 100644 --- a/tools/hmapdev/proc_runtime_test.go +++ b/tools/hmapdev/proc_runtime_test.go @@ -26,6 +26,21 @@ func loadProcTemplate(t *testing.T) string { return string(data) } +// extractShmFlagsOff 取出模板里 shmFlagsOff 函数的函数体,供断言其取值来源。 +func extractShmFlagsOff(t *testing.T, src string) string { + t.Helper() + const sig = "func shmFlagsOff() uint32 {" + i := strings.Index(src, sig) + if i < 0 { + t.Fatal("模板缺少 shmFlagsOff()") + } + rest := src[i+len(sig):] + if j := strings.Index(rest, "\n}"); j >= 0 { + return rest[:j] + } + return rest +} + // stripComments 去掉源码中的注释(用空白填充以保持偏移),只留可执行代码。 func stripComments(t *testing.T, src string) string { t.Helper() @@ -218,17 +233,18 @@ func TestProcTemplate_ShmLayoutMatchesKernel(t *testing.T) { // 与内核 shm.go 的 offXxx 常量对齐(值比较,不依赖 gofmt 的对齐空白) layout := map[string]string{ - "shmOffMagic": "0", - "shmOffVersion": "4", - "shmOffArenaBase": "8", - "shmOffArenaCap": "12", - "shmOffArenaUsed": "16", - "shmOffCtxBase": "20", - "shmOffSeq": "24", - // 与内核 stageFieldCount / sliceSize 对齐 - "shmStageFieldCount": "18", + "shmOffMagic": "0", + "shmOffFieldCount": "4", + "shmOffArenaBase": "8", + "shmOffArenaCap": "12", + "shmOffArenaUsed": "16", + "shmOffCtxBase": "20", + "shmOffSeq": "24", + // 与内核 stageFieldCount / sliceSize 对齐。 + // 注意:这里不校验人工版本号 —— 它已被删除,布局一致性 + // 改由描述符个数这种派生事实校验(见下方全局规约断言)。 + "shmStageFieldCount": "19", "shmSliceSize": "8", - "shmVersion": "1", } constRe := func(name, want string) bool { // gofmt 会对齐常量块,故容许 name 与 = 之间有任意空白 @@ -241,6 +257,26 @@ func TestProcTemplate_ShmLayoutMatchesKernel(t *testing.T) { } } + // 布局必须自描述:标志位基址由内核下发的描述符个数推出, + // 不能用本插件的编译期常量。否则内核加字段时会静默错位。 + if !strings.Contains(src, "func shmFieldCount()") { + t.Error("模板缺少 shmFieldCount():标志位基址必须从 header 读取,不能自行推算") + } + flagsOff := extractShmFlagsOff(t, src) + if strings.Contains(flagsOff, "shmStageFieldCount") { + t.Errorf("shmFlagsOff 不得使用编译期常量 shmStageFieldCount(应经 shmFieldCount() 从 header 读取):\n%s", flagsOff) + } + if !strings.Contains(flagsOff, "shmFieldCount()") { + t.Errorf("shmFlagsOff 应基于 shmFieldCount():\n%s", flagsOff) + } + + // 不得再出现人工版本号(内核与模板都不应有)。 + for _, banned := range []string{"shmVersion", "shmOffVersion"} { + if strings.Contains(src, banned) { + t.Errorf("模板不得再有 %s:布局一致性由描述符个数校验,不靠人工版本号", banned) + } + } + // 字段枚举顺序:内核 stageField 的前若干项 fieldOrder := []string{ "fRawMessage = iota", "fUserID", "fGroupID", "fLLMText", @@ -343,15 +379,24 @@ func TestProcTemplate_DispatchesRequestsConcurrently(t *testing.T) { } } -// 协议与共享内存区域版本/魔数不匹配必须拒绝,不得半兼容运行。 -func TestProcTemplate_RejectsVersionMismatch(t *testing.T) { +// 主版本与共享内存区域结构不匹配必须拒绝,不得半兼容运行。 +// +// 注意:这里不再检查任何「协议版本」/「共享段版本」自定号 —— +// 它们已废除。兼容性只认双方版本号的**主版本段**(跨主版本即破坏性变更), +// 布局一致性由描述符个数把关。 +func TestProcTemplate_RejectsIncompatiblePeer(t *testing.T) { src := loadProcTemplate(t) - // §13.1 起共享段合并为单一「统一区域」,魔数校验文案随之更新。 - for _, want := range []string{"协议版本不匹配", "共享段版本不匹配", "统一区域魔数不匹配"} { + for _, want := range []string{"主版本不匹配", "字段数不匹配", "统一区域魔数不匹配"} { if !strings.Contains(src, want) { t.Errorf("握手应校验并拒绝 %q", want) } } + // 自定版本号必须已彻底移除,否则会出现「谁负责升这个号」的腐烂面。 + for _, banned := range []string{"procProtocolVersion", "shmVersion", "unifiedVersion"} { + if strings.Contains(src, banned) { + t.Errorf("模板不得再有自定版本号 %s", banned) + } + } } // 全平台统一产出 plugin.bin。 diff --git a/tools/hmapdev/templates/proc_main.go.tmpl b/tools/hmapdev/templates/proc_main.go.tmpl index adcf167..88dda74 100644 --- a/tools/hmapdev/templates/proc_main.go.tmpl +++ b/tools/hmapdev/templates/proc_main.go.tmpl @@ -18,30 +18,38 @@ import ( "fmt" "log" "os" + "strings" "sync" sdk "github.com/JianFeeeee/homeagentsdk/sdk" ) -// ---- 协议常量(须与内核 internal/plugin/proc/protocol.go 一致)---- +// majorVersion 取版本号的主版本段("1.4.0" → "1")。 +// 兼容性只认主版本:1.x 内的中版本跃迁都是纯追加,不调新面就不受影响; +// 主版本跃迁才是破坏性变更(如 1.0.0 从 C ABI 改子进程)。 +func majorVersion(v string) string { + v = strings.TrimPrefix(strings.TrimSpace(v), "v") + if i := strings.IndexByte(v, '.'); i >= 0 { + return v[:i] + } + return v +} -// procProtocolVersion 必须与内核的 proc.ProtocolVersion 完全一致。 +// ---- 协议常量(须与内核 internal/plugin/proc/protocol.go 一致)---- // -// v2:内核→插件的 payload 改用调用帧(tool/cleaner/output),媒体块改走 -// blocks_ref。v1 插件只读内联 args,遇上 v2 内核会拿到空参数;反过来 v2 -// 插件发 blocks_ref,v1 内核也会静默忽略。两边错配都不报错、只是静默失效, -// 所以靠这个常量在握手上显式拦下。 -const procProtocolVersion = 2 +// 不自定协议号:兼容性由双方版本号的**主版本**表态(内核 meta.Version、 +// 插件 sdk.SDKVersion)。主版本相同即兼容(1.x 内全是纯追加), +// 跨主版本则拒绝建链(主版本跃迁正是破坏性变更的定义)。 +// 布局一致性另由 field_count(描述符个数)把关。 // ---- 统一共享内存区域布局(与内核 internal/plugin/proc/unified.go 一致)---- const ( - unifiedMagic = 0x554D5352 // "UMSR" — Unified Memory Shared Region - unifiedVersion = 1 + unifiedMagic = 0x554D5352 // "UMSR" — Unified Memory Shared Region superBlockSize = 64 sbOffMagic = 0 - sbOffVersion = 4 + sbOffReserved0 = 4 // 保留(原 version 槽位,布局不变以免动其余偏移) sbOffGeneration = 8 sbOffCapacity = 16 sbOffCtxOff = 20 @@ -59,16 +67,15 @@ const ( shmStageFieldCount = 19 shmSliceSize = 8 - shmOffMagic = 0 - shmOffVersion = 4 - shmOffArenaBase = 8 - shmOffArenaCap = 12 - shmOffArenaUsed = 16 - shmOffCtxBase = 20 - shmOffSeq = 24 + shmOffMagic = 0 + shmOffFieldCount = 4 // 描述符个数,插件据此定位标志位(不再用自身常量推算) + shmOffArenaBase = 8 + shmOffArenaCap = 12 + shmOffArenaUsed = 16 + shmOffCtxBase = 20 + shmOffSeq = 24 - shmMagic = 0x48415348 - shmVersion = 2 + shmMagic = 0x48415348 ) // 字段索引(顺序须与内核 stageField 枚举一致) @@ -430,6 +437,13 @@ func shmArenaBase() uint32 { return shmU32(shmOffArenaBase) } func shmArenaCap() uint32 { return shmU32(shmOffArenaCap) } func shmCtxBase() uint32 { return shmU32(shmOffCtxBase) } +// shmFieldCount 返回内核写入的描述符个数。 +// +// 标志位紧跟在描述符数组之后,其偏移**必须**由内核下发的个数推出, +// 不能用本插件的编译期常量:内核加字段时两者会不一致, +// 自行推算会静默错位(而握手时已按此值做过一致性检查)。 +func shmFieldCount() uint32 { return shmU32(shmOffFieldCount) } + func shmDescOff(field int) uint32 { return shmCtxBase() + uint32(field*shmSliceSize) } @@ -446,7 +460,7 @@ func shmSetDesc(field int, off, ln uint32) { } func shmFlagsOff() uint32 { - return shmCtxBase() + uint32(shmStageFieldCount*shmSliceSize) + return shmCtxBase() + shmFieldCount()*shmSliceSize } func shmGetFlag(bit int) bool { return shm[shmFlagsOff()+uint32(bit)] != 0 } @@ -1511,21 +1525,24 @@ func handleKernelRequest(req *rpcRequest) { func handleHandshake(req *rpcRequest) { var p struct { - Protocol int `json:"protocol"` - ShmVersion uint32 `json:"shm_version"` + CoreVersion string `json:"core_version"` + FieldCount uint32 `json:"field_count"` ShmSize int `json:"shm_size"` PluginName string `json:"plugin_name"` EvtRingSize int `json:"evt_ring_size,omitempty"` } json.Unmarshal(req.Params, &p) - if p.Protocol != procProtocolVersion { - respondErr(req.ID, fmt.Errorf("协议版本不匹配(内核 %d,插件 %d)——请用配套 plugindev 重编", - p.Protocol, procProtocolVersion)) + // 主版本号是唯一兼容判据:跨主版本必是被破坏性变更分开的两端。 + if mine, theirs := majorVersion(sdk.SDKVersion), majorVersion(p.CoreVersion); mine != theirs { + respondErr(req.ID, fmt.Errorf("主版本不匹配(内核 %s,插件 SDK %s)——请用配套 hmapdev 重编", + p.CoreVersion, sdk.SDKVersion)) return } - if p.ShmVersion != shmVersion { - respondErr(req.ID, fmt.Errorf("共享段版本不匹配(内核 %d,插件 %d)", p.ShmVersion, shmVersion)) + // 字段个数必须与编译期一致:它决定标志位基址,不一致会静默错位。 + if p.FieldCount != shmStageFieldCount { + respondErr(req.ID, fmt.Errorf("StageContext 字段数不匹配(内核 %d,插件 %d)——请用配套 hmapdev 重编", + p.FieldCount, shmStageFieldCount)) return } if p.PluginName != "" { @@ -1573,7 +1590,6 @@ func handleHandshake(req *rpcRequest) { } respond(req.ID, map[string]interface{}{ - "protocol": procProtocolVersion, "sdk_version": sdk.SDKVersion, "plugin_name": pluginName, "pid": os.Getpid(),