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