[net.sources] At

richmon@astrovax.UUCP (Michael Richmond) (08/19/85)

Here is a version of the Unix at command that allows things
to be run at a later time. It was specially written to run
on an AT&T 7300, but might be modified to run on other
machines. It hasn't been tested very much, so there may be
bugs - if you find any, or have improvements, please let
me know. The at.doc file should be nroff'ed with the -man
macros; if anyone knows how to make a 7300 show boldface type,
I'd appreciated knowing that, too. Enjoy.

Michael Richmond			Princeton University, Astrophysics

{allegra,akgua,burl,cbosgd,decvax,ihnp4,noao,princeton,vax135}!astrovax!richmon



# This is a shell archive.  Remove anything before this line,
# then unpack it by saving it in a file and typing "sh file".
#
# Wrapped by richmon on Sun Aug 18 18:34:10 EDT 1985
# Contents:  makefile at.doc at at.c atrun.c at.h
 
echo x - makefile
sed 's/^\@//' > "makefile" <<'\@//E*O*F makefile//'

#
# note that you should 'su bin' before typing 'make install'.
#

BIN=/usr/bin

at.exe:  at.c
	cc at.c -o at.exe

atrun: atrun.c
	cc atrun.c -o atrun

install: at.exe atrun
	cp at at.exe $(BIN)
	chmod +x $(BIN)/at
	chmod +x $(BIN)/at.exe
	cp atrun /usr/lib/crontab
	chmod 700 /usr/lib/crontab
	echo '0,15,30,45 * * * * /bin/su root -c "/usr/lib/atrun > /dev_null"' \
		>> /usr/lib/crontab
\@//E*O*F makefile//
chmod u=rw,g=r,o=r makefile
 
echo x - at.doc
sed 's/^\@//' > "at.doc" <<'\@//E*O*F at.doc//'
\@.TH at 1
\@.SH NAME
\@.PP
at - run commands via
\@.IR sh (1)
at a later time
\@.SH SYNOPSIS
\@.PP
At has two forms:
\@.HP
at time weekday [ week ] [ file ]
\@.HP
at time month day_of_month [ file ]
\@.SH DESCRIPTION
\@.PP
\@.I At
takes commands from the given file (standard input default) and runs
them via
\@.IR sh (1)
at the time specified by its arguments. Input and output are lost
unless redirected. The 
\@.I time 
argument consists of up to four digits
and an optional trailing 'A' (for AM), 'P' (for PM), 'N' (for noon)
or 'M' for midnight. If one or two digits are present, 
both are assumed to be hours; if three or four,
the first one or two are hours,
the last two minutes. If no 'A' or 'P' is supplied, twenty-four
hour time is assumed. With no further arguments, the next occurence of
the given time (later today or tomorrow) is used.
\@.PP
The
\@.I weekday
argument specifies one of the seven calendar weekdays by name; either
upper or lower case may be used, with only enough letters to uniquely
identify one. If the word
\@.I week
follows, the date is moved seven days into the future.
\@.PP
The 
\@.I month
argument specifies one of the twelve months by name, again in either
or lower case, with the following argument supplying the day of that
month during which execution is desired. If 
\@.I month
is earlier than the current month, or the same and 
\@.I day_of_month
earlier the current day, a date in the next calendar year is used.
\@.SH OPERATION
\@.PP
\@.I At
creates a file which switches to the current directory
and sets up the current environment before executing the user's
comamnds. Actual execution is carried out by /usr/lib/atrun,
invoked every so often by an entry in the file /usr/lib/crontab.
\@.SH EXAMPLES
\@.PP
at 450 file
\@.br
at 1200m mar 23
\@.br
at 8P fr week
\@.SH FILES
\@.PP
/usr/bin/at
\@.br
/usr/bin/at.exe
\@.br
/usr/spool/at/YYDDD.HHMM
\@.br
/usr/lib/atrun
\@.SH "SEE ALSO"
\@.PP
\@.IR cron (1), sh (1), crontab (4)
\@.SH DIAGNOSTICS
\@.PP
All error messages produced by 
\@.I at
appear on the user's terminal, while those produced by 
\@.I atrun
are place in the file /usr/spool/at/ATRUN.ERR.
\@.SH BUGS
\@.PP
The granularity of
\@.IR cron (1)
means that commands will be executed only within some reasonable
period (usually fifteen minutes) after the exact time given.
\@.PP
\@.I At
is very stupid about dates and
times - it does not check them for validity. Thus,
\@.IP
at 499 jan 88
\@.PP
will cause no warnings or error messages, just a file that will run
on the 88th day after January first at 99 minutes past four AM.
\@//E*O*F at.doc//
chmod u=rw,g=r,o=r at.doc
 
