From d027c964e21cd26e3647da628d6461e8ff0a2a2c Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Wed, 2 Sep 2026 19:07:14 +0800 Subject: [PATCH] =?UTF-8?q?proc:=20Windows=20=E5=85=B1=E4=BA=AB=E5=86=85?= =?UTF-8?q?=E5=AD=98=20+=20=E4=BA=8B=E4=BB=B6=E9=80=9A=E7=9F=A5=E9=80=82?= =?UTF-8?q?=E9=85=8D=EF=BC=88Part=206.2=20=E5=86=85=E6=A0=B8=E4=BE=A7?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补齐内核侧的 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 --- internal/plugin/proc/e2e_template_test.go | 32 +++-- internal/plugin/proc/evtfd_darwin.go | 68 +++++++++-- internal/plugin/proc/evtfd_linux.go | 3 + internal/plugin/proc/evtfd_other.go | 5 +- internal/plugin/proc/evtfd_windows.go | 103 ++++++++++++++++ internal/plugin/proc/evtring.go | 4 +- internal/plugin/proc/host.go | 50 +++++--- internal/plugin/proc/plugin.go | 24 ++-- internal/plugin/proc/process.go | 30 +++-- internal/plugin/proc/protocol.go | 8 +- internal/plugin/proc/shmalloc_other.go | 2 +- internal/plugin/proc/shmalloc_windows.go | 137 ++++++++++++++++++++++ internal/plugin/proc/shmpass_unix.go | 21 ++++ internal/plugin/proc/shmpass_windows.go | 26 ++++ 14 files changed, 445 insertions(+), 68 deletions(-) create mode 100644 internal/plugin/proc/evtfd_windows.go create mode 100644 internal/plugin/proc/shmalloc_windows.go create mode 100644 internal/plugin/proc/shmpass_unix.go create mode 100644 internal/plugin/proc/shmpass_windows.go diff --git a/internal/plugin/proc/e2e_template_test.go b/internal/plugin/proc/e2e_template_test.go index aba143b..e80e24e 100644 --- a/internal/plugin/proc/e2e_template_test.go +++ b/internal/plugin/proc/e2e_template_test.go @@ -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 { diff --git a/internal/plugin/proc/evtfd_darwin.go b/internal/plugin/proc/evtfd_darwin.go index fb39f2e..1f62f5c 100644 --- a/internal/plugin/proc/evtfd_darwin.go +++ b/internal/plugin/proc/evtfd_darwin.go @@ -4,30 +4,78 @@ package proc import ( "os" + "sync" + "syscall" ) -// evtfdCreate 用 pipe 模拟 Linux eventfd 的通知语义(macOS 无 eventfd)。 +// macOS 侧事件通知:用 pipe 模拟 eventfd(macOS 无 eventfd_create)。 // -// 限制:不具 eventfd 的计数合并(多次写会触发多次读), -// 但事件环本身允许溢出丢弃,consumer 在 drainEvents 里按 readSeq 批量读取, -// 故多次唤醒只多几次无效循环(readSeq == writeSeq 时立即返回),不造成正确性问题。 +// 与 eventfd 的语义差异:pipe 不具计数合并,多次写会触发多次读。 +// 这不影响正确性——消费者在 drainEvents 里按 readSeq 追 writeSeq 批量读, +// 多次唤醒只多几次空循环(readSeq == writeSeq 时立即返回)。 // -// 走 Go netpoller(os.File.Read 阻塞时只 park goroutine,实验 1 已验证)。 +// 走 Go netpoller:os.File.Read 阻塞时只 park goroutine,不占 OS 线程 +// (实验 1 已验证 200 个等待者仅增 1 个 OS 线程)。 +// +// ❗ 必须同时持有读端与写端:写端若被 GC 回收,读端会收到 EOF 而非阻塞, +// 消费循环变成忙转。故用 pipePair 表存住两端。 +type pipePair struct { + r *os.File + w *os.File +} + +var ( + evtPipes = map[int]*pipePair{} + evtPipesMu sync.Mutex +) + +// evtfdCreate 建 pipe,返回读端 fd。 func evtfdCreate() (int, error) { r, w, err := os.Pipe() if err != nil { return -1, err } - return int(r.Fd()), nil + fd := int(r.Fd()) + evtPipesMu.Lock() + evtPipes[fd] = &pipePair{r: r, w: w} + evtPipesMu.Unlock() + return fd, nil } -// evtfdNotify 写 1 字节通知子进程有新事件(post-and-forget)。 +// EvtfdNotify 写 1 字节通知子进程有新事件(post-and-forget)。 +// +// 直接写裸 fd 而非 pipePair.w:本函数在 Bus.Publish 路径上被高频调用, +// 查表加锁不值得。写端 fd 由 pipePair 持有引用故不会被 GC 回收。 func EvtfdNotify(efd int) { + evtPipesMu.Lock() + p, ok := evtPipes[efd] + evtPipesMu.Unlock() + if !ok { + return + } var buf [1]byte - syscall.Write(efd, buf[:]) + // 忽略错误:管道满说明消费者落后,事件环本身允许溢出丢弃 + syscall.Write(int(p.w.Fd()), buf[:]) } -// evtfdReadFile 把事件通知读端包装成 *os.File 供 netpoller 消费。 +// evtfdReadFile 返回通知读端(供 netpoller 消费)。 func evtfdReadFile(efd int) *os.File { - return os.NewFile(uintptr(efd), "evtring-notify") + evtPipesMu.Lock() + defer evtPipesMu.Unlock() + if p, ok := evtPipes[efd]; ok { + return p.r + } + return nil +} + +// evtfdClose 关闭 pipe 两端。 +func evtfdClose(efd int) { + evtPipesMu.Lock() + p, ok := evtPipes[efd] + delete(evtPipes, efd) + evtPipesMu.Unlock() + if ok { + p.r.Close() + p.w.Close() + } } diff --git a/internal/plugin/proc/evtfd_linux.go b/internal/plugin/proc/evtfd_linux.go index 18a5fbe..11ba3a2 100644 --- a/internal/plugin/proc/evtfd_linux.go +++ b/internal/plugin/proc/evtfd_linux.go @@ -33,3 +33,6 @@ func EvtfdNotify(efd int) { func evtfdReadFile(efd int) *os.File { return os.NewFile(uintptr(efd), "evtring-notify") } + +// evtfdClose 关闭通知句柄。Unix 侧由 *os.File.Close 负责,此处为跨平台签名占位。 +func evtfdClose(efd int) {} diff --git a/internal/plugin/proc/evtfd_other.go b/internal/plugin/proc/evtfd_other.go index ea8349e..60ce3b4 100644 --- a/internal/plugin/proc/evtfd_other.go +++ b/internal/plugin/proc/evtfd_other.go @@ -1,4 +1,4 @@ -//go:build !linux && !darwin +//go:build !linux && !darwin && !windows package proc @@ -16,3 +16,6 @@ type errPlatformNotSupported string func (e errPlatformNotSupported) Error() string { return "当前平台尚未支持事件环通知(" + string(e) + ",§9.2)" } + +// evtfdClose 关闭通知句柄。Unix 侧由 *os.File.Close 负责,此处为跨平台签名占位。 +func evtfdClose(efd int) {} diff --git a/internal/plugin/proc/evtfd_windows.go b/internal/plugin/proc/evtfd_windows.go new file mode 100644 index 0000000..07cba3c --- /dev/null +++ b/internal/plugin/proc/evtfd_windows.go @@ -0,0 +1,103 @@ +//go:build windows + +package proc + +import ( + "fmt" + "os" + "sync" + "sync/atomic" + + "golang.org/x/sys/windows" +) + +// Windows 侧事件通知:命名 Event 对象。 +// +// 与 eventfd 的语义差异:Event 是二元信号(Set/Reset),不是计数器。 +// 多次 SetEvent 只对应一次唤醒,不会累积。 +// +// 这不影响正确性:消费者被唤醒后按 readSeq 追 writeSeq 批量 drain, +// 一次唤醒能处理累积的全部事件(漏掉的是"唤醒次数",不是"事件")。 +// 事件环本身允许溢出丢弃并让消费者知道丢了(dropped 计数), +// 通知面从来不是可靠投递语义。 +// +// 代价:WaitForSingleObject 阻塞 OS 线程而非仅 goroutine,不如 eventfd +// 的 netpoller 路径省线程。每插件一个消费 goroutine,17 插件即 17 线程 +// (实验 5 实测 17 子进程共 84 线程,仍在可接受范围)。 +var evtEventSeq atomic.Uint64 + +// evtEventHandles 记录 fd 伪值 → Event 句柄的映射。 +// +// 为何需要:跨平台签名用 int 表示通知句柄(Unix 是真 fd)。 +// Windows 的 windows.Handle 是 uintptr,直接转 int 在 32 位上会截断, +// 故用递增伪 fd 做 key,句柄存表里。 +var ( + evtEvents = map[int]windows.Handle{} + evtEventsMu sync.Mutex + evtEventFd atomic.Int64 +) + +// evtfdCreate 创建命名 Event 对象,返回伪 fd。 +// +// 手动重置(manualReset=false → 自动重置):被一个等待者唤醒后自动 Reset, +// 语义最接近 eventfd 的"取出后清零"。 +func evtfdCreate() (int, error) { + name := fmt.Sprintf("%s_%d_%d", evtEventNamePfx, os.Getpid(), evtEventSeq.Add(1)) + namePtr, err := windows.UTF16PtrFromString(name) + if err != nil { + return -1, fmt.Errorf("proc: 事件对象名字非法 %q: %w", name, err) + } + h, err := windows.CreateEvent(nil, 0 /*autoReset*/, 0 /*initiallyNonSignaled*/, namePtr) + if err != nil { + return -1, fmt.Errorf("proc: 创建事件对象 %q: %w", name, err) + } + + fd := int(evtEventFd.Add(1)) + evtEventsMu.Lock() + evtEvents[fd] = h + evtEventNames[fd] = name + evtEventsMu.Unlock() + return fd, nil +} + +// evtEventNames 记录伪 fd → 对象名(供注入子进程环境变量)。 +var evtEventNames = map[int]string{} + +// EvtfdNotify 唤醒等待者(post-and-forget)。 +// +// SetEvent 不阻塞,满足 §3.6 约束 B(Bus.Publish 路径上绝不等待)。 +func EvtfdNotify(efd int) { + evtEventsMu.Lock() + h, ok := evtEvents[efd] + evtEventsMu.Unlock() + if !ok { + return + } + // 忽略错误:句柄有效时 SetEvent 不会失败 + windows.SetEvent(h) +} + +// evtfdReadFile 在 Windows 上返回 nil。 +// +// 内核侧不消费事件环(只写入),消费在插件进程里由模板的 +// windowsEvtWaiter 完成。这个函数只为跨平台签名存在。 +func evtfdReadFile(efd int) *os.File { return nil } + +// evtEventNameOf 返回某个伪 fd 对应的 Event 对象名(供注入子进程环境变量)。 +func evtEventNameOf(efd int) string { + evtEventsMu.Lock() + defer evtEventsMu.Unlock() + return evtEventNames[efd] +} + +// evtfdClose 关闭 Event 句柄。 +func evtfdClose(efd int) { + evtEventsMu.Lock() + h, ok := evtEvents[efd] + delete(evtEvents, efd) + delete(evtEventNames, efd) + evtEventsMu.Unlock() + if ok { + windows.CloseHandle(h) + } +} diff --git a/internal/plugin/proc/evtring.go b/internal/plugin/proc/evtring.go index aeddbdc..604164d 100644 --- a/internal/plugin/proc/evtring.go +++ b/internal/plugin/proc/evtring.go @@ -68,8 +68,8 @@ func evtTypeMask(types ...pubsdk.EventType) uint32 { const ( evtRingMagic uint32 = 0x48455654 // "HEVT" evtRingVersion uint32 = 1 - evtRingCap uint32 = 8192 // 2^13,满足流式场景突发(实验 4) - evtRingSlotLen uint32 = 32 // seq(8)+type(4)+off(4)+len(4)+pad(12) + evtRingCap uint32 = 8192 // 2^13,满足流式场景突发(实验 4) + evtRingSlotLen uint32 = 32 // seq(8)+type(4)+off(4)+len(4)+pad(12) evtOffMagic uint32 = 0 evtOffVersion uint32 = 4 diff --git a/internal/plugin/proc/host.go b/internal/plugin/proc/host.go index 7efa044..5c50955 100644 --- a/internal/plugin/proc/host.go +++ b/internal/plugin/proc/host.go @@ -31,10 +31,11 @@ type Host struct { shmSize int // 事件环段(独立于 StageContext) - evtfd *os.File // eventfd fd(fd 5 的句柄,子进程读取消费) - evtRing *EvtRing // 内核侧事件环句柄 - evtRingFd *os.File // 事件环段 memfd(fd 4,子进程 mmap 读事件) - evtData []byte // 事件环段 mmap 数据 + evtfd *os.File // Unix:eventfd/pipe 读端(fd 5)。Windows 为 nil,用 evtNotifyFd 。 + evtNotifyFd int // 通知句柄的平台无关标识(Unix 是真 fd,Windows 是伪 fd) + evtRing *EvtRing // 内核侧事件环句柄 + evtRingFd *os.File // Unix:事件环段 memfd(fd 4)。Windows 为 nil(命名段)。 + evtData []byte // 事件环段 mmap 数据 // evtSubscriber 由 internal/plugin 注入,coreHandler 用它接子进程的 events.subscribe 请求。 // proc 包不依赖 internal/plugin(循环依赖),故用接口类型存储。 @@ -48,11 +49,13 @@ type Host struct { // NewHost 创建共享段(平台层 allocShm + 布局初始化)。 // -// 段的分配按平台分开(shmalloc_*.go):Linux 用 memfd,macOS 用 -// 立即 unlink 的临时文件(无 memfd_create),其余平台明确报错。 -// 两者语义一致:无文件名残留,fd 可经 ExtraFiles 传给子进程, -// 子进程 mmap 同一 inode——「全部插件共享一块段」的前提得以成立。 -// 实验 2 已验证父子 mmap 到不同虚拟地址时相对偏移仍正确解引用。 +// 段的**传递机制**按平台分开(shmalloc_*.go),但**布局**完全一致: +// - Linux:memfd,经 ExtraFiles 传继承 fd +// - macOS:立即 unlink 的临时文件(无 memfd_create),同样走 fd 继承 +// - Windows:命名 FileMapping(无 fd 继承语义),插件按名字打开 +// +// 三者共同点:全部插件看到同一份物理页,段内一律用相对偏移而非指针 +// (实验 2 已验证各进程 mmap 到不同虚拟地址时偏移解引用仍正确)。 func NewHost() (*Host, error) { memfd, data, err := allocShm(shmDefaultSize) if err != nil { @@ -78,15 +81,16 @@ func NewHost() (*Host, error) { evtRing.Init() return &Host{ - memfd: memfd, - data: data, - seg: seg, - shmSize: shmDefaultSize, - evtfd: evtfdReadFile(efd), - evtRing: evtRing, - evtRingFd: evtRingFd, - evtData: evtData, - locks: &lockRegistry{}, + memfd: memfd, + data: data, + seg: seg, + shmSize: shmDefaultSize, + evtfd: evtfdReadFile(efd), + evtNotifyFd: efd, + evtRing: evtRing, + evtRingFd: evtRingFd, + evtData: evtData, + locks: &lockRegistry{}, }, nil } @@ -117,6 +121,7 @@ func (h *Host) Close() error { h.evtfd.Close() h.evtfd = nil } + evtfdClose(h.evtNotifyFd) return firstErr } @@ -240,9 +245,16 @@ func (h *Host) ShmSize() int { return h.shmSize } // EvtRing 返回内核侧事件环句柄。 func (h *Host) EvtRing() *EvtRing { return h.evtRing } -// Evtfd 返回 eventfd 的 *os.File(供 EventRing 写通知)。 +// Evtfd 返回通知读端的 *os.File(Unix;eventfd/pipe)。 +// Windows 返回 nil——命名 Event 不是文件句柄,用 EvtNotifyFd 代替。 func (h *Host) Evtfd() *os.File { return h.evtfd } +// EvtNotifyFd 返回通知句柄的平台无关标识,供 EventRing 写通知。 +// +// Unix 是真 fd;Windows 是映射到命名 Event 句柄的伪 fd。 +// EvtfdNotify 接受这个值并按平台分派。 +func (h *Host) EvtNotifyFd() int { return h.evtNotifyFd } + // SetEvtSubscriber 注入事件环订阅接口(由 Registry 在创建 Host 后设置)。 func (h *Host) SetEvtSubscriber(sub EvtRingSubscriber) { h.evtSubscriber = sub } diff --git a/internal/plugin/proc/plugin.go b/internal/plugin/proc/plugin.go index c20a68f..eac7b75 100644 --- a/internal/plugin/proc/plugin.go +++ b/internal/plugin/proc/plugin.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "log" - "os" "sync" pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" @@ -69,10 +68,10 @@ func (p *Plugin) Start(core CoreSDK) error { } p.handler = &coreHandler{ - sdk: core, - name: p.name, - host: p.host, - locks: p.host.locks, + sdk: core, + name: p.name, + host: p.host, + locks: p.host.locks, evtRing: p.host.evtSubscriber, } // 反向调用闭包:注册回调时捕获,运行期经 RPC 打到插件进程。 @@ -82,12 +81,15 @@ func (p *Plugin) Start(core CoreSDK) error { proc, err := Spawn(p.name, p.bin, Options{ Dir: p.dir, - Env: p.env, - // 子进程 fd 布局:3=StageContext 段,4=事件环段,5=eventfd - ExtraFiles: []*os.File{p.host.memfd, p.host.evtRingFd, p.host.evtfd}, - ShmSize: p.host.shmSize, - Handler: p.handler.Handle, - OnExit: p.handleExit, + // 共享段的传递机制按平台不同(shmpass_*.go): + // Unix 经 ExtraFiles 传继承 fd( 3=StageContext, 4=事件环, 5=通知); + // Windows 无 fd 继承语义,改用命名内核对象,名字经环境变量传入。 + Env: append(p.env, p.host.procEnvForShm()...), + ExtraFiles: p.host.procExtraFilesForShm(), + ShmSize: p.host.shmSize, + EvtRingSize: evtTotalSize, + Handler: p.handler.Handle, + OnExit: p.handleExit, }) if err != nil { return err diff --git a/internal/plugin/proc/process.go b/internal/plugin/proc/process.go index 16e3102..0c04f50 100644 --- a/internal/plugin/proc/process.go +++ b/internal/plugin/proc/process.go @@ -62,6 +62,8 @@ type Process struct { // shmSize 是握手时告知插件的共享段大小(0 表示本插件不用共享段)。 shmSize int + // evtRingSize 是事件环段大小(0 表示不支持事件环)。 + evtRingSize int } // RequestHandler 处理插件 → 内核的调用。 @@ -79,6 +81,8 @@ type Options struct { ExtraFiles []*os.File // ShmSize 是共享段大小,握手时告知插件(与 ExtraFiles[0] 的 memfd 对应)。 ShmSize int + // EvtRingSize 是事件环段大小(0 表示不支持事件环)。 + EvtRingSize int // Handler 处理插件反向调用。 Handler RequestHandler // OnExit 进程退出回调。 @@ -127,18 +131,19 @@ func Spawn(name, bin string, opts Options) (*Process, error) { } p := &Process{ - name: name, - bin: bin, - dir: opts.Dir, - cmd: cmd, - stdin: bufio.NewWriter(stdinPipe), - stdout: stdoutPipe, - pending: make(map[uint64]chan *Response), - handler: opts.Handler, - exited: make(chan struct{}), - ready: make(chan struct{}), - onExit: opts.OnExit, - shmSize: opts.ShmSize, + name: name, + bin: bin, + dir: opts.Dir, + cmd: cmd, + stdin: bufio.NewWriter(stdinPipe), + stdout: stdoutPipe, + pending: make(map[uint64]chan *Response), + handler: opts.Handler, + exited: make(chan struct{}), + ready: make(chan struct{}), + onExit: opts.OnExit, + shmSize: opts.ShmSize, + evtRingSize: opts.EvtRingSize, } if err := cmd.Start(); err != nil { @@ -190,6 +195,7 @@ func (p *Process) handshake(timeout time.Duration) error { PluginName: p.name, ShmVersion: shmVersion, ShmSize: p.shmSize, + EvtRingSize: p.evtRingSize, }) if err != nil { return fmt.Errorf("proc: %s 握手失败: %w", p.name, err) diff --git a/internal/plugin/proc/protocol.go b/internal/plugin/proc/protocol.go index 74515a3..0b3a7de 100644 --- a/internal/plugin/proc/protocol.go +++ b/internal/plugin/proc/protocol.go @@ -153,10 +153,10 @@ type HandshakeParams struct { Protocol int `json:"protocol"` // 内核支持的协议版本 CoreVersion string `json:"core_version"` // 内核版本(诊断用) PluginName string `json:"plugin_name"` // 内核分配的插件名 - // ShmVersion 让插件确认共享段布局一致;不匹配时插件应拒绝启动而非错读。 - ShmVersion uint32 `json:"shm_version"` - // ShmSize 是内核分配的共享段大小,插件据此 mmap(段本身经 fd 3 传入)。 - ShmSize int `json:"shm_size"` + ShmVersion uint32 `json:"shm_version"` + ShmSize int `json:"shm_size"` + // EvtRingSize 是事件环段大小(0 表示不支持事件环)。插件据此 mmap fd 4。 + EvtRingSize int `json:"evt_ring_size,omitempty"` } // HandshakeResult 是插件 → 内核的建链应答:上报自身信息。 diff --git a/internal/plugin/proc/shmalloc_other.go b/internal/plugin/proc/shmalloc_other.go index 06175e1..abb7b30 100644 --- a/internal/plugin/proc/shmalloc_other.go +++ b/internal/plugin/proc/shmalloc_other.go @@ -1,4 +1,4 @@ -//go:build !linux && !darwin +//go:build !linux && !darwin && !windows package proc diff --git a/internal/plugin/proc/shmalloc_windows.go b/internal/plugin/proc/shmalloc_windows.go new file mode 100644 index 0000000..8ed9d1e --- /dev/null +++ b/internal/plugin/proc/shmalloc_windows.go @@ -0,0 +1,137 @@ +//go:build windows + +package proc + +import ( + "fmt" + "os" + "sync" + "sync/atomic" + "unsafe" + + "golang.org/x/sys/windows" +) + +// Windows 侧共享段:命名 FileMapping + 命名 Event。 +// +// 与 Unix 的机制差异(不是能力差异): +// Windows 没有 fd 继承语义——os/exec 的 ExtraFiles 在 Windows 实现里不被支持。 +// 等价机制是命名内核对象:父进程 CreateFileMappingW 建带名字的段, +// 子进程 OpenFileMappingW 按同名打开,拿到同一份物理页。 +// +// **这是 §9.2 的正解**。C ABI 时代 Windows 是第三套独立 ABI 实现 +// (dynamic_dll_windows.go),stage 只下发 3 字段且完全没有写回, +// sanitizer 这类改写型插件静默失效。三套 ABI 收敛为单一 RPC 后, +// Windows 与 Unix 共用同一份 stage 逻辑与同一份共享段布局, +// 平台差异只剩本文件的创建端 + 插件侧模板的打开端。 +// +// 名字带 PID 与递增序号:多个 homed 实例并存时不能撞名, +// 同一实例内 StageContext 段与事件环段也必须分开。 +var shmNameSeq atomic.Uint64 + +const ( + shmNamePrefix = "Local\\HomeAgentShm" + evtRingNamePfx = "Local\\HomeAgentEvtRing" + evtEventNamePfx = "Local\\HomeAgentEvtSignal" + envStageShmName = "HOMEAGENT_SHM_STAGE" + envEvtRingName = "HOMEAGENT_SHM_EVTRING" + envEvtEventName = "HOMEAGENT_EVT_EVENT" +) + +// namedShm 持有一块命名共享段。 +// +// 不用 *os.File 承载:Windows 的 FileMapping 句柄不是文件句柄, +// 包进 os.File 后 Close 语义不对(会尝试当文件关)。故用独立类型, +// 由 shmHandles 表按 mmap 地址反查——freeShm 只拿到 (*os.File, []byte)。 +type namedShm struct { + name string + mapping windows.Handle + addr uintptr + size int +} + +// shmHandles 记录已分配的段,供 freeShm 按数据指针反查句柄。 +// +// 为何需要这张表:allocShm 的跨平台签名返回 (*os.File, []byte), +// Windows 没有对应的 fd,只能把句柄存在旁路。key 用切片首地址。 +var ( + shmHandles = map[uintptr]*namedShm{} + shmHandlesMu sync.Mutex +) + +// allocShm 创建命名共享段并映射。 +// +// 返回的 *os.File 为 nil:Windows 不经 fd 传递段,插件按名字打开。 +// 名字通过 procEnvForShm 注入子进程环境变量。 +func allocShm(size int) (*os.File, []byte, error) { + name := fmt.Sprintf("%s_%d_%d", shmNamePrefix, os.Getpid(), shmNameSeq.Add(1)) + 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 +} + +// createNamedMapping 建命名段并映射为 []byte。 +func createNamedMapping(name string, size int) (*namedShm, []byte, error) { + namePtr, err := windows.UTF16PtrFromString(name) + if err != nil { + return nil, nil, fmt.Errorf("proc: 共享段名字非法 %q: %w", name, err) + } + + // INVALID_HANDLE_VALUE + 命名 → 由系统页文件支撑的匿名段(不落盘) + mapping, err := windows.CreateFileMapping( + windows.InvalidHandle, nil, windows.PAGE_READWRITE, + uint32(uint64(size)>>32), uint32(size), namePtr) + if err != nil { + return nil, nil, fmt.Errorf("proc: 创建命名共享段 %q: %w", name, err) + } + + addr, err := windows.MapViewOfFile(mapping, windows.FILE_MAP_WRITE, 0, 0, uintptr(size)) + if err != nil { + windows.CloseHandle(mapping) + return nil, nil, fmt.Errorf("proc: 映射共享段 %q: %w", name, err) + } + + return &namedShm{name: name, mapping: mapping, addr: addr, size: size}, + unsafe.Slice((*byte)(unsafe.Pointer(addr)), size), nil +} + +// freeShm 解除映射并关闭段句柄。 +func freeShm(f *os.File, data []byte) error { + if len(data) == 0 { + return nil + } + key := uintptr(unsafe.Pointer(&data[0])) + shmHandlesMu.Lock() + shm, ok := shmHandles[key] + delete(shmHandles, key) + shmHandlesMu.Unlock() + if !ok { + return nil + } + var firstErr error + if err := windows.UnmapViewOfFile(shm.addr); err != nil { + firstErr = err + } + if err := windows.CloseHandle(shm.mapping); err != nil && firstErr == nil { + firstErr = err + } + return firstErr +} + +// shmNameOf 返回某块已分配段的名字(供注入子进程环境变量)。 +func shmNameOf(data []byte) string { + if len(data) == 0 { + return "" + } + shmHandlesMu.Lock() + defer shmHandlesMu.Unlock() + if shm, ok := shmHandles[uintptr(unsafe.Pointer(&data[0]))]; ok { + return shm.name + } + return "" +} diff --git a/internal/plugin/proc/shmpass_unix.go b/internal/plugin/proc/shmpass_unix.go new file mode 100644 index 0000000..daec8b2 --- /dev/null +++ b/internal/plugin/proc/shmpass_unix.go @@ -0,0 +1,21 @@ +//go:build linux || darwin || freebsd + +package proc + +import "os" + +// procEnvForShm 在 Unix 返回 nil:段经继承 fd 传递,无需环境变量。 +func (h *Host) procEnvForShm() []string { return nil } + +// procExtraFilesForShm 返回经 ExtraFiles 传给子进程的 fd 列表。 +// +// 顺序即 fd 编号(cmd.ExtraFiles[0] → 子进程 fd 3): +// +// fd 3 = StageContext 段 +// fd 4 = 事件环段 +// fd 5 = 事件通知(eventfd / pipe 读端) +// +// 插件侧模板 z_proc_shm_unix.go 的常量与此严格对应。 +func (h *Host) procExtraFilesForShm() []*os.File { + return []*os.File{h.memfd, h.evtRingFd, h.evtfd} +} diff --git a/internal/plugin/proc/shmpass_windows.go b/internal/plugin/proc/shmpass_windows.go new file mode 100644 index 0000000..66e9b44 --- /dev/null +++ b/internal/plugin/proc/shmpass_windows.go @@ -0,0 +1,26 @@ +//go:build windows + +package proc + +import ( + "fmt" + "os" +) + +// 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)), + } +} + +// procExtraFilesForShm 在 Windows 返回 nil:段不经 fd 传递。 +func (h *Host) procExtraFilesForShm() []*os.File { return nil }