return s, nil
}
+// save writes the file whole, with mode 0600. The caller holds the mutex.
+func (s *Schedules) save() error {
+ data, err := json.Marshal(s.file)
+ if err == nil {
+ err = writeFileAtomic(s.path, append(data, '\n'))
+ }
+
+ if err != nil {
+ return fmt.Errorf("save schedules: %w", err)
+ }
+
+ return nil
+}
+
// Run starts the runs of the due schedules in l until ctx ends. It waits until
// the first due time, at most scheduleClockWaitTimeout at a time, and also
// wakes at a set or a remove. So a schedule that came due while the daemon was
}
}
-// Set adds a schedule with text and returns the answer of the tool. Exactly one
-// of at and cron is not nil. at is a local time in scheduleAtLayout after now,
-// and it goes through scheduleWallTime, so a time in the hour that the clock
-// repeats is its first occurrence. cron is an expression of scheduleParseCron.
-func (s *Schedules) Set(text string, at, cron *string) (string, error) {
- text = strings.TrimSpace(text)
- if text == "" || len(text) > scheduleTextBytes {
- return "", fmt.Errorf("the text must have 1 to %d bytes", scheduleTextBytes)
- }
-
- now := s.clock()
- schedule := Schedule{
- Text: text,
- Owner: s.owner,
- }
-
- switch {
- case (at == nil) == (cron == nil):
- return "", errors.New("give either at or cron, and null for the other")
- case at != nil:
- parsed, err := time.Parse(scheduleAtLayout, *at)
- if err != nil {
- return "", fmt.Errorf("at must be a local time such as %s", now.Format(scheduleAtLayout))
- }
-
- schedule.Due = scheduleWallTime(parsed.Year(), parsed.Month(), parsed.Day(), parsed.Hour(), parsed.Minute(), now.Location())
- if !schedule.Due.After(now) {
- return "", fmt.Errorf("the time %s is not after now, %s", schedule.Due.Format(scheduleTimeLayout), now.Format(scheduleTimeLayout))
- }
- default:
- parsed, err := scheduleParseCron(*cron)
- if err != nil {
- return "", err
- }
-
- next, found := parsed.Next(now)
- if !found {
- return "", fmt.Errorf("the cron expression %q matches no time in %d years", *cron, scheduleCronYears)
- }
-
- schedule.Cron = parsed.String()
- schedule.Due = next
- }
-
- s.mutex.Lock()
- defer s.mutex.Unlock()
-
- s.file.Next++
- schedule.ID = s.file.Next
- s.file.Schedules = append(s.file.Schedules, schedule)
- err := s.save()
- if err != nil {
- return "", err
- }
-
- s.change()
-
- return fmt.Sprintf("Schedule %d set. Next: %s.", schedule.ID, schedule.Due.In(now.Location()).Format(scheduleTimeLayout)), nil
-}
-
-// List returns the answer of the tool: one line for each schedule, by due time,
-// with its ID, its next time, its cron expression or once, and its text on one
-// line.
-func (s *Schedules) List() string {
- location := s.clock().Location()
- schedules := s.sorted()
- if len(schedules) == 0 {
- return "No schedules."
- }
-
- lines := make([]string, 0, len(schedules))
- for _, schedule := range schedules {
- lines = append(lines, strconv.Itoa(schedule.ID)+" | "+schedule.Due.In(location).Format(scheduleTimeLayout)+" | "+cmp.Or(schedule.Cron, "once")+" | "+strings.Join(strings.Fields(schedule.Text), " "))
- }
-
- return strings.Join(lines, "\n")
-}
-
-// Len returns the number of schedules.
-func (s *Schedules) Len() int {
- s.mutex.Lock()
- defer s.mutex.Unlock()
-
- return len(s.file.Schedules)
-}
-
-// Remove removes the schedule id and returns the answer of the tool.
-func (s *Schedules) Remove(id int) (string, error) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
-
- index := slices.IndexFunc(s.file.Schedules, func(schedule Schedule) bool {
- return schedule.ID == id
- })
-
- if index < 0 {
- return "", fmt.Errorf("no schedule %d", id)
- }
-
- s.file.Schedules = slices.Delete(s.file.Schedules, index, index+1)
- err := s.save()
- if err != nil {
- return "", err
- }
-
- s.change()
-
- return fmt.Sprintf("Schedule %d removed.", id), nil
-}
-
// deliver logs the message of each due schedule in l, in due order, as a work
// message of scheduleSource, and wakes the loop once with all of them. A
// schedule ends its run with Done after its message is durable in the log. A
return nil
}
+// Wait returns how long after now the first schedule is due, or false when
+// there is no schedule.
+func (s *Schedules) Wait(now time.Time) (time.Duration, bool) {
+ schedules := s.sorted()
+ if len(schedules) == 0 {
+ return 0, false
+ }
+
+ return max(schedules[0].Due.Sub(now), 0), true
+}
+
// Due returns the schedules whose time is not after now, by due time.
func (s *Schedules) Due(now time.Time) []Schedule {
var due []Schedule
return s.save()
}
-// Wait returns how long after now the first schedule is due, or false when
-// there is no schedule.
-func (s *Schedules) Wait(now time.Time) (time.Duration, bool) {
- schedules := s.sorted()
- if len(schedules) == 0 {
- return 0, false
+// sorted returns a copy of the schedules by due time, then by ID.
+func (s *Schedules) sorted() []Schedule {
+ s.mutex.Lock()
+
+ schedules := slices.Clone(s.file.Schedules)
+ s.mutex.Unlock()
+
+ slices.SortFunc(schedules, func(a, b Schedule) int {
+ return cmp.Or(a.Due.Compare(b.Due), cmp.Compare(a.ID, b.ID))
+ })
+
+ return schedules
+}
+
+// Set adds a schedule with text and returns the answer of the tool. Exactly one
+// of at and cron is not nil. at is a local time in scheduleAtLayout after now,
+// and it goes through scheduleWallTime, so a time in the hour that the clock
+// repeats is its first occurrence. cron is an expression of scheduleParseCron.
+func (s *Schedules) Set(text string, at, cron *string) (string, error) {
+ text = strings.TrimSpace(text)
+ if text == "" || len(text) > scheduleTextBytes {
+ return "", fmt.Errorf("the text must have 1 to %d bytes", scheduleTextBytes)
}
- return max(schedules[0].Due.Sub(now), 0), true
+ now := s.clock()
+ schedule := Schedule{
+ Text: text,
+ Owner: s.owner,
+ }
+
+ switch {
+ case (at == nil) == (cron == nil):
+ return "", errors.New("give either at or cron, and null for the other")
+ case at != nil:
+ parsed, err := time.Parse(scheduleAtLayout, *at)
+ if err != nil {
+ return "", fmt.Errorf("at must be a local time such as %s", now.Format(scheduleAtLayout))
+ }
+
+ schedule.Due = scheduleWallTime(parsed.Year(), parsed.Month(), parsed.Day(), parsed.Hour(), parsed.Minute(), now.Location())
+ if !schedule.Due.After(now) {
+ return "", fmt.Errorf("the time %s is not after now, %s", schedule.Due.Format(scheduleTimeLayout), now.Format(scheduleTimeLayout))
+ }
+ default:
+ parsed, err := scheduleParseCron(*cron)
+ if err != nil {
+ return "", err
+ }
+
+ next, found := parsed.Next(now)
+ if !found {
+ return "", fmt.Errorf("the cron expression %q matches no time in %d years", *cron, scheduleCronYears)
+ }
+
+ schedule.Cron = parsed.String()
+ schedule.Due = next
+ }
+
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+
+ s.file.Next++
+ schedule.ID = s.file.Next
+ s.file.Schedules = append(s.file.Schedules, schedule)
+ err := s.save()
+ if err != nil {
+ return "", err
+ }
+
+ s.change()
+
+ return fmt.Sprintf("Schedule %d set. Next: %s.", schedule.ID, schedule.Due.In(now.Location()).Format(scheduleTimeLayout)), nil
}
// change wakes Run after a set or a remove.
}
}
-// sorted returns a copy of the schedules by due time, then by ID.
-func (s *Schedules) sorted() []Schedule {
- s.mutex.Lock()
+// List returns the answer of the tool: one line for each schedule, by due time,
+// with its ID, its next time, its cron expression or once, and its text on one
+// line.
+func (s *Schedules) List() string {
+ location := s.clock().Location()
+ schedules := s.sorted()
+ if len(schedules) == 0 {
+ return "No schedules."
+ }
- schedules := slices.Clone(s.file.Schedules)
- s.mutex.Unlock()
+ lines := make([]string, 0, len(schedules))
+ for _, schedule := range schedules {
+ lines = append(lines, strconv.Itoa(schedule.ID)+" | "+schedule.Due.In(location).Format(scheduleTimeLayout)+" | "+cmp.Or(schedule.Cron, "once")+" | "+strings.Join(strings.Fields(schedule.Text), " "))
+ }
- slices.SortFunc(schedules, func(a, b Schedule) int {
- return cmp.Or(a.Due.Compare(b.Due), cmp.Compare(a.ID, b.ID))
- })
+ return strings.Join(lines, "\n")
+}
- return schedules
+// Len returns the number of schedules.
+func (s *Schedules) Len() int {
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+
+ return len(s.file.Schedules)
}
-// save writes the file whole, with mode 0600. The caller holds the mutex.
-func (s *Schedules) save() error {
- data, err := json.Marshal(s.file)
- if err == nil {
- err = writeFileAtomic(s.path, append(data, '\n'))
+// Remove removes the schedule id and returns the answer of the tool.
+func (s *Schedules) Remove(id int) (string, error) {
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+
+ index := slices.IndexFunc(s.file.Schedules, func(schedule Schedule) bool {
+ return schedule.ID == id
+ })
+
+ if index < 0 {
+ return "", fmt.Errorf("no schedule %d", id)
}
+ s.file.Schedules = slices.Delete(s.file.Schedules, index, index+1)
+ err := s.save()
if err != nil {
- return fmt.Errorf("save schedules: %w", err)
+ return "", err
}
- return nil
+ s.change()
+
+ return fmt.Sprintf("Schedule %d removed.", id), nil
}
// Message returns the text of a run at now for the log, with the times in the