echo x - at
sed 's/^\@//' > "at" <<'\@//E*O*F at//'

# set up the file /usr/spool/at/temp to so that at.exe can append the user's
# commands to it and then move it to a file with a name that tells when
# it should be run. 

echo "cd `pwd`" > /usr/spool/at/temp
env | awk -F= '{ print $0; print "export "$1 }' >> /usr/spool/at/temp
\@./at.exe $*
\@//E*O*F at//
chmod u=rwx,g=rx,o=rx at
 
echo x - at.c
sed 's/^\@//' > "at.c" <<'\@//E*O*F at.c//'
#include <ctype.h>
#include <stdio.h>
#include "at.h"

	/* number of days in the months of the year */

int mon_days[12] = { 0, 31, 59, 90, 120, 150, 181, 212, 242, 273, 303, 334 };


	/* names of things */

char *mon_names[] = {
	"january",
	"february",
	"march",
	"april",
	"may",
	"june",
	"july",
	"august",
	"september",
	"october",
	"november",
	"decmeber"
};

char *day_names[] = {
	"sunday",
	"monday",
	"tuesday",
	"wednesday",
	"thursday",
	"friday",
	"saturday"
};


#define MONTH  1
#define WEEK   2

#define TEMPFILE "/usr/spool/at/temp"

	/* index into the array of either months or days of the given 
	   second argument */

int index = 0;
int fouryr, year, day, hour, minute;
int pres_day, pres_wkday;
int at_days, at_wkday;
long now_time, at_time, till_tomorrow, leftover;
char error[100];



main(argc, argv)
int argc;
char *argv[];
{
	long to_seconds();
	char temp[10];
	char filename[20];
	int type;
	
	
	if (argc < 2)
	  err_msg("usage: at time [ month [ day ] || weekday [ week ] ] [ file ]");

	now();
	at_time = to_seconds(argv[1]);

	/* the possible cases are:

		1.	at 340  [ file ]
		2.	at 340 jan 23  [ file ]
		3.  at 340 sat  [ week ] [ file ]

	   take care of each in turn below: get the number of days in the future
	   each one is (in 'at_days') and if there is a filename, place
	   it in 'filename'.  */

	filename[0] = '\0';
	if (argc == 2) {
		at_days = 0;
	}
	else if (argc == 3) {
		if (access(argv[2], 0) == 0) {
			strcpy(filename, argv[2]);	
			at_days = 0;
		}
		else {
			if (analyze(argv[2]) != WEEK)
				err_msg("invalid second argument");
			at_days = then(argv[2], "");
		}
	}
	else {
		type = analyze(argv[2]);
		at_days = then(argv[2], argv[3]);
		if ((type == WEEK) && (strcmp(argv[3], "week") != 0))
			strcpy(filename, argv[3]);
		if (argc > 4)
			strcpy(filename, argv[4]);
	}

	if ((at_days == 0) && (at_time < S_DAY - till_tomorrow))
		at_days = 1;
	if (at_days == 0)
		at_time += now_time + till_tomorrow + 1 - S_DAY;
	else {
		at_time += now_time + till_tomorrow;
		if (at_days > 1)
			at_time += S_DAY * (at_days - 1);
	}

	calstr(at_time, temp);
	makefile(temp, filename);

}

	/* create a file in the directory /usr/spool/at which is owned
	   by the current at user and contains the commands which he
	   wishes to execute via /bin/sh. already in the file are a
	   'cd' command into the current directory, and a bunch of
	   commands to set up the environment to be identical to the
	   present one. */


makefile(time, filename)
char *time, *filename;
{
	FILE *fp, *fp2;
	char t_file[20], buf[BUFLEN];

	if ((fp = fopen(TEMPFILE, "a")) == NULL)
		err_msg("internal error - call the doctor");
	if (*filename == '\0')
		fp2 = stdin;
	else
		if ((fp2 = fopen(filename, "r")) == NULL) {
			sprintf(error, "can't open %s", filename);
			err_msg(error);
		}

	while (fgets(buf, BUFLEN, fp2) != NULL)
		fputs(buf, fp);
	fclose(fp);
	fclose(fp2);

	sprintf(t_file, "/usr/spool/at/%s", time);
	if (link(TEMPFILE, t_file) < 0) {
		sprintf(error, "cannot create file %s", t_file);
		err_msg(error);
	}
	if (unlink(TEMPFILE) < 0)
		fprintf(stderr, "warning: cannot remove temporary file %s", TEMPFILE);
}
		


	/* turn the given time string, such as '356am', into the number of
	   seconds from (the previous) midnight. trailing letters can be
	   'A' for AM, 'P' for PM, 'N' for noon, 'M' for midnight. */

