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!) --- AUTHORS | 1 + BUGS | 13 + ChangeLog | 30 ++ NEWS | 24 ++ README | 50 +++ TODO | 43 +++ config.scm.in | 27 ++ configure.ac | 82 ++++ crontab.scm | 199 ++++++++++ email.scm | 182 +++++++++ environment.scm | 121 ++++++ makefile.am | 61 +++ makefile.ed | 42 +++ mcron.c.template | 124 +++++++ mcron.scm | 846 +++++++++++++++++++++++++++++++++++++++++ mcron.texinfo | 1094 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ vixie.scm | 452 ++++++++++++++++++++++ 17 files changed, 3391 insertions(+) create mode 100644 AUTHORS create mode 100644 BUGS create mode 100644 ChangeLog create mode 100644 NEWS create mode 100644 README create mode 100644 TODO create mode 100644 config.scm.in create mode 100644 configure.ac create mode 100644 crontab.scm create mode 100644 email.scm create mode 100644 environment.scm create mode 100644 makefile.am create mode 100644 makefile.ed create mode 100644 mcron.c.template create mode 100644 mcron.scm create mode 100644 mcron.texinfo create mode 100644 vixie.scm diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..da27b1c --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +Dale Mellor (dale_mellor@users.sourceforge.net) diff --git a/BUGS b/BUGS new file mode 100644 index 0000000..9eabf92 --- /dev/null +++ b/BUGS @@ -0,0 +1,13 @@ + -*-text-*- + +* If two users modify their crontabs simultaneously, there will be contention + for /var/cron/update between themselves and with the main daemon. + +* Daylight savings time shifts are not taken into account very well. If things + are critical, your best bet is to set your TZ environment variable to + `:Universal', and express all your configuration files in Universal + Coordinated Time (UTC). + + +* As often as not the cron daemon crashes (segfaults) when crontab sends it a + SIGHUP. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..ae6a219 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,30 @@ +2003-06-30 hydro23 > + + * mcron.scm: Introduced arbiters to marshall access to updates + structure between main routing and HUP signal action procedure. + + * crontab.scm: When an empty /tmp file is produced for editing, + make it owned by the real user (so he can edit it). + + * mcron.scm, makefile.am: Check explicitly for root user when + running cron personality. Install with only root execute + permission. + + * mcron.scm: Don't create /var/run/cron.pid if the -s option has + been used (this is an undocumented possibility). + + * configure.ac, config.scm.in: Added configure option + --enable-debug to determine whether debugging and traceback should + be turned on. + + * Version bumped to 0.99.2. + + +2003-06-28 Dale Mellor + + * First cut, fully functional, production quality code, just needs + testing... + + * Version set at 0.99.1 + + diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..5050482 --- /dev/null +++ b/NEWS @@ -0,0 +1,24 @@ +Historic moments in the life of mcron. + +Copyright (C) 1992, 1993, 1995-2002 Free Software Foundation, Inc. +See the end for copying conditions. + +Please send bug reports to dale_mellor@users.sourceforge.net. + + + + +---------------------------------------------------------------------- +Copyright information: + +Copyright (C) 2003 Dale Mellor + + Permission is granted to anyone to make or distribute verbatim copies + of this document as received, in any medium, provided that the + copyright notice and this permission notice are preserved, + thus giving the recipient permission to redistribute in turn. + + Permission is granted to distribute modified versions + of this document, or of portions of it, + under the above conditions, provided also that they + carry prominent notices stating who last changed them. diff --git a/README b/README new file mode 100644 index 0000000..06ccae1 --- /dev/null +++ b/README @@ -0,0 +1,50 @@ +This is version 0.99.1 of the mcron program, designed and written by Dale +Mellor, which replaces and hugely enhances Vixie cron. It is functionally +complete, production quality code (did you expect less?), but has not received +much testing yet and contains known bugs. It has only been built on a GNU/Linux +system, and will most likely fail on others (but you never know...). + + +---------------------------------------------------------------------- +IMPORTANT NOTICES + +Read the BUGS file. + +Do not (yet) install this software on a machine which relies for its functioning +on its current set of crontabs. + +The package must be installed by root. + +Before installing this package for the first time, it is necessary to terminate +any running cron daemons on your system. If your old cron is not accurately +Vixie compatible (files in /var/cron/tabs*, /var/cron/allow, /var/cron/deny, +/etc/crontab, /var/run/cron.pid) then you will need to clear out all old +crontabs and make new ones afresh. + +If your old cron is Vixie, or very similar, mcron should fall right into place +where your old cron was (the binaries cron and crontab will be replaced), and +you should be able to continue to use your existing crontabs without noticing +any changes. Bear in mind that if you use /etc/crontab, then changes to this +file will *not* take immediate effect (this is the 1% incompatibility between +mcron and Vixie cron); you may want to add a comment to this file with a note to +this effect. Alternatively, use the new mcron program, it's better! + +If you don't want to clobber your existing cron executables, you can specify the +--program-prefix option to configure with a prefix ending in a non-alphabetic +character, for example "m.", and then run the programs as m.mcron, m.cron and +m.crontab. +---------------------------------------------------------------------- + + +See the file INSTALL for building and installation instructions. + +After installation, read the info file for full instructions for use (type +`info mcron' at the command line). + +Known bugs are noted in the BUGS file, and features which might be implemented +sometime sooner or later are noted in the TODO file. + +Please send all other bug reports by electronic mail to: + dale_mellor@users.sourceforge.net + +Mcron is free software. See the file COPYING for copying conditions. diff --git a/TODO b/TODO new file mode 100644 index 0000000..5f61fcf --- /dev/null +++ b/TODO @@ -0,0 +1,43 @@ +Maybe in the near future... + + * Logging. + + * Check POSIX compliance. + + + +There are no plans to actually do the following any time soon... + + * Develop at, batch modes of operation. + + * Make compatibilities with other crons (BSD, SYSV, Solaris, Dillon's, ...) + + * Port to BSD, other operating systems. + + * Full security audit for Vixie mode. + + * Move internal functions into a namespace such that configuration files + cannot interfere with mcron itself. + + + +Quite likely to happen if version 2.0 ever materializes... + + * Split program into Vixie and mcron separates (should streamline mcron + code by a factor of three; removes need for security audit). + + * UNIX or TCP socket will allow interrogation and control of a running + daemon (should be more reliable, efficient and useful than using the + SIGHUP-/var/cron/update method). + + + +May happen if version 2.0 ever materializes... + + * Add anacron functionality (run missed jobs if the daemon is stopped, for + example if a personal computer does not run 24 hours a day). + + * TCP socket to allow control via HTTP (web browser interface). Or maybe + just CGI personality. + + * GTK+/Bononbo interface. diff --git a/config.scm.in b/config.scm.in new file mode 100644 index 0000000..f31ac1e --- /dev/null +++ b/config.scm.in @@ -0,0 +1,27 @@ +;; -*-scheme-*- + +;; 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. + + +;; Some constants set by the configuration process. + +(define config-debug @CONFIG_DEBUG@) +(define config-package-string "@PACKAGE_STRING@") +(define config-package-bugreport "@PACKAGE_BUGREPORT@") +(define config-sendmail "@SENDMAIL@") +(define config-cat "@CAT@") diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..f56b111 --- /dev/null +++ b/configure.ac @@ -0,0 +1,82 @@ +# -*- Autoconf -*- +# Process this file with autoconf to produce a configure script. + +AC_PREREQ(2.57) +AC_INIT(mcron, 0.99.1, dale_mellor@users.sourceforge.net) +AM_INIT_AUTOMAKE + + +AC_MSG_CHECKING([whether debugging is requested]) +AC_ARG_ENABLE(debug, + AC_HELP_STRING([--enable-debug], + [enable debugging and traceback on error]), + CONFIG_DEBUG=$enableval, + CONFIG_DEBUG=no) +AC_MSG_RESULT($CONFIG_DEBUG) +if test "$CONFIG_DEBUG" = "no"; then + CONFIG_DEBUG="#f" +else + CONFIG_DEBUG="#t" +fi +AC_SUBST(CONFIG_DEBUG) + + +AC_PROG_CC +GUILE_PROGS +GUILE_FLAGS + +# Checks for programs. + # AC_CHECK_PROG(CHMOD, chmod, chmod) +AC_CHECK_PROGS(CHMOD, chmod) +if test "x$ac_cv_prog_CHMOD" = "x"; then + AC_MSG_ERROR(chmod not found) +fi +AC_CHECK_PROGS(ED, ed) +if test "x$ac_cv_prog_ED" = "x"; then + AC_MSG_ERROR(ed not found) +fi +AC_CHECK_PROGS(CAT, cat) +if test "x$ac_cv_prog_CAT" = "x"; then + AC_MSG_ERROR(cat not found) +fi +AC_CHECK_PROGS(WHICH, which) +if test "x$ac_cv_prog_WHICH" = "x"; then + AC_MSG_ERROR(which not found) +fi + +# Now find a sendmail or equivalent. + +AC_CHECK_PROGS(SENDMAIL, sendmail) +if test "x$ac_cv_prog_SENDMAIL" != "x"; then + AC_MSG_CHECKING(sendmail path and arguments) + ac_cv_prog_SENDMAIL="`$ac_cv_prog_WHICH sendmail` -FCronDaemon -odi -oem " +dnl -or0s" + AC_MSG_RESULT($ac_cv_prog_SENDMAIL) + +else + AC_CHECK_PROGS(SENDMAIL, mail) + if test "x$ac_cv_prog_SENDMAIL" != "x"; then + AC_MSG_CHECKING(mail path) + ac_cv_prog_SENDMAIL="`$ac_cv_prog_WHICH sendmail` -d " + AC_MSG_RESULT($ac_cv_prog_SENDMAIL) + else + AC_MSG_RESULT(No mail program found) + fi +fi +SENDMAIL=$ac_cv_prog_SENDMAIL + + +# Checks for libraries. + +# Checks for header files. + +# Checks for typedefs, structures, and compiler characteristics. + +# Checks for library functions. + +real_program_prefix=`echo $program_prefix | sed s/NONE//` +AC_SUBST(real_program_prefix) + + +AC_CONFIG_FILES(makefile config.scm) +AC_OUTPUT diff --git a/crontab.scm b/crontab.scm new file mode 100644 index 0000000..417a900 --- /dev/null +++ b/crontab.scm @@ -0,0 +1,199 @@ +;; 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. + + +;; Apart from the collecting of options and the handling of --help and --version +;; (which are done in the mcron.scm file), this file provides all the +;; functionality of the crontab personality. It is designed to be loaded and run +;; once, and then the calling program can exit and the crontab program will have +;; completed its function. + + + +;; Procedure to communicate with running cron daemon that a user has modified +;; his crontab. The user name is placed in /var/cron/update, and the process +;; whose PID is held in /var/run/cron.pid is sent a SIGHUP. + +(define (hit-server user-name) + (catch #t (lambda () + (let ((server-pid (with-input-from-file "/var/run/cron.pid" + (lambda () (string->number (read-line)))))) + (catch #t (lambda () + (with-output-to-file "/var/cron/update" (lambda () + (display user-name)(newline)))) + (lambda (key . args) + (display "Cannot write to /var/cron/update.\n") + (primitive-exit 14))) + (kill server-pid SIGHUP))) + (lambda (key . args) + (display "Warning: a cron daemon is not running.\n")))) + + + +;; Procedure to scan a file containing one user name per line (such as +;; /var/cron/allow and /var/cron/deny), and determine if the given name is in +;; there. The procedure returns #t, #f, or '() if the file does not exist. + +(define (in-access-file? file name) + (catch #t (lambda () + (with-input-from-file file (lambda () + (let loop ((input (read-line))) + (if (eof-object? input) + #f + (if (string=? input name) + #t + (loop (read-line)))))))) + (lambda (key . args) '()))) + + + +;; This program should have been installed SUID root. Here we get the passwd +;; entry for the real user who is running this program. + +(define crontab-real-user (passwd:name (getpw (getuid)))) + + + +;; If the real user is not allowed to use crontab due to the /var/cron/allow +;; and/or /var/cron/deny files, bomb out now. + +(if (or (eq? (in-access-file? "/var/cron/allow" crontab-real-user) #f) + (eq? (in-access-file? "/var/cron/deny" crontab-real-user) #t)) + (begin + (display "Access denied by system operator.\n") + (primitive-exit 6))) + + + +;; Iff the real user is root, he can use the -u option to access files of +;; another user. + +(define crontab-user + (option-ref options 'user crontab-real-user)) + + + +;; So now we know which crontab file we will be manipulating. + +(define crontab-file (string-append "/var/cron/tabs/" crontab-user)) + + + +;; Check that no more than one of the mutually exclusive options are being used. + +(if (> (+ (if (option-ref options 'edit #f) 1 0) + (if (option-ref options 'list #f) 1 0) + (if (option-ref options 'remove #f) 1 0)) + 1) + (begin + (display "crontab: Only one of options -e, -l or -r can be used.\n") + (primitive-exit 7))) + + + +;; Check that a non-root user is trying to read someone else's files. + +(if (and (not (eqv? (getuid) 0)) + (option-ref options 'user #f)) + (begin (display "crontab: Only root can use the -u option.\n") + (primitive-exit 8))) + + + +;; There are four possible sub-personalities to the crontab personality: list, +;; remove, edit and replace (when the user uses no options but supplies file +;; names on the command line). + +(cond + + + ;; In the list personality, we simply open the crontab and copy it + ;; character-by-character to the standard output. If anything goes wrong, it + ;; can only mean that this user does not have a crontab file. + + ((option-ref options 'list #f) + (catch #t (lambda () + (with-input-from-file crontab-file (lambda () + (do ((input (read-char) (read-char))) + ((eof-object? input)) + (display input))))) + (lambda (key . args) + (display (string-append "No crontab for " + crontab-user + " exists.\n"))))) + + + ;; In the edit personality, we determine the name of a temporary file and an + ;; editor command, copy an existing crontab file (if it is there) to the + ;; temporary file, making sure the ownership is set so the real user can edit + ;; it; once the editor returns we try to read the file to check that it is + ;; parseable (but do nothing more with the configuration), and if it is okay + ;; (this program is still running!) we move the temporary file to the real + ;; crontab, wake the cron daemon up, and remove the temporary file. + + ((option-ref options 'edit #f) + (let ((temp-file (string-append "/tmp/crontab." (number->string (getpid)))) + (editor (if (getenv "VISUAL") (getenv "VISUAL") + (if (getenv "EDITOR") (getenv "EDITOR") + "vi")))) + (catch #t + (lambda () (copy-file crontab-file temp-file)) + (lambda (key . args) (with-output-to-file temp-file (lambda () #t)))) + (chown temp-file (getuid) (getgid)) + (system (string-append editor " " temp-file)) + (read-vixie-file temp-file) + (copy-file temp-file crontab-file) + (delete-file temp-file) + (hit-server crontab-user))) + + + ;; In the remove personality we simply make an effort to delete the crontab and + ;; wake the daemon. No worries if this fails. + + ((option-ref options 'remove #f) + (catch #t (lambda () (delete-file crontab-file) + (hit-server crontab-user)) + (lambda (key . args) #t))) + + + ;; In the case of the replace personality we loop over all the arguments on the + ;; command line, and for each one parse the file to make sure it is parseable + ;; (but subsequently ignore the configuration), and all being well we copy it + ;; to the crontab location; we deal with the standard input in the same way but + ;; different. :-) In either case the server is woken so that it will read the + ;; newly installed crontab. + + ((not (null? (option-ref options '() '()))) + (let ((input-file (car (option-ref options '() '())))) + (if (string=? input-file "-") + (let ((input-string (stdin->string))) + (read-vixie-port (open-input-string input-string)) + (with-output-to-file crontab-file (lambda () + (display input-string)))) + (begin + (read-vixie-file input-file) + (copy-file input-file crontab-file)))) + (hit-server crontab-user)) + + + ;; The user is being silly. The message here is identical to the one Vixie cron + ;; used to put out, for total compatibility. + + (else + (display + "crontab: usage error: file name must be specified for replace.\n") + (primitive-exit 15))) diff --git a/email.scm b/email.scm new file mode 100644 index 0000000..ec300a7 --- /dev/null +++ b/email.scm @@ -0,0 +1,182 @@ +;; 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 the (with-mail-out action . user) procedure. This +;; procedure runs the action in a child process, allowing the user control over +;; the input and output (including standard error). The input is governed (only +;; in the case of a string action) by the placing of percentage signs in the +;; string; the first delimits the true action from the standard input, and +;; subsequent ones denote newlines to be placed into the input. The output (if +;; there actually is any) is controlled by the MAILTO environment variable. If +;; this is not defined, output is e-mailed to the user passed as argument, if +;; any, or else the owner of the action; if defined but empty then any output is +;; sunk to /dev/null; otherwise output is e-mailed to the address held in the +;; MAILTO variable. + + +;; An action string consists of a sequence of characters forming a command +;; executable by the shell, possibly followed by an non-escaped percentage +;; sign. The text after the percentage sign is to be fed to the command's +;; standard input, with further unescaped percents being substituted with +;; newlines. The escape character can itself be escaped. +;; +;; This regexp separates the two halves of the string, and indeed determines if +;; the second part is present. + +(define action-string-regexp (make-regexp "((\\\\%|[^%])*)%(.*)$")) + + + +;; This regexp identifies an escaped percentage sign. + +(define e-percent (make-regexp "\\\\%")) + + +;; Function to execute some action (this may be a shell command, lamdba function +;; or list of scheme procedures) in a forked process, with the input coming from +;; the string, and output (including the error output) being sent to a pipe +;; opened on a mail transport. + +(use-modules (ice-9 popen)) + +(define (with-mail-out action . user) + + ;; Determine the name of the user who is to recieve the mail, looking for a + ;; name in the optional user argument, then in the MAILTO environment + ;; variable, and finally in the LOGNAME environment variable. (The case + ;; MAILTO="" is dealt with specially below.) + + (let* ((mailto (getenv "MAILTO")) + (user (cond (mailto mailto) + ((not (null? user)) (car user)) + (else (getenv "LOGNAME")))) + (parent->child (pipe)) + (child->parent (pipe)) + (child-pid (primitive-fork))) + + + ;; The child process. Close redundant ends of pipes, remap the standard + ;; streams, and run the action, taking care to chop off the input part of an + ;; action string. + + (if (eqv? child-pid 0) + (begin + (close (cdr parent->child)) + (close (car child->parent)) + + (dup2 (port->fdes (car parent->child)) 0) + (close (car parent->child)) + (dup2 (port->fdes (cdr child->parent)) 1) + (close (cdr child->parent)) + (dup2 1 2) + + (cond ((string? action) + (let ((match (regexp-exec action-string-regexp action))) + (system (if match + (let ((action (match:substring match 1))) + (do ((match (regexp-exec e-percent action) + (regexp-exec e-percent action))) + ((not match)) + (set! action (string-append + (match:prefix match) + "%" + (match:suffix match)))) + action) + action)))) + + ((procedure? action) (action)) + ((list? action) (primitive-eval action))) + + (primitive-exit 0))) + + + ;; The parent process. Get rid of redundant pipe ends. + + (close (car parent->child)) + (close (cdr child->parent)) + + + ;; Put stuff to child from after '%' in command line, replacing + ;; other %'s with newlines. Ugly or what? + + (if (string? action) + (let ((port (cdr parent->child)) + (match (regexp-exec action-string-regexp action))) + (if (and match + (match:substring match 3)) + (with-input-from-string (match:substring match 3) + (lambda () + (let loop () + (let ((next-char (read-char))) + (if (not (eof-object? next-char)) + (cond + ((char=? next-char #\%) + (newline port) + (loop)) + ((char=? next-char #\\) + (let ((escape (read-char))) + (if (eof-object? escape) + (display #\\ port) + (if (char=? escape #\%) + (begin + (display #\% port) + (loop)) + (begin + (display #\\ port) + (display escape port) + (loop)))))) + (else + (display next-char port) + (loop))))))))))) + + + ;; So the child process doesn't hang on to its input expecting more stuff. + + (close (cdr parent->child)) + + + ;; That's got streaming into the child's input out of the way, now we stream + ;; the child's output to a mail sink, but only if there is something there + ;; in the first place. + + (if (eof-object? (peek-char (car child->parent))) + + (read-char (car child->parent)) + + (begin + (set-current-output-port (if (and (string? mailto) + (string=? mailto "")) + (open-output-file "/dev/null") + (open-output-pipe + (string-append config-sendmail + " " + user)))) + (set-current-input-port (car child->parent)) + (display "To: ") (display user) (newline) + (display "From: mcron") (newline) + (display (string-append "Subject: " user "@" (gethostname))) + (newline) + (newline) + + (do ((next-char (read-char) (read-char))) + ((eof-object? next-char)) + (display next-char)))) + + (close (car child->parent)) + + (waitpid child-pid))) diff --git a/environment.scm b/environment.scm new file mode 100644 index 0000000..e4d59a8 --- /dev/null +++ b/environment.scm @@ -0,0 +1,121 @@ +;; 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 defines the global variable current-environment-mods, and the +;; procedures append-environment-mods (which is available to user configuration +;; files), clear-environment-mods, modify-environment, and +;; parse-vixie-environment. The idea is that the current-environment-mods is a +;; list of pairs of environment names and values, and represents the cumulated +;; environment settings in a configuration file. When a job definition is seen +;; in a configuration file, the current-environment-mods are copied into the +;; internal job description, and when the job actually runs these environment +;; modifications are applied to the UNIX environment in which the job runs. + + + +;; The env-alist is an association list of variable names and values. Variables +;; later in the list will take precedence over variables before. We return a +;; fixed-up version in which some variables are given specific default values +;; (which the user can override), and one variable which the user is not allowed +;; to control is added at the end of the list. + +(define (impose-default-environment env-alist passwd-entry) + (append (list (cons "HOME" (passwd:dir passwd-entry)) + (cons "CWD" (passwd:dir passwd-entry)) + (cons "SHELL" (passwd:shell passwd-entry)) + '("TERM" . #f) + '("TERMCAP" . #f)) + env-alist + (list (cons "LOGNAME" (passwd:name passwd-entry)) + (cons "USER" (passwd:name passwd-entry))))) + + + + +;; Modify the UNIX environment for the current process according to the given +;; association list of variables, with the default variable values imposed. + +(define (modify-environment env-alist passwd-entry) + (for-each (lambda (variable) + (setenv (car variable) (cdr variable))) + (impose-default-environment env-alist passwd-entry))) + + + + +;; As we parse configuration files, we build up an alist of environment +;; variables here. + +(define current-environment-mods '()) + + + + +;; When we start to parse a new configuration file, we want to start with a +;; fresh environment (actually an umodified version of the pervading mcron +;; environment). + +(define (clear-environment-mods) + (set! current-environment-mods '())) + + + + +;; Procedure to add another environment setting to the alist above. This is used +;; both implicitly by the Vixie parser, and can be used directly by users in +;; scheme configuration files. The return value is purely for the convenience of +;; the parse-vixie-environment procedure below. + +(define (append-environment-mods name value) + (set! current-environment-mods (append current-environment-mods + (list (cons name value)))) + #t) + + + + +;; Procedure to act on an environment variable specification in a Vixie-style +;; configuration file, by adding an entry to the alist above. Returns #t if the +;; operation was successful, #f if the line could not be interpreted as an +;; environment specification. + +(define parse-vixie-environment-regexp1 + (make-regexp + "^[ \t]*([[:alpha:]_][[:alnum:]_]*)[ \t]*=[ \t]*\"(.*)\"[ \t]*$")) +(define parse-vixie-environment-regexp2 + (make-regexp + "^[ \t]*([[:alpha:]_][[:alnum:]_]*)[ \t]*=[ \t]*\'(.*)\'[ \t]*$")) +(define parse-vixie-environment-regexp3 + (make-regexp + "^[ \t]*([[:alpha:]_][[:alnum:]_]*)[ \t]*=[ \t]*(.*[^ \t])[ \t]*$")) +(define parse-vixie-environment-regexp4 + (make-regexp + "^[ \t]*([[:alpha:]_][[:alnum:]_]*)[ \t]*=[ \t]*$")) + +(use-modules (srfi srfi-2)) + +(define (parse-vixie-environment string) + (let ((match (or (regexp-exec parse-vixie-environment-regexp1 string) + (regexp-exec parse-vixie-environment-regexp2 string) + (regexp-exec parse-vixie-environment-regexp3 string)))) + (if match + (append-environment-mods (match:substring match 1) + (match:substring match 2)) + (and-let* ((match (regexp-exec parse-vixie-environment-regexp4 string))) + (append-environment-mods (match:substring match 1) #f))))) diff --git a/makefile.am b/makefile.am new file mode 100644 index 0000000..00b54ee --- /dev/null +++ b/makefile.am @@ -0,0 +1,61 @@ +## Makefile for the toplevel directory of mcron. +## 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. + +## Process this file with automake to produce Makefile.in + +ED = @ED@ + +MAINTAINERCLEANFILES = configure makefile makefile.in \ + install-sh missing mkinstalldirs texinfo.tex INSTALL \ + aclocal.m4 compile depcomp COPYING + +CLEANFILES = mcron.c + +EXTRA_DIST = makefile.ed config.scm mcron.scm vixie.scm environment.scm \ + email.scm crontab.scm mcron.c.template + +info_TEXINFOS = mcron.texinfo +bin_PROGRAMS = mcron +mcron_SOURCES = mcron.c +mcron_LDFLAGS = @GUILE_LDFLAGS@ +mcron_CFLAGS = @GUILE_CFLAGS@ + +mcron.c : config.scm mcron.scm vixie.scm environment.scm email.scm crontab.scm \ + makefile.ed mcron.c.template + @echo 'Building mcron.c...' + @$(ED) < makefile.ed > /dev/null 2>&1 + @rm -f mcron.escaped.scm > /dev/null 2>&1 + +install-exec-local: + @if [ `id -u` -ne 0 ]; then \ + echo "*** MUST BE ROOT TO INSTALL MCRON ***"; \ + exit 1; \ + fi + +#full program prefix +fpp = $(DESTDIR)$(bindir)/@real_program_prefix@ + +install-exec-hook: + @rm -f $(fpp)cron$(EXEEXT) > /dev/null 2>&1 + @$(INSTALL) --mode='u=rwx' mcron$(EXEEXT) $(fpp)cron$(EXEEXT) + @rm -f $(fpp)crontab$(EXEEXT) > /dev/null 2>&1 + @$(INSTALL) --mode='u=rwxs,og=rx' mcron$(EXEEXT) $(fpp)crontab$(EXEEXT) + ./mkinstalldirs -m 'u=rwx' /var/cron + ./mkinstalldirs -m 'u=rwx,og=rx' /var/run + +uninstall-hook: + @rm -f $(fpp){cron,crontab}$(EXEEXT) diff --git a/makefile.ed b/makefile.ed new file mode 100644 index 0000000..15fe15d --- /dev/null +++ b/makefile.ed @@ -0,0 +1,42 @@ +# 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. +# +# +# +e mcron.scm +/\(load "config.scm"\)/d +-1r config.scm +/\(load "vixie.scm"\)/d +-1r vixie.scm +/\(load "email.scm"\)/d +-1r email.scm +/\(load "environment.scm"\)/d +-1r environment.scm +/\(load "crontab.scm"\)/d +-1r crontab.scm +%s/\\/\\\\/g +%s/"/\\"/g +%s/ *;;.*$/ /g +g/^ *$/d +%s/^/\"/ +%s/$/\"/ +w mcron.escaped.scm +e mcron.c.template +/GUILE_PROGRAM_GOES_HERE/d +-1r mcron.escaped.scm +w mcron.c +q diff --git a/mcron.c.template b/mcron.c.template new file mode 100644 index 0000000..8277732 --- /dev/null +++ b/mcron.c.template @@ -0,0 +1,124 @@ +/* + * 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 C code represents the thinnest possible wrapper around the Guile code + which constitutes all the functionality of the mcron program. There are two + plus one reasons why we need to do this, and one very unfortunate + consequence. + + Firstly, SUID does not work on an executable script. In the end, it is + the execution of the translator, in our case guile, which determines the + effective user, and it is not wise to make the system guile installation + SUID root! + + Secondly, executable scripts show up in ugly ways in listings of the + system process table. Guile in particular, with its multi-line + #! ...\ \n -s ...!# + idiosyncracies shows up in process listings in a way that is difficult + to determine what program is actually running. + + A third reason for the C wrapper which might be mentioned is that a + security-conscious system administrator can choose to only install a + binary, thus removing the possibility of a user studying a guile script + and working out ways of hacking it to his own ends, or worse still + finding a way to modify it to his own ends. + + Unfortunately, running the guile script from inside a C program means + that the sigaction function does not work. Instead, it is necessary to + perform the signal processing in C. + + The guile code itself is substituted for the GU1LE_PROGRAM_GOES_HERE (sic) + token by the makefile, which processes the scheme to make it look like one + big string. +*/ + + + +#include +#include + + + +/* This is a function designed to be installed as a signal handler, for signals + which are supposed to initiate shutdown of this program. It calls the scheme + procedure (see mcron.scm for details) to do all the work, and then exits. */ + +void react_to_terminal_signal (int sig) +{ + scm_eval_string (scm_take0str ("(delete-run-file)") ); + exit (1); +} + + + +/* This is a function designed to be installed as a signal handler. It calls the + scheme procedure to do all the work (see mcron.scm for details). */ + +void react_to_hup_signal (int sig) +{ + scm_eval_string (scm_take0str ("(process-hup)") ); +} + + + +/* This is a function designed to be callable from scheme, and sets up all the + signal handlers required by the cron personality. */ + +SCM set_cron_signals () +{ + static struct sigaction sa; + memset (&sa, 0, sizeof (sa)); + sa.sa_handler = react_to_terminal_signal; + sigaction (SIGTERM, &sa, 0); + sigaction (SIGINT, &sa, 0); + sigaction (SIGQUIT, &sa, 0); + + static struct sigaction hup; hup = sa; + hup.sa_handler = react_to_hup_signal; + sigaction (SIGHUP, &hup, 0); + + return SCM_BOOL_T; +} + + + +/* The effective main function (i.e. the one that actually does some work). We + register the function above with the guile system, and then execute the mcron + guile program. */ + +void inner_main () +{ + scm_c_define_gsubr ("c-set-cron-signals", 0, 0, 0, set_cron_signals); + + scm_eval_string (scm_take0str ( + GUILE_PROGRAM_GOES_HERE + ) ); +} + + + +/* The real main function. Does nothing but start up the guile subsystem. */ + +int main (int argc, char **argv) +{ + scm_boot_guile (argc, argv, inner_main, 0); + return 0; +} diff --git a/mcron.scm b/mcron.scm new file mode 100644 index 0000000..b41a88b --- /dev/null +++ b/mcron.scm @@ -0,0 +1,846 @@ +;; 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 is the 'main' routine for the whole system; the top of this file is the +;; global entry point (after the minimal C wrapper, mcron.c.template). To all +;; intents and purposes the program is pure Guile and starts here. +;; +;; This file is built into mcron.c.template by the makefile, which stringifies +;; the whole lot, and escapes quotation marks and escape characters +;; accordingly. Bear this in mind when considering literal multi-line strings. +;; +;; (load ...)'s are inlined by the makefile. + + +;; Make a note of the time the script started; regardless of how long it takes +;; to initialize things, we will run any job scheduled to run after this exact +;; second. + +(define configuration-time (current-time)) + + + +;; Pull in some constants set by the builder (via autoconf) at configuration +;; time. Turn debugging on if indicated. + +(load "config.scm") +(if config-debug (begin (debug-enable 'debug) + (debug-enable 'backtrace))) + + + +;; To determine the name of the program, scan the first item of the command line +;; backwards for the first non-alphabetic character. This allows names like +;; in.cron to be accepted as an invocation of the cron command. + +(use-modules (ice-9 regex)) + +(define command-name (match:substring (regexp-exec (make-regexp "[[:alpha:]]*$") + (car (command-line))))) + + + +;; We will be doing a lot of testing of the command name, so it makes sense to +;; perform the string comparisons once and for all here. + +(define command-type (cond ((string=? command-name "mcron") 'mcron) + ((or (string=? command-name "cron") + (string=? command-name "crond")) 'cron) + ((string=? command-name "crontab") 'crontab) + (else + (display "The command name is invalid.\n") + (primitive-exit 12)))) + + + +;; There are a different set of options for the crontab personality compared to +;; all the others, with the --help and --version options common to all the +;; personalities. + +(use-modules (ice-9 getopt-long)) + +(define options + (getopt-long (command-line) + (append + (case command-type ('crontab + '((user (single-char #\u) (value #t)) + (edit (single-char #\e) (value #f)) + (list (single-char #\l) (value #f)) + (remove (single-char #\r) (value #f)))) + (else `((schedule (single-char #\s) (value optional)) + (daemon (single-char #\d) (value #f)) + (stdin (single-char #\i) (value #t) + (predicate + ,(lambda (value) + (or (string=? "vixie" value) + (string=? "guile" value)))))))) + '((version (single-char #\v) (value #f)) + (help (single-char #\h) (value #f)))))) + + + + +;; If the user asked for the version of this program, give it to him and get +;; out. + +(if (option-ref options 'version #f) + (begin + (display (string-append "\n +" command-name " (" config-package-string ")\n +Written by Dale Mellor\n +\n +Copyright (C) 2003 Dale Mellor\n +This is free software; see the source for copying conditions. There is NO\n +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n +")) + (quit))) + + + +;; Likewise if the user requested the help text. + +(if (option-ref options 'help #f) + (begin + (display (string-append " +Usage: " (car (command-line)) +(case command-type ('mcron +" [OPTIONS] [FILES]\n +Run an mcron process according to the specifications in the FILES (`-' for\n +standard input), or use all the files in ~/.cron with .guile or .vixie\n +extensions.\n +\n + -v, --version Display version\n + -h, --help Display this help message\n + -s, --schedule[=COUNT] Display the next COUNT jobs (default 8) that\n + will be run by mcron\n + -d, --daemon Immediately detach the program from the terminal and\n + run as a daemon process\n + -i, --stdin=(guile|vixie) Format of data passed as standard input\n + (default guile)") + + ('cron +" [OPTIONS]\n +Unless an option is specified, run a cron daemon as a detached process, \n +reading all the information in the users' crontabs and in /etc/crontab.\n +\n + -v, --version Display version\n + -h, --help Display this help message\n + -s, --schedule[=COUNT] Display the next COUNT jobs (default 8) that\n + will be run by cron") + + ('crontab + (string-append " [-u user] file\n" + " " (car (command-line)) " [-u user] { -e | -l | -r }\n" + " (default operation is replace, per 1003.2)\n" + " -e (edit user's crontab)\n" + " -l (list user's crontab)\n" + " -r (delete user's crontab)\n"))) + +"\n\n +Report bugs to " config-package-bugreport ".\n +")) + (quit))) + + +;;---------------------------------------------------------------------- +;; Perform setup processing specific to cron, crond personalities. +;;---------------------------------------------------------------------- + +;; This is called from the C front-end whenever a terminal signal is +;; received. We simply remove the /var/run/cron.pid file so that crontab and +;; other invokations of cron don't get the wrong idea that a daemon is currently +;; running. + +(define (delete-run-file) + (catch #t (lambda () (delete-file "/var/run/cron.pid")) + (lambda (key . args) #t)) + (quit)) + + + +;; Every time a SIGHUP is received from a crontab process, we read the +;; /var/cron/update file for a user name (he whose crontab has been modified) +;; and add it to this list (thus it may be regarded as a deferred update list). + +(define hup-received-for '()) + + + +;; Two arbiters to control access to the above list. When an interrupt is +;; received, the list will only be modified if pending-lock is available. If it +;; is not, then the interrupt routine will lock interrupt-required and return +;; immediately to the system, which should at convenient times check this lock +;; and send a SIGHUP to the process to re-run the interrupt routine (obviously, +;; if the main program locks pending-lock (or leaves locked) and issues an +;; interrupt the interrupt routine will be a no-op). + +(define pending-lock (make-arbiter "pending-lock")) +(define interrupt-required (make-arbiter "interrupt-required")) + + + +;; This is called from the C front-end whenever a HUP signal is received. We +;; read the name of the user whose crontab has been modified, add his name to +;; the list of pending requests, and remove the update file as an +;; acknowledgement that we received the signal. +;; +;; ! We should put a warning in a log file if we receive a HUP and the update +;; file is not present. + +(define (process-hup) + (if (try-arbiter pending-lock) + (begin + (with-input-from-file "/var/cron/update" (lambda () + (set! hup-received-for (append hup-received-for (list (read-line)))))) + (delete-file "/var/cron/update") + (release-arbiter pending-lock)) + (try-arbiter interrupt-required))) + + + +;; Setup the cron process, if appropriate. If there is already a +;; /var/run/cron.pid file, then we must assume a cron daemon is already running +;; and refuse to start another one. +;; +;; Otherwise, clear the MAILTO environment variable so that output from cron +;; jobs is sent to the various users (this may still be overridden in the +;; configuration files), and call the function in the C wrapper to set up +;; terminal and hangup signal responses to vector to the two procedures +;; above. The PID file will be filled in properly later when we have forked our +;; daemon process (but not done if we are only viewing the schedules). + +(if (eq? command-type 'cron) + (begin + (if (not (eqv? (getuid) 0)) + (begin + (display "This program must be run by the root user (and should ") + (display "have been installed as such).\n") + (primitive-exit 16))) + (if (access? "/var/run/cron.pid" F_OK) + (begin + (display "A cron daemon is already running.\n") + (display " (If you are sure this is not true, remove the file\n") + (display " /var/run/cron.pid.)\n") + (primitive-exit 1))) + (if (not (option-ref options 'schedule #f)) + (with-output-to-file "/var/run/cron.pid" + (lambda () #t))) + (setenv "MAILTO" #f) + (c-set-cron-signals))) + + + +;;---------------------------------------------------------------------- +;; Define the functions available to the configuration files. +;;---------------------------------------------------------------------- + + +;; Define the with-mail-out command for configuration files to use (directly or +;; indirectly as is the case when we parse vixie-style files). + +(load "email.scm") + + + +;; Function (available to user configuration files) which produces a list of +;; values from start up to (but not including) end. An optional step may be +;; supplied, and (if positive) only every step'th value will go into the +;; list. For example, (range 1 6 2) returns '(1 3 5). + +(define (range start end . step) + (let ((step (if (or (null? step) + (<= (car step) 0)) + 1 + (car step)))) + (let loop ((start start)) + (if (>= start end) '() + (cons start + (loop (+ start step))))))) + + + +;; Internal function (not supposed to be used directly in configuration files) +;; which takes a value and a list of possible next values (all assumed less than +;; 9999). It returns a pair consisting of the smallest element of the list, and +;; the smallest element larger than the current value. If an example of the +;; latter cannot be found, 9999 will be returned. + +(define (find-best-next current next-list) + (let ((current-best (cons 9999 9999))) + (for-each (lambda (allowed-time) + (if (< allowed-time (car current-best)) + (set-car! current-best allowed-time)) + (if (and (> allowed-time current) + (< allowed-time (cdr current-best))) + (set-cdr! current-best allowed-time))) + next-list) + current-best)) + + + +;; Internal function to return the time corresponding to some near future +;; hour. If hour-list is not supplied, the time returned corresponds to the +;; start of the next hour of the day. +;; +;; If the hour-list is supplied the time returned corresponds to the first hour +;; of the day in the future which is contained in the list. If all the values in +;; the list are less than the current hour, then the time returned will +;; correspond to the first hour in the list *on the following day*. +;; +;; ... except that the function is actually generalized to deal with seconds, +;; minutes, etc., in an obvious way :-) +;; +;; Note that value-list always comes from an optional argument to a procedure, +;; so is wrapped up as the first element of a list (i.e. it is a list inside a +;; list). + +(define (bump-time time value-list component higher-component + set-component! set-higher-component!) + (if (null? value-list) + (set-component! time (+ (component time) 1)) + (let ((best-next (find-best-next (component time) (car value-list)))) + (if (eqv? 9999 (cdr best-next)) + (begin + (set-higher-component! time (+ (higher-component time) 1)) + (set-component! time (car best-next))) + (set-component! time (cdr best-next))))) + (car (mktime time))) + + + + +;; Set of configuration methods which use the above general function to bump +;; specific components of time to the next legitimate value. In each case, all +;; the components smaller than that of interest are taken to zero, so that for +;; example the time of the next year will be the time at which the next year +;; actually starts. + +(define (next-year-from current-time . year-list) + (let ((time (localtime current-time))) + (set-tm:mon time 0) + (set-tm:mday time 1) + (set-tm:hour time 0) + (set-tm:min time 0) + (set-tm:sec time 0) + (bump-time time year-list tm:year tm:year set-tm:year set-tm:year))) + +(define (next-month-from current-time . month-list) + (let ((time (localtime current-time))) + (set-tm:mday time 1) + (set-tm:hour time 0) + (set-tm:min time 0) + (set-tm:sec time 0) + (bump-time time month-list tm:mon tm:year set-tm:mon set-tm:year))) + +(define (next-day-from current-time . day-list) + (let ((time (localtime current-time))) + (set-tm:hour time 0) + (set-tm:min time 0) + (set-tm:sec time 0) + (bump-time time day-list tm:mday tm:mon set-tm:mday set-tm:mon))) + +(define (next-hour-from current-time . hour-list) + (let ((time (localtime current-time))) + (set-tm:min time 0) + (set-tm:sec time 0) + (bump-time time hour-list tm:hour tm:mday set-tm:hour set-tm:mday))) + +(define (next-minute-from current-time . minute-list) + (let ((time (localtime current-time))) + (set-tm:sec time 0) + (bump-time time minute-list tm:min tm:hour set-tm:min set-tm:hour))) + +(define (next-second-from current-time . second-list) + (let ((time (localtime current-time))) + (bump-time time second-list tm:sec tm:min set-tm:sec set-tm:min))) + + + +;; The current-action-time is the time a job was last run, the time from which +;; the next time to run a job must be computed. (When the program is first run, +;; this time is set to the configuration time so that jobs run from that moment +;; forwards.) Once we have this, we supply versions of the time computation +;; commands above which implicitly assume this value. + +(define current-action-time configuration-time) + + + +;; We want to provide functions which take a single optional argument (as well +;; as implicitly the current action time), but unlike usual scheme behaviour if +;; the argument is missing we want to act like it is really missing, and if it +;; is there we want to act like it is a genuine argument, not a list of +;; optionals. + +(define (maybe-args function args) + (if (null? args) + (function current-action-time) + (function current-action-time (car args)))) + + + +;; These are the convenience functions we were striving to define for the +;; configuration files. They are wrappers for the next-X-from functions above, +;; but implicitly use the current-action-time for the time argument. + +(define (next-year . args) (maybe-args next-year-from args)) +(define (next-month . args) (maybe-args next-month-from args)) +(define (next-day . args) (maybe-args next-day-from args)) +(define (next-hour . args) (maybe-args next-hour-from args)) +(define (next-minute . args) (maybe-args next-minute-from args)) +(define (next-second . args) (maybe-args next-second-from args)) + + + +;; The list of all jobs known to the system. Each element of the list is +;; +;; (vector user next-time-function action environment next-time) +;; +;; where action may be a string (indicating a shell command) or a list +;; (indicating scheme code) or a procedure, and the environment is an alist of +;; modifications that need making to the UNIX environment before the action is +;; run. The next-time elements is the only one that is modified during the +;; running of a cron process (i.e. all the others are set once and for all at +;; configuration time). + +(define job-list '()) + + + +;; Convenience functions for getting and setting the elements of a job object. + +(define (job:user job) (vector-ref job 0)) +(define (job:next-time-function job) (vector-ref job 1)) +(define (job:action job) (vector-ref job 2)) +(define (job:environment job) (vector-ref job 3)) +(define (job:next-time job) (vector-ref job 4)) +(define (job:set-next-time! job time) (vector-set! job 4 time)) + + + +;; Introduce the definition of an environment object, and provide methods for +;; its manipulation and application to the environment in which we run a job. + +(load "environment.scm") + + + +;; Introduce functions which can be used directly in configuration files or +;; indirectly to parse vixie-style time specification strings and manufacture +;; corresponding next-time functions like the ones above. + +(load "vixie.scm") + + + +;; The default user for running jobs is the current one (who invoked this +;; program). There are exceptions: when cron parses /etc/crontab the user is +;; specified on each individual line; when cron parses /var/cron/tabs/* the user +;; is derived from the filename of the crontab. These cases are dealt with by +;; mutating this variable. Note that the variable is only used at configuration +;; time; a UID is stored with each job and it is that which takes effect when +;; the job actually runs. + +(define configuration-user (getpw (getuid))) + + + +;; The job function, available to configuration files for adding a job rule to +;; the system. +;; +;; Here we must 'normalize' the next-time-function so that it is always a lambda +;; function which takes one argument (the last time the job ran) and returns a +;; single value (the next time the job should run). If the input value is a +;; string this is parsed as a Vixie-style time specification, and if it is a +;; list then we arrange to eval it (but note that such lists are expected to +;; ignore the function parameter - the last run time is always read from the +;; current-action-time global variable). A similar normalization is applied to +;; the action. +;; +;; Here we also compute the first time that the job is supposed to run, by +;; finding the next legitimate time from the current configuration time (set +;; right at the top of this program). +;; +;; Note that the new job is added at the front of the job-list (this is +;; important so that the entries in the system crontab /etc/crontab finish up at +;; the front of the list when we scan that file). + +(define (job time-proc action) + (let ((action (cond ((procedure? action) action) + ((list? action) (lambda () (primitive-eval action))) + ((string? action) (lambda () (system action))) + (else + (display "job: invalid second argument (action; should be lamdba") + (display "function, string or list)\n") + (primitive-exit 2)))) + + (time-proc + (cond ((procedure? time-proc) time-proc) + ((string? time-proc) (parse-vixie-time time-proc)) + ((list? time-proc) (lambda (dummy) + (primitive-eval time-proc))) + (else + + (display "job: invalid first argument (next-time-function; should ") + (display "be function, string or list)") + (primitive-exit 3))))) + + (set! job-list (cons (vector configuration-user + time-proc + action + (list-copy current-environment-mods) + (time-proc current-action-time)) + job-list)))) + + +;;---------------------------------------------------------------------- +;; End of definition of procedures for configuration files. +;;---------------------------------------------------------------------- + + + +;; Procedure to slurp the standard input into a string. + +(define (stdin->string) + (with-output-to-string (lambda () (do ((in (read-char) (read-char))) + ((eof-object? in)) + (display in))))) + + + +;; Now we have the procedures in place for dealing with the contents of +;; configuration files, the crontab personality is able to validate such +;; files. If the user requested the crontab personality, we load and run the +;; code here and then get out. + +(if (eq? command-type 'crontab) + (begin + (load "crontab.scm") + (quit))) + + + +;;---------------------------------------------------------------------- +;; Procedures for effecting the configuration process itself. +;;---------------------------------------------------------------------- + + +;; Procedure which processes any configuration file according to the +;; extension. If a file is not recognized, it is silently ignored (this deals +;; properly with most editors' backup files, for instance). + +(define guile-file-regexp (make-regexp "\\.gui(le)?$")) +(define vixie-file-regexp (make-regexp "\\.vix(ie)?$")) + +(define (process-user-file file-path) + (cond ((string=? file-path "-") + (if (string=? (option-ref options 'stdin "guile") "vixie") + (read-vixie-port (current-input-port)) + (eval-string (stdin->string)))) + ((regexp-exec guile-file-regexp file-path) + (load file-path)) + ((regexp-exec vixie-file-regexp file-path) + (read-vixie-file file-path)))) + + + +;; Procedure to run through all the files in a user's ~/.cron directory (only +;; happens under the mcron personality). + +(define (process-files-in-user-directory) + (catch #t (lambda () + (let* ((dir-path (string-append (passwd:dir configuration-user) + "/.cron")) + (directory (opendir dir-path))) + (do ((file-name (readdir directory) (readdir directory))) + ((eof-object? file-name) (closedir directory)) + (process-user-file (string-append dir-path + "/" + file-name))))) + (lambda (key . args) + (display "Cannot read files in your ~/.cron directory.\n") + (primitive-exit 13)))) + + + +;; Procedure to check that a user name is the the passwd database (it may happen +;; that a user is removed after creating a crontab). If the user name is valid, +;; the full passwd entry for that user is returned to the caller. + +(define (valid-user user-name) + (setpwent) + (do ((entry (getpw) (getpw))) + ((or (not entry) + (string=? (passwd:name entry) user-name)) + (endpwent) + entry))) + + + +;; Procedure to process all the files in the crontab directory, making sure that +;; each file is for a legitimate user and setting the configuration-user to that +;; user. In this way, when the job procedure is run on behalf of the +;; configuration files, the jobs are registered with the system with the +;; appropriate user. Note that only the root user should be able to perform this +;; operation, but we leave it to the permissions on the /var/cron/tabs directory +;; to enforce this. + +(use-modules (srfi srfi-2)) + +(define (process-files-in-system-directory) +;;; (catch #t (lambda () + (let ((directory (opendir "/var/cron/tabs"))) + (do ((file-name (readdir directory) (readdir directory))) + ((eof-object? file-name) (closedir directory)) + (and-let* ((user (valid-user file-name))) + (set! configuration-user user) + (read-vixie-file (string-append "/var/cron/tabs/" + file-name))))) +;;; ) +;;; (lambda (key . args) +;;; (display "You do not have permission to access the system crontabs.\n") +;;; (primitive-exit 4))) + ) + + + +;; The head of the jobs list will contain the jobs specified in /etc/crontab, +;; and this variable tells us how long that head is. + +(define system-jobs 0) + + + +;; Having defined all the necessary procedures for scanning various sets of +;; files, we perform the actual configuration of the program depending on the +;; personality we are running as. If it is mcron, we either scan the files +;; passed on the command line, or else all the ones in the user's .cron +;; directory. If we are running under the cron personality, we read the +;; /var/cron/tabs directory and also the /etc/crontab file. + +(case command-type + ('mcron (if (null? (option-ref options '() '())) + (process-files-in-user-directory) + (for-each (lambda (file-path) + (process-user-file file-path)) + (option-ref options '() '())))) + + ('cron (process-files-in-system-directory) + (let ((start-length (length job-list))) + (read-vixie-file "/etc/crontab" parse-system-vixie-line) + (set! system-jobs (- (length job-list) start-length))))) + + + +;;---------------------------------------------------------------------- +;; End of configuration section. +;; +;; Now the main execution loop. +;;---------------------------------------------------------------------- + + + +;; Procedure to locate the jobs in the global job-list with the lowest +;; (soonest) next-times. These are the jobs for which we must schedule the mcron +;; program (under any personality) to next wake up. The return value is a cons +;; cell consisting of the next time (maintained in the next-time variable) and a +;; list of the job entries that are to run at this time (maintained in the +;; next-jobs-list variable). +;; +;; The procedure works by first obtaining the time of the first job on the list, +;; and setting this job in the next-jobs-list. Then for each other entry on the +;; job-list, either the job runs earlier than any other that have been scanned, +;; in which case the next-time and next-jobs-list are re-initialized to +;; accomodate, or the job runs at the same time as the next job, in which case +;; the next-jobs-list is simply augmented with the new job, or else the job runs +;; later than others noted in which case we ignore it for now and continue to +;; recurse the list. + +(define (find-next-jobs) + + (if (null? job-list) + (if (eq? command-type 'mcron) + (begin (display "Nothing to do.\n") + (primitive-exit 5)) + (cons #f '())) + + (let ((next-time (job:next-time (car job-list))) + (next-jobs-list (list (car job-list)))) + + (for-each + (lambda (job) + (let ((this-time (job:next-time job))) + (cond ((< this-time next-time) + (set! next-time this-time) + (set! next-jobs-list (list job))) + ((eqv? this-time next-time) + (set! next-jobs-list (cons job next-jobs-list)))))) + (cdr job-list)) + + (cons next-time next-jobs-list)))) + + + +;; If the user has requested a schedule of jobs that will run, we provide the +;; information here and then get out. +;; +;; Start by determining the number of time points in the future that output is +;; required for. This may be provided on the command line as a parameter to the +;; --schedule option, or else we assume a default of 8. Having determined this +;; count we enter a loop of displaying the next set of jobs to run, artificially +;; forwarding the time to the next time point (instead of waiting for it to +;; occur as we would do in a normal run of mcron), and recurse around the loop +;; count times. + +(and-let* ((count (option-ref options 'schedule #f))) + (set! count (if (eq? count #t) + 8 + (string->number count))) + (if (<= count 0) (set! count 1)) + (do ((count count (- count 1))) + ((eqv? count 0)) + (let* ((next-jobs (find-next-jobs)) + (date-string (strftime "%c\n" (localtime (car next-jobs))))) + (for-each (lambda (job) (display date-string) + (write (job:action job)) + (newline)(newline)) + (cdr next-jobs)))) + (quit)) + + + +;; For proper housekeeping, it is necessary to keep a record of the number of +;; child processes we fork off to run the jobs. + +(define number-children 0) + + + +;; For every job on the list, fork a process to run it (noting the fact by +;; increasing the number-children counter), and in the new process set up the +;; run-time environment exactly as it should be before running the job proper. +;; +;; In the parent, update the job entry by computing the next time the job needs +;; to run. + +(define (run-jobs jobs-list) + (for-each (lambda (job) + (if (eqv? (primitive-fork) 0) + (begin + (setuid (passwd:uid (job:user job))) + (chdir (passwd:dir (job:user job))) + (modify-environment (job:environment job) (job:user job)) + ((job:action job)) + (primitive-exit 0)) + (begin + (set! number-children (+ number-children 1)) + (set! current-action-time (job:next-time job)) + (job:set-next-time! job + ((job:next-time-function job) + current-action-time))))) + jobs-list)) + + + +;; If we are supposed to run as a daemon process (either a --daemon option has +;; been explicitly used, or we are running as cron or crond), detach from the +;; terminal now. If we are running as cron, we can now write the PID file. + +(if (option-ref options 'daemon (eq? command-type 'cron)) + (begin + (if (not (eqv? (primitive-fork) 0)) + (quit)) + (setsid) + (if (eq? command-type 'cron) + (with-output-to-file "/var/run/cron.pid" + (lambda () (display (getpid)) (newline)))))) + + + +;; Now the main loop. Take the current time. 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. Repeat ad infinitum. + +(use-modules (srfi srfi-1)) + +(let main-loop () + + (release-arbiter pending-lock) + + ;; Check for any pending updates to the configuration files (as notified by + ;; crontab). If one is seen, remove all work from the job-list that belongs to + ;; this user, set up the global variables current-action-time and + ;; configuration-user appropriately, and then process the new configuration + ;; file for the user. + + (do () ((and (if (release-arbiter interrupt-required) + (begin (kill (getpid) SIGHUP) #f) + #t) + (null? hup-received-for))) + (try-arbiter pending-lock) + (let ((user (car hup-received-for))) + (set! hup-received-for (cdr hup-received-for)) + (release-arbiter pending-lock) + (set! configuration-user (getpw user)) + (let ((uid (passwd:uid configuration-user)) + (old-job-list job-list)) + (set! current-action-time (current-time)) + (set! job-list + (append + (list-head old-job-list system-jobs) + (begin (set! job-list '()) + (read-vixie-file (string-append "/var/cron/tabs/" user)) + job-list) + (remove (lambda (job) (eqv? (passwd:uid (job:user job)) uid)) + (list-tail old-job-list system-jobs))))))) + + + ;; Compute the amount of time that we must sleep until the next job is due to + ;; run. + + (let* ((next-jobs (find-next-jobs)) + (next-time (car next-jobs)) + (next-jobs-list (cdr next-jobs)) + (sleep-time (if next-time (- next-time (current-time)) + #f))) + + + ;; If an update signal has just come in, or there are no current jobs and a + ;; pause operation has been interrupted (presumably by a SIGHUP), or the + ;; sleep operation has been interrupted (presumably by a SIGHUP), then undo + ;; the latest time calculations and jump back to the top of the loop where + ;; the pending updates will be dealt with. + ;; + ;; Otherwise, when we wake from our sleep, first try to collect as many + ;; child zombies as possible from previous job runs, then run the current + ;; set of jobs (on the next-jobs-list). + + (if (and (null? hup-received-for) + ;; ! If a signal occurs now, we won't see it + ;; until the next signal. + (eqv? 0 (cond ((not sleep-time) (pause) 1) + ((> sleep-time 0) (sleep sleep-time)) + (else 0)))) + (run-jobs next-jobs-list))) + + (do () ((or (<= number-children 0) + (eqv? (car (waitpid WAIT_ANY WNOHANG)) 0))) + (set! number-children (- number-children 1))) + + (main-loop)) diff --git a/mcron.texinfo b/mcron.texinfo new file mode 100644 index 0000000..4ceb290 --- /dev/null +++ b/mcron.texinfo @@ -0,0 +1,1094 @@ +\input texinfo +@c %**start of header +@setfilename mcron.info +@settitle mcron 1.0.0 +@c %**end of header + +@syncodeindex fn cp + +@copying +This file documents the @code{mcron} command for running jobs at +scheduled times. + +Copyright (C) 2003 Dale Mellor +This is free software. See the source files for the terms of the +copyright. + +@ignore +Permission is granted to make and distribute verbatim copies of +this manual provided the copyright notice and this permission notice +are preserved on all copies. + +Permission is granted to process this file through TeX and print the +results, provided the printed document carries copying permission +notice identical to this one except for the removal of this paragraph +(this paragraph not being relevant to the printed manual). + +Permission is granted to copy and distribute modified versions of this +manual under the conditions for verbatim copying, provided that the entire +resulting derived work is distributed under the terms of a permission +notice identical to this one. + +Permission is granted to copy and distribute translations of this manual +into another language, under the above conditions for modified versions, +except that this permission notice may be stated in a translation approved +by the Foundation. +@end ignore +@end copying + + +@ifinfo + +@dircategory Individual utilities + +@direntry +* mcron: (mcron). Run jobs at scheduled times. +@end direntry + +@end ifinfo + + +@titlepage +@title mcron - Mellor's cron daemon +@author Dale Mellor + +@page +@vskip 0pt plus 1fill +@insertcopying + +@end titlepage + +@contents + +@ifnottex +@node Top, Introduction, (dir), (dir) +@top mcron + +@insertcopying +@end ifnottex + +@menu +* Introduction:: Introducing mcron. +* Simple examples:: How to use mcron 99.9% of the time. +* Syntax:: All the possibilities for configuring cron jobs. +* Invoking:: What happens when you run the mcron command. +* Index:: The complete index. + +@detailmenu + --- The Detailed Node Listing --- + +Simple examples + +* Guile Simple Examples:: +* Vixie Simple Examples:: + +Full available syntax + +* Guile Syntax:: +* Extended Guile examples:: +* Vixie Syntax:: + +Extended Guile examples + +* AT commands:: +* Every second Sunday:: +* Two hours every day:: +* Missing the first appointment:: +* Penultimate day of every month:: + +Vixie + +* Paul Dixie's copyright:: +* Crontab file:: +* Incompatibilities with old Unices:: + +Detailed invoking + +* Running mcron:: +* Running cron or crond:: +* Running crontab:: +* Exit codes:: + +@end detailmenu +@end menu + +@node Introduction, Simple examples, Top, Top +@chapter Introducing mcron +@cindex introduction +@cindex mcron +The mcron program represents a complete re-think of the cron concept +originally found in the Berkeley and AT&T unices, and subsequently +rationalized by Paul Vixie. The original idea was to have a daemon +that wakes up every minute, scans a set of files under a special +directory, and determines from those files if any shell commands +should be executed in this minute. + +The new idea is to read the required command instructions, work out +which command needs to be executed next, and then sleep until the +inferred time has arrived. On waking the commands are run, and the +time of the next command is computed. Furthermore, the specifications +are written in scheme, allowing at the same time simple command +execution instructions and very much more flexible ones to be composed +than the original Vixie format. This has several useful advantages +over the original idea. + +@cindex advantages of mcron +@itemize @bullet +@item +Does not consume CPU resources when not needed. Many cron daemons only +run jobs once an hour, or even just once a day. +@item +Can easily allow for finer time-points to be specified, +i.e. seconds. In principle this could be extended to microseconds, but +this is not implemented. +@item +Times can be more or less regular. For example, a job that runs +every 17 hours can be specified, or a job that runs on the first +Sunday of every month. +@item +Times can be dynamic. Arbitrary Guile (scheme) code can be provided to +compute the next time that a command needs to be run. This could, for +example, take the system load into consideration. +@item +Turns out to be easy to provide complete backwards compatibility with +Vixie cron. +@item +Each user looks after his own files in his own directory. He can use +more than one to break up complicated cron specifications. +@item +Each user can run his own daemon. This removes the need for suid +programs to manipulate the crontabs, and eliminates many security +concerns that surround all existing cron programs. +@item +The user can obtain an advance schedule of all the jobs that are due +to run. +@item +Vixie cron is implemented in 4500 lines of C code; mcron is 1500 lines +of scheme, despite the fact that it offers many more features and much +more flexibility, and complete compatibility with Vixie cron. +@end itemize + +A full discussion of the design and philosophy of mcron can be found +in the white paper at http://.../mcron.html [FIXME]. + + +@node Simple examples, Syntax, Introduction, Top +@chapter Simple examples +The vast majority of uses of cron are sublimely simple: run a program +every hour, or every day. With this in mind the design of mcron has +been to allow such simple specifications to be made easily. The +examples show how to create the command descriptions, and subsequently +how to run mcron to make them happen. +@menu +* Guile Simple Examples:: +* Vixie Simple Examples:: +@end menu + +@node Guile Simple Examples, Vixie Simple Examples, Simple examples, Simple examples +@section Guile +@cindex guile examples +@cindex examples, guile +@cindex example, run a program every hour +You have an executable @code{my-program} in your home directory, which +you want to run every hour. Create a file @code{job.guile} in directory +@code{~/.cron} with the following contents + +@example +(job '(next-hour) "my-program") +@end example + +then run the command @code{mcron}. + +Want the program to run fifteen minutes past the hour, every two +hours? Edit the file to read + +@example +(job + '(next-minute-from + (next-hour (range 0 24 2)) + 15) + "my-program") +@end example + +and run the command @code{mcron}. + +Or, if you are not comfortable with Scheme, you could use (and see +also the next section) + +@example +(job "15 */2 * * *" "my-program") +@end example + +and run the @code{mcron} command. + +If you want to run other jobs, you can either add more lines to this +file, or you can create other files in your @code{.cron} directory +with the @code{.guile} extension. Alternatively, you can use any file +you want and pass it as an argument to @code{mcron}, or even pipe the +commands into the standard input. + + +@node Vixie Simple Examples, , Guile Simple Examples, Simple examples +@section Vixie +@cindex examples +@cindex examples, vixie +@cindex vixie examples +You have an executable @code{my-program} in your home directory, which +you want to run every hour. Create a file @code{job.vixie} in directory +@code{~/.cron} with the following contents + +@example +0 * * * * my-program +@end example + +then run the command @code{mcron}. + +@cindex vixie compatibility +@cindex compatibility +Alternatively (full compatibility with Vixie cron), set your +environment variable @code{EDITOR} to your favorite editor, run +@code{crontab -e}, put the above line into the edit buffer, save and +exit. For this to work the @code{crond} daemon must be already running +on your system, by root. + +@node Syntax, Invoking, Simple examples, Top +@chapter Full available syntax +@menu +* Guile Syntax:: +* Extended Guile examples:: +* Vixie Syntax:: +@end menu +@node Guile Syntax, Extended Guile examples, Syntax, Syntax +@section Guile Syntax +@subsection Job specification +@cindex guile syntax +@cindex syntax, guile +@findex job +In Guile-formatted configuration files each command that needs +executing is introduced with the @code{job} function. This function +always takes exactly two arguments, the first a time specification, +and the second a command specification. + +@cindex time specification, procedure +@cindex procedure time specification +The first argument can be a procedure, a list, or a string. If a +function is supplied, it must take exactly one argument, which will be +the ``current'' time in UNIX format, and the return value of the +function must be the time in UNIX format when this action should next +be run. The following functions are available to facilitate the +computation: + +@findex next-second-from +@code{(next-second-from time . args)} without arguments this +returns the second after the current one. With the extra arguments, +these form a list of seconds in the minute when the action should run, +and the function will return the time of the next allowed second +(which may be in the next minute of the hour). @footnote{Note that +while commands can be scheduled to run at any second, it is unlikely +that they will be executed then but some time shortly thereafter, +depending on the load on the system and the number of jobs that mcron +has to start at the same time.} + +@findex next-minute-from +@findex next-hour-from +@findex next-day-from +@findex next-week-from +@findex next-month-from +@findex next-year-from +Similarly to @code{next-second-from}, there are also +@code{next-minute-from}, @code{next-hour-from}, @code{next-day-from}, +@code{next-week-from}, @code{next-month-from}, @code{next-year-from}. + +@findex range +Furthermore, the optional argument can be fulfilled by the function +@code{(range start end . step)}, which will provide a list of values +from start to (but not including) end, with the step if given. For +example @code{(range 0 10 2)} will yield the list @code{'(0 2 4 6 8)}. + +@findex next-second +@findex next-minute +@findex next-hour +@findex next-day +@findex next-week +@findex next-month +@findex next-year +@cindex time specification, list +@cindex list time specification +If the first argument to the @code{job} function is a list, it is +taken to be program code made up of the functions @code{(next-second +. args)}, @code{(next-minute...)}, etc, where the optional arguments +can be supplied with the @code{(range)} function above (these +functions are analogous to the ones above except that they implicitly +assume the current time; it is supplied by the mcron core when the +list is eval'd). + +@cindex time specification +@cindex time specification, string +@cindex string time specification +@cindex time specification, vixie-style +@cindex vixie-style time specification +If the first argument to the @code{job} function is a string, it is +expected to be a Vixie cron-style time specification. See the section +on Vixie syntax for this. + +@cindex job execution +@cindex command execution +@cindex execution +The second argument to the @code{(job)} function can be either a +string, a list, or a function. In all cases the command is executed in +the user's home directory, under the user's own UID. If a string is +passed, it is assumed to be shell script and is executed with the +user's default shell. If a list is passed it is assumed to be scheme +code and is eval'd as such. A supplied function should take exactly +zero arguments, and will be called at the pertinent times. + +@subsection Sending output as e-mail +@cindex email output +@cindex email from guile script +@cindex standard input to commands +@findex with-mail-out +When jobs are specified in a vixie-style configuration, the command is +broken at a percentage sign, and the stuff that comes after this is +sent into the command's standard input. Furthermore, any output from +the command is mailed to the user. This functionality is provided for +compatibility with Vixie cron, but it is also available to scheme +configuration files. The command (with-mail-out action . user) can be +used to direct output from the action (which may be a procedure, list, +or string) into an e-mail to the user. + +In the case that the action is a string, then percentage signs are +processed as per the vixie specifications, and information is piped to +the shell command's standard input. + +@subsection Setting environment variables +@cindex environment variables in scheme +@cindex setting environment variables +@findex append-environment-mods +Also for compatibility with Vixie cron, mcron has the ability to set +environment variables in configuration files. To access this +functionality from a scheme configuration file, use the command +(append-environment-mods name value), where name is the name of an +environment variable, and value is the value put to it. A value of #f +will remove the variable from the environment. + +Note that environment modifications are accumulated as the +configuration file is processed, so when a job actually runs, its +environment will be modified according to the modifications specified +before the job specification in the configuration file. + + +@node Extended Guile examples, Vixie Syntax, Guile Syntax, Syntax +@section Extended Guile examples +@cindex examples, extended guile +@cindex extended guile examples +While Guile gives you flexibility to do anything, and the power to +represent complex requirements succinctly, things are not always as +they seem. The following examples illustrate some pitfalls, and +demonstrate how to code around them. + +@menu +* AT commands:: +* Every second Sunday:: +* Two hours every day:: +* Missing the first appointment:: +* Penultimate day of every month:: +@end menu + +@node AT commands, Every second Sunday, Extended Guile examples, Extended Guile examples +@subsection Synthesizing ``at'' commands +@cindex at command +The current implementation of mcron does not provide for an at command +(a command-line program that allows the user to specify that a job +runs exactly once at a certain time). This can, however, be achieved. + +Suppose the program @code{my-program} needs to be run at midnight +tonight. A Guile script like the following should work. FIXME: TEST +THIS EXAMPLE. + +@example +(define my-program-flag #t) + +(job (lambda (current-time) + (if my-program-flag + (begin + (set! my-program-flag #f) + (next-day-from current-time)) + 99999999)) + (lambda () (system "my-program") + (kill (getppid)))) +@end example + +@node Every second Sunday, Two hours every day, AT commands, Extended Guile examples +@subsection Every second Sunday +@cindex examples, every second sunday +To run @code{my-program} on the second Sunday of every month, a Guile +script like the following should suffice (it is left as an exercise to +the student to understand how this works!). FIXME: TEST THIS EXAMPLE. + +@example +(job (lambda (current-time) + (let* ((next-month (next-month-from current-time)) + (first-day (tm:wday (localtime next-month))) + (second-sunday (if (eqv? first-day 0) + 8 + (- 15 first-day)))) + (+ next-month (* 24 60 60 second-sunday)))) + "my-program") +@end example + + +@node Two hours every day, Missing the first appointment, Every second Sunday, Extended Guile examples +@subsection Two hours every day +@cindex examples, two hours every day +@cindex pitfalls, two hours every day +Surprisingly perhaps, the following will @strong{not} have the desired +effect. + +@example +(job '(next-hour-from (next-day) '(1 2)) + "my-program") +@end example + +Rather than running the my-program program at one o'clock and two +o'clock every day, it will only run it at one o'clock. This is because +each time mcron has to compute the next time to run the command, it +first obtains the next day, and then finds the earliest hour in that +day to run at. Thus, after running the command at one o'clock, the +program first skips forwards to the next midnight (missing the two +o'clock appointment), and then finds the next one o'clock schedule. + +The following simple command is the correct way to specify this +behaviour. + +@example +(job '(next-hour '(1 2)) "my-program") +@end example + + +@node Missing the first appointment, Penultimate day of every month, Two hours every day, Extended Guile examples +@subsection Missing the first appointment +@cindex examples, missing the first appointment +@cindex pitfalls, missing the first appointment +The command + +@example +(job '(next-hour-from (next-day) '(16)) + "my-program") +@end example + +will run @code{my-program} every day at four o'clock in the +afternoon. However, if mcron is started with this script at midday, +the first time the command will run will be four o'clock tomorrow; +today's appointment will be missed (one time only). + +The correct way to specify this requirement is simply + +@example +(job '(next-hour '(16)) + "my-program") +@end example + + +@node Penultimate day of every month, , Missing the first appointment, Extended Guile examples +@subsection Penultimate day of every month +@cindex examples, penultimate day of every month +The following will run the @code{my-program} program on the +second-to-last day of every month. + +@example +(job '(- (next-month-from (next-month)) (* 48 3600)) + "my-program") +@end example + + + +@node Vixie Syntax, , Extended Guile examples, Syntax +@section Vixie +@cindex syntax, vixie +@cindex vixie syntax +@cindex vixie definition +@cindex vixie compatibility +@cindex compatibility, vixie +@emph{NOTE} that this section is definitive. If there is a difference in +behaviour between the mcron program and this part of the manual, then +there is a bug in the program. This section is also copied verbatim +from Paul Dixie's documentation for his cron program, and his +copyright notice is duly reproduced below. + +@cindex /etc/crontab +@cindex system crontab +@cindex incompatibility +@cindex vixie incompatibility +There is one single exception to the above. @strong{Mcron does not +notice changes made to /etc/crontab}. If a change is made, then it is +necessary to kill the cron daemon and restart it for the change to +take effect. + +There are three problems with this specification. + +@cindex zero'th day of month +@cindex 0'th day of month +1. It is allowed to specify days of the month in the range 0-31. What +does it mean to specify day 0? Well, if I'm not mistaken mcron will +run the command on the last day of the previous month (but don't rely +on this). I don't know what Vixie cron would have done. + +@cindex thirteenth month of year +@cindex 13th month of year +2. Similarly to the above (but different), months of the year can be +specified in the range 0-12. In the case of mcron (don't know what +Vixie cron did) month 12 will cause the program to wait until January +of the following year (but don't rely on this). + +@cindex shell +@cindex environment variables, shell +@cindex /etc/passwd +3. Somewhere it says that cron sets the SHELL environment variable to +/bin/sh, and elsewhere it implies that the default behaviour is for +the user's default shell to be used to execute commands. Mcron sets +the variable and runs the command in the user's default shell, as +advertised by the /etc/passwd file. + +@menu +* Paul Dixie's copyright:: +* Crontab file:: +* Incompatibilities with old Unices:: +@end menu + + +@node Paul Dixie's copyright, Crontab file, Vixie Syntax, Vixie Syntax +@subsection Paul Dixie's copyright +@cindex copyright, Paul Dixie's +@cindex Paul Dixie's copyright +@quotation +Copyright 1988,1990,1993,1994 by Paul Vixie +All rights reserved + +Distribute freely, except: don't remove my name from the source or +documentation (don't take credit for my work), mark your changes (don't +get me blamed for your possible bugs), don't alter or remove this +notice. May be sold if buildable source is provided to buyer. No +warrantee of any kind, express or implied, is included with this +software; use at your own risk, responsibility for damages (if any) to +anyone resulting from the use of this software rests entirely with the +user. +@end quotation + + + + +@node Crontab file, Incompatibilities with old Unices, Paul Dixie's copyright, Vixie Syntax +@subsection Crontab files. +@cindex crontab file +@cindex vixie crontab file +A @code{crontab} file contains instructions to the @code{cron} daemon +of the general form: ``run this command at this time on this date''. +Each user has their own crontab, and commands in any given crontab +will be executed as the user who owns the crontab. Uucp and News will +usually have their own crontabs, eliminating the need for explicitly +running @code{su} as part of a cron command. + +@cindex comments, vixie-style +Blank lines and leading spaces and tabs are ignored. Lines whose first +non-space character is a pound-sign (#) are comments, and are ignored. +Note that comments are not allowed on the same line as cron commands, since +they will be taken to be part of the command. Similarly, comments are not +allowed on the same line as environment variable settings. + +An active line in a crontab will be either an environment setting or a cron +command. An environment setting is of the form, + +@cindex environment setting, vixie-style +@example +name = value +@end example + +where the spaces around the equal-sign (=) are optional, and any +subsequent non-leading spaces in @code{value} will be part of the +value assigned to @code{name}. The @code{value} string may be placed +in quotes (single or double, but matching) to preserve leading or +trailing blanks. + +@cindex environment variables, SHELL +@cindex environment variables, LOGNAME +@cindex environment variables, HOME +@cindex SHELL environment variable +@cindex LOGNAME environment variable +@cindex HOME environment variable +@cindex /etc/passwd +Several environment variables are set up automatically by the +@code{cron} daemon. SHELL is set to /bin/sh, and LOGNAME and HOME are +set from the /etc/passwd line of the crontab's owner. HOME and SHELL +may be overridden by settings in the crontab; LOGNAME may not. + +@cindex environment variables, USER +@cindex USER environment variable +@cindex BSD +(Another note: the LOGNAME variable is sometimes called USER on BSD systems... +on these systems, USER will be set also.) @footnote{mcron has not been +ported to BSD, so these notes are not relevant.} + +@cindex environment variables, MAILTO +@cindex MAILTO environment variable +In addition to LOGNAME, HOME, and SHELL, @code{cron} will look at +MAILTO if it has any reason to send mail as a result of running +commands in ``this'' crontab. If MAILTO is defined (and non-empty), +mail is sent to the user so named. If MAILTO is defined but empty +(MAILTO=""), no mail will be sent. Otherwise mail is sent to the +owner of the crontab. This option is useful if you decide on +/bin/mail instead of /usr/lib/sendmail as your mailer when you install +cron -- /bin/mail doesn't do aliasing, and UUCP usually doesn't read +its mail. + +The format of a cron command is very much the V7 standard, with a number of +upward-compatible extensions. Each line has five time and date fields, +followed by a user name if this is the system crontab file, +followed by a command. Commands are executed by @code{cron} +when the minute, hour, and month of year fields match the current +time, @strong{and} when at least one of the two day fields (day of month, or day of week) +match the current time (see ``Note'' below). @code{cron} examines cron entries once every minute. +The time and date fields are: + +@cindex vixie time specification fields +@cindex fields, vixie time specification +@multitable @columnfractions .2 .5 +@item Field @tab Allowed values +@item ----- @tab -------------- +@item minute @tab 0-59 +@item hour @tab 0-23 +@item day of month @tab 0-31 +@item month @tab 0-12 (or names, see below) +@item day of week @tab 0-7 (0 or 7 is Sun, or use names) +@end multitable + +A field may be an asterisk (*), which always stands for ``first-last''. + +@cindex ranges in vixie time specifications +Ranges of numbers are allowed. Ranges are two numbers separated +with a hyphen. The specified range is inclusive. For example, +8-11 for an ``hours'' entry specifies execution at hours 8, 9, 10 +and 11. + +@cindex lists in vixie time specifications +Lists are allowed. A list is a set of numbers (or ranges) +separated by commas. Examples: ``1,2,5,9'', ``0-4,8-12''. + +@cindex steps in vixie time specifications +Step values can be used in conjunction with ranges. Following +a range with ``/'' specifies skips of the number's value +through the range. For example, ``0-23/2'' can be used in the hours +field to specify command execution every other hour (the alternative +in the V7 standard is ``0,2,4,6,8,10,12,14,16,18,20,22''). Steps are +also permitted after an asterisk, so if you want to say ``every two +hours'', just use ``*/2''. + +@cindex names in vixie-style time specifications +Names can also be used for the ``month'' and ``day of week'' +fields. Use the first three letters of the particular +day or month (case doesn't matter). Ranges or +lists of names are not allowed. @footnote{Mcron allows any alphabetic +characters after a name, so full names of days or months are also valid.} + +@cindex % character on vixie-style commands +@cindex standard input, vixie-style +The ``sixth'' field (the rest of the line) specifies the command to be +run. +The entire command portion of the line, up to a newline or % +character, will be executed by /bin/sh or by the shell +specified in the SHELL variable of the cronfile. +Percent-signs (%) in the command, unless escaped with backslash +(\\), will be changed into newline characters, and all data +after the first % will be sent to the command as standard +input. + +@cindex day specification, vixie-style +@cindex vixie-style day specification +Note: The day of a command's execution can be specified by two +fields -- day of month, and day of week. If both fields are +restricted (ie, aren't *), the command will be run when +@emph{either} +field matches the current time. For example, + +``30 4 1,15 * 5'' + +would cause a command to be run at 4:30 am on the 1st and 15th of each +month, plus every Friday. + +EXAMPLE CRON FILE + +@example +# use /bin/sh to run commands, no matter what /etc/passwd says +SHELL=/bin/sh +# mail any output to `paul', no matter whose crontab this is +MAILTO=paul +# +# run five minutes after midnight, every day +5 0 * * * $HOME/bin/daily.job >> $HOME/tmp/out 2>&1 +# run at 2:15pm on the first of every month -- output mailed to paul +15 14 1 * * $HOME/bin/monthly +# run at 10 pm on weekdays, annoy Joe +0 22 * * 1-5 mail -s "It's 10pm" joe%Joe,%%Where are your kids?% +23 0-23/2 * * * echo "run 23 minutes after midn, 2am, 4am ..., everyday" +5 4 * * sun echo "run at 5 after 4 every sunday" +@end example + +@node Incompatibilities with old Unices, , Crontab file, Vixie Syntax +@subsection Extensions and incompatibilities. +@cindex incompatibilities with old Unices +@cindex extensions, vixie over old Unices +This section lists differences between Paul Vixie's cron and the +olde-worlde BSD and AT&T programs, for the benefit of system +administrators and users who are upgrading all the way. + +@itemize @bullet +@item +@cindex day 7 +When specifying day of week, both day 0 and day 7 will be considered Sunday. +BSD and AT&T seem to disagree about this. + +@item +Lists and ranges are allowed to co-exist in the same field. "1-3,7-9" would +be rejected by AT&T or BSD cron -- they want to see "1-3" or "7,8,9" ONLY. + +@item +Ranges can include "steps", so "1-9/2" is the same as "1,3,5,7,9". + +@item +Names of months or days of the week can be specified by name. + +@item +Environment variables can be set in the crontab. In BSD or AT&T, the +environment handed to child processes is basically the one from /etc/rc. + +@item +Command output is mailed to the crontab owner (BSD can't do this), can be +mailed to a person other than the crontab owner (SysV can't do this), or the +feature can be turned off and no mail will be sent at all (SysV can't do this +either). + +@end itemize + + +@node Invoking, Index, Syntax, Top +@chapter Detailed invoking +@cindex invoking +@cindex personality +@cindex mcron program +@cindex cron program +@cindex crond program +@cindex crontab program +The program adopts one of three different personalities depending on +the name used to invoke it. In a standard installation, the program is +installed in the system under the names mcron, cron and crontab +(installed SUID). + +The recommended way to invoke the program is via the mcron personality +described in the next section. The program can also be run as cron by +root, and by the SUID program crontab by individual users to gain +backwards compatibility with Vixie cron. However, due to the fact that +this daemon process is shared by, and under control of, all the users +of the system it is possible (though very unlikely) that it may become +unusable, hence the recommendation to use the mcron personality. + +@cindex deprecated, vixie personality +Furthermore, the Vixie personality is considered deprecated by this +author (it offers not a single advantage over the mcron personality, +and bloats the code by a factor of three). It is unlikely that this +personality will ever actually go away, but the program may in future +be split into two distinct parts, and new developments will only take +place in the part which implements the mcron personality. + + + +@menu +* Running mcron:: +* Running cron or crond:: +* Running crontab:: +* Exit codes:: +@end menu + +@node Running mcron, Running cron or crond, Invoking, Invoking +@section Running mcron +@cindex invoking mcron +@cindex mcron options +@cindex mcron arguments +@cindex command line, mcron +@cindex mcron command line +Mcron should be run by the user who wants to schedule his jobs. It may +be made a background job using the facilities of the shell. The basic +command is +@code{mcron [OPTION ...] [file ...]} +which has the effect of reading all the configuration files specified +(subject to the options) and then waiting until it is time to execute +some command. If no files are given on the command line, then mcron +will look in the user's ~/.cron directory. In either case, files which +end in the extension .vixie or .vix will be assumed to contain +Vixie-style crontabs, and files ending .guile or .gle will be assumed +to contain scheme code and will be executed as such. + +The program accepts the following options. + +@table @option +@item -s [count] +@itemx --schedule[=count] +@cindex printout of jobs schedule +@cindex schedule of jobs, listing +@cindex options, schedule +@cindex options, -s +@cindex -s option +@cindex --schedule option +With this option specified no commands are run. Instead, the program +computes the times the commands would be run and prints the +information to the screen, and then immediately exits. + +The count, if supplied, indicates the number of commands to +display. The default value is 8. + +@cindex daemon option +@cindex options, daemon +@cindex options, -d +@cindex -d option +@cindex --daemon option +@item -d +@itemx --daemon +With this option the program will detach itself from the controlling +terminal and run as a daemon process. + +@cindex stdin option +@cindex options, stdin +@cindex options, -i +@cindex -i option +@cindex --stdin option +@cindex standard input, configuring from +@cindex configuring from standard input +@item -i (vixie|guile) +@itemx --stdin=(vixie|guile) +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 -v option +@cindex --version option +@cindex options, -v +@cindex options, version +@item -v +@itemx --version +This option causes a message to be printed on the standard output with +information about the version and copyright for the current program. + +@cindex -h option +@cindex --help option +@cindex options, -h +@cindex options, --help +@item -h +@itemx --help +This causes a short but complete usage message to be displayed on +standard output. + +@end table + +@node Running cron or crond, Running crontab, Running mcron, Invoking +@section Running cron or crond +@cindex cron, invokation +@cindex running cron +@cindex crond, invokation +@cindex running crond +@cindex /var/cron/tabs +@cindex /var/run/cron.pid +If the program runs by the name of cron or crond, then it will read +all the files in /var/cron/tabs (which should only be readable by +root) and the file /etc/crontab, and then detaches itself from the +terminal to live forever as a daemon process. Additionally, it puts +its PID into /var/run/cron.pid, and listens for SIGHUPs, in which case +it will look for a file /var/cron/update which should contain a single +username, and the program will re-read that user's crontab. This is +for correct functioning with the crontab program. + +@cindex /etc/crontab +@cindex incompatibility +@strong{NOTE} that it does not detect changes in /etc/crontab; if this file +is ever changed then it will be necessary to kill and then restart the +daemon. This is the one and only incompatibility with Vixie's cron +program. + +The options which may be used with this program are as follows. + +@table @option + +@cindex -v option +@cindex --version option +@cindex options, -v +@cindex options, version +@item -v +@itemx --version +This option causes a message to be printed on the standard output with +information about the version and copyright for the current program. + +@cindex -h option +@cindex --help option +@cindex options, -h +@cindex options, --help +@item -h +@itemx --help +This causes a short but complete usage message to be displayed on +standard output. + +@end table + +@node Running crontab, Exit codes, Running cron or crond, Invoking +@section Running crontab +@cindex crontab, invoking +@cindex running crontab +This program is run by individual users to inspect or modify their +crontab files. If a change is made to the file, then the root daemon +process will be given a kick, and will immediately read the new +configuration. A warning will be issued to standard output if it +appears that a cron daemon is not running. + +The command is used as + +@code{crontab [-u user] file} + +or + +@code{crontab [-u user] ( -l | -e | -r )} + +Only the root user can use the -u option, to specify the manipulation +of another user's crontab file. In the first instance, the entire +crontab file of the user is replaced with the contents of the +specified file, or standard input if the file is ``-''. + +In the latter case, the program behaves according to which of the +(mutually exclusive) options was given (note that the long options are +an mcron extension). + +@table @option + +@cindex -l option +@cindex list option, crontab +@cindex options, -l +@cindex options, --list +@cindex viewing a crontab +@cindex listing a crontab +@item -l +@itemx --list +Print the user's crontab file to the standard output, and exit. + +@cindex -r option +@cindex remove option +@cindex options, -r +@cindex options, --remove +@cindex deleting a crontab +@cindex removing a crontab +@item -r +@item --remove +Delete the user's crontab file, and exit. + +@cindex -e option +@cindex edit option +@cindex options, -e +@cindex options, --edit +@cindex editing a crontab +@cindex creating a crontab +@item -e +@item --edit +Using the editor specified in the user's VISUAL or EDITOR environment +variables, allow the user to edit his crontab. Once the user exits the +editor, the crontab is checked for parseability, and if it is okay +then it is installed as the user's new crontab and the daemon is +notified that a change has taken place, so that the new file will +become immediately effective. + +@end table + +@node Exit codes, , Running crontab, Invoking +@section Exit codes +@cindex exit codes +@cindex error conditions +@cindex errors +The following are the status codes returned to the operating system +when the program terminates. + +@table @asis +@item 0 +No problems. + +@item 1 +An attempt has been made to start cron but there is already a +/var/run/cron.pid file. If there really is no other cron daemon +running (this does not include invokations of mcron) then you should +remove this file before attempting to run cron. + +@item 2 +In parsing a guile configuration file, a @code{job} command has been +seen but the second argument is neither a procedure, list or +string. This argument is the job's action, and needs to be specified +in one of these forms. + +@item 3 +In parsing a guile configuration file, a @code{job} command has been +seen but the first argument is neither a procedure, list or +string. This argument is the job's next-time specification, and needs +to be specified in one of these forms. + +@item 4 +An attempt to run cron has been made by a user who does not have +permission to access the crontabs in /var/cron/tabs. These files +should be readable only by root, and the cron daemon must be run as +root. + +@item 5 +An attempt to run mcron has been made, but there are no jobs to +schedule! + +@item 6 +The system administrator has blocked this user from using crontab with +the files /var/cron/allow and /var/cron/deny. + +@item 7 +Crontab has been run with more than one of the arguments @code{-l}, +@code{-r}, @code{-e}. These are mutually exclusive options. + +@item 8 +Crontab has been run with the -u option by a user other than +root. Only root is allowed to use this option. + +@item 9 +An invalid vixie-style time specification has been supplied. + +@item 10 +An invalid vixie-style job specification has been supplied. + +@item 11 +A bad line has been seen in /etc/crontab. + +@item 12 +The last component of the name of the program was not one of +@code{mcron}, @code{cron}, @code{crond} or @code{crontab}. + +@item 13 +Either the ~/.cron directory does not exist, or there is a problem +reading the files there. + +@item 14 +There is a problem writing to /var/cron/update. This is probably +because the crontab program is not installed SUID root, as it should +be. + +@item 15 +Crontab has been run without any arguments at all. There is no default +behaviour in this case. + +@item 16 +Cron has been run by a user other than root. + +@end table + + + +@node Index, , Invoking, Top +@unnumbered Index + +@printindex cp + +@bye 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