guix-commits
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[shepherd] 02/02: DRAFT Add timer service.


From: Ludovic Courtès
Subject: [shepherd] 02/02: DRAFT Add timer service.
Date: Fri, 15 Mar 2024 19:06:05 -0400 (EDT)

civodul pushed a commit to branch wip-timers
in repository shepherd.

commit c930551fdc2d9b02c50111f1198a1c0b0eeae372
Author: Ludovic Courtès <ludo@gnu.org>
AuthorDate: Sat Mar 16 00:04:00 2024 +0100

    DRAFT Add timer service.
    
    DRAFT Actual service is missing.
    
    * modules/shepherd/service/timer.scm,
    tests/services/timer.scm: New files.
    * Makefile.am (dist_servicesub_DATA, TESTS): Add them.
---
 Makefile.am                        |   8 +-
 modules/shepherd/service/timer.scm | 275 +++++++++++++++++++++++++++++++++++++
 tests/services/timer.scm           |  73 ++++++++++
 3 files changed, 353 insertions(+), 3 deletions(-)

diff --git a/Makefile.am b/Makefile.am
index 25f7429..ccff59d 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,6 +1,6 @@
 # Makefile.am -- How to build and install the Shepherd.
 # Copyright © 2002, 2003 Wolfgang Jährling <wolfgang@pro-linux.de>
-# Copyright © 2013-2016, 2018-2020, 2022-2023 Ludovic Courtès <ludo@gnu.org>
+# Copyright © 2013-2016, 2018-2020, 2022-2024 Ludovic Courtès <ludo@gnu.org>
 # Copyright © 2018 Carlo Zancanaro <carlo@zancanaro.id.au>
 # Copyright © 2023 Oleg Pykhalov <go.wigust@gmail.com>
 #
@@ -49,7 +49,8 @@ nodist_shepherdsub_DATA =                     \
   modules/shepherd/system.scm
 dist_servicesub_DATA =                         \
   modules/shepherd/service/monitoring.scm      \
-  modules/shepherd/service/repl.scm
+  modules/shepherd/service/repl.scm            \
+  modules/shepherd/service/timer.scm
 
 shepherdgosubdir = $(guileobjectdir)/shepherd
 servicegosubdir = $(guileobjectdir)/shepherd/service
@@ -277,7 +278,8 @@ TESTS =                                             \
   tests/daemonize.sh                           \
   tests/eval-load.sh                           \
   tests/services/monitoring.sh                 \
-  tests/services/repl.sh
+  tests/services/repl.sh                       \
+  tests/services/timer.scm
 
 TEST_EXTENSIONS = .sh .scm
 EXTRA_DIST += $(TESTS)
