civodul pushed a commit to branch devel
in repository shepherd.

commit 8ad7997fb92276aecab6b667d9e3aadfe44561d3
Author: Ludovic Courtès <[email protected]>
AuthorDate: Sat Oct 12 11:28:10 2024 +0200

    timer: Add ‘timer’ service with ‘schedule’ action.
    
    * modules/shepherd/service/timer.scm (invalid-argument)
    (string->calendar-event, timer-arguments->calendar-event+command)
    (schedule-timer, timer-service): New procedures.
    * tests/services/timer-at.sh: New file.
    * Makefile.am (TESTS): Add it.
    * tests/services/timer-events.scm ("timer-arguments->calendar-event, 
simple")
    ("timer-arguments->calendar-event, \"as\"")
    ("timer-arguments->calendar-event, wrong-type-arg"): New tests.
    * doc/shepherd.texi (Timers): Document it.
---
 Makefile.am                        |   1 +
 doc/shepherd.texi                  |  41 +++++++++++++-
 modules/shepherd/service/timer.scm | 113 ++++++++++++++++++++++++++++++++++++-
 tests/services/timer-at.sh         |  65 +++++++++++++++++++++
 tests/services/timer-events.scm    |  34 +++++++++++
 5 files changed, 252 insertions(+), 2 deletions(-)

diff --git a/Makefile.am b/Makefile.am
index 2d8e70b..d530dc6 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -287,6 +287,7 @@ TESTS =                                             \
   tests/services/monitoring.sh                 \
   tests/services/repl.sh                       \
   tests/services/timer.sh                      \
+  tests/services/timer-at.sh                   \
   tests/services/timer-events.scm              \
   tests/services/log-rotation.sh               \
   tests/services/log-rotation-internal.scm     \
diff --git a/doc/shepherd.texi b/doc/shepherd.texi
index 8857627..1b4f995 100644
--- a/doc/shepherd.texi
+++ b/doc/shepherd.texi
@@ -1570,7 +1570,7 @@ These arguments are the same as for 
@code{fork+exec-command} and related
 procedures (@pxref{exec-command, @code{fork+exec-command}}).
 @end deffn
 
-Last, it's also possible to add a @code{trigger} action to timer
+It's also possible to add a @code{trigger} action to timer
 services, such that one can trigger it with:
 
 @example
@@ -1587,6 +1587,45 @@ to invoke the action passed to 
@code{make-timer-constructor}.
 
 @xref{timer-example, timer example}, to see how to put it all together.
 
+@cindex timer service, like @command{at}
+@cindex @command{at} command, with timer service
+Last, the @code{(shepherd service timer)} provides a sort of ``meta''
+timer service that lets you dynamically create timed services, from the
+command line, in a way similar to the traditional @command{at} command:
+
+@example
+# Run the 'mail' command, as root, as 12PM.
+herd schedule timer at 12:00 -- \
+  mail --to=charlie -s "Lunch time!"
+
+# Run the 'mpg123' command as user 'charlie' at 7AM, from charlie's
+# home directory.
+herd schedule timer at 07:00 as charlie -- \
+  mpg123 Music/alarm.mp3
+@end example
+
+Each of these @command{herd schedule timer} commands creates a new timed
+service, which, like any other service, can be inspected and stopped;
+those services are transient and vanish after they have executed their
+command (@pxref{Defining Services}).
+
+This @code{timer} service can be added to your configuration like so:
+
+@lisp
+(use-modules (shepherd service timer))
+
+(register-services (list (timer-service)))
+@end lisp
+
+The reference follows.
+
+@deffn {Procedure} timer-service [@var{provision}] @
+         [#:requirement '()]
+Return a timer service with the given @var{provision} and
+@var{requirement}.  The service has a @code{schedule} action that lets users
+schedule command execution similar to the venerable @command{at} command.
+@end deffn
+
 @c @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
 
 @node The root Service
diff --git a/modules/shepherd/service/timer.scm 
b/modules/shepherd/service/timer.scm
index 1c3ad85..0e10d40 100644
--- a/modules/shepherd/service/timer.scm
+++ b/modules/shepherd/service/timer.scm
@@ -58,7 +58,10 @@
             make-timer-destructor
 
             trigger-timer
-            timer-trigger-action))
+            timer-trigger-action
+
+            timer-arguments->calendar-event+command
+            timer-service))
 
 ;;; Commentary:
 ;;;
