minix3/commands/cron/tab.c

817 lines
19 KiB
C

/* tab.c - process crontabs and create in-core crontab data
* Author: Kees J. Bot
* 7 Dec 1996
* Changes:
* 17 Jul 2000 by Philip Homburg
* - Tab_reschedule() rewritten (and fixed).
*/
#define nil ((void*)0)
#include <sys/types.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <limits.h>
#include <time.h>
#include <dirent.h>
#include <sys/stat.h>
#include "misc.h"
#include "tab.h"
static int nextbit(bitmap_t map, int bit)
/* Return the next bit set in 'map' from 'bit' onwards, cyclic. */
{
for (;;) {
bit= (bit+1) & 63;
if (bit_isset(map, bit)) break;
}
return bit;
}
void tab_reschedule(cronjob_t *job)
/* Reschedule one job. Compute the next time to run the job in job->rtime.
*/
{
struct tm prevtm, nexttm, tmptm;
time_t nodst_rtime, dst_rtime;
/* AT jobs are only run once. */
if (job->atjob) { job->rtime= NEVER; return; }
/* Was the job scheduled late last time? */
if (job->late) job->rtime= now;
prevtm= *localtime(&job->rtime);
prevtm.tm_sec= 0;
nexttm= prevtm;
nexttm.tm_min++; /* Minimal increment */
for (;;)
{
if (nexttm.tm_min > 59)
{
nexttm.tm_min= 0;
nexttm.tm_hour++;
}
if (nexttm.tm_hour > 23)
{
nexttm.tm_min= 0;
nexttm.tm_hour= 0;
nexttm.tm_mday++;
}
if (nexttm.tm_mday > 31)
{
nexttm.tm_hour= nexttm.tm_min= 0;
nexttm.tm_mday= 1;
nexttm.tm_mon++;
}
if (nexttm.tm_mon >= 12)
{
nexttm.tm_hour= nexttm.tm_min= 0;
nexttm.tm_mday= 1;
nexttm.tm_mon= 0;
nexttm.tm_year++;
}
/* Verify tm_year. A crontab entry cannot restrict tm_year
* directly. However, certain dates (such as Feb, 29th) do
* not occur every year. We limit the difference between
* nexttm.tm_year and prevtm.tm_year to detect impossible dates
* (e.g, Feb, 31st). In theory every date occurs within a
* period of 4 years. However, some years at the end of a
* century are not leap years (e.g, the year 2100). An extra
* factor of 2 should be enough.
*/
if (nexttm.tm_year-prevtm.tm_year > 2*4)
{
job->rtime= NEVER;
return; /* Impossible combination */
}
if (!job->do_wday)
{
/* Verify the mon and mday fields. If do_wday and
* do_mday are both true we have to merge both
* schedules. This is done after the call to mktime.
*/
if (!bit_isset(job->mon, nexttm.tm_mon))
{
/* Clear other fields */
nexttm.tm_mday= 1;
nexttm.tm_hour= nexttm.tm_min= 0;
nexttm.tm_mon++;
continue;
}
/* Verify mday */
if (!bit_isset(job->mday, nexttm.tm_mday))
{
/* Clear other fields */
nexttm.tm_hour= nexttm.tm_min= 0;
nexttm.tm_mday++;
continue;
}
}
/* Verify hour */
if (!bit_isset(job->hour, nexttm.tm_hour))
{
/* Clear tm_min field */
nexttm.tm_min= 0;
nexttm.tm_hour++;
continue;
}
/* Verify min */
if (!bit_isset(job->min, nexttm.tm_min))
{
nexttm.tm_min++;
continue;
}
/* Verify that we don't have any problem with DST. Try
* tm_isdst=0 first. */
tmptm= nexttm;
tmptm.tm_isdst= 0;
#if 0
fprintf(stderr,
"tab_reschedule: trying %04d-%02d-%02d %02d:%02d:%02d isdst=0\n",
1900+nexttm.tm_year, nexttm.tm_mon+1,
nexttm.tm_mday, nexttm.tm_hour,
nexttm.tm_min, nexttm.tm_sec);
#endif
nodst_rtime= job->rtime= mktime(&tmptm);
if (job->rtime == -1) {
/* This should not happen. */
log(LOG_ERR,
"mktime failed for %04d-%02d-%02d %02d:%02d:%02d",
1900+nexttm.tm_year, nexttm.tm_mon+1,
nexttm.tm_mday, nexttm.tm_hour,
nexttm.tm_min, nexttm.tm_sec);
job->rtime= NEVER;
return;
}
tmptm= *localtime(&job->rtime);
if (tmptm.tm_hour != nexttm.tm_hour ||
tmptm.tm_min != nexttm.tm_min)
{
assert(tmptm.tm_isdst);
tmptm= nexttm;
tmptm.tm_isdst= 1;
#if 0
fprintf(stderr,
"tab_reschedule: trying %04d-%02d-%02d %02d:%02d:%02d isdst=1\n",
1900+nexttm.tm_year, nexttm.tm_mon+1,
nexttm.tm_mday, nexttm.tm_hour,
nexttm.tm_min, nexttm.tm_sec);
#endif
dst_rtime= job->rtime= mktime(&tmptm);
if (job->rtime == -1) {
/* This should not happen. */
log(LOG_ERR,
"mktime failed for %04d-%02d-%02d %02d:%02d:%02d\n",
1900+nexttm.tm_year, nexttm.tm_mon+1,
nexttm.tm_mday, nexttm.tm_hour,
nexttm.tm_min, nexttm.tm_sec);
job->rtime= NEVER;
return;
}
tmptm= *localtime(&job->rtime);
if (tmptm.tm_hour != nexttm.tm_hour ||
tmptm.tm_min != nexttm.tm_min)
{
assert(!tmptm.tm_isdst);
/* We have a problem. This time neither
* exists with DST nor without DST.
* Use the latest time, which should be
* nodst_rtime.
*/
assert(nodst_rtime > dst_rtime);
job->rtime= nodst_rtime;
#if 0
fprintf(stderr,
"During DST trans. %04d-%02d-%02d %02d:%02d:%02d\n",
1900+nexttm.tm_year, nexttm.tm_mon+1,
nexttm.tm_mday, nexttm.tm_hour,
nexttm.tm_min, nexttm.tm_sec);
#endif
}
}
/* Verify this the combination (tm_year, tm_mon, tm_mday). */
if (tmptm.tm_mday != nexttm.tm_mday ||
tmptm.tm_mon != nexttm.tm_mon ||
tmptm.tm_year != nexttm.tm_year)
{
/* Wrong day */
#if 0
fprintf(stderr, "Wrong day\n");
#endif
nexttm.tm_hour= nexttm.tm_min= 0;
nexttm.tm_mday++;
continue;
}
/* Check tm_wday */
if (job->do_wday && bit_isset(job->wday, tmptm.tm_wday))
{
/* OK, wday matched */
break;
}
/* Check tm_mday */
if (job->do_mday && bit_isset(job->mon, tmptm.tm_mon) &&
bit_isset(job->mday, tmptm.tm_mday))
{
/* OK, mon and mday matched */
break;
}
if (!job->do_wday && !job->do_mday)
{
/* No need to match wday and mday */
break;
}
/* Wrong day */
#if 0
fprintf(stderr, "Wrong mon+mday or wday\n");
#endif
nexttm.tm_hour= nexttm.tm_min= 0;
nexttm.tm_mday++;
}
#if 0
fprintf(stderr, "Using %04d-%02d-%02d %02d:%02d:%02d \n",
1900+nexttm.tm_year, nexttm.tm_mon+1, nexttm.tm_mday,
nexttm.tm_hour, nexttm.tm_min, nexttm.tm_sec);
tmptm= *localtime(&job->rtime);
fprintf(stderr, "Act. %04d-%02d-%02d %02d:%02d:%02d isdst=%d\n",
1900+tmptm.tm_year, tmptm.tm_mon+1, tmptm.tm_mday,
tmptm.tm_hour, tmptm.tm_min, tmptm.tm_sec,
tmptm.tm_isdst);
#endif
/* Is job issuing lagging behind with the progress of time? */
job->late= (job->rtime < now);
/* The result is in job->rtime. */
if (job->rtime < next) next= job->rtime;
}
#define isdigit(c) ((unsigned) ((c) - '0') < 10)
static char *get_token(char **ptr)
/* Return a pointer to the next token in a string. Move *ptr to the end of
* the token, and return a pointer to the start. If *ptr == start of token
* then we're stuck against a newline or end of string.
*/
{
char *start, *p;
p= *ptr;
while (*p == ' ' || *p == '\t') p++;
start= p;
while (*p != ' ' && *p != '\t' && *p != '\n' && *p != 0) p++;
*ptr= p;
return start;
}
static int range_parse(char *file, char *data, bitmap_t map,
int min, int max, int *wildcard)
/* Parse a comma separated series of 'n', 'n-m' or 'n:m' numbers. 'n'
* includes number 'n' in the bit map, 'n-m' includes all numbers between
* 'n' and 'm' inclusive, and 'n:m' includes 'n+k*m' for any k if in range.
* Numbers must fall between 'min' and 'max'. A '*' means all numbers. A
* '?' is allowed as a synonym for the current minute, which only makes sense
* in the minute field, i.e. max must be 59. Return true iff parsed ok.
*/
{
char *p;
int end;
int n, m;
/* Clear all bits. */
for (n= 0; n < 8; n++) map[n]= 0;
p= data;
while (*p != ' ' && *p != '\t' && *p != '\n' && *p != 0) p++;
end= *p;
*p= 0;
p= data;
if (*p == 0) {
log(LOG_ERR, "%s: not enough time fields\n", file);
return 0;
}
/* Is it a '*'? */
if (p[0] == '*' && p[1] == 0) {
for (n= min; n <= max; n++) bit_set(map, n);
p[1]= end;
*wildcard= 1;
return 1;
}
*wildcard= 0;
/* Parse a comma separated series of numbers or ranges. */
for (;;) {
if (*p == '?' && max == 59 && p[1] != '-') {
n= localtime(&now)->tm_min;
p++;
} else {
if (!isdigit(*p)) goto syntax;
n= 0;
do {
n= 10 * n + (*p++ - '0');
if (n > max) goto range;
} while (isdigit(*p));
}
if (n < min) goto range;
if (*p == '-') { /* A range of the form 'n-m'? */
p++;
if (!isdigit(*p)) goto syntax;
m= 0;
do {
m= 10 * m + (*p++ - '0');
if (m > max) goto range;
} while (isdigit(*p));
if (m < n) goto range;
do {
bit_set(map, n);
} while (++n <= m);
} else
if (*p == ':') { /* A repeat of the form 'n:m'? */
p++;
if (!isdigit(*p)) goto syntax;
m= 0;
do {
m= 10 * m + (*p++ - '0');
if (m > (max-min+1)) goto range;
} while (isdigit(*p));
if (m == 0) goto range;
while (n >= min) n-= m;
while ((n+= m) <= max) bit_set(map, n);
} else {
/* Simply a number */
bit_set(map, n);
}
if (*p == 0) break;
if (*p++ != ',') goto syntax;
}
*p= end;
return 1;
syntax:
log(LOG_ERR, "%s: field '%s': bad syntax for a %d-%d time field\n",
file, data, min, max);
return 0;
range:
log(LOG_ERR, "%s: field '%s': values out of the %d-%d allowed range\n",
file, data, min, max);
return 0;
}
void tab_parse(char *file, char *user)
/* Parse a crontab file and add its data to the tables. Handle errors by
* yourself. Table is owned by 'user' if non-null.
*/
{
crontab_t **atab, *tab;
cronjob_t **ajob, *job;
int fd;
struct stat st;
char *p, *q;
size_t n;
ssize_t r;
int ok, wc;
for (atab= &crontabs; (tab= *atab) != nil; atab= &tab->next) {
if (strcmp(file, tab->file) == 0) break;
}
/* Try to open the file. */
if ((fd= open(file, O_RDONLY)) < 0 || fstat(fd, &st) < 0) {
if (errno != ENOENT) {
log(LOG_ERR, "%s: %s\n", file, strerror(errno));
}
if (fd != -1) close(fd);
return;
}
/* Forget it if the file is awfully big. */
if (st.st_size > TAB_MAX) {
log(LOG_ERR, "%s: %lu bytes is bigger than my %lu limit\n",
file,
(unsigned long) st.st_size,
(unsigned long) TAB_MAX);
return;
}
/* If the file is the same as before then don't bother. */
if (tab != nil && st.st_mtime == tab->mtime) {
close(fd);
tab->current= 1;
return;
}
/* Create a new table structure. */
tab= allocate(sizeof(*tab));
tab->file= allocate((strlen(file) + 1) * sizeof(tab->file[0]));
strcpy(tab->file, file);
tab->user= nil;
if (user != nil) {
tab->user= allocate((strlen(user) + 1) * sizeof(tab->user[0]));
strcpy(tab->user, user);
}
tab->data= allocate((st.st_size + 1) * sizeof(tab->data[0]));
tab->jobs= nil;
tab->mtime= st.st_mtime;
tab->current= 0;
tab->next= *atab;
*atab= tab;
/* Pull a new table in core. */
n= 0;
while (n < st.st_size) {
if ((r = read(fd, tab->data + n, st.st_size - n)) < 0) {
log(LOG_CRIT, "%s: %s", file, strerror(errno));
close(fd);
return;
}
if (r == 0) break;
n+= r;
}
close(fd);
tab->data[n]= 0;
if (strlen(tab->data) < n) {
log(LOG_ERR, "%s contains a null character\n", file);
return;
}
/* Parse the file. */
ajob= &tab->jobs;
p= tab->data;
ok= 1;
while (ok && *p != 0) {
q= get_token(&p);
if (*q == '#' || q == p) {
/* Comment or empty. */
while (*p != 0 && *p++ != '\n') {}
continue;
}
/* One new job coming up. */
*ajob= job= allocate(sizeof(*job));
*(ajob= &job->next)= nil;
job->tab= tab;
if (!range_parse(file, q, job->min, 0, 59, &wc)) {
ok= 0;
break;
}
q= get_token(&p);
if (!range_parse(file, q, job->hour, 0, 23, &wc)) {
ok= 0;
break;
}
q= get_token(&p);
if (!range_parse(file, q, job->mday, 1, 31, &wc)) {
ok= 0;
break;
}
job->do_mday= !wc;
q= get_token(&p);
if (!range_parse(file, q, job->mon, 1, 12, &wc)) {
ok= 0;
break;
}
job->do_mday |= !wc;
q= get_token(&p);
if (!range_parse(file, q, job->wday, 0, 7, &wc)) {
ok= 0;
break;
}
job->do_wday= !wc;
/* 7 is Sunday, but 0 is a common mistake because it is in the
* tm_wday range. We allow and even prefer it internally.
*/
if (bit_isset(job->wday, 7)) {
bit_clr(job->wday, 7);
bit_set(job->wday, 0);
}
/* The month range is 1-12, but tm_mon likes 0-11. */
job->mon[0] >>= 1;
if (bit_isset(job->mon, 8)) bit_set(job->mon, 7);
job->mon[1] >>= 1;
/* Scan for options. */
job->user= nil;
while (q= get_token(&p), *q == '-') {
q++;
if (q[0] == '-' && q+1 == p) {
/* -- */
q= get_token(&p);
break;
}
while (q < p) switch (*q++) {
case 'u':
if (q == p) q= get_token(&p);
if (q == p) goto usage;
memmove(q-1, q, p-q); /* gross... */
p[-1]= 0;
job->user= q-1;
q= p;
break;
default:
usage:
log(LOG_ERR,
"%s: bad option -%c, good options are: -u username\n",
file, q[-1]);
ok= 0;
goto endtab;
}
}
/* A crontab owned by a user can only do things as that user. */
if (tab->user != nil) job->user= tab->user;
/* Inspect the first character of the command. */
job->cmd= q;
if (q == p || *q == '#') {
/* Rest of the line is empty, i.e. the commands are on
* the following lines indented by one tab.
*/
while (*p != 0 && *p++ != '\n') {}
if (*p++ != '\t') {
log(LOG_ERR, "%s: contains an empty command\n",
file);
ok= 0;
goto endtab;
}
while (*p != 0) {
if ((*q = *p++) == '\n') {
if (*p != '\t') break;
p++;
}
q++;
}
} else {
/* The command is on this line. Alas we must now be
* backwards compatible and change %'s to newlines.
*/
p= q;
while (*p != 0) {
if ((*q = *p++) == '\n') break;
if (*q == '%') *q= '\n';
q++;
}
}
*q= 0;
job->rtime= now;
job->late= 0; /* It is on time. */
job->atjob= 0; /* True cron job. */
job->pid= IDLE_PID; /* Not running yet. */
tab_reschedule(job); /* Compute next time to run. */
}
endtab:
if (ok) tab->current= 1;
}
void tab_find_atjob(char *atdir)
/* Find the first to be executed AT job and kludge up an crontab job for it.
* We set tab->file to "atdir/jobname", tab->data to "atdir/past/jobname",
* and job->cmd to "jobname".
*/
{
DIR *spool;
struct dirent *entry;
time_t t0, t1;
struct tm tmnow, tmt1;
static char template[] = "96.365.1546.00";
char firstjob[sizeof(template)];
int i;
crontab_t *tab;
cronjob_t *job;
if ((spool= opendir(atdir)) == nil) return;
tmnow= *localtime(&now);
t0= NEVER;
while ((entry= readdir(spool)) != nil) {
/* Check if the name fits the template. */
for (i= 0; template[i] != 0; i++) {
if (template[i] == '.') {
if (entry->d_name[i] != '.') break;
} else {
if (!isdigit(entry->d_name[i])) break;
}
}
if (template[i] != 0 || entry->d_name[i] != 0) continue;
/* Convert the name to a time. Careful with the century. */
memset(&tmt1, 0, sizeof(tmt1));
tmt1.tm_year= atoi(entry->d_name+0);
while (tmt1.tm_year < tmnow.tm_year-10) tmt1.tm_year+= 100;
tmt1.tm_mday= 1+atoi(entry->d_name+3);
tmt1.tm_min= atoi(entry->d_name+7);
tmt1.tm_hour= tmt1.tm_min / 100;
tmt1.tm_min%= 100;
tmt1.tm_isdst= -1;
if ((t1= mktime(&tmt1)) == -1) {
/* Illegal time? Try in winter time. */
tmt1.tm_isdst= 0;
if ((t1= mktime(&tmt1)) == -1) continue;
}
if (t1 < t0) {
t0= t1;
strcpy(firstjob, entry->d_name);
}
}
closedir(spool);
if (t0 == NEVER) return; /* AT job spool is empty. */
/* Create new table and job structures. */
tab= allocate(sizeof(*tab));
tab->file= allocate((strlen(atdir) + 1 + sizeof(template))
* sizeof(tab->file[0]));
strcpy(tab->file, atdir);
strcat(tab->file, "/");
strcat(tab->file, firstjob);
tab->data= allocate((strlen(atdir) + 6 + sizeof(template))
* sizeof(tab->data[0]));
strcpy(tab->data, atdir);
strcat(tab->data, "/past/");
strcat(tab->data, firstjob);
tab->user= nil;
tab->mtime= 0;
tab->current= 1;
tab->next= crontabs;
crontabs= tab;
tab->jobs= job= allocate(sizeof(*job));
job->next= nil;
job->tab= tab;
job->user= nil;
job->cmd= tab->data + strlen(atdir) + 6;
job->rtime= t0;
job->late= 0;
job->atjob= 1; /* One AT job disguised as a cron job. */
job->pid= IDLE_PID;
if (job->rtime < next) next= job->rtime;
}
void tab_purge(void)
/* Remove table data that is no longer current. E.g. a crontab got removed.
*/
{
crontab_t **atab, *tab;
cronjob_t *job;
atab= &crontabs;
while ((tab= *atab) != nil) {
if (tab->current) {
/* Table is fine. */
tab->current= 0;
atab= &tab->next;
} else {
/* Table is not, or no longer valid; delete. */
*atab= tab->next;
while ((job= tab->jobs) != nil) {
tab->jobs= job->next;
deallocate(job);
}
deallocate(tab->data);
deallocate(tab->file);
deallocate(tab->user);
deallocate(tab);
}
}
}
static cronjob_t *reap_or_find(pid_t pid)
/* Find a finished job or search for the next one to run. */
{
crontab_t *tab;
cronjob_t *job;
cronjob_t *nextjob;
nextjob= nil;
next= NEVER;
for (tab= crontabs; tab != nil; tab= tab->next) {
for (job= tab->jobs; job != nil; job= job->next) {
if (job->pid == pid) {
job->pid= IDLE_PID;
tab_reschedule(job);
}
if (job->pid != IDLE_PID) continue;
if (job->rtime < next) next= job->rtime;
if (job->rtime <= now) nextjob= job;
}
}
return nextjob;
}
void tab_reap_job(pid_t pid)
/* A job has finished. Try to find it among the crontab data and reschedule
* it. Recompute time next to run a job.
*/
{
(void) reap_or_find(pid);
}
cronjob_t *tab_nextjob(void)
/* Find a job that should be run now. If none are found return null.
* Update 'next'.
*/
{
return reap_or_find(NO_PID);
}
static void pr_map(FILE *fp, bitmap_t map)
{
int last_bit= -1, bit;
char *sep;
sep= "";
for (bit= 0; bit < 64; bit++) {
if (bit_isset(map, bit)) {
if (last_bit == -1) last_bit= bit;
} else {
if (last_bit != -1) {
fprintf(fp, "%s%d", sep, last_bit);
if (last_bit != bit-1) {
fprintf(fp, "-%d", bit-1);
}
last_bit= -1;
sep= ",";
}
}
}
}
void tab_print(FILE *fp)
/* Print out a stored crontab file for debugging purposes. */
{
crontab_t *tab;
cronjob_t *job;
char *p;
struct tm tm;
for (tab= crontabs; tab != nil; tab= tab->next) {
fprintf(fp, "tab->file = \"%s\"\n", tab->file);
fprintf(fp, "tab->user = \"%s\"\n",
tab->user == nil ? "(root)" : tab->user);
fprintf(fp, "tab->mtime = %s", ctime(&tab->mtime));
for (job= tab->jobs; job != nil; job= job->next) {
if (job->atjob) {
fprintf(fp, "AT job");
} else {
pr_map(fp, job->min); fputc(' ', fp);
pr_map(fp, job->hour); fputc(' ', fp);
pr_map(fp, job->mday); fputc(' ', fp);
pr_map(fp, job->mon); fputc(' ', fp);
pr_map(fp, job->wday);
}
if (job->user != nil && job->user != tab->user) {
fprintf(fp, " -u %s", job->user);
}
fprintf(fp, "\n\t");
for (p= job->cmd; *p != 0; p++) {
fputc(*p, fp);
if (*p == '\n') fputc('\t', fp);
}
fputc('\n', fp);
tm= *localtime(&job->rtime);
fprintf(fp, " rtime = %.24s%s\n", asctime(&tm),
tm.tm_isdst ? " (DST)" : "");
if (job->pid != IDLE_PID) {
fprintf(fp, " pid = %ld\n", (long) job->pid);
}
}
}
}
/*
* $PchId: tab.c,v 1.5 2000/07/25 22:07:51 philip Exp $
*/