mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-20 21:05:02 +03:00
fix(settings): accept five-field crontab for report and LDAP schedules
The panel scheduler runs WithSeconds(), whose parser rejects a standard five-field crontab such as "30 8 * * *". An admin who typed one into the Telegram/Discord notify time or the LDAP sync schedule saved it without error, AddJob then failed at startup, and the job silently never ran. Admin-typed schedules now go through internal/util/cronspec, which takes an optional seconds column plus the @descriptors and hands the scheduler a parsed cron.Schedule. CheckValid rejects an unparseable schedule on save, and a stored legacy value falls back to the job's default instead of dropping the job.
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
// Package cronspec parses the cron schedules an admin types into the settings.
|
||||
package cronspec
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
|
||||
)
|
||||
|
||||
// The panel scheduler runs WithSeconds(), whose parser rejects five-field crontab,
|
||||
// so admin schedules are parsed here with the seconds column optional.
|
||||
var parser = cron.NewParser(
|
||||
cron.SecondOptional | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor,
|
||||
)
|
||||
|
||||
func Parse(spec string) (cron.Schedule, error) {
|
||||
schedule, err := parser.Parse(strings.TrimSpace(spec))
|
||||
if err != nil {
|
||||
return nil, common.NewErrorf("cron schedule <%v> is not valid: %v", spec, err)
|
||||
}
|
||||
return schedule, nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package cronspec
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// #1648 upstream: the panel's scheduler runs WithSeconds(), whose parser refused
|
||||
// a standard five-field crontab, so an admin's "30 8 * * *" never fired.
|
||||
func TestParseNextFire(t *testing.T) {
|
||||
from := time.Date(2026, 10, 11, 7, 0, 0, 0, time.UTC)
|
||||
cases := []struct {
|
||||
spec string
|
||||
want time.Time
|
||||
}{
|
||||
{"30 8 * * *", time.Date(2026, 10, 11, 8, 30, 0, 0, time.UTC)},
|
||||
{" 15 30 8 * * * ", time.Date(2026, 10, 11, 8, 30, 15, 0, time.UTC)},
|
||||
{"@daily", time.Date(2026, 10, 12, 0, 0, 0, 0, time.UTC)},
|
||||
{"@every 90m", from.Add(90 * time.Minute)},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.spec, func(t *testing.T) {
|
||||
schedule, err := Parse(c.spec)
|
||||
if err != nil {
|
||||
t.Fatalf("Parse(%q): %v", c.spec, err)
|
||||
}
|
||||
if got := schedule.Next(from); !got.Equal(c.want) {
|
||||
t.Errorf("Parse(%q).Next = %v, want %v", c.spec, got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRejects(t *testing.T) {
|
||||
for _, spec := range []string{"", " ", "every day", "61 * * * *", "* * * *"} {
|
||||
t.Run(spec, func(t *testing.T) {
|
||||
_, err := Parse(spec)
|
||||
if err == nil {
|
||||
t.Fatalf("Parse(%q): want error, got nil", spec)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "cron schedule") {
|
||||
t.Errorf("Parse(%q) error = %q, want it to name the cron schedule", spec, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user