Index: locker/cron/bin/cronload
===================================================================
--- locker/cron/bin/cronload	(revision 359)
+++ locker/cron/bin/cronload	(revision 359)
@@ -0,0 +1,135 @@
+#!/usr/bin/perl
+
+# Author: <quentin@mit.edu>
+
+use strict;
+use warnings;
+
+use File::Spec::Functions;
+use Getopt::Long;
+
+use constant {
+    CRON_DIR => "cron_scripts",
+    CRONTAB_FILE => "crontab",
+    AUTO_DIR => "AUTO",
+    SPOOL_DIR => "/mit/scripts/cron/crontabs",
+};
+
+my $force = 0;
+my $list = 0;
+my $pretend = 0;
+
+sub get_crontabs() {
+    my $crontab = catfile($ENV{"HOME"}, CRON_DIR, CRONTAB_FILE);
+    my $crontabdir = catdir($ENV{"HOME"}, CRON_DIR, AUTO_DIR);
+    
+    my @crontabs;
+    
+    opendir(CRONTABS, $crontabdir) or print "You don't have a ".CRON_DIR."/".AUTO_DIR."/ directory\n";
+    push(@crontabs, grep { -r $_ } map { catfile($crontabdir, $_) } grep { !/^[\.#]/ } readdir(CRONTABS));
+    closedir(CRONTABS);
+    
+    push (@crontabs, $crontab) if (-r $crontab);
+    return @crontabs;
+}
+
+sub read_crontab($) {
+    my ($file) = @_;
+    # local $/;
+    
+    open(CRONTAB, $file) or die "Couldn't read crontab $file!";
+    my @lines = <CRONTAB>;
+    close(CRONTAB);
+    
+    return @lines;
+}
+
+sub check_crontab(@) {
+    my (@lines) = @_;
+    
+    my @errors;
+    
+    foreach my $line (@lines) {
+        $line =~ s|#.*$||; # Remove comments
+        $line =~ s|^\s*(.*?)\s*$|$1|; # Remove whitespace
+        
+        if ($line =~ m|^\w[\w\d]*=|) {
+            # Comment
+            next;
+        } elsif ($line =~ m|^(?:(\S+)\s+){5}(.*)|) {
+            # Crontab line
+            my ($minute, $hour, $day, $month, $dow) = ($1,$2,$3,$4,$5);
+            # FIXME: Validate the time fields.
+            next;
+        } elsif ($line =~ m|^\s*$|) {
+            # Whitespace
+            next;
+        } else {
+            push(@errors, "Unrecognized crontab line:\n$line\n");
+        }
+    }
+    return @errors;
+}
+
+
+
+GetOptions("force|f+" => \$force,
+	   "list|l" => \$list,
+	   "pretend|p" => \$pretend);
+
+if ($list) {
+    my $file = catfile(SPOOL_DIR, $ENV{"USER"});
+    local $/;
+    open (CRONTAB, $file) or die "No crontab installed.\n";
+    print <CRONTAB>;
+    close (CRONTAB);
+    exit;
+}
+
+my @crontabs = get_crontabs();
+my @all_errors;
+my @final_crontab;
+my ($numvalid, $numinvalid) = (0,0);
+
+foreach my $crontab (@crontabs) {
+    push(@final_crontab, "### $crontab\n");
+    my @crontab = read_crontab($crontab);
+    my @errors = check_crontab(@crontab);
+    if (@errors == 0) {
+        print "$crontab is a valid crontab\n";
+        push(@final_crontab, @crontab);
+        $numvalid++;
+    } else {
+        print "$crontab has errors:\n";
+        push(@all_errors, scalar(@errors)." errors in $crontab:\n", @errors);
+        print join("\n", @errors);
+        $numinvalid++;
+        if ($force >= 2) {
+            push(@final_crontab, @crontab);
+        } else {
+            my $errors = join("\n", @errors);
+            $errors =~ s|^|# |mg;
+            push(@final_crontab, "## $crontab was not installed due to errors:\n", $errors);
+        }
+    }
+}
+if ($pretend) {
+    print "Would install this crontab:\n";
+    print @final_crontab;
+    exit;
+}
+
+if ($force < 1 && @all_errors) {
+    print "Not loading new crontab. Use -f to force.\n";
+    exit;
+}
+if ($force >= 2 && @all_errors) {
+    print "Loading $numvalid crontab ($numinvalid BROKEN!) files...\n";
+} else {
+    print "Loading $numvalid crontab files...\n";
+}
+
+# FIXME
+# Load @final_crontab somehow
+
+print "done.\n";
Index: locker/cron/bin/crontab
===================================================================
--- locker/cron/bin/crontab	(revision 359)
+++ locker/cron/bin/crontab	(revision 359)
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+# Author: <quentin@mit.edu>
+
+if [[ "$1" = "-l" ]]; then
+	`dirname $0`/cronload -l;
+else
+	cat <<EOF;
+To edit your user-specific crontab, edit ~/cron_scripts/crontab and run
+cronload. cronload will concatenate ~/cron_scripts/crontab with the
+contents of ~/cron_scripts/AUTO/ and load them into the cron
+system. To see the full contents of your crontab on the server, use
+crontab -l
+EOF
+fi
Index: locker/cron/bin/heartbeat
===================================================================
--- locker/cron/bin/heartbeat	(revision 359)
+++ locker/cron/bin/heartbeat	(revision 359)
@@ -0,0 +1,67 @@
+#!/bin/bash
+
+CRONROOT=/afs/athena.mit.edu/contrib/scripts/cron
+
+# Find our real hostname
+
+# This big long mess just results in a list of ip/name.
+for i in `/sbin/ip addr show dev eth0 | grep ' inet ' | cut -f 6 -d ' ' | cut -f 1 -d '/' | xargs -n 1 host | cut -f 1,5 -d ' ' | sed 'y/ /\//'`; do 
+	hostip=`echo $i | cut -f 1 -d '.'`
+	name=`echo $i | cut -f 2 -d '/'`
+	case $name in
+		SCRIPTS*) echo "$name";;
+		*) echo "Heartbeat for $name ($hostip)"; HOSTNAME=$name; HOSTIP=$hostip;;
+	esac;
+done
+
+# Tell everyone who's watching that we're alive
+touch $CRONROOT/servers/$HOSTNAME
+
+# Sleep based on our IP, in an attempt to not collide with another server also trying to gain control of the mirroring
+sleep $(($HOSTIP - 50))
+
+# Find the current master
+MASTER="DOES-NOT-EXIST"
+current_server () {
+	for i in $CRONROOT/server-crontabs/*; do
+		if [ -h $i ]; then
+			MASTER=`basename $i`
+			echo "Current master $MASTER"
+		fi
+	done
+}
+if lockfile -1 -r10 -l90 $CRONROOT/lock/heartbeat.lock; then
+    current_server
+
+	# The only way to compare times in bash is to compare the modtimes of two files.
+	compare=`mktemp /tmp/heartbeat-compare.XXXXXXXXXX`
+	touch -d '2 minutes ago' $compare
+	
+	if [[ $CRONROOT/servers/$MASTER -ot $compare ]]; then
+		# Master died! Take over.
+		echo "Master '$MASTER' died! Taking over."
+		for i in $CRONROOT/server-crontabs/*; do
+			if [ -h $i ]; then
+				echo rm $i
+				rm $i
+			else
+				echo rmdir $i
+				rmdir $i
+			fi
+		done
+		for i in $CRONROOT/servers/*; do
+			server=`basename $i`
+			case $server in
+				$HOSTNAME)
+					echo ln -s ../crontabs/ $CRONROOT/server-crontabs/$HOSTNAME
+					ln -s ../crontabs/ $CRONROOT/server-crontabs/$HOSTNAME;;
+				*)
+					echo mkdir $CRONROOT/server-crontabs/$server
+					mkdir $CRONROOT/server-crontabs/$server;;
+			esac;
+		done;
+	fi
+
+	rm $compare
+    rm -f $CRONROOT/lock/heartbeat.lock
+fi
Index: locker/cron/doc/cron-commands.txt
===================================================================
--- locker/cron/doc/cron-commands.txt	(revision 359)
+++ locker/cron/doc/cron-commands.txt	(revision 359)
@@ -0,0 +1,74 @@
+$ crontab *
+
+To edit your user-specific crontab, edit ~/cron_scripts/crontab and run
+cronload. cronload will concatenate ~/cron_scripts/crontab with the
+contents of ~/cron_scripts/AUTO/ and load them into the cron
+system. To see the full contents of your crontab on the server, use
+crontab -l
+
+$ crontab -l
+$ cronload -l
+
+#### Generated by cronload. See crontab -h.
+### ~/cron_scripts/AUTO/cacti
+foo
+### ~/cron_scripts/AUTO/gallery
+bar
+### ~/cron_scripts/crontab
+baz
+
+$ cronload
+
+~/cron_scripts/AUTO/cacti is a valid crontab
+~/cron_scripts/AUTO/gallery is a valid crontab
+~/cron_scripts/crontab has errors:
+Invalid month "foo".
+
+Not loading new crontab. Use -f to force.
+
+$ cronload -f
+
+~/cron_scripts...
+...
+
+Loading 2 crontab files... done.
+
+$ cronload -f -f
+
+~/cron_scripts...
+...
+
+Loading 3 crontab (1 BROKEN!) files... done.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Index: locker/cron/src/Makefile
===================================================================
--- locker/cron/src/Makefile	(revision 359)
+++ locker/cron/src/Makefile	(revision 359)
@@ -0,0 +1,27 @@
+# Makefile for dillon's cron and crontab
+#
+
+DESTDIR ?= /usr/local
+CC  = gcc
+CFLAGS = -O2 -Wall -Wstrict-prototypes
+LIB = 
+D_SRCS = cronload.real.c subs.c
+D_OBJS = cronload.real.o subs.o
+
+all:	cronload.real
+
+cronload.real:  ${D_OBJS}
+	${CC} ${CFLAGS} -o cronload.real ${D_OBJS}
+	strip cronload.real
+
+clean:  cleano
+	rm -f cronload.real
+
+cleano:
+	rm -f *.o
+
+install:
+	install -o root -g wheel -m 4755 cronload.real ${DESTDIR}/bin/cronload.real
+#	install -o root -g wheel -m 0644 crontab.1 ${DESTDIR}/man/man1/crontab.1
+
+
Index: locker/cron/src/cronload.real.c
===================================================================
--- locker/cron/src/cronload.real.c	(revision 359)
+++ locker/cron/src/cronload.real.c	(revision 359)
@@ -0,0 +1,262 @@
+
+/*
+ * cronload.real.c
+ *
+ * CRONTAB
+ *
+ * usually setuid root, -c option only works if getuid() == geteuid()
+ *
+ * Copyright 1994 Matthew Dillon (dillon@apollo.backplane.com)
+ * May be distributed under the GNU General Public License
+ */
+
+#include "defs.h"
+
+#define VERSION	"$Revision$"
+
+const char *CDir = SCRIPTS_CRONTABS;
+int   UserId;
+short LogLevel = 9;
+
+int GetReplaceStream(const char *user, const char *file);
+extern int ChangeUser(const char *user, short dochdir);
+
+int
+main(int ac, char **av)
+{
+    enum { NONE, LIST, REPLACE, DELETE } option = NONE;
+    struct passwd *pas;
+    char *repFile = NULL;
+    int repFd = 0;
+    int i;
+    char caller[256];		/* user that ran program */
+
+    UserId = getuid();
+    if ((pas = getpwuid(UserId)) == NULL) {
+        perror("getpwuid");
+        exit(1);
+    }
+    snprintf(caller, sizeof(caller), "%s", pas->pw_name);
+
+    i = 1;
+    if (ac > 1) {
+        if (av[1][0] == '-' && av[1][1] == 0) {
+            option = REPLACE;
+            ++i;
+	} else if (av[1][0] != '-') {
+            option = REPLACE;
+            ++i;
+            repFile = av[1];
+	}
+    }
+
+    for (; i < ac; ++i) {
+        char *ptr = av[i];
+
+        if (*ptr != '-')
+            break;
+	ptr += 2;
+
+	switch(ptr[-1]) {
+	case 'l':
+	    if (ptr[-1] == 'l')
+		option = LIST;
+	    /* fall through */
+	case 'd':
+	    if (ptr[-1] == 'd')
+		option = DELETE;
+	    /* fall through */
+	case 'u':
+	    if (i + 1 < ac && av[i+1][0] != '-') {
+	        ++i;
+	        if (getuid() == geteuid()) {
+		    pas = getpwnam(av[i]);
+		    if (pas) {
+			UserId = pas->pw_uid;
+		    } else {
+			errx(1, "user %s unknown\n", av[i]);
+		    }
+		} else {
+		    errx(1, "only the superuser may specify a user\n");
+		}
+	    }
+	    break;
+	case 'c':
+	    if ((getuid() == geteuid()) && (0 == getuid())) {
+		CDir = (*ptr) ? ptr : av[++i];
+	    } else {
+	        errx(1, "-c option: superuser only\n");
+	    }
+	    break;
+	default:
+	    i = ac;
+	    break;
+	}
+    }
+    if (i != ac || option == NONE) {
+	printf("cronload.real " VERSION "\n");
+	printf("cronload.real file <opts>  replace crontab from file\n");
+	printf("cronload.real -    <opts>  replace crontab from stdin\n");
+	printf("cronload.real -u user      specify user\n");
+	printf("cronload.real -l [user]    list crontab for user\n");
+	printf("cronload.real -d [user]    delete crontab for user\n");
+	printf("cronload.real -c dir       specify crontab directory\n");
+	exit(0);
+    }
+
+    /*
+     * Get password entry
+     */
+
+    if ((pas = getpwuid(UserId)) == NULL) {
+        perror("getpwuid");
+        exit(1);
+    }
+
+    /*
+     * If there is a replacement file, obtain a secure descriptor to it.
+     */
+
+    if (repFile) {
+        repFd = GetReplaceStream(caller, repFile);
+        if (repFd < 0) {
+            errx(1, "unable to read replacement file\n");
+        }
+    }
+
+    /*
+     * Change directory to our crontab directory
+     */
+
+    if (chdir(CDir) < 0) {
+        errx(1, "cannot change dir to %s: %s\n", CDir, strerror(errno));
+    }
+
+    /*
+     * Handle options as appropriate
+     */
+
+    switch(option) {
+    case LIST:
+	{
+	    FILE *fi;
+	    char buf[1024];
+
+	    if ((fi = fopen(pas->pw_name, "r"))) {
+		while (fgets(buf, sizeof(buf), fi) != NULL)
+		    fputs(buf, stdout);
+		fclose(fi);
+	    } else {
+		fprintf(stderr, "no crontab for %s\n", pas->pw_name);
+	    }
+	}
+	break;
+    case REPLACE:
+	{
+	    char buf[1024];
+	    char path[1024];
+	    int fd;
+	    int n;
+
+	    snprintf(path, sizeof(path), "%s.new", pas->pw_name);
+	    if ((fd = open(path, O_CREAT|O_TRUNC|O_EXCL|O_APPEND|O_WRONLY, 0600)) >= 0) {
+		while ((n = read(repFd, buf, sizeof(buf))) > 0) {
+		    write(fd, buf, n);
+		}
+		close(fd);
+		rename(path, pas->pw_name);
+	    } else {
+		fprintf(stderr, "unable to create %s/%s: %s\n", 
+		    CDir,
+		    path,
+		    strerror(errno)
+		);
+	    }
+	    close(repFd);
+	}
+	break;
+    case DELETE:
+        remove(pas->pw_name);
+        break;
+    case NONE:
+    default: 
+        break;
+    }
+
+    /*
+     *  Bump notification file.  Handle window where crond picks file up
+     *  before we can write our entry out.
+     */
+	/* // only applicable to dcron
+    if (option == REPLACE || option == DELETE) {
+        FILE *fo;
+        struct stat st;
+
+        while ((fo = fopen(CRONUPDATE, "a"))) {
+			fprintf(fo, "%s\n", pas->pw_name);
+			fflush(fo);
+			if (fstat(fileno(fo), &st) != 0 || st.st_nlink != 0) {
+			fclose(fo);
+			break;
+			}
+			fclose(fo);
+			// * loop * /
+		}
+		if (fo == NULL) {
+			fprintf(stderr, "unable to append to %s/%s\n", CDir, CRONUPDATE);
+		}
+    }
+    */
+    (volatile void)exit(0);
+    /* not reached */
+}
+
+int
+GetReplaceStream(const char *user, const char *file)
+{
+    int filedes[2];
+    int pid;
+    int fd;
+    int n;
+    char buf[1024];
+
+    if (pipe(filedes) < 0) {
+        perror("pipe");
+        return(-1);
+    }
+    if ((pid = fork()) < 0) {
+        perror("fork");
+        return(-1);
+    }
+    if (pid > 0) {
+        /*
+         * PARENT
+         */
+
+	close(filedes[1]);
+	if (read(filedes[0], buf, 1) != 1) {
+	    close(filedes[0]);
+	    filedes[0] = -1;
+	}
+	return(filedes[0]);
+    }
+
+    /*
+     * CHILD
+     */
+
+    close(filedes[0]);
+
+    if (ChangeUser(user, 0) < 0)
+        exit(0);
+
+    fd = open(file, O_RDONLY);
+    if (fd < 0)
+        errx(0, "unable to open %s\n", file);
+    buf[0] = 0;
+    write(filedes[1], buf, 1);
+    while ((n = read(fd, buf, sizeof(buf))) > 0) {
+        write(filedes[1], buf, n);
+    }
+    exit(0);
+}
Index: locker/cron/src/defs.h
===================================================================
--- locker/cron/src/defs.h	(revision 359)
+++ locker/cron/src/defs.h	(revision 359)
@@ -0,0 +1,46 @@
+
+/*
+ * DEFS.H
+ *
+ * Copyright 1994-1998 Matthew Dillon (dillon@backplane.com)
+ * May be distributed under the GNU General Public License
+ */
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/ioctl.h>
+#include <sys/wait.h>
+#include <sys/resource.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <errno.h>
+#include <time.h>
+#include <dirent.h>
+#include <fcntl.h>
+#include <pwd.h>
+#include <unistd.h>
+#include <grp.h>
+#include <err.h>
+
+#define Prototype extern
+#define arysize(ary)	(sizeof(ary)/sizeof((ary)[0]))
+
+#ifndef SCRIPTS_CRONTABS
+#define SCRIPTS_CRONTABS	"/mit/scripts/cron/crontabs"
+#endif
+#ifndef TMPDIR
+#define TMPDIR		"/tmp"
+#endif
+#ifndef OPEN_MAX
+#define OPEN_MAX	256
+#endif
+
+#ifndef CRONUPDATE
+#define CRONUPDATE	"cron.update"
+#endif
+
+#ifndef MAXLINES
+#define MAXLINES	256		/* max lines in non-root crontabs */
+#endif
Index: locker/cron/src/subs.c
===================================================================
--- locker/cron/src/subs.c	(revision 359)
+++ locker/cron/src/subs.c	(revision 359)
@@ -0,0 +1,147 @@
+
+/*
+ * SUBS.C
+ *
+ * Copyright 1994 Matthew Dillon (dillon@apollo.backplane.com)
+ * May be distributed under the GNU General Public License
+ */
+
+#include "defs.h"
+
+Prototype void logn(int level, const char *ctl, ...);
+Prototype void log9(const char *ctl, ...);
+Prototype void logfd(int fd, const char *ctl, ...);
+Prototype void fdprintf(int fd, const char *ctl, ...);
+Prototype int ChangeUser(const char *user, short dochdir);
+Prototype void vlog(int level, int fd, const char *ctl, va_list va);
+Prototype int slog(char *buf, const char *ctl, int nmax, va_list va, short useDate);
+
+extern short LogLevel;
+
+void 
+log9(const char *ctl, ...)
+{
+    va_list va;
+
+    va_start(va, ctl);
+    vlog(9, 2, ctl, va);
+    va_end(va);
+}
+
+void 
+logn(int level, const char *ctl, ...)
+{
+    va_list va;
+
+    va_start(va, ctl);
+    vlog(level, 2, ctl, va);
+    va_end(va);
+}
+
+void 
+logfd(int fd, const char *ctl, ...)
+{
+    va_list va;
+
+    va_start(va, ctl);
+    vlog(9, fd, ctl, va);
+    va_end(va);
+}
+
+void 
+fdprintf(int fd, const char *ctl, ...)
+{
+    va_list va;
+    char buf[2048];
+
+    va_start(va, ctl);
+    vsnprintf(buf, sizeof(buf), ctl, va);
+    write(fd, buf, strlen(buf));
+    va_end(va);
+}
+
+void
+vlog(int level, int fd, const char *ctl, va_list va)
+{
+    char buf[2048];
+    short n;
+    static short useDate = 1;
+
+    if (level >= LogLevel) {
+        write(fd, buf, n = slog(buf, ctl, sizeof(buf), va, useDate));
+	useDate = (n && buf[n-1] == '\n');
+    }
+}
+
+int
+slog(char *buf, const char *ctl, int nmax, va_list va, short useDate)
+{
+    time_t t = time(NULL);
+    struct tm *tp = localtime(&t);
+
+    buf[0] = 0;
+    if (useDate)
+	strftime(buf, 128, "%d-%b-%y %H:%M  ", tp);
+    vsnprintf(buf + strlen(buf), nmax, ctl, va);
+    return(strlen(buf));
+}
+
+int
+ChangeUser(const char *user, short dochdir)
+{
+    struct passwd *pas;
+
+    /*
+     * Obtain password entry and change privilages
+     */
+
+    if ((pas = getpwnam(user)) == 0) {
+        logn(9, "failed to get uid for %s", user);
+        return(-1);
+    }
+    setenv("USER", pas->pw_name, 1);
+    setenv("HOME", pas->pw_dir, 1);
+    setenv("SHELL", "/bin/sh", 1);
+
+    /*
+     * Change running state to the user in question
+     */
+
+    if (initgroups(user, pas->pw_gid) < 0) {
+	logn(9, "initgroups failed: %s %s", user, strerror(errno));
+	return(-1);
+    }
+    if (setregid(pas->pw_gid, pas->pw_gid) < 0) {
+	logn(9, "setregid failed: %s %d", user, pas->pw_gid);
+	return(-1);
+    }
+    if (setreuid(pas->pw_uid, pas->pw_uid) < 0) {
+	logn(9, "setreuid failed: %s %d", user, pas->pw_uid);
+	return(-1);
+    }
+    if (dochdir) {
+	if (chdir(pas->pw_dir) < 0) {
+	    logn(8, "chdir failed: %s %s", user, pas->pw_dir);
+	    if (chdir(TMPDIR) < 0) {
+		logn(9, "chdir failed: %s %s", user, pas->pw_dir);
+		logn(9, "chdir failed: %s " TMPDIR, user);
+		return(-1);
+	    }
+	}
+    }
+    return(pas->pw_uid);
+}
+
+#if 0
+
+char *
+strdup(const char *str)
+{
+    char *ptr = malloc(strlen(str) + 1);
+
+    if (ptr)
+        strcpy(ptr, str);
+    return(ptr);
+}
+
+#endif
