AboutSummaryRefsLogTreeCommitDiffStats
diff options
context:
space:
mode:
-rw-r--r--doc/mcron.texi84
-rw-r--r--src/cron.in3
-rw-r--r--src/mcron.in5
-rw-r--r--src/mcron/base.scm328
-rw-r--r--src/mcron/scripts/cron.scm77
-rw-r--r--src/mcron/scripts/mcron.scm20
-rw-r--r--tests/base.scm106
-rw-r--r--tests/basic.sh13
8 files changed, 516 insertions, 120 deletions
diff --git a/doc/mcron.texi b/doc/mcron.texi
index a176b2e..efc7d36 100644
--- a/doc/mcron.texi
+++ b/doc/mcron.texi
@@ -13,6 +13,7 @@ program for running jobs at scheduled times.
Copyright @copyright{} 2003, 2005, 2006, 2012, 2014, 2022 Dale Mellor
Copyright @copyright{} 2018 Mathieu Lirzin
+Copyright @copyright{} 2021 Maxim Cournoyer
@quotation
Permission is granted to copy, distribute and/or modify this
@@ -887,6 +888,53 @@ This option is used to indicate whether the configuration information
being passed on the standard input is in Vixie format or Guile
format. Guile is the default.
+@cindex log-format option
+@cindex options, log-format
+@cindex --log-format option
+@cindex logging output, configuration
+@cindex configuring the logging output
+@anchor{--log-format}
+@item --log-format
+This option accepts an @code{(ice-9 format)} format string that can be used to
+customize the appearance of the output. The format string is applied to
+@code{format} with the following four arguments:
+@enumerate
+@item A date/time string.
+@item The job process PID (as as number).
+@item The action name.
+@item The message to log.
+@end enumerate
+
+It defaults to @code{"~a ~2@*~a: ~a~%"}, which produces output messages like:
+
+@example
+2021-08-17T12:01:01 some-job: completed in 0.218s
+@end example
+
+If you'd rather see the job process PID instead of a timestamp, you could
+instead specify the format string as @code{"~1@*~a ~a: ~a~%"}, which would
+result in something like:
+
+@example
+39234 some-job: completed in 0.218s
+@end example
+
+To learn about all the possibilities offered by @code{(ice-9 format)}, refer
+to @ref{Formatted Output,,, guile, GNU@tie{}Guile@tie{}Reference@tie{}Manual}.
+
+@cindex date-format option
+@cindex options, date-format
+@cindex --date-format option
+@cindex timestamp, modification
+@cindex changing the default timestamp
+@anchor{--date-format}
+@item --date-format
+This option accepts a @code{(srfi srfi-19)} date string format, to customize
+the appearance of the timestamp in output messages. It defaults to
+@code{"~5"}, which corresponds to a local ISO-8601 date/time format
+(@pxref{SRFI-19 Date to string,,,
+guile,GNU@tie{}Guile@tie{}Reference@tie{}Manual}).
+
@cindex -v option
@cindex --version option
@cindex options, -v
@@ -981,6 +1029,12 @@ minute to check for modifications to @code{/etc/crontab}. It is
recommended that this option be used (and further that the
@code{/etc/crontab} file be taken off the system altogether!)
+@item --log-format
+Analogous to mcron's @ref{--log-format}.
+
+@item --date-format
+Analogous to mcron's @ref{--date-format}.
+
@end table
@node Invoking crontab, Behaviour on laptops, Invoking cron or crond, Invoking
@@ -1266,6 +1320,36 @@ last job that was reported in the schedule report. The report itself
is returned to the calling program as a string.
@end deffn
+@defopt %date-format
+@cindex parameters, date format
+@cindex date format parameter
+This parameter holds the @code{(srfi srfi-19)} format string used to produce
+the timestamp found in output messages. It defaults to @code{"~5"}.
+@end defopt
+
+@deffn{Scheme procedure} validate-date-format @var{fmt}
+@cindex date format validator
+This procedure is used to validate @var{fmt}, a @code{(srfi srfi-19)} format
+string. When @var{fmt} is invalid, an error message is displayed and the
+program is aborted.
+@end deffn
+
+@defopt %log-format
+@cindex parameters, log format
+@cindex log format parameter
+This parameter holds the @code{(ice-9 format)} format string used to produce
+the output messages. The four arguments applied to format are the timestamp,
+the process PID, the job name and the message. It defaults to @code{"~a
+~2@*~a: ~a~%"}.
+@end defopt
+
+@deffn{Scheme procedure} validate-log-format @var{fmt}
+@cindex log format validator
+This procedure is used to validate @var{fmt}, a @code{(ice-9 format)} format
+string. When @var{fmt} is invalid, an error message is displayed and the
+program is aborted.
+@end deffn
+
@node The redirect module, The vixie-time module, The base module, Guile modules
@section The redirect module
@cindex redirect module
diff --git a/src/cron.in b/src/cron.in
index 25ad273..be624e0 100644
--- a/src/cron.in
+++ b/src/cron.in
@@ -7,4 +7,5 @@
(set! %load-compiled-path (cons "%modbuilddir%" %load-compiled-path)))
(use-modules (mcron scripts cron))
-(main)
+(use-modules (mcron utils))
+(catch-mcron-error (main))
diff --git a/src/mcron.in b/src/mcron.in
index 268743c..07023f9 100644
--- a/src/mcron.in
+++ b/src/mcron.in
@@ -6,5 +6,6 @@
(set! %load-path (cons "%modsrcdir%" %load-path))
(set! %load-compiled-path (cons "%modbuilddir%" %load-compiled-path)))
-(use-modules (mcron scripts mcron))
-(main)
+(use-modules (mcron scripts mcron)
+ (mcron utils))
+(catch-mcron-error (main))
diff --git a/src/mcron/base.scm b/src/mcron/base.scm
index 037a9b7..6e325f7 100644
--- a/src/mcron/base.scm
+++ b/src/mcron/base.scm
@@ -2,6 +2,7 @@
;;; Copyright © 2003 Dale Mellor <dale_mellor@users.sourceforge.net>
;;; Copyright © 2015, 2016, 2018 Mathieu Lirzin <mthl@gnu.org>
;;; Copyright © 2016, 2020 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2021 Maxim Cournoyer <maxim.cournoyer@gmail.com>
;;;
;;; This file is part of GNU Mcron.
;;;
@@ -26,18 +27,29 @@
;;;; Code:
(define-module (mcron base)
+ #:use-module (ice-9 format)
#:use-module (ice-9 match)
#:use-module (ice-9 control)
+ #:use-module (ice-9 rdelim)
+ #:use-module (ice-9 suspendable-ports)
#:use-module (mcron environment)
#:use-module (mcron utils)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-2)
#:use-module (srfi srfi-9)
+ #:use-module ((srfi srfi-19) #:prefix srfi-19:)
+ #:use-module (srfi srfi-26)
#:use-module (srfi srfi-111)
#:export (add-job
remove-user-jobs
display-schedule
run-job-loop
+
+ %date-format
+ %log-format
+ validate-date-format
+ validate-log-format
+
;; Deprecated and undocumented procedures.
use-system-job-list
use-user-job-list
@@ -45,6 +57,8 @@
#:re-export (clear-environment-mods
append-environment-mods))
+(install-suspendable-ports!)
+
;; A cron job.
(define-record-type <job>
(make-job user time-proc action environment displayable next-time)
@@ -71,6 +85,48 @@
(user schedule-user set-schedule-user!) ;list of <job>
(current schedule-current set-schedule-current!)) ;symbol 'user or 'system
+;; A (srfi srfi-19) format string for the date. It is used to format the
+;; timestamp argument. Defaults to the local ISO-8601 date/time format.
+(define %date-format (make-parameter "~5"))
+
+(define (validate-date-format fmt)
+ "Validate that FMT is a valid srfi-19 date format string."
+ (let ((time (srfi-19:current-time)))
+ (unless (false-if-exception
+ (srfi-19:date->string (srfi-19:time-utc->date time) fmt))
+ (throw 'mcron-error 1 "invalid srfi-19 date format string
+hint: consult 'info \"(guile) SRFI-19 Date to string\"'"))))
+
+;; An (ice-9 format) format string. The positional arguments applied to
+;; format are:
+;; 1. the timestamp;
+;; 2. the job process PID;
+;; 3. the action name;
+;; 4. the message.
+(define %log-format (make-parameter "~a ~2@*~a: ~a~%"))
+
+(define (validate-log-format fmt)
+ "Validate that FMT is a valid (ice-9 format) log format string."
+ (unless (with-output-to-port (%make-void-port "w")
+ (lambda ()
+ (with-error-to-port (%make-void-port "w")
+ (lambda ()
+ (false-if-exception
+ (format #f "~@?" fmt
+ "2021-08-17T15:23:12" 39143 "dummy" "message"))))))
+ (throw 'mcron-error 1 "invalid (ice-9 format) format string
+hint: consult 'info \"(guile) Formatted Output\"'")))
+
+;; Data about a running job process.
+(define-record-type <job-data>
+ (make-job-data pid port continuation name)
+ job-data?
+ (pid job-data:pid) ;string : the job process PID
+ (port job-data:port) ;port : an input
+ (continuation job-data:continuation ;a partial continuation to read port
+ set-job-data-continuation!)
+ (name job-data:name)) ;string : the name of the job action
+
(define %global-schedule
;; Global schedule used by 'mcron' and 'cron'.
(make-schedule '() '() 'user))
@@ -172,50 +228,185 @@ unusable."
;; Apply PROC to the value stored in 'number-children'.
(set-box! number-children (proc (unbox number-children))))
-(define (run-job job)
- "Run JOB in a separate process. The process is run as JOB user with the
+(define* (run-job job)
+ "Run JOB in a separate process. The process is run as JOB user with the
environment properly set. Update the NEXT-TIME field of JOB by computing its
-next value."
- (if (= (primitive-fork) 0)
- (dynamic-wind ;child
- (const #t)
- (λ ()
- (setgid (passwd:gid (job:user job)))
- (setuid (passwd:uid (job:user job)))
- ;; Handle the case where the home directory points to a nonexistent
- ;; location, as can be the case when running the job as the "nobody"
- ;; user.
- (catch 'system-error
- (lambda ()
- (chdir (passwd:dir (job:user job))))
- (lambda args
- (let ((errno (system-error-errno args)))
- (cond
- ((= ENOENT errno) (chdir "/"))
- (else (throw 'system-error args))))))
- (modify-environment (job:environment job) (job:user job))
- ((job:action job)))
- (λ ()
- (primitive-exit 0)))
- (begin ;parent
- (update-number-children! 1+)
- (job:next-time-set! job ((job:next-time-function job)
- (current-time))))))
-
-(define (child-cleanup)
- ;; Give any zombie children a chance to die, and decrease the number known
- ;; to exist.
- (unless (or (<= (unbox number-children) 0)
- (= (car (waitpid WAIT_ANY WNOHANG)) 0))
- (update-number-children! 1-)
- (child-cleanup)))
-
-(define* (run-job-loop #:optional fd-list #:key (schedule %global-schedule))
- ;; Loop over all job specifications, get a list of the next ones to run (may
- ;; be more than one). Set an alarm and go to sleep. When we wake, run the
- ;; jobs and reap any children (old jobs) that have completed. Repeat ad
- ;; infinitum.
- ;;
+next value. Return a <job-data> record object containing the job process
+PID, the input pipe from which the process standard output and standard error
+streams can be read as well as the name of the job."
+ (define start (srfi-19:current-time srfi-19:time-monotonic)) ;start time
+
+ (define (seconds-since start)
+ ;; Return the time elapsed in seconds since START.
+ (let* ((end (srfi-19:current-time srfi-19:time-monotonic))
+ (elapsed (srfi-19:time-difference end start)))
+ (+ (srfi-19:time-second elapsed)
+ (* 1e-9 (srfi-19:time-nanosecond elapsed)))))
+
+ ;; Create a pipe, and set its read side to non-blocking mode.
+ (define child->parent-pipe (pipe))
+ (let ((flags (fcntl (car child->parent-pipe) F_GETFL)))
+ (fcntl (car child->parent-pipe) F_SETFL (logior O_NONBLOCK flags)))
+
+ ;; Empty buffers to avoid duplicated output.
+ (flush-all-ports)
+
+ (match (primitive-fork)
+ (0 ;child
+ ;; Prepare the environment.
+
+ ;; Connect the stdout and stderr outputs of the child process to the
+ ;; pipe established in the parent.
+ (close (car child->parent-pipe)) ;unused input pipe
+ (dup2 (port->fdes (cdr child->parent-pipe)) 1)
+ (dup2 1 2)
+ (set-current-output-port (cdr child->parent-pipe))
+ (set-current-error-port (cdr child->parent-pipe))
+
+ ;; Use line buffering so the output is printed in "real time".
+ (setvbuf (current-output-port) 'line)
+ (setvbuf (current-error-port) 'line)
+
+ (setgid (passwd:gid (job:user job)))
+ (setuid (passwd:uid (job:user job)))
+ ;; Handle a nonexistent home directory, as can be the case when running
+ ;; the job as the "nobody" user.
+ (catch 'system-error
+ (lambda ()
+ (chdir (passwd:dir (job:user job))))
+ (lambda args
+ (let ((errno (system-error-errno args)))
+ (cond
+ ((= ENOENT errno) (chdir "/"))
+ (else (throw 'system-error args))))))
+ (modify-environment (job:environment job) (job:user job))
+
+ ;; Execute the action.
+ (catch #t
+ (lambda ()
+ (format #t "running...~%")
+ (flush-all-ports)
+ (let* ((result ((job:action job)))
+ (exit-val/maybe (false-if-exception
+ (status:exit-val result))))
+ (when (and exit-val/maybe
+ (not (= 0 exit-val/maybe)))
+ (error "unclean exit status" exit-val/maybe)))
+ (format #t "completed in ~,3fs~%" (seconds-since start))
+ (flush-all-ports)
+ (primitive-exit 0))
+ (lambda args
+ (format (current-error-port) "failed after ~,3fs with: ~a~%"
+ (seconds-since start) args)
+ (flush-all-ports)
+ (primitive-exit 1))))
+ (child-pid ;parent
+ (update-number-children! 1+)
+ (job:next-time-set! job ((job:next-time-function job)
+ (current-time)))
+ (close (cdr child->parent-pipe)) ;disconnect the write end of the pipe
+ (make-job-data
+ child-pid ;pid
+ (car child->parent-pipe) ;port
+ #f ;continuation for a suspended port
+ (job:displayable job))))) ;name
+
+(define* (process-output children-data)
+ "Read the child processes output from their input port recorded in
+CHILDREN-DATA and print an annotated version of each line to the standard
+output port. Ports are closed upon reading the EOF character. As a side
+effect, save the partial continuation of any suspended port to their
+associated <job-data> instance."
+ (define timestamp (srfi-19:date->string
+ (srfi-19:time-monotonic->date
+ (srfi-19:current-time srfi-19:time-monotonic))
+ (%date-format)))
+
+ ;; Use line buffering so the output is printed in "real time".
+ (setvbuf (current-output-port) 'line)
+
+ (define (log-data data)
+ ;; Print the lines as they become available. Do not block when a line
+ ;; could not be read.
+ (let ((name (job-data:name data))
+ (pid (job-data:pid data))
+ (port (job-data:port data)))
+
+ (define (read-line*)
+ ;; Return, as a pair, the line and the terminated delimiter or end-of-file
+ ;; object. When a line cannot be read, return the '(suspended
+ ;; . partial-continuation) pair, where partial-continuation can be
+ ;; evaluated in the future when the port is ready to be read.
+ (call-with-prompt 'continue
+ (lambda ()
+ (parameterize ((current-read-waiter
+ (lambda (_)
+ (abort-to-prompt 'continue))))
+ (let ((cont (job-data:continuation data)))
+ (if cont
+ (begin
+ (set-job-data-continuation! data #f) ;reset continuation
+ (cont))
+ ;; Also use the carriage return as a line delimiter to
+ ;; preserve the full output in a readable way.
+ (read-delimited "\n\r" port 'split)))))
+ (lambda (partial-continuation)
+ (cons 'suspended partial-continuation))))
+
+ (define (format-line line)
+ (format #t "~@?" (%log-format) timestamp pid name line))
+
+ (let loop ((line+delim (read-line*)))
+ (match line+delim
+ (('suspended . partial-continuation)
+ (set-job-data-continuation! data partial-continuation))
+ ((line . (? eof-object?))
+ (close port)
+ (unless (eof-object? line)
+ (format-line line)))
+ (("" . #\cr)
+ ;; A carriage return directly followed a delimiter. Ignore it.
+ (loop (read-line*)))
+ ((line . _)
+ (format-line line)
+ (loop (read-line*)))))))
+
+ (for-each log-data
+ (remove (compose port-closed? job-data:port)
+ children-data)))
+
+(define (child-cleanup children-data)
+ "Give any zombie children a chance to die, and decrease the number known to
+exist. CHILDREN-DATA is a list of <job-data> objects. Return the pruned list
+of CHILDREN-DATA."
+ (define has-children? (> (unbox number-children) 0))
+ (define collected-pid (or (and has-children?
+ (car (waitpid WAIT_ANY WNOHANG)))
+ 0))
+ (define (match-collected-pid? job-data)
+ (= (job-data:pid job-data) collected-pid))
+
+ (if (and has-children?
+ (not (= 0 collected-pid))) ;at least one process was collected
+ (begin
+ (update-number-children! 1-)
+ ;; Fully collect the output of the reaped child process.
+ (and=> (find match-collected-pid? children-data)
+ (lambda (child-data)
+ (process-output (list child-data))))
+ (child-cleanup (remove match-collected-pid? children-data)))
+ children-data))
+
+(define* (run-job-loop #:optional (fd-list '())
+ #:key (schedule %global-schedule))
+ "Loop over all job specifications in SCHEDULE, get a list of the next ones
+to run (may be more than one). Set an alarm and go to sleep. When we wake,
+run the jobs and reap any children (old jobs) that have completed. Repeat ad
+infinitum."
+ ;; Validate the format parameters, so that we can fail early.
+ (validate-date-format (%date-format))
+ (validate-log-format (%log-format))
+
;; Note that, if we wake ahead of time, it can only mean that a signal has
;; been sent by a crontab job to tell us to re-read a crontab file. In this
;; case we break out of the loop here, and let the main procedure deal with
@@ -236,23 +427,44 @@ next value."
(apply throw args)))))))
(let/ec break
- (let loop ()
+ (let loop ((children-data '())) ;list of <job-data> objects
(match (find-next-jobs #:schedule schedule)
((next-time . next-jobs-lst)
- (let ((sleep-time (if next-time
- (- next-time (current-time))
- 2000000000)))
+ (let* ((sleep-time (if next-time
+ (- next-time (current-time))
+ 2000000000))
+ (ports (map job-data:port children-data))
+ ;; Ensure closed ports are not put back into select, otherwise
+ ;; it would not block and EOFs would be read infinitely.
+ (children-fdes (filter-map (lambda (p)
+ (and (not (port-closed? p))
+ (port->fdes p)))
+ ports)))
(when (> sleep-time 0)
- (match (select* fd-list '() '() sleep-time)
+ (match (select* (append fd-list children-fdes)
+ '() '() sleep-time)
((() () ())
;; 'select' returned an empty set, perhaps because it got
- ;; EINTR or EAGAIN. It's a good time to wait for child
- ;; processes.
- (child-cleanup))
- (((lst ...) () ())
- ;; There's some activity so leave the loop.
- (break))))
-
- (for-each run-job next-jobs-lst)
- (child-cleanup)
- (loop)))))))
+ ;; EINTR or EAGAIN.
+ (loop (child-cleanup children-data)))
+ (((fdes ...) () ())
+ ;; Process any children input fdes ready to be read.
+ (let ((children-fdes/read-ready (lset-intersection
+ = children-fdes fdes)))
+ (when (not (null? (lset-difference
+ = fdes children-fdes/read-ready)))
+ ;; There was some crontab activity so leave the loop to
+ ;; process the update request.
+ (break))
+ (unless (null? children-fdes/read-ready)
+ (process-output
+ (filter (lambda (x)
+ (member (port->fdes (job-data:port x))
+ children-fdes/read-ready))
+ children-data))
+ (loop (child-cleanup children-data)))))))
+
+ ;; The timeout has elapsed. Run the scheduled job(s).
+ (let ((new-children-data (map run-job next-jobs-lst))
+ (pruned-children-data (child-cleanup children-data)))
+ (loop (append new-children-data pruned-children-data)))))))))
diff --git a/src/mcron/scripts/cron.scm b/src/mcron/scripts/cron.scm
index 25c8a1a..a0c9a68 100644
--- a/src/mcron/scripts/cron.scm
+++ b/src/mcron/scripts/cron.scm
@@ -1,6 +1,7 @@
;;;; cron -- daemon for running jobs at scheduled times
;;; Copyright © 2003, 2012 Dale Mellor <dale_mellor@users.sourceforge.net>
;;; Copyright © 2015, 2016, 2018 Mathieu Lirzin <mthl@gnu.org>
+;;; Copyright © 2021 Maxim Cournoyer <maxim.cournoyer@gmail.com>
;;;
;;; This file is part of GNU Mcron.
;;;
@@ -40,7 +41,9 @@ reading all the information in the users' crontabs and in /etc/crontab.
-h, --help Display this help message
-sN, --schedule[=]N Display the next N jobs that will be run by cron
-n, --noetc Do not check /etc/crontab for updates (HIGHLY
- RECOMMENDED).")
+ RECOMMENDED).
+ --log-format=FMT (ice-9 format) format string for log messages
+ --date-format=FMT (srfi srfi-19) date format string for log messages")
(newline)
(show-package-information))
@@ -49,6 +52,8 @@ reading all the information in the users' crontabs and in /etc/crontab.
(define %options `((schedule (single-char #\s) (value #t)
(predicate ,string->number))
(noetc (single-char #\n) (value #f))
+ (log-format (value #t) (predicate ,validate-log-format))
+ (date-format (value #t) (predicate ,validate-date-format))
(version (single-char #\v) (value #f))
(help (single-char #\h) (value #f))))
@@ -142,31 +147,34 @@ option.\n")
;;;
(define* (main #:optional (args (command-line)))
- (let ((opts (getopt-long args %options)))
- (when config-debug
- (debug-enable 'backtrace))
- (cond ((option-ref opts 'help #f)
- (show-help)
- (exit 0))
- ((option-ref opts 'version #f)
- (show-version "cron")
- (exit 0))
- ((not (zero? (getuid)))
- (mcron-error 16
- "This program must be run by the root user (and should"
- " have been installed as such)."))
- ((access? config-pid-file F_OK)
- (mcron-error 1
- "A cron daemon is already running.\n (If you are sure"
- " this is not true, remove the file\n "
- config-pid-file ".)"))
- (else
- (%process-files (option-ref opts 'schedule #f)
- (option-ref opts 'noetc #f))
- (cond ((option-ref opts 'schedule #f)
- => (λ (count)
- (display-schedule (max 1 (string->number count)))
- (exit 0)))))))
+
+ (define opts (getopt-long args %options))
+
+ (when config-debug
+ (debug-enable 'backtrace))
+
+ (cond ((option-ref opts 'help #f)
+ (show-help)
+ (exit 0))
+ ((option-ref opts 'version #f)
+ (show-version "cron")
+ (exit 0))
+ ((not (zero? (getuid)))
+ (mcron-error 16
+ "This program must be run by the root user (and should"
+ " have been installed as such)."))
+ ((access? config-pid-file F_OK)
+ (mcron-error 1
+ "A cron daemon is already running.\n (If you are sure"
+ " this is not true, remove the file\n "
+ config-pid-file ".)"))
+ (else
+ (%process-files (option-ref opts 'schedule #f)
+ (option-ref opts 'noetc #f))
+ (cond ((option-ref opts 'schedule #f)
+ => (λ (count)
+ (display-schedule (max 1 (string->number count)))
+ (exit 0))))))
;; Daemonize ourself.
(unless (eq? 0 (primitive-fork)) (exit 0))
@@ -187,11 +195,12 @@ option.\n")
(with-output-to-file config-pid-file
(λ () (display (getpid)) (newline)))
- ;; Forever execute the 'run-job-loop', and when it drops out (can
- ;; only be because a message has come in on the socket) we
- ;; process the socket request before restarting the loop again.
- (catch-mcron-error
- (let ((fdes-list (cron-file-descriptors)))
- (while #t
- (run-job-loop fdes-list)
- (unless (null? fdes-list) (process-update-request fdes-list))))))
+ (parameterize ((%log-format (option-ref opts 'log-format (%log-format)))
+ (%date-format (option-ref opts 'date-format (%date-format))))
+ ;; Forever execute the 'run-job-loop', and when it drops out (can
+ ;; only be because a message has come in on the socket) we
+ ;; process the socket request before restarting the loop again.
+ (let ((fdes-list (cron-file-descriptors)))
+ (while #t
+ (run-job-loop fdes-list)
+ (unless (null? fdes-list) (process-update-request fdes-list))))))
diff --git a/src/mcron/scripts/mcron.scm b/src/mcron/scripts/mcron.scm
index a6b1fa2..300aad0 100644
--- a/src/mcron/scripts/mcron.scm
+++ b/src/mcron/scripts/mcron.scm
@@ -1,6 +1,7 @@
;;;; mcron -- run jobs at scheduled times
;;; Copyright © 2003, 2012 Dale Mellor <dale_mellor@users.sourceforge.net>
;;; Copyright © 2015, 2016, 2018 Mathieu Lirzin <mthl@gnu.org>
+;;; Copyright © 2021 Maxim Cournoyer <maxim.cournoyer@gmail.com>
;;;
;;; This file is part of GNU Mcron.
;;;
@@ -41,6 +42,8 @@ standard input), or use all the files in ~/.config/cron (or the deprecated
-i, --stdin=(guile|vixie) Format of data passed as standard input
(default guile)
-s, --schedule[=N] Display the next N (or 8) jobs that will be run
+ --log-format=FMT (ice-9 format) format string for log messages
+ --date-format=FMT (srfi srfi-19) date format string for log messages
-?, --help Give this help list
-V, --version Print program version
@@ -119,6 +122,8 @@ directory. Double-check the folder and file permissions and syntax."))))
(string=? in "vixie")))))
(schedule (single-char #\s) (value optional)
(predicate ,string->number))
+ (log-format (value #t) (predicate ,validate-log-format))
+ (date-format (value #t) (predicate ,validate-date-format))
(help (single-char #\?))
(version (single-char #\V))))))
@@ -141,14 +146,7 @@ directory. Double-check the folder and file permissions and syntax."))))
((0) (setsid))
(else (exit 0)))))
- ;; Forever execute the 'run-job-loop', and when it drops out (can only be
- ;; because a message has come in on the socket) we process the socket
- ;; request before restarting the loop again.
- (catch-mcron-error
- (let ((fdes-list '()))
- (while #t
- (run-job-loop fdes-list)
- ;; we can also drop out of run-job-loop because of a SIGCHLD,
- ;; so must test FDES-LIST.
- (unless (null? fdes-list)
- (process-update-request fdes-list)))))))
+ (parameterize
+ ((%log-format (option-ref options 'log-format (%log-format)))
+ (%date-format (option-ref options 'date-format (%date-format))))
+ (run-job-loop))))
diff --git a/tests/base.scm b/tests/base.scm
index eb9e11a..914b4c6 100644
--- a/tests/base.scm
+++ b/tests/base.scm
@@ -1,5 +1,6 @@
;;;; base.scm -- tests for (mcron base) module
;;; Copyright © 2018 Mathieu Lirzin <mthl@gnu.org>
+;;; Copyright © 2021 Maxim Cournoyer <maxim.cournoyer@gmail.com>
;;;
;;; This file is part of GNU Mcron.
;;;
@@ -16,7 +17,8 @@
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Mcron. If not, see <http://www.gnu.org/licenses/>.
-(use-modules (srfi srfi-64)
+(use-modules ((rnrs base) #:select (assert))
+ (srfi srfi-64)
(srfi srfi-111)
(mcron base))
@@ -40,7 +42,7 @@
#:key
(user (getpw))
(time-proc 1+)
- (action (λ () "dummy action"))
+ (action (lambda () "dummy action"))
(environment '())
(next-time 0))
(make-job user time-proc action environment displayable next-time))
@@ -191,25 +193,101 @@
;;; Check 'run-job' and 'child-cleanup'.
;;; XXX: Having to use the filesystem for a unit test is wrong.
(let* ((filename (tmpnam))
- (action (λ () (close-port (open-output-file filename))))
+ (action (lambda () (close-port (open-output-file filename))))
(job (make-dummy-job #:user (getpw (getuid)) #:action action)))
(dynamic-wind
(const #t)
- (λ ()
+ (lambda ()
(sigaction SIGCHLD (const #t))
- (run-job job)
- ;; Wait for the SIGCHLD signal sent when job exits.
- (pause)
- ;; Check 'run-job' result and if the number of children is up-to-date.
- (test-equal "run-job: basic"
- 1
- (and (access? filename F_OK)
- (unbox number-children)))
- (child-cleanup)
+ (let ((child-data (run-job job)))
+ ;; Wait for the SIGCHLD signal sent when job exits.
+ (pause)
+ ;; Check 'run-job' result and if the number of children is up-to-date.
+ (test-equal "run-job: basic"
+ 1
+ (and (access? filename F_OK)
+ (unbox number-children)))
+ (child-cleanup (list child-data)))
;; Check that 'child-cleanup' updates the number of children.
(test-equal "child-cleanup: one" 0 (unbox number-children)))
- (λ ()
+ (lambda ()
(and (access? filename F_OK) (delete-file filename))
(sigaction SIGCHLD SIG_DFL))))
+(define (dummy-job/capture-output action)
+ "Return the output of a dummy-job that ran ACTION."
+ (with-output-to-string
+ (lambda ()
+ (dynamic-wind
+ (const #t)
+ (lambda ()
+ (sigaction SIGCHLD (const #t))
+ (let ((child-data
+ (run-job
+ (make-dummy-job
+ #:user (getpw (getuid))
+ #:action action))))
+ (pause)
+ (child-cleanup (list child-data))))
+ (lambda ()
+ #t
+ (sigaction SIGCHLD SIG_DFL))))))
+
+(test-assert "run-job, output"
+ (let ((output (dummy-job/capture-output
+ (lambda ()
+ (format #t "output line 1~%")
+ (format #t "output line 2\nand 3~%")
+ (system "echo poutine")
+ (format (current-error-port)
+ "some error~%")))))
+ (assert (string-contains output "dummy: running"))
+ (assert (string-contains output "dummy: output line 1"))
+ (assert (string-contains output "dummy: and 3"))
+ (assert (string-contains output "dummy: poutine"))
+ (assert (string-contains output "dummy: some error"))
+ (assert (string-contains output "dummy: completed in"))))
+
+(test-assert "validate-date-format, valid"
+ (validate-date-format "~1"))
+
+(test-assert "validate-date-format, invalid"
+ (catch 'mcron-error
+ (lambda ()
+ (validate-date-format "~¾")
+ #f)
+ (const #t)))
+
+(test-assert "validate-log-format, valid"
+ (validate-log-format "the message only: ~3@*~a~%"))
+
+(test-assert "validate-log-format, invalid"
+ (catch 'mcron-error
+ (lambda ()
+ ;; There aren't that many arguments!
+ (validate-log-format "~20@*~a~%")
+ #f)
+ (const #t)))
+
+(test-assert "run-job, output with custom format"
+ (let ((output (parameterize ((%log-format "the message only: ~3@*~a~%"))
+ (dummy-job/capture-output
+ (lambda ()
+ (format #t "output line 1~%"))))))
+ (string-contains output "the message only: output line 1\n")))
+
+(test-assert "run-job, failure"
+ (let ((output (dummy-job/capture-output
+ (lambda ()
+ (error "that didn't go well")))))
+ (assert (string-contains output "that didn't go well"))
+ (assert (string-contains output "failed after"))))
+
+(test-assert "run-job, failure in shell action"
+ (let ((output (dummy-job/capture-output
+ (lambda ()
+ (system "exit 1")))))
+ (assert (string-contains output "unclean exit status"))
+ (assert (string-contains output "failed after"))))
+
(test-end)
diff --git a/tests/basic.sh b/tests/basic.sh
index 7b2ca55..79b2032 100644
--- a/tests/basic.sh
+++ b/tests/basic.sh
@@ -1,5 +1,6 @@
# basic.sh -- basic tests for mcron
# Copyright © 2017 Mathieu Lirzin <mthl@gnu.org>
+# Copyright © 2021 Maxim Cournoyer <maxim.cournoyer@gmail.com>
#
# This file is part of GNU Mcron.
#
@@ -33,4 +34,16 @@ grep -e "foo" "output$$" || fail_ "'foo.guile' job is not scheduled"
mcron --schedule=1 > "output$$"
grep -e "foo" "output$$" || fail_ "'foo.guile' job is not scheduled"
+mcron --date-format="~½" cron/foo.guile \
+ && fail_ "mcron --date-format unexpected pass"
+
+mcron --log-format="~½" cron/foo.guile \
+ && fail_ "mcron --log-format unexpected pass"
+
+cron --date-format="~½" cron/foo.guile \
+ && fail_ "cron --date-format unexpected pass"
+
+cron --log-format="~½" cron/foo.guile \
+ && fail_ "cron --log-format unexpected pass"
+
Exit 0