package oncalendarfile import ( "context" "os" "sync" "time" "github.com/OliveTin/OliveTin/internal/auth" "github.com/OliveTin/OliveTin/internal/config" "github.com/OliveTin/OliveTin/internal/executor" "github.com/OliveTin/OliveTin/internal/filehelper" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" ) type timerEntry struct { timer *time.Timer cancel context.CancelFunc } type existingTimers struct { timers map[time.Time]timerEntry } var ( scheduleMap = make(map[string]existingTimers) scheduleMapMutex sync.RWMutex ) func Schedule(cfg *config.Config, ex *executor.Executor) { for _, action := range cfg.Actions { captured := action if action.ExecOnCalendarFile != "" { x := func(filename string) { parseCalendarFile(captured, cfg, ex, filename) } go filehelper.WatchFileWrite(action.ExecOnCalendarFile, x, filehelper.WatchMeta{ ActionID: action.ID, ActionTitle: action.Title, ConfigFile: action.SourceFile, }) x(action.ExecOnCalendarFile) } } } func clearExistingTimers(action *config.Action) { scheduleMapMutex.Lock() defer scheduleMapMutex.Unlock() if _, exists := scheduleMap[action.ID]; exists { for instant, entry := range scheduleMap[action.ID].timers { log.WithFields(log.Fields{ "instant": instant, "actionTitle": action.Title, }).Infof("Clearing existing scheduled action from calendar") entry.cancel() entry.timer.Stop() } } scheduleMap[action.ID] = existingTimers{ timers: make(map[time.Time]timerEntry), } } func parseCalendarFile(action *config.Action, cfg *config.Config, ex *executor.Executor, filename string) { clearExistingTimers(action) filehelper.Touch(action.ExecOnCalendarFile, "calendar file") log.WithFields(log.Fields{ "actionTitle": action.Title, "filename": filename, }).Infof("Parsing calendar file") yfile, err := os.ReadFile(action.ExecOnCalendarFile) if err != nil { log.Errorf("ReadIn: %v", err) return } data := make([]string, 1) err = yaml.Unmarshal(yfile, &data) if err != nil { log.Errorf("Unmarshal: %v", err) } scheduleCalendarActions(data, action, cfg, ex) } func scheduleCalendarActions(entries []string, action *config.Action, cfg *config.Config, ex *executor.Executor) { ctx := context.Background() for _, instant := range entries { if instant == "" { continue } until, err := time.Parse(time.RFC3339, instant) if err != nil { log.WithFields(log.Fields{ "instant": instant, "actionTitle": action.Title, }).Warnf("Invalid calendar entry, skipping: %v", err) continue } go sleepUntil(ctx, until, action, cfg, ex) } } func registerTimer(action *config.Action, instant time.Time, timer *time.Timer, cancel context.CancelFunc) { scheduleMapMutex.Lock() defer scheduleMapMutex.Unlock() if _, exists := scheduleMap[action.ID]; !exists { scheduleMap[action.ID] = existingTimers{ timers: make(map[time.Time]timerEntry), } } scheduleMap[action.ID].timers[instant] = timerEntry{ timer: timer, cancel: cancel, } } func unregisterTimer(action *config.Action, instant time.Time) { scheduleMapMutex.Lock() v := scheduleMap[action.ID] if v.timers != nil { delete(v.timers, instant) } scheduleMapMutex.Unlock() } func sleepUntil(ctx context.Context, instant time.Time, action *config.Action, cfg *config.Config, ex *executor.Executor) { if time.Now().After(instant) { log.WithFields(log.Fields{ "instant": instant, "actionTitle": action.Title, }).Warnf("Not scheduling stale calendar action") return } log.WithFields(log.Fields{ "instant": instant, "actionTitle": action.Title, }).Infof("Scheduling action on calendar") childCtx, cancel := context.WithCancel(ctx) timer := time.NewTimer(time.Until(instant)) registerTimer(action, instant, timer, cancel) defer timer.Stop() defer cancel() select { case <-timer.C: unregisterTimer(action, instant) exec(instant, action, cfg, ex) return case <-childCtx.Done(): unregisterTimer(action, instant) log.Infof("Cancelled scheduled action") return } } func exec(instant time.Time, action *config.Action, cfg *config.Config, ex *executor.Executor) { log.WithFields(log.Fields{ "instant": instant, "actionTitle": action.Title, }).Infof("Executing action from calendar") req := &executor.ExecutionRequest{ Binding: ex.FindBindingWithNoEntity(action), Cfg: cfg, Tags: []string{}, AuthenticatedUser: auth.UserFromSystem(cfg, "calendar"), } ex.ExecRequest(req) }