• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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