1 /* tab.c - process crontabs and create in-core crontab data
5 * 17 Jul 2000 by Philip Homburg
6 * - Tab_reschedule() rewritten (and fixed).
24 static int nextbit(bitmap_t map
, int bit
)
25 /* Return the next bit set in 'map' from 'bit' onwards, cyclic. */
29 if (bit_isset(map
, bit
)) break;
34 void tab_reschedule(cronjob_t
*job
)
35 /* Reschedule one job. Compute the next time to run the job in job->rtime.
38 struct tm prevtm
, nexttm
, tmptm
;
39 time_t nodst_rtime
, dst_rtime
;
41 /* AT jobs are only run once. */
42 if (job
->atjob
) { job
->rtime
= NEVER
; return; }
44 /* Was the job scheduled late last time? */
45 if (job
->late
) job
->rtime
= now
;
47 prevtm
= *localtime(&job
->rtime
);
51 nexttm
.tm_min
++; /* Minimal increment */
55 if (nexttm
.tm_min
> 59)
60 if (nexttm
.tm_hour
> 23)
66 if (nexttm
.tm_mday
> 31)
68 nexttm
.tm_hour
= nexttm
.tm_min
= 0;
72 if (nexttm
.tm_mon
>= 12)
74 nexttm
.tm_hour
= nexttm
.tm_min
= 0;
80 /* Verify tm_year. A crontab entry cannot restrict tm_year
81 * directly. However, certain dates (such as Feb, 29th) do
82 * not occur every year. We limit the difference between
83 * nexttm.tm_year and prevtm.tm_year to detect impossible dates
84 * (e.g, Feb, 31st). In theory every date occurs within a
85 * period of 4 years. However, some years at the end of a
86 * century are not leap years (e.g, the year 2100). An extra
87 * factor of 2 should be enough.
89 if (nexttm
.tm_year
-prevtm
.tm_year
> 2*4)
92 return; /* Impossible combination */
97 /* Verify the mon and mday fields. If do_wday and
98 * do_mday are both true we have to merge both
99 * schedules. This is done after the call to mktime.
101 if (!bit_isset(job
->mon
, nexttm
.tm_mon
))
103 /* Clear other fields */
105 nexttm
.tm_hour
= nexttm
.tm_min
= 0;
112 if (!bit_isset(job
->mday
, nexttm
.tm_mday
))
114 /* Clear other fields */
115 nexttm
.tm_hour
= nexttm
.tm_min
= 0;
123 if (!bit_isset(job
->hour
, nexttm
.tm_hour
))
125 /* Clear tm_min field */
133 if (!bit_isset(job
->min
, nexttm
.tm_min
))
139 /* Verify that we don't have any problem with DST. Try
140 * tm_isdst=0 first. */
145 "tab_reschedule: trying %04d-%02d-%02d %02d:%02d:%02d isdst=0\n",
146 1900+nexttm
.tm_year
, nexttm
.tm_mon
+1,
147 nexttm
.tm_mday
, nexttm
.tm_hour
,
148 nexttm
.tm_min
, nexttm
.tm_sec
);
150 nodst_rtime
= job
->rtime
= mktime(&tmptm
);
151 if (job
->rtime
== -1) {
152 /* This should not happen. */
154 "mktime failed for %04d-%02d-%02d %02d:%02d:%02d",
155 1900+nexttm
.tm_year
, nexttm
.tm_mon
+1,
156 nexttm
.tm_mday
, nexttm
.tm_hour
,
157 nexttm
.tm_min
, nexttm
.tm_sec
);
161 tmptm
= *localtime(&job
->rtime
);
162 if (tmptm
.tm_hour
!= nexttm
.tm_hour
||
163 tmptm
.tm_min
!= nexttm
.tm_min
)
165 assert(tmptm
.tm_isdst
);
170 "tab_reschedule: trying %04d-%02d-%02d %02d:%02d:%02d isdst=1\n",
171 1900+nexttm
.tm_year
, nexttm
.tm_mon
+1,
172 nexttm
.tm_mday
, nexttm
.tm_hour
,
173 nexttm
.tm_min
, nexttm
.tm_sec
);
175 dst_rtime
= job
->rtime
= mktime(&tmptm
);
176 if (job
->rtime
== -1) {
177 /* This should not happen. */
179 "mktime failed for %04d-%02d-%02d %02d:%02d:%02d\n",
180 1900+nexttm
.tm_year
, nexttm
.tm_mon
+1,
181 nexttm
.tm_mday
, nexttm
.tm_hour
,
182 nexttm
.tm_min
, nexttm
.tm_sec
);
186 tmptm
= *localtime(&job
->rtime
);
187 if (tmptm
.tm_hour
!= nexttm
.tm_hour
||
188 tmptm
.tm_min
!= nexttm
.tm_min
)
190 assert(!tmptm
.tm_isdst
);
191 /* We have a problem. This time neither
192 * exists with DST nor without DST.
193 * Use the latest time, which should be
196 assert(nodst_rtime
> dst_rtime
);
197 job
->rtime
= nodst_rtime
;
200 "During DST trans. %04d-%02d-%02d %02d:%02d:%02d\n",
201 1900+nexttm
.tm_year
, nexttm
.tm_mon
+1,
202 nexttm
.tm_mday
, nexttm
.tm_hour
,
203 nexttm
.tm_min
, nexttm
.tm_sec
);
208 /* Verify this the combination (tm_year, tm_mon, tm_mday). */
209 if (tmptm
.tm_mday
!= nexttm
.tm_mday
||
210 tmptm
.tm_mon
!= nexttm
.tm_mon
||
211 tmptm
.tm_year
!= nexttm
.tm_year
)
215 fprintf(stderr
, "Wrong day\n");
217 nexttm
.tm_hour
= nexttm
.tm_min
= 0;
223 if (job
->do_wday
&& bit_isset(job
->wday
, tmptm
.tm_wday
))
225 /* OK, wday matched */
230 if (job
->do_mday
&& bit_isset(job
->mon
, tmptm
.tm_mon
) &&
231 bit_isset(job
->mday
, tmptm
.tm_mday
))
233 /* OK, mon and mday matched */
237 if (!job
->do_wday
&& !job
->do_mday
)
239 /* No need to match wday and mday */
245 fprintf(stderr
, "Wrong mon+mday or wday\n");
247 nexttm
.tm_hour
= nexttm
.tm_min
= 0;
251 fprintf(stderr
, "Using %04d-%02d-%02d %02d:%02d:%02d \n",
252 1900+nexttm
.tm_year
, nexttm
.tm_mon
+1, nexttm
.tm_mday
,
253 nexttm
.tm_hour
, nexttm
.tm_min
, nexttm
.tm_sec
);
254 tmptm
= *localtime(&job
->rtime
);
255 fprintf(stderr
, "Act. %04d-%02d-%02d %02d:%02d:%02d isdst=%d\n",
256 1900+tmptm
.tm_year
, tmptm
.tm_mon
+1, tmptm
.tm_mday
,
257 tmptm
.tm_hour
, tmptm
.tm_min
, tmptm
.tm_sec
,
262 /* Is job issuing lagging behind with the progress of time? */
263 job
->late
= (job
->rtime
< now
);
265 /* The result is in job->rtime. */
266 if (job
->rtime
< next
) next
= job
->rtime
;
269 #define isdigit(c) ((unsigned) ((c) - '0') < 10)
271 static char *get_token(char **ptr
)
272 /* Return a pointer to the next token in a string. Move *ptr to the end of
273 * the token, and return a pointer to the start. If *ptr == start of token
274 * then we're stuck against a newline or end of string.
280 while (*p
== ' ' || *p
== '\t') p
++;
283 while (*p
!= ' ' && *p
!= '\t' && *p
!= '\n' && *p
!= 0) p
++;
288 static int range_parse(char *file
, char *data
, bitmap_t map
,
289 int min
, int max
, int *wildcard
)
290 /* Parse a comma separated series of 'n', 'n-m' or 'n:m' numbers. 'n'
291 * includes number 'n' in the bit map, 'n-m' includes all numbers between
292 * 'n' and 'm' inclusive, and 'n:m' includes 'n+k*m' for any k if in range.
293 * Numbers must fall between 'min' and 'max'. A '*' means all numbers. A
294 * '?' is allowed as a synonym for the current minute, which only makes sense
295 * in the minute field, i.e. max must be 59. Return true iff parsed ok.
302 /* Clear all bits. */
303 for (n
= 0; n
< 8; n
++) map
[n
]= 0;
306 while (*p
!= ' ' && *p
!= '\t' && *p
!= '\n' && *p
!= 0) p
++;
312 log(LOG_ERR
, "%s: not enough time fields\n", file
);
317 if (p
[0] == '*' && p
[1] == 0) {
318 for (n
= min
; n
<= max
; n
++) bit_set(map
, n
);
325 /* Parse a comma separated series of numbers or ranges. */
327 if (*p
== '?' && max
== 59 && p
[1] != '-') {
328 n
= localtime(&now
)->tm_min
;
331 if (!isdigit(*p
)) goto syntax
;
334 n
= 10 * n
+ (*p
++ - '0');
335 if (n
> max
) goto range
;
336 } while (isdigit(*p
));
338 if (n
< min
) goto range
;
340 if (*p
== '-') { /* A range of the form 'n-m'? */
342 if (!isdigit(*p
)) goto syntax
;
345 m
= 10 * m
+ (*p
++ - '0');
346 if (m
> max
) goto range
;
347 } while (isdigit(*p
));
348 if (m
< n
) goto range
;
353 if (*p
== ':') { /* A repeat of the form 'n:m'? */
355 if (!isdigit(*p
)) goto syntax
;
358 m
= 10 * m
+ (*p
++ - '0');
359 if (m
> (max
-min
+1)) goto range
;
360 } while (isdigit(*p
));
361 if (m
== 0) goto range
;
362 while (n
>= min
) n
-= m
;
363 while ((n
+= m
) <= max
) bit_set(map
, n
);
365 /* Simply a number */
369 if (*p
++ != ',') goto syntax
;
374 log(LOG_ERR
, "%s: field '%s': bad syntax for a %d-%d time field\n",
375 file
, data
, min
, max
);
378 log(LOG_ERR
, "%s: field '%s': values out of the %d-%d allowed range\n",
379 file
, data
, min
, max
);
383 void tab_parse(char *file
, char *user
)
384 /* Parse a crontab file and add its data to the tables. Handle errors by
385 * yourself. Table is owned by 'user' if non-null.
388 crontab_t
**atab
, *tab
;
389 cronjob_t
**ajob
, *job
;
397 for (atab
= &crontabs
; (tab
= *atab
) != nil
; atab
= &tab
->next
) {
398 if (strcmp(file
, tab
->file
) == 0) break;
401 /* Try to open the file. */
402 if ((fd
= open(file
, O_RDONLY
)) < 0 || fstat(fd
, &st
) < 0) {
403 if (errno
!= ENOENT
) {
404 log(LOG_ERR
, "%s: %s\n", file
, strerror(errno
));
406 if (fd
!= -1) close(fd
);
410 /* Forget it if the file is awfully big. */
411 if (st
.st_size
> TAB_MAX
) {
412 log(LOG_ERR
, "%s: %lu bytes is bigger than my %lu limit\n",
414 (unsigned long) st
.st_size
,
415 (unsigned long) TAB_MAX
);
419 /* If the file is the same as before then don't bother. */
420 if (tab
!= nil
&& st
.st_mtime
== tab
->mtime
) {
426 /* Create a new table structure. */
427 tab
= allocate(sizeof(*tab
));
428 tab
->file
= allocate((strlen(file
) + 1) * sizeof(tab
->file
[0]));
429 strcpy(tab
->file
, file
);
432 tab
->user
= allocate((strlen(user
) + 1) * sizeof(tab
->user
[0]));
433 strcpy(tab
->user
, user
);
435 tab
->data
= allocate((st
.st_size
+ 1) * sizeof(tab
->data
[0]));
437 tab
->mtime
= st
.st_mtime
;
442 /* Pull a new table in core. */
444 while (n
< st
.st_size
) {
445 if ((r
= read(fd
, tab
->data
+ n
, st
.st_size
- n
)) < 0) {
446 log(LOG_CRIT
, "%s: %s", file
, strerror(errno
));
455 if (strlen(tab
->data
) < n
) {
456 log(LOG_ERR
, "%s contains a null character\n", file
);
460 /* Parse the file. */
464 while (ok
&& *p
!= 0) {
466 if (*q
== '#' || q
== p
) {
467 /* Comment or empty. */
468 while (*p
!= 0 && *p
++ != '\n') {}
472 /* One new job coming up. */
473 *ajob
= job
= allocate(sizeof(*job
));
474 *(ajob
= &job
->next
)= nil
;
477 if (!range_parse(file
, q
, job
->min
, 0, 59, &wc
)) {
483 if (!range_parse(file
, q
, job
->hour
, 0, 23, &wc
)) {
489 if (!range_parse(file
, q
, job
->mday
, 1, 31, &wc
)) {
496 if (!range_parse(file
, q
, job
->mon
, 1, 12, &wc
)) {
503 if (!range_parse(file
, q
, job
->wday
, 0, 7, &wc
)) {
509 /* 7 is Sunday, but 0 is a common mistake because it is in the
510 * tm_wday range. We allow and even prefer it internally.
512 if (bit_isset(job
->wday
, 7)) {
513 bit_clr(job
->wday
, 7);
514 bit_set(job
->wday
, 0);
517 /* The month range is 1-12, but tm_mon likes 0-11. */
519 if (bit_isset(job
->mon
, 8)) bit_set(job
->mon
, 7);
522 /* Scan for options. */
524 while (q
= get_token(&p
), *q
== '-') {
526 if (q
[0] == '-' && q
+1 == p
) {
531 while (q
< p
) switch (*q
++) {
533 if (q
== p
) q
= get_token(&p
);
534 if (q
== p
) goto usage
;
535 memmove(q
-1, q
, p
-q
); /* gross... */
543 "%s: bad option -%c, good options are: -u username\n",
550 /* A crontab owned by a user can only do things as that user. */
551 if (tab
->user
!= nil
) job
->user
= tab
->user
;
553 /* Inspect the first character of the command. */
555 if (q
== p
|| *q
== '#') {
556 /* Rest of the line is empty, i.e. the commands are on
557 * the following lines indented by one tab.
559 while (*p
!= 0 && *p
++ != '\n') {}
561 log(LOG_ERR
, "%s: contains an empty command\n",
567 if ((*q
= *p
++) == '\n') {
568 if (*p
!= '\t') break;
574 /* The command is on this line. Alas we must now be
575 * backwards compatible and change %'s to newlines.
579 if ((*q
= *p
++) == '\n') break;
580 if (*q
== '%') *q
= '\n';
586 job
->late
= 0; /* It is on time. */
587 job
->atjob
= 0; /* True cron job. */
588 job
->pid
= IDLE_PID
; /* Not running yet. */
589 tab_reschedule(job
); /* Compute next time to run. */
593 if (ok
) tab
->current
= 1;
596 void tab_find_atjob(char *atdir
)
597 /* Find the first to be executed AT job and kludge up an crontab job for it.
598 * We set tab->file to "atdir/jobname", tab->data to "atdir/past/jobname",
599 * and job->cmd to "jobname".
603 struct dirent
*entry
;
605 struct tm tmnow
, tmt1
;
606 static char template[] = "96.365.1546.00";
607 char firstjob
[sizeof(template)];
612 if ((spool
= opendir(atdir
)) == nil
) return;
614 tmnow
= *localtime(&now
);
617 while ((entry
= readdir(spool
)) != nil
) {
618 /* Check if the name fits the template. */
619 for (i
= 0; template[i
] != 0; i
++) {
620 if (template[i
] == '.') {
621 if (entry
->d_name
[i
] != '.') break;
623 if (!isdigit(entry
->d_name
[i
])) break;
626 if (template[i
] != 0 || entry
->d_name
[i
] != 0) continue;
628 /* Convert the name to a time. Careful with the century. */
629 memset(&tmt1
, 0, sizeof(tmt1
));
630 tmt1
.tm_year
= atoi(entry
->d_name
+0);
631 while (tmt1
.tm_year
< tmnow
.tm_year
-10) tmt1
.tm_year
+= 100;
632 tmt1
.tm_mday
= 1+atoi(entry
->d_name
+3);
633 tmt1
.tm_min
= atoi(entry
->d_name
+7);
634 tmt1
.tm_hour
= tmt1
.tm_min
/ 100;
637 if ((t1
= mktime(&tmt1
)) == -1) {
638 /* Illegal time? Try in winter time. */
640 if ((t1
= mktime(&tmt1
)) == -1) continue;
644 strcpy(firstjob
, entry
->d_name
);
649 if (t0
== NEVER
) return; /* AT job spool is empty. */
651 /* Create new table and job structures. */
652 tab
= allocate(sizeof(*tab
));
653 tab
->file
= allocate((strlen(atdir
) + 1 + sizeof(template))
654 * sizeof(tab
->file
[0]));
655 strcpy(tab
->file
, atdir
);
656 strcat(tab
->file
, "/");
657 strcat(tab
->file
, firstjob
);
658 tab
->data
= allocate((strlen(atdir
) + 6 + sizeof(template))
659 * sizeof(tab
->data
[0]));
660 strcpy(tab
->data
, atdir
);
661 strcat(tab
->data
, "/past/");
662 strcat(tab
->data
, firstjob
);
669 tab
->jobs
= job
= allocate(sizeof(*job
));
673 job
->cmd
= tab
->data
+ strlen(atdir
) + 6;
676 job
->atjob
= 1; /* One AT job disguised as a cron job. */
679 if (job
->rtime
< next
) next
= job
->rtime
;
683 /* Remove table data that is no longer current. E.g. a crontab got removed.
686 crontab_t
**atab
, *tab
;
690 while ((tab
= *atab
) != nil
) {
696 /* Table is not, or no longer valid; delete. */
698 while ((job
= tab
->jobs
) != nil
) {
699 tab
->jobs
= job
->next
;
702 deallocate(tab
->data
);
703 deallocate(tab
->file
);
704 deallocate(tab
->user
);
710 static cronjob_t
*reap_or_find(pid_t pid
)
711 /* Find a finished job or search for the next one to run. */
719 for (tab
= crontabs
; tab
!= nil
; tab
= tab
->next
) {
720 for (job
= tab
->jobs
; job
!= nil
; job
= job
->next
) {
721 if (job
->pid
== pid
) {
725 if (job
->pid
!= IDLE_PID
) continue;
726 if (job
->rtime
< next
) next
= job
->rtime
;
727 if (job
->rtime
<= now
) nextjob
= job
;
733 void tab_reap_job(pid_t pid
)
734 /* A job has finished. Try to find it among the crontab data and reschedule
735 * it. Recompute time next to run a job.
738 (void) reap_or_find(pid
);
741 cronjob_t
*tab_nextjob(void)
742 /* Find a job that should be run now. If none are found return null.
746 return reap_or_find(NO_PID
);
749 static void pr_map(FILE *fp
, bitmap_t map
)
751 int last_bit
= -1, bit
;
755 for (bit
= 0; bit
< 64; bit
++) {
756 if (bit_isset(map
, bit
)) {
757 if (last_bit
== -1) last_bit
= bit
;
759 if (last_bit
!= -1) {
760 fprintf(fp
, "%s%d", sep
, last_bit
);
761 if (last_bit
!= bit
-1) {
762 fprintf(fp
, "-%d", bit
-1);
771 void tab_print(FILE *fp
)
772 /* Print out a stored crontab file for debugging purposes. */
779 for (tab
= crontabs
; tab
!= nil
; tab
= tab
->next
) {
780 fprintf(fp
, "tab->file = \"%s\"\n", tab
->file
);
781 fprintf(fp
, "tab->user = \"%s\"\n",
782 tab
->user
== nil
? "(root)" : tab
->user
);
783 fprintf(fp
, "tab->mtime = %s", ctime(&tab
->mtime
));
785 for (job
= tab
->jobs
; job
!= nil
; job
= job
->next
) {
787 fprintf(fp
, "AT job");
789 pr_map(fp
, job
->min
); fputc(' ', fp
);
790 pr_map(fp
, job
->hour
); fputc(' ', fp
);
791 pr_map(fp
, job
->mday
); fputc(' ', fp
);
792 pr_map(fp
, job
->mon
); fputc(' ', fp
);
793 pr_map(fp
, job
->wday
);
795 if (job
->user
!= nil
&& job
->user
!= tab
->user
) {
796 fprintf(fp
, " -u %s", job
->user
);
799 for (p
= job
->cmd
; *p
!= 0; p
++) {
801 if (*p
== '\n') fputc('\t', fp
);
804 tm
= *localtime(&job
->rtime
);
805 fprintf(fp
, " rtime = %.24s%s\n", asctime(&tm
),
806 tm
.tm_isdst
? " (DST)" : "");
807 if (job
->pid
!= IDLE_PID
) {
808 fprintf(fp
, " pid = %ld\n", (long) job
->pid
);
815 * $PchId: tab.c,v 1.5 2000/07/25 22:07:51 philip Exp $