mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
网络检测 (monitor.go): - TCP 拨测 (8.8.8.8:53 / 1.1.1.1:53 / 208.67.222.222:53) - 延迟阈值检测 (>5s 标记降级) - DNS 多目标检测 (google/baidu/cloudflare) - NetworkCheckResult 新增 TCPReachable + LatencyDegraded Tracker: - 新增 WithKeepChangesets / WithMaxChangesetAge 选项 - Init 时自动清理过期 changeset (默认保留100份/30天) - 先按年龄裁剪,再按数量裁剪
305 lines
6.6 KiB
Go
305 lines
6.6 KiB
Go
package tracker
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"sort"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
type Tracker struct {
|
||
mu sync.Mutex
|
||
dataDir string
|
||
workDir string
|
||
lowerDir string
|
||
upperDir string
|
||
mergeDir string
|
||
mounted bool
|
||
active bool
|
||
before *FSState
|
||
changeSets []*ChangeSet
|
||
keepChangesets int // 保留最近 N 份 changeset,0 = 不限
|
||
maxChangesetAge time.Duration // changeset 最大保留时长,0 = 不限
|
||
}
|
||
|
||
type TrackerOption func(*Tracker)
|
||
|
||
func WithKeepChangesets(n int) TrackerOption {
|
||
return func(t *Tracker) { t.keepChangesets = n }
|
||
}
|
||
|
||
func WithMaxChangesetAge(d time.Duration) TrackerOption {
|
||
return func(t *Tracker) { t.maxChangesetAge = d }
|
||
}
|
||
|
||
func NewTracker(dataDir, workDir string, opts ...TrackerOption) *Tracker {
|
||
t := &Tracker{
|
||
dataDir: dataDir,
|
||
workDir: workDir,
|
||
lowerDir: filepath.Join(workDir, "lower"),
|
||
upperDir: filepath.Join(workDir, "upper"),
|
||
mergeDir: filepath.Join(workDir, "merged"),
|
||
changeSets: make([]*ChangeSet, 0),
|
||
}
|
||
for _, opt := range opts {
|
||
opt(t)
|
||
}
|
||
return t
|
||
}
|
||
|
||
func (t *Tracker) Init() error {
|
||
for _, d := range []string{t.lowerDir, t.upperDir, t.mergeDir} {
|
||
if err := os.MkdirAll(d, 0755); err != nil {
|
||
return fmt.Errorf("create overlay dir %s: %w", d, err)
|
||
}
|
||
}
|
||
t.cleanupChangeSets()
|
||
log.Printf("[tracker] initialized (work=%s)", t.workDir)
|
||
return nil
|
||
}
|
||
|
||
func (t *Tracker) Start() error {
|
||
t.mu.Lock()
|
||
defer t.mu.Unlock()
|
||
|
||
if t.mounted {
|
||
return nil
|
||
}
|
||
|
||
if err := t.mountOverlay(); err != nil {
|
||
return fmt.Errorf("mount overlay: %w", err)
|
||
}
|
||
t.mounted = true
|
||
t.active = true
|
||
|
||
t.before = t.capture()
|
||
|
||
log.Printf("[tracker] overlay mounted at %s", t.mergeDir)
|
||
return nil
|
||
}
|
||
|
||
func (t *Tracker) Stop() error {
|
||
t.mu.Lock()
|
||
defer t.mu.Unlock()
|
||
|
||
if !t.mounted {
|
||
return nil
|
||
}
|
||
|
||
if err := t.umountOverlay(); err != nil {
|
||
return fmt.Errorf("umount overlay: %w", err)
|
||
}
|
||
t.mounted = false
|
||
t.active = false
|
||
return nil
|
||
}
|
||
|
||
func (t *Tracker) PreAction(action string) *ChangeSet {
|
||
t.mu.Lock()
|
||
defer t.mu.Unlock()
|
||
|
||
cs := NewChangeSet(action)
|
||
t.before = t.capture()
|
||
return cs
|
||
}
|
||
|
||
func (t *Tracker) PostAction(action string) *ChangeSet {
|
||
t.mu.Lock()
|
||
defer t.mu.Unlock()
|
||
|
||
after := t.capture()
|
||
changes := diffStates(t.before, after)
|
||
|
||
cs := NewChangeSet(action)
|
||
cs.Files = changes
|
||
if len(changes) > 0 {
|
||
t.changeSets = append(t.changeSets, cs)
|
||
t.saveChangeSet(cs)
|
||
log.Printf("[tracker] action=%s changed=%d files", action, len(changes))
|
||
for _, f := range changes {
|
||
log.Printf(" %s: %s", f.Type, f.Path)
|
||
}
|
||
}
|
||
|
||
t.before = t.capture()
|
||
return cs
|
||
}
|
||
|
||
func (t *Tracker) HasChanges() bool {
|
||
return len(t.changeSets) > 0
|
||
}
|
||
|
||
func (t *Tracker) ChangeSets() []*ChangeSet {
|
||
t.mu.Lock()
|
||
defer t.mu.Unlock()
|
||
result := make([]*ChangeSet, len(t.changeSets))
|
||
copy(result, t.changeSets)
|
||
return result
|
||
}
|
||
|
||
func (t *Tracker) capture() *FSState {
|
||
state, err := captureFSState(t.upperDir)
|
||
if err != nil {
|
||
return &FSState{Files: make(map[string]FileChange), Root: t.upperDir}
|
||
}
|
||
return state
|
||
}
|
||
|
||
func (t *Tracker) mountOverlay() error {
|
||
workDir := filepath.Join(t.workDir, "work")
|
||
os.MkdirAll(workDir, 0755)
|
||
|
||
args := []string{
|
||
"-t", "overlay",
|
||
"overlay",
|
||
"-o", fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", t.lowerDir, t.upperDir, workDir),
|
||
t.mergeDir,
|
||
}
|
||
|
||
cmd := exec.Command("mount", args...)
|
||
if output, err := cmd.CombinedOutput(); err != nil {
|
||
return fmt.Errorf("mount overlayfs failed: %s: %w", string(output), err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (t *Tracker) umountOverlay() error {
|
||
cmd := exec.Command("umount", t.mergeDir)
|
||
if output, err := cmd.CombinedOutput(); err != nil {
|
||
return fmt.Errorf("umount overlayfs failed: %s: %w", string(output), err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (t *Tracker) saveChangeSet(cs *ChangeSet) {
|
||
dir := filepath.Join(t.dataDir, "changesets")
|
||
os.MkdirAll(dir, 0755)
|
||
|
||
path := filepath.Join(dir, cs.ID+".json")
|
||
data, err := json.MarshalIndent(cs, "", " ")
|
||
if err != nil {
|
||
log.Printf("[tracker] save changeset %s: %v", cs.ID, err)
|
||
return
|
||
}
|
||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||
log.Printf("[tracker] write changeset %s: %v", cs.ID, err)
|
||
}
|
||
}
|
||
|
||
func (t *Tracker) cleanupChangeSets() {
|
||
dir := filepath.Join(t.dataDir, "changesets")
|
||
entries, err := os.ReadDir(dir)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
type csFile struct {
|
||
name string
|
||
info os.FileInfo
|
||
}
|
||
var files []csFile
|
||
for _, e := range entries {
|
||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") {
|
||
continue
|
||
}
|
||
info, err := e.Info()
|
||
if err != nil {
|
||
continue
|
||
}
|
||
files = append(files, csFile{name: e.Name(), info: info})
|
||
}
|
||
|
||
// 按修改时间排序
|
||
sort.Slice(files, func(i, j int) bool {
|
||
return files[i].info.ModTime().Before(files[j].info.ModTime())
|
||
})
|
||
|
||
now := time.Now()
|
||
remaining := make([]csFile, 0, len(files))
|
||
|
||
for _, f := range files {
|
||
keep := true
|
||
|
||
if t.maxChangesetAge > 0 && now.Sub(f.info.ModTime()) > t.maxChangesetAge {
|
||
keep = false
|
||
}
|
||
|
||
if keep {
|
||
remaining = append(remaining, f)
|
||
}
|
||
}
|
||
|
||
// 再按数量裁剪
|
||
if t.keepChangesets > 0 && len(remaining) > t.keepChangesets {
|
||
excess := len(remaining) - t.keepChangesets
|
||
for i := 0; i < excess; i++ {
|
||
path := filepath.Join(dir, remaining[i].name)
|
||
os.Remove(path)
|
||
}
|
||
remaining = remaining[excess:]
|
||
}
|
||
|
||
if len(files) != len(remaining) {
|
||
log.Printf("[tracker] cleanup: removed %d changesets, kept %d",
|
||
len(files)-len(remaining), len(remaining))
|
||
}
|
||
}
|
||
|
||
func (t *Tracker) Rollback() error {
|
||
t.mu.Lock()
|
||
defer t.mu.Unlock()
|
||
|
||
if t.mounted {
|
||
if err := t.umountOverlay(); err != nil {
|
||
return fmt.Errorf("umount for rollback: %w", err)
|
||
}
|
||
}
|
||
|
||
if err := os.RemoveAll(t.upperDir); err != nil {
|
||
return fmt.Errorf("remove upper: %w", err)
|
||
}
|
||
if err := os.RemoveAll(filepath.Join(t.workDir, "work")); err != nil {
|
||
return fmt.Errorf("remove work: %w", err)
|
||
}
|
||
|
||
if err := os.MkdirAll(t.upperDir, 0755); err != nil {
|
||
return fmt.Errorf("recreate upper: %w", err)
|
||
}
|
||
|
||
t.changeSets = nil
|
||
t.before = nil
|
||
t.mounted = false
|
||
|
||
log.Printf("[tracker] rollback complete")
|
||
return nil
|
||
}
|
||
|
||
func (t *Tracker) MergeDir() string {
|
||
return t.mergeDir
|
||
}
|
||
|
||
func (t *Tracker) Stats() map[string]interface{} {
|
||
t.mu.Lock()
|
||
defer t.mu.Unlock()
|
||
totalChanges := 0
|
||
for _, cs := range t.changeSets {
|
||
totalChanges += len(cs.Files)
|
||
}
|
||
return map[string]interface{}{
|
||
"mounted": t.mounted,
|
||
"active": t.active,
|
||
"change_sets": len(t.changeSets),
|
||
"total_changes": totalChanges,
|
||
"merge_dir": t.mergeDir,
|
||
"upper_dir": t.upperDir,
|
||
"keep_changesets": t.keepChangesets,
|
||
"max_changeset_age": t.maxChangesetAge.String(),
|
||
}
|
||
}
|