package core import ( "fmt" "sync" "time" "github.com/evil7/hostsync/utils" "github.com/robfig/cron/v3" ) // 全局文件保存互斥锁,防止多个定时任务同时保存文件 var hostsSaveMutex sync.Mutex // CronManager 定时任务管理器 type CronManager struct { cron *cron.Cron jobs map[string]cron.EntryID mutex sync.RWMutex } // NewCronManager 创建定时任务管理器 func NewCronManager() *CronManager { // 创建支持秒级的cron实例,支持6位表达式 c := cron.New(cron.WithSeconds(), cron.WithLocation(time.Local)) return &CronManager{ cron: c, jobs: make(map[string]cron.EntryID), mutex: sync.RWMutex{}, } } // Start 启动调度器 func (cm *CronManager) Start() { cm.cron.Start() } // Stop 停止调度器 func (cm *CronManager) Stop() { cm.cron.Stop() } // AddJob 添加定时任务 func (cm *CronManager) AddJob(blockName, cronExpr string, task func()) error { cm.mutex.Lock() defer cm.mutex.Unlock() // 如果已存在,先删除 if existingID, exists := cm.jobs[blockName]; exists { cm.cron.Remove(existingID) delete(cm.jobs, blockName) } // 创建新任务 entryID, err := cm.cron.AddFunc(cronExpr, func() { utils.LogDebug("执行定时任务: %s", blockName) task() }) if err != nil { return fmt.Errorf("添加定时任务失败: %v", err) } cm.jobs[blockName] = entryID utils.LogDebug("已添加定时任务: %s (%s)", blockName, cronExpr) return nil } // RemoveJob 删除定时任务 func (cm *CronManager) RemoveJob(blockName string) error { cm.mutex.Lock() defer cm.mutex.Unlock() entryID, exists := cm.jobs[blockName] if !exists { return fmt.Errorf("定时任务不存在: %s", blockName) } cm.cron.Remove(entryID) delete(cm.jobs, blockName) utils.LogDebug("已删除定时任务: %s", blockName) return nil } // ListJobs 列出所有定时任务 func (cm *CronManager) ListJobs() map[string]cron.EntryID { cm.mutex.RLock() defer cm.mutex.RUnlock() result := make(map[string]cron.EntryID) for name, entryID := range cm.jobs { result[name] = entryID } return result } // GetJob 获取指定的定时任务 func (cm *CronManager) GetJob(blockName string) (cron.EntryID, bool) { cm.mutex.RLock() defer cm.mutex.RUnlock() entryID, exists := cm.jobs[blockName] return entryID, exists } // LoadFromHosts 从hosts文件加载定时任务 func (cm *CronManager) LoadFromHosts(hm *HostsManager) error { resolver := NewDNSResolver() for blockName, block := range hm.Blocks { if block.CronJob != "" { task := func(bn string) func() { return func() { defer func() { if r := recover(); r != nil { utils.LogError("定时任务 %s 发生错误: %v", bn, r) } }() utils.LogInfo("开始执行定时更新: %s", bn) // 使用全局锁确保hosts文件保存的原子性 hostsSaveMutex.Lock() defer hostsSaveMutex.Unlock() // 重新加载hosts文件以获取最新配置 freshHM := NewHostsManager() if err := freshHM.Load(); err != nil { utils.LogError("定时任务 %s 重新加载hosts文件失败: %v", bn, err) return } // 检查块是否仍然存在 freshBlock := freshHM.GetBlock(bn) if freshBlock == nil { utils.LogError("定时任务 %s 对应的块已不存在,跳过执行", bn) return } // 检查定时任务是否仍然启用 if freshBlock.CronJob == "" { utils.LogInfo("定时任务 %s 已被禁用,跳过执行", bn) return } // 使用最新的hosts数据执行更新 utils.LogInfo("开始DNS解析更新: %s", bn) err := resolver.UpdateBlock(freshHM, bn, "", "", "", false) if err != nil { utils.LogError("定时更新失败 %s: %v", bn, err) } else { // 获取更新后的块信息,记录更新时间 updatedBlock := freshHM.GetBlock(bn) if updatedBlock != nil && !updatedBlock.UpdateAt.IsZero() { utils.LogInfo("定时更新完成: %s,更新时间: %s", bn, updatedBlock.UpdateAt.Format("2006-01-02 15:04:05")) } else { utils.LogInfo("定时更新完成: %s", bn) } } } }(blockName) if err := cm.AddJob(blockName, block.CronJob, task); err != nil { utils.LogError("加载定时任务失败 %s: %v", blockName, err) } } } return nil } // ValidateCronExpression 验证cron表达式 func (cm *CronManager) ValidateCronExpression(expr string) error { // 使用robfig/cron/v3的解析器验证表达式 parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) _, err := parser.Parse(expr) return err } // ReloadFromHosts 重新从hosts文件同步定时任务配置 func (cm *CronManager) ReloadFromHosts() error { utils.LogDebug("正在重新同步定时任务配置...") // 加载最新的hosts文件 hm := NewHostsManager() if err := hm.Load(); err != nil { return fmt.Errorf("重新加载hosts文件失败: %v", err) } cm.mutex.Lock() defer cm.mutex.Unlock() // 收集当前活跃的任务 currentJobs := make(map[string]string) // blockName -> cronExpr for blockName := range cm.jobs { currentJobs[blockName] = "" } // 收集hosts文件中的任务配置 hostsJobs := make(map[string]string) for blockName, block := range hm.Blocks { if block.CronJob != "" { hostsJobs[blockName] = block.CronJob } } // 删除不再存在的任务 for blockName := range currentJobs { if _, exists := hostsJobs[blockName]; !exists { if entryID, exists := cm.jobs[blockName]; exists { cm.cron.Remove(entryID) delete(cm.jobs, blockName) utils.LogInfo("已删除不存在的定时任务: %s", blockName) } } } // 添加或更新任务 resolver := NewDNSResolver() for blockName, cronExpr := range hostsJobs { // 检查是否需要更新任务 needsUpdate := false if _, exists := cm.jobs[blockName]; !exists { needsUpdate = true } else { // 这里可以进一步检查cron表达式是否有变化 // 目前简化处理,每次都重新创建以确保同步 if existingEntryID, exists := cm.jobs[blockName]; exists { cm.cron.Remove(existingEntryID) delete(cm.jobs, blockName) } needsUpdate = true } if needsUpdate { task := func(bn string) func() { return func() { defer func() { if r := recover(); r != nil { utils.LogError("定时任务 %s 发生错误: %v", bn, r) } }() utils.LogInfo("开始执行定时更新: %s", bn) // 使用全局锁确保hosts文件保存的原子性 hostsSaveMutex.Lock() defer hostsSaveMutex.Unlock() // 重新加载hosts文件以获取最新配置 freshHM := NewHostsManager() if err := freshHM.Load(); err != nil { utils.LogError("定时任务 %s 重新加载hosts文件失败: %v", bn, err) return } // 检查块是否仍然存在 freshBlock := freshHM.GetBlock(bn) if freshBlock == nil { utils.LogError("定时任务 %s 对应的块已不存在,跳过执行", bn) return } // 检查定时任务是否仍然启用 if freshBlock.CronJob == "" { utils.LogInfo("定时任务 %s 已被禁用,跳过执行", bn) return } // 使用最新的hosts数据执行更新 utils.LogInfo("开始DNS解析更新: %s", bn) err := resolver.UpdateBlock(freshHM, bn, "", "", "", false) if err != nil { utils.LogError("定时更新失败 %s: %v", bn, err) } else { // 获取更新后的块信息,记录更新时间 updatedBlock := freshHM.GetBlock(bn) if updatedBlock != nil && !updatedBlock.UpdateAt.IsZero() { utils.LogInfo("定时更新完成: %s,更新时间: %s", bn, updatedBlock.UpdateAt.Format("2006-01-02 15:04:05")) } else { utils.LogInfo("定时更新完成: %s", bn) } } } }(blockName) entryID, err := cm.cron.AddFunc(cronExpr, func() { utils.LogDebug("执行定时任务: %s", blockName) task() }) if err != nil { utils.LogError("重新加载定时任务失败 %s: %v", blockName, err) } else { cm.jobs[blockName] = entryID utils.LogDebug("已重新加载定时任务: %s (%s)", blockName, cronExpr) } } } utils.LogDebug("定时任务配置重新同步完成,当前活跃任务: %d 个", len(cm.jobs)) return nil }