diff --git a/modules/shepherd/service/timer.scm 
b/modules/shepherd/service/timer.scm
new file mode 100644
index 0000000..6a786ea
--- /dev/null
+++ b/modules/shepherd/service/timer.scm
@@ -0,0 +1,275 @@
+;; timer.scm -- Timer service.
+;; Copyright (C) 2024 Ludovic Courtès <ludo@gnu.org>
+;;
+;; This file is part of the GNU Shepherd.
+;;
+;; The GNU Shepherd is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3 of the License, or (at
+;; your option) any later version.
+;;
+;; The GNU Shepherd is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with the GNU Shepherd.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (shepherd service timer)
+  #:use-module (shepherd service)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-9)
+  #:use-module (srfi srfi-9 gnu)
+  #:use-module (srfi srfi-19)
+  #:use-module (ice-9 match)
+  #:export (calendar-event
+            calendar-event?
+            calendar-event-months
+            calendar-event-days-of-month
+            calendar-event-days-of-week
+            calendar-event-hours
+            calendar-event-minutes
+
+            next-calendar-event))
+
+;; herd list timer -> shows current timers like 'systemctl list-timers'
+;; herd at timer TIME COMMAND -> like 'at'
+;; herd stop timer -> stop all the timers
+;; herd stop foo-timer -> stop specific timed service
+
+;; Vixie:
+;; "minute hour day-of-month month day-of-week"
+
+(define-record-type <calendar-event>
+  (%calendar-event minutes hours days-of-month months days-of-week)
+  calendar-event?
+  (minutes       calendar-event-minutes)
+  (hours         calendar-event-hours)
+  (days-of-month calendar-event-days-of-month)
+  (months        calendar-event-months)
+  (days-of-week  calendar-event-days-of-week))
+
+(define any-minute (iota 60))
+(define any-hour (iota 24))
+(define any-day-of-month (iota 31 1))
+(define any-month (iota 12 1))
+(define* (calendar-event #:key
+                         (minutes any-minute)
+                         (hours any-hour)
+                         days-of-week
+                         (days-of-month
+                          (and (not days-of-week) any-day-of-month))
+                         (months any-month))
+  (%calendar-event minutes hours days-of-month months days-of-week))
+
+(define-syntax-rule (define-date-setter name getter)
+  (define (name date value)
+    (set-field date (getter) value)))
+
+(define-date-setter set-date-nanosecond date-nanosecond)
+(define-date-setter set-date-second date-second)
+(define-date-setter set-date-minute date-minute)
+(define-date-setter set-date-hour date-hour)
+(define-date-setter set-date-day date-day)
+(define-date-setter set-date-month date-month)
+(define-date-setter set-date-year date-year)
+
+(define (increment-year date)
+  (set-date-year date (+ 1 (date-year date))))
+
+(define (increment-month date)
+  (if (< (date-month date) 12)
+      (set-date-month date (+ (date-month date) 1))
+      (set-date-month (increment-year date) 1)))
+
+(define (increment-day date)
+  (if (< (date-day date)
+         (days-in-month (date-month date) (date-year date)))
+      (set-date-day date (+ (date-day date) 1))
+      (set-date-day (increment-month date) 1)))
+
+(define (increment-hour date)
+  (if (< (date-hour date) 23)
+      (set-date-hour date (+ (date-hour date) 1))
+      (set-date-hour (increment-day date) 0)))
+
+(define (increment-minute date)
+  (if (< (date-minute date) 59)
+      (set-date-minute date (+ (date-minute date) 1))
+      (set-date-minute (increment-hour date) 0)))
+
+(define (days-in-month month year)
+  "Return the number of days in @var{month} of @var{year}."
+  (let* ((next-day (make-date 0 0 0 0
+                              1 (modulo (+ 1 month) 12)
+                              (if (= 12 month) (+ 1 year) year)
+                              0))
+         (time (date->time-utc next-day))
+         (date (time-utc->date
+                (make-time time-utc 0
+                           (- (time-second time) 3600))
+                0)))
+    (date-day date)))
+
+(define (sooner current max)
+  "Return a two-argument procedure that returns true when its first argument
+is closer to @var{current} than its second argument.  The distance to
+@var{current} is computed modulo @var{max}."
+  (define (distance value)
+    (modulo (- value current) max))
+
+  (lambda (value1 value2)
+    (< (distance value1) (distance value2))))
+
+(define (fit-month date months)
+  (let loop ((candidates (sort months
+                               (sooner (date-month date) 12))))
+    (match candidates
+      ((first . rest)
+       (if (and (= first (date-month date))
+                (> (date-day date) 1))
+           (loop rest)
+           (let ((next (if (>= first (date-month date))
+                           date
+                           (increment-year date))))
+             (set-date-month next first)))))))
+
+(define (fit-day date days)
+  (let loop ((candidates (sort days
+                               (sooner (date-day date)
+                                       (days-in-month (date-month date)
+                                                      (date-year date))))))
+    (match candidates
+      ((first . rest)
+       (if (and (= first (date-day date))
+                (> (date-hour date) 0))
+           (loop rest)
+           (let ((next (if (>= first (date-day date))
+                           date
+                           (increment-month date))))
+             (set-date-day next first)
+             #;(make-date 0 0 0 0
+             first (date-month next) (date-year next)  ; ; ; ; ; ;
+             (date-zone-offset next))))))))
+
+(define (fit-hour date hours)
+  (let loop ((candidates (sort hours
+                               (sooner (date-hour date) 24))))
+    (match candidates
+      ((first . rest)
+       (if (and (= first (date-hour date))
+                (> (date-minute date) 0))
+           (loop rest)
+           (let ((next (if (>= first (date-hour date))
+                           date
+                           (increment-day date))))
+             (set-date-hour next first)
+             #;(make-date 0 0 0 next
+             (date-day next) (date-month next) (date-year next) ; ; ; ; ; ;
+             (date-zone-offset next))))))))
+
+(define (fit-minute date minutes)
+  (let loop ((candidates (sort minutes
+                               (sooner (date-minute date) 60))))
+    (match candidates
+      ((first . rest)
+       (if (and (= first (date-minute date))
+                (> (date-second date) 0))
+           (loop rest)
+           (let ((next (if (>= first (date-minute date))
+                           date
+                           (increment-hour date))))
+             (set-date-minute next first)
+             #;(make-date 0 0 0 next
+             (date-day next) (date-month next) (date-year next) ; ; ; ; ; ;
+             (date-zone-offset next))))))))
+
+(define (week-days->month-days week-days month year)
+  "Given @var{week-days}, a list of week-days (between 0 and 6, where 0 is
+Sunday), return the corresponding list of days in @var{month} of @var{year}."
+  (let loop ((date (make-date 0 0 0 0 1 month year 0))
+             (days '()))
+    (if (= (date-month date) month)
+        (loop (increment-day date)
+              (if (memv (date-week-day date) week-days)
+                  (cons (date-day date) days)
+                  days))
+        (reverse days))))
+
+(define (next-calendar-event event)
+  "Return a procedure that, given a date object, returns the next date that
+matches @var{event}."
+  (define (month date)
+    (if (memv (date-month date) (calendar-event-months event))
+        date
+        (fit-month date (calendar-event-months event))))
+
+  (define (day date)
+    (let ((days (append
+                 (or (calendar-event-days-of-month event) '())
+                 (match (calendar-event-days-of-week event)
+                   (#f
+                    '())
+                   (days (week-days->month-days days
+                                                (date-month date)
+                                                (date-year date)))))))
+      (if (memv (date-day date) days)
+          date
+          (fit-day date days))))
+
+  (define (hour date)
+    (if (memv (date-hour date) (calendar-event-hours event))
+        date
+        (fit-hour date (calendar-event-hours event))))
+
+  (define (minute date)
+    (if (memv (date-minute date) (calendar-event-minutes event))
+        date
+        (fit-minute date (calendar-event-minutes event))))
+
+  (define (second date)
+    ;; Clear seconds and nanoseconds and jump to the next minute.
+    (increment-minute (set-date-nanosecond (set-date-second date 0) 0)))
+
+  (lambda (date)
+    ;; TODO: Adjust timezone.
+    (month (day (hour (minute (second date)))))))
+
+(define (seconds-to-wait event)
+  "Return the number of seconds to wait before the next occurrence of
+@var{event}."
+  (let* ((now (current-time time-utc))
+         (then ((next-calendar-event event) (time-utc->date now))))
+    (time-second (time-difference (date->time-utc then) now))))
+
+
+(define (make-timer-constructor event command)
+  (lambda ()
+    (let ((timer (lookup-service 'timer))
+          (wakeup (make-channel)))
+      (put-message (service-control timer)
+                   `(register ,(current-service)
+                              ,spec ,wakeup))
+      (spawn-fiber
+       (lambda ()
+         (let loop ()
+           (match (get-message* wakeup (seconds-to-wait event))
+             ('terminate
+              #t)
+             ('timeout
+              (spawn-command (command-line command)
+                             #:user (command-user command)
+                             #:group (command-group command)
+                             #:environment-variables
+                             (command-environment-variables command)
+                             #:directory (command-directory command)
+                             #:resource-limits
+                             (command-resource-limits command))
+              (loop))))))
+      wakeup)))
+
+(define (make-timer-destructor)
+  (lambda (wakeup)
+    (put-message wakeup 'terminate)
+    #f))
diff --git a/tests/services/timer.scm b/tests/services/timer.scm
new file mode 100644
index 0000000..689597c
--- /dev/null
+++ b/tests/services/timer.scm
@@ -0,0 +1,73 @@
+;; GNU Shepherd --- Test timer service.
+;; Copyright © 2024 Ludovic Courtès <ludo@gnu.org>
+;;
+;; This file is part of the GNU Shepherd.
+;;
+;; The GNU Shepherd is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3 of the License, or (at
+;; your option) any later version.
+;;
+;; The GNU Shepherd is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with the GNU Shepherd.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (test-timer)
+  #:use-module (shepherd service timer)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-19)
+  #:use-module (srfi srfi-64))
+
+(test-begin "timer")
+
+(test-equal "next-calendar-event, leap year"
+  (make-date 0 0 00 12 29 02 2024 3600)
+  ((next-calendar-event (calendar-event #:hours '(12) #:minutes '(0)))
+   (make-date 123456789 42 44 12 28 02 2024 3600)))
+
+(test-equal "next-calendar-event, non-leap year"
+  (make-date 0 0 00 12 01 03 2023 3600)
+  ((next-calendar-event (calendar-event #:hours '(12) #:minutes '(0)))
+   (make-date 123456789 42 44 12 28 02 2023 3600)))
+
+(test-equal "next-calendar-event, same day"
+  (make-date 0 0 42 12 28 02 2024 3600)
+  ((next-calendar-event (calendar-event #:hours '(12) #:minutes '(42)))
+   (make-date 123456789 42 09 12 28 02 2024 3600)))
+
+(test-equal "next-calendar-event, days of week"
+  `(,(make-date 0 0 30 12 02 03 2024 3600)
+    ,(make-date 0 0 30 18 02 03 2024 3600)
+    ,@(append-map (lambda (day)
+                    (list (make-date 0 0 30 06 day 03 2024 3600)
+                          (make-date 0 0 30 12 day 03 2024 3600)
+                          (make-date 0 0 30 18 day 03 2024 3600)))
+                  '(03 06 09)))
+  ;;      March 2024
+  ;; Su Mo Tu We Th Fr Sa
+  ;;                 1  2
+  ;;  3  4  5  6  7  8  9
+  ;; 10 11 12 13 14 15 16
+  ;; 17 18 19 20 21 22 23
+  ;; 24 25 26 27 28 29 30
+  ;; 31
+  (let ((next (next-calendar-event
+               (calendar-event #:hours '(6 12 18)
+                               #:minutes '(30)
+                               ;; Sunday, Wednesday, Saturday
+                               #:days-of-week '(0 3 6)))))
+    (let loop ((date (make-date 123456789 42 09 12
+                                ;; Start on Saturday, March 2nd.
+                                02 03 2024 3600))
+               (n 0)
+               (result '()))
+      (if (< n 11)
+          (let ((date (next date)))
+            (loop date (+ 1 n) (cons date result)))
+          (reverse result)))))
+
+(test-end "timer")



reply via email to

[Prev in Thread] Current Thread [Next in Thread]