package schedule import ( "context" "errors" "log" "os" "os/exec" "time" "github.com/robfig/cron/v3" ) type Config struct { CronExpr string ScriptPath string Logger *log.Logger } type Scheduler struct { cron *cron.Cron logger *log.Logger } // Start configures and starts the cron scheduler. It runs the given script at the // specified cron expression (KST). The caller owns the returned scheduler and must // call Stop on shutdown. func Start(cfg Config) (*Scheduler, error) { if cfg.CronExpr == "" { return nil, errors.New("CronExpr is required") } if cfg.ScriptPath == "" { return nil, errors.New("ScriptPath is required") } if cfg.Logger == nil { cfg.Logger = log.Default() } if _, err := os.Stat(cfg.ScriptPath); err != nil { return nil, err } kst, err := time.LoadLocation("Asia/Seoul") if err != nil { kst = time.FixedZone("KST", 9*60*60) } parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) spec, err := parser.Parse(cfg.CronExpr) if err != nil { return nil, err } c := cron.New(cron.WithLocation(kst), cron.WithParser(parser)) c.Schedule(spec, cron.FuncJob(func() { runScript(cfg.Logger, cfg.ScriptPath) })) c.Start() cfg.Logger.Printf("scheduler started with cron=%s script=%s tz=%s", cfg.CronExpr, cfg.ScriptPath, kst) return &Scheduler{ cron: c, logger: cfg.Logger, }, nil } // Stop halts the scheduler. It does not cancel a currently running job. func (s *Scheduler) Stop() context.Context { if s == nil || s.cron == nil { return context.Background() } return s.cron.Stop() } func runScript(logger *log.Logger, script string) { start := time.Now() logger.Printf("scheduler: running %s", script) cmd := exec.Command("/bin/bash", script) cmd.Env = os.Environ() out, err := cmd.CombinedOutput() duration := time.Since(start) if len(out) > 0 { logger.Printf("scheduler: output:\n%s", string(out)) } if err != nil { logger.Printf("scheduler: %s failed after %s: %v", script, duration, err) return } logger.Printf("scheduler: %s completed in %s", script, duration) }