From 3725aedd02d9b24c1239610066dd62387ef4d5a9 Mon Sep 17 00:00:00 2001 From: dale_mellor Date: Fri, 4 Jul 2003 08:09:04 +0000 Subject: Initial version. Production quality, fully complete source but contains known bugs (lots!) --- vixie.scm | 452 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 452 insertions(+) create mode 100644 vixie.scm (limited to 'vixie.scm') diff --git a/vixie.scm b/vixie.scm new file mode 100644 index 0000000..2e7df93 --- /dev/null +++ b/vixie.scm @@ -0,0 +1,452 @@ +;; Copyright (C) 2003 Dale Mellor +;; +;; This program 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 2, or (at your option) +;; any later version. +;; +;; This program 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 this program; if not, write to the Free Software +;; Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +;; USA. + + + +;; This file provides methods for reading a complete Vixie-style configuration +;; file, either from a real file or an already opened port. It also exposes the +;; method for parsing the time-specification part of a Vixie string, so that +;; these can be used to form the next-time-function of a job in a Guile +;; configuration file. + + + +(use-modules (ice-9 regex) (ice-9 rdelim) (srfi srfi-13) (srfi srfi-14)) + + + +;; In Vixie-style time specifications three-letter symbols are allowed to stand +;; for the numbers corresponding to months and days of the week. We deal with +;; this by making a textual substitution early on in the processing of the +;; strings. +;; +;; We start by defining, once and for all, a list of cons cells consisting of +;; regexps which will match the symbols - which allow an arbitrary number of +;; other letters to appear after them (so that the user can optionally complete +;; the month and day names; this is an extension of Vixie) - and the value which +;; is to replace the symbol. +;; +;; The procedure then takes a string, and then for each symbol in the +;; parse-symbols list attempts to locate an instance and replace it with an +;; ASCII representation of the value it stands for. The procedure returns the +;; modified string. (Note that each symbol can appear only once, which meets the +;; Vixie specifications technically but still allows silly users to mess things +;; up). + +(define parse-symbols + (map (lambda (symbol-cell) + (cons (make-regexp (string-append (car symbol-cell) "[[:alpha:]]*") + regexp/icase) + (cdr symbol-cell))) + '(("jan" . "0") ("feb" . "1") ("mar" . "2") ("apr" . "3") + ("may" . "4") ("jun" . "5") ("jul" . "6") ("aug" . "7") + ("sep" . "8") ("oct" . "9") ("nov" . "10") ("dec" . "11") + + ("sun" . "0") ("mon" . "1") ("tue" . "2") ("wed" . "3") + ("thu" . "4") ("fri" . "5") ("sat" . "6") ))) + +(define (vixie-substitute-parse-symbols string) + (for-each (lambda (symbol-cell) + (let ((match (regexp-exec (car symbol-cell) string))) + (if match + (set! string (string-append (match:prefix match) + (cdr symbol-cell) + (match:suffix match)))))) + parse-symbols) + string) + + + +;; A Vixie time specification is made up of a space-separated list of elements, +;; and the elements consist of a comma-separated list of subelements. The +;; procedure below takes a string holding a subelement, which should have no +;; spaces or symbols (see above) in it, and returns a list of all values which +;; that subelement indicates. There are five distinct cases which must be dealt +;; with: [1] a single '*' which returns a list of all values; [2] a '*' followed +;; by a step specifier; [3] a range and step specifier; [4] a range; and [5] a +;; single number. +;; +;; To perform the computation required for the '*' cases, we need to pass the +;; limit of the allowable range for this subelement as the third argument. As +;; days of the month start at 1 while all the other time components start at 0, +;; we must pass the base of the range to deal with this case also. + +(define parse-vixie-subelement-regexp + (make-regexp "^([[:digit:]]+)(-([[:digit:]]+)(/([[:digit:]]+))?)?$")) + +(define (parse-vixie-subelement string base limit) + (if (char=? (string-ref string 0) #\*) + (range base limit (if (> (string-length string) 1) + (string->number (substring string 2)) ;; [2] + 1)) ;; [1] + (let ((match (regexp-exec parse-vixie-subelement-regexp string))) + (cond ((not match) + (display "Error: Bad Vixie-style time specification.\n") + (primitive-exit 9)) + ((match:substring match 5) + (range (string->number (match:substring match 1)) + (+ 1 (string->number (match:substring match 3))) + (string->number (match:substring match 5)))) ;; [3] + ((match:substring match 3) + (range (string->number (match:substring match 1)) + (+ 1 (string->number (match:substring match 3))))) ;; [4] + (else + (list (string->number (match:substring match 1)))))))) ;; [5] + + + +;; A Vixie element contains the entire specification, without spaces or symbols, +;; of the acceptable values for one of the time components (minutes, hours, +;; days, months, week days). Here we break the comma-separated list into +;; subelements, and process each with the procedure above. The return value is a +;; list of all the valid values of all the subcomponents. +;; +;; The second and third arguments are the base and upper limit on the values +;; that can be accepted for this time element. +;; +;; The effect of the 'apply append' is to merge a list of lists into a single +;; list. + +(define (parse-vixie-element string base limit) + (apply append + (map (lambda (sub-element) + (parse-vixie-subelement sub-element base limit)) + (string-tokenize string (char-set-complement (char-set #\,)))))) + + + +;; Consider there are two lists, one of days in the month, the other of days in +;; the week. This procedure returns an augmented list of days in the month with +;; weekdays accounted for. + +(define (interpolate-weekdays mday-list wday-list month year) + (let ((t (localtime 0))) + (set-tm:mday t 1) + (set-tm:mon t month) + (set-tm:year t year) + (let ((first-day (tm:wday (cdr (mktime t))))) + (apply append + mday-list + (map (lambda (wday) + (let ((first (- wday first-day))) + (if (< first 0) (set! first (+ first 7))) + (range (+ 1 first) 32 7))) + wday-list))))) + + + +;; Return the number of days in a month. Fix up a tm object for the zero'th day +;; of the next month, rationalize the object and extract the day. + +(define (days-in-month month year) + (let ((t (localtime 0))) (set-tm:mday t 0) + (set-tm:mon t (+ month 1)) + (set-tm:year t year) + (tm:mday (cdr (mktime t))))) + + + +;; We will be working with a list of time-spec's, one for each element of a time +;; specification (minute, hour, ...). Each time-spec holds three pieces of +;; information: a list of acceptable values for this time component, a procedure +;; to get the component from a tm object, and a procedure to set the component +;; in a tm object. + +(define (time-spec:list time-spec) (vector-ref time-spec 0)) +(define (time-spec:getter time-spec) (vector-ref time-spec 1)) +(define (time-spec:setter time-spec) (vector-ref time-spec 2)) + + + +;; This procedure modifies the time tm object by setting the component referred +;; to by the time-spec object to its next acceptable value. If this value is not +;; greater than the original (because we have wrapped around the top of the +;; acceptable values list), then the function returns #t, otherwise it returns +;; #f. Thus, if the return value is true then it will be necessary for the +;; caller to increment the next coarser time component as well. +;; +;; The first part of the let block is a concession to humanity; the procedure is +;; simply unreadable without all of these aliases. + +(define (increment-time-component time time-spec) + (let* ((time-list (time-spec:list time-spec)) + (getter (time-spec:getter time-spec)) + (setter (time-spec:setter time-spec)) + (next-best (find-best-next (getter time) time-list)) + (wrap-around (eqv? (cdr next-best) 9999))) + (setter time ((if wrap-around car cdr) next-best)) + wrap-around)) + + + +;; There now follows a set of procedures for adjusting an element of time, +;; i.e. taking it to the next acceptable value. In each case, the head of the +;; time-spec-list is expected to correspond to the component of time in +;; question. If the adjusted value wraps around its allowed range, then the next +;; biggest element of time must be adjusted, and so on. + +;; There is no specification allowed for the year component of +;; time. Therefore, if we have to make an adjustment (presumably because a +;; monthly adjustment has wrapped around the top of its range) we can simply +;; go to the next year. + +(define (nudge-year! time) + (set-tm:year time (+ (tm:year time) 1))) + + +;; We nudge the month by finding the next allowable value, and if it wraps +;; around we also nudge the year. The time-spec-list will have time-spec +;; objects for month and weekday. + +(define (nudge-month! time time-spec-list) + (and (increment-time-component time (car time-spec-list)) + (nudge-year! time))) + + +;; Try to increment the day component of the time according to the combination +;; of the mday-list and the wday-list. If this wraps around the range, or if +;; this falls outside the current month (31st February, for example), then +;; bump the month, set the day to zero, and recurse on this procedure to find +;; the next day in the new month. +;; +;; The time-spec-list will have time-spec entries for mday, month, and +;; weekday. + +(define (nudge-day! time time-spec-list) + (if (or (increment-time-component + time + (vector + (interpolate-weekdays (time-spec:list (car time-spec-list)) + (time-spec:list (caddr time-spec-list)) + (tm:mon time) + (tm:year time)) + tm:mday + set-tm:mday)) + (> (tm:mday time) (days-in-month (tm:mon time) (tm:year time)))) + (begin + (nudge-month! time (cdr time-spec-list)) + (set-tm:mday time 0) + (nudge-day! time time-spec-list)))) + + + +;; The hour is bumped to the next accceptable value, and the day is bumped if +;; the hour wraps around. +;; +;; The time-spec-list holds specifications for hour, mday, month and weekday. + +(define (nudge-hour! time time-spec-list) + (and (increment-time-component time (car time-spec-list)) + (nudge-day! time (cdr time-spec-list)))) + + + +;; The minute is bumped to the next accceptable value, and the hour is bumped +;; if the minute wraps around. +;; +;; The time-spec-list holds specifications for minute, hour, day-date, month +;; and weekday. + +(define (nudge-min! time time-spec-list) + (and (increment-time-component time (car time-spec-list)) + (nudge-hour! time (cdr time-spec-list)))) + + + + +;; This is a procedure which returns a procedure which computes the next time a +;; command should run after the current time, based on the information in the +;; Vixie-style time specification. +;; +;; We start by computing a list of time-spec objects (described above) for the +;; minute, hour, date, month, year and weekday components of the overall time +;; specification [1]. When we create the return procedure, it is this list to +;; which references to a time-spec-list will be bound. It will be used by the +;; returned procedure [3] to compute the next time a function should run. Any +;; 7's in the weekday component of the list (the last one) are folded into 0's +;; (both values represent sunday) [2]. +;; +;; The returned procedure itself:- +;; +;; Starts by obtaining the current broken-down time [4], and fixing it to +;; ensure that it is an acceptable value, as follows. Each component from the +;; biggest down is checked for acceptability, and if it is not acceptable it +;; is bumped to the next acceptable value (this may cause higher components to +;; also be bumped if there is range wrap-around) and all the lower components +;; are set to -1 so that it can successfully be bumped up to zero if this is +;; an allowed value. The -1 value will be bumped up subsequently to an allowed +;; value [5]. +;; +;; Once it has been asserted that the current time is acceptable, or has been +;; adjusted to one minute before the next acceptable time, the minute +;; component is then bumped to the next acceptable time, which may ripple +;; through the higher components if necessary [6]. We now have the next time +;; the command needs to run. +;; +;; The new time is then converted back into a UNIX time, and returned [7]. + +(define (parse-vixie-time string) + (let* ((tokens (string-tokenize (vixie-substitute-parse-symbols string))) + (time-spec-list + (map-in-order (lambda (x) (vector (parse-vixie-element + (list-ref tokens (vector-ref x 0)) + (vector-ref x 1) + (vector-ref x 2)) + (vector-ref x 3) + (vector-ref x 4))) + ;; token range-top+1 getter setter + `( #( 0 0 60 ,tm:min ,set-tm:min ) + #( 1 0 24 ,tm:hour ,set-tm:hour ) + #( 2 1 32 ,tm:mday ,set-tm:mday ) + #( 3 0 12 ,tm:mon ,set-tm:mon ) + #( 4 0 7 ,tm:wday ,set-tm:wday ))))) ;; [1] + + (vector-set! (car (last-pair time-spec-list)) + 0 + (map (lambda (time-spec) + (if (eqv? time-spec 7) 0 time-spec)) + (vector-ref (car (last-pair time-spec-list)) 0))) ;; [2] + + (lambda (current-time) ;; [3] + (let ((time (localtime current-time))) ;; [4] + + (if (not (member (tm:mon time) + (time-spec:list (cadddr time-spec-list)))) + (begin + (nudge-month! time (cdddr time-spec-list)) + (set-tm:mday time 0) + (set-tm:hour time -1) + (set-tm:min time -1))) + (if (not (member (tm:mday time) ;; !! + (time-spec:list (caddr time-spec-list)))) + (begin + (nudge-day! time (cddr time-spec-list)) + (set-tm:hour time -1) + (set-tm:min time -1))) + (if (not (member (tm:hour time) + (time-spec:list (cadr time-spec-list)))) + (begin + (nudge-hour! time (cdr time-spec-list)) + (set-tm:min time -1))) ;; [5] + + (set-tm:sec time 0) + (nudge-min! time time-spec-list) ;; [6] + + (car (mktime time)))))) ;; [7] + + + + +;; A line in a Vixie-style crontab file which gives a command specification +;; carries two pieces of information: a time specification consisting of five +;; space-separated items, and a command which is also separated from the time +;; specification by a space. The line is broken into the two components, and the +;; job procedure run to add the two pieces of information to the job list (this +;; will in turn use the above function to turn the time specification into a +;; function for computing future run times of the command). + +(define parse-user-vixie-line-regexp + (make-regexp "^[[:space:]]*(([^[:space:]]+[[:space:]]+){5})(.*)$")) + +(define (parse-user-vixie-line line) + (let ((match (regexp-exec parse-user-vixie-line-regexp line))) + (if (not match) (begin (display "Bad job line in Vixie file.\n") + (primitive-exit 10))) + (job (match:substring match 1) + (lambda () (with-mail-out (match:substring match 3)))))) + + + +;; The case of reading a line from /etc/crontab is similar to above but the user +;; ID appears in the sixth field, before the action. + +(define parse-system-vixie-line-regexp + (make-regexp (string-append "^[[:space:]]*(([^[:space:]]+[[:space:]]+){5})" + "([[:alpha:]][[:alnum:]_]*)[[:space:]]+(.*)$"))) + +(define (parse-system-vixie-line line) + (let ((match (regexp-exec parse-user-vixie-line-regexp line))) + (if (not match) (begin (display "Bad job line in /etc/crontab.\n") + (primitive-exit 11))) + (set! configuration-user (passwd (match:substring match 3))) + (job (match:substring match 1) + (lambda () (with-mail-out (match:substring match 4) + (passwd:name configuration-user)))))) + + + + +;; The next procedure reads an entire Vixie-style file. For each line in the +;; file there are three possibilities (after continuation lines have been +;; appended): the line is blank or contains only a comment, the line contains an +;; environment modifier which will be handled in environment.scm, or the line +;; contains a command specification in which case we use the procedure above to +;; add an entry to the internal job list. +;; +;; Note that the environment modifications are cleared, so that there is no +;; interference between crontab files (this might lead to unpredictable +;; behaviour because the order in which crontab files are processed, if there is +;; more than one, is generally undefined). + +(define read-vixie-file-comment-regexp + (make-regexp "^[[:space:]]*(#.*)?$")) + + +(define (read-vixie-port port . parse-vixie-line) + (clear-environment-mods) + (if port + (let ((parse-vixie-line + (if (null? parse-vixie-line) parse-user-vixie-line + (car parse-vixie-line)))) + (do ((line (read-line port) (read-line port))) + ((eof-object? line)) + + ;; If the line ends with \, append the next line. + (do () + ((or (< (string-length line) 1) + (not (char=? (string-ref line + (- (string-length line) 1)) + #\\)))) + (let ((next-line (read-line port))) + (if (eof-object? next-line) + (set! next-line "")) + (set! line + (string-append + (substring line 0 (- (string-length line) 1)) + next-line)))) + + ;; Consider the three cases mentioned in the description. + (or (regexp-exec read-vixie-file-comment-regexp line) + (parse-vixie-environment line) + (parse-vixie-line line)))))) + + + +;; If a file cannot be opened, we must silently ignore it because it may have +;; been removed by crontab. However, if the file is there it must be parseable, +;; otherwise the error must be propagated to the caller. + +(define (read-vixie-file file-path . parse-vixie-line) + (let ((port #f)) + (catch #t (lambda () (set! port (open-input-file file-path))) + (lambda (key . args) (set! port #f))) + (if port + (begin + (if (null? parse-vixie-line) + (read-vixie-port port) + (read-vixie-port port (car parse-vixie-line))) + (close port))))) -- cgit v1.2.3