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 cronlog(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 cronlog(LOG_ERR
, "%s: field '%s': bad syntax for a %d-%d time field\n",
375 file
, data
, min
, max
);
379 "%s: field '%s': values out of the %d-%d allowed range\n",
380 file
, data
, min
, max
);
384 void tab_parse(char *file
, char *user
)
385 /* Parse a crontab file and add its data to the tables. Handle errors by
386 * yourself. Table is owned by 'user' if non-null.
389 crontab_t
**atab
, *tab
;
390 cronjob_t
**ajob
, *job
;
398 for (atab
= &crontabs
; (tab
= *atab
) != nil
; atab
= &tab
->next
) {
399 if (strcmp(file
, tab
->file
) == 0) break;
402 /* Try to open the file. */
403 if ((fd
= open(file
, O_RDONLY
)) < 0 || fstat(fd
, &st
) < 0) {
404 if (errno
!= ENOENT
) {
405 cronlog(LOG_ERR
, "%s: %s\n", file
, strerror(errno
));
407 if (fd
!= -1) close(fd
);
411 /* Forget it if the file is awfully big. */
412 if (st
.st_size
> TAB_MAX
) {
413 cronlog(LOG_ERR
, "%s: %lu bytes is bigger than my %lu limit\n",
415 (unsigned long) st
.st_size
,
416 (unsigned long) TAB_MAX
);
420 /* If the file is the same as before then don't bother. */
421 if (tab
!= nil
&& st
.st_mtime
== tab
->mtime
) {
427 /* Create a new table structure. */
428 tab
= allocate(sizeof(*tab
));
429 tab
->file
= allocate((strlen(file
) + 1) * sizeof(tab
->file
[0]));
430 strcpy(tab
->file
, file
);
433 tab
->user
= allocate((strlen(user
) + 1) * sizeof(tab
->user
[0]));
434 strcpy(tab
->user
, user
);
436 tab
->data
= allocate((st
.st_size
+ 1) * sizeof(tab
->data
[0]));
438 tab
->mtime
= st
.st_mtime
;
443 /* Pull a new table in core. */
445 while (n
< st
.st_size
) {
446 if ((r
= read(fd
, tab
->data
+ n
, st
.st_size
- n
)) < 0) {
447 cronlog(LOG_CRIT
, "%s: %s", file
, strerror(errno
));
456 if (strlen(tab
->data
) < n
) {
457 cronlog(LOG_ERR
, "%s contains a null character\n", file
);
461 /* Parse the file. */
465 while (ok
&& *p
!= 0) {
467 if (*q
== '#' || q
== p
) {
468 /* Comment or empty. */
469 while (*p
!= 0 && *p
++ != '\n') {}
473 /* One new job coming up. */
474 *ajob
= job
= allocate(sizeof(*job
));
475 *(ajob
= &job
->next
)= nil
;
478 if (!range_parse(file
, q
, job
->min
, 0, 59, &wc
)) {
484 if (!range_parse(file
, q
, job
->hour
, 0, 23, &wc
)) {
490 if (!range_parse(file
, q
, job
->mday
, 1, 31, &wc
)) {
497 if (!range_parse(file
, q
, job
->mon
, 1, 12, &wc
)) {
504 if (!range_parse(file
, q
, job
->wday
, 0, 7, &wc
)) {
510 /* 7 is Sunday, but 0 is a common mistake because it is in the
511 * tm_wday range. We allow and even prefer it internally.
513 if (bit_isset(job
->wday
, 7)) {
514 bit_clr(job
->wday
, 7);
515 bit_set(job
->wday
, 0);
518 /* The month range is 1-12, but tm_mon likes 0-11. */
520 if (bit_isset(job
->mon
, 8)) bit_set(job
->mon
, 7);
523 /* Scan for options. */
525 while (q
= get_token(&p
), *q
== '-') {
527 if (q
[0] == '-' && q
+1 == p
) {
532 while (q
< p
) switch (*q
++) {
534 if (q
== p
) q
= get_token(&p
);
535 if (q
== p
) goto usage
;
536 memmove(q
-1, q
, p
-q
); /* gross... */
544 "%s: bad option -%c, good options are: -u username\n",
551 /* A crontab owned by a user can only do things as that user. */
552 if (tab
->user
!= nil
) job
->user
= tab
->user
;
554 /* Inspect the first character of the command. */
556 if (q
== p
|| *q
== '#') {
557 /* Rest of the line is empty, i.e. the commands are on
558 * the following lines indented by one tab.
560 while (*p
!= 0 && *p
++ != '\n') {}
563 "%s: contains an empty command\n",
569 if ((*q
= *p
++) == '\n') {
570 if (*p
!= '\t') break;
576 /* The command is on this line. Alas we must now be
577 * backwards compatible and change %'s to newlines.
581 if ((*q
= *p
++) == '\n') break;
582 if (*q
== '%') *q
= '\n';
588 job
->late
= 0; /* It is on time. */
589 job
->atjob
= 0; /* True cron job. */
590 job
->pid
= IDLE_PID
; /* Not running yet. */
591 tab_reschedule(job
); /* Compute next time to run. */
595 if (ok
) tab
->current
= 1;
598 void tab_find_atjob(char *atdir
)
599 /* Find the first to be executed AT job and kludge up an crontab job for it.
600 * We set tab->file to "atdir/jobname", tab->data to "atdir/past/jobname",
601 * and job->cmd to "jobname".
605 struct dirent
*entry
;
607 struct tm tmnow
, tmt1
;
608 static char template[] = "96.365.1546.00";
609 char firstjob
[sizeof(template)];
614 if ((spool
= opendir(atdir
)) == nil
) return;
616 tmnow
= *localtime(&now
);
619 while ((entry
= readdir(spool
)) != nil
) {
620 /* Check if the name fits the template. */
621 for (i
= 0; template[i
] != 0; i
++) {
622 if (template[i
] == '.') {
623 if (entry
->d_name
[i
] != '.') break;
625 if (!isdigit(entry
->d_name
[i
])) break;
628 if (template[i
] != 0 || entry
->d_name
[i
] != 0) continue;
630 /* Convert the name to a time. Careful with the century. */
631 memset(&tmt1
, 0, sizeof(tmt1
));
632 tmt1
.tm_year
= atoi(entry
->d_name
+0);
633 while (tmt1
.tm_year
< tmnow
.tm_year
-10) tmt1
.tm_year
+= 100;
634 tmt1
.tm_mday
= 1+atoi(entry
->d_name
+3);
635 tmt1
.tm_min
= atoi(entry
->d_name
+7);
636 tmt1
.tm_hour
= tmt1
.tm_min
/ 100;
639 if ((t1
= mktime(&tmt1
)) == -1) {
640 /* Illegal time? Try in winter time. */
642 if ((t1
= mktime(&tmt1
)) == -1) continue;
646 strcpy(firstjob
, entry
->d_name
);
651 if (t0
== NEVER
) return; /* AT job spool is empty. */
653 /* Create new table and job structures. */
654 tab
= allocate(sizeof(*tab
));
655 tab
->file
= allocate((strlen(atdir
) + 1 + sizeof(template))
656 * sizeof(tab
->file
[0]));
657 strcpy(tab
->file
, atdir
);
658 strcat(tab
->file
, "/");
659 strcat(tab
->file
, firstjob
);
660 tab
->data
= allocate((strlen(atdir
) + 6 + sizeof(template))
661 * sizeof(tab
->data
[0]));
662 strcpy(tab
->data
, atdir
);
663 strcat(tab
->data
, "/past/");
664 strcat(tab
->data
, firstjob
);
671 tab
->jobs
= job
= allocate(sizeof(*job
));
675 job
->cmd
= tab
->data
+ strlen(atdir
) + 6;
678 job
->atjob
= 1; /* One AT job disguised as a cron job. */
681 if (job
->rtime
< next
) next
= job
->rtime
;
685 /* Remove table data that is no longer current. E.g. a crontab got removed.
688 crontab_t
**atab
, *tab
;
692 while ((tab
= *atab
) != nil
) {
698 /* Table is not, or no longer valid; delete. */
700 while ((job
= tab
->jobs
) != nil
) {
701 tab
->jobs
= job
->next
;
704 deallocate(tab
->data
);
705 deallocate(tab
->file
);
706 deallocate(tab
->user
);
712 static cronjob_t
*reap_or_find(pid_t pid
)
713 /* Find a finished job or search for the next one to run. */
721 for (tab
= crontabs
; tab
!= nil
; tab
= tab
->next
) {
722 for (job
= tab
->jobs
; job
!= nil
; job
= job
->next
) {
723 if (job
->pid
== pid
) {
727 if (job
->pid
!= IDLE_PID
) continue;
728 if (job
->rtime
< next
) next
= job
->rtime
;
729 if (job
->rtime
<= now
) nextjob
= job
;
735 void tab_reap_job(pid_t pid
)
736 /* A job has finished. Try to find it among the crontab data and reschedule
737 * it. Recompute time next to run a job.
740 (void) reap_or_find(pid
);
743 cronjob_t
*tab_nextjob(void)
744 /* Find a job that should be run now. If none are found return null.
748 return reap_or_find(NO_PID
);
751 static void pr_map(FILE *fp
, bitmap_t map
)
753 int last_bit
= -1, bit
;
757 for (bit
= 0; bit
< 64; bit
++) {
758 if (bit_isset(map
, bit
)) {
759 if (last_bit
== -1) last_bit
= bit
;
761 if (last_bit
!= -1) {
762 fprintf(fp
, "%s%d", sep
, last_bit
);
763 if (last_bit
!= bit
-1) {
764 fprintf(fp
, "-%d", bit
-1);
773 void tab_print(FILE *fp
)
774 /* Print out a stored crontab file for debugging purposes. */
781 for (tab
= crontabs
; tab
!= nil
; tab
= tab
->next
) {
782 fprintf(fp
, "tab->file = \"%s\"\n", tab
->file
);
783 fprintf(fp
, "tab->user = \"%s\"\n",
784 tab
->user
== nil
? "(root)" : tab
->user
);
785 fprintf(fp
, "tab->mtime = %s", ctime(&tab
->mtime
));
787 for (job
= tab
->jobs
; job
!= nil
; job
= job
->next
) {
789 fprintf(fp
, "AT job");
791 pr_map(fp
, job
->min
); fputc(' ', fp
);
792 pr_map(fp
, job
->hour
); fputc(' ', fp
);
793 pr_map(fp
, job
->mday
); fputc(' ', fp
);
794 pr_map(fp
, job
->mon
); fputc(' ', fp
);
795 pr_map(fp
, job
->wday
);
797 if (job
->user
!= nil
&& job
->user
!= tab
->user
) {
798 fprintf(fp
, " -u %s", job
->user
);
801 for (p
= job
->cmd
; *p
!= 0; p
++) {
803 if (*p
== '\n') fputc('\t', fp
);
806 tm
= *localtime(&job
->rtime
);
807 fprintf(fp
, " rtime = %.24s%s\n", asctime(&tm
),
808 tm
.tm_isdst
? " (DST)" : "");
809 if (job
->pid
!= IDLE_PID
) {
810 fprintf(fp
, " pid = %ld\n", (long) job
->pid
);
817 * $PchId: tab.c,v 1.5 2000/07/25 22:07:51 philip Exp $