mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 01:18:08 +00:00
fix(test): 崩溃隔离测试误杀同机生产插件,且断言无效
## 现象
生产 homed 的 editdoc 子进程从 9-03 起被 SIGKILL 9 次,间隔完全不规律
(126~485 分钟),全部发生在无工具调用的空闲期。查过 OOM(dmesg/
journalctl -k/cgroup oom_kill 全为 0)、systemd 内存限制(MemoryMax=
infinity)、cron/timer、内核自身的 StopAll 路径、agent 执行过的 cmd_run
命令,以及 Pdeathsig 绑创建线程的可能——全部排除。
## 真因:测试杀了生产的进程
用 ftrace 的 signal_generate tracepoint 挂监视器后抓到发送者 cmdline:
/tmp/go-build.../plugins.test -test.run=TestRealPlugin_CrashDoesNotKillKernel
findPluginPID 用**全系统** `pgrep -f plugin.bin`,然后只比"exe 路径含
editdoc"。生产实例的 /home/newqqagent/plugins/editdoc/plugin.bin 也满足
这个条件,谁先被 pgrep 列出来就杀谁。9 次 kill 全部落在有人跑 go test
的时段——18:18:30 那次正是一轮 `go test ./... -race` 的窗口。
之前几轮排查一直在生产实例内部找原因,方向从一开始就错了:杀手在仓库里。
## 更严重的是这个测试本身无效
旧断言是"SIGKILL 之后内核仍存活"。可内核本来就活着——即使信号发错了
对象(杀了生产实例的插件),测试内核的插件压根没死,断言照样通过。
**它在测一件没发生的事**,同时把生产环境打坏了,而绿色的测试结果掩盖了
这一切。这也是它能连续 9 次造成生产故障却从没被注意到的原因。
## 修法
findPluginPID 增加 root 硬约束:/proc/<pid>/exe 必须以测试自己的 plgDir
为前缀,且是目标插件,两道条件同时成立才算命中。root 为空直接 t.Fatal
——这不是可选过滤器,是防误杀的前提。
- 用 exe 而非 cmdline:cmdline 可被进程自行改写,exe 符链由内核维护。
- root 先过 EvalSymlinks:/tmp 在部分发行版上是符链,不归一化会让前缀
比较永远不命中,退化成静默 Skip(那样测试就白跑了)。
断言改成两步:先轮询确认目标进程真的退出(3s 上限),再验内核未被连带。
两步都成立才能证明隔离生效。
## 验证
修复后跑 TestRealPlugin_CrashDoesNotKillKernel:
- 杀的是 pid=3448366,exe 在 /tmp/hc_integration_3823918128/plugins 下 ✓
- 生产 editdoc pid 测试前后均为 3362892,存活时长连续 ✓
- 四个 TestRealPlugin_* 全部 PASS
This commit is contained in:
@ -213,27 +213,68 @@ func TestRealPlugin_CrashDoesNotKillKernel(t *testing.T) {
|
||||
t.Fatal("editdoc 未加载")
|
||||
}
|
||||
|
||||
// 找到插件子进程并 SIGKILL
|
||||
pid := findPluginPID(t, "editdoc")
|
||||
// 找插件子进程并 SIGKILL。
|
||||
//
|
||||
// 必须拿 plgDir 限定范围:旧实现用全系统 pgrep -f plugin.bin 后
|
||||
// 只比“路径含 editdoc”,于是在跑着生产实例的机器上,它会把
|
||||
// /home/newqqagent/plugins/editdoc/plugin.bin 当成目标杀掉(实测 9 次,
|
||||
// 全部落在有人跑 go test 的时段)。更糟的是此时本测试仍会通过:
|
||||
// 它断言的是测试内核存活,而那个内核的插件压根没死——**它在测一件
|
||||
// 没发生的事**,同时还把生产环境打坏了。
|
||||
pid := findPluginPID(t, plgDir, "editdoc")
|
||||
if pid == 0 {
|
||||
t.Skip("未找到插件子进程(进程名匹配失败)")
|
||||
t.Skip("未找到本测试自己拉起的插件子进程")
|
||||
}
|
||||
t.Logf("kill 插件进程 pid=%d", pid)
|
||||
t.Logf("kill 插件进程 pid=%d (exe 在 %s 下)", pid, plgDir)
|
||||
if err := syscall.Kill(pid, syscall.SIGKILL); err != nil {
|
||||
t.Fatalf("kill: %v", err)
|
||||
}
|
||||
|
||||
// 内核必须存活并能继续工作
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
// 先确认目标进程真的死了。
|
||||
//
|
||||
// 这步不能省:旧版直接断言“内核存活”,而内核本来就活着——
|
||||
// 即使 SIGKILL 发错了对象(杀了生产实例的插件)测试也会结束。
|
||||
// 先验“目标真死”再验“内核未被连带”,两步都成立才能证明隔离生效。
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
dead := false
|
||||
for time.Now().Before(deadline) {
|
||||
if syscall.Kill(pid, 0) != nil {
|
||||
dead = true
|
||||
break
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
if !dead {
|
||||
t.Fatalf("pid=%d 在 SIGKILL 后 3s 内未退出,崩溃隔离无从验证", pid)
|
||||
}
|
||||
|
||||
// 内核(本测试进程)必须存活并能继续工作
|
||||
if env.pluginReg.List() == nil {
|
||||
t.Fatal("内核在插件崩溃后不可用")
|
||||
}
|
||||
t.Logf("插件崩溃后内核存活,已加载插件数=%d", len(env.pluginReg.List()))
|
||||
t.Logf("插件进程已确认退出,内核存活,已加载插件数=%d", len(env.pluginReg.List()))
|
||||
}
|
||||
|
||||
// findPluginPID 按二进制路径找插件子进程 pid。
|
||||
func findPluginPID(t *testing.T, name string) int {
|
||||
// findPluginPID 在**指定插件目录下**找插件子进程 pid。
|
||||
//
|
||||
// root 参数是硬约束,不是可选过滤器:本函数的唯一用途是给崩溃隔离
|
||||
// 测试提供一个“可以安全 SIGKILL 的 pid”,而安全的定义就是它必须属于
|
||||
// 本测试自己的临时目录。不带这个约束就会误杀同机生产实例的插件。
|
||||
//
|
||||
// 匹配依据是 /proc/<pid>/exe 的真实路径必须以 root 为前缀。
|
||||
// 用 exe 而不用 cmdline:cmdline 可被进程自行改写,而 exe 符链由内核维护。
|
||||
// root 先过一道 EvalSymlinks:/tmp 在部分发行版上是符链(如 macOS 的
|
||||
// /tmp -> /private/tmp),不归一化会让前缀比较永远不命中,退化成静默 Skip。
|
||||
func findPluginPID(t *testing.T, root, name string) int {
|
||||
t.Helper()
|
||||
if root == "" {
|
||||
t.Fatal("findPluginPID: root 不得为空(防止误杀全系统同名插件)")
|
||||
}
|
||||
realRoot, err := filepath.EvalSymlinks(root)
|
||||
if err != nil {
|
||||
realRoot = root
|
||||
}
|
||||
|
||||
out, err := exec.Command("pgrep", "-f", "plugin.bin").Output()
|
||||
if err != nil {
|
||||
return 0
|
||||
@ -244,15 +285,18 @@ func findPluginPID(t *testing.T, name string) int {
|
||||
if pid == 0 {
|
||||
continue
|
||||
}
|
||||
// 校验 cwd 或 cmdline 含插件名
|
||||
exe, err := os.Readlink(fmt.Sprintf("/proc/%d/exe", pid))
|
||||
if err == nil && strings.Contains(exe, name) {
|
||||
return pid
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
cwd, err := os.Readlink(fmt.Sprintf("/proc/%d/cwd", pid))
|
||||
if err == nil && strings.Contains(cwd, name) {
|
||||
return pid
|
||||
// 两道条件同时成立才算命中:在本测试的目录树内,且是目标插件
|
||||
if !strings.HasPrefix(exe, realRoot+string(os.PathSeparator)) {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(exe, name) {
|
||||
continue
|
||||
}
|
||||
return pid
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user