proc: Windows 共享内存 + 事件通知适配(Part 6.2 内核侧)

补齐内核侧的 Windows 创建端,与 6.1 的插件侧打开端配对。三平台
(linux/darwin/windows)现在都能构建 internal/plugin/proc。

## Windows 走命名内核对象(无 fd 继承语义)

os/exec 的 ExtraFiles 在 Windows 实现里不被支持,故:
- shmalloc_windows.go:CreateFileMappingW(INVALID_HANDLE_VALUE + 命名 →
  系统页文件支撑的匿名段,不落盘)+ MapViewOfFile
- evtfd_windows.go:CreateEventW 命名 Event 对象 + SetEvent 通知
- shmpass_windows.go:把段名/对象名经环境变量注入子进程
  (HOMEAGENT_SHM_STAGE / HOMEAGENT_SHM_EVTRING / HOMEAGENT_EVT_EVENT)

名字带 PID + 递增序号:多个 homed 实例并存时不能撞名。

Event 与 eventfd 的语义差异:Event 是二元信号,多次 SetEvent 只对应一次
唤醒,不累积。不影响正确性——消费者被唤醒后按 readSeq 追 writeSeq 批量
drain,丢的是"唤醒次数"不是"事件";事件环本身就允许溢出丢弃并让消费者
知道丢了(dropped 计数),通知面从来不是可靠投递语义。

## 传递机制抽象为 shmpass_*.go

Plugin.Start 不再直接构造 ExtraFiles 列表,改为问 Host 要:
  Env:        p.host.procEnvForShm()        // Windows 返回段名,Unix 返回 nil
  ExtraFiles: p.host.procExtraFilesForShm() // Unix 返回 fd 列表,Windows 返回 nil

平台差异被收敛到这一对函数,Plugin/coreHandler/stage 全部平台无关。

## macOS pipe 生命周期修正

原实现只返回读端 fd,写端 *os.File 无人持有 → 可能被 GC 回收 →
读端收到 EOF 而非阻塞 → 消费循环变忙转。改为 pipePair 表同时持有两端,
evtfdClose 一并关闭。

## E2E 测试跟进模板拆分

模板从单文件拆成三个(主体 + unix/windows 挂载),测试需要一并落盘,
否则编译报 attachStageShm undefined。procRuntimeTemplates 表必须与
SDK 仓 proc_runtime.go 的 procRuntimeFiles 一致。

验证:三平台 go build ./internal/plugin/... 通过(gojieba 的 cgo 依赖
导致 internal/memory 在非 linux 失败,与本次无关);
go test -race ./internal/plugin/... 全绿,含 2 项真实模板 E2E。

Ref: docs/zh/架构迁移评估.md §9.2、docs/zh/plugin-migration-plan.md Part 6
This commit is contained in:
JianFeeeee
2026-09-02 19:07:14 +08:00
parent 2a64d6d715
commit 5d2f21fe1e
14 changed files with 445 additions and 68 deletions

View File

@ -68,6 +68,20 @@ func (p *e2ePlugin) Start(s *sdk.PluginSDK) error {
func (p *e2ePlugin) Stop() error { return nil }
`
// procRuntimeTemplates 列出 plugindev 会生成到插件目录的运行时文件。
//
// 必须与 SDK 仓 tools/plugindev/proc_runtime.go 的 procRuntimeFiles 一致:
// 共享段与事件通知的传递机制按平台不同(Unix 继承 fd,Windows 命名
// 内核对象),故拆成带 build tag 的文件;只写主模板会编译失败。
var procRuntimeTemplates = []struct {
tmpl string
out string
}{
{"proc_main.go.tmpl", "z_proc_gen.go"},
{"proc_shm_unix.go.tmpl", "z_proc_shm_unix.go"},
{"proc_shm_windows.go.tmpl", "z_proc_shm_windows.go"},
}
// buildPluginWithRealTemplate 用 plugindev 的真实模板编译一个插件二进制。
func buildPluginWithRealTemplate(t *testing.T, businessCode string) string {
t.Helper()
@ -75,17 +89,19 @@ func buildPluginWithRealTemplate(t *testing.T, businessCode string) string {
t.Skip("环境无 go 工具链,跳过端到端测试")
}
tmpl := filepath.Join("..", "..", "..",
"third_party", "homeagent-sdk", "tools", "plugindev",
"templates", "proc_main.go.tmpl")
runtime, err := os.ReadFile(tmpl)
if err != nil {
t.Skipf("plugindev 模板不可读(SDK 仓可能未就位): %v", err)
}
tmplDir := filepath.Join("..", "..", "..",
"third_party", "homeagent-sdk", "tools", "plugindev", "templates")
dir := t.TempDir()
mustWriteFile(t, filepath.Join(dir, "plugin.go"), businessCode)
mustWriteFile(t, filepath.Join(dir, "z_proc_gen.go"), string(runtime))
for _, rt := range procRuntimeTemplates {
data, err := os.ReadFile(filepath.Join(tmplDir, rt.tmpl))
if err != nil {
t.Skipf("plugindev 模板 %s 不可读(SDK 仓可能未就位): %v", rt.tmpl, err)
}
mustWriteFile(t, filepath.Join(dir, rt.out), string(data))
}
sdkPath, err := filepath.Abs(filepath.Join("..", "..", "..", "third_party", "homeagent-sdk"))
if err != nil {