long
to_seconds(t_str)
char *t_str;
{
	long retval;
	char last;

	if (sscanf(t_str, "%ld", &retval) < 1)
		err_msg("invalid time of day");
	if (strlen(t_str) < 3)
		retval *= 100;
	if ((retval < 0) || (retval > 2400))
		err_msg("invalid time of day");

	last = toupper(t_str[strlen(t_str) - 1]);
	switch (last) {
	case 'A':
		/* do nothing - 24-hour time is default. */
		break;
	case 'P':
		retval += 1200;
		break;
	case 'N':
		/* again, do nothing, since 1200 is by default noon */
		break;
	case 'M':
		if (retval == 1200)
			retval = 2400;
		break;
	default:
		break;
	}

	return((retval / 100) * 3600 + (retval % 100) * 60);	
}

	/* figure out whether the given string is a month name or a day
	   name. set the global variable 'index' to the index of the
	   found item in its array, and return either MONTH or S_DAY
	   to the calling routine. if ambiguous, terminate program. */

analyze(str)
char *str;
{
	int i, j, k, max, max2, m_max;

	max = max2 = 0;

	/* first see how it compares to months */
	for (i = 0; i < 12; i++) {
		for (j = 0; mon_name[i][j] == str[j]; j++)
			;
		if (j > max) {
			max = j;
			index = i;
		}
		else if (j == max)
			max2 = max;
	}

	m_max = max;

	/* now do days */
	for (k = 0; k < 7; k++) {
		for (j = 0; day_name[k][j] == str[j]; j++)
			;
		if (j > max) {
			max = j;
			index = k;
		}
		else if (j == max)
			max2 = max;
	}

	if (max2 == max)
		err_msg("ambiguous date");
	else 
		return(m_max == max ? MONTH : WEEK);
}
		
	
		

	/* print an error message and quit */

err_msg(str)
char *str;
{
	fprintf(stderr, "at: %s\n", str);
	exit(-1);
}


	/* get the current time in seconds since Jan 1 1970, 
	   day-of-the-year, weekday and number of seconds until midnight tonight. */

now()
{
	char nowstr[10];

	now_time = time((long *) 0) - LOCAL;
	calstr(now_time, nowstr);

	pres_day = day;
	pres_wkday = ((366 * fouryr) + (365 * year) + pres_day + 6) % 7; 
	till_tomorrow = (23 - hour) * S_HOUR + (59 - minute) * S_MINUTE	+ 
		(60 - leftover);
}


	/* figure out how many S_DAYS into the future the given date is, and
	   return that number. pass argv[2] in arg2, which sohuld have either
	   a month name or a weekday name, and argv[3] in arg3, which should
	   be either a number (in the case of MONTH) or "week" (in the case of
	   S_DAY) or possibly the name of the file to be run later. */

then(arg2, arg3)
char *arg2, *arg3;
{
	int diff, day, wk_extra, leap_day;

	if (analyze(arg2) == MONTH) {
		if (sscanf(arg3, "%d", &day) < 1)
			err_msg("invalid day number following month name");
		day += mon_days[index];
		if (((year == 0) && (pres_day < 60) && (day >= 60)) ||
		    ((year == 3) && (day < pres_day) && (day >= 60)))
			leap_day = 1;
		else	
			leap_day = 0;
		if ((diff = day - pres_day) < 0)
			return((365 - diff) + leap_day);
		else
			return(diff + leap_day);
	}
	else {
		if (strcmp(arg3, "week") == 0)
			wk_extra = 7;
		else
			wk_extra = 0;
		if ((diff = index - pres_wkday) < 0)
			return((7 + diff) + wk_extra);
		else
			return(diff + wk_extra);
	}
} 


	/* given some number of seconds since 0:00 Jan 1, 1970 in the
	   parameter num, put in the parameter str the date in form
	   YYDDD.HHMM */

calstr(num, str)
long num;
char *str;
{
	
	num -= (fouryr = num / S_FOUR_YEAR) * S_FOUR_YEAR;
	num -= (year = num / S_YEAR) * S_YEAR;
	num -= (day = num / S_DAY) * S_DAY;
	num -= (hour = num / S_HOUR) * S_HOUR;
	num -= (minute = num / S_MINUTE) * S_MINUTE;
	leftover = num;

	sprintf(str, "%02d%03d.%02d%02d", 4*fouryr + year + 70, day, hour, minute);
}
\@//E*O*F at.c//
chmod u=rw,g=r,o=r at.c
 
