mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
fix(plugins): 修三处端口/监听缺陷 + 让测试用临时端口(消除既有 flaky)
排查内核 SIGSEGV 时用 A/B 对照(我的树 20 轮 vs 干净树 20 轮)确认了
两条**既有** flaky,与 C 化改动无关。本提交把它们修掉。
## 缺陷 ①(真 bug,不只是测试卫生):pluginmgr 监听地址是包级可变全局
`var HTTPAddr = "127.0.0.1:9876"` 是包级可变全局,`Start()` 还把 settings 读到的值
**反写**回它,`startHTTPServer` 再读它。后果:
- 多实例互相污染:后启动的实例把地址写进全局,先启动那个读到的是**别人的**地址
(实测与生产 homed 抢 9876)
- 全局读写无同步,属数据竞态
修法:改为实例字段 `p.httpAddr`(默认走 `const defaultHTTPAddr`),
不再有可被任意代码改写的包级状态;并新增 `HTTPURL()` 访问器。
## 缺陷 ②:remotedevice 用 ListenAndServe,监听失败静默且 :0 无法回报端口
`p.server = &http.Server{Addr: p.addr}` + `ListenAndServe()` 在后台 goroutine 里报错,
端口被占时只打一行日志、`Start()` 仍返回 nil —— 插件表面「已加载」而网关根本没跑。
且 `:0` 下拿不到真实端口。
修法:改为 `net.Listen` + `Serve`(与 webui/pluginmgr 同形):
- 监听失败**同步**返回,交给加载器
- 用**实际绑定**地址回写 p.addr,日志与诊断面显示真实端口
## 测试侧:全部改用 :0,不再抢固定端口
新增 `ConfigRegistry.SetPluginConfig(name, key, value)`:插件表原本只在
`RegisterDef`(插件 Start 时)创建,导致「想在插件加载前预置配置」无从下手
(直接 Set 会因表不存在而失败,错误常被忽略)。新方法先建表再写,填补该时序缺口。
`setupIntegration` 在 `Load()` 前预置:
- pluginmgr.http_addr / remotedevice.listen_addr → `127.0.0.1:0`
- webui 走已有的 `SetListenOverride("127.0.0.1:0")`(它有独立旁路)
实测三个插件现在各自绑到 OS 分配的空闲端口(41895 / 35855 / 34021)。
## 缺陷 ③:deepsearch 测试把「上游限流」当成功能回归
`TestRealPlugin_DeepSearchInvoke` 的断言会在上游限流时失败,但插件此时返回的是
**正常结果**(err==nil,content 含 "未返回结果" 与无响应引擎列表)——那是外部条件。
实测失败信息:`brave(Suspended: too many requests), duckduckgo(CAPTCHA), google cse(...)`。
更糟的是它**不可控地随机红**:干净树连跑 20 轮复现 2 次,与代码改动无关。
这种判据会让真正的回归淹没在噪声里。
修法:区分「上游不可用(限流/CAPTCHA)⇒ t.Skip 并说明理由」与
「其他异常 ⇒ fail」。不用静默 return,避免环境退化时判据无声失效。
## 由此发现并修掉的真缺陷:监听地址被硬编码在三处
`127.0.0.1:9876` 曾硬编码在 pluginmgr / cli / webui 各一份。cli 与 webui 后来改为
运行时读 `pluginmgr.http_addr` 设置(本次核实),pluginmgr 自己却仍是全局 —— 三处
口径现在统一为「读设置 + 实例字段」。
## 验证
- 新增 `TestTwoInstances_ListenIndependently`(pluginmgr):两个实例同时监听、
各自 HTTPURL 指向自己端口、两个地址都真的可连。
★ 经**忠实变异**验证有牙:复原「包级全局 + Start 反写 + 读全局」后该测试判红
(我第一版测试只断言字段不共享,变异证明它没牙,已重写为端到端判据)。
- `internal/plugins` 连跑 **30 轮:30/30 全过**(修复前干净树 18/20)。
- 全量连跑 3 轮:38 ok / 0 FAIL / 0 bind 冲突。
- go build ./... / go vet ./... 干净。
This commit is contained in:
@ -7,6 +7,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@ -75,7 +76,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
p.sdk = s
|
||||
|
||||
// ---- 设置 ----------------
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "listen_addr", Default: defaultAddr, Type: "string", DisplayName: "监听地址", Description: "设备网关 HTTP/WS 监听地址(默认 127.0.0.1:9890,仅本机)", Category: "remotedevice"})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "listen_addr", Default: defaultAddr, Type: "string", DisplayName: "监听地址", Description: "设备网关 HTTP/WS 监听地址(默认 127.0.0.1:9890,仅本机);填 127.0.0.1:0 让系统分配空闲端口", Category: "remotedevice"})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "ws_token", Default: "", Type: "password", DisplayName: "接入 Token", Description: "设备绑定/接入时使用的令牌;留空启动时自动生成", Category: "remotedevice"})
|
||||
// 注意:不注册 authorized_devices 设置项 —— 鉴权在设备端执行(客户端存储),
|
||||
// 服务端不保存授权状态,避免 agent 经 config_set 工具自行授权。
|
||||
@ -176,10 +177,25 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
|
||||
// ---- REST 管理面 + WS 设备通道 ----------------
|
||||
p.registerRoutes()
|
||||
p.server = &http.Server{Addr: p.addr, Handler: p.mux}
|
||||
|
||||
// 显式 net.Listen + Serve,而非 ListenAndServe:
|
||||
//
|
||||
// 1. 配 `127.0.0.1:0` 时只有 net.Listener 知道真实端口,ListenAndServe 拿不到。
|
||||
// 这不只是测试便利——它是 `:0` 语义能工作的前提(多实例/沙箱需要)。
|
||||
// 2. 监听失败必须**可见**:此前 ListenAndServe 在后台 goroutine 里报错,
|
||||
// 端口被占时只打一行日志、Start 仍返回 nil(插件表面「已加载」而网关根本没跑)。
|
||||
// 现在在 Start 里同步 Listen,把错误交给调用方。
|
||||
ln, err := net.Listen("tcp", p.addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("remotedevice: 监听 %s 失败: %w", p.addr, err)
|
||||
}
|
||||
// 用**实际绑定**地址回写,使日志与诊断面显示真实端口(配 :0 时尤其重要)。
|
||||
p.addr = ln.Addr().String()
|
||||
|
||||
p.server = &http.Server{Handler: p.mux}
|
||||
go func() {
|
||||
log.Printf("[remotedevice] device gateway listening on %s", p.addr)
|
||||
if err := p.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
if err := p.server.Serve(ln); err != nil && err != http.ErrServerClosed {
|
||||
log.Printf("[remotedevice] server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
Reference in New Issue
Block a user