Added extra/crond.service for systemd
[dcron.git] / database.c
blobdd971ceb614b3fe7ab713a86be4980c2530f159b
2 /*
3 * DATABASE.C
5 * Copyright 1994 Matthew Dillon (dillon@apollo.backplane.com)
6 * Copyright 2009-2011 James Pryor <profjim@jimpryor.net>
7 * May be distributed under the GNU General Public License
8 */
10 #include "defs.h"
12 Prototype void CheckUpdates(const char *dpath, const char *user_override, time_t t1, time_t t2);
13 Prototype void SynchronizeDir(const char *dpath, const char *user_override, int initial_scan);
14 Prototype void ReadTimestamps(const char *user);
15 Prototype int TestJobs(time_t t1, time_t t2);
16 Prototype int TestStartupJobs(void);
17 Prototype int ArmJob(CronFile *file, CronLine *line, time_t t1, time_t t2);
18 Prototype void RunJobs(void);
19 Prototype int CheckJobs(void);
21 void SynchronizeFile(const char *dpath, const char *fname, const char *uname);
22 void DeleteFile(CronFile **pfile);
23 char *ParseInterval(int *interval, char *ptr);
24 char *ParseField(char *userName, char *ary, int modvalue, int off, int onvalue, const char **names, char *ptr);
25 void FixDayDow(CronLine *line);
27 CronFile *FileBase = NULL;
29 const char *DowAry[] = {
30 "sun",
31 "mon",
32 "tue",
33 "wed",
34 "thu",
35 "fri",
36 "sat",
38 "Sun",
39 "Mon",
40 "Tue",
41 "Wed",
42 "Thu",
43 "Fri",
44 "Sat",
45 NULL
48 const char *MonAry[] = {
49 "jan",
50 "feb",
51 "mar",
52 "apr",
53 "may",
54 "jun",
55 "jul",
56 "aug",
57 "sep",
58 "oct",
59 "nov",
60 "dec",
62 "Jan",
63 "Feb",
64 "Mar",
65 "Apr",
66 "May",
67 "Jun",
68 "Jul",
69 "Aug",
70 "Sep",
71 "Oct",
72 "Nov",
73 "Dec",
74 NULL
77 const char *FreqAry[] = {
78 "noauto",
79 "reboot",
80 "hourly",
81 "daily",
82 "weekly",
83 "monthly",
84 "yearly",
85 NULL
89 * Check the cron.update file in the specified directory. If user_override
90 * is NULL then the files in the directory belong to the user whose name is
91 * the file, otherwise they belong to the user_override user.
93 void
94 CheckUpdates(const char *dpath, const char *user_override, time_t t1, time_t t2)
96 FILE *fi;
97 char buf[SMALL_BUFFER];
98 char *fname, *ptok, *job;
99 char *path;
101 if (!(path = concat(dpath, "/", CRONUPDATE, NULL))) {
102 errno = ENOMEM;
103 perror("CheckUpdates");
104 exit(1);
106 if ((fi = fopen(path, "r")) != NULL) {
107 remove(path);
108 printlogf(LOG_INFO, "reading %s/%s\n", dpath, CRONUPDATE);
109 while (fgets(buf, sizeof(buf), fi) != NULL) {
111 * if buf has only sep chars, return NULL and point ptok at buf's terminating 0
112 * else return pointer to first non-sep of buf and
113 * if there's a following sep, overwrite it to 0 and point ptok to next char
114 * else point ptok at buf's terminating 0
116 fname = strtok_r(buf, " \t\n", &ptok);
118 if (user_override)
119 SynchronizeFile(dpath, fname, user_override);
120 else if (!getpwnam(fname))
121 printlogf(LOG_WARNING, "ignoring %s/%s (non-existent user)\n", dpath, fname);
122 else if (*ptok == 0 || *ptok == '\n') {
123 SynchronizeFile(dpath, fname, fname);
124 ReadTimestamps(fname);
125 } else {
126 /* if fname is followed by whitespace, we prod any following jobs */
127 CronFile *file = FileBase;
128 while (file) {
129 if (strcmp(file->cf_UserName, fname) == 0)
130 break;
131 file = file->cf_Next;
133 if (!file)
134 printlogf(LOG_WARNING, "unable to prod for user %s: no crontab\n", fname);
135 else {
136 CronLine *line;
137 /* calling strtok(ptok...) then strtok(NULL) is equiv to calling strtok_r(NULL,..&ptok) */
138 while ((job = strtok(ptok, " \t\n")) != NULL) {
139 time_t force = t2;
140 ptok = NULL;
141 if (*job == '!') {
142 force = (time_t)-1;
143 ++job;
145 line = file->cf_LineBase;
146 while (line) {
147 if (line->cl_JobName && strcmp(line->cl_JobName, job) == 0)
148 break;
149 line = line->cl_Next;
151 if (line)
152 ArmJob(file, line, t1, force);
153 else {
154 printlogf(LOG_WARNING, "unable to prod for user %s: unknown job %s\n", fname, job);
155 /* we can continue parsing this line, we just don't install any CronWaiter for the requested job */
161 fclose(fi);
163 free(path);
166 void
167 SynchronizeDir(const char *dpath, const char *user_override, int initial_scan)
169 CronFile **pfile;
170 CronFile *file;
171 struct dirent *den;
172 DIR *dir;
173 char *path;
175 if (DebugOpt)
176 printlogf(LOG_DEBUG, "Synchronizing %s\n", dpath);
179 * Delete all database CronFiles for this directory. DeleteFile() will
180 * free *pfile and relink the *pfile pointer, or in the alternative will
181 * mark it as deleted.
183 pfile = &FileBase;
184 while ((file = *pfile) != NULL) {
185 if (file->cf_Deleted == 0 && strcmp(file->cf_DPath, dpath) == 0) {
186 DeleteFile(pfile);
187 } else {
188 pfile = &file->cf_Next;
193 * Since we are resynchronizing the entire directory, remove the
194 * the CRONUPDATE file.
196 if (!(path = concat(dpath, "/", CRONUPDATE, NULL))) {
197 errno = ENOMEM;
198 perror("SynchronizeDir");
199 exit(1);
201 remove(path);
202 free(path);
205 * Scan the specified directory
207 if ((dir = opendir(dpath)) != NULL) {
208 while ((den = readdir(dir)) != NULL) {
209 if (strchr(den->d_name, '.') != NULL)
210 continue;
211 if (strcmp(den->d_name, CRONUPDATE) == 0)
212 continue;
213 if (user_override) {
214 SynchronizeFile(dpath, den->d_name, user_override);
215 } else if (getpwnam(den->d_name)) {
216 SynchronizeFile(dpath, den->d_name, den->d_name);
217 } else {
218 printlogf(LOG_WARNING, "ignoring %s/%s (non-existent user)\n",
219 dpath, den->d_name);
222 closedir(dir);
223 } else {
224 if (initial_scan)
225 printlogf(LOG_ERR, "unable to scan directory %s\n", dpath);
226 /* softerror, do not exit the program */
231 void
232 ReadTimestamps(const char *user)
234 CronFile *file;
235 CronLine *line;
236 FILE *fi;
237 char buf[SMALL_BUFFER];
238 char *ptr;
239 struct tm tm = {0};
240 time_t sec, freq;
242 file = FileBase;
243 while (file != NULL) {
244 if (file->cf_Deleted == 0 && (!user || strcmp(user, file->cf_UserName) == 0)) {
245 line = file->cf_LineBase;
246 while (line != NULL) {
247 if (line->cl_Timestamp) {
248 if ((fi = fopen(line->cl_Timestamp, "r")) != NULL) {
249 if (fgets(buf, sizeof(buf), fi) != NULL) {
250 int fake = 0;
251 ptr = buf;
252 if (strncmp(buf, "after ", 6) == 0) {
253 fake = 1;
254 ptr += 6;
256 sec = (time_t)-1;
257 ptr = strptime(ptr, CRONSTAMP_FMT, &tm);
258 if (ptr && (*ptr == 0 || *ptr == '\n'))
259 /* strptime uses current seconds when seconds not specified? anyway, we don't get round minutes */
260 tm.tm_sec = 0;
261 tm.tm_isdst = -1;
262 sec = mktime(&tm);
263 if (sec == (time_t)-1) {
264 printlogf(LOG_ERR, "unable to parse timestamp (user %s job %s)\n", file->cf_UserName, line->cl_JobName);
265 /* we continue checking other timestamps in this CronFile */
266 } else {
267 /* sec -= sec % 60; */
268 if (fake) {
269 line->cl_NotUntil = sec;
270 } else {
271 line->cl_LastRan = sec;
272 freq = (line->cl_Freq > 0) ? line->cl_Freq : line->cl_Delay;
273 /* if (line->cl_NotUntil < line->cl_LastRan + freq) */
274 line->cl_NotUntil = line->cl_LastRan + freq;
278 fclose(fi);
279 } else {
280 int succeeded = 0;
281 printlogf(LOG_NOTICE, "no timestamp found (user %s job %s)\n", file->cf_UserName, line->cl_JobName);
282 /* write a fake timestamp file so our initial NotUntil doesn't keep being reset every hour when crond does a SynchronizeDir */
283 if ((fi = fopen(line->cl_Timestamp, "w")) != NULL) {
284 if (strftime(buf, sizeof(buf), CRONSTAMP_FMT, localtime(&line->cl_NotUntil)))
285 if (fputs("after ", fi) >= 0)
286 if (fputs(buf,fi) >= 0)
287 succeeded = 1;
288 fclose(fi);
290 if (!succeeded)
291 printlogf(LOG_WARNING, "unable to write timestamp to %s (user %s %s)\n", line->cl_Timestamp, file->cf_UserName, line->cl_Description);
294 line = line->cl_Next;
297 file = file->cf_Next;
301 void
302 SynchronizeFile(const char *dpath, const char *fileName, const char *userName)
304 CronFile **pfile;
305 CronFile *file;
306 int maxEntries;
307 int maxLines;
308 char buf[RW_BUFFER]; /* max length for crontab lines */
309 char *path;
310 FILE *fi;
313 * Limit entries
315 if (strcmp(userName, "root") == 0)
316 maxEntries = 65535;
317 else
318 maxEntries = MAXLINES;
319 maxLines = maxEntries * 10;
322 * Delete any existing copy of this CronFile
324 pfile = &FileBase;
325 while ((file = *pfile) != NULL) {
326 if (file->cf_Deleted == 0 && strcmp(file->cf_DPath, dpath) == 0 &&
327 strcmp(file->cf_FileName, fileName) == 0
329 DeleteFile(pfile);
330 } else {
331 pfile = &file->cf_Next;
335 if (!(path = concat(dpath, "/", fileName, NULL))) {
336 errno = ENOMEM;
337 perror("SynchronizeFile");
338 exit(1);
340 if ((fi = fopen(path, "r")) != NULL) {
341 struct stat sbuf;
343 if (fstat(fileno(fi), &sbuf) == 0 && sbuf.st_uid == DaemonUid) {
344 CronFile *file = calloc(1, sizeof(CronFile));
345 CronLine **pline;
346 time_t tnow = time(NULL);
347 tnow -= tnow % 60;
349 file->cf_UserName = strdup(userName);
350 file->cf_FileName = strdup(fileName);
351 file->cf_DPath = strdup(dpath);
352 pline = &file->cf_LineBase;
354 /* fgets reads at most size-1 chars until \n or EOF, then adds a\0; \n if present is stored in buf */
355 while (fgets(buf, sizeof(buf), fi) != NULL && --maxLines) {
356 CronLine line;
357 char *ptr = buf;
358 int len;
360 while (*ptr == ' ' || *ptr == '\t' || *ptr == '\n')
361 ++ptr;
363 len = strlen(ptr);
364 if (len && ptr[len-1] == '\n')
365 ptr[--len] = 0;
367 if (*ptr == 0 || *ptr == '#')
368 continue;
370 if (--maxEntries == 0)
371 break;
373 memset(&line, 0, sizeof(line));
375 if (DebugOpt)
376 printlogf(LOG_DEBUG, "User %s Entry %s\n", userName, buf);
378 if (*ptr == '@') {
380 * parse @hourly, etc
382 int j;
383 line.cl_Delay = -1;
384 ptr += 1;
385 for (j = 0; FreqAry[j]; ++j) {
386 if (strncmp(ptr, FreqAry[j], strlen(FreqAry[j])) == 0) {
387 break;
390 if (FreqAry[j]) {
391 ptr += strlen(FreqAry[j]);
392 switch(j) {
393 case 0:
394 /* noauto */
395 line.cl_Freq = -2;
396 line.cl_Delay = 0;
397 break;
398 case 1:
399 /* reboot */
400 line.cl_Freq = -1;
401 line.cl_Delay = 0;
402 break;
403 case 2:
404 line.cl_Freq = HOURLY_FREQ;
405 break;
406 case 3:
407 line.cl_Freq = DAILY_FREQ;
408 break;
409 case 4:
410 line.cl_Freq = WEEKLY_FREQ;
411 break;
412 case 5:
413 line.cl_Freq = MONTHLY_FREQ;
414 break;
415 case 6:
416 line.cl_Freq = YEARLY_FREQ;
417 break;
418 /* else line.cl_Freq will remain 0 */
422 if (!line.cl_Freq || (*ptr != ' ' && *ptr != '\t')) {
423 printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s\n", userName, buf);
424 continue;
427 if (line.cl_Delay < 0) {
429 * delays on @daily, @hourly, etc are 1/20 of the frequency
430 * so they don't all start at once
431 * this also affects how they behave when the job returns EAGAIN
433 line.cl_Delay = line.cl_Freq / 20;
434 line.cl_Delay -= line.cl_Delay % 60;
435 if (line.cl_Delay == 0)
436 line.cl_Delay = 60;
437 /* all minutes are permitted */
438 for (j=0; j<60; ++j)
439 line.cl_Mins[j] = 1;
440 for (j=0; j<24; ++j)
441 line.cl_Hrs[j] = 1;
442 for (j=1; j<32; ++j)
443 /* days are numbered 1..31 */
444 line.cl_Days[j] = 1;
445 for (j=0; j<12; ++j)
446 line.cl_Mons[j] = 1;
449 while (*ptr == ' ' || *ptr == '\t')
450 ++ptr;
452 } else {
454 * parse date ranges
457 ptr = ParseField(file->cf_UserName, line.cl_Mins, 60, 0, 1,
458 NULL, ptr);
459 ptr = ParseField(file->cf_UserName, line.cl_Hrs, 24, 0, 1,
460 NULL, ptr);
461 ptr = ParseField(file->cf_UserName, line.cl_Days, 32, 0, 1,
462 NULL, ptr);
463 ptr = ParseField(file->cf_UserName, line.cl_Mons, 12, -1, 1,
464 MonAry, ptr);
465 ptr = ParseField(file->cf_UserName, line.cl_Dow, 7, 0, 31,
466 DowAry, ptr);
468 * check failure
471 if (ptr == NULL)
472 continue;
475 * fix days and dow - if one is not * and the other
476 * is *, the other is set to 0, and vise-versa
479 FixDayDow(&line);
482 /* check for ID=... and AFTER=... and FREQ=... */
483 do {
484 if (strncmp(ptr, ID_TAG, strlen(ID_TAG)) == 0) {
485 if (line.cl_JobName) {
486 /* only assign ID_TAG once */
487 printlogf(LOG_WARNING, "failed parsing crontab for user %s: repeated %s\n", userName, ptr);
488 ptr = NULL;
489 } else {
490 ptr += strlen(ID_TAG);
492 * name = strsep(&ptr, seps):
493 * return name = ptr, and if ptr contains sep chars, overwrite first with 0 and point ptr to next char
494 * else set ptr=NULL
496 if (!(line.cl_Description = concat("job ", strsep(&ptr, " \t"), NULL))) {
497 errno = ENOMEM;
498 perror("SynchronizeFile");
499 exit(1);
501 line.cl_JobName = line.cl_Description + 4;
502 if (!ptr)
503 printlogf(LOG_WARNING, "failed parsing crontab for user %s: no command after %s%s\n", userName, ID_TAG, line.cl_JobName);
505 } else if (strncmp(ptr, FREQ_TAG, strlen(FREQ_TAG)) == 0) {
506 if (line.cl_Freq) {
507 /* only assign FREQ_TAG once */
508 printlogf(LOG_WARNING, "failed parsing crontab for user %s: repeated %s\n", userName, ptr);
509 ptr = NULL;
510 } else {
511 char *base = ptr;
512 ptr += strlen(FREQ_TAG);
513 ptr = ParseInterval(&line.cl_Freq, ptr);
514 if (ptr && *ptr == '/')
515 ptr = ParseInterval(&line.cl_Delay, ++ptr);
516 else
517 line.cl_Delay = line.cl_Freq;
518 if (!ptr) {
519 printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s\n", userName, base);
520 } else if (*ptr != ' ' && *ptr != '\t') {
521 printlogf(LOG_WARNING, "failed parsing crontab for user %s: no command after %s\n", userName, base);
522 ptr = NULL;
525 } else if (strncmp(ptr, WAIT_TAG, strlen(WAIT_TAG)) == 0) {
526 if (line.cl_Waiters) {
527 /* only assign WAIT_TAG once */
528 printlogf(LOG_WARNING, "failed parsing crontab for user %s: repeated %s\n", userName, ptr);
529 ptr = NULL;
530 } else {
531 short more = 1;
532 char *name;
533 ptr += strlen(WAIT_TAG);
534 do {
535 CronLine *job, **pjob;
536 if (strcspn(ptr,",") < strcspn(ptr," \t"))
537 name = strsep(&ptr, ",");
538 else {
539 more = 0;
540 name = strsep(&ptr, " \t");
542 if (!ptr || *ptr == 0) {
543 /* unexpectedly this was the last token in buf; so abort */
544 printlogf(LOG_WARNING, "failed parsing crontab for user %s: no command after %s%s\n", userName, WAIT_TAG, name);
545 ptr = NULL;
546 } else {
547 int waitfor = 0;
548 char *w, *wsave;
549 if ((w = strchr(name, '/')) != NULL) {
550 wsave = w++;
551 w = ParseInterval(&waitfor, w);
552 if (!w || *w != 0) {
553 printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s%s\n", userName, WAIT_TAG, name);
554 ptr = NULL;
555 } else
556 /* truncate name */
557 *wsave = 0;
559 if (ptr) {
560 /* look for a matching CronLine */
561 pjob = &file->cf_LineBase;
562 while ((job = *pjob) != NULL) {
563 if (job->cl_JobName && strcmp(job->cl_JobName, name) == 0) {
564 CronWaiter *waiter = malloc(sizeof(CronWaiter));
565 CronNotifier *notif = malloc(sizeof(CronNotifier));
566 waiter->cw_Flag = -1;
567 waiter->cw_MaxWait = waitfor;
568 waiter->cw_NotifLine = job;
569 waiter->cw_Notifier = notif;
570 waiter->cw_Next = line.cl_Waiters; /* add to head of line.cl_Waiters */
571 line.cl_Waiters = waiter;
572 notif->cn_Waiter = waiter;
573 notif->cn_Next = job->cl_Notifs; /* add to head of job->cl_Notifs */
574 job->cl_Notifs = notif;
575 break;
576 } else
577 pjob = &job->cl_Next;
579 if (!job) {
580 printlogf(LOG_WARNING, "failed parsing crontab for user %s: unknown job %s\n", userName, name);
581 /* we can continue parsing this line, we just don't install any CronWaiter for the requested job */
585 } while (ptr && more);
587 } else
588 break;
589 if (!ptr)
590 break;
591 while (*ptr == ' ' || *ptr == '\t')
592 ++ptr;
593 } while (!line.cl_JobName || !line.cl_Waiters || !line.cl_Freq);
595 if (line.cl_JobName && (!ptr || *line.cl_JobName == 0)) {
596 /* we're aborting, or ID= was empty */
597 free(line.cl_Description);
598 line.cl_Description = NULL;
599 line.cl_JobName = NULL;
601 if (ptr && line.cl_Delay > 0 && !line.cl_JobName) {
602 printlogf(LOG_WARNING, "failed parsing crontab for user %s: writing timestamp requires job %s to be named\n", userName, ptr);
603 ptr = NULL;
605 if (!ptr) {
606 /* couldn't parse so we abort; free any cl_Waiters */
607 if (line.cl_Waiters) {
608 CronWaiter **pwaiters, *waiters;
609 pwaiters = &line.cl_Waiters;
610 while ((waiters = *pwaiters) != NULL) {
611 *pwaiters = waiters->cw_Next;
612 /* leave the Notifier allocated but disabled */
613 waiters->cw_Notifier->cn_Waiter = NULL;
614 free(waiters);
617 continue;
619 /* now we've added any ID=... or AFTER=... */
622 * copy command string
624 line.cl_Shell = strdup(ptr);
626 if (line.cl_Delay > 0) {
627 if (!(line.cl_Timestamp = concat(TSDir, "/", userName, ".", line.cl_JobName, NULL))) {
628 errno = ENOMEM;
629 perror("SynchronizeFile");
630 exit(1);
632 line.cl_NotUntil = tnow + line.cl_Delay;
635 if (line.cl_JobName) {
636 if (DebugOpt)
637 printlogf(LOG_DEBUG, " Command %s Job %s\n", line.cl_Shell, line.cl_JobName);
638 } else {
639 /* when cl_JobName is NULL, we point cl_Description to cl_Shell */
640 line.cl_Description = line.cl_Shell;
641 if (DebugOpt)
642 printlogf(LOG_DEBUG, " Command %s\n", line.cl_Shell);
645 *pline = calloc(1, sizeof(CronLine));
646 /* copy working CronLine to newly allocated one */
647 **pline = line;
649 pline = &((*pline)->cl_Next);
652 *pline = NULL;
654 file->cf_Next = FileBase;
655 FileBase = file;
657 if (maxLines == 0 || maxEntries == 0)
658 printlogf(LOG_WARNING, "maximum number of lines reached for user %s\n", userName);
660 fclose(fi);
662 free(path);
665 char *
666 ParseInterval(int *interval, char *ptr)
668 int n = 0;
669 if (ptr && *ptr >= '0' && *ptr <= '9' && (n = strtol(ptr, &ptr, 10)) > 0)
670 switch (*ptr) {
671 case 'm':
672 n *= 60;
673 break;
674 case 'h':
675 n *= HOURLY_FREQ;
676 break;
677 case 'd':
678 n *= DAILY_FREQ;
679 break;
680 case 'w':
681 n *= WEEKLY_FREQ;
682 break;
683 default:
684 n = 0;
686 if (n > 0) {
687 *interval = n;
688 return (ptr+1);
689 } else
690 return (NULL);
693 char *
694 ParseField(char *user, char *ary, int modvalue, int off, int onvalue, const char **names, char *ptr)
696 char *base = ptr;
697 int n1 = -1;
698 int n2 = -1;
700 if (base == NULL)
701 return (NULL);
703 while (*ptr != ' ' && *ptr != '\t' && *ptr != '\n') {
704 int skip = 0;
707 * Handle numeric digit or symbol or '*'
710 if (*ptr == '*') {
711 n1 = 0; /* everything will be filled */
712 n2 = modvalue - 1;
713 skip = 1;
714 ++ptr;
715 } else if (*ptr >= '0' && *ptr <= '9') {
716 if (n1 < 0)
717 n1 = strtol(ptr, &ptr, 10) + off;
718 else
719 n2 = strtol(ptr, &ptr, 10) + off;
720 skip = 1;
721 } else if (names) {
722 int i;
724 for (i = 0; names[i]; ++i) {
725 if (strncmp(ptr, names[i], strlen(names[i])) == 0) {
726 break;
729 if (names[i]) {
730 ptr += strlen(names[i]);
731 if (n1 < 0)
732 n1 = i;
733 else
734 n2 = i;
735 skip = 1;
740 * handle optional range '-'
743 if (skip == 0) {
744 printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s\n", user, base);
745 return(NULL);
747 if (*ptr == '-' && n2 < 0) {
748 ++ptr;
749 continue;
753 * collapse single-value ranges, handle skipmark, and fill
754 * in the character array appropriately.
757 if (n2 < 0)
758 n2 = n1;
760 n2 = n2 % modvalue;
762 if (*ptr == '/')
763 skip = strtol(ptr + 1, &ptr, 10);
766 * fill array, using a failsafe is the easiest way to prevent
767 * an endless loop
771 int s0 = 1;
772 int failsafe = 1024;
774 --n1;
775 do {
776 n1 = (n1 + 1) % modvalue;
778 if (--s0 == 0) {
779 ary[n1] = onvalue;
780 s0 = skip;
782 } while (n1 != n2 && --failsafe);
784 if (failsafe == 0) {
785 printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s\n", user, base);
786 return(NULL);
789 if (*ptr != ',')
790 break;
791 ++ptr;
792 n1 = -1;
793 n2 = -1;
796 if (*ptr != ' ' && *ptr != '\t' && *ptr != '\n') {
797 printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s\n", user, base);
798 return(NULL);
801 while (*ptr == ' ' || *ptr == '\t' || *ptr == '\n')
802 ++ptr;
804 if (DebugOpt) {
805 int i;
807 for (i = 0; i < modvalue; ++i)
808 if (modvalue == 7)
809 printlogf(LOG_DEBUG, "%2x ", ary[i]);
810 else
811 printlogf(LOG_DEBUG, "%d", ary[i]);
812 printlogf(LOG_DEBUG, "\n");
815 return(ptr);
818 void
819 FixDayDow(CronLine *line)
821 unsigned short i,j;
822 short weekUsed = 0;
823 short daysUsed = 0;
825 for (i = 0; i < arysize(line->cl_Dow); ++i) {
826 if (line->cl_Dow[i] == 0) {
827 weekUsed = 1;
828 break;
831 for (i = 0; i < arysize(line->cl_Days); ++i) {
832 if (line->cl_Days[i] == 0) {
833 if (weekUsed) {
834 if (!daysUsed) {
835 daysUsed = 1;
836 /* change from "every Mon" to "ith Mon"
837 * 6th,7th... Dow are treated as 1st,2nd... */
838 for (j = 0; j < arysize(line->cl_Dow); ++j) {
839 line->cl_Dow[j] &= 1 << (i-1)%5;
841 } else {
842 /* change from "nth Mon" to "nth or ith Mon" */
843 for (j = 0; j < arysize(line->cl_Dow); ++j) {
844 if (line->cl_Dow[j])
845 line->cl_Dow[j] |= 1 << (i-1)%5;
848 /* continue cycling through cl_Days */
850 else {
851 daysUsed = 1;
852 break;
856 if (weekUsed) {
857 memset(line->cl_Days, 0, sizeof(line->cl_Days));
859 if (daysUsed && !weekUsed) {
860 memset(line->cl_Dow, 0, sizeof(line->cl_Dow));
865 * DeleteFile() - destroy a CronFile.
867 * The CronFile (*pfile) is destroyed if possible, and marked cf_Deleted
868 * if there are still active processes running on it. *pfile is relinked
869 * on success.
871 void
872 DeleteFile(CronFile **pfile)
874 CronFile *file = *pfile;
875 CronLine **pline = &file->cf_LineBase;
876 CronLine *line;
877 CronWaiter **pwaiters, *waiters;
878 CronNotifier **pnotifs, *notifs;
880 file->cf_Running = 0;
881 file->cf_Deleted = 1;
883 while ((line = *pline) != NULL) {
884 if (line->cl_Pid > 0) {
885 file->cf_Running = 1;
886 pline = &line->cl_Next;
887 } else {
888 *pline = line->cl_Next;
889 free(line->cl_Shell);
891 if (line->cl_JobName)
892 /* this frees both cl_Description and cl_JobName
893 * if cl_JobName is NULL, Description pointed to ch_Shell, which was already freed
895 free(line->cl_Description);
896 if (line->cl_Timestamp)
897 free(line->cl_Timestamp);
899 pnotifs = &line->cl_Notifs;
900 while ((notifs = *pnotifs) != NULL) {
901 *pnotifs = notifs->cn_Next;
902 if (notifs->cn_Waiter) {
903 notifs->cn_Waiter->cw_NotifLine = NULL;
904 notifs->cn_Waiter->cw_Notifier = NULL;
906 free(notifs);
908 pwaiters = &line->cl_Waiters;
909 while ((waiters = *pwaiters) != NULL) {
910 *pwaiters = waiters->cw_Next;
911 if (waiters->cw_Notifier)
912 waiters->cw_Notifier->cn_Waiter = NULL;
913 free(waiters);
916 free(line);
919 if (file->cf_Running == 0) {
920 *pfile = file->cf_Next;
921 free(file->cf_DPath);
922 free(file->cf_FileName);
923 free(file->cf_UserName);
924 free(file);
930 * TestJobs()
932 * determine which jobs need to be run. Under normal conditions, the
933 * period is about a minute (one scan). Worst case it will be one
934 * hour (60 scans).
938 TestJobs(time_t t1, time_t t2)
940 short nJobs = 0;
941 time_t t;
942 CronFile *file;
943 CronLine *line;
945 for (file = FileBase; file; file = file->cf_Next) {
946 if (file->cf_Deleted)
947 continue;
948 for (line = file->cf_LineBase; line; line = line->cl_Next) {
949 struct CronWaiter *waiter;
951 if (line->cl_Pid == -2) {
952 /* can job stop waiting? */
953 int ready = 1;
954 waiter = line->cl_Waiters;
955 while (waiter != NULL) {
956 if (waiter->cw_Flag > 0) {
957 /* notifier exited unsuccessfully */
958 ready = 2;
959 break;
960 } else if (waiter->cw_Flag < 0)
961 /* still waiting, notifier hasn't run to completion */
962 ready = 0;
963 waiter = waiter->cw_Next;
965 if (ready == 2) {
966 if (DebugOpt)
967 printlogf(LOG_DEBUG, "cancelled waiting: user %s %s\n", file->cf_UserName, line->cl_Description);
968 line->cl_Pid = 0;
969 } else if (ready) {
970 if (DebugOpt)
971 printlogf(LOG_DEBUG, "finished waiting: user %s %s\n", file->cf_UserName, line->cl_Description);
972 nJobs += ArmJob(file, line, 0, -1);
974 if (line->cl_NotUntil)
975 line->cl_NotUntil = t2;
983 * Find jobs > t1 and <= t2
986 for (t = t1 - t1 % 60; t <= t2; t += 60) {
987 if (t > t1) {
988 struct tm *tp = localtime(&t);
990 unsigned short n_wday = (tp->tm_mday - 1)%7 + 1;
991 if (n_wday >= 4) {
992 struct tm tnext = *tp;
993 tnext.tm_mday += 7;
994 if (mktime(&tnext) != (time_t)-1 && tnext.tm_mon != tp->tm_mon)
995 n_wday |= 16; /* last dow in month is always recognized as 5th */
998 for (file = FileBase; file; file = file->cf_Next) {
999 if (file->cf_Deleted)
1000 continue;
1001 for (line = file->cf_LineBase; line; line = line->cl_Next) {
1002 if ((line->cl_Pid == -2 || line->cl_Pid == 0) && (line->cl_Freq == 0 || (line->cl_Freq > 0 && t2 >= line->cl_NotUntil))) {
1003 /* (re)schedule job? */
1004 if (line->cl_Mins[tp->tm_min] &&
1005 line->cl_Hrs[tp->tm_hour] &&
1006 (line->cl_Days[tp->tm_mday] || (n_wday && line->cl_Dow[tp->tm_wday]) ) &&
1007 line->cl_Mons[tp->tm_mon]
1009 if (line->cl_NotUntil)
1010 line->cl_NotUntil = t2 - t2 % 60 + line->cl_Delay; /* save what minute this job was scheduled/started waiting, plus cl_Delay */
1011 nJobs += ArmJob(file, line, t1, t2);
1018 return(nJobs);
1022 * ArmJob: if t2 is (time_t)-1, we force-schedule the job without any waiting
1023 * else it will wait on any of its declared notifiers who will run <= t2 + cw_MaxWait
1027 ArmJob(CronFile *file, CronLine *line, time_t t1, time_t t2)
1029 struct CronWaiter *waiter;
1030 if (line->cl_Pid > 0) {
1031 printlogf(LOG_NOTICE, "process already running (%d): user %s %s\n",
1032 line->cl_Pid,
1033 file->cf_UserName,
1034 line->cl_Description
1036 } else if (t2 == -1 && line->cl_Pid != -1) {
1037 line->cl_Pid = -1;
1038 file->cf_Ready = 1;
1039 return 1;
1040 } else if (line->cl_Pid == 0) {
1041 /* arming a waiting job (cl_Pid == -2) without forcing has no effect */
1042 line->cl_Pid = -1;
1043 /* if we have any waiters, zero them and arm cl_Pid=-2 */
1044 waiter = line->cl_Waiters;
1045 while (waiter != NULL) {
1046 /* check if notifier will run <= t2 + cw_Max_Wait? */
1047 if (!waiter->cw_NotifLine)
1048 /* notifier deleted */
1049 waiter->cw_Flag = 0;
1050 else if (waiter->cw_NotifLine->cl_Pid != 0) {
1051 /* if notifier is armed, or waiting, or running, we wait for it */
1052 waiter->cw_Flag = -1;
1053 line->cl_Pid = -2;
1054 } else if (waiter->cw_NotifLine->cl_Freq < 0) {
1055 /* arm any @noauto or @reboot jobs we're waiting on */
1056 ArmJob(file, waiter->cw_NotifLine, t1, t2);
1057 waiter->cw_Flag = -1;
1058 line->cl_Pid = -2;
1059 } else {
1060 time_t t;
1061 if (waiter->cw_MaxWait == 0)
1062 /* when no MaxWait interval specified, we always wait */
1063 waiter->cw_Flag = -1;
1064 else if (waiter->cw_NotifLine->cl_Freq == 0 || (waiter->cw_NotifLine->cl_Freq > 0 && t2 + waiter->cw_MaxWait >= waiter->cw_NotifLine->cl_NotUntil)) {
1065 /* default is don't wait */
1066 waiter->cw_Flag = 0;
1067 for (t = t1 - t1 % 60; t <= t2; t += 60) {
1068 if (t > t1) {
1069 struct tm *tp = localtime(&t);
1071 unsigned short n_wday = (tp->tm_mday - 1)%7 + 1;
1072 if (n_wday >= 4) {
1073 struct tm tnext = *tp;
1074 tnext.tm_mday += 7;
1075 if (mktime(&tnext) != (time_t)-1 && tnext.tm_mon != tp->tm_mon)
1076 n_wday |= 16; /* last dow in month is always recognized as 5th */
1078 if (line->cl_Mins[tp->tm_min] &&
1079 line->cl_Hrs[tp->tm_hour] &&
1080 (line->cl_Days[tp->tm_mday] || (n_wday && line->cl_Dow[tp->tm_wday]) ) &&
1081 line->cl_Mons[tp->tm_mon]
1083 /* notifier will run soon enough, we wait for it */
1084 waiter->cw_Flag = -1;
1085 line->cl_Pid = -2;
1086 break;
1092 waiter = waiter->cw_Next;
1094 if (line->cl_Pid == -1) {
1095 /* job is ready to run */
1096 file->cf_Ready = 1;
1097 if (DebugOpt)
1098 printlogf(LOG_DEBUG, "scheduled: user %s %s\n",
1099 file->cf_UserName,
1100 line->cl_Description
1102 return 1;
1103 } else if (DebugOpt)
1104 printlogf(LOG_DEBUG, "waiting: user %s %s\n",
1105 file->cf_UserName,
1106 line->cl_Description
1109 return 0;
1113 TestStartupJobs(void)
1115 short nJobs = 0;
1116 time_t t1 = time(NULL);
1117 CronFile *file;
1118 CronLine *line;
1120 t1 = t1 - t1 % 60 + 60;
1122 for (file = FileBase; file; file = file->cf_Next) {
1123 if (DebugOpt)
1124 printlogf(LOG_DEBUG, "TestStartup for FILE %s/%s USER %s:\n",
1125 file->cf_DPath, file->cf_FileName, file->cf_UserName);
1126 for (line = file->cf_LineBase; line; line = line->cl_Next) {
1127 struct CronWaiter *waiter;
1128 if (DebugOpt) {
1129 if (line->cl_JobName)
1130 printlogf(LOG_DEBUG, " LINE %s JOB %s\n", line->cl_Shell, line->cl_JobName);
1131 else
1132 printlogf(LOG_DEBUG, " LINE %s\n", line->cl_Shell);
1135 if (line->cl_Freq == -1) {
1136 /* freq is @reboot */
1138 line->cl_Pid = -1;
1139 /* if we have any waiters, reset them and arm Pid = -2 */
1140 waiter = line->cl_Waiters;
1141 while (waiter != NULL) {
1142 waiter->cw_Flag = -1;
1143 line->cl_Pid = -2;
1144 /* we only arm @noauto jobs we're waiting on, not other @reboot jobs */
1145 if (waiter->cw_NotifLine && waiter->cw_NotifLine->cl_Freq == -2)
1146 ArmJob(file, waiter->cw_NotifLine, t1, t1+60);
1147 waiter = waiter->cw_Next;
1149 if (line->cl_Pid == -1) {
1150 /* job is ready to run */
1151 file->cf_Ready = 1;
1152 ++nJobs;
1153 if (DebugOpt)
1154 printlogf(LOG_DEBUG, " scheduled: %s\n", line->cl_Description);
1155 } else if (DebugOpt)
1156 printlogf(LOG_DEBUG, " waiting: %s\n", line->cl_Description);
1160 } /* for line */
1162 return(nJobs);
1165 void
1166 RunJobs(void)
1168 CronFile *file;
1169 CronLine *line;
1171 for (file = FileBase; file; file = file->cf_Next) {
1172 if (file->cf_Ready) {
1173 file->cf_Ready = 0;
1175 for (line = file->cf_LineBase; line; line = line->cl_Next) {
1176 if (line->cl_Pid == -1) {
1178 RunJob(file, line);
1180 printlogf(LOG_INFO, "FILE %s/%s USER %s PID %3d %s\n",
1181 file->cf_DPath,
1182 file->cf_FileName,
1183 file->cf_UserName,
1184 line->cl_Pid,
1185 line->cl_Description
1187 if (line->cl_Pid < 0)
1188 /* QUESTION how could this happen? RunJob will leave cl_Pid set to 0 or the actual pid */
1189 file->cf_Ready = 1;
1190 else if (line->cl_Pid > 0)
1191 file->cf_Running = 1;
1199 * CheckJobs() - check for job completion
1201 * Check for job completion, return number of CronFiles still running after
1202 * all done.
1206 CheckJobs(void)
1208 CronFile *file;
1209 CronLine *line;
1210 int nStillRunning = 0;
1212 for (file = FileBase; file; file = file->cf_Next) {
1213 if (file->cf_Running) {
1214 file->cf_Running = 0;
1216 for (line = file->cf_LineBase; line; line = line->cl_Next) {
1217 if (line->cl_Pid > 0) {
1218 int status;
1219 int r = waitpid(line->cl_Pid, &status, WNOHANG);
1221 /* waitpid returns -1 for error, 0 if cl_Pid still running, cl_Pid if it's dead */
1223 if (r < 0 || r == line->cl_Pid) {
1224 if (r > 0 && WIFEXITED(status))
1225 status = WEXITSTATUS(status);
1226 else
1227 status = 1;
1228 EndJob(file, line, status);
1230 } else if (r == 0) {
1231 file->cf_Running = 1;
1236 nStillRunning += file->cf_Running;
1238 return(nStillRunning);