echo x - atrun.c
sed 's/^\@//' > "atrun.c" <<'\@//E*O*F atrun.c//'
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include "at.h"


long num;
int fouryr, year, day, hour, minute, par_pid;
char error[80];
FILE *err_fp;


main()
{
	FILE *fp;
	char buf[BUFLEN];

	chdir("/usr/spool/at");
	if ((err_fp = fopen("ATRUN.ERR", "a+")) < 0)
		exit(-1);
	
	par_pid = getpid();
	system("/bin/sh -c 'cd /usr/spool/at; /bin/ls [0-9]* > temp'");
	now();
	if ((fp = fopen("temp", "r")) < 0)
		die("cannot open /usr/spool/at/temp - help");
	while (fscanf(fp, "%s", buf) != EOF) {
		process(buf);
	}
	unlink("temp");
	if (fscanf(err_fp, "%s", buf) == EOF)
		unlink("ATRUN.ERR");
		
}

	/* 'str' contains the name of a file which we check against the current
	    time. if it ought to be run, setuid and setgid, then run it by
	    exec'ing /bin/sh with the filename as its first argument. if it
	    was run, remove it afterwards. */

process(str)
char *str;
{
	char fullname[30];
	struct stat stat_buf;
	int fd, pid;

	if (!ripe(str))
		return;

	/* get the directory entry of the file in order to find the owner
	   and group ID's. if unable to get the info, just skip it. */
	if (stat(str, &stat_buf) < 0)
		return;

	sprintf(fullname, "/usr/spool/at/%s", str);
	if ((pid = fork()) < 0)
		die("unable to fork");
	else if (pid != 0) {
		wait((int *) 0);
		unlink(fullname);
	}
	else {

		/* set uid and gid, then force file descriptors 0, 1 and 2 to point
		   to /dev/null so that any output from the 'sh' process (NOT
		   the processes it is running, of course) is sent there. */

		setuid((int) stat_buf.st_uid);
		setgid((int) stat_buf.st_gid);
		fd = open("/dev/null", O_RDWR);
		close(0); close(1); close(2);
		dup(fd); dup(fd); dup(fd);
		execl("/bin/sh", "/bin/sh", fullname, 0);
	}
}


	/* return 1 if the filename indicates a time earlier than the present,
	   0 otherwise. */

#define  DECIDED(x, y) (x < y ? 1 : ((x == y) ? -1 : 0))

ripe(str)
char *str;
{
	int d, f_year, f_day, f_hour, f_minute;

	if (sscanf(str, "%2d%3d.%2d%2d", &f_year, &f_day, &f_hour, &f_minute) < 4) {
		sprintf(error, "bad file name %s", str);
		burp(error);
	}
	if ((d = DECIDED(f_year, year)) >= 0) 
		return(d);
	if ((d = DECIDED(f_day, day)) >= 0)
		return(d);
	if ((d = DECIDED(f_hour, hour)) >= 0)
		return(d);
	return(f_minute <= minute);	
}


	/* put error message in error file and quit. remove temporay
	   files if this process is the parant process called in
	   crontab, but not if ot is just a child process called
	   in a fork() earlier. */

die(str)
char *str;
{
	fprintf(err_fp, "atrun: %s\n", str);
	if (getpid() == par_pid)
		unlink("/usr/spool/at/temp");
	exit(-1);
}

	/* put message in error file but don't quit. */

burp(str)
char *str;
{
	fprintf(err_fp, "atrun: %s\n", str);
}

now()
{

	num = time((long *) 0) - LOCAL;
	num -= (fouryr = num / S_FOUR_YEAR) * S_FOUR_YEAR;
	num -= (year = num / S_YEAR) * S_YEAR;
	year += 70 + (4 * fouryr);
	num -= (day = num / S_DAY) * S_DAY;
	num -= (hour = num / S_HOUR) * S_HOUR;
	num -= (minute = num / S_MINUTE) * S_MINUTE;

}

\@//E*O*F atrun.c//
chmod u=rw,g=r,o=r atrun.c
 
echo x - at.h
sed 's/^\@//' > "at.h" <<'\@//E*O*F at.h//'


	/* number of seconds in a variety of time periods */
#define S_FOUR_YEAR   126230400
#define S_YEAR         31536000
#define S_DAY             86400
#define S_HOUR             3600
#define S_MINUTE             60

	/* correction for my time zone */
#define EST         14400
#define CST         18000
#define MST         21600
#define PST         25200

#define LOCAL       EST
#define BUFLEN      100
\@//E*O*F at.h//
chmod u=rw,g=r,o=r at.h
 
exit 0