tweak log levels & messages
[yacron.git] / database.c
blob1c6fe68188754db0d3c5bb54833ffe821f228080
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;
171 if (DebugOpt)
172 logn(LOG_DEBUG, "Synchronizing %s\n", dpath);
175 * Delete all database CronFiles for this directory. DeleteFile() will
176 * free *pfile and relink the *pfile pointer, or in the alternative will
177 * mark it as deleted.
179 pfile = &FileBase;
180 while ((file = *pfile) != NULL) {
181 if (file->cf_Deleted == 0 && strcmp(file->cf_DPath, dpath) == 0) {
182 DeleteFile(pfile);
183 } else {
184 pfile = &file->cf_Next;
189 * Since we are resynchronizing the entire directory, remove the
190 * the CRONUPDATE file.
192 asprintf(&path, "%s/%s", dpath, CRONUPDATE);
193 remove(path);
194 free(path);
197 * Scan the specified directory
199 if ((dir = opendir(dpath)) != NULL) {
200 while ((den = readdir(dir)) != NULL) {
201 if (strchr(den->d_name, '.') != NULL)
202 continue;
203 if (strcmp(den->d_name, CRONUPDATE) == 0)
204 continue;
205 if (user_override) {
206 SynchronizeFile(dpath, den->d_name, user_override);
207 } else if (getpwnam(den->d_name)) {
208 SynchronizeFile(dpath, den->d_name, den->d_name);
209 } else {
210 logn(LOG_WARNING, "ignoring %s/%s (non-existent user)\n",
211 dpath, den->d_name);
214 closedir(dir);
215 } else {
216 if (initial_scan)
217 logn(LOG_ERR, "unable to scan directory %s\n", dpath);
218 /* softerror, do not exit the program */
223 void
224 ReadTimestamps(const char *user)
226 CronFile *file;
227 CronLine *line;
228 FILE *fi;
229 char buf[256];
230 char *ptr;
231 struct tm tm;
232 time_t sec, freq;
234 file = FileBase;
235 while (file != NULL) {
236 if (file->cf_Deleted == 0 && (!user || strcmp(user, file->cf_UserName) == 0)) {
237 line = file->cf_LineBase;
238 while (line != NULL) {
239 if (line->cl_Timestamp) {
240 if ((fi = fopen(line->cl_Timestamp, "r")) != NULL) {
241 if (fgets(buf, sizeof(buf), fi) != NULL) {
242 int fake = 0;
243 ptr = buf;
244 if (strncmp(buf, "after ", 6) == 0) {
245 fake = 1;
246 ptr += 6;
248 sec = (time_t)-1;
249 ptr = strptime(ptr, TIMESTAMP_FMT, &tm);
250 if (ptr && (*ptr == 0 || *ptr == '\n'))
251 /* strptime uses current seconds when seconds not specified? anyway, we don't get round minutes */
252 tm.tm_sec = 0;
253 sec = mktime(&tm);
254 if (sec == (time_t)-1) {
255 logn(LOG_ERR, "unable to parse timestamp (user %s job %s)\n", file->cf_UserName, line->cl_JobName);
256 /* we continue checking other timestamps in this CronFile */
257 } else {
258 /* sec -= sec % 60; */
259 if (fake) {
260 line->cl_NotUntil = sec;
261 } else {
262 line->cl_LastRan = sec;
263 freq = (line->cl_Freq > 0) ? line->cl_Freq : line->cl_Delay;
264 /* if (line->cl_NotUntil < line->cl_LastRan + freq) */
265 line->cl_NotUntil = line->cl_LastRan + freq;
269 fclose(fi);
270 } else {
271 int succeeded = 0;
272 logn(LOG_NOTICE, "no timestamp found (user %s job %s)\n", file->cf_UserName, line->cl_JobName);
273 /* write a fake timestamp file so our initial NotUntil doesn't keep being reset every hour when crond does a SynchronizeDir */
274 if ((fi = fopen(line->cl_Timestamp, "w")) != NULL) {
275 if (strftime(buf, sizeof(buf), TIMESTAMP_FMT, localtime(&line->cl_NotUntil)))
276 if (fputs("after ", fi) >= 0)
277 if (fputs(buf,fi) >= 0)
278 succeeded = 1;
279 fclose(fi);
281 if (!succeeded)
282 logn(LOG_WARNING, "unable to write timestamp to %s (user %s %s)\n", line->cl_Timestamp, file->cf_UserName, line->cl_Description);
285 line = line->cl_Next;
288 file = file->cf_Next;
292 void
293 SynchronizeFile(const char *dpath, const char *fileName, const char *userName)
295 CronFile **pfile;
296 CronFile *file;
297 int maxEntries;
298 int maxLines;
299 char buf[1024];
300 char *path;
301 FILE *fi;
304 * Limit entries
306 if (strcmp(userName, "root") == 0)
307 maxEntries = 65535;
308 else
309 maxEntries = MAXLINES;
310 maxLines = maxEntries * 10;
313 * Delete any existing copy of this CronFile
315 pfile = &FileBase;
316 while ((file = *pfile) != NULL) {
317 if (file->cf_Deleted == 0 && strcmp(file->cf_DPath, dpath) == 0 &&
318 strcmp(file->cf_FileName, fileName) == 0
320 DeleteFile(pfile);
321 } else {
322 pfile = &file->cf_Next;
326 asprintf(&path, "%s/%s", dpath, fileName);
327 if ((fi = fopen(path, "r")) != NULL) {
328 struct stat sbuf;
330 if (fstat(fileno(fi), &sbuf) == 0 && sbuf.st_uid == DaemonUid) {
331 CronFile *file = calloc(1, sizeof(CronFile));
332 CronLine **pline;
333 time_t tnow = time(NULL);
334 tnow -= tnow % 60;
336 file->cf_UserName = strdup(userName);
337 file->cf_FileName = strdup(fileName);
338 file->cf_DPath = strdup(dpath);
339 pline = &file->cf_LineBase;
341 while (fgets(buf, sizeof(buf), fi) != NULL && --maxLines) {
342 CronLine line;
343 char *ptr = buf;
344 int len;
346 while (*ptr == ' ' || *ptr == '\t' || *ptr == '\n')
347 ++ptr;
349 len = strlen(ptr);
350 if (len && ptr[len-1] == '\n')
351 ptr[--len] = 0;
353 if (*ptr == 0 || *ptr == '#')
354 continue;
356 if (--maxEntries == 0)
357 break;
359 bzero(&line, sizeof(line));
361 if (DebugOpt)
362 logn(LOG_DEBUG, "User %s Entry %s\n", userName, buf);
364 if (*ptr == '@') {
366 * parse @hourly, etc
368 int j;
369 line.cl_Delay = -1;
370 ptr += 1;
371 for (j = 0; FreqAry[j]; ++j) {
372 if (strncmp(ptr, FreqAry[j], strlen(FreqAry[j])) == 0) {
373 break;
376 if (FreqAry[j]) {
377 ptr += strlen(FreqAry[j]);
378 switch(j) {
379 case 0:
380 /* noauto */
381 line.cl_Freq = -2;
382 line.cl_Delay = 0;
383 break;
384 case 1:
385 /* reboot */
386 line.cl_Freq = -1;
387 line.cl_Delay = 0;
388 break;
389 case 2:
390 line.cl_Freq = HOURLY_FREQ;
391 break;
392 case 3:
393 line.cl_Freq = DAILY_FREQ;
394 break;
395 case 4:
396 line.cl_Freq = WEEKLY_FREQ;
397 break;
398 case 5:
399 line.cl_Freq = MONTHLY_FREQ;
400 case 6:
401 line.cl_Freq = YEARLY_FREQ;
402 /* else line.cl_Freq will remain 0 */
406 if (!line.cl_Freq || (*ptr != ' ' && *ptr != '\t')) {
407 logn(LOG_WARNING, "failed parsing crontab for user %s: %s\n", userName, buf);
408 continue;
411 if (line.cl_Delay < 0) {
413 * delays on @daily, @hourly, etc are 1/20 of the frequency
414 * so they don't all start at once
415 * this also affects how they behave when the job returns EAGAIN
417 line.cl_Delay = line.cl_Freq / 20;
418 line.cl_Delay -= line.cl_Delay % 60;
419 if (line.cl_Delay == 0)
420 line.cl_Delay = 60;
421 /* all minutes are permitted */
422 for (j=0; j<60; ++j)
423 line.cl_Mins[j] = 1;
424 for (j=0; j<24; ++j)
425 line.cl_Hrs[j] = 1;
426 for (j=1; j<32; ++j)
427 /* days are numbered 1..31 */
428 line.cl_Days[j] = 1;
429 for (j=0; j<12; ++j)
430 line.cl_Mons[j] = 1;
433 while (*ptr == ' ' || *ptr == '\t')
434 ++ptr;
436 } else {
438 * parse date ranges
441 ptr = ParseField(file->cf_UserName, line.cl_Mins, 60, 0, 1,
442 NULL, ptr);
443 ptr = ParseField(file->cf_UserName, line.cl_Hrs, 24, 0, 1,
444 NULL, ptr);
445 ptr = ParseField(file->cf_UserName, line.cl_Days, 32, 0, 1,
446 NULL, ptr);
447 ptr = ParseField(file->cf_UserName, line.cl_Mons, 12, -1, 1,
448 MonAry, ptr);
449 ptr = ParseField(file->cf_UserName, line.cl_Dow, 7, 0, 31,
450 DowAry, ptr);
452 * check failure
455 if (ptr == NULL)
456 continue;
459 * fix days and dow - if one is not * and the other
460 * is *, the other is set to 0, and vise-versa
463 FixDayDow(&line);
466 /* check for ID=... and AFTER=... and FREQ=... */
467 do {
468 if (strncmp(ptr, ID_TAG, strlen(ID_TAG)) == 0) {
469 if (line.cl_JobName) {
470 /* only assign ID_TAG once */
471 logn(LOG_WARNING, "failed parsing crontab for user %s: repeated %s\n", userName, ptr);
472 ptr = NULL;
473 } else {
474 ptr += strlen(ID_TAG);
476 * name = strsep(&ptr, seps):
477 * return name = ptr, and if ptr contains sep chars, overwrite first with 0 and point ptr to next char
478 * else set ptr=NULL
480 asprintf(&line.cl_Description, "job %s", strsep(&ptr, " \t"));
481 line.cl_JobName = line.cl_Description + 4;
482 if (!ptr)
483 logn(LOG_WARNING, "failed parsing crontab for user %s: no command after %s%s\n", userName, ID_TAG, line.cl_JobName);
485 } else if (strncmp(ptr, FREQ_TAG, strlen(FREQ_TAG)) == 0) {
486 if (line.cl_Freq) {
487 /* only assign FREQ_TAG once */
488 logn(LOG_WARNING, "failed parsing crontab for user %s: repeated %s\n", userName, ptr);
489 ptr = NULL;
490 } else {
491 char *base = ptr;
492 ptr += strlen(FREQ_TAG);
493 ptr = ParseInterval(&line.cl_Freq, ptr);
494 if (ptr && *ptr == '/')
495 ptr = ParseInterval(&line.cl_Delay, ++ptr);
496 else
497 line.cl_Delay = line.cl_Freq;
498 if (!ptr) {
499 logn(LOG_WARNING, "failed parsing crontab for user %s: %s\n", userName, base);
500 } else if (*ptr != ' ' && *ptr != '\t') {
501 logn(LOG_WARNING, "failed parsing crontab for user %s: no command after %s\n", userName, base);
502 ptr = NULL;
505 } else if (strncmp(ptr, WAIT_TAG, strlen(WAIT_TAG)) == 0) {
506 if (line.cl_Waiters) {
507 /* only assign WAIT_TAG once */
508 logn(LOG_WARNING, "failed parsing crontab for user %s: repeated %s\n", userName, ptr);
509 ptr = NULL;
510 } else {
511 short more = 1;
512 char *name;
513 ptr += strlen(WAIT_TAG);
514 do {
515 CronLine *job, **pjob;
516 if (strcspn(ptr,",") < strcspn(ptr," \t"))
517 name = strsep(&ptr, ",");
518 else {
519 more = 0;
520 name = strsep(&ptr, " \t");
522 if (!ptr || *ptr == 0) {
523 /* unexpectedly this was the last token in buf; so abort */
524 logn(LOG_WARNING, "failed parsing crontab for user %s: no command after %s%s\n", userName, WAIT_TAG, name);
525 ptr = NULL;
526 } else {
527 int waitfor = 0;
528 char *w, *wsave;
529 if ((w = index(name, '/')) != NULL) {
530 wsave = w++;
531 w = ParseInterval(&waitfor, w);
532 if (!w || *w != 0) {
533 logn(LOG_WARNING, "failed parsing crontab for user %s: %s%s\n", userName, WAIT_TAG, name);
534 ptr = NULL;
535 } else
536 /* truncate name */
537 *wsave = 0;
539 if (ptr) {
540 /* look for a matching CronLine */
541 pjob = &file->cf_LineBase;
542 while ((job = *pjob) != NULL) {
543 if (job->cl_JobName && strcmp(job->cl_JobName, name) == 0) {
544 CronWaiter *waiter = malloc(sizeof(CronWaiter));
545 CronNotifier *notif = malloc(sizeof(CronNotifier));
546 waiter->cw_Flag = -1;
547 waiter->cw_MaxWait = waitfor;
548 waiter->cw_NotifLine = job;
549 waiter->cw_Notifier = notif;
550 waiter->cw_Next = line.cl_Waiters; /* add to head of line.cl_Waiters */
551 line.cl_Waiters = waiter;
552 notif->cn_Waiter = waiter;
553 notif->cn_Next = job->cl_Notifs; /* add to head of job->cl_Notifs */
554 job->cl_Notifs = notif;
555 break;
556 } else
557 pjob = &job->cl_Next;
559 if (!job) {
560 logn(LOG_WARNING, "failed parsing crontab for user %s: unknown job %s\n", userName, name);
561 /* we can continue parsing this line, we just don't install any CronWaiter for the requested job */
565 } while (ptr && more);
567 } else
568 break;
569 if (!ptr)
570 break;
571 while (*ptr == ' ' || *ptr == '\t')
572 ++ptr;
573 } while (!line.cl_JobName || !line.cl_Waiters || !line.cl_Freq);
575 if (line.cl_JobName && (!ptr || *line.cl_JobName == 0)) {
576 /* we're aborting, or ID= was empty */
577 free(line.cl_Description);
578 line.cl_Description = NULL;
579 line.cl_JobName = NULL;
581 if (ptr && line.cl_Delay > 0 && !line.cl_JobName) {
582 logn(LOG_WARNING, "failed parsing crontab for user %s: writing timestamp requires job %s to be named\n", userName, ptr);
583 ptr = NULL;
585 if (!ptr) {
586 /* couldn't parse so we abort; free any cl_Waiters */
587 if (line.cl_Waiters) {
588 CronWaiter **pwaiters, *waiters;
589 pwaiters = &line.cl_Waiters;
590 while ((waiters = *pwaiters) != NULL) {
591 *pwaiters = waiters->cw_Next;
592 /* leave the Notifier allocated but disabled */
593 waiters->cw_Notifier->cn_Waiter = NULL;
594 free(waiters);
597 continue;
599 /* now we've added any ID=... or AFTER=... */
602 * copy command string
604 line.cl_Shell = strdup(ptr);
606 if (line.cl_Delay > 0) {
607 asprintf(&line.cl_Timestamp, "%s/%s.%s", TSDir, userName, line.cl_JobName);
608 line.cl_NotUntil = tnow + line.cl_Delay;
611 if (line.cl_JobName) {
612 if (DebugOpt)
613 logn(LOG_DEBUG, " Command %s Job %s\n", line.cl_Shell, line.cl_JobName);
614 } else {
615 /* when cl_JobName is NULL, we point cl_Description to cl_Shell */
616 line.cl_Description = line.cl_Shell;
617 if (DebugOpt)
618 logn(LOG_DEBUG, " Command %s\n", line.cl_Shell);
621 *pline = calloc(1, sizeof(CronLine));
622 /* copy working CronLine to newly allocated one */
623 **pline = line;
625 pline = &((*pline)->cl_Next);
628 *pline = NULL;
630 file->cf_Next = FileBase;
631 FileBase = file;
633 if (maxLines == 0 || maxEntries == 0)
634 logn(LOG_WARNING, "maximum number of lines reached for user %s\n", userName);
636 fclose(fi);
638 free(path);
641 char *
642 ParseInterval(int *interval, char *ptr)
644 int n = 0;
645 if (ptr && *ptr >= '0' && *ptr <= '9' && (n = strtol(ptr, &ptr, 10)) > 0)
646 switch (*ptr) {
647 case 'm':
648 n *= 60;
649 break;
650 case 'h':
651 n *= HOURLY_FREQ;
652 break;
653 case 'd':
654 n *= DAILY_FREQ;
655 break;
656 case 'w':
657 n *= WEEKLY_FREQ;
658 break;
659 default:
660 n = 0;
662 if (n > 0) {
663 *interval = n;
664 return (ptr+1);
665 } else
666 return (NULL);
669 char *
670 ParseField(char *user, char *ary, int modvalue, int off, int onvalue, const char **names, char *ptr)
672 char *base = ptr;
673 int n1 = -1;
674 int n2 = -1;
676 if (base == NULL)
677 return (NULL);
679 while (*ptr != ' ' && *ptr != '\t' && *ptr != '\n') {
680 int skip = 0;
683 * Handle numeric digit or symbol or '*'
686 if (*ptr == '*') {
687 n1 = 0; /* everything will be filled */
688 n2 = modvalue - 1;
689 skip = 1;
690 ++ptr;
691 } else if (*ptr >= '0' && *ptr <= '9') {
692 if (n1 < 0)
693 n1 = strtol(ptr, &ptr, 10) + off;
694 else
695 n2 = strtol(ptr, &ptr, 10) + off;
696 skip = 1;
697 } else if (names) {
698 int i;
700 for (i = 0; names[i]; ++i) {
701 if (strncmp(ptr, names[i], strlen(names[i])) == 0) {
702 break;
705 if (names[i]) {
706 ptr += strlen(names[i]);
707 if (n1 < 0)
708 n1 = i;
709 else
710 n2 = i;
711 skip = 1;
716 * handle optional range '-'
719 if (skip == 0) {
720 logn(LOG_WARNING, "failed parsing crontab for user %s: %s\n", user, base);
721 return(NULL);
723 if (*ptr == '-' && n2 < 0) {
724 ++ptr;
725 continue;
729 * collapse single-value ranges, handle skipmark, and fill
730 * in the character array appropriately.
733 if (n2 < 0)
734 n2 = n1;
736 if (*ptr == '/')
737 skip = strtol(ptr + 1, &ptr, 10);
740 * fill array, using a failsafe is the easiest way to prevent
741 * an endless loop
745 int s0 = 1;
746 int failsafe = 1024;
748 --n1;
749 do {
750 n1 = (n1 + 1) % modvalue;
752 if (--s0 == 0) {
753 ary[n1] = onvalue;
754 s0 = skip;
756 } while (n1 != n2 && --failsafe);
758 if (failsafe == 0) {
759 logn(LOG_WARNING, "failed parsing crontab for user %s: %s\n", user, base);
760 return(NULL);
763 if (*ptr != ',')
764 break;
765 ++ptr;
766 n1 = -1;
767 n2 = -1;
770 if (*ptr != ' ' && *ptr != '\t' && *ptr != '\n') {
771 logn(LOG_WARNING, "failed parsing crontab for user %s: %s\n", user, base);
772 return(NULL);
775 while (*ptr == ' ' || *ptr == '\t' || *ptr == '\n')
776 ++ptr;
778 if (DebugOpt) {
779 int i;
781 for (i = 0; i < modvalue; ++i)
782 if (modvalue == 7)
783 logn(LOG_DEBUG, "%2x ", ary[i]);
784 else
785 logn(LOG_DEBUG, "%d", ary[i]);
786 logn(LOG_DEBUG, "\n");
789 return(ptr);
792 void
793 FixDayDow(CronLine *line)
795 unsigned short i,j;
796 short weekUsed = 0;
797 short daysUsed = 0;
799 for (i = 0; i < arysize(line->cl_Dow); ++i) {
800 if (line->cl_Dow[i] == 0) {
801 weekUsed = 1;
802 break;
805 for (i = 0; i < arysize(line->cl_Days); ++i) {
806 if (line->cl_Days[i] == 0) {
807 if (weekUsed) {
808 if (!daysUsed) {
809 daysUsed = 1;
810 /* change from "every Mon" to "ith Mon"
811 * 6th,7th... Dow are treated as 1st,2nd... */
812 for (j = 0; j < arysize(line->cl_Dow); ++j) {
813 line->cl_Dow[j] &= 1 << (i-1)%5;
815 } else {
816 /* change from "nth Mon" to "nth or ith Mon" */
817 for (j = 0; j < arysize(line->cl_Dow); ++j) {
818 if (line->cl_Dow[j])
819 line->cl_Dow[j] |= 1 << (i-1)%5;
822 /* continue cycling through cl_Days */
824 else {
825 daysUsed = 1;
826 break;
830 if (weekUsed) {
831 memset(line->cl_Days, 0, sizeof(line->cl_Days));
833 if (daysUsed && !weekUsed) {
834 memset(line->cl_Dow, 0, sizeof(line->cl_Dow));
839 * DeleteFile() - destroy a CronFile.
841 * The CronFile (*pfile) is destroyed if possible, and marked cf_Deleted
842 * if there are still active processes running on it. *pfile is relinked
843 * on success.
845 void
846 DeleteFile(CronFile **pfile)
848 CronFile *file = *pfile;
849 CronLine **pline = &file->cf_LineBase;
850 CronLine *line;
851 CronWaiter **pwaiters, *waiters;
852 CronNotifier **pnotifs, *notifs;
854 file->cf_Running = 0;
855 file->cf_Deleted = 1;
857 while ((line = *pline) != NULL) {
858 if (line->cl_Pid > 0) {
859 file->cf_Running = 1;
860 pline = &line->cl_Next;
861 } else {
862 *pline = line->cl_Next;
863 free(line->cl_Shell);
865 if (line->cl_JobName)
866 /* this frees both cl_Description and cl_JobName
867 * if cl_JobName is NULL, Description pointed to ch_Shell, which was already freed
869 free(line->cl_Description);
870 if (line->cl_Timestamp)
871 free(line->cl_Timestamp);
873 pnotifs = &line->cl_Notifs;
874 while ((notifs = *pnotifs) != NULL) {
875 *pnotifs = notifs->cn_Next;
876 if (notifs->cn_Waiter) {
877 notifs->cn_Waiter->cw_NotifLine = NULL;
878 notifs->cn_Waiter->cw_Notifier = NULL;
880 free(notifs);
882 pwaiters = &line->cl_Waiters;
883 while ((waiters = *pwaiters) != NULL) {
884 *pwaiters = waiters->cw_Next;
885 if (waiters->cw_Notifier)
886 waiters->cw_Notifier->cn_Waiter = NULL;
887 free(waiters);
890 free(line);
893 if (file->cf_Running == 0) {
894 *pfile = file->cf_Next;
895 free(file->cf_DPath);
896 free(file->cf_FileName);
897 free(file->cf_UserName);
898 free(file);
904 * TestJobs()
906 * determine which jobs need to be run. Under normal conditions, the
907 * period is about a minute (one scan). Worst case it will be one
908 * hour (60 scans).
912 TestJobs(time_t t1, time_t t2)
914 short nJobs = 0;
915 time_t t;
916 CronFile *file;
917 CronLine *line;
919 for (file = FileBase; file; file = file->cf_Next) {
920 if (file->cf_Deleted)
921 continue;
922 for (line = file->cf_LineBase; line; line = line->cl_Next) {
923 struct CronWaiter *waiter;
925 if (line->cl_Pid == -2) {
926 /* can job stop waiting? */
927 int ready = 1;
928 waiter = line->cl_Waiters;
929 while (waiter != NULL) {
930 if (waiter->cw_Flag > 0) {
931 /* notifier exited unsuccessfully */
932 ready = 2;
933 break;
934 } else if (waiter->cw_Flag < 0)
935 /* still waiting, notifier hasn't run to completion */
936 ready = 0;
937 waiter = waiter->cw_Next;
939 if (ready == 2) {
940 if (DebugOpt)
941 logn(LOG_DEBUG, "cancelled waiting: user %s %s\n", file->cf_UserName, line->cl_Description);
942 line->cl_Pid = 0;
943 } else if (ready) {
944 if (DebugOpt)
945 logn(LOG_DEBUG, "finished waiting: user %s %s\n", file->cf_UserName, line->cl_Description);
946 nJobs += ArmJob(file, line, 0, -1);
948 if (line->cl_NotUntil)
949 line->cl_NotUntil = t2;
957 * Find jobs > t1 and <= t2
960 for (t = t1 - t1 % 60; t <= t2; t += 60) {
961 if (t > t1) {
962 struct tm *tp = localtime(&t);
964 unsigned short n_wday = (tp->tm_mday - 1)%7 + 1;
965 if (n_wday >= 4) {
966 struct tm tnext = *tp;
967 tnext.tm_mday += 7;
968 if (mktime(&tnext) != (time_t)-1 && tnext.tm_mon != tp->tm_mon)
969 n_wday |= 16; /* last dow in month is always recognized as 5th */
972 for (file = FileBase; file; file = file->cf_Next) {
973 if (file->cf_Deleted)
974 continue;
975 for (line = file->cf_LineBase; line; line = line->cl_Next) {
976 if (line->cl_Pid != -1 && (line->cl_Freq == 0 || (line->cl_Freq > 0 && t2 >= line->cl_NotUntil))) {
977 /* (re)schedule job? */
978 if (line->cl_Mins[tp->tm_min] &&
979 line->cl_Hrs[tp->tm_hour] &&
980 (line->cl_Days[tp->tm_mday] || (n_wday && line->cl_Dow[tp->tm_wday]) ) &&
981 line->cl_Mons[tp->tm_mon]
983 if (line->cl_NotUntil)
984 line->cl_NotUntil = t2 - t2 % 60 + line->cl_Delay; /* save what minute this job was scheduled/started waiting, plus cl_Delay */
985 nJobs += ArmJob(file, line, t1, t2);
992 return(nJobs);
996 * ArmJob: if t2 is (time_t)-1, we force-schedule the job without any waiting
997 * else it will wait on any of its declared notifiers who will run <= t2 + cw_MaxWait
1001 ArmJob(CronFile *file, CronLine *line, time_t t1, time_t t2)
1003 struct CronWaiter *waiter;
1004 if (line->cl_Pid > 0) {
1005 logn(LOG_NOTICE, "process already running (%d): user %s %s\n",
1006 line->cl_Pid,
1007 file->cf_UserName,
1008 line->cl_Description
1010 } else if (t2 == -1 && line->cl_Pid != -1) {
1011 line->cl_Pid = -1;
1012 file->cf_Ready = 1;
1013 return 1;
1014 } else if (line->cl_Pid == 0) {
1015 /* arming a waiting job (cl_Pid == -2) without forcing has no effect */
1016 line->cl_Pid = -1;
1017 /* if we have any waiters, zero them and arm cl_Pid=-2 */
1018 waiter = line->cl_Waiters;
1019 while (waiter != NULL) {
1020 /* check if notifier will run <= t2 + cw_Max_Wait? */
1021 if (!waiter->cw_NotifLine)
1022 /* notifier deleted */
1023 waiter->cw_Flag = 0;
1024 else if (waiter->cw_NotifLine->cl_Pid != 0) {
1025 /* if notifier is armed, or waiting, or running, we wait for it */
1026 waiter->cw_Flag = -1;
1027 line->cl_Pid = -2;
1028 } else if (waiter->cw_NotifLine->cl_Freq < 0) {
1029 /* arm any @noauto or @reboot jobs we're waiting on */
1030 ArmJob(file, waiter->cw_NotifLine, t1, t2);
1031 waiter->cw_Flag = -1;
1032 line->cl_Pid = -2;
1033 } else {
1034 time_t t;
1035 if (waiter->cw_MaxWait == 0)
1036 /* when no MaxWait interval specified, we always wait */
1037 waiter->cw_Flag = -1;
1038 else if (waiter->cw_NotifLine->cl_Freq == 0 || (waiter->cw_NotifLine->cl_Freq > 0 && t2 + waiter->cw_MaxWait >= waiter->cw_NotifLine->cl_NotUntil)) {
1039 /* default is don't wait */
1040 waiter->cw_Flag = 0;
1041 for (t = t1 - t1 % 60; t <= t2; t += 60) {
1042 if (t > t1) {
1043 struct tm *tp = localtime(&t);
1045 unsigned short n_wday = (tp->tm_mday - 1)%7 + 1;
1046 if (n_wday >= 4) {
1047 struct tm tnext = *tp;
1048 tnext.tm_mday += 7;
1049 if (mktime(&tnext) != (time_t)-1 && tnext.tm_mon != tp->tm_mon)
1050 n_wday |= 16; /* last dow in month is always recognized as 5th */
1052 if (line->cl_Mins[tp->tm_min] &&
1053 line->cl_Hrs[tp->tm_hour] &&
1054 (line->cl_Days[tp->tm_mday] || (n_wday && line->cl_Dow[tp->tm_wday]) ) &&
1055 line->cl_Mons[tp->tm_mon]
1057 /* notifier will run soon enough, we wait for it */
1058 waiter->cw_Flag = -1;
1059 line->cl_Pid = -2;
1060 break;
1066 waiter = waiter->cw_Next;
1068 if (line->cl_Pid == -1) {
1069 /* job is ready to run */
1070 file->cf_Ready = 1;
1071 if (DebugOpt)
1072 logn(LOG_DEBUG, "scheduled: user %s %s\n",
1073 file->cf_UserName,
1074 line->cl_Description
1076 return 1;
1077 } else if (DebugOpt)
1078 logn(LOG_DEBUG, "waiting: user %s %s\n",
1079 file->cf_UserName,
1080 line->cl_Description
1083 return 0;
1087 TestStartupJobs(void)
1089 short nJobs = 0;
1090 time_t t1 = time(NULL);
1091 CronFile *file;
1092 CronLine *line;
1094 t1 = t1 - t1 % 60 + 60;
1096 for (file = FileBase; file; file = file->cf_Next) {
1097 if (DebugOpt)
1098 logn(LOG_DEBUG, "TestStartup for FILE %s/%s USER %s:\n",
1099 file->cf_DPath, file->cf_FileName, file->cf_UserName);
1100 for (line = file->cf_LineBase; line; line = line->cl_Next) {
1101 struct CronWaiter *waiter;
1102 if (DebugOpt) {
1103 if (line->cl_JobName)
1104 logn(LOG_DEBUG, " LINE %s JOB %s\n", line->cl_Shell, line->cl_JobName);
1105 else
1106 logn(LOG_DEBUG, " LINE %s\n", line->cl_Shell);
1109 if (line->cl_Freq == -1) {
1110 /* freq is @reboot */
1112 line->cl_Pid = -1;
1113 /* if we have any waiters, reset them and arm Pid = -2 */
1114 waiter = line->cl_Waiters;
1115 while (waiter != NULL) {
1116 waiter->cw_Flag = -1;
1117 line->cl_Pid = -2;
1118 /* we only arm @noauto jobs we're waiting on, not other @reboot jobs */
1119 if (waiter->cw_NotifLine && waiter->cw_NotifLine->cl_Freq == -2)
1120 ArmJob(file, waiter->cw_NotifLine, t1, t1+60);
1121 waiter = waiter->cw_Next;
1123 if (line->cl_Pid == -1) {
1124 /* job is ready to run */
1125 file->cf_Ready = 1;
1126 ++nJobs;
1127 if (DebugOpt)
1128 logn(LOG_DEBUG, " scheduled: %s\n", line->cl_Description);
1129 } else if (DebugOpt)
1130 logn(LOG_DEBUG, " waiting: %s\n", line->cl_Description);
1134 } /* for line */
1136 return(nJobs);
1139 void
1140 RunJobs(void)
1142 CronFile *file;
1143 CronLine *line;
1145 for (file = FileBase; file; file = file->cf_Next) {
1146 if (file->cf_Ready) {
1147 file->cf_Ready = 0;
1149 for (line = file->cf_LineBase; line; line = line->cl_Next) {
1150 if (line->cl_Pid == -1) {
1152 RunJob(file, line);
1154 logn(LOG_INFO, "FILE %s/%s USER %s PID %3d %s\n",
1155 file->cf_DPath,
1156 file->cf_FileName,
1157 file->cf_UserName,
1158 line->cl_Pid,
1159 line->cl_Description
1161 if (line->cl_Pid < 0)
1162 /* how could this happen? RunJob will leave cl_Pid set to 0 or the actual pid */
1163 file->cf_Ready = 1;
1164 else if (line->cl_Pid > 0)
1165 file->cf_Running = 1;
1173 * CheckJobs() - check for job completion
1175 * Check for job completion, return number of CronFiles still running after
1176 * all done.
1180 CheckJobs(void)
1182 CronFile *file;
1183 CronLine *line;
1184 int nStillRunning = 0;
1186 for (file = FileBase; file; file = file->cf_Next) {
1187 if (file->cf_Running) {
1188 file->cf_Running = 0;
1190 for (line = file->cf_LineBase; line; line = line->cl_Next) {
1191 if (line->cl_Pid > 0) {
1192 int status;
1193 int r = waitpid(line->cl_Pid, &status, WNOHANG);
1195 if (r < 0 || r == line->cl_Pid) {
1196 if (r > 0 && WIFEXITED(status))
1197 status = WEXITSTATUS(status);
1198 else
1199 status = 1;
1200 EndJob(file, line, status);
1201 if (line->cl_Pid)
1202 file->cf_Running = 1;
1203 } else if (r == 0) {
1204 file->cf_Running = 1;
1209 nStillRunning += file->cf_Running;
1211 return(nStillRunning);