/* 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 #include #include #include #include #include #include #include #include #include #include #include #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 $ */