mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 01:48:11 +00:00
三层回退恢复机制(L0写前留档/L1恢复梯子/L2离线回滚)+ guard 父守护
- L0: files 插件写受保护系统路径(/etc 等)前自动留档,AbstractBeforeWrite 到 data/file_baseline - L1: failback 受限 worker 执行恢复梯子 probe→还原DNS/proxy→还原LLM配置+ReloadFromConfig→probe,N轮有界 - L2: tracker changeset 持久化原文 blob,guard 离线 RollbackFromDisk 回滚 agentfs;SystemSnapshot 支撑 - guard 父守护: 心跳 IPC(PING/ACK unix socket, 文件心跳回退)、失败计数、退出码协议(42/43/44)、最后手段 - 发行版路径适配: system.protected_paths/network_paths 可注入,默认面向主流 Linux - Windows 兼容: guard.go/failback.go 加 //go:build linux, guard_windows.go 提供 no-op 桩 - 修复: guard.yaml last_resort 键冲突、changeset Content 不落盘导致离线回滚丢原文 Build 全绿, vet 干净, system/recovery/ipc/tracker 单元测试全过
This commit is contained in:
138
cmd/homed/failback.go
Normal file
138
cmd/homed/failback.go
Normal file
@ -0,0 +1,138 @@
|
||||
//go:build linux
|
||||
|
||||
// L1 failback 恢复 worker(Linux 专属):在受限 boot 下执行恢复梯子,
|
||||
// 依赖 DNS/代理网络基线、LLM 配置快照与 reload 等 Linux 语义。
|
||||
// 非 Linux 平台不编译,由 guard_windows.go 提供桩。
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||||
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
|
||||
luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/recovery"
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/system"
|
||||
)
|
||||
|
||||
// runFailbackRecovery 在 failback worker 内执行 L1 恢复梯子:
|
||||
// probe(rescue 源) → 还原 DNS/proxy → probe → 还原 LLM 配置快照+ReloadFromConfig → probe
|
||||
// 成功后写 recovery_result.json 并以 exitRecovered 退出;失败以 exitRecoveryFailed 退出。
|
||||
// 全程确定性、无 token 消耗(除 rescue 源 QuickChat 复检外)。
|
||||
func runFailbackRecovery(dataDir string, cfgReg *internalConfig.ConfigRegistry, lua *luaVM.VM, providerMgr *agentAPI.ProviderManager, baseAPIKey string) {
|
||||
applySystemConfig(loadGuardConfig(dataDir)) // 发行版/部署路径适配:guard.yaml system.*
|
||||
task, err := recovery.LoadTask(recovery.TaskPath(dataDir))
|
||||
if err != nil {
|
||||
log.Printf("[failback] no recovery task, skipping: %v", err)
|
||||
os.Exit(exitRecoveryFailed)
|
||||
}
|
||||
if task.RescueSource.Name == "" || task.RescueSource.BaseURL == "" {
|
||||
log.Printf("[failback] rescue source missing in task, cannot recover")
|
||||
os.Exit(exitRecoveryFailed)
|
||||
}
|
||||
log.Printf("[failback] recovery task: attempt %d/%d rescue=%s", task.Attempt, task.MaxAttempts, task.RescueSource.Name)
|
||||
|
||||
timeout := 120 * time.Second
|
||||
if d, err := time.ParseDuration(task.AttemptTimeout); err == nil && d > 0 {
|
||||
timeout = d
|
||||
}
|
||||
|
||||
// 注册 rescue 源并设为默认(锚定 IP 直连,绕开被破坏的 DNS/代理)
|
||||
key := task.RescueSource.APIKey
|
||||
if key == "" {
|
||||
key = baseAPIKey
|
||||
}
|
||||
rescueProvider := agentAPI.NewLuaAdaptedProvider(agentAPI.BaseConfig{
|
||||
Model: task.RescueSource.Model,
|
||||
BaseURL: task.RescueSource.BaseURL,
|
||||
APIKey: key,
|
||||
Temperature: 0.7,
|
||||
MaxTokens: 512,
|
||||
ContextWindow: 8192,
|
||||
}, lua, task.RescueSource.Name, task.RescueSource.Adapter)
|
||||
providerMgr.Register("rescue", rescueProvider)
|
||||
if err := providerMgr.SetDefault("rescue"); err != nil {
|
||||
log.Printf("[failback] set rescue default: %v", err)
|
||||
}
|
||||
|
||||
probe := func(ctx context.Context, label string) (bool, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
resp, err := providerMgr.QuickChat(ctx, "回复 OK")
|
||||
if err != nil {
|
||||
log.Printf("[failback] probe(%s): %v", label, err)
|
||||
return false, nil
|
||||
}
|
||||
log.Printf("[failback] probe(%s): reachable, reply=%q", label, truncateStr(resp.Content, 60))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
restoreNet := func() ([]string, error) {
|
||||
base, err := system.LoadNetworkBaseline(dataDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load network baseline: %w", err)
|
||||
}
|
||||
changed, err := base.RestoreFiles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(changed) > 0 {
|
||||
log.Printf("[failback] restored network files: %v", changed)
|
||||
}
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
restoreCfg := func() (int, error) {
|
||||
snapPath := filepath.Join(dataDir, "llm_snapshot.json")
|
||||
snap, err := internalConfig.LoadLLMSnapshot(snapPath)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("load llm snapshot: %w", err)
|
||||
}
|
||||
if err := cfgReg.RestoreCoreLLM(snap); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := sdk.NewLLM(providerMgr, cfgReg, lua, baseAPIKey).ReloadFromConfig(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
log.Printf("[failback] restored %d llm keys and reloaded from config", len(snap))
|
||||
return len(snap), nil
|
||||
}
|
||||
|
||||
ladder := &recovery.Ladder{
|
||||
ResultFile: recovery.ResultPath(dataDir),
|
||||
Attempt: task.Attempt,
|
||||
Probe: probe,
|
||||
RestoreNetwork: restoreNet,
|
||||
RestoreConfig: restoreCfg,
|
||||
Log: log.Printf,
|
||||
}
|
||||
res := ladder.Run(context.Background())
|
||||
log.Printf("[failback] recovery round result: %s", res.Quote())
|
||||
|
||||
if res.Success {
|
||||
os.Exit(exitRecovered)
|
||||
}
|
||||
os.Exit(exitRecoveryFailed)
|
||||
}
|
||||
|
||||
func truncateStr(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n] + "..."
|
||||
}
|
||||
|
||||
// lastDiagSummary 读取最近一次 recovery_result 生成一行自诊断摘要(worker IPC ACK 上报)。
|
||||
func lastDiagSummary(dataDir string) string {
|
||||
res, err := recovery.LoadResult(recovery.ResultPath(dataDir))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return res.Quote()
|
||||
}
|
||||
456
cmd/homed/guard.go
Normal file
456
cmd/homed/guard.go
Normal file
@ -0,0 +1,456 @@
|
||||
//go:build linux
|
||||
|
||||
// L0/L1/L2 三层回退恢复机制的核心:父守护 guard(Linux 专属)。
|
||||
//
|
||||
// 依赖 Linux 设施:syscall.Reboot / /etc 网络基线 / overlayfs 离线回滚 /
|
||||
// unix socket IPC / systemctl 重启。Windows 等平台不编译本文件,
|
||||
// 由 guard_windows.go 提供 no-op 桩,保证 cmd/homed 跨平台可构建。
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/ipc"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/recovery"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/system"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// guardConfig 父守护配置:独立于 config.db,避开被改坏的配置数据库。
|
||||
type guardConfig struct {
|
||||
MaxRestarts int `yaml:"max_restarts"` // 连续 normal 崩溃重试上限
|
||||
HeartbeatTimeout string `yaml:"heartbeat_timeout"` // 心跳多久未更新判定卡死(如 30s)
|
||||
HeartbeatInterval string `yaml:"heartbeat_interval"` // 期待 worker 心跳频率(仅日志)
|
||||
LLMSnapshot string `yaml:"llm_snapshot"` // LLM 配置基线文件,为空用 <data>/llm_snapshot.json
|
||||
FailbackEnabled bool `yaml:"failback_enabled"` // 崩溃超限后是否进入受限 failback 启动
|
||||
LastResort string `yaml:"last_resort"` // restart_app | reboot
|
||||
RestartCommand []string `yaml:"restart_command"` // last_resort=restart_app 时的命令(如 systemctl restart homeagent)
|
||||
RebootGrace string `yaml:"reboot_grace"` // last_resort=reboot 前的缓冲
|
||||
LLM llmConfig `yaml:"llm"` // rescue 源(锚定 IP,绕开被破坏的 DNS/代理)
|
||||
Recovery recoveryConfig `yaml:"recovery"` // failback 恢复参数(N 轮有界)
|
||||
LastResortCfg lastResortConfig `yaml:"last_resort_cfg"` // 最后手段细节(snapshot_before 等)
|
||||
System systemConfig `yaml:"system"` // 发行版/部署路径适配
|
||||
|
||||
heartbeatTimeout time.Duration
|
||||
heartbeatInterval time.Duration
|
||||
rebootGrace time.Duration
|
||||
}
|
||||
|
||||
type llmConfig struct {
|
||||
Sources []recovery.RescueSource `yaml:"sources"`
|
||||
}
|
||||
|
||||
type recoveryConfig struct {
|
||||
MaxAttempts int `yaml:"max_attempts"` // failback 恢复尝试轮数上限
|
||||
AttemptTimeout string `yaml:"attempt_timeout"` // 单轮恢复超时
|
||||
KnowledgeBase string `yaml:"knowledge_base"` // 恢复知识库目录
|
||||
TriggerPrompt string `yaml:"trigger_prompt"` // 恢复 agent 的系统提示词(未知/混合分支)
|
||||
Plugins []string `yaml:"plugins"` // failback 插件集(缺省用 core.agent.failback_plugins)
|
||||
attemptTimeout time.Duration
|
||||
}
|
||||
|
||||
type lastResortConfig struct {
|
||||
SnapshotBefore bool `yaml:"snapshot_before"` // 最后手段前是否回滚 agentfs 到最近快照
|
||||
}
|
||||
|
||||
// systemConfig 发行版/部署路径适配:把 L0 写前留档保护范围与网络基线文件列表
|
||||
// 从默认的 Linux 路径换成当前部署实际路径,避免硬编码失效。
|
||||
type systemConfig struct {
|
||||
ProtectedPaths []string `yaml:"protected_paths"` // L0 写前留档保护前缀(默认 /etc/)
|
||||
NetworkPaths []string `yaml:"network_paths"` // 网络基线文件(默认 resolv.conf/hosts/environment)
|
||||
}
|
||||
|
||||
// applySystemConfig 应用发行版路径适配,供 guard 与 failback worker 共用。
|
||||
func applySystemConfig(cfg *guardConfig) {
|
||||
if len(cfg.System.ProtectedPaths) > 0 {
|
||||
system.SetProtectedPaths(cfg.System.ProtectedPaths)
|
||||
}
|
||||
if len(cfg.System.NetworkPaths) > 0 {
|
||||
system.SetNetworkPaths(cfg.System.NetworkPaths)
|
||||
}
|
||||
}
|
||||
|
||||
func defaultGuardConfig(dataDir string) *guardConfig {
|
||||
return &guardConfig{
|
||||
MaxRestarts: 3,
|
||||
HeartbeatTimeout: "30s",
|
||||
HeartbeatInterval: "5s",
|
||||
LLMSnapshot: filepath.Join(dataDir, "llm_snapshot.json"),
|
||||
FailbackEnabled: true,
|
||||
LastResort: "restart_app",
|
||||
RestartCommand: []string{"systemctl", "restart", "homeagent"},
|
||||
RebootGrace: "10s",
|
||||
Recovery: recoveryConfig{
|
||||
MaxAttempts: 3,
|
||||
AttemptTimeout: "120s",
|
||||
},
|
||||
LastResortCfg: lastResortConfig{SnapshotBefore: true},
|
||||
}
|
||||
}
|
||||
|
||||
func loadGuardConfig(dataDir string) *guardConfig {
|
||||
cfg := defaultGuardConfig(dataDir)
|
||||
path := filepath.Join(dataDir, "guard.yaml")
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return cfg
|
||||
}
|
||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||
log.Printf("[guard] parse %s: %v, using defaults", path, err)
|
||||
return cfg
|
||||
}
|
||||
if cfg.LLMSnapshot == "" {
|
||||
cfg.LLMSnapshot = filepath.Join(dataDir, "llm_snapshot.json")
|
||||
}
|
||||
parseDur := func(s string, def time.Duration) time.Duration {
|
||||
if s == "" {
|
||||
return def
|
||||
}
|
||||
d, err := time.ParseDuration(s)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return d
|
||||
}
|
||||
cfg.heartbeatTimeout = parseDur(cfg.HeartbeatTimeout, 30*time.Second)
|
||||
cfg.heartbeatInterval = parseDur(cfg.HeartbeatInterval, 5*time.Second)
|
||||
cfg.rebootGrace = parseDur(cfg.RebootGrace, 10*time.Second)
|
||||
cfg.Recovery.attemptTimeout = parseDur(cfg.Recovery.AttemptTimeout, 120*time.Second)
|
||||
if cfg.MaxRestarts < 1 {
|
||||
cfg.MaxRestarts = 1
|
||||
}
|
||||
if cfg.LastResort != "reboot" && cfg.LastResort != "restart_app" {
|
||||
cfg.LastResort = "restart_app"
|
||||
}
|
||||
if len(cfg.RestartCommand) == 0 {
|
||||
cfg.RestartCommand = []string{"systemctl", "restart", "homeagent"}
|
||||
}
|
||||
if cfg.Recovery.MaxAttempts < 1 {
|
||||
cfg.Recovery.MaxAttempts = 1
|
||||
}
|
||||
// 缺省 failback 插件集
|
||||
if len(cfg.Recovery.Plugins) == 0 {
|
||||
cfg.Recovery.Plugins = []string{"webui", "pluginmgr", "recoverydiag"}
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// runGuard 父守护主循环:拉起 worker(--role=agent),心跳探活 + waitpid,
|
||||
// 崩溃后按序 重试→LLM 配置基线恢复→failback 受限启动(N 轮有界,每轮复检)→L2 最后手段。
|
||||
func runGuard(dataDir string) {
|
||||
cfg := loadGuardConfig(dataDir)
|
||||
applySystemConfig(cfg)
|
||||
hbPath := filepath.Join(dataDir, "heartbeat")
|
||||
os.MkdirAll(filepath.Join(dataDir, "log"), 0755)
|
||||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
log.Fatalf("[guard] resolve executable: %v", err)
|
||||
}
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
log.Printf("[guard] starting, data=%s max_restarts=%d hb_timeout=%v last_resort=%s failback_rounds=%d",
|
||||
dataDir, cfg.MaxRestarts, cfg.heartbeatTimeout, cfg.LastResort, cfg.Recovery.MaxAttempts)
|
||||
|
||||
// 启动即捕获网络基线(L0:写前留档 + L1 还原依据),并给当前 worker 恢复代理环境
|
||||
captureNetworkBaseline(dataDir)
|
||||
|
||||
failures := 0 // 连续 normal 崩溃次数
|
||||
failbackRound := 0 // 已进行的 failback 轮次
|
||||
failbackDone := false // 本轮 failback 是否已进入
|
||||
|
||||
for {
|
||||
// 决定本次 boot 模式:normal 崩溃超限 → 进入 failback
|
||||
boot := "normal"
|
||||
if cfg.FailbackEnabled && !failbackDone && failures >= cfg.MaxRestarts {
|
||||
log.Printf("[guard] %d failures >= max %d, entering failback (Safe-Mode)", failures, cfg.MaxRestarts)
|
||||
// L1 前置:恢复 LLM 基线 + 还原 DNS/proxy + 写恢复任务
|
||||
restoreLLMBaseline(dataDir, cfg.LLMSnapshot)
|
||||
restoreNetworkBaseline(dataDir)
|
||||
failbackRound = 1
|
||||
failbackDone = true
|
||||
if err := writeRecoveryTask(dataDir, cfg, failbackRound); err != nil {
|
||||
log.Printf("[guard] write recovery task: %v", err)
|
||||
}
|
||||
boot = "failback"
|
||||
} else if failbackDone {
|
||||
boot = "failback"
|
||||
}
|
||||
|
||||
// 恢复 agent 环境(代理变量)在 worker 拉起前注入
|
||||
if b, err := system.LoadNetworkBaseline(dataDir); err == nil {
|
||||
b.ApplyProxyEnv()
|
||||
}
|
||||
|
||||
outcome, spawnErr := spawnWorkerOnce(exe, dataDir, boot, hbPath, cfg, sigCh)
|
||||
if spawnErr != nil {
|
||||
log.Printf("[guard] spawn worker: %v, last_resort=%s", spawnErr, cfg.LastResort)
|
||||
doLastResort(cfg)
|
||||
failures = 0
|
||||
failbackDone = false
|
||||
failbackRound = 0
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
switch outcome {
|
||||
case workerCleanExit:
|
||||
log.Printf("[guard] worker exited cleanly, guard exiting")
|
||||
return
|
||||
case workerRestartRequested:
|
||||
log.Printf("[guard] worker requested restart, respawning")
|
||||
continue
|
||||
case workerRecovered:
|
||||
// failback 成功:重置失败轮次,交回主 agent(下一轮 normal boot)
|
||||
log.Printf("[guard] failback recovery succeeded, handing back to main agent")
|
||||
failures = 0
|
||||
failbackDone = false
|
||||
failbackRound = 0
|
||||
continue
|
||||
case workerCrash:
|
||||
// fallthrough below
|
||||
}
|
||||
failures++
|
||||
|
||||
if failbackDone {
|
||||
// failback 轮次内崩溃:读结果判定是否成功
|
||||
if res, err := recovery.LoadResult(recovery.ResultPath(dataDir)); err == nil && res.Success {
|
||||
log.Printf("[guard] failback round %d result success: %s", failbackRound, res.Quote())
|
||||
failures = 0
|
||||
failbackDone = false
|
||||
failbackRound = 0
|
||||
continue
|
||||
}
|
||||
if failbackRound >= cfg.Recovery.MaxAttempts {
|
||||
log.Printf("[guard] failback exhausted after %d rounds, L2 last_resort=%s", failbackRound, cfg.LastResort)
|
||||
if cfg.LastResortCfg.SnapshotBefore {
|
||||
rollbackAgentFS(dataDir)
|
||||
}
|
||||
doLastResort(cfg)
|
||||
failures = 0
|
||||
failbackDone = false
|
||||
failbackRound = 0
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
// 进入下一轮 failback
|
||||
failbackRound++
|
||||
log.Printf("[guard] failback round %d failed, next round %d (max %d)", failbackRound-1, failbackRound, cfg.Recovery.MaxAttempts)
|
||||
if err := writeRecoveryTask(dataDir, cfg, failbackRound); err != nil {
|
||||
log.Printf("[guard] write recovery task: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if failures >= cfg.MaxRestarts {
|
||||
log.Printf("[guard] %d normal failures, entering failback next loop", failures)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type workerOutcome int
|
||||
|
||||
const (
|
||||
workerCleanExit workerOutcome = iota
|
||||
workerCrash
|
||||
workerRestartRequested
|
||||
workerRecovered
|
||||
)
|
||||
|
||||
// spawnWorkerOnce 拉起一个 worker 并监控到其退出。
|
||||
// 返回 (outcome workerOutcome, spawnErr error);spawnErr 非 nil 表示未能拉起进程。
|
||||
func spawnWorkerOnce(exe, dataDir, boot, hbPath string, cfg *guardConfig, sigCh chan os.Signal) (workerOutcome, error) {
|
||||
cmd := exec.Command(exe, "--role=agent", "--data="+dataDir, "--boot="+boot)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = nil
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return workerCrash, err
|
||||
}
|
||||
pid := cmd.Process.Pid
|
||||
log.Printf("[guard] worker up pid=%d boot=%s", pid, boot)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- cmd.Wait() }()
|
||||
|
||||
hbTicker := time.NewTicker(cfg.heartbeatInterval)
|
||||
defer hbTicker.Stop()
|
||||
|
||||
ipcClient := ipc.NewClient(dataDir)
|
||||
|
||||
for {
|
||||
select {
|
||||
case sig := <-sigCh:
|
||||
log.Printf("[guard] signal %v, stopping worker", sig)
|
||||
cmd.Process.Signal(syscall.SIGTERM)
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(10 * time.Second):
|
||||
cmd.Process.Kill()
|
||||
<-done
|
||||
}
|
||||
return workerCleanExit, nil
|
||||
case err := <-done:
|
||||
if err == nil {
|
||||
log.Printf("[guard] worker pid=%d exited cleanly", pid)
|
||||
return workerCleanExit, nil
|
||||
}
|
||||
var ee *exec.ExitError
|
||||
if errors.As(err, &ee) {
|
||||
switch ee.ExitCode() {
|
||||
case exitRestartRequested:
|
||||
log.Printf("[guard] worker pid=%d requested restart (exit %d), respawning without counting failure", pid, exitRestartRequested)
|
||||
return workerRestartRequested, nil
|
||||
case exitRecovered:
|
||||
log.Printf("[guard] worker pid=%d recovered main agent (exit %d)", pid, exitRecovered)
|
||||
return workerRecovered, nil
|
||||
case exitRecoveryFailed:
|
||||
log.Printf("[guard] worker pid=%d failback attempt failed (exit %d)", pid, exitRecoveryFailed)
|
||||
return workerCrash, nil
|
||||
}
|
||||
}
|
||||
log.Printf("[guard] worker pid=%d crashed: %v", pid, err)
|
||||
return workerCrash, nil
|
||||
case <-hbTicker.C:
|
||||
// IPC PING/ACK 存活判定(带自诊断上报),失败回退文件心跳 mtime
|
||||
if st, perr := ipcClient.Ping(cfg.heartbeatTimeout); perr != nil {
|
||||
if heartbeatStale(hbPath, cfg.heartbeatTimeout) {
|
||||
log.Printf("[guard] heartbeat stale for pid=%d (ipc: %v), treating as hang, SIGKILL", pid, perr)
|
||||
cmd.Process.Kill()
|
||||
<-done
|
||||
return workerCrash, nil
|
||||
}
|
||||
} else if st != nil && st.LastDiag != "" {
|
||||
log.Printf("[guard] worker pid=%d self-diagnosis: %s", pid, st.LastDiag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func heartbeatStale(path string, timeout time.Duration) bool {
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
// 无心跳文件:worker 刚启动或异常;宽限处理
|
||||
return false
|
||||
}
|
||||
return time.Since(fi.ModTime()) > timeout
|
||||
}
|
||||
|
||||
// restoreLLMBaseline 从文件基线恢复 core.llm.* 配置(存在才恢复)。
|
||||
func restoreLLMBaseline(dataDir, snapPath string) {
|
||||
if _, err := os.Stat(snapPath); err != nil {
|
||||
log.Printf("[guard] no llm snapshot baseline at %s, skip restore", snapPath)
|
||||
return
|
||||
}
|
||||
snap, err := internalConfig.LoadLLMSnapshot(snapPath)
|
||||
if err != nil {
|
||||
log.Printf("[guard] load llm snapshot %s: %v", snapPath, err)
|
||||
return
|
||||
}
|
||||
cfgReg := internalConfig.NewConfigRegistry(filepath.Join(dataDir, "config.db"))
|
||||
defer cfgReg.Close()
|
||||
if err := cfgReg.RestoreCoreLLM(snap); err != nil {
|
||||
log.Printf("[guard] restore llm baseline: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("[guard] restored %d llm keys from %s", len(snap), snapPath)
|
||||
}
|
||||
|
||||
// captureNetworkBaseline L0:启动即捕获网络基线(DNS/hosts/代理),供 failback 还原。
|
||||
func captureNetworkBaseline(dataDir string) {
|
||||
base, err := system.CaptureNetwork()
|
||||
if err != nil {
|
||||
log.Printf("[guard] capture network baseline: %v", err)
|
||||
return
|
||||
}
|
||||
if err := system.SaveNetworkBaseline(dataDir, base); err != nil {
|
||||
log.Printf("[guard] save network baseline: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("[guard] network baseline captured: %s", base.Summary())
|
||||
}
|
||||
|
||||
// restoreNetworkBaseline L1:把 resolv.conf/hosts 还原到基线(DNS/proxy 小修命中即停)。
|
||||
func restoreNetworkBaseline(dataDir string) {
|
||||
base, err := system.LoadNetworkBaseline(dataDir)
|
||||
if err != nil {
|
||||
log.Printf("[guard] no network baseline, skip DNS/proxy restore: %v", err)
|
||||
return
|
||||
}
|
||||
changed, err := base.RestoreFiles()
|
||||
if err != nil {
|
||||
log.Printf("[guard] restore network baseline: %v", err)
|
||||
return
|
||||
}
|
||||
if len(changed) > 0 {
|
||||
log.Printf("[guard] restored network files: %v", changed)
|
||||
} else {
|
||||
log.Printf("[guard] network baseline already consistent")
|
||||
}
|
||||
}
|
||||
|
||||
// writeRecoveryTask 写本轮 failback 恢复任务文件。
|
||||
func writeRecoveryTask(dataDir string, cfg *guardConfig, attempt int) error {
|
||||
task := &recovery.Task{
|
||||
Attempt: attempt,
|
||||
MaxAttempts: cfg.Recovery.MaxAttempts,
|
||||
AttemptTimeout: cfg.Recovery.AttemptTimeout,
|
||||
TriggerPrompt: cfg.Recovery.TriggerPrompt,
|
||||
KnowledgeBase: cfg.Recovery.KnowledgeBase,
|
||||
PluginList: cfg.Recovery.Plugins,
|
||||
}
|
||||
for _, s := range cfg.LLM.Sources {
|
||||
if s.Name == "rescue" || s.Name != "" {
|
||||
task.RescueSource = s
|
||||
break
|
||||
}
|
||||
}
|
||||
return recovery.SaveTask(recovery.TaskPath(dataDir), task)
|
||||
}
|
||||
|
||||
// rollbackAgentFS L2:guard 在 worker 离线时回滚 agentfs 最近快照(read changesets 逆应用)。
|
||||
func rollbackAgentFS(dataDir string) {
|
||||
workDir := filepath.Join(dataDir, "agentfs")
|
||||
trk := tracker.NewOfflineTracker(dataDir, workDir)
|
||||
n, err := trk.RollbackFromDisk()
|
||||
if err != nil {
|
||||
log.Printf("[guard] agentfs rollback: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("[guard] agentfs rolled back %d change sets", n)
|
||||
}
|
||||
|
||||
func doLastResort(cfg *guardConfig) {
|
||||
switch cfg.LastResort {
|
||||
case "reboot":
|
||||
log.Printf("[guard] last_resort=reboot, sync + reboot in %v", cfg.rebootGrace)
|
||||
time.Sleep(cfg.rebootGrace)
|
||||
syscall.Sync()
|
||||
if err := syscall.Reboot(syscall.LINUX_REBOOT_CMD_RESTART); err != nil {
|
||||
log.Printf("[guard] reboot failed (likely no privilege): %v", err)
|
||||
}
|
||||
case "restart_app":
|
||||
cmd := exec.Command(cfg.RestartCommand[0], cfg.RestartCommand[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Printf("[guard] restart_app command %s: %v", strings.Join(cfg.RestartCommand, " "), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var _ = fmt.Sprintf
|
||||
32
cmd/homed/guard_windows.go
Normal file
32
cmd/homed/guard_windows.go
Normal file
@ -0,0 +1,32 @@
|
||||
//go:build !linux
|
||||
|
||||
// L0/L1/L2 三层回退恢复机制为 Linux 专属(依赖 /etc 网络基线、overlayfs、
|
||||
// syscall.Reboot、unix socket IPC、systemctl)。在 Windows 等非 Linux 平台
|
||||
// 本文件提供 no-op 桩:guard/failback 角色不执行恢复,其余主 agent 功能照常。
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||||
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
|
||||
luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
|
||||
)
|
||||
|
||||
// runGuard 桩:guard 父守护模式仅支持 Linux,其余平台打印提示并退出。
|
||||
func runGuard(dataDir string) {
|
||||
log.Printf("[guard] guard mode is Linux-only (data=%s), exiting", dataDir)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// runFailbackRecovery 桩:failback 恢复为 Linux 专属,其余平台直接退出。
|
||||
func runFailbackRecovery(dataDir string, cfgReg *internalConfig.ConfigRegistry, lua *luaVM.VM, providerMgr *agentAPI.ProviderManager, baseAPIKey string) {
|
||||
log.Printf("[failback] recovery is Linux-only (data=%s), exiting", dataDir)
|
||||
os.Exit(exitRecoveryFailed)
|
||||
}
|
||||
|
||||
// lastDiagSummary 桩:无 recovery_result 时返回空自诊断。
|
||||
func lastDiagSummary(dataDir string) string {
|
||||
return ""
|
||||
}
|
||||
@ -7,58 +7,61 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
agentPkg "gitcode.com/JianFeeeee/HomeAgent/internal/agent"
|
||||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||||
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
|
||||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||||
agentPkg "gitcode.com/JianFeeeee/HomeAgent/internal/agent"
|
||||
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
|
||||
logpkg "gitcode.com/JianFeeeee/HomeAgent/internal/log"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/ipc"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/meta"
|
||||
logpkg "gitcode.com/JianFeeeee/HomeAgent/internal/log"
|
||||
luapkg "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/pipeline"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/social"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/text"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/meta"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/nlp"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||
cli "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/cli"
|
||||
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins"
|
||||
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/clawhubadapter"
|
||||
cli "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/cli"
|
||||
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/healthcheck"
|
||||
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/pluginmgr"
|
||||
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/webui"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/recovery"
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/supervisor"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
|
||||
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
|
||||
)
|
||||
|
||||
func main() {
|
||||
dataDir := flag.String("data", "", "data directory (default: auto-detect next to binary)")
|
||||
httpAddr := flag.String("webui", "", "webui listen address (default: webui.listen_addr from config)")
|
||||
cliSocket := flag.String("socket", "", "cli unix socket path (default: <data>/cli.sock)")
|
||||
role := flag.String("role", "agent", "process role: guard (父守护) | agent (工作进程)")
|
||||
boot := flag.String("boot", "normal", "agent boot mode: normal | failback (受限启动,仅 failback 插件集)")
|
||||
flag.Parse()
|
||||
|
||||
// 父守护模式:只负责拉起/守护 worker,不初始化 agent 内核
|
||||
if *role == "guard" {
|
||||
runGuard(resolveDataDir(*dataDir))
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[homed] role=agent boot=%s", *boot)
|
||||
|
||||
if *dataDir == "" {
|
||||
exe, err := os.Executable()
|
||||
if err == nil {
|
||||
*dataDir = filepath.Join(filepath.Dir(exe), "data")
|
||||
} else {
|
||||
if exe, err := exec.LookPath(os.Args[0]); err == nil {
|
||||
*dataDir = filepath.Join(filepath.Dir(exe), "data")
|
||||
} else {
|
||||
*dataDir = "./data"
|
||||
}
|
||||
}
|
||||
*dataDir = resolveDataDir(*dataDir)
|
||||
}
|
||||
|
||||
if *cliSocket == "" {
|
||||
@ -137,6 +140,8 @@ func main() {
|
||||
cfgReg := internalConfig.NewConfigRegistry(filepath.Join(*dataDir, "config.db"))
|
||||
defer cfgReg.Close()
|
||||
cfgReg.SeedDefaults(*dataDir)
|
||||
// LLM 配置写前留档(config_set 写 core.llm.* 前自动快照),guard 恢复用基线
|
||||
cfgReg.SetLLMSnapshotFile(filepath.Join(*dataDir, "llm_snapshot.json"))
|
||||
cfg := cfgReg.ToConfig()
|
||||
|
||||
// 共享词嵌入:蒸馏提取(Phase 3 TransE 验证)与 Agent 上下文复用同一实例,
|
||||
@ -235,24 +240,24 @@ func main() {
|
||||
if err := textMem.Append(te); err != nil {
|
||||
log.Printf("[homed] text memory append: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if input != "" && memDB != nil {
|
||||
distiller.Append("agent", "user", input)
|
||||
}
|
||||
if response != "" && memDB != nil {
|
||||
distiller.Append("agent", "assistant", response)
|
||||
}
|
||||
if input != "" && memDB != nil {
|
||||
distiller.Append("agent", "user", input)
|
||||
}
|
||||
if response != "" && memDB != nil {
|
||||
distiller.Append("agent", "assistant", response)
|
||||
}
|
||||
|
||||
// 工具输出接入蒸馏管线
|
||||
for _, tr := range toolResults {
|
||||
if trMap, ok := tr.(map[string]interface{}); ok {
|
||||
if text, ok := trMap["output"].(string); ok && text != "" && memDB != nil {
|
||||
distiller.Append("agent", "tool", text)
|
||||
// 工具输出接入蒸馏管线
|
||||
for _, tr := range toolResults {
|
||||
if trMap, ok := tr.(map[string]interface{}); ok {
|
||||
if text, ok := trMap["output"].(string); ok && text != "" && memDB != nil {
|
||||
distiller.Append("agent", "tool", text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
@ -291,6 +296,12 @@ func main() {
|
||||
}
|
||||
provider := providerMgr.Default()
|
||||
|
||||
// L1 failback:受限 worker 启动即跑恢复梯子(probe→还原DNS/proxy→还原config+ReloadFromConfig→probe),
|
||||
// 结果以退出码 exitRecovered=43 / exitRecoveryFailed=44 交回 guard,不进入主 agent 循环。
|
||||
if *boot == "failback" {
|
||||
runFailbackRecovery(*dataDir, cfgReg, luaVM, providerMgr, baseAPIKey)
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 文档记忆 + 知识库
|
||||
// ========================================================================
|
||||
@ -383,32 +394,32 @@ func main() {
|
||||
}
|
||||
|
||||
agent := agentCore.New(agentCore.AgentConfig{
|
||||
ID: "main",
|
||||
SystemPrompt: sysPrompt,
|
||||
Provider: provider,
|
||||
ProviderManager: providerMgr,
|
||||
IO: iom,
|
||||
Memory: memDB,
|
||||
Indexer: memIdx,
|
||||
Tracker: trk,
|
||||
DocStore: docStore,
|
||||
Knowledge: ks,
|
||||
SocialStore: socialStore,
|
||||
TextMemory: textMem,
|
||||
Personality: personality,
|
||||
PluginReg: pluginReg,
|
||||
PluginDir: cfg.Plugin.Dir,
|
||||
DistillInterval: cfgReg.GetDuration("core.agent.distill_interval", 30*time.Minute),
|
||||
ArchiveInterval: cfgReg.GetDuration("core.agent.archive_interval", 60*time.Minute),
|
||||
ReviewInterval: cfgReg.GetDuration("core.agent.review_interval", 120*time.Minute),
|
||||
MergeInterval: cfgReg.GetDuration("core.agent.merge_interval", 120*time.Minute),
|
||||
ID: "main",
|
||||
SystemPrompt: sysPrompt,
|
||||
Provider: provider,
|
||||
ProviderManager: providerMgr,
|
||||
IO: iom,
|
||||
Memory: memDB,
|
||||
Indexer: memIdx,
|
||||
Tracker: trk,
|
||||
DocStore: docStore,
|
||||
Knowledge: ks,
|
||||
SocialStore: socialStore,
|
||||
TextMemory: textMem,
|
||||
Personality: personality,
|
||||
PluginReg: pluginReg,
|
||||
PluginDir: cfg.Plugin.Dir,
|
||||
DistillInterval: cfgReg.GetDuration("core.agent.distill_interval", 30*time.Minute),
|
||||
ArchiveInterval: cfgReg.GetDuration("core.agent.archive_interval", 60*time.Minute),
|
||||
ReviewInterval: cfgReg.GetDuration("core.agent.review_interval", 120*time.Minute),
|
||||
MergeInterval: cfgReg.GetDuration("core.agent.merge_interval", 120*time.Minute),
|
||||
ContextSavePath: filepath.Join(cfg.Daemon.DataDir, "memory", "context.json"),
|
||||
EmbeddingModelPath: cfgReg.GetString("core.agent.embedding_model_path", ""),
|
||||
Embedder: embedder,
|
||||
Embedder: embedder,
|
||||
StageHost: stageHost,
|
||||
EventBus: evBus,
|
||||
ThinkingEnabled: cfg.LLM.ThinkingEnabled,
|
||||
InputProcessing: cfg.InputProcessing,
|
||||
EventBus: evBus,
|
||||
ThinkingEnabled: cfg.LLM.ThinkingEnabled,
|
||||
InputProcessing: cfg.InputProcessing,
|
||||
})
|
||||
|
||||
// 通过 Registry 将内核依赖注入每个插件的 PluginSDK(阶段6 将替换遗留的 util.Configure)
|
||||
@ -454,6 +465,24 @@ func main() {
|
||||
// Auto-create plugins directory (without hardcoding plugin names)
|
||||
os.MkdirAll(cfg.Plugin.Dir, 0755)
|
||||
|
||||
// failback 受限启动:仅装载 failback 插件集(webfetch/files/cmd 为内核内置,
|
||||
// 此处仅控制外部插件,默认含 recoverydiag 以便直接在受限态产出恢复结论)
|
||||
if *boot == "failback" {
|
||||
list := cfgReg.GetString("core.agent.failback_plugins", "webui,pluginmgr,recoverydiag")
|
||||
// 优先使用 guard.yaml 经过 recovery 任务下发的插件集(guard 是 failback 权威)
|
||||
if task, terr := recovery.LoadTask(recovery.TaskPath(*dataDir)); terr == nil && len(task.Plugins()) > 0 {
|
||||
list = strings.Join(task.Plugins(), ",")
|
||||
}
|
||||
var names []string
|
||||
for _, s := range strings.Split(list, ",") {
|
||||
if s = strings.TrimSpace(s); s != "" {
|
||||
names = append(names, s)
|
||||
}
|
||||
}
|
||||
pluginReg.SetLoadAllowlist(names)
|
||||
log.Printf("[homed] failback boot: plugin allowlist = %v", names)
|
||||
}
|
||||
|
||||
// Load all plugins — each scans its own dir and is loaded via factory or .so
|
||||
if err := pluginReg.Load(cfg.Plugin.Dir); err != nil {
|
||||
log.Printf("[homed] warning: load plugins: %v", err)
|
||||
@ -468,9 +497,60 @@ func main() {
|
||||
agent.Start()
|
||||
defer agent.Stop()
|
||||
|
||||
// PING/ACK 心跳服务:worker 监听 unix socket,guard 发 PING、worker 回 ACK
|
||||
// (含自诊断 kernel 状态快照),替换纯文件心跳。文件心跳保留作回退。
|
||||
ipcServer := ipc.NewServer(*dataDir, func() *ipc.Status {
|
||||
st := agent.GetKernelStatus()
|
||||
llmOK := st != nil && st.LLM.Available
|
||||
tools := 0
|
||||
if st != nil {
|
||||
tools = len(st.Tools)
|
||||
}
|
||||
uptime := int64(0)
|
||||
if st != nil {
|
||||
if d, err := time.ParseDuration(st.Uptime); err == nil {
|
||||
uptime = int64(d.Seconds())
|
||||
}
|
||||
}
|
||||
return &ipc.Status{
|
||||
PID: os.Getpid(),
|
||||
Boot: *boot,
|
||||
UptimeSec: uptime,
|
||||
LLMOK: &llmOK,
|
||||
Tools: tools,
|
||||
LastDiag: lastDiagSummary(*dataDir),
|
||||
}
|
||||
})
|
||||
if err := ipcServer.Start(); err != nil {
|
||||
log.Printf("[homed] warning: ipc heartbeat server: %v", err)
|
||||
} else {
|
||||
defer ipcServer.Stop()
|
||||
}
|
||||
|
||||
sup.SetTracker(trk)
|
||||
sup.RegisterAgent("main")
|
||||
|
||||
// 真实存活源 + 重启通道:daemon 心跳语义由此修正(lastHB 只在确认存活时更新),
|
||||
// 重启动作不再空转——清理后以特殊退出码交给 guard/systemd 重建。
|
||||
restartCh := make(chan struct{}, 1)
|
||||
sup.SetHeartbeatSource(func(id types.AgentID) (time.Time, types.HealthStatus, error) {
|
||||
st := agent.GetKernelStatus()
|
||||
if st == nil {
|
||||
return time.Time{}, types.HealthDown, fmt.Errorf("no kernel status")
|
||||
}
|
||||
h := types.HealthHealthy
|
||||
if !st.LLM.Available {
|
||||
h = types.HealthDegraded
|
||||
}
|
||||
return time.Now(), h, nil
|
||||
})
|
||||
sup.SetRestartHandler(func(id types.AgentID) {
|
||||
select {
|
||||
case restartCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
})
|
||||
|
||||
log.Printf("[homed] main agent started, model=%s base=%s sources=%d adapters=%d",
|
||||
cfg.LLM.Model, cfg.LLM.BaseURL, len(cfg.LLM.Sources), len(luaVM.ListAdapters()))
|
||||
log.Printf("[homed] kernel ready, waiting for plugin IO...")
|
||||
@ -481,9 +561,42 @@ func main() {
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigCh
|
||||
|
||||
log.Printf("[homed] shutting down...")
|
||||
// 心跳:每 5s 触碰 <data>/heartbeat,guard 据此判定工作进程是否存活/卡死
|
||||
hbPath := filepath.Join(*dataDir, "heartbeat")
|
||||
hbStop := make(chan struct{})
|
||||
go func() {
|
||||
t := time.NewTicker(5 * time.Second)
|
||||
defer t.Stop()
|
||||
writeHB := func() {
|
||||
if f, err := os.OpenFile(hbPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err == nil {
|
||||
fmt.Fprintf(f, "t=%d\n", time.Now().Unix())
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
writeHB()
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
writeHB()
|
||||
case <-hbStop:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
restartRequested := false
|
||||
select {
|
||||
case <-sigCh:
|
||||
log.Printf("[homed] shutting down...")
|
||||
case <-restartCh:
|
||||
restartRequested = true
|
||||
log.Printf("[homed] restart requested, shutting down cleanly then exiting with code %d", exitRestartRequested)
|
||||
}
|
||||
|
||||
close(hbStop)
|
||||
pluginReg.StopAll()
|
||||
if trk != nil {
|
||||
trk.Stop()
|
||||
@ -493,4 +606,8 @@ func main() {
|
||||
}
|
||||
sup.Shutdown()
|
||||
log.Printf("[homed] stopped")
|
||||
|
||||
if restartRequested {
|
||||
os.Exit(exitRestartRequested)
|
||||
}
|
||||
}
|
||||
|
||||
30
cmd/homed/worker_exit.go
Normal file
30
cmd/homed/worker_exit.go
Normal file
@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// worker 退出码协议:guard 通过 worker 退出码判定下一步动作。
|
||||
// 该协议跨平台一致(Windows 下 guard 为 no-op,退出码仍保留)。
|
||||
const (
|
||||
exitRestartRequested = 42 // worker 清理后请求重建(不计失败轮次)
|
||||
exitRecovered = 43 // failback worker 恢复成功,guard 应重置失败轮次并交回主 agent
|
||||
exitRecoveryFailed = 44 // failback worker 单轮恢复失败,guard 计入失败并进入下一轮/最后手段
|
||||
)
|
||||
|
||||
// resolveDataDir 复用原有的数据目录探测逻辑。
|
||||
func resolveDataDir(dataDir string) string {
|
||||
if dataDir != "" {
|
||||
return dataDir
|
||||
}
|
||||
exe, err := os.Executable()
|
||||
if err == nil {
|
||||
return filepath.Join(filepath.Dir(exe), "data")
|
||||
}
|
||||
if exe, err := exec.LookPath(os.Args[0]); err == nil {
|
||||
return filepath.Join(filepath.Dir(exe), "data")
|
||||
}
|
||||
return "./data"
|
||||
}
|
||||
Reference in New Issue
Block a user