node build fixed
This commit is contained in:
515
seanime-2.9.10/internal/plugin/cron.go
Normal file
515
seanime-2.9.10/internal/plugin/cron.go
Normal file
@@ -0,0 +1,515 @@
|
||||
// Package cron implements a crontab-like service to execute and schedule
|
||||
// repeative tasks/jobs.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// c := cron.New()
|
||||
// c.MustAdd("dailyReport", "0 0 * * *", func() { ... })
|
||||
// c.Start()
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"seanime/internal/extension"
|
||||
goja_util "seanime/internal/util/goja"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// Cron is a crontab-like struct for tasks/jobs scheduling.
|
||||
type Cron struct {
|
||||
timezone *time.Location
|
||||
ticker *time.Ticker
|
||||
startTimer *time.Timer
|
||||
tickerDone chan bool
|
||||
jobs []*CronJob
|
||||
interval time.Duration
|
||||
mux sync.RWMutex
|
||||
scheduler *goja_util.Scheduler
|
||||
}
|
||||
|
||||
// New create a new Cron struct with default tick interval of 1 minute
|
||||
// and timezone in UTC.
|
||||
//
|
||||
// You can change the default tick interval with Cron.SetInterval().
|
||||
// You can change the default timezone with Cron.SetTimezone().
|
||||
func New(scheduler *goja_util.Scheduler) *Cron {
|
||||
return &Cron{
|
||||
interval: 1 * time.Minute,
|
||||
timezone: time.UTC,
|
||||
jobs: []*CronJob{},
|
||||
tickerDone: make(chan bool),
|
||||
scheduler: scheduler,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AppContextImpl) BindCronToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) *Cron {
|
||||
cron := New(scheduler)
|
||||
cronObj := vm.NewObject()
|
||||
_ = cronObj.Set("add", cron.Add)
|
||||
_ = cronObj.Set("remove", cron.Remove)
|
||||
_ = cronObj.Set("removeAll", cron.RemoveAll)
|
||||
_ = cronObj.Set("total", cron.Total)
|
||||
_ = cronObj.Set("stop", cron.Stop)
|
||||
_ = cronObj.Set("start", cron.Start)
|
||||
_ = cronObj.Set("hasStarted", cron.HasStarted)
|
||||
_ = obj.Set("cron", cronObj)
|
||||
|
||||
return cron
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// SetInterval changes the current cron tick interval
|
||||
// (it usually should be >= 1 minute).
|
||||
func (c *Cron) SetInterval(d time.Duration) {
|
||||
// update interval
|
||||
c.mux.Lock()
|
||||
wasStarted := c.ticker != nil
|
||||
c.interval = d
|
||||
c.mux.Unlock()
|
||||
|
||||
// restart the ticker
|
||||
if wasStarted {
|
||||
c.Start()
|
||||
}
|
||||
}
|
||||
|
||||
// SetTimezone changes the current cron tick timezone.
|
||||
func (c *Cron) SetTimezone(l *time.Location) {
|
||||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
c.timezone = l
|
||||
}
|
||||
|
||||
// MustAdd is similar to Add() but panic on failure.
|
||||
func (c *Cron) MustAdd(jobId string, cronExpr string, run func()) {
|
||||
if err := c.Add(jobId, cronExpr, run); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add registers a single cron job.
|
||||
//
|
||||
// If there is already a job with the provided id, then the old job
|
||||
// will be replaced with the new one.
|
||||
//
|
||||
// cronExpr is a regular cron expression, eg. "0 */3 * * *" (aka. at minute 0 past every 3rd hour).
|
||||
// Check cron.NewSchedule() for the supported tokens.
|
||||
func (c *Cron) Add(jobId string, cronExpr string, fn func()) error {
|
||||
if fn == nil {
|
||||
return errors.New("failed to add new cron job: fn must be non-nil function")
|
||||
}
|
||||
|
||||
schedule, err := NewSchedule(cronExpr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add new cron job: %w", err)
|
||||
}
|
||||
|
||||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
// remove previous (if any)
|
||||
c.jobs = slices.DeleteFunc(c.jobs, func(j *CronJob) bool {
|
||||
return j.Id() == jobId
|
||||
})
|
||||
|
||||
// add new
|
||||
c.jobs = append(c.jobs, &CronJob{
|
||||
id: jobId,
|
||||
fn: fn,
|
||||
schedule: schedule,
|
||||
scheduler: c.scheduler,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes a single cron job by its id.
|
||||
func (c *Cron) Remove(jobId string) {
|
||||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
if c.jobs == nil {
|
||||
return // nothing to remove
|
||||
}
|
||||
|
||||
c.jobs = slices.DeleteFunc(c.jobs, func(j *CronJob) bool {
|
||||
return j.Id() == jobId
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveAll removes all registered cron jobs.
|
||||
func (c *Cron) RemoveAll() {
|
||||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
c.jobs = []*CronJob{}
|
||||
}
|
||||
|
||||
// Total returns the current total number of registered cron jobs.
|
||||
func (c *Cron) Total() int {
|
||||
c.mux.RLock()
|
||||
defer c.mux.RUnlock()
|
||||
|
||||
return len(c.jobs)
|
||||
}
|
||||
|
||||
// Jobs returns a shallow copy of the currently registered cron jobs.
|
||||
func (c *Cron) Jobs() []*CronJob {
|
||||
c.mux.RLock()
|
||||
defer c.mux.RUnlock()
|
||||
|
||||
copy := make([]*CronJob, len(c.jobs))
|
||||
for i, j := range c.jobs {
|
||||
copy[i] = j
|
||||
}
|
||||
|
||||
return copy
|
||||
}
|
||||
|
||||
// Stop stops the current cron ticker (if not already).
|
||||
//
|
||||
// You can resume the ticker by calling Start().
|
||||
func (c *Cron) Stop() {
|
||||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
if c.startTimer != nil {
|
||||
c.startTimer.Stop()
|
||||
c.startTimer = nil
|
||||
}
|
||||
|
||||
if c.ticker == nil {
|
||||
return // already stopped
|
||||
}
|
||||
|
||||
c.tickerDone <- true
|
||||
c.ticker.Stop()
|
||||
c.ticker = nil
|
||||
}
|
||||
|
||||
// Start starts the cron ticker.
|
||||
//
|
||||
// Calling Start() on already started cron will restart the ticker.
|
||||
func (c *Cron) Start() {
|
||||
c.Stop()
|
||||
|
||||
// delay the ticker to start at 00 of 1 c.interval duration
|
||||
now := time.Now()
|
||||
next := now.Add(c.interval).Truncate(c.interval)
|
||||
delay := next.Sub(now)
|
||||
|
||||
c.mux.Lock()
|
||||
c.startTimer = time.AfterFunc(delay, func() {
|
||||
c.mux.Lock()
|
||||
c.ticker = time.NewTicker(c.interval)
|
||||
c.mux.Unlock()
|
||||
|
||||
// run immediately at 00
|
||||
c.runDue(time.Now())
|
||||
|
||||
// run after each tick
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-c.tickerDone:
|
||||
return
|
||||
case t := <-c.ticker.C:
|
||||
c.runDue(t)
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
c.mux.Unlock()
|
||||
}
|
||||
|
||||
// HasStarted checks whether the current Cron ticker has been started.
|
||||
func (c *Cron) HasStarted() bool {
|
||||
c.mux.RLock()
|
||||
defer c.mux.RUnlock()
|
||||
|
||||
return c.ticker != nil
|
||||
}
|
||||
|
||||
// runDue runs all registered jobs that are scheduled for the provided time.
|
||||
func (c *Cron) runDue(t time.Time) {
|
||||
c.mux.RLock()
|
||||
defer c.mux.RUnlock()
|
||||
|
||||
moment := NewMoment(t.In(c.timezone))
|
||||
|
||||
for _, j := range c.jobs {
|
||||
if j.schedule.IsDue(moment) {
|
||||
go j.Run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////
|
||||
|
||||
// CronJob defines a single registered cron job.
|
||||
type CronJob struct {
|
||||
fn func()
|
||||
schedule *Schedule
|
||||
id string
|
||||
scheduler *goja_util.Scheduler
|
||||
}
|
||||
|
||||
// Id returns the cron job id.
|
||||
func (j *CronJob) Id() string {
|
||||
return j.id
|
||||
}
|
||||
|
||||
// Expression returns the plain cron job schedule expression.
|
||||
func (j *CronJob) Expression() string {
|
||||
return j.schedule.rawExpr
|
||||
}
|
||||
|
||||
// Run runs the cron job function.
|
||||
func (j *CronJob) Run() {
|
||||
if j.fn != nil {
|
||||
j.scheduler.ScheduleAsync(func() error {
|
||||
j.fn()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON implements [json.Marshaler] and export the current
|
||||
// jobs data into valid JSON.
|
||||
func (j CronJob) MarshalJSON() ([]byte, error) {
|
||||
plain := struct {
|
||||
Id string `json:"id"`
|
||||
Expression string `json:"expression"`
|
||||
}{
|
||||
Id: j.Id(),
|
||||
Expression: j.Expression(),
|
||||
}
|
||||
|
||||
return json.Marshal(plain)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////
|
||||
|
||||
// Moment represents a parsed single time moment.
|
||||
type Moment struct {
|
||||
Minute int `json:"minute"`
|
||||
Hour int `json:"hour"`
|
||||
Day int `json:"day"`
|
||||
Month int `json:"month"`
|
||||
DayOfWeek int `json:"dayOfWeek"`
|
||||
}
|
||||
|
||||
// NewMoment creates a new Moment from the specified time.
|
||||
func NewMoment(t time.Time) *Moment {
|
||||
return &Moment{
|
||||
Minute: t.Minute(),
|
||||
Hour: t.Hour(),
|
||||
Day: t.Day(),
|
||||
Month: int(t.Month()),
|
||||
DayOfWeek: int(t.Weekday()),
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule stores parsed information for each time component when a cron job should run.
|
||||
type Schedule struct {
|
||||
Minutes map[int]struct{} `json:"minutes"`
|
||||
Hours map[int]struct{} `json:"hours"`
|
||||
Days map[int]struct{} `json:"days"`
|
||||
Months map[int]struct{} `json:"months"`
|
||||
DaysOfWeek map[int]struct{} `json:"daysOfWeek"`
|
||||
|
||||
rawExpr string
|
||||
}
|
||||
|
||||
// IsDue checks whether the provided Moment satisfies the current Schedule.
|
||||
func (s *Schedule) IsDue(m *Moment) bool {
|
||||
if _, ok := s.Minutes[m.Minute]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, ok := s.Hours[m.Hour]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, ok := s.Days[m.Day]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, ok := s.DaysOfWeek[m.DayOfWeek]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, ok := s.Months[m.Month]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
var macros = map[string]string{
|
||||
"@yearly": "0 0 1 1 *",
|
||||
"@annually": "0 0 1 1 *",
|
||||
"@monthly": "0 0 1 * *",
|
||||
"@weekly": "0 0 * * 0",
|
||||
"@daily": "0 0 * * *",
|
||||
"@midnight": "0 0 * * *",
|
||||
"@hourly": "0 * * * *",
|
||||
"@30min": "*/30 * * * *",
|
||||
"@15min": "*/15 * * * *",
|
||||
"@10min": "*/10 * * * *",
|
||||
"@5min": "*/5 * * * *",
|
||||
}
|
||||
|
||||
// NewSchedule creates a new Schedule from a cron expression.
|
||||
//
|
||||
// A cron expression could be a macro OR 5 segments separated by space,
|
||||
// representing: minute, hour, day of the month, month and day of the week.
|
||||
//
|
||||
// The following segment formats are supported:
|
||||
// - wildcard: *
|
||||
// - range: 1-30
|
||||
// - step: */n or 1-30/n
|
||||
// - list: 1,2,3,10-20/n
|
||||
//
|
||||
// The following macros are supported:
|
||||
// - @yearly (or @annually)
|
||||
// - @monthly
|
||||
// - @weekly
|
||||
// - @daily (or @midnight)
|
||||
// - @hourly
|
||||
func NewSchedule(cronExpr string) (*Schedule, error) {
|
||||
if v, ok := macros[cronExpr]; ok {
|
||||
cronExpr = v
|
||||
}
|
||||
|
||||
segments := strings.Split(cronExpr, " ")
|
||||
if len(segments) != 5 {
|
||||
return nil, errors.New("invalid cron expression - must be a valid macro or to have exactly 5 space separated segments")
|
||||
}
|
||||
|
||||
minutes, err := parseCronSegment(segments[0], 0, 59)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hours, err := parseCronSegment(segments[1], 0, 23)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
days, err := parseCronSegment(segments[2], 1, 31)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
months, err := parseCronSegment(segments[3], 1, 12)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
daysOfWeek, err := parseCronSegment(segments[4], 0, 6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Schedule{
|
||||
Minutes: minutes,
|
||||
Hours: hours,
|
||||
Days: days,
|
||||
Months: months,
|
||||
DaysOfWeek: daysOfWeek,
|
||||
rawExpr: cronExpr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseCronSegment parses a single cron expression segment and
|
||||
// returns its time schedule slots.
|
||||
func parseCronSegment(segment string, min int, max int) (map[int]struct{}, error) {
|
||||
slots := map[int]struct{}{}
|
||||
|
||||
list := strings.Split(segment, ",")
|
||||
for _, p := range list {
|
||||
stepParts := strings.Split(p, "/")
|
||||
|
||||
// step (*/n, 1-30/n)
|
||||
var step int
|
||||
switch len(stepParts) {
|
||||
case 1:
|
||||
step = 1
|
||||
case 2:
|
||||
parsedStep, err := strconv.Atoi(stepParts[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parsedStep < 1 || parsedStep > max {
|
||||
return nil, fmt.Errorf("invalid segment step boundary - the step must be between 1 and the %d", max)
|
||||
}
|
||||
step = parsedStep
|
||||
default:
|
||||
return nil, errors.New("invalid segment step format - must be in the format */n or 1-30/n")
|
||||
}
|
||||
|
||||
// find the min and max range of the segment part
|
||||
var rangeMin, rangeMax int
|
||||
if stepParts[0] == "*" {
|
||||
rangeMin = min
|
||||
rangeMax = max
|
||||
} else {
|
||||
// single digit (1) or range (1-30)
|
||||
rangeParts := strings.Split(stepParts[0], "-")
|
||||
switch len(rangeParts) {
|
||||
case 1:
|
||||
if step != 1 {
|
||||
return nil, errors.New("invalid segement step - step > 1 could be used only with the wildcard or range format")
|
||||
}
|
||||
parsed, err := strconv.Atoi(rangeParts[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parsed < min || parsed > max {
|
||||
return nil, errors.New("invalid segment value - must be between the min and max of the segment")
|
||||
}
|
||||
rangeMin = parsed
|
||||
rangeMax = rangeMin
|
||||
case 2:
|
||||
parsedMin, err := strconv.Atoi(rangeParts[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parsedMin < min || parsedMin > max {
|
||||
return nil, fmt.Errorf("invalid segment range minimum - must be between %d and %d", min, max)
|
||||
}
|
||||
rangeMin = parsedMin
|
||||
|
||||
parsedMax, err := strconv.Atoi(rangeParts[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parsedMax < parsedMin || parsedMax > max {
|
||||
return nil, fmt.Errorf("invalid segment range maximum - must be between %d and %d", rangeMin, max)
|
||||
}
|
||||
rangeMax = parsedMax
|
||||
default:
|
||||
return nil, errors.New("invalid segment range format - the range must have 1 or 2 parts")
|
||||
}
|
||||
}
|
||||
|
||||
// fill the slots
|
||||
for i := rangeMin; i <= rangeMax; i += step {
|
||||
slots[i] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return slots, nil
|
||||
}
|
||||
Reference in New Issue
Block a user