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