Files
HomeAgent/internal/plugin/proc/host.go
JianFeeeee 6d38a164eb chore(module): Go module 路径随托管地迁移 gitcode → github
## 背景

仓库早已迁移到 GitHub(`github.com/JianFeeeee/HomeAgent`,CI/CD 都在那边跑),
gitcode 保留为**国内只读镜像**(`ci.yml` 的 sync-gitcode-main 快进推送)。
但 go.mod 的 module 路径一直是 `gitcode.com/JianFeeeee/...` —— 既与权威源
不符,`meta.SourceURL`(AGPL §13 的源码提供入口)也指向镜像而非实际运行的代码。

## 关键区分(这是本次改动安全的前提)

两个 module 是**不同**的,影响面差一个数量级:

| module | 谁依赖 | 改它的后果 |
|---|---|---|
| `.../HomeAgent`(主仓) | **只有主仓自己** | 内部重构,零外部影响 |
| `.../homeagent-sdk`(SDK 仓) | 主仓 49 文件 + 所有外部插件 | 需重编插件 |

★ 实测确认:**插件不 import 主仓 module**(`strings qq/plugin.bin | grep HomeAgent` = 0),
只依赖 SDK 仓。而插件与内核的边界是 **RPC 方法名字符串**
(`plugin.init` / `tool.invoke`…,见 proc/protocol.go),**与 Go module 名无关**。
⇒ module 改名**不改变任何 wire 协议**,现网已装的插件照常运行。

SDK 的 GitHub 仓名是 `homeagentsdk`(无连字符),module 路径必须与之精确匹配,
否则 `go get` 会因路径不符失败,故取 `github.com/JianFeeeee/homeagentsdk`。

## 改动

- `go.mod` / `go.sum`:两个 module 路径 + require/replace
- 277 个 Go 文件 + Makefile + 打包脚本的 import 与 ldflags 路径
- SDK 仓 `meta.CoreModule`、`tools/hmapdev` 的 module 声明与工程模板
- 对外文档/链接的仓库地址(README、PLUGIN_DEV、OVERVIEW 等)

## 明确**保留** gitcode 的地方(镜像机制,不能动)

- `GITCODE_TOKEN`、`GC_URL="…@gitcode.com"`(CI 同步 job)
- `gitcode.com/api/v5/...`(release 附件上传)
- git remote `gitcode`(镜像拉取/推送)
- `docs/zh/ci-cd-runbook.md`(整篇讲镜像运维)

## 验证

- `go build ./...` / `go vet ./...` 绿
- `go test ./... -count=1` → 48 包 ok / 0 FAIL
- hmapdev 工具链编译通过(`third_party/homeagent-sdk/tools/hmapdev`)
- 全仓残留检查:module 形态的 `gitcode.com/JianFeeeee` = 0
- 镜像机制抽验:GC_URL / @gitcode / api/v5 / remote 均完好

## 顺带修正

`hmapdev/templates.go` 的注释此前写「gitcode 的模块不在 proxy.golang.org 上」——
module 已迁 GitHub,该论断不再成立。改为如实说明**为何仍默认 replace 到本机 SDK**:
离线可构建、避免版本漂移、内网可用。理由与托管地无关,故 replace 保留。
2026-10-08 10:40:17 +08:00

392 lines
14 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.

