mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
fix(proc): 子进程崩溃自愈 + 集中台账 + 注册面摘除
根因:子进程插件被 kill 后,内核只发了一个无人订阅的事件, 工具/stage handler/IO 通道全留在注册表里指向死进程, 模型继续调用只吃 ErrProcessExited,没有任何路径把插件拉回来。 ## 四层修复 ### 1. 专职 waitLoop(进程收割) - 每个子进程配一根 waitLoop goroutine,是 cmd.Wait() 的唯一调用点 - 不再依赖 stdout EOF 判定死亡(孙子进程继承 stdout 时 EOF 永不到来) - 手工 os.Pipe 替代 cmd.StdinPipe/StdoutPipe,避免 waitLoop 与 os/exec 的内部关闭竞争 - host.go: Host.Supervisor(),Host.Close() 先 StopAll 再拆段 ### 2. 集中台账 Supervisor - proc/supervisor.go: 插件 Spawn 握手成功即 track,进程退出即 untrack - StopAll: 并发发 plugin.stop 走优雅路径,到期仍在的一律 Kill - 关停后才完成握手的进程被立即结束,不会活过内核 - 消除「孤儿进程持共享段映射 → SIGBUS」的隐患 ### 3. 注册面摘除(detachPlugin) - 新增 StageHost.UnregisterPluginStages:摘除指定插件的全部 stage handler - 新增 Registry.pluginChannels 台账:记录每个插件注册的 IO 通道 - 三条路径统一走 detachPlugin:Disable / ReloadOne / RemovePlugin - StopAndUnload 漏了 IO 通道也一并补上 ### 4. 自动重启 - onProcCrash 从「只发事件」改为「摘注册面 → 从注册表移除 → 异步排重启」 - scheduleProcRestart: 窗口 5 分钟内最多 3 次,线性退避 1s/2s/3s - 超限停手留日志;重启前复核是否已被 Disable 或被其他路径加载 - 崩溃计数窗口过期自动归零 ### 5. 主动停止 vs 崩溃的区分 - proc.Plugin 新增 stopping 标志:Stop()/Close() 里 Set(true) - handleExit 读 stopping 标志,主动停止不上报 onCrash - 防止重载/禁用/卸载被误判为崩溃触发多余重启 ### 6. Linux Pdeathsig 兜底 - procattr_linux.go: SysProcAttr.Pdeathsig = SIGKILL - 兜 homed 自身被 SIGKILL/OOM 时子进程变孤儿的场景 - macOS/Windows 无等价物,空实现 ### 7. pluginmgr 升级 - PluginManager 接口新增 PluginRuntime / ListPluginRuntimes - plugin_list 输出运行态:loaded / alive / pid / crash_count / channel - 新增 plugin_status: 全量运行期快照 + dead/unhealthy 汇总 - 新增 plugin_restart: 无条件重启单个插件(plgreload 不动未改二进制的插件) ### 测试 - process_test.go: 3 例(grandchild stdout 感知 / Supervisor track-untrack / StopAll 无孤儿) - crash_recovery_test.go: 8 例(detach 三项齐全 / 通道重注册 / 崩溃不阻塞 / 退避阈值 / 窗口过期 / 关停中跳过 / PluginRuntime 通道识别) - stages_plugin_test.go: 4 例(stage 按插件摘除 / 空 stage 清理 / 空名 no-op / 工具+stage 双摘后可重新注册同名)
This commit is contained in:
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
)
|
||||
@ -43,6 +44,14 @@ type Plugin struct {
|
||||
caps *capabilitySet
|
||||
|
||||
stopOnce sync.Once
|
||||
|
||||
// stopping 标记「本次退出是内核主动发起的」,用于压掉 onCrash。
|
||||
//
|
||||
// 必要性:Stop() 宽限期超时与 Close() 都走 Process.Kill(),
|
||||
// 而 Kill 产生的 `signal: killed` 是非 nil 的 waitErr——若不区分,
|
||||
// 重载/禁用/卸载这些**内核自己发起**的停止会被 handleExit 当成崩溃上报,
|
||||
// 触发一轮多余的自动重启(重载路径下等于把刚装好的插件又推倒一次)。
|
||||
stopping atomic.Bool
|
||||
}
|
||||
|
||||
// New 创建子进程插件(不启动进程)。
|
||||
@ -67,6 +76,31 @@ func New(name, bin, dir string, config map[string]interface{}, host *Host, onCra
|
||||
// Name 实现 sdk.Plugin。
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
// PID 返回子进程号;未启动或已退出返回 0。
|
||||
// 供 pluginmgr 呈现「插件实际在跑哪个进程」。
|
||||
func (p *Plugin) PID() int {
|
||||
if p.proc == nil {
|
||||
return 0
|
||||
}
|
||||
if !p.Alive() {
|
||||
return 0
|
||||
}
|
||||
return p.proc.PID()
|
||||
}
|
||||
|
||||
// Alive 报告子进程是否仍存活。
|
||||
func (p *Plugin) Alive() bool {
|
||||
if p.proc == nil {
|
||||
return false
|
||||
}
|
||||
select {
|
||||
case <-p.proc.Exited():
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动子进程并完成注册。
|
||||
//
|
||||
// core 是内核为该插件构建的能力面(internal/sdk.PluginSDK 天然满足 CoreSDK)。
|
||||
@ -99,6 +133,7 @@ func (p *Plugin) Start(core CoreSDK) error {
|
||||
EvtRingSize: evtTotalSize,
|
||||
Handler: p.handler.Handle,
|
||||
OnExit: p.handleExit,
|
||||
Supervisor: p.host.Supervisor(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@ -124,6 +159,7 @@ func (p *Plugin) Start(core CoreSDK) error {
|
||||
|
||||
// Stop 优雅停止(实现 sdk.Plugin)。
|
||||
func (p *Plugin) Stop() error {
|
||||
p.stopping.Store(true)
|
||||
var err error
|
||||
p.stopOnce.Do(func() {
|
||||
if p.proc != nil {
|
||||
@ -138,6 +174,7 @@ func (p *Plugin) Stop() error {
|
||||
// **这里是真 kill + wait**——对比 cabi 路径的 Close 只做 dlclose,
|
||||
// 而 dlclose 对 Go c-shared 是 no-op(§1.1,热重载静默失效的根因)。
|
||||
func (p *Plugin) Close() error {
|
||||
p.stopping.Store(true)
|
||||
var err error
|
||||
p.stopOnce.Do(func() {
|
||||
if p.proc != nil {
|
||||
@ -156,6 +193,11 @@ func (p *Plugin) handleExit(name string, err error) {
|
||||
if p.host != nil && p.host.ForceReleaseLock(name) {
|
||||
log.Printf("[proc] %s 退出,内核已释放其持有的 stage 锁", name)
|
||||
}
|
||||
// 内核主动停止(Stop/Close,含宽限期超时后的 Kill)不算崩溃:
|
||||
// 否则重载/禁用/卸载都会误触发自动重启。
|
||||
if p.stopping.Load() {
|
||||
return
|
||||
}
|
||||
if err != nil && p.onCrash != nil {
|
||||
p.onCrash(name, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user