package lua import ( "os/exec" "testing" "time" ) // TestOnUnloadReapsPluginProcesses 验收通用卸载钩子。 // // 内核职责是"调度插件"——卸载是调度的一部分,而插件可能持有外部资源(进程、 // 文件句柄、远端会话)。内核不知道也不需要知道这些资源是什么;它只要在关闭 // 状态前给插件一次自清理的机会(on_unload),资源归属就始终在插件侧。 // 本测试用"插件起一个进程、卸载时杀掉"来证明这条链路真的闭合。 func TestOnUnloadReapsPluginProcesses(t *testing.T) { _, ps, _ := runtimePlugins(t) code := ` local plugin = { name = "reaper", version = "1.0" } plugin.exports = { "touch" } local pid = nil function plugin.touch() local h = io.popen("sh -c 'sleep 300 & echo $!'") pid = h:read("*l"); h:close() return { status = 200, json = { pid = pid } } end function plugin.on_unload() if pid then os.execute("kill -9 " .. pid .. " 2>/dev/null") end end return plugin` if err := ps.LoadSource("reaper", code); err != nil { t.Fatal(err) } out, err := ps.Call("reaper", "touch", map[string]interface{}{}) if err != nil { t.Fatal(err) } m := out.(map[string]interface{}) js := m["json"].(map[string]interface{}) pid := js["pid"].(string) time.Sleep(300 * time.Millisecond) if err := exec.Command("kill", "-0", pid).Run(); err != nil { t.Fatalf("进程应存活: %v", err) } if err := ps.Unload("reaper"); err != nil { t.Fatalf("unload: %v", err) } time.Sleep(500 * time.Millisecond) if err := exec.Command("kill", "-0", pid).Run(); err == nil { exec.Command("kill", "-9", pid).Run() t.Fatal("★ unload 后进程仍存活 —— on_unload 未被调用") } }