package proc
import (
"fmt"
"log"
"os"
"sync"
"sync/atomic"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
)
// Host 持有**被全部子进程插件共享的统一内存区域**,是共享数据面的
// 所有权中心(§3.3/§3.4)。
//
// ❗ 为什么必须共享一块区域:
// 若每个插件各持一块段,则「内核 ctx → 段 → 插件改 → 回读 ctx」退化成
// 副本模型——两个插件各写各的段、各自回读,最后回读者覆盖前者,
// lost update 原样复现(§8.4 实测 35.8~36.8%)。
//
// 统一区域(§13.1):单 memfd 包含 SuperBlock + StageContext + EvtRing
// + Exchange Arena。子进程经 fd 3 mmap 同一 memfd → 同一份物理页,
// 区域头 SuperBlock 告知各 segment 的偏移与大小。
//
// fd 分配:fd 3 = 统一区域,fd 4 = eventfd。
type Host struct {
memfd *os.File
data []byte // 统一区域完整 mmap
unified *unifiedRegion // SuperBlock 解析结果
seg *Segment // StageContext segment(位于 unified ctxData)
shmSize int // 统一区域总大小
// arena 是跨进程共享内存分配器(§13.2 重设计),由内核独占管理:
// 插件通过 RPC 申请/归还,不做任何分配决策。
//
// 分配与回收都在内核进程内进行,一把 mu 即可保证安全,
// 不存在跨进程分配器那种“共享游标被两个进程各自更新”的竞态。
arena *arenaRegion
// nextOwner 给每个插件进程分配一个不透明 owner ID,供 ReclaimOwner 使用。
nextOwner atomic.Uint32
// 事件通知(独立于共享段)
evtfd *os.File // Unix:eventfd/pipe 读端(fd 4)。Windows 为 nil。
evtNotifyFd int // 通知句柄的平台无关标识
evtRing *EvtRing // 内核侧事件环句柄(位于 unified evtData)
evtSubscriber EvtRingSubscriber
locks *lockRegistry
stageMu sync.Mutex
coordMu sync.Mutex
coord *stageCoordinator
sup *Supervisor
}
// NewHost 创建共享段(平台层 allocShm + 布局初始化)。
//
// 段的**传递机制**按平台分开(shmalloc_*.go),但**布局**完全一致:
// - Linux:memfd,经 ExtraFiles 传继承 fd
// - macOS:立即 unlink 的临时文件(无 memfd_create),同样走 fd 继承
// - Windows:命名 FileMapping(无 fd 继承语义),插件按名字打开
//
// 三者共同点:全部插件看到同一份物理页,段内一律用相对偏移而非指针
// (实验 2 已验证各进程 mmap 到不同虚拟地址时偏移解引用仍正确)。
func NewHost() (*Host, error) {
// 统一区域大小:SuperBlock + StageContext + EvtRing + Exchange Arena
//
// +8 是 arena 基址 8 字节对齐的 padding 余量:arenaOff 向上取整可能
// 吃掉最多 4 字节,预留 8 字节保证 arenaCap 不会小于 arenaPublishSize。
unifiedSize := superBlockSize + shmDefaultSize + evtTotalSize + int(arenaPublishSize) + 8
memfd, data, err := allocShm(unifiedSize)
if err != nil {
return nil, err
}
// 初始化 SuperBlock + 两个 segment
ur, err := initUnifiedRegion(data, shmDefaultSize, evtTotalSize)
if err != nil {
freeShm(memfd, data)
return nil, err
}
// 初始化 Exchange Arena(内核独占的变长块分配器)
arena, err := initArena(data, ur.arenaOff, ur.arenaCap)
if err != nil {
freeShm(memfd, data)
return nil, fmt.Errorf("共享内存分配器初始化: %w", err)
}
if used, total := arena.Stats(); used != 0 || total != ur.arenaCap {
freeShm(memfd, data)
return nil, fmt.Errorf("分配器初始化异常:used=%d total=%d", used, total)
}
// 创建 StageContext segment(位于 SuperBlock 之后)
seg, err := NewSegment(ur.ctxData())
if err != nil {
freeShm(memfd, data)
return nil, err
}
// 初始化 EvtRing 子区域头部(magic/version/cap)
evtData := ur.evtData()
putU32(evtData[evtOffMagic:], evtRingMagic)
putU32(evtData[evtOffVersion:], evtRingVersion)
putU32(evtData[evtOffCap:], evtRingCap)
// 创建 EvtRing segment(位于 StageContext 之后)
evtRing, err := NewEvtRing(evtData)
if err != nil {
freeShm(memfd, data)
return nil, fmt.Errorf("事件环初始化: %w", err)
}
evtRing.Init()
// eventfd 独立于共享段,仍为单独 fd
efd, err := evtfdCreate()
if err != nil {
freeShm(memfd, data)
return nil, fmt.Errorf("创建 eventfd: %w", err)
}
return &Host{
sup: NewSupervisor(),
memfd: memfd,
data: data,
unified: ur,
seg: seg,
shmSize: unifiedSize,
arena: arena,
evtfd: evtfdReadFile(efd),
evtNotifyFd: efd,
evtRing: evtRing,
locks: &lockRegistry{},
}, nil
}
// shmDefaultSize 是共享 StageContext 段的大小。
//
// 取 256KB:StageContext 全字段 JSON 化后典型 < 4KB(工具结果中位 93B,§2.5),
// append-only 中间垃圾由 stage 结束时 Compact 回收,256KB 给足余量。
// 全部插件共享一块,总开销恒定,不随插件数增长。
const shmDefaultSize = 256 * 1024
// Close 释放共享段(StageContext + 事件环)。
// Supervisor 返回子进程台账(供 registry 查询/关停)。
func (h *Host) Supervisor() *Supervisor { return h.sup }
func (h *Host) Close() error {
// 先停全部子进程再拆段:插件还持有映射时 unmap,
// 它们下一次访问共享段就是 SIGBUS。
if h.sup != nil {
h.sup.StopAll(0)
}
// ★ 必须在 unmap **之前**退掉事件环订阅。
//
// 那些订阅的 handler 会 ring.WritePush(写共享内存)。若让它们留在
// Bus 上,munmap 之后只要有一条事件经过 Publish,handler 就写已解除
// 映射的内存 ⇒ SIGSEGV。注意 Bus.safeCall 的 recover **捕不到**它
// (runtime 致命错误不是 panic),所以后果是整个 homed 被杀。
//
// 顺序要求:StopAll 之后(不再有新订阅进来)、freeShm 之前。
if c, ok := h.evtSubscriber.(evtCloser); ok && c != nil {
c.Close()
}
var firstErr error
if h.data != nil {
if err := freeShm(h.memfd, h.data); err != nil && firstErr == nil {
firstErr = err
}
h.data, h.memfd, h.unified = nil, nil, nil
}
if h.evtfd != nil {
h.evtfd.Close()
h.evtfd = nil
}
evtfdClose(h.evtNotifyFd)
return firstErr
}
// beginStage 由插件 handler 进入时调用。
//
// 首个进入者:获取 stageMu(独占共享段)→ 把内核 StageContext 写入段。
// 后续进入者:仅递增 inflight。
//
// enter() 在 coordMu 内完成,两个原因:
// 1. 首进者的 WriteAll 未结束前不能让后到者拿到 coord 就去读共享段
// (旧码的后到者 enter 立即返回,可能读到写一半的段)。
// 2. 与 endStage 的摘除互斥,防止后到者挂进一个正在收尾的协调器
// (具体见 endStage 的注释)。
//
// 锁序:stageMu → coordMu。endStage 只解锁 stageMu、不获取,所以无环。
func (h *Host) beginStage(sc *pubsdk.StageContext) (*stageCoordinator, error) {
h.coordMu.Lock()
if h.coord == nil {
// 首个进入者:独占共享段直到本次 stage 全部插件离开。
// 必须先放 coordMu 再取 stageMu,不能反序。
h.coordMu.Unlock()
h.stageMu.Lock()
h.coordMu.Lock()
if h.coord == nil {
coord := newStageCoordinator(h.seg)
h.coord = coord
if err := coord.enter(sc, true); err != nil {
// 注意:runStage 的 defer endStage(coord) 是在 beginStage
// 返回 err 的检查之后才注册的,所以这条路径上
// endStage 永远不会被调用——stageMu 必须在此自行释放,
// 否则整个 stage 通道永久卡死。
h.coord = nil
h.coordMu.Unlock()
h.stageMu.Unlock()
return nil, err
}
h.coordMu.Unlock()
h.locks.bind(coord.lock)
return coord, nil
}
// 双检失败:等锁期间已有其他插件建好协调器,退回后到者路径。
h.stageMu.Unlock()
}
coord := h.coord
if err := coord.enter(sc, false); err != nil {
h.coordMu.Unlock()
return nil, err
}
h.coordMu.Unlock()
h.locks.bind(coord.lock)
return coord, nil
}
// endStage 由插件 handler 返回时调用。
// 最后离开者:把共享段结果读回内核 StageContext → 压实 arena → 释放 stageMu。
//
// coordMu 必须覆盖「递减 inflight → 判定最后离开者 → 摘除 h.coord」全过程。
// 旧码把 leave() 放在 coordMu 之外,留出了这个窗口(即 2026-09-04 06:56:18
// 线上 fatal error: sync: unlock of unlocked mutex 的真因):
//
// A.endStage: leave() → inflight 1→0, last=true,尚未摘除 h.coord
// B.beginStage: 看到 h.coord != nil,以「后到者」身份 enter,inflight 0→1
// (后到者不取 stageMu)
// A.endStage: h.coord = nil;stageMu.Unlock() ← 第 1 次
// B.endStage: leave() → inflight 1→0, last=true → stageMu.Unlock() ← 第 2 次 💥
//
// B 从未持有 stageMu(它是后到者),却因为挂进了一个正在收尾的协调器
// 而成为“最后离开者”,于是对同一把锁解了两次。sync.Mutex 的双重解锁是
// runtime fatal,**recover 捕不到**——这就是为何 stage.go / stages.go 里
// 那两层 recover 全部失效、整个 homed 直接死掉的原因。
func (h *Host) endStage(coord *stageCoordinator) error {
h.coordMu.Lock()
last, sc, written := coord.depart()
if last && h.coord == coord {
h.coord = nil
}
h.coordMu.Unlock()
if !last {
return nil
}
// finish 必须在 stageMu.Unlock() 之前:先放锁会让下一轮 stage
// 在回读未完时就改写共享段。
err := coord.finish(sc, written)
h.stageMu.Unlock()
// ★ 先代为释放可能泄漏的锁,再摘除路由(2026-10-07)。
// 顺序很重要:先释放才有意义(锁已无人持有时本就没东西可释放),
// 而摘除必须最后——否则释放期间来的 stage.lock 会打到 nil 上。
//
// 这两件事缺一不可:
// · releaseIfLeaked —— 插件异常路径未 unlock 时,锁不会毒化下一轮
// · unbind —— 下一轮的 stage.lock 不得再打到本轮的旧锁上
if h.locks.releaseIfLeaked(coord.lock) {
log.Printf("[proc] stage 协调器结束时清理了泄漏的 stage 锁")
}
h.locks.unbind(coord.lock)
return err
}
// ForceReleaseLock 在插件进程崩溃时释放其可能持有的 stage 锁(实验 9 自愈机制)。
func (h *Host) ForceReleaseLock(plugin string) bool {
return h.locks.forceRelease(plugin)
}
// Segment 暴露共享段(供诊断与测试)。
func (h *Host) Segment() *Segment { return h.seg }
// stageCoordinator 跟踪一次 stage 执行中参与插件的进出。
type stageCoordinator struct {
seg *Segment
lock *stageLock
mu sync.Mutex
inflight int
written bool
ctxRef *pubsdk.StageContext
}
func newStageCoordinator(seg *Segment) *stageCoordinator {
return &stageCoordinator{seg: seg, lock: newStageLock()}
}
// enter 登记一个插件进入本次 stage;first 为真时把内核状态写入共享段。
func (c *stageCoordinator) enter(sc *pubsdk.StageContext, first bool) error {
c.mu.Lock()
defer c.mu.Unlock()
c.inflight++
if !first || c.written {
return nil
}
c.ctxRef = sc
if err := c.seg.WriteAll(sc); err != nil {
c.inflight--
return fmt.Errorf("写入共享段: %w", err)
}
c.written = true
return nil
}
// leave 登记一个插件离开;返回是否为最后一个离开者。
//
// 拆成两段:depart() 只动计数(由 endStage 在 coordMu 内调用,使
// 「递减 → 判定最后者 → 摘除 h.coord」成为原子操作),finish() 做
// 共享段回读与压实。本方法保留给单测用。
func (c *stageCoordinator) leave() (last bool, err error) {
last, sc, written := c.depart()
if !last {
return last, nil
}
return last, c.finish(sc, written)
}
// depart 递减 inflight 并报告是否为最后离开者。
func (c *stageCoordinator) depart() (last bool, sc *pubsdk.StageContext, written bool) {
c.mu.Lock()
defer c.mu.Unlock()
c.inflight--
return c.inflight == 0, c.ctxRef, c.written
}
// finish 把共享段结果读回内核 StageContext 并压实 arena
// (此时无插件持锁,满足 §3.3 的压实前提)。
func (c *stageCoordinator) finish(sc *pubsdk.StageContext, written bool) error {
if !written || sc == nil {
return nil
}
if rErr := c.seg.ReadInto(sc); rErr != nil {
return fmt.Errorf("回读共享段: %w", rErr)
}
if reclaimed := c.seg.Compact(); reclaimed > 0 {
log.Printf("[proc] stage 结束,arena 压实回收 %d 字节", reclaimed)
}
return nil
}
// Arena 返回跨进程共享槽池。
func (h *Host) Arena() *arenaRegion { return h.arena }
// Generation 返回统一区域当前 generation(SharedRef 校验用)。
func (h *Host) Generation() uint64 { return h.unified.generation() }
// NextOwnerID 分配一个插件专用的槽 owner ID。
//
// 从 1 开始(OwnerHost=0 保留给内核),单调递增,不会重复。
func (h *Host) NextOwnerID() uint32 { return h.nextOwner.Add(1) }
// ReclaimOwner 回收某个 owner 名下所有槽(插件退出时调用)。
func (h *Host) ReclaimOwner(owner uint32) int { return h.arena.ReclaimOwner(owner) }
// ShmSize 返回共享段大小(供诊断/日志)。
func (h *Host) ShmSize() int { return h.shmSize }
// EvtRing 返回内核侧事件环句柄。
func (h *Host) EvtRing() *EvtRing { return h.evtRing }
// 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 }
// EvtData 返回事件环段 mmap 数据(子进程消费者用)。
func (h *Host) EvtData() []byte { return h.unified.evtData() }
// EvtfdReadFile 返回 eventfd 的 *os.File(供子进程读取消费)。
func (h *Host) EvtfdReadFile() *os.File { return h.evtfd }