|
|
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 |
|
|
|
ba367ddf67
|
test(proc): 修 grandchild 测试的三个设计缺陷(不是生产代码问题)
## 定位结论
`TestKillReturnsEvenWhenGrandchildSurvives` 曾在 `go test ./...`(600s 超时)
与 `make test`(20.4s FAIL)里失败,但**单跑 0.24s 通过**、连跑 3 次全绿
⇒ 「单跑绿、合跑红」。查下来是**三个测试设计缺陷**,
生产代码(`process.go`)没问题。
### 缺陷 1:名字说 Survives,实际测的是「被杀」
| 测试 | 源 | 孙进程 | kill(-pgid) 能杀吗 |
| --- | --- | --- | --- |
| …EvenWhenGrandchildSurvives | grandchildPluginSource | sleep 400,**不**设 Setpgid | **能** |
| …WhenGrandchildEscapesProcessGroup | escapingGrandchildSource | sleep 401 + Setsid | **不能** |
`grandchildPluginSource` 自己的注释写着「孙进程**不**设 Setpgid:它要留在
插件的进程组里」⇒ 第一个测试里孙进程不会 Survive。容易让人误以为
「脱组场景已被覆盖」,而它覆盖的是另一个场景。
**已改名** `…WhenGrandchildDiesWithProcessGroup`。
### 缺陷 2:判据数的是全系统进程
两个计数器扫 `/proc` 找 `"sleep 400"` / `"sleep 401"` 字符串,
**不区分父子关系** ⇒ 同机任何命中同样 cmdline 的进程/容器都串味。
原注释记过一次前车之鉴(「我第一版就踩了:明明单跑通过,合跑却红」),
但当时只加了 base 快照,**没解决全局匹配这个根因** —— base 救不了
「别的测试中途拉起 sleep 400」。
**已修**:新增 `procPPid()`,两个计数器都限定 PPid 属于本测试的插件。
顺带补 `e.Name()` 的 `Atoi` 校验(原来会把 /proc/self、/proc/net 也读一遍)。
### 缺陷 3:defer 清理「拿不到 pid 就整个跳过」
`if pid := pluginPid(p); pid > 0 { Kill }` 在 pid 取不到时静默跳过
⇒ 残留 sleep 400 污染后续测试 ⇒ 变成下一个测试的假失败。
**已修**:新增 `cleanupSleepMarkers(marker)`,按唯一 cmdline 标记兜底清理。
## ★ 我被推翻的一个假设
我一度认定根因是 `waitLoop` 里 `p.cmd.Wait()` **先阻塞**、拆管道在**之后**
(`process.go:359-367`)—— Go 的 `exec` 里 `Wait()` 会等 copy goroutine,
而那些要等所有管道写端关闭,孙进程持有着 ⇒ 死锁。
**实测推翻了它**:把拆管道提到 `Wait` 之前,那个测试 **5 次全 FAIL**
(改前只是偶发)。真正的根因是上面三个测试设计问题;「Wait 阻塞」只是
**被孙进程持管道放大**的效应。
⇒ 改生产代码不但没修好,还把偶发变成必现。**先证明因果再动手。**
## 判据:grandchild_design_test.go(4 条)
★ 它是**查源码文本**的,我一改源文件(改名/加 ppid 限定/加兜底清理),
锚点就全过期 ⇒ 三条判据一起红。
⇒ 判据自己被重构打断时,要改的是**判据的锚点**(认新旧两种形态),
不是回退修复。最后把判据①从「解函数体比对 spawn 参数」简化为
「只问名字是否还说 Survives」—— 少耦合一层,少失效一处。
变异测试三个都抓到:改名回 Survives / 抽掉 ppid 限定 / 去掉兜底清理。
## 门禁
- 全量 `go test ./...`:**43 包 ok、0 FAIL**
- `internal/plugin/proc` 连跑 **5 次全绿**(原来会红的地方)
- `go test -race ./internal/plugin/proc/`:ok
- 无 sleep 400/401 残留
## 顺带
`9e76132` 之后 README 的 biome 格式化不再 churn:仓库**没有** biome 配置,
格式来自流水线默认 ⇒ 每次提交后它都会把工作区改脏。我这次会话里反复
`git checkout --` 把它丢掉,那是和流水线对抗。**提交后 biome 再跑就是
no-op**,问题根除。
|
2026-09-28 10:00:10 +08:00 |
|
|
|
d6157b67d5
|
fix(proc): 杀插件进程组 + readerWG 超时兜底 —— 修孙进程拖死关停
## 现象
线上关停必超时:systemd 报 `State 'stop-sigterm' timed out. Killing.`,
进程组里 23 个插件全退完了,最后那条 `[homed] stopped` 仍打不出来。
其中只有 bili 报 `[proc] bili SIGKILL 后 2s 仍未被收割`,之后近 90 秒无日志。
## 根因
bili 用 exec.Command 拉 yt-dlp(源码 example/bili/plugin.go:122/212),
无 CommandContext、无 Setpgid、Stop() 是空的。yt-dlp 再 fork ffmpeg,
**孙进程继承插件的 stdout 管道写端**。
插件被 SIGKILL → 孙进程仍存活、写端不关
→ readLoop 的 scanner.Scan() 永不 EOF
→ p.readerWG.Wait() 永不返回(Kill 的最后一行,**无超时**)
→ StopAll 的 wg.Wait() 永不返回 ⇒ 关停挂死 ⇒ systemd SIGKILL
内核 process.go:340 的注释早已预警过这个场景("插件 fork 的孙子进程继承
同一 stdout 写端时,插件本体死了 EOF 也不会到"),但 Kill 没有对应保护。
## 内核三处修法(缺任一条都不够)
1. **spawn 时 Setpgid**:插件自成进程组,不再与内核同组
2. **Kill 杀整个进程组**(kill(-pgid)):孙进程一起死,管道写端才关。
兜底:负 pid 失败时退回杀本体(老插件/非 Unix 平台)
3. **readerWG.Wait() 加超时兜底**:这是唯一能保证 Kill 一定返回的地方。
超时后主动关读端逼 readLoop 退出,再兜一层仍不退就放弃等待 ——
宁可少等 2 秒,也不能把关停无限期挂住。
## 插件侧(bili)
CommandContext + Setpgid + Stop() 里 cancel 并 wait:
- 只 cancel 不 wait 的话内核会先释放共享段,而 yt-dlp 还在写 stdout
- waitRunGroup 杀整个进程组(ffmpeg 也在内),不留孤儿
## 判据:5 条 + 3 组变异
判据用**真实模板编译的插件**(复用 buildPluginWithRealTemplate,
与 e2e_template_test 同一条路)+ NewHost 启动,不是自造 shim:
裸 Spawn 没有 Host 建共享内存段,插件握手会报 permission denied。
★ 判据自己踩了三次坑,都由变异/合跑抓出来:
1. 给孙进程也加 Setpgid ⇒ 它逃出插件进程组,kill(-pgid) 杀不到,
造出假失败(真实场景 yt-dlp 不会脱离进程组)
2. 各测试数全局孙进程数 ⇒ 前一个泄漏的被后一个数进去,
单跑通过、合跑变红。改为记录基线只关心自己新增的
3. readerWG 超时那条用纯构造 &Process{cmd:nil} ⇒ Kill 第 607 行
早退,根本走不到那段,撤掉超时照样绿。补了「脱组孙进程」
场景(Setsid 逃出进程组)才真正覆盖到
变异:去 Setpgid → 判红;只杀本体不杀组 → 判红。
全量 41 包绿。
|
2026-09-26 16:58:41 +08:00 |
|