@@ -804,3 +807,111 @@ doing nothing."))))
 (define timer-trigger-action
   (action 'trigger trigger-timer
           "Trigger the action associated with this timer."))
+
+
+;;;
+;;; 'at'-like timer.
+;;;
+
+(define (invalid-argument message . args)
+  "Raise an exception with the given @var{message} and format @var{args} using
+a good old key/arguments exception, such that the message is properly
+serialized as an action exception returned to the client, which, in turn, can
+display it correctly."
+  (apply throw 'wrong-type-arg #f message args '()))
+
+(define (string->calendar-event str)
+  "Parse @var{str}, a string such as @code{\"17:14\"}, and return the
+corresponding calendar event."
+  (define (hour? obj)
+    (and (integer? obj) (exact? obj)
+         (>= obj 0) (< obj 24)))
+  (define (minute? obj)
+    (and (integer? obj) (exact? obj)
+         (>= obj 0) (< obj 60)))
+  (define second? minute?)
+
+  (match (string-split str #\:)
+    (((= string->number (? hour? hour))
+      (= string->number (? minute? minute)))
+     (calendar-event #:seconds '(0)
+                     #:minutes (list minute)
+                     #:hours (list hour)))
+    (((= string->number (? hour? hour))
+      (= string->number (? minute? minute))
+      (= string->number (? second? second)))
+     (calendar-event #:seconds (list second)
+                     #:minutes (list minute)
+                     #:hours (list hour)))
+    (_
+     (invalid-argument (l10n "~a: invalid calendar time specification")
+                       str))))
+
+(define (timer-arguments->calendar-event+command spec)
+  "Parse @var{spec}, a list of arguments passed to the @code{schedule} timer
+action, and return two values: a calendar event and a command to run on the
+first occurrence of that event.  Raise an error if @var{spec} is invalid."
+  (define user-group
+    (compose group:name getgrnam passwd:gid getpwnam))
+
+  (define user-home
+    (compose passwd:dir getpwnam))
+
+  (let loop ((spec spec)
+             (event #f)
+             (user #f)
+             (arguments '()))
+    (match spec
+      (()
+       (unless event
+         (invalid-argument (l10n "Timer spec lacks 'at TIME'.")))
+       (unless (pair? arguments)
+         (invalid-argument (l10n "Timer spec lacks a command to run.")))
+       (values event
+               (command arguments
+                        #:user user
+                        #:group (and=> user user-group)
+                        #:directory
+                        (or (and=> user user-home)
+                            (default-service-directory)))))
+      (("at" time-spec rest ...)
+       (loop rest (string->calendar-event time-spec) user arguments))
+      (("as" user rest ...)
+       (loop rest event user arguments))
+      ((lst ...)
+       (if event
+           (loop '() event user lst)
+           (invalid-argument (l10n "Timer spec lacks 'at TIME'.")))))))
+
+(define* (schedule-timer spec #:key (requirement '()))
+  "Schedule a timer according to @var{spec}, a list of strings such as
+@code{'(\"at\" \"12:00\" \"xlock\")}.  The resulting timer is a registered as
+a transient timed service and started.  Raise an error if @var{spec} is
+invalid."
+  (let* ((event command (timer-arguments->calendar-event+command spec))
+         (timer (service (list (gensym "timer-"))
+                         #:requirement requirement
+                         #:transient? #t
+                         #:start (make-timer-constructor
+                                  event command
+                                  #:occurrences 1)
+                         #:stop (make-timer-destructor)
+                         #:actions (list timer-trigger-action))))
+    (register-services (list timer))
+    (start-service timer)))
+
+(define* (timer-service #:optional (provision '(timer))
+                        #:key (requirement '()))
+  "Return a timer service with the given @var{provision} and
+@var{requirement}.  The service has a @code{schedule} action that lets users
+schedule command execution similar to the venerable @command{at} command."
+  (define schedule-action
+    (action 'schedule
+            (lambda (_ . args)
+              (schedule-timer args
+                              #:requirement (list (first provision))))))
+
+  (service provision
+           #:start (const #t)
+           #:stop (const #f)
+           #:actions (list schedule-action)))
diff --git a/tests/services/timer-at.sh b/tests/services/timer-at.sh
new file mode 100644
index 0000000..60d49ec
--- /dev/null
+++ b/tests/services/timer-at.sh
@@ -0,0 +1,65 @@
+# GNU Shepherd --- Test timers.
+# Copyright © 2024 Ludovic Courtès <[email protected]>
+#
+# 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/>.
+
+shepherd --version
+herd --version
+
+socket="t-socket-$$"
+conf="t-conf-$$"
+log="t-log-$$"
+pid="t-pid-$$"
+witness="t-witness-$$"
+
+herd="herd -s $socket"
+
+trap "cat $log || true; rm -f $socket $conf $log $witness;
+      test -f $pid && kill \`cat $pid\` || true; rm -f $pid" EXIT
+
+cat > "$conf" <<EOF
+(use-modules (shepherd service timer))
+
+(register-services (list (timer-service)))
+(start-in-the-background '(timer))
+EOF
+
+rm -f "$pid"
+shepherd -I -s "$socket" -c "$conf" -l "$log" --pid="$pid" &
+
+# Wait till it's ready.
+while ! test -f "$pid" ; do sleep 0.3 ; done
+
+shepherd_pid="`cat $pid`"
+
+function soonish
+{
+    guile -c '(use-modules (srfi srfi-19))
+(let* ((now (current-time time-utc))
+       (soon (make-time time-utc 0 (+ 5 (time-second now)))))
+  (display (date->string (time-utc->date soon) "~H:~M:~S")))'
+}
+
+$herd schedule timer at $(soonish) -- \
+      sh -c "echo TIMER; echo success > $PWD/$witness"
+
+until test -f "$witness"; do sleep 0.5; done
+grep TIMER "$log"
+
+$herd schedule timer at whatever && false
+$herd schedule timer do something && false
+
+$herd stop timer
diff --git a/tests/services/timer-events.scm b/tests/services/timer-events.scm
index 8da8f00..e373a18 100644
--- a/tests/services/timer-events.scm
+++ b/tests/services/timer-events.scm
@@ -226,4 +226,38 @@ invalid ~a field"
   (test-cron-error "30 4 22 * 9" 'days-of-week)
   (test-cron-error "0-99 4 22 * *" 'minutes))
 
+(test-equal "timer-arguments->calendar-event, simple"
+  (list (calendar-event #:seconds '(0)
+                        #:minutes '(14)
+                        #:hours '(17))
+        (command '("echo" "hello")))
+  (call-with-values
+      (lambda ()
+        (timer-arguments->calendar-event+command
+         '("at" "17:14" "echo" "hello")))
+    list))
+
+(test-equal "timer-arguments->calendar-event, \"as\""
+  (list (calendar-event #:seconds '(56)
+                        #:minutes '(34)
+                        #:hours '(12))
+        (let ((me (getpwuid (getuid))))
+          (command '("echo" "hello")
+                   #:user (passwd:name me)
+                   #:group (group:name (getgrgid (passwd:gid me)))
+                   #:directory (passwd:dir me))))
+  (call-with-values
+      (lambda ()
+        (timer-arguments->calendar-event+command
+         `("at" "12:34:56" "as" ,(passwd:name (getpwuid (getuid)))
+           "echo" "hello")))
+    list))
+
+(test-assert "timer-arguments->calendar-event, wrong-type-arg"
+  (catch 'wrong-type-arg
+    (lambda ()
+      (timer-arguments->calendar-event+command '("echo" "forgot something"))
+      #f)
+    (const #t)))
+
 (test-end "timer")

Reply via email to