write "fake" timestamp file when none exists yet
[yacron.git] / database.c
blob3e44f8fa5221f0a06f8af82d358c6841d141047e
2 /*
3 * DATABASE.C
5 * Copyright 1994 Matthew Dillon (dillon@apollo.backplane.com)
6 * Copyright 2009 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[256];
98 char *fname, *ptok, *job;
99 char *path;
101 asprintf(&path, "%s/%s", dpath, CRONUPDATE);
102 if ((fi = fopen(path, "r")) != NULL) {
103 remove(path);
104 logn(LOG_INFO, "reading %s/%s\n", dpath, CRONUPDATE);
105 while (fgets(buf, sizeof(buf), fi) != NULL) {
107 * if buf has only sep chars, return NULL and point ptok at buf's terminating 0
108 * else return pointer to first non-sep of buf and
109 * if there's a following sep, overwrite it to 0 and point ptok to next char
110 * else point ptok at buf's terminating 0
112 fname = strtok_r(buf, " \t\n", &ptok);
114 if (user_override)
115 SynchronizeFile(dpath, fname, user_override);
116 else if (!getpwnam(fname))
117 logn(LOG_WARNING, "ignoring %s/%s (non-existent user)\n", dpath, fname);
118 else if (*ptok == 0 || *ptok == '\n') {
119 SynchronizeFile(dpath, fname, fname);
120 ReadTimestamps(fname);
121 } else {
122 /* if fname is followed by whitespace, we prod any following jobs */
123 CronFile *file = FileBase;
124 while (file) {
125 if (strcmp(file->cf_UserName, fname) == 0)
126 break;
127 file = file->cf_Next;
129 if (!file)
130 logn(LOG_WARNING, "unable to prod for user %s: no crontab\n", fname);
131 else {
132 CronLine *line;
133 /* calling strtok(ptok...) then strtok(NULL) is equiv to calling strtok_r(NULL,..&ptok) */
134 while ((job = strtok(ptok, " \t\n")) != NULL) {
135 time_t force = t2;
136 ptok = NULL;
137 if (*job == '!') {
138 force = (time_t)-1;
139 ++job;
141 line = file->cf_LineBase;
142 while (line) {
143 if (line->cl_JobName && strcmp(line->cl_JobName, job) == 0)
144 break;
145 line = line->cl_Next;
147 if (line)
148 ArmJob(file, line, t1, force);
149 else {
150 logn(LOG_WARNING, "unable to prod for user %s: unknown job %s\n", fname, job);
151 /* we can continue parsing this line, we just don't install any CronWaiter for the requested job */
157 fclose(fi);
159 free(path);
162 void
163 SynchronizeDir(const char *dpath, const char *user_override, int initial_scan)
165 CronFile **pfile;
166 CronFile *file;
167 struct dirent *den;
168 DIR *dir;
169 char *path;
172 * Delete all database CronFiles for this directory. DeleteFile() will
173 * free *pfile and relink the *pfile pointer, or in the alternative will
174 * mark it as deleted.
176 pfile = &FileBase;
177 while ((file = *pfile) != NULL) {
178 if (file->cf_Deleted == 0 && strcmp(file->cf_DPath, dpath) == 0) {
179 DeleteFile(pfile);
180 } else {
181 pfile = &file->cf_Next;
186 * Since we are resynchronizing the entire directory, remove the
187 * the CRONUPDATE file.
189 asprintf(&path, "%s/%s", dpath, CRONUPDATE);
190 remove(path);
191 free(path);
194 * Scan the specified directory
196 if ((dir = opendir(dpath)) != NULL) {
197 while ((den = readdir(dir)) != NULL) {
198 if (strchr(den->d_name, '.') != NULL)
199 continue;
200 if (strcmp(den->d_name, CRONUPDATE) == 0)
201 continue;
202 if (user_override) {
203 SynchronizeFile(dpath, den->d_name, user_override);
204 } else if (getpwnam(den->d_name)) {
205 SynchronizeFile(dpath, den->d_name, den->d_name);
206 } else {
207 logn(LOG_WARNING, "ignoring %s/%s (non-existent user)\n",
208 dpath, den->d_name);
211 closedir(dir);
212 } else {
213 if (initial_scan)
214 logn(LOG_ERR, "unable to scan directory %s\n", dpath);
215 /* softerror, do not exit the program */
220 void
221 ReadTimestamps(const char *user)
223 CronFile *file;
224 CronLine *line;
225 FILE *fi;
226 char buf[256];
227 char *ptr;
228 struct tm tm;
229 time_t sec, freq;
231 file = FileBase;
232 while (file != NULL) {
233 if (file->cf_Deleted == 0 && (!user || strcmp(user, file->cf_UserName) == 0)) {
234 line = file->cf_LineBase;
235 while (line != NULL) {
236 if (line->cl_Timestamp) {
237 if ((fi = fopen(line->cl_Timestamp, "r")) != NULL) {
238 if (fgets(buf, sizeof(buf), fi) != NULL) {
239 int fake = 0;
240 ptr = buf;
241 if (strncmp(buf, "after ", 6) == 0) {
242 fake = 1;
243 ptr += 6;
245 sec = (time_t)-1;
246 ptr = strptime(ptr, TIMESTAMP_FMT, &tm);
247 if (ptr && (*ptr == 0 || *ptr == '\n'))
248 sec = mktime(&tm);
249 if (sec == (time_t)-1) {
250 logn(LOG_WARNING, "unable to parse timestamp (user %s job %s)\n", file->cf_UserName, line->cl_JobName);
251 /* we continue checking other timestamps in this CronFile */
252 } else if (fake) {
253 line->cl_NotUntil = sec;
254 } else {
255 line->cl_LastRan = sec;
256 freq = (line->cl_Freq > 0) ? line->cl_Freq : line->cl_Delay;
257 if (line->cl_NotUntil < line->cl_LastRan + freq)
258 line->cl_NotUntil = line->cl_LastRan + freq;
261 fclose(fi);
262 } else {
263 int succeeded = 0;
264 logn(LOG_NOTICE, "no timestamp found (user %s job %s)\n", file->cf_UserName, line->cl_JobName);
265 /* write a fake timestamp file so our initial NotUntil doesn't keep being reset every hour when crond does a SynchronizeDir */
266 if ((fi = fopen(line->cl_Timestamp, "w")) != NULL) {
267 if (strftime(buf, sizeof(buf), TIMESTAMP_FMT, localtime(&line->cl_NotUntil)))
268 if (fputs("after ", fi) >= 0)
269 if (fputs(buf,fi) >= 0)
270 succeeded = 1;
271 fclose(fi);
273 if (!succeeded)
274 logn(LOG_WARNING, "unable to write timestamp to %s (user %s %s)\n", line->cl_Timestamp, file->cf_UserName, line->cl_Description);
277 line = line->cl_Next;
280 file = file->cf_Next;
284 void
285 SynchronizeFile(const char *dpath, const char *fileName, const char *userName)
287 CronFile **pfile;
288 CronFile *file;
289 int maxEntries;
290 int maxLines;
291 char buf[1024];
292 char *path;
293 FILE *fi;
296 * Limit entries
298 if (strcmp(userName, "root") == 0)
299 maxEntries = 65535;
300 else
301 maxEntries = MAXLINES;
302 maxLines = maxEntries * 10;
305 * Delete any existing copy of this CronFile
307 pfile = &FileBase;
308 while ((file = *pfile) != NULL) {
309 if (file->cf_Deleted == 0 && strcmp(file->cf_DPath, dpath) == 0 &&
310 strcmp(file->cf_FileName, fileName) == 0
312 DeleteFile(pfile);
313 } else {
314 pfile = &file->cf_Next;
318 asprintf(&path, "%s/%s", dpath, fileName);
319 if ((fi = fopen(path, "r")) != NULL) {
320 struct stat sbuf;
322 if (fstat(fileno(fi), &sbuf) == 0 && sbuf.st_uid == DaemonUid) {
323 CronFile *file = calloc(1, sizeof(CronFile));
324 CronLine **pline;
325 time_t tnow = time(NULL);
327 file->cf_UserName = strdup(userName);
328 file->cf_FileName = strdup(fileName);
329 file->cf_DPath = strdup(dpath);
330 pline = &file->cf_LineBase;
332 while (fgets(buf, sizeof(buf), fi) != NULL && --maxLines) {
333 CronLine line;
334 char *ptr = buf;
335 int len;
337 while (*ptr == ' ' || *ptr == '\t' || *ptr == '\n')
338 ++ptr;
340 len = strlen(ptr);
341 if (len && ptr[len-1] == '\n')
342 ptr[--len] = 0;
344 if (*ptr == 0 || *ptr == '#')
345 continue;
347 if (--maxEntries == 0)
348 break;
350 bzero(&line, sizeof(line));
352 if (DebugOpt)
353 logn(LOG_DEBUG, "User %s Entry %s\n", userName, buf);
355 if (*ptr == '@') {
357 * parse @hourly, etc
359 int j;
360 line.cl_Delay = -1;
361 ptr += 1;
362 for (j = 0; FreqAry[j]; ++j) {
363 if (strncmp(ptr, FreqAry[j], strlen(FreqAry[j])) == 0) {
364 break;
367 if (FreqAry[j]) {
368 ptr += strlen(FreqAry[j]);
369 switch(j) {
370 case 0:
371 /* noauto */
372 line.cl_Freq = -2;
373 line.cl_Delay = 0;
374 break;
375 case 1:
376 /* reboot */
377 line.cl_Freq = -1;
378 line.cl_Delay = 0;
379 break;
380 case 2:
381 line.cl_Freq = HOURLY_FREQ;
382 break;
383 case 3:
384 line.cl_Freq = DAILY_FREQ;
385 break;
386 case 4:
387 line.cl_Freq = WEEKLY_FREQ;
388 break;
389 case 5:
390 line.cl_Freq = MONTHLY_FREQ;
391 case 6:
392 line.cl_Freq = YEARLY_FREQ;
393 /* else line.cl_Freq will remain 0 */
397 if (!line.cl_Freq || (*ptr != ' ' && *ptr != '\t')) {
398 logn(LOG_WARNING, "failed parsing crontab for user %s: %s\n", userName, buf);
399 continue;
402 if (line.cl_Delay < 0) {
404 * delays on @daily, @hourly, etc are 1/20 of the frequency
405 * so they don't all start at once
406 * this also affects how they behave when the job returns EAGAIN
408 line.cl_Delay = line.cl_Freq / 20;
409 /* all minutes are permitted */
410 for (j=0; j<60; ++j)
411 line.cl_Mins[j] = 1;
412 for (j=0; j<24; ++j)
413 line.cl_Hrs[j] = 1;
414 for (j=1; j<32; ++j)
415 /* days are numbered 1..31 */
416 line.cl_Days[j] = 1;
417 for (j=0; j<12; ++j)
418 line.cl_Mons[j] = 1;
421 while (*ptr == ' ' || *ptr == '\t')
422 ++ptr;
424 } else {
426 * parse date ranges
429 ptr = ParseField(file->cf_UserName, line.cl_Mins, 60, 0, 1,
430 NULL, ptr);
431 ptr = ParseField(file->cf_UserName, line.cl_Hrs, 24, 0, 1,
432 NULL, ptr);
433 ptr = ParseField(file->cf_UserName, line.cl_Days, 32, 0, 1,
434 NULL, ptr);
435 ptr = ParseField(file->cf_UserName, line.cl_Mons, 12, -1, 1,
436 MonAry, ptr);
437 ptr = ParseField(file->cf_UserName, line.cl_Dow, 7, 0, 31,
438 DowAry, ptr);
440 * check failure
443 if (ptr == NULL)
444 continue;
447 * fix days and dow - if one is not * and the other
448 * is *, the other is set to 0, and vise-versa
451 FixDayDow(&line);
454 /* check for ID=... and AFTER=... and FREQ=... */
455 do {
456 if (strncmp(ptr, ID_TAG, strlen(ID_TAG)) == 0) {
457 if (line.cl_JobName) {
458 /* only assign ID_TAG once */
459 logn(LOG_WARNING, "failed parsing crontab for user %s: repeated %s\n", userName, ptr);
460 ptr = NULL;
461 } else {
462 ptr += strlen(ID_TAG);
464 * name = strsep(&ptr, seps):
465 * return name = ptr, and if ptr contains sep chars, overwrite first with 0 and point ptr to next char
466 * else set ptr=NULL
468 asprintf(&line.cl_Description, "job %s", strsep(&ptr, " \t"));
469 line.cl_JobName = line.cl_Description + 4;
470 if (!ptr)
471 logn(LOG_WARNING, "failed parsing crontab for user %s: no command after %s%s\n", userName, ID_TAG, line.cl_JobName);
473 } else if (strncmp(ptr, FREQ_TAG, strlen(FREQ_TAG)) == 0) {
474 if (line.cl_Freq) {
475 /* only assign FREQ_TAG once */
476 logn(LOG_WARNING, "failed parsing crontab for user %s: repeated %s\n", userName, ptr);
477 ptr = NULL;
478 } else {
479 char *base = ptr;
480 ptr += strlen(FREQ_TAG);
481 ptr = ParseInterval(&line.cl_Freq, ptr);
482 if (ptr && *ptr == '/')
483 ptr = ParseInterval(&line.cl_Delay, ++ptr);
484 else
485 line.cl_Delay = line.cl_Freq;
486 if (!ptr) {
487 logn(LOG_WARNING, "failed parsing crontab for user %s: %s\n", userName, base);
488 } else if (*ptr != ' ' && *ptr != '\t') {
489 logn(LOG_WARNING, "failed parsing crontab for user %s: no command after %s\n", userName, base);
490 ptr = NULL;
493 } else if (strncmp(ptr, WAIT_TAG, strlen(WAIT_TAG)) == 0) {
494 if (line.cl_Waiters) {
495 /* only assign WAIT_TAG once */
496 logn(LOG_WARNING, "failed parsing crontab for user %s: repeated %s\n", userName, ptr);
497 ptr = NULL;
498 } else {
499 short more = 1;
500 char *name;
501 ptr += strlen(WAIT_TAG);
502 do {
503 CronLine *job, **pjob;
504 if (strcspn(ptr,",") < strcspn(ptr," \t"))
505 name = strsep(&ptr, ",");
506 else {
507 more = 0;
508 name = strsep(&ptr, " \t");
510 if (!ptr || *ptr == 0) {
511 /* unexpectedly this was the last token in buf; so abort */
512 logn(LOG_WARNING, "failed parsing crontab for user %s: no command after %s%s\n", userName, WAIT_TAG, name);
513 ptr = NULL;
514 } else {
515 int waitfor = 0;
516 char *w, *wsave;
517 if ((w = index(name, '/')) != NULL) {
518 wsave = w++;
519 w = ParseInterval(&waitfor, w);
520 if (!w || *w != 0) {
521 logn(LOG_WARNING, "failed parsing crontab for user %s: %s%s\n", userName, WAIT_TAG, name);
522 ptr = NULL;
523 } else
524 /* truncate name */
525 *wsave = 0;
527 if (ptr) {
528 /* look for a matching CronLine */
529 pjob = &file->cf_LineBase;
530 while ((job = *pjob) != NULL) {
531 if (job->cl_JobName && strcmp(job->cl_JobName, name) == 0) {
532 CronWaiter *waiter = malloc(sizeof(CronWaiter));
533 CronNotifier *notif = malloc(sizeof(CronNotifier));
534 waiter->cw_Flag = -1;
535 waiter->cw_MaxWait = waitfor;
536 waiter->cw_NotifLine = job;
537 waiter->cw_Notifier = notif;
538 waiter->cw_Next = line.cl_Waiters; /* add to head of line.cl_Waiters */
539 line.cl_Waiters = waiter;
540 notif->cn_Waiter = waiter;
541 notif->cn_Next = job->cl_Notifs; /* add to head of job->cl_Notifs */
542 job->cl_Notifs = notif;
543 break;
544 } else
545 pjob = &job->cl_Next;
547 if (!job) {
548 logn(LOG_WARNING, "failed parsing crontab for user %s: unknown job %s\n", userName, name);
549 /* we can continue parsing this line, we just don't install any CronWaiter for the requested job */
553 } while (ptr && more);
555 } else
556 break;
557 if (!ptr)
558 break;
559 while (*ptr == ' ' || *ptr == '\t')
560 ++ptr;
561 } while (!line.cl_JobName || !line.cl_Waiters || !line.cl_Freq);
563 if (line.cl_JobName && (!ptr || *line.cl_JobName == 0)) {
564 /* we're aborting, or ID= was empty */
565 free(line.cl_Description);
566 line.cl_Description = NULL;
567 line.cl_JobName = NULL;
569 if (ptr && line.cl_Delay > 0 && !line.cl_JobName) {
570 logn(LOG_WARNING, "failed parsing crontab for user %s: writing timestamp requires job %s to be named\n", userName, ptr);
571 ptr = NULL;
573 if (!ptr) {
574 /* couldn't parse so we abort; free any cl_Waiters */
575 if (line.cl_Waiters) {
576 CronWaiter **pwaiters, *waiters;
577 pwaiters = &line.cl_Waiters;
578 while ((waiters = *pwaiters) != NULL) {
579 *pwaiters = waiters->cw_Next;
580 /* leave the Notifier allocated but disabled */
581 waiters->cw_Notifier->cn_Waiter = NULL;
582 free(waiters);
585 continue;
587 /* now we've added any ID=... or AFTER=... */
590 * copy command string
592 line.cl_Shell = strdup(ptr);
594 if (line.cl_Delay > 0) {
595 asprintf(&line.cl_Timestamp, "%s/%s.%s", TSDir, userName, line.cl_JobName);
596 line.cl_NotUntil = tnow + line.cl_Delay;
599 if (line.cl_JobName) {
600 if (DebugOpt)
601 logn(LOG_DEBUG, " Command %s Job %s\n", line.cl_Shell, line.cl_JobName);
602 } else {
603 /* when cl_JobName is NULL, we point cl_Description to cl_Shell */
604 line.cl_Description = line.cl_Shell;
605 if (DebugOpt)
606 logn(LOG_DEBUG, " Command %s\n", line.cl_Shell);
609 *pline = calloc(1, sizeof(CronLine));
610 /* copy working CronLine to newly allocated one */
611 **pline = line;
613 pline = &((*pline)->cl_Next);
616 *pline = NULL;
618 file->cf_Next = FileBase;
619 FileBase = file;
621 if (maxLines == 0 || maxEntries == 0)
622 logn(LOG_WARNING, "maximum number of lines reached for user %s\n", userName);
624 fclose(fi);
626 free(path);
629 char *
630 ParseInterval(int *interval, char *ptr)
632 int n = 0;
633 if (ptr && *ptr >= '0' && *ptr <= '9' && (n = strtol(ptr, &ptr, 10)) > 0)
634 switch (*ptr) {
635 case 'm':
636 n *= 60;
637 break;
638 case 'h':
639 n *= HOURLY_FREQ;
640 break;
641 case 'd':
642 n *= DAILY_FREQ;
643 break;
644 case 'w':
645 n *= WEEKLY_FREQ;
646 break;
647 default:
648 n = 0;
650 if (n > 0) {
651 *interval = n;
652 return (ptr+1);
653 } else
654 return (NULL);
657 char *
658 ParseField(char *user, char *ary, int modvalue, int off, int onvalue, const char **names, char *ptr)
660 char *base = ptr;
661 int n1 = -1;
662 int n2 = -1;
664 if (base == NULL)
665 return (NULL);
667 while (*ptr != ' ' && *ptr != '\t' && *ptr != '\n') {
668 int skip = 0;
671 * Handle numeric digit or symbol or '*'
674 if (*ptr == '*') {
675 n1 = 0; /* everything will be filled */
676 n2 = modvalue - 1;
677 skip = 1;
678 ++ptr;
679 } else if (*ptr >= '0' && *ptr <= '9') {
680 if (n1 < 0)
681 n1 = strtol(ptr, &ptr, 10) + off;
682 else
683 n2 = strtol(ptr, &ptr, 10) + off;
684 skip = 1;
685 } else if (names) {
686 int i;
688 for (i = 0; names[i]; ++i) {
689 if (strncmp(ptr, names[i], strlen(names[i])) == 0) {
690 break;
693 if (names[i]) {
694 ptr += strlen(names[i]);
695 if (n1 < 0)
696 n1 = i;
697 else
698 n2 = i;
699 skip = 1;
704 * handle optional range '-'
707 if (skip == 0) {
708 logn(LOG_WARNING, "failed parsing crontab for user %s: %s\n", user, base);
709 return(NULL);
711 if (*ptr == '-' && n2 < 0) {
712 ++ptr;
713 continue;
717 * collapse single-value ranges, handle skipmark, and fill
718 * in the character array appropriately.
721 if (n2 < 0)
722 n2 = n1;
724 if (*ptr == '/')
725 skip = strtol(ptr + 1, &ptr, 10);
728 * fill array, using a failsafe is the easiest way to prevent
729 * an endless loop
733 int s0 = 1;
734 int failsafe = 1024;
736 --n1;
737 do {
738 n1 = (n1 + 1) % modvalue;
740 if (--s0 == 0) {
741 ary[n1] = onvalue;
742 s0 = skip;
744 } while (n1 != n2 && --failsafe);
746 if (failsafe == 0) {
747 logn(LOG_WARNING, "failed parsing crontab for user %s: %s\n", user, base);
748 return(NULL);
751 if (*ptr != ',')
752 break;
753 ++ptr;
754 n1 = -1;
755 n2 = -1;
758 if (*ptr != ' ' && *ptr != '\t' && *ptr != '\n') {
759 logn(LOG_WARNING, "failed parsing crontab for user %s: %s\n", user, base);
760 return(NULL);
763 while (*ptr == ' ' || *ptr == '\t' || *ptr == '\n')
764 ++ptr;
766 if (DebugOpt) {
767 int i;
769 for (i = 0; i < modvalue; ++i)
770 logn(LOG_DEBUG, "%2x ", ary[i]);
771 logn(LOG_DEBUG, "\n");
774 return(ptr);
777 void
778 FixDayDow(CronLine *line)
780 unsigned short i,j;
781 short weekUsed = 0;
782 short daysUsed = 0;
784 for (i = 0; i < arysize(line->cl_Dow); ++i) {
785 if (line->cl_Dow[i] == 0) {
786 weekUsed = 1;
787 break;
790 for (i = 0; i < arysize(line->cl_Days); ++i) {
791 if (line->cl_Days[i] == 0) {
792 if (weekUsed) {
793 if (!daysUsed) {
794 daysUsed = 1;
795 /* change from "every Mon" to "ith Mon"
796 * 6th,7th... Dow are treated as 1st,2nd... */
797 for (j = 0; j < arysize(line->cl_Dow); ++j) {
798 line->cl_Dow[j] &= 1 << (i-1)%5;
800 } else {
801 /* change from "nth Mon" to "nth or ith Mon" */
802 for (j = 0; j < arysize(line->cl_Dow); ++j) {
803 if (line->cl_Dow[j])
804 line->cl_Dow[j] |= 1 << (i-1)%5;
807 /* continue cycling through cl_Days */
809 else {
810 daysUsed = 1;
811 break;
815 if (weekUsed) {
816 memset(line->cl_Days, 0, sizeof(line->cl_Days));
818 if (daysUsed && !weekUsed) {
819 memset(line->cl_Dow, 0, sizeof(line->cl_Dow));
824 * DeleteFile() - destroy a CronFile.
826 * The CronFile (*pfile) is destroyed if possible, and marked cf_Deleted
827 * if there are still active processes running on it. *pfile is relinked
828 * on success.
830 void
831 DeleteFile(CronFile **pfile)
833 CronFile *file = *pfile;
834 CronLine **pline = &file->cf_LineBase;
835 CronLine *line;
836 CronWaiter **pwaiters, *waiters;
837 CronNotifier **pnotifs, *notifs;
839 file->cf_Running = 0;
840 file->cf_Deleted = 1;
842 while ((line = *pline) != NULL) {
843 if (line->cl_Pid > 0) {
844 file->cf_Running = 1;
845 pline = &line->cl_Next;
846 } else {
847 *pline = line->cl_Next;
848 free(line->cl_Shell);
850 if (line->cl_JobName)
851 /* this frees both cl_Description and cl_JobName
852 * if cl_JobName is NULL, Description pointed to ch_Shell, which was already freed
854 free(line->cl_Description);
855 if (line->cl_Timestamp)
856 free(line->cl_Timestamp);
858 pnotifs = &line->cl_Notifs;
859 while ((notifs = *pnotifs) != NULL) {
860 *pnotifs = notifs->cn_Next;
861 if (notifs->cn_Waiter) {
862 notifs->cn_Waiter->cw_NotifLine = NULL;
863 notifs->cn_Waiter->cw_Notifier = NULL;
865 free(notifs);
867 pwaiters = &line->cl_Waiters;
868 while ((waiters = *pwaiters) != NULL) {
869 *pwaiters = waiters->cw_Next;
870 if (waiters->cw_Notifier)
871 waiters->cw_Notifier->cn_Waiter = NULL;
872 free(waiters);
875 free(line);
878 if (file->cf_Running == 0) {
879 *pfile = file->cf_Next;
880 free(file->cf_DPath);
881 free(file->cf_FileName);
882 free(file->cf_UserName);
883 free(file);
889 * TestJobs()
891 * determine which jobs need to be run. Under normal conditions, the
892 * period is about a minute (one scan). Worst case it will be one
893 * hour (60 scans).
897 TestJobs(time_t t1, time_t t2)
899 short nJobs = 0;
900 time_t t;
901 CronFile *file;
902 CronLine *line;
904 for (file = FileBase; file; file = file->cf_Next) {
905 if (file->cf_Deleted)
906 continue;
907 for (line = file->cf_LineBase; line; line = line->cl_Next) {
908 struct CronWaiter *waiter;
910 if (line->cl_Pid == -2) {
911 /* can job stop waiting? */
912 int ready = 1;
913 waiter = line->cl_Waiters;
914 while (waiter != NULL) {
915 if (waiter->cw_Flag > 0) {
916 /* notifier exited unsuccessfully */
917 ready = 2;
918 break;
919 } else if (waiter->cw_Flag < 0)
920 /* still waiting, notifier hasn't run to completion */
921 ready = 0;
922 waiter = waiter->cw_Next;
924 if (ready == 2) {
925 if (DebugOpt)
926 logn(LOG_DEBUG, "cancelled waiting: user %s %s\n", file->cf_UserName, line->cl_Description);
927 line->cl_Pid = 0;
928 } else if (ready) {
929 if (DebugOpt)
930 logn(LOG_DEBUG, "finished waiting: user %s %s\n", file->cf_UserName, line->cl_Description);
931 nJobs += ArmJob(file, line, 0, -1);
933 if (line->cl_NotUntil)
934 line->cl_NotUntil = t2;
942 * Find jobs > t1 and <= t2
945 for (t = t1 - t1 % 60; t <= t2; t += 60) {
946 if (t > t1) {
947 struct tm *tp = localtime(&t);
949 unsigned short n_wday = (tp->tm_mday - 1)%7 + 1;
950 if (n_wday >= 4) {
951 struct tm tnext = *tp;
952 tnext.tm_mday += 7;
953 if (mktime(&tnext) != (time_t)-1 && tnext.tm_mon != tp->tm_mon)
954 n_wday |= 16; /* last dow in month is always recognized as 5th */
957 for (file = FileBase; file; file = file->cf_Next) {
958 if (file->cf_Deleted)
959 continue;
960 for (line = file->cf_LineBase; line; line = line->cl_Next) {
961 if (line->cl_Pid != -1 && (line->cl_Freq == 0 || (line->cl_Freq > 0 && t2 >= line->cl_NotUntil))) {
962 /* (re)schedule job? */
963 if (line->cl_Mins[tp->tm_min] &&
964 line->cl_Hrs[tp->tm_hour] &&
965 (line->cl_Days[tp->tm_mday] || (n_wday && line->cl_Dow[tp->tm_wday]) ) &&
966 line->cl_Mons[tp->tm_mon]
968 if (line->cl_NotUntil)
969 line->cl_NotUntil = t2 - t2 % 60 + line->cl_Delay; /* save what minute this job was scheduled/started waiting, plus cl_Delay */
970 nJobs += ArmJob(file, line, t1, t2);
977 return(nJobs);
981 * ArmJob: if t2 is (time_t)-1, we force-schedule the job without any waiting
982 * else it will wait on any of its declared notifiers who will run <= t2 + cw_MaxWait
986 ArmJob(CronFile *file, CronLine *line, time_t t1, time_t t2)
988 struct CronWaiter *waiter;
989 if (line->cl_Pid > 0) {
990 logn(LOG_NOTICE, "process already running (%d): user %s %s\n",
991 line->cl_Pid,
992 file->cf_UserName,
993 line->cl_Description
995 } else if (t2 == -1 && line->cl_Pid != -1) {
996 line->cl_Pid = -1;
997 file->cf_Ready = 1;
998 return 1;
999 } else if (line->cl_Pid == 0) {
1000 /* arming a waiting job (cl_Pid == -2) without forcing has no effect */
1001 line->cl_Pid = -1;
1002 /* if we have any waiters, zero them and arm cl_Pid=-2 */
1003 waiter = line->cl_Waiters;
1004 while (waiter != NULL) {
1005 /* check if notifier will run <= t2 + cw_Max_Wait? */
1006 if (!waiter->cw_NotifLine)
1007 /* notifier deleted */
1008 waiter->cw_Flag = 0;
1009 else if (waiter->cw_NotifLine->cl_Pid != 0) {
1010 /* if notifier is armed, or waiting, or running, we wait for it */
1011 waiter->cw_Flag = -1;
1012 line->cl_Pid = -2;
1013 } else if (waiter->cw_NotifLine->cl_Freq < 0) {
1014 /* arm any @noauto or @reboot jobs we're waiting on */
1015 ArmJob(file, waiter->cw_NotifLine, t1, t2);
1016 waiter->cw_Flag = -1;
1017 line->cl_Pid = -2;
1018 } else {
1019 time_t t;
1020 if (waiter->cw_MaxWait == 0)
1021 /* when no MaxWait interval specified, we always wait */
1022 waiter->cw_Flag = -1;
1023 else if (waiter->cw_NotifLine->cl_Freq == 0 || (waiter->cw_NotifLine->cl_Freq > 0 && t2 + waiter->cw_MaxWait >= waiter->cw_NotifLine->cl_NotUntil)) {
1024 /* default is don't wait */
1025 waiter->cw_Flag = 0;
1026 for (t = t1 - t1 % 60; t <= t2; t += 60) {
1027 if (t > t1) {
1028 struct tm *tp = localtime(&t);
1030 unsigned short n_wday = (tp->tm_mday - 1)%7 + 1;
1031 if (n_wday >= 4) {
1032 struct tm tnext = *tp;
1033 tnext.tm_mday += 7;
1034 if (mktime(&tnext) != (time_t)-1 && tnext.tm_mon != tp->tm_mon)
1035 n_wday |= 16; /* last dow in month is always recognized as 5th */
1037 if (line->cl_Mins[tp->tm_min] &&
1038 line->cl_Hrs[tp->tm_hour] &&
1039 (line->cl_Days[tp->tm_mday] || (n_wday && line->cl_Dow[tp->tm_wday]) ) &&
1040 line->cl_Mons[tp->tm_mon]
1042 /* notifier will run soon enough, we wait for it */
1043 waiter->cw_Flag = -1;
1044 line->cl_Pid = -2;
1045 break;
1051 waiter = waiter->cw_Next;
1053 if (line->cl_Pid == -1) {
1054 /* job is ready to run */
1055 file->cf_Ready = 1;
1056 if (DebugOpt)
1057 logn(LOG_DEBUG, "scheduled: user %s %s\n",
1058 file->cf_UserName,
1059 line->cl_Description
1061 return 1;
1062 } else if (DebugOpt)
1063 logn(LOG_DEBUG, "waiting: user %s %s\n",
1064 file->cf_UserName,
1065 line->cl_Description
1068 return 0;
1072 TestStartupJobs(void)
1074 short nJobs = 0;
1075 time_t t1 = time(NULL);
1076 CronFile *file;
1077 CronLine *line;
1079 t1 = t1 - t1 % 60 + 60;
1081 for (file = FileBase; file; file = file->cf_Next) {
1082 if (DebugOpt)
1083 logn(LOG_DEBUG, "TestStartup for FILE %s/%s USER %s:\n",
1084 file->cf_DPath, file->cf_FileName, file->cf_UserName);
1085 for (line = file->cf_LineBase; line; line = line->cl_Next) {
1086 struct CronWaiter *waiter;
1087 if (DebugOpt) {
1088 if (line->cl_JobName)
1089 logn(LOG_DEBUG, " LINE %s JOB %s\n", line->cl_Shell, line->cl_JobName);
1090 else
1091 logn(LOG_DEBUG, " LINE %s\n", line->cl_Shell);
1094 if (line->cl_Freq == -1) {
1095 /* freq is @reboot */
1097 line->cl_Pid = -1;
1098 /* if we have any waiters, reset them and arm Pid = -2 */
1099 waiter = line->cl_Waiters;
1100 while (waiter != NULL) {
1101 waiter->cw_Flag = -1;
1102 line->cl_Pid = -2;
1103 /* we only arm @noauto jobs we're waiting on, not other @reboot jobs */
1104 if (waiter->cw_NotifLine && waiter->cw_NotifLine->cl_Freq == -2)
1105 ArmJob(file, waiter->cw_NotifLine, t1, t1+60);
1106 waiter = waiter->cw_Next;
1108 if (line->cl_Pid == -1) {
1109 /* job is ready to run */
1110 file->cf_Ready = 1;
1111 ++nJobs;
1112 if (DebugOpt)
1113 logn(LOG_DEBUG, " scheduled: %s\n", line->cl_Description);
1114 } else if (DebugOpt)
1115 logn(LOG_DEBUG, " waiting: %s\n", line->cl_Description);
1119 } /* for line */
1121 return(nJobs);
1124 void
1125 RunJobs(void)
1127 CronFile *file;
1128 CronLine *line;
1130 for (file = FileBase; file; file = file->cf_Next) {
1131 if (file->cf_Ready) {
1132 file->cf_Ready = 0;
1134 for (line = file->cf_LineBase; line; line = line->cl_Next) {
1135 if (line->cl_Pid == -1) {
1137 RunJob(file, line);
1139 logn(LOG_INFO, "FILE %s/%s USER %s PID %3d %s\n",
1140 file->cf_DPath,
1141 file->cf_FileName,
1142 file->cf_UserName,
1143 line->cl_Pid,
1144 line->cl_Description
1146 if (line->cl_Pid < 0)
1147 /* how could this happen? RunJob will leave cl_Pid set to 0 or the actual pid */
1148 file->cf_Ready = 1;
1149 else if (line->cl_Pid > 0)
1150 file->cf_Running = 1;
1158 * CheckJobs() - check for job completion
1160 * Check for job completion, return number of CronFiles still running after
1161 * all done.
1165 CheckJobs(void)
1167 CronFile *file;
1168 CronLine *line;
1169 int nStillRunning = 0;
1171 for (file = FileBase; file; file = file->cf_Next) {
1172 if (file->cf_Running) {
1173 file->cf_Running = 0;
1175 for (line = file->cf_LineBase; line; line = line->cl_Next) {
1176 if (line->cl_Pid > 0) {
1177 int status;
1178 int r = waitpid(line->cl_Pid, &status, WNOHANG);
1180 if (r < 0 || r == line->cl_Pid) {
1181 if (r > 0 && WIFEXITED(status))
1182 status = WEXITSTATUS(status);
1183 else
1184 status = 1;
1185 if (DebugOpt)
1186 logn(LOG_DEBUG, "user %s %s finished with %d\n", file->cf_UserName, line->cl_Description, status);
1187 EndJob(file, line, status);
1188 if (line->cl_Pid)
1189 file->cf_Running = 1;
1190 } else if (r == 0) {
1191 file->cf_Running = 1;
1196 nStillRunning += file->cf_Running;
1198 return(nStillRunning);