1 /* crond.c - daemon to execute scheduled commands.
2 *
3 * Copyright 2014 Ranjan Kumar <ranjankumar.bth@gmail.com>
4 *
5 * No Standard
6
7 USE_CROND(NEWTOY(crond, "fbSl#<0=8d#<0L:c:[-bf][-LS][-ld]", TOYFLAG_USR|TOYFLAG_SBIN|TOYFLAG_NEEDROOT))
8
9 config CROND
10 bool "crond"
11 default n
12 help
13 usage: crond [-fbS] [-l N] [-d N] [-L LOGFILE] [-c DIR]
14
15 A daemon to execute scheduled commands.
16
17 -b Background (default)
18 -c crontab dir
19 -d Set log level, log to stderr
20 -f Foreground
21 -l Set log level. 0 is the most verbose, default 8
22 -S Log to syslog (default)
23 -L Log to file
24 */
25
26 #define FOR_crond
27 #include "toys.h"
28
29 GLOBALS(
30 char *crontabs_dir;
31 char *logfile;
32 int loglevel_d;
33 int loglevel;
34
35 time_t crontabs_dir_mtime;
36 uint8_t flagd;
37 )
38
39 typedef struct _var {
40 struct _var *next, *prev;
41 char *name, *val;
42 } VAR;
43
44 typedef struct _job {
45 struct _job *next, *prev;
46 char min[60], hour[24], dom[31], mon[12], dow[7], *cmd;
47 int isrunning, needstart, mailsize;
48 pid_t pid;
49 } JOB;
50
51 typedef struct _cronfile {
52 struct _cronfile *next, *prev;
53 struct double_list *job, *var;
54 char *username, *mailto;
55 int invalid;
56 } CRONFILE;
57
58 static char days[]={"sun""mon""tue""wed""thu""fri""sat"};
59 static char months[]={"jan""feb""mar""apr""may""jun""jul"
60 "aug""sep""oct""nov""dec"};
61 CRONFILE *gclist;
62
63 #define LOG_EXIT 0
64 #define LOG_LEVEL5 5
65 #define LOG_LEVEL7 7
66 #define LOG_LEVEL8 8
67 #define LOG_LEVEL9 9 // warning
68 #define LOG_ERROR 20
69
loginfo(uint8_t loglevel,char * msg,...)70 static void loginfo(uint8_t loglevel, char *msg, ...)
71 {
72 va_list s, d;
73
74 va_start(s, msg);
75 va_copy(d, s);
76 if (loglevel >= TT.loglevel) {
77 int used;
78 char *smsg;
79
80 if (!TT.flagd && TT.logfile) {
81 int fd = open(TT.logfile, O_WRONLY | O_CREAT | O_APPEND, 0666);
82 if (fd >=0 && fd != 2) {
83 dup2(fd, 2);
84 close(fd);
85 } else if (fd < 0) perror_msg("'%s", TT.logfile);
86 }
87 used = vsnprintf(NULL, 0, msg, d);
88 smsg = xzalloc(++used);
89 vsnprintf(smsg, used, msg, s);
90 if (TT.flagd || TT.logfile) {
91 fflush(NULL);
92 smsg[used-1] = '\n';
93 writeall((loglevel > 8) ? 2 : 1, smsg, used);
94 } else syslog((loglevel > 8) ? LOG_ERR : LOG_INFO, "%s", smsg);
95 free(smsg);
96 }
97 va_end(d);
98 va_end(s);
99 if (!loglevel) exit(20);
100 }
101
102 /*
103 * Names can also be used for the 'month' and 'day of week' fields
104 * (First three letters of the particular day or month).
105 */
getindex(char * src,int size)106 static int getindex(char *src, int size)
107 {
108 int i;
109 char *field = (size == 12) ? months : days;
110
111 // strings are not allowed for min, hour and dom fields.
112 if (!(size == 7 || size == 12)) return -1;
113
114 for (i = 0; field[i]; i += 3) {
115 if (!strncasecmp(src, &field[i], 3))
116 return (i/3);
117 }
118 return -1;
119 }
120
121 // set elements of minute, hour, day of month, month and day of week arrays.
fillarray(char * dst,int start,int end,int skip)122 static void fillarray(char *dst, int start, int end, int skip)
123 {
124 int sk = 1;
125
126 if (end < 0) {
127 dst[start] = 1;
128 return;
129 }
130 if (!skip) skip = 1;
131 do {
132 if (!--sk) {
133 dst[start] = 1;
134 sk = skip;
135 }
136 } while (start++ != end);
137 }
138
getval(char * num,long low,long high)139 static long getval(char *num, long low, long high)
140 {
141 long val = strtol(num, &num, 10);
142
143 if (*num || (val < low) || (val > high)) return -1;
144 return val;
145 }
146
147 //static int parse_and_fillarray(char *dst, int size, char *src)
parse_and_fillarray(char * dst,int min,int max,char * src)148 static int parse_and_fillarray(char *dst, int min, int max, char *src)
149 {
150 int start, end, skip = 0;
151 char *ptr = strchr(src, '/');
152
153 if (ptr) {
154 *ptr++ = 0;
155 if ((skip = getval(ptr, min, (min ? max: max-1))) < 0) goto ERROR;
156 }
157
158 if (*src == '-' || *src == ',') goto ERROR;
159 if (*src == '*') {
160 if (*(src+1)) goto ERROR;
161 fillarray(dst, 0, max-1, skip);
162 } else {
163 for (;;) {
164 char *ctoken = strsep(&src, ","), *dtoken;
165
166 if (!ctoken) break;
167 if (!*ctoken) goto ERROR;
168
169 // Get start position.
170 dtoken = strsep(&ctoken, "-");
171 if (isdigit(*dtoken)) {
172 if ((start = getval(dtoken, min, (min ? max : max-1))) < 0) goto ERROR;
173 start = min ? (start-1) : start;
174 } else if ((start = getindex(dtoken, max)) < 0) goto ERROR;
175
176 // Get end position.
177 if (!ctoken) end = -1; // e.g. N1,N2,N3
178 else if (*ctoken) {// e.g. N-M
179 if (isdigit(*ctoken)) {
180 if ((end = getval(ctoken, min, (min ? max : max-1))) < 0) goto ERROR;
181 end = min ? (end-1) : end;
182 } else if ((end = getindex(ctoken, max)) < 0) goto ERROR;
183 if (end == start) end = -1;
184 } else goto ERROR; // error condition 'N-'
185 fillarray(dst, start, end, skip);
186 }
187 }
188
189 if (TT.flagd && (TT.loglevel <= 5)) {
190 for (start = 0; start < max; start++)
191 fprintf(stderr, "%d", (unsigned char)dst[start]);
192 fputc('\n', stderr);
193 }
194 return 0;
195 ERROR:
196 loginfo(LOG_LEVEL9, "parse error at %s", src);
197 return -1;
198 }
199
omitspace(char * line)200 static char *omitspace(char *line)
201 {
202 while (*line == ' ' || *line == '\t') line++;
203 return line;
204 }
205
parse_line(char * line,CRONFILE * cfile)206 static void parse_line(char *line, CRONFILE *cfile)
207 {
208 int count = 0;
209 char *name, *val, *tokens[5] = {0,};
210 VAR *v;
211 JOB *j;
212
213 line = omitspace(line);
214 if (!*line || *line == '#') return;
215
216 /*
217 * TODO: Enhancement to support 8 special strings
218 * @reboot -> Run once at startup.
219 * @yearly -> Run once a year (0 0 1 1 *).
220 * @annually -> Same as above.
221 * @monthly -> Run once a month (0 0 1 * *).
222 * @weekly -> Run once a week (0 0 * * 0).
223 * @daily -> Run once a day (0 0 * * *).
224 * @midnight -> same as above.
225 * @hourly -> Run once an hour (0 * * * *).
226 */
227 if (*line == '@') return;
228 if (TT.flagd) loginfo(LOG_LEVEL5, "user:%s entry:%s", cfile->username, line);
229 while (count<5) {
230 int len = strcspn(line, " \t");
231
232 if (line[len]) line[len++] = '\0';
233 tokens[count++] = line;
234 line += len;
235 line = omitspace(line);
236 if (!*line) break;
237 }
238
239 switch (count) {
240 case 1: // form SHELL=/bin/sh
241 name = tokens[0];
242 if ((val = strchr(name, '='))) *val++ = 0;
243 if (!val || !*val) return;
244 break;
245 case 2: // form SHELL =/bin/sh or SHELL= /bin/sh
246 name = tokens[0];
247 if ((val = strchr(name, '='))) {
248 *val = 0;
249 val = tokens[1];
250 } else {
251 if (*(tokens[1]) != '=') return;
252 val = tokens[1] + 1;
253 }
254 if (!*val) return;
255 break;
256 case 3: // NAME = VAL
257 name = tokens[0];
258 val = tokens[2];
259 if (*(tokens[1]) != '=') return;
260 break;
261 case 5:
262 // don't have any cmd to execute.
263 if (!*line) return;
264 j = xzalloc(sizeof(JOB));
265
266 if (parse_and_fillarray(j->min, 0, sizeof(j->min), tokens[0]))
267 goto STOP_PARSING;
268 if (parse_and_fillarray(j->hour, 0, sizeof(j->hour), tokens[1]))
269 goto STOP_PARSING;
270 if (parse_and_fillarray(j->dom, 1, sizeof(j->dom), tokens[2]))
271 goto STOP_PARSING;
272 if (parse_and_fillarray(j->mon, 1, sizeof(j->mon), tokens[3]))
273 goto STOP_PARSING;
274 if (parse_and_fillarray(j->dow, 0, sizeof(j->dow), tokens[4]))
275 goto STOP_PARSING;
276 j->cmd = xstrdup(line);
277
278 if (TT.flagd) loginfo(LOG_LEVEL5, " command:%s", j->cmd);
279 dlist_add_nomalloc((struct double_list **)&cfile->job, (struct double_list *)j);
280 return;
281 STOP_PARSING:
282 free(j);
283 return;
284 default: return;
285 }
286 if (!strcmp(name, "MAILTO")) cfile->mailto = xstrdup(val);
287 else {
288 v = xzalloc(sizeof(VAR));
289 v->name = xstrdup(name);
290 v->val = xstrdup(val);
291 dlist_add_nomalloc((struct double_list **)&cfile->var, (struct double_list *)v);
292 }
293 }
294
free_jobs(JOB ** jlist)295 static void free_jobs(JOB **jlist)
296 {
297 JOB *j = dlist_pop(jlist);
298 free(j->cmd);
299 free(j);
300 }
301
free_cronfile(CRONFILE ** list)302 static void free_cronfile(CRONFILE **list)
303 {
304 CRONFILE *l = dlist_pop(list);
305 VAR *v, *vnode = (VAR *)l->var;
306
307 if (l->username != l->mailto) free(l->mailto);
308 free(l->username);
309 while (vnode && (v = dlist_pop(&vnode))) {
310 free(v->name);
311 free(v->val);
312 free(v);
313 }
314 free(l);
315 }
316
317 /*
318 * Iterate all cronfiles to identify the completed jobs and freed them.
319 * If all jobs got completed for a cronfile, freed cronfile too.
320 */
remove_completed_jobs()321 static void remove_completed_jobs()
322 {
323 CRONFILE *lstart, *list = gclist;
324
325 lstart = list;
326 while (list) {
327 int delete = 1;
328 JOB *jstart, *jlist = (JOB *)list->job;
329
330 list->invalid = 1;
331 jstart = jlist;
332 while (jlist) {
333 jlist->isrunning = 0;
334 if (jlist->pid > 0) {
335 jlist->isrunning = 1;
336 delete = 0;
337 jlist = jlist->next;
338 } else {
339 if (jlist == jstart) { // if 1st node has to delete.
340 jstart = jstart->next;
341 free_jobs(&jlist);
342 continue;
343 } else free_jobs(&jlist);
344 }
345 if (jlist == jstart) break;
346 }
347 list->job = (struct double_list *)jlist;
348
349 if (delete) {
350 if (lstart == list) {
351 lstart = lstart->next;
352 free_cronfile(&list);
353 continue;
354 } else free_cronfile(&list);
355 }
356 list = list->next;
357 if (lstart == list) break;
358 }
359 gclist = list;
360 }
361
362 // Scan cronfiles and prepare the list of cronfiles with their jobs.
scan_cronfiles()363 static void scan_cronfiles()
364 {
365 DIR *dp;
366 struct dirent *entry;
367
368 remove_completed_jobs();
369 if (chdir(TT.crontabs_dir)) loginfo(LOG_EXIT, "chdir(%s)", TT.crontabs_dir);
370 if (!(dp = opendir("."))) loginfo(LOG_EXIT, "chdir(%s)", ".");
371
372 while ((entry = readdir(dp))) {
373 int fd;
374 char *line;
375 CRONFILE *cfile;
376
377 if (entry->d_name[0] == '.' && (!entry->d_name[1] ||
378 (entry->d_name[1] == '.' && !entry->d_name[2])))
379 continue;
380
381 if (!getpwnam(entry->d_name)) {
382 loginfo(LOG_LEVEL7, "ignoring file '%s' (no such user)", entry->d_name);
383 continue;
384 }
385 if ((fd = open(entry->d_name, O_RDONLY)) < 0) continue;
386
387 // one node for each user
388 cfile = xzalloc(sizeof(CRONFILE));
389 cfile->username = xstrdup(entry->d_name);
390
391 for (; (line = get_line(fd)); free(line))
392 parse_line(line, cfile);
393
394 // If there is no job for a cron, remove the VAR list.
395 if (!cfile->job) {
396 VAR *v, *vnode = (VAR *)cfile->var;
397
398 free(cfile->username);
399 if (cfile->mailto) free(cfile->mailto);
400
401 while (vnode && (v = dlist_pop(&vnode))) {
402 free(v->name);
403 free(v->val);
404 free(v);
405 }
406 free(cfile);
407 } else {
408 if (!cfile->mailto) cfile->mailto = cfile->username;
409 dlist_add_nomalloc((struct double_list **)&gclist,
410 (struct double_list *)cfile);
411 }
412 close(fd);
413 }
414 closedir(dp);
415 }
416
417 /*
418 * Set env variables, if any in the cronfile. Execute given job with the given
419 * SHELL or Default SHELL and send an e-mail with respect to every successfully
420 * completed job (as per the given param 'prog').
421 */
do_fork(CRONFILE * cfile,JOB * job,int fd,char * prog)422 static void do_fork(CRONFILE *cfile, JOB *job, int fd, char *prog)
423 {
424 pid_t pid = vfork();
425
426 if (pid == 0) {
427 VAR *v, *vstart = (VAR *)cfile->var;
428 struct passwd *pwd = getpwnam(cfile->username);
429
430 if (!pwd) loginfo(LOG_LEVEL9, "can't get uid for %s", cfile->username);
431 else {
432 char *file = "/bin/sh";
433
434 if (setenv("USER", pwd->pw_name, 1)) _exit(1);
435 for (v = vstart; v;) {
436 if (!strcmp("SHELL", v->name)) file = v->val;
437 if (setenv(v->name, v->val, 1)) _exit(1);
438 if ((v=v->next) == vstart) break;
439 }
440 if (!getenv("HOME")) {
441 if (setenv("HOME", pwd->pw_dir, 1))
442 _exit(1);
443 }
444 xsetuser(pwd);
445 if (chdir(pwd->pw_dir)) loginfo(LOG_LEVEL9, "chdir(%s)", pwd->pw_dir);
446 if (prog) file = prog;
447 if (TT.flagd) loginfo(LOG_LEVEL5, "child running %s", file);
448
449 if (fd >= 0) {
450 int newfd = prog ? 0 : 1;
451 if (fd != newfd) {
452 dup2(fd, newfd);
453 close(fd);
454 }
455 dup2(1, 2);
456 }
457 setpgrp();
458 execlp(file, file, (prog ? "-ti" : "-c"), (prog ? NULL : job->cmd), (char *) NULL);
459 loginfo(LOG_ERROR, "can't execute '%s' for user %s", file, cfile->username);
460
461 if (!prog) dprintf(1, "Exec failed: %s -c %s\n", file, job->cmd);
462 _exit(EXIT_SUCCESS);
463 }
464 }
465 if (pid < 0) {
466 loginfo(LOG_ERROR, "can't vfork");
467 pid = 0;
468 }
469 if (fd >=0) close(fd);
470 job->pid = pid;
471 }
472
473 // Send an e-mail for each successfully completed jobs.
sendmail(CRONFILE * cfile,JOB * job)474 static void sendmail(CRONFILE *cfile, JOB *job)
475 {
476 pid_t pid = job->pid;
477 int mailfd;
478 struct stat sb;
479
480 job->pid = 0;
481 if (pid <=0 || job->mailsize <=0) {
482 job->isrunning = 0;
483 job->needstart = 1;
484 return;
485 }
486 snprintf(toybuf, sizeof(toybuf), "/var/spool/cron/cron.%s.%d",
487 cfile->username, (int)pid);
488
489 mailfd = open(toybuf, O_RDONLY);
490 unlink(toybuf);
491 if (mailfd < 0) return;
492
493 if (fstat(mailfd, &sb) == -1 || sb.st_uid != 0 || sb.st_nlink != 0
494 || sb.st_size == job->mailsize || !S_ISREG(sb.st_mode)) {
495 xclose(mailfd);
496 return;
497 }
498 job->mailsize = 0;
499 do_fork(cfile, job, mailfd, "sendmail");
500 }
501
502 // Count the number of jobs, which are not completed.
count_running_jobs()503 static int count_running_jobs()
504 {
505 CRONFILE *cfile = gclist;
506 JOB *job, *jstart;
507 int count = 0;
508
509 while (cfile) {
510 job = jstart = (JOB *)cfile->job;
511 while (job) {
512 int ret;
513
514 if (!job->isrunning || job->pid<=0) goto NEXT_JOB;
515 job->isrunning = 0;
516 ret = waitpid(job->pid, NULL, WNOHANG);
517 if (ret < 0 || ret == job->pid) {
518 sendmail(cfile, job);
519 if (job->pid) count += (job->isrunning=1);
520 else {
521 job->isrunning = 0;
522 job->needstart = 1;
523 }
524 }
525 else count += (job->isrunning=1);
526
527 NEXT_JOB:
528 if ((job = job->next) == jstart) break;
529 }
530 if ((cfile = cfile->next) == gclist) break;
531 }
532 return count;
533 }
534
535 // Execute jobs one by one and prepare for the e-mail sending.
execute_jobs(void)536 static void execute_jobs(void)
537 {
538 CRONFILE *cfile = gclist;
539 JOB *job, *jstart;
540
541 while (cfile) {
542 job = jstart = (JOB *)cfile->job;
543 while (job) {
544 if (job->needstart) {
545 job->needstart = 0;
546 if (job->pid < 0) {
547 int mailfd = -1;
548
549 job->mailsize = job->pid = 0;
550 snprintf(toybuf, sizeof(toybuf), "/var/spool/cron/cron.%s.%d",
551 cfile->username, getpid());
552 if ((mailfd = open(toybuf, O_CREAT|O_TRUNC|O_WRONLY|O_EXCL|O_APPEND,
553 0600)) < 0) {
554 loginfo(LOG_ERROR, "can't create mail file %s for user %s, "
555 "discarding output", toybuf, cfile->username);
556 } else {
557 dprintf(mailfd, "To: %s\nSubject: cron: %s\n\n", cfile->mailto, job->cmd);
558 job->mailsize = lseek(mailfd, 0, SEEK_CUR);
559 }
560 do_fork(cfile, job, mailfd, NULL);
561 if (mailfd >= 0) {
562 if (job->pid <= 0) unlink(toybuf);
563 else {
564 char *mailfile = xmprintf("/var/spool/cron/cron.%s.%d",
565 cfile->username, (int)job->pid);
566 rename(toybuf, mailfile);
567 free(mailfile);
568 }
569 }
570 loginfo(LOG_LEVEL8, "USER %s pid %3d cmd %s",
571 cfile->username, job->pid, job->cmd);
572 if (job->pid < 0) job->needstart = 1;
573 else job->isrunning = 1;
574 }
575 }
576 if ((job = job->next) == jstart) break;
577 }
578 if ((cfile = cfile->next) == gclist) break;
579 }
580 }
581
582 // Identify jobs, which needs to be started at the given time interval.
schedule_jobs(time_t ctime,time_t ptime)583 static void schedule_jobs(time_t ctime, time_t ptime)
584 {
585 time_t tm = ptime-ptime%60;
586
587 for (; tm <= ctime; tm += 60) {
588 struct tm *lt;
589 CRONFILE *cfile = gclist;
590 JOB *job, *jstart;
591
592 if (tm <= ptime) continue;
593 lt = localtime(&tm);
594
595 while (cfile) {
596 if (TT.flagd) loginfo(LOG_LEVEL5, "file %s:", cfile->username);
597 if (cfile->invalid) goto NEXT_CRONFILE;
598 job = jstart = (JOB *)cfile->job;
599
600 while (job) {
601 if (TT.flagd) loginfo(LOG_LEVEL5, " line %s", job->cmd);
602
603 if (job->min[lt->tm_min] && job->hour[lt->tm_hour]
604 && (job->dom[lt->tm_mday] || job->dow[lt->tm_wday])
605 && job->mon[lt->tm_mon-1]) {
606 if (TT.flagd)
607 loginfo(LOG_LEVEL5, " job: %d %s\n", (int)job->pid, job->cmd);
608 if (job->pid > 0) {
609 loginfo(LOG_LEVEL8, "user %s: process already running: %s",
610 cfile->username, job->cmd);
611 } else if (!job->pid) {
612 job->pid = -1;
613 job->needstart = 1;
614 job->isrunning = 0;
615 }
616 }
617 if ((job = job->next) == jstart) break;
618 }
619 NEXT_CRONFILE:
620 if ((cfile = cfile->next) == gclist) break;
621 }
622 }
623 }
624
crond_main(void)625 void crond_main(void)
626 {
627 time_t ctime, ptime;
628 int sleepfor = 60;
629 struct stat sb;
630
631 TT.flagd = (toys.optflags & FLAG_d);
632
633 // Setting default params.
634 if (TT.flagd) TT.loglevel = TT.loglevel_d;
635 if (!(toys.optflags & (FLAG_f | FLAG_b))) toys.optflags |= FLAG_b;
636 if (!(toys.optflags & (FLAG_S | FLAG_L))) toys.optflags |= FLAG_S;
637
638 if ((toys.optflags & FLAG_c)
639 && (TT.crontabs_dir[strlen(TT.crontabs_dir)-1] != '/'))
640 TT.crontabs_dir = xmprintf("%s/", TT.crontabs_dir);
641
642 if (!TT.crontabs_dir) TT.crontabs_dir = xstrdup("/var/spool/cron/crontabs/");
643 if (toys.optflags & FLAG_b) daemon(0,0);
644
645 if (!TT.flagd && !TT.logfile)
646 openlog(toys.which->name, LOG_CONS | LOG_PID, LOG_CRON);
647
648 // Set default shell once.
649 if (setenv("SHELL", "/bin/sh", 1)) error_exit("Can't set default shell");
650 xchdir(TT.crontabs_dir);
651 loginfo(LOG_LEVEL8, "crond started, log level %d", TT.loglevel);
652
653 if (stat(TT.crontabs_dir, &sb)) sb.st_mtime = 0;
654 TT.crontabs_dir_mtime = sb.st_mtime;
655 scan_cronfiles();
656 ctime = time(NULL);
657
658 while (1) {
659 long tdiff;
660
661 ptime = ctime;
662 sleep(sleepfor - (ptime%sleepfor) +1);
663 tdiff =(long) ((ctime = time(NULL)) - ptime);
664
665 if (stat(TT.crontabs_dir, &sb)) sb.st_mtime = 0;
666 if (TT.crontabs_dir_mtime != sb.st_mtime) {
667 TT.crontabs_dir_mtime = sb.st_mtime;
668 scan_cronfiles();
669 }
670
671 if (TT.flagd) loginfo(LOG_LEVEL5, "wakeup diff=%ld\n", tdiff);
672 if (tdiff < -60 * 60 || tdiff > 60 * 60)
673 loginfo(LOG_LEVEL9, "time disparity of %ld minutes detected", tdiff / 60);
674 else if (tdiff > 0) {
675 schedule_jobs(ctime, ptime);
676 execute_jobs();
677 if (count_running_jobs()) sleepfor = 10;
678 else sleepfor = 60;
679 }
680 }
681 }
682