package daemon import ( "fmt" "os" "os/signal" "sync" "syscall" "time" "github.com/muyue/muyue/internal/config" "github.com/muyue/muyue/internal/scanner" "github.com/muyue/muyue/internal/updater" ) type Daemon struct { config *config.MuyueConfig interval time.Duration stopCh chan struct{} mu sync.RWMutex running bool lastCheck time.Time lastStatus []updater.UpdateStatus logs []string onUpdate func([]updater.UpdateStatus) } func NewDaemon(cfg *config.MuyueConfig, interval time.Duration) *Daemon { if interval == 0 { interval = 1 * time.Hour } return &Daemon{ config: cfg, interval: interval, stopCh: make(chan struct{}), logs: []string{}, } } func (d *Daemon) OnUpdate(fn func([]updater.UpdateStatus)) { d.onUpdate = fn } func (d *Daemon) Start() error { d.mu.Lock() if d.running { d.mu.Unlock() return fmt.Errorf("daemon already running") } d.running = true d.mu.Unlock() d.log("daemon started (interval: %s)", d.interval) go d.run() return nil } func (d *Daemon) Stop() { d.mu.Lock() defer d.mu.Unlock() if !d.running { return } d.running = false d.stopCh <- struct{}{} d.log("daemon stopped") } func (d *Daemon) IsRunning() bool { d.mu.RLock() defer d.mu.RUnlock() return d.running } func (d *Daemon) LastCheck() time.Time { d.mu.RLock() defer d.mu.RUnlock() return d.lastCheck } func (d *Daemon) LastStatus() []updater.UpdateStatus { d.mu.RLock() defer d.mu.RUnlock() return d.lastStatus } func (d *Daemon) Logs() []string { d.mu.RLock() defer d.mu.RUnlock() return d.logs } func (d *Daemon) TriggerCheck() []updater.UpdateStatus { return d.checkUpdates() } func (d *Daemon) run() { d.checkUpdates() ticker := time.NewTicker(d.interval) defer ticker.Stop() for { select { case <-ticker.C: d.checkUpdates() case <-d.stopCh: return } } } func (d *Daemon) checkUpdates() []updater.UpdateStatus { d.log("checking for updates...") result := scanner.ScanSystem() statuses := updater.CheckUpdates(result) needsUpdate := false for _, s := range statuses { if s.NeedsUpdate { needsUpdate = true d.log("update available: %s %s -> %s", s.Tool, s.Current, s.Latest) } } if !needsUpdate { d.log("all tools up to date") } d.mu.Lock() d.lastCheck = time.Now() d.lastStatus = statuses d.mu.Unlock() if d.config.Profile.Preferences.AutoUpdate && needsUpdate { d.log("auto-updating...") results := updater.RunAutoUpdate(statuses) for _, r := range results { if r.Message != "" { d.log(" %s: %s", r.Tool, r.Message) } } } if d.onUpdate != nil { d.onUpdate(statuses) } return statuses } func (d *Daemon) log(format string, args ...interface{}) { msg := fmt.Sprintf("[%s] %s", time.Now().Format("15:04:05"), fmt.Sprintf(format, args...)) d.mu.Lock() d.logs = append(d.logs, msg) if len(d.logs) > 500 { d.logs = d.logs[250:] } d.mu.Unlock() } func RunStandalone(cfg *config.MuyueConfig) { d := NewDaemon(cfg, 1*time.Hour) d.Start() sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) <-sigCh d.Stop() }