mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-23 02:18:06 +00:00
feat(core): 注入行为的记忆/裁剪标志位落地 + jieba 词库内嵌 + Windows 改走 WSL
配套 SDK 提交:homeagent-sdk ba49dfd(公开 API 纯追加,无签名变更)。
本仓第三方的库镜像同步至该版本,以保证全新 clone 能编译。
## 1. 注入标志位(内核侧)
- 7 条注入路径(排队/中断/同步 × 纯文本/带媒体 + 旧 NoMem 变体)解析并转发
no_memory / context_policy / cleaner_name;策略在入口**校验**,
非法值报错而不是静默降级成 none(降级会让调用方以为自己声明的裁剪在生效)。
- 新增 validateContextPolicy(与 tool.register 同一套规则)与 pubSdkInjectOpts。
- input.register 不再手写字段白名单重建 ChannelDef,改为整体传递 + 补 ContextPolicy。
- io 层:applyInjectOpts 把标志位写进事件 payload,仅非零时写
(零值与旧 payload 逐字节一致,事件订阅方与旧内核都不受影响)。
- ioAdapter / procCore / internal-sdk 别名补齐六个 *Opts 实现。
## 2. 修掉「输入无条件裁剪」这个真缺陷
eventloop 此前对**每条非中断输入**都调 `context.Prune(...)`:破坏性(低相关事件被
归档移出上下文)且无法从调用点看出是谁触发的。改为 pruneOnInput/pruneDeclared:
优先级:注入点声明(payload.context_policy)> 通道声明(ChannelDef.ContextPolicy)
> 默认**不裁剪**
查询向量仍取清洗后的内容;新增 cleanInputFor 解析清洗文本,优先级为
注入点声明的 cleaner(cleaner_name)> 按 source 查到的通道 cleaner > 原文,
名字查不到时**记日志再回退**(注入是 fire-and-forget,插件看不到错误,
至少要在内核日志留下「你声明的清洗没生效」的痕迹)。
## 3. jieba 词库内嵌(修「猜 GOMODCACHE → 静默失效」)
原 jiebaDictDir() 去猜 GOMODCACHE/GOPATH/~/go/pkg/mod,部署机上通常没有 Go 模块
缓存 → GetJieba() 返回 nil → 分词/关键词提取/NLP 依存解析(进而 doc→graph 三元组
抽取)/静态词向量 tokenizer **一律静默返回空列表**,只有一行日志。本机看起来正常
只因开发机与生产机重合、恰好有那份缓存。
现在词库随二进制分发:internal/memory/jiebadict/ 5 文件约 11.6MB + go:embed,
按**内容哈希**命名缓存目录落盘(词库升级不复用旧文件),已齐全则跳过写入。
模块缓存降为兜底。homed 体积 32MB。
顺带确认(并有测试佐证):gojieba 的 Tag() 不需要 pos_dict/ 目录——
cppjieba 的 PosTagger 从主词典每行的词性列取 tag。
## 4. homed 放弃 Windows 原生,改走 WSL2
插件体系依赖「继承的 fd」+「统一共享内存区的段内偏移解引用」,Windows 既无 fd
继承语义,其句柄模型也无法表达后者;强行适配等于再维护一套平台专属 ABI
(C ABI 时代三套 ABI 并存曾导致改写型插件在某平台静默失效)。
- cmd/homed/platform_{windows,other}.go:原生 Windows 启动即拒绝并打印 WSL2 指引。
- internal/plugin/proc/shmalloc_windows.go:allocShm 直接返回「请用 WSL2」,
**不返回半可用的段**(与 shmalloc_other.go 同风格:未支持平台显式报错);
procEnvForShm 返回 nil。顺手修掉两处长期编译错误
(cryptorand→rand、h.evData→h.unified.evtData),使 GOOS=windows 至少能编译。
注:homed 本就编不出 Windows——internal/memory 依赖 cgo-only 的 gojieba。
- deploy/packaging/installer.nsi:不再安装 homed.exe/initconfig.exe,改为携带
**linux payload** 并调用新的 install-via-wsl.ps1;退出码 20/21 表示
「需先装 WSL/发行版」,走指引而非报错。
- deploy/packaging/windows/install-via-wsl.ps1(新):检测 WSL → 引导安装 →
确保 WSL2 → 送包进发行版 → 在 WSL 内按 Linux 方式安装。**复用 Linux 包与
linux/setup.sh**,不另写一套安装逻辑;落点与 deb 布局统一
(/usr/bin/homed + /usr/lib/homeagent/setup.sh)。
- deploy/packaging/linux/setup.sh:API Key 允许 HOMEAGENT_API_KEY 覆盖
(否则安装器界面显示一份、config.db 里另一份 → 登录不上)。
- deploy/packaging/build.sh:windows 目标只构建 waiter + gui,并新增
stage_linux_payload 把 Linux 包暂存给安装器;homed/initconfig 在 windows
目标下明确拒绝。
## 5. 插件调用点统一写明意图
- webui 的 OpenAI 兼容端点(固定提示词模板)→ InjectTextSyncNoMemory。
- agentcli 的 5 处纯状态通知(已启动/超时/执行结束/进程退出/读取结束)→ NoMemory;
**带输出**的 2 处(定时反馈、有新输出)刻意保留记忆并注明理由。
- timer 的定时提醒 → NoMemory(中断本来也隐含 NoMemory,这里是写明意图)。
## 6. 版本
meta.Version 仍为 1.2.0(main 是下一个未发布中版本);
SDKCompatibleVersion 1.1.0 → **1.2.0**(本内核已实现 SDK 1.2.0 全部新增方法)。
## 测试
- core:默认不裁剪(无声明/none/空)、通道 opt-in、注入点双向覆盖通道、
nil context/io 安全、cleaner 优先级与未知名回退。
- io:零值 opts 与历史 payload 逐键相同;text/中断/媒体三类注入标志位都落到
payload;旧方法仍生效。
- proc:validateContextPolicy 只接受 ""/none/prune,报错含位置与实际值;
**跨进程** e2e——testdata 插件经 io.injectText 送出三个标志位,断言它们穿过 RPC
到达内核。
- memory:模块缓存不可见时内嵌词库仍可用(分词与 POS 内容词均非空)、
落盘幂等、内容哈希稳定。
验证:go build ./... / go vet ./... / go vet -tags onnxruntime ./...
go test -short ./internal/memory/... ./internal/nlp/... ./internal/plugin/...
./internal/agent/{core,io}/... ./pkg/...
This commit is contained in:
33
internal/plugin/proc/contextpolicy_test.go
Normal file
33
internal/plugin/proc/contextpolicy_test.go
Normal file
@ -0,0 +1,33 @@
|
||||
package proc
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 非法 context_policy 必须报错,而不是静默当成 none。
|
||||
//
|
||||
// 为什么这条值得单独测:把拼写错误降级成「不裁剪」不会有任何报错、日志或
|
||||
// 行为异常——调用方会一直以为自己声明的裁剪在生效,直到某天上下文被撑爆。
|
||||
// 这类静默降级是本次改造要消掉的东西,所以要钉住。
|
||||
func TestValidateContextPolicy(t *testing.T) {
|
||||
ok := []string{"", "none", "prune"}
|
||||
for _, policy := range ok {
|
||||
if err := validateContextPolicy("tool.register", policy); err != nil {
|
||||
t.Errorf("合法取值 %q 被拒绝: %v", policy, err)
|
||||
}
|
||||
}
|
||||
|
||||
bad := []string{"prune ", "PRUNE", "True", "None", "always", "裁剪"}
|
||||
for _, policy := range bad {
|
||||
err := validateContextPolicy("io.injectText", policy)
|
||||
if err == nil {
|
||||
t.Errorf("非法取值 %q 应被拒绝", policy)
|
||||
continue
|
||||
}
|
||||
// 报错要指出位置与实际值,否则排查时不知道是谁传错了。
|
||||
if !strings.Contains(err.Error(), "io.injectText") || !strings.Contains(err.Error(), policy) {
|
||||
t.Errorf("错误信息应包含位置与实际值,实际: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -101,6 +101,15 @@ type CoreSDK interface {
|
||||
InjectInputMediaSync(source, channel, text string, blocks []pubsdk.ContentBlock) string
|
||||
InjectInterruptMedia(source, channel, text string, blocks []pubsdk.ContentBlock)
|
||||
|
||||
// 带标志位的注入:声明这一次注入是否记入记忆、是否据此裁剪上下文。
|
||||
// 上面的三参数方法是它们的零值糖。
|
||||
InjectTextOpts(source, channel, text string, opts pubsdk.InjectOptions)
|
||||
InjectInterruptTextOpts(source, channel, text string, opts pubsdk.InjectOptions)
|
||||
InjectInputSyncOpts(source, channel, text string, opts pubsdk.InjectOptions) string
|
||||
InjectInputMediaOpts(source, channel, text string, blocks []pubsdk.ContentBlock, opts pubsdk.InjectOptions)
|
||||
InjectInputMediaSyncOpts(source, channel, text string, blocks []pubsdk.ContentBlock, opts pubsdk.InjectOptions) string
|
||||
InjectInterruptMediaOpts(source, channel, text string, blocks []pubsdk.ContentBlock, opts pubsdk.InjectOptions)
|
||||
|
||||
// SetToolBlocks 注入媒体块,内核在下一条 tool message 携带(§3.8)。
|
||||
SetToolBlocks(blocks []pubsdk.ContentBlock)
|
||||
|
||||
@ -150,50 +159,75 @@ func (h *coreHandler) Handle(method string, params json.RawMessage) (interface{}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("input.register: %w", err)
|
||||
}
|
||||
return nil, h.sdk.RegisterInputChannel(p.Name, pubsdk.ChannelDef{
|
||||
NoMemory: p.Def.NoMemory,
|
||||
Cleaner: cleaner,
|
||||
})
|
||||
if err := validateContextPolicy("input.register", p.Def.ContextPolicy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 整体传 p.Def(只是把函数型的 Cleaner 换成代理),不要手写字段白名单:
|
||||
// 白名单会让新增字段静默丢失。
|
||||
def := p.Def
|
||||
def.Cleaner = cleaner
|
||||
return nil, h.sdk.RegisterInputChannel(p.Name, def)
|
||||
|
||||
// ---- IO 注入(原 case 5/6/7/47)----
|
||||
//
|
||||
// 注入标志位(no_memory / context_policy)由插件在调用点声明,默认
|
||||
// 记入记忆 + 不裁剪。策略值在入口校验:静默降级成 none 会让调用方
|
||||
// 以为自己声明的裁剪在生效。
|
||||
case MethodIOInjectText:
|
||||
var p injectParams
|
||||
if err := unmarshal(params, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.sdk.InjectText(p.Source, p.Channel, h.resolveText(p))
|
||||
if err := validateContextPolicy("io.injectText", p.ContextPolicy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.sdk.InjectTextOpts(p.Source, p.Channel, h.resolveText(p), pubSdkInjectOpts(p.NoMemory, p.ContextPolicy, p.CleanerName))
|
||||
return nil, nil
|
||||
case MethodIOInjectInterrupt:
|
||||
var p injectParams
|
||||
if err := unmarshal(params, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.sdk.InjectInterruptText(p.Source, p.Channel, h.resolveText(p))
|
||||
if err := validateContextPolicy("io.injectInterrupt", p.ContextPolicy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.sdk.InjectInterruptTextOpts(p.Source, p.Channel, h.resolveText(p), pubSdkInjectOpts(p.NoMemory, p.ContextPolicy, p.CleanerName))
|
||||
return nil, nil
|
||||
case MethodIOInjectTextNoMem:
|
||||
var p injectParams
|
||||
if err := unmarshal(params, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.sdk.InjectTextNoMemory(p.Source, p.Channel, h.resolveText(p))
|
||||
if err := validateContextPolicy("io.injectTextNoMem", p.ContextPolicy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 旧 RPC 语义就是「不进记忆」,显式标志位只可能再叠上 context_policy。
|
||||
h.sdk.InjectTextOpts(p.Source, p.Channel, h.resolveText(p), pubSdkInjectOpts(true, p.ContextPolicy, p.CleanerName))
|
||||
return nil, nil
|
||||
case MethodIOInjectSync:
|
||||
var p injectParams
|
||||
if err := unmarshal(params, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"reply": h.sdk.InjectInputSync(p.Source, p.Channel, h.resolveText(p))}, nil
|
||||
if err := validateContextPolicy("io.injectInputSync", p.ContextPolicy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reply := h.sdk.InjectInputSyncOpts(p.Source, p.Channel, h.resolveText(p), pubSdkInjectOpts(p.NoMemory, p.ContextPolicy, p.CleanerName))
|
||||
return map[string]interface{}{"reply": reply}, nil
|
||||
|
||||
case MethodIOInjectMedia:
|
||||
var p injectMediaParams
|
||||
if err := unmarshal(params, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateContextPolicy("io.injectMedia", p.ContextPolicy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blocks, err := h.resolveBlocks(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.sdk.InjectInputMedia(p.Source, p.Channel, p.Text, blocks)
|
||||
h.sdk.InjectInputMediaOpts(p.Source, p.Channel, p.Text, blocks, pubSdkInjectOpts(p.NoMemory, p.ContextPolicy, p.CleanerName))
|
||||
return nil, nil
|
||||
|
||||
case MethodIOInjectMediaSync:
|
||||
@ -201,11 +235,14 @@ func (h *coreHandler) Handle(method string, params json.RawMessage) (interface{}
|
||||
if err := unmarshal(params, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateContextPolicy("io.injectMediaSync", p.ContextPolicy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blocks, err := h.resolveBlocks(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reply := h.sdk.InjectInputMediaSync(p.Source, p.Channel, p.Text, blocks)
|
||||
reply := h.sdk.InjectInputMediaSyncOpts(p.Source, p.Channel, p.Text, blocks, pubSdkInjectOpts(p.NoMemory, p.ContextPolicy, p.CleanerName))
|
||||
return map[string]interface{}{"reply": reply}, nil
|
||||
|
||||
case MethodIOInjectInterruptMedia:
|
||||
@ -213,11 +250,14 @@ func (h *coreHandler) Handle(method string, params json.RawMessage) (interface{}
|
||||
if err := unmarshal(params, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateContextPolicy("io.injectInterruptMedia", p.ContextPolicy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blocks, err := h.resolveBlocks(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.sdk.InjectInterruptMedia(p.Source, p.Channel, p.Text, blocks)
|
||||
h.sdk.InjectInterruptMediaOpts(p.Source, p.Channel, p.Text, blocks, pubSdkInjectOpts(p.NoMemory, p.ContextPolicy, p.CleanerName))
|
||||
return nil, nil
|
||||
|
||||
// ---- 生命周期(原 case 8)----
|
||||
@ -648,10 +688,13 @@ func (h *coreHandler) resolveText(p injectParams) string {
|
||||
}
|
||||
|
||||
type injectParams struct {
|
||||
Source string `json:"source"`
|
||||
Channel string `json:"channel"`
|
||||
Text string `json:"text,omitempty"`
|
||||
TextRef SharedRef `json:"text_ref,omitempty"`
|
||||
Source string `json:"source"`
|
||||
Channel string `json:"channel"`
|
||||
Text string `json:"text,omitempty"`
|
||||
TextRef SharedRef `json:"text_ref,omitempty"`
|
||||
NoMemory bool `json:"no_memory,omitempty"`
|
||||
ContextPolicy string `json:"context_policy,omitempty"`
|
||||
CleanerName string `json:"cleaner_name,omitempty"`
|
||||
}
|
||||
|
||||
// injectMediaParams 是带媒体注入/工具块注入的参数。
|
||||
@ -664,11 +707,33 @@ type injectParams struct {
|
||||
//
|
||||
// 没有 BlocksRef 时(直连 RPC 测试、arena 不可用)回退内联 Blocks。
|
||||
type injectMediaParams struct {
|
||||
Source string `json:"source"`
|
||||
Channel string `json:"channel"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Blocks []pubsdk.ContentBlock `json:"blocks,omitempty"`
|
||||
BlocksRef SharedRef `json:"blocks_ref,omitempty"`
|
||||
Source string `json:"source"`
|
||||
Channel string `json:"channel"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Blocks []pubsdk.ContentBlock `json:"blocks,omitempty"`
|
||||
BlocksRef SharedRef `json:"blocks_ref,omitempty"`
|
||||
NoMemory bool `json:"no_memory,omitempty"`
|
||||
ContextPolicy string `json:"context_policy,omitempty"`
|
||||
CleanerName string `json:"cleaner_name,omitempty"`
|
||||
}
|
||||
|
||||
// pubSdkInjectOpts 把 RPC 报文里的三个字段转成公开 SDK 的 InjectOptions。
|
||||
//
|
||||
// 单独提一个转换函数是为了让「默认值」只有一个出处:零值即记入记忆 + 不裁剪,
|
||||
// 与旧三参数注入等价。
|
||||
func pubSdkInjectOpts(noMemory bool, policy, cleanerName string) pubsdk.InjectOptions {
|
||||
return pubsdk.InjectOptions{NoMemory: noMemory, ContextPolicy: policy, CleanerName: cleanerName}
|
||||
}
|
||||
|
||||
// validateContextPolicy 校验上下文策略取值,与 tool.register 同一套规则。
|
||||
//
|
||||
// 空串等价于 none(不裁剪)。非法值必须报错而不是当成 none:把拼写错误
|
||||
// 静默降级成「不裁剪」会让调用方以为自己声明的裁剪在生效。
|
||||
func validateContextPolicy(where, policy string) error {
|
||||
if !pubsdk.ValidContextPolicy(policy) {
|
||||
return fmt.Errorf("%s: context_policy 只允许 none/prune,实际 %q", where, policy)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveJSONRef 若 ref 非零则从共享内存读取并 JSON 反序列化到 out;
|
||||
@ -812,10 +877,8 @@ func (h *coreHandler) toolRegister(params json.RawMessage) (interface{}, error)
|
||||
if p.Name == "" {
|
||||
return nil, fmt.Errorf("tool.register: 缺少 name")
|
||||
}
|
||||
switch p.Def.ContextPolicy {
|
||||
case "", "none", "prune":
|
||||
default:
|
||||
return nil, fmt.Errorf("tool.register: context_policy 只允许 none/prune,实际 %q", p.Def.ContextPolicy)
|
||||
if err := validateContextPolicy("tool.register", p.Def.ContextPolicy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Def.Plugin = h.name
|
||||
// 函数本身不进 JSON;has_cleaner 只声明其存在,实际执行回到插件进程。
|
||||
|
||||
@ -29,6 +29,8 @@ type fakeCoreSDK struct {
|
||||
|
||||
// injected 记录经 InjectText 注入的文本(验证跨进程共享槽路径)。
|
||||
injected []string
|
||||
// lastInjectOpts 记录最近一次带标志位注入的 opts(跨进程转发断言用)。
|
||||
lastInjectOpts pubsdk.InjectOptions
|
||||
// toolBlocks 累积 SetToolBlocks 收到的块(多模态注入通道)。
|
||||
toolBlocks []pubsdk.ContentBlock
|
||||
// 文档/知识:验证大正文经 doc_ref / content_ref 走共享内存。
|
||||
@ -64,6 +66,13 @@ func (f *fakeCoreSDK) InjectText(s, c, t string) {
|
||||
f.mu.Unlock()
|
||||
}
|
||||
|
||||
// lastInjectOpts 记录最近一次带标志位注入的 opts(跨进程转发断言用)。
|
||||
func (f *fakeCoreSDK) lastOpts() pubsdk.InjectOptions {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return f.lastInjectOpts
|
||||
}
|
||||
|
||||
func (f *fakeCoreSDK) injectedTexts() []string {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
@ -78,6 +87,33 @@ func (f *fakeCoreSDK) InjectInputMediaSync(s, c, t string, b []pubsdk.ContentBlo
|
||||
}
|
||||
func (f *fakeCoreSDK) InjectInterruptMedia(s, c, t string, b []pubsdk.ContentBlock) {}
|
||||
|
||||
// ---- 带 InjectOptions 的注入(1.2.0)----
|
||||
//
|
||||
// 转发到旧方法即可:本测试关心的是「注入了什么话」,标志位的转发在
|
||||
// corehandler 与 io 层的测试里覆盖。
|
||||
func (f *fakeCoreSDK) InjectTextOpts(s, c, t string, o pubsdk.InjectOptions) {
|
||||
// 记录 opts:跨进程测试要断言插件在调用点声明的标志位确实穿过了 RPC。
|
||||
f.mu.Lock()
|
||||
f.lastInjectOpts = o
|
||||
f.mu.Unlock()
|
||||
f.InjectText(s, c, t)
|
||||
}
|
||||
func (f *fakeCoreSDK) InjectInterruptTextOpts(s, c, t string, o pubsdk.InjectOptions) {
|
||||
f.InjectInterruptText(s, c, t)
|
||||
}
|
||||
func (f *fakeCoreSDK) InjectInputSyncOpts(s, c, t string, o pubsdk.InjectOptions) string {
|
||||
return f.InjectInputSync(s, c, t)
|
||||
}
|
||||
func (f *fakeCoreSDK) InjectInputMediaOpts(s, c, t string, b []pubsdk.ContentBlock, o pubsdk.InjectOptions) {
|
||||
f.InjectInputMedia(s, c, t, b)
|
||||
}
|
||||
func (f *fakeCoreSDK) InjectInputMediaSyncOpts(s, c, t string, b []pubsdk.ContentBlock, o pubsdk.InjectOptions) string {
|
||||
return f.InjectInputMediaSync(s, c, t, b)
|
||||
}
|
||||
func (f *fakeCoreSDK) InjectInterruptMediaOpts(s, c, t string, b []pubsdk.ContentBlock, o pubsdk.InjectOptions) {
|
||||
f.InjectInterruptMedia(s, c, t, b)
|
||||
}
|
||||
|
||||
// SetToolBlocks 记录收到的媒体块,供测试断言共享内存通道真的把内容带到了内核侧。
|
||||
func (f *fakeCoreSDK) SetToolBlocks(blocks []pubsdk.ContentBlock) {
|
||||
f.mu.Lock()
|
||||
@ -779,6 +815,20 @@ func TestPlugin_ArenaAllocFreeAcrossProcess(t *testing.T) {
|
||||
t.Fatalf("经共享槽读到的内容不一致:len(got)=%d len(want)=%d", len(got[0]), len(payload))
|
||||
}
|
||||
|
||||
// 注入标志位必须穿过 RPC 到达内核:插件在调用点声明「不进记忆 / 据此裁剪 /
|
||||
// 用哪个 cleaner」,内核得拿到才能照做。只测 SDK 侧记录不到这一点——
|
||||
// 字段在 JSON 与参数结构之间丢掉的失败模式是静默的。
|
||||
opts := core.lastOpts()
|
||||
if !opts.NoMemory {
|
||||
t.Errorf("no_memory 未穿过 RPC: %+v", opts)
|
||||
}
|
||||
if opts.ContextPolicy != "prune" {
|
||||
t.Errorf("context_policy 未穿过 RPC: %+v", opts)
|
||||
}
|
||||
if opts.CleanerName != "demo_cleaner" {
|
||||
t.Errorf("cleaner_name 未穿过 RPC: %+v", opts)
|
||||
}
|
||||
|
||||
// 插件已归还槽:池必须回到全空,否则说明 arena.free 没生效。
|
||||
if used, total := host.Arena().Stats(); used != 0 {
|
||||
t.Fatalf("插件归还后槽池应全空,实际 used=%d/%d", used, total)
|
||||
|
||||
@ -15,19 +15,19 @@ import (
|
||||
|
||||
// Windows 侧共享段:命名 FileMapping + 命名 Event。
|
||||
//
|
||||
// 与 Unix 的机制差异(不是能力差异):
|
||||
// Windows 没有 fd 继承语义——os/exec 的 ExtraFiles 在 Windows 实现里不被支持。
|
||||
// 等价机制是命名内核对象:父进程 CreateFileMappingW 建带名字的段,
|
||||
// 子进程 OpenFileMappingW 按同名打开,拿到同一份物理页。
|
||||
// ⚠️ 本文件**已不是可用路径**:homed 已放弃 Windows 原生支持
|
||||
// (见 cmd/homed/platform_windows.go)。原因:插件体系依赖「继承的 fd」与
|
||||
// 「统一共享内存区的段内偏移解引用」,而 Windows 既没有 fd 继承语义
|
||||
// (os/exec 的 ExtraFiles 在 Windows 不支持),本文件描述的也仍是**旧的**
|
||||
// 两段布局(StageContext 段 + 事件环段),跟不上 §13.1 的单块统一区域。
|
||||
//
|
||||
// **这是 §9.2 的正解**。C ABI 时代 Windows 是第三套独立 ABI 实现
|
||||
// (dynamic_dll_windows.go),stage 只下发 3 字段且完全没有写回,
|
||||
// sanitizer 这类改写型插件静默失效。三套 ABI 收敛为单一 RPC 后,
|
||||
// Windows 与 Unix 共用同一份 stage 逻辑与同一份共享段布局,
|
||||
// 平台差异只剩本文件的创建端 + 插件侧模板的打开端。
|
||||
// 保留本文件只为让 GOOS=windows 仍能编译:否则平台门根本跑不起来,
|
||||
// 用户看到的会是「产物缺失」而不是一句「请用 WSL」。
|
||||
// allocShm 因此在入口直接报错,不返回一个「看起来能用」的段——
|
||||
// 让它跑起来只会得到无法解释的握手失败,这比启动失败难查得多
|
||||
// (与 shmalloc_other.go 的处理方式一致)。
|
||||
//
|
||||
// 名字带 PID 与递增序号:多个 homed 实例并存时不能撞名,
|
||||
// 同一实例内 StageContext 段与事件环段也必须分开。
|
||||
// Windows 用户的正确路径:WSL2(在 WSL 里就是普通 linux/amd64)。
|
||||
var shmNameSeq atomic.Uint64
|
||||
|
||||
const (
|
||||
@ -60,20 +60,15 @@ var (
|
||||
shmHandlesMu sync.Mutex
|
||||
)
|
||||
|
||||
// allocShm 创建命名共享段并映射。
|
||||
// allocShm 在 Windows 上明确报错:homed 不支持 Windows 原生运行。
|
||||
//
|
||||
// 返回的 *os.File 为 nil:Windows 不经 fd 传递段,插件按名字打开。
|
||||
// 名字通过 procEnvForShm 注入子进程环境变量。
|
||||
// 不返回「能用的段」:本文件实现的是 §13.1 之前的**两段**布局,
|
||||
// 与当前内核的单块统一区域不兼容。静默返回只会在握手阶段变成一句
|
||||
// 无法解释的魔数不匹配。报错文案直接给出行动:用 WSL2。
|
||||
func allocShm(size int) (*os.File, []byte, error) {
|
||||
name := shmNameForMode()
|
||||
shm, data, err := createNamedMapping(name, size)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
shmHandlesMu.Lock()
|
||||
shmHandles[uintptr(unsafe.Pointer(&data[0]))] = shm
|
||||
shmHandlesMu.Unlock()
|
||||
return nil, data, nil
|
||||
return nil, nil, fmt.Errorf("proc: homed 不支持 Windows 原生运行" +
|
||||
"(插件体系依赖 fd 继承与统一共享内存区的段内偏移解引用)——请使用 WSL2;" +
|
||||
"详见 cmd/homed/platform_windows.go")
|
||||
}
|
||||
|
||||
// shmNameForMode 按安全模式生成命名段名。
|
||||
@ -87,7 +82,7 @@ func shmNameForMode() string {
|
||||
return fmt.Sprintf("%s_%d_%d", shmNamePrefix, os.Getpid(), seq)
|
||||
default: // safe / debug
|
||||
var b [12]byte
|
||||
if _, err := cryptorand.Read(b[:]); err != nil {
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
// 退化为 PID+seq(极端情况,crypto rand 几乎不会失败)
|
||||
return fmt.Sprintf("%s_%d_%d", shmNamePrefix, os.Getpid(), seq)
|
||||
}
|
||||
|
||||
@ -3,24 +3,16 @@
|
||||
package proc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// procEnvForShm 返回子进程挂载共享段所需的环境变量(Windows)。
|
||||
// procEnvForShm 在 Windows 上返回空:这条路径已不可用。
|
||||
//
|
||||
// Windows 没有 fd 继承语义(os/exec 的 ExtraFiles 在 Windows 不被支持),
|
||||
// 故段与事件对象的**名字**经环境变量传给子进程,插件侧模板的
|
||||
// z_proc_shm_windows.go 按同名 OpenFileMappingW / OpenEventW 打开。
|
||||
//
|
||||
// 名字带 PID 与递增序号:多个 homed 实例并存时不能撞名。
|
||||
func (h *Host) procEnvForShm() []string {
|
||||
return []string{
|
||||
fmt.Sprintf("HOMEAGENT_SHM_STAGE=%s", shmNameOf(h.data)),
|
||||
fmt.Sprintf("HOMEAGENT_SHM_EVTRING=%s", shmNameOf(h.evtData)),
|
||||
fmt.Sprintf("HOMEAGENT_EVT_EVENT=%s", evtEventNameOf(h.evtNotifyFd)),
|
||||
}
|
||||
}
|
||||
// 原实现传的是旧的两段布局(SHM_STAGE + SHM_EVTRING)的两个名字,
|
||||
// 而 §13.1 之后内核只有一块统一区域;名字的数量本身就是错的。
|
||||
// 真正的失败发生在更早的 allocShm(那里给出明确的「请用 WSL2」),
|
||||
// 所以这里不再返回任何东西——返回半套名字只会让人以为「只是名字没更新」。
|
||||
func (h *Host) procEnvForShm() []string { return nil }
|
||||
|
||||
// procExtraFilesForShm 在 Windows 返回 nil:段不经 fd 传递。
|
||||
func (h *Host) procExtraFilesForShm() []*os.File { return nil }
|
||||
|
||||
5
internal/plugin/proc/testdata/stageplugin.go
vendored
5
internal/plugin/proc/testdata/stageplugin.go
vendored
@ -438,8 +438,13 @@ func main() {
|
||||
}
|
||||
copy(region[ref.Offset:ref.Offset+uint32(len(text))], text)
|
||||
ref.Length = uint32(len(text))
|
||||
// 注入标志位随参数一起过 RPC:内核据此决定这次注入是否
|
||||
// 记入记忆、是否据此裁剪上下文、用哪个已注册 cleaner 清洗。
|
||||
callKernel("io.injectText", map[string]interface{}{
|
||||
"source": "plugin", "channel": "demo", "text_ref": ref,
|
||||
"no_memory": true,
|
||||
"context_policy": "prune",
|
||||
"cleaner_name": "demo_cleaner",
|
||||
})
|
||||
arenaFree(ref)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user