/* crond.c - daemon to execute scheduled commands. * * Copyright 2014 Ranjan Kumar <ranjankumar.bth@gmail.com> * * No Standard USE_CROND(NEWTOY(crond, "fbSl#<0=8d#<0L:c:[-bf][-LS][-ld]", TOYFLAG_USR|TOYFLAG_SBIN|TOYFLAG_NEEDROOT)) config CROND bool "crond" default n help usage: crond [-fbS] [-l N] [-d N] [-L LOGFILE] [-c DIR] A daemon to execute scheduled commands. -b Background (default) -c crontab dir -d Set log level, log to stderr -f Foreground -l Set log level. 0 is the most verbose, default 8 -S Log to syslog (default) -L Log to file */ #define FOR_crond #include "toys.h" GLOBALS( char *crontabs_dir; char *logfile; int loglevel_d; int loglevel; time_t crontabs_dir_mtime; uint8_t flagd; ) typedef struct _var { struct _var *next, *prev; char *name, *val; } VAR; typedef struct _job { struct _job *next, *prev; char min[60], hour[24], dom[31], mon[12], dow[7], *cmd; int isrunning, needstart, mailsize; pid_t pid; } JOB; typedef struct _cronfile { struct _cronfile *next, *prev; struct double_list *job, *var; char *username, *mailto; int invalid; } CRONFILE; static char days[]={"sun""mon""tue""wed""thu""fri""sat"}; static char months[]={"jan""feb""mar""apr""may""jun""jul" "aug""sep""oct""nov""dec"}; CRONFILE *gclist; #define LOG_EXIT 0 #define LOG_LEVEL5 5 #define LOG_LEVEL7 7 #define LOG_LEVEL8 8 #define LOG_LEVEL9 9 // warning #define LOG_ERROR 20 static void loginfo(uint8_t loglevel, char *msg, ...) { va_list s, d; va_start(s, msg); va_copy(d, s); if (loglevel >= TT.loglevel) { int used; char *smsg; if (!TT.flagd && TT.logfile) { int fd = open(TT.logfile, O_WRONLY | O_CREAT | O_APPEND, 0666); if (fd==-1) perror_msg("'%s", TT.logfile); else { dup2(fd, 2); close(fd); } } used = vsnprintf(NULL, 0, msg, d); smsg = xzalloc(++used); vsnprintf(smsg, used, msg, s); if (TT.flagd || TT.logfile) { fflush(NULL); smsg[used-1] = '\n'; writeall((loglevel > 8) ? 2 : 1, smsg, used); } else syslog((loglevel > 8) ? LOG_ERR : LOG_INFO, "%s", smsg); free(smsg); } va_end(d); va_end(s); if (!loglevel) exit(20); } /* * Names can also be used for the 'month' and 'day of week' fields * (First three letters of the particular day or month). */ static int getindex(char *src, int size) { int i; char *field = (size == 12) ? months : days; // strings are not allowed for min, hour and dom fields. if (!(size == 7 || size == 12)) return -1; for (i = 0; field[i]; i += 3) { if (!strncasecmp(src, &field[i], 3)) return (i/3); } return -1; } // set elements of minute, hour, day of month, month and day of week arrays. static void fillarray(char *dst, int start, int end, int skip) { int sk = 1; if (end < 0) { dst[start] = 1; return; } if (!skip) skip = 1; do { if (!--sk) { dst[start] = 1; sk = skip; } } while (start++ != end); } static long getval(char *num, long low, long high) { long val = strtol(num, &num, 10); if (*num || (val < low) || (val > high)) return -1; return val; } //static int parse_and_fillarray(char *dst, int size, char *src) static int parse_and_fillarray(char *dst, int min, int max, char *src) { int start, end, skip = 0; char *ptr = strchr(src, '/'); if (ptr) { *ptr++ = 0; if ((skip = getval(ptr, min, (min ? max: max-1))) < 0) goto ERROR; } if (*src == '-' || *src == ',') goto ERROR; if (*src == '*') { if (*(src+1)) goto ERROR; fillarray(dst, 0, max-1, skip); } else { for (;;) { char *ctoken = strsep(&src, ","), *dtoken; if (!ctoken) break; if (!*ctoken) goto ERROR; // Get start position. dtoken = strsep(&ctoken, "-"); if (isdigit(*dtoken)) { if ((start = getval(dtoken, min, (min ? max : max-1))) < 0) goto ERROR; start = min ? (start-1) : start; } else if ((start = getindex(dtoken, max)) < 0) goto ERROR; // Get end position. if (!ctoken) end = -1; // e.g. N1,N2,N3 else if (*ctoken) {// e.g. N-M if (isdigit(*ctoken)) { if ((end = getval(ctoken, min, (min ? max : max-1))) < 0) goto ERROR; end = min ? (end-1) : end; } else if ((end = getindex(ctoken, max)) < 0) goto ERROR; if (end == start) end = -1; } else goto ERROR; // error condition 'N-' fillarray(dst, start, end, skip); } } if (TT.flagd && (TT.loglevel <= 5)) { for (start = 0; start < max; start++) fprintf(stderr, "%d", (unsigned char)dst[start]); fputc('\n', stderr); } return 0; ERROR: loginfo(LOG_LEVEL9, "parse error at %s", src); return -1; } static char *omitspace(char *line) { while (*line == ' ' || *line == '\t') line++; return line; } static void parse_line(char *line, CRONFILE *cfile) { int count = 0; char *name, *val, *tokens[5] = {0,}; VAR *v; JOB *j; line = omitspace(line); if (!*line || *line == '#') return; /* * TODO: Enhancement to support 8 special strings * @reboot -> Run once at startup. * @yearly -> Run once a year (0 0 1 1 *). * @annually -> Same as above. * @monthly -> Run once a month (0 0 1 * *). * @weekly -> Run once a week (0 0 * * 0). * @daily -> Run once a day (0 0 * * *). * @midnight -> same as above. * @hourly -> Run once an hour (0 * * * *). */ if (*line == '@') return; if (TT.flagd) loginfo(LOG_LEVEL5, "user:%s entry:%s", cfile->username, line); while (count<5) { int len = strcspn(line, " \t"); if (line[len]) line[len++] = '\0'; tokens[count++] = line; line += len; line = omitspace(line); if (!*line) break; } switch (count) { case 1: // form SHELL=/bin/sh name = tokens[0]; if ((val = strchr(name, '='))) *val++ = 0; if (!val || !*val) return; break; case 2: // form SHELL =/bin/sh or SHELL= /bin/sh name = tokens[0]; if ((val = strchr(name, '='))) { *val = 0; val = tokens[1]; } else { if (*(tokens[1]) != '=') return; val = tokens[1] + 1; } if (!*val) return; break; case 3: // NAME = VAL name = tokens[0]; val = tokens[2]; if (*(tokens[1]) != '=') return; break; case 5: // don't have any cmd to execute. if (!*line) return; j = xzalloc(sizeof(JOB)); if (parse_and_fillarray(j->min, 0, sizeof(j->min), tokens[0])) goto STOP_PARSING; if (parse_and_fillarray(j->hour, 0, sizeof(j->hour), tokens[1])) goto STOP_PARSING; if (parse_and_fillarray(j->dom, 1, sizeof(j->dom), tokens[2])) goto STOP_PARSING; if (parse_and_fillarray(j->mon, 1, sizeof(j->mon), tokens[3])) goto STOP_PARSING; if (parse_and_fillarray(j->dow, 0, sizeof(j->dow), tokens[4])) goto STOP_PARSING; j->cmd = xstrdup(line); if (TT.flagd) loginfo(LOG_LEVEL5, " command:%s", j->cmd); dlist_add_nomalloc((struct double_list **)&cfile->job, (struct double_list *)j); return; STOP_PARSING: free(j); return; default: return; } if (!strcmp(name, "MAILTO")) cfile->mailto = xstrdup(val); else { v = xzalloc(sizeof(VAR)); v->name = xstrdup(name); v->val = xstrdup(val); dlist_add_nomalloc((struct double_list **)&cfile->var, (struct double_list *)v); } } static void free_jobs(JOB **jlist) { JOB *j = dlist_pop(jlist); free(j->cmd); free(j); } static void free_cronfile(CRONFILE **list) { CRONFILE *l = dlist_pop(list); VAR *v, *vnode = (VAR *)l->var; if (l->username != l->mailto) free(l->mailto); free(l->username); while (vnode && (v = dlist_pop(&vnode))) { free(v->name); free(v->val); free(v); } free(l); } /* * Iterate all cronfiles to identify the completed jobs and freed them. * If all jobs got completed for a cronfile, freed cronfile too. */ static void remove_completed_jobs() { CRONFILE *lstart, *list = gclist; lstart = list; while (list) { int delete = 1; JOB *jstart, *jlist = (JOB *)list->job; list->invalid = 1; jstart = jlist; while (jlist) { jlist->isrunning = 0; if (jlist->pid > 0) { jlist->isrunning = 1; delete = 0; jlist = jlist->next; } else { if (jlist == jstart) { // if 1st node has to delete. jstart = jstart->next; free_jobs(&jlist); continue; } else free_jobs(&jlist); } if (jlist == jstart) break; } list->job = (struct double_list *)jlist; if (delete) { if (lstart == list) { lstart = lstart->next; free_cronfile(&list); continue; } else free_cronfile(&list); } list = list->next; if (lstart == list) break; } gclist = list; } // Scan cronfiles and prepare the list of cronfiles with their jobs. static void scan_cronfiles() { DIR *dp; struct dirent *entry; remove_completed_jobs(); if (chdir(TT.crontabs_dir)) loginfo(LOG_EXIT, "chdir(%s)", TT.crontabs_dir); if (!(dp = opendir("."))) loginfo(LOG_EXIT, "chdir(%s)", "."); while ((entry = readdir(dp))) { int fd; char *line; CRONFILE *cfile; if (entry->d_name[0] == '.' && (!entry->d_name[1] || (entry->d_name[1] == '.' && !entry->d_name[2]))) continue; if (!getpwnam(entry->d_name)) { loginfo(LOG_LEVEL7, "ignoring file '%s' (no such user)", entry->d_name); continue; } if ((fd = open(entry->d_name, O_RDONLY)) < 0) continue; // one node for each user cfile = xzalloc(sizeof(CRONFILE)); cfile->username = xstrdup(entry->d_name); for (; (line = get_line(fd)); free(line)) parse_line(line, cfile); // If there is no job for a cron, remove the VAR list. if (!cfile->job) { VAR *v, *vnode = (VAR *)cfile->var; free(cfile->username); if (cfile->mailto) free(cfile->mailto); while (vnode && (v = dlist_pop(&vnode))) { free(v->name); free(v->val); free(v); } free(cfile); } else { if (!cfile->mailto) cfile->mailto = cfile->username; dlist_add_nomalloc((struct double_list **)&gclist, (struct double_list *)cfile); } close(fd); } closedir(dp); } /* * Set env variables, if any in the cronfile. Execute given job with the given * SHELL or Default SHELL and send an e-mail with respect to every successfully * completed job (as per the given param 'prog'). */ static void do_fork(CRONFILE *cfile, JOB *job, int fd, char *prog) { pid_t pid = vfork(); if (pid == 0) { VAR *v, *vstart = (VAR *)cfile->var; struct passwd *pwd = getpwnam(cfile->username); if (!pwd) loginfo(LOG_LEVEL9, "can't get uid for %s", cfile->username); else { char *file = "/bin/sh"; if (setenv("USER", pwd->pw_name, 1)) _exit(1); for (v = vstart; v;) { if (!strcmp("SHELL", v->name)) file = v->val; if (setenv(v->name, v->val, 1)) _exit(1); if ((v=v->next) == vstart) break; } if (!getenv("HOME")) { if (setenv("HOME", pwd->pw_dir, 1)) _exit(1); } xsetuser(pwd); if (chdir(pwd->pw_dir)) loginfo(LOG_LEVEL9, "chdir(%s)", pwd->pw_dir); if (prog) file = prog; if (TT.flagd) loginfo(LOG_LEVEL5, "child running %s", file); if (fd >= 0) { int newfd = prog ? 0 : 1; if (fd != newfd) { dup2(fd, newfd); close(fd); } dup2(1, 2); } setpgrp(); execlp(file, file, (prog ? "-ti" : "-c"), (prog ? NULL : job->cmd), (char *) NULL); loginfo(LOG_ERROR, "can't execute '%s' for user %s", file, cfile->username); if (!prog) dprintf(1, "Exec failed: %s -c %s\n", file, job->cmd); _exit(EXIT_SUCCESS); } } if (pid < 0) { loginfo(LOG_ERROR, "can't vfork"); pid = 0; } if (fd >=0) close(fd); job->pid = pid; } // Send an e-mail for each successfully completed jobs. static void sendmail(CRONFILE *cfile, JOB *job) { pid_t pid = job->pid; int mailfd; struct stat sb; job->pid = 0; if (pid <=0 || job->mailsize <=0) { job->isrunning = 0; job->needstart = 1; return; } snprintf(toybuf, sizeof(toybuf), "/var/spool/cron/cron.%s.%d", cfile->username, (int)pid); mailfd = open(toybuf, O_RDONLY); unlink(toybuf); if (mailfd < 0) return; if (fstat(mailfd, &sb) == -1 || sb.st_uid != 0 || sb.st_nlink != 0 || sb.st_size == job->mailsize || !S_ISREG(sb.st_mode)) { xclose(mailfd); return; } job->mailsize = 0; do_fork(cfile, job, mailfd, "sendmail"); } // Count the number of jobs, which are not completed. static int count_running_jobs() { CRONFILE *cfile = gclist; JOB *job, *jstart; int count = 0; while (cfile) { job = jstart = (JOB *)cfile->job; while (job) { int ret; if (!job->isrunning || job->pid<=0) goto NEXT_JOB; job->isrunning = 0; ret = waitpid(job->pid, NULL, WNOHANG); if (ret < 0 || ret == job->pid) { sendmail(cfile, job); if (job->pid) count += (job->isrunning=1); else { job->isrunning = 0; job->needstart = 1; } } else count += (job->isrunning=1); NEXT_JOB: if ((job = job->next) == jstart) break; } if ((cfile = cfile->next) == gclist) break; } return count; } // Execute jobs one by one and prepare for the e-mail sending. static void execute_jobs(void) { CRONFILE *cfile = gclist; JOB *job, *jstart; while (cfile) { job = jstart = (JOB *)cfile->job; while (job) { if (job->needstart) { job->needstart = 0; if (job->pid < 0) { int mailfd = -1; job->mailsize = job->pid = 0; snprintf(toybuf, sizeof(toybuf), "/var/spool/cron/cron.%s.%d", cfile->username, getpid()); if ((mailfd = open(toybuf, O_CREAT|O_TRUNC|O_WRONLY|O_EXCL|O_APPEND, 0600)) < 0) { loginfo(LOG_ERROR, "can't create mail file %s for user %s, " "discarding output", toybuf, cfile->username); } else { dprintf(mailfd, "To: %s\nSubject: cron: %s\n\n", cfile->mailto, job->cmd); job->mailsize = lseek(mailfd, 0, SEEK_CUR); } do_fork(cfile, job, mailfd, NULL); if (mailfd >= 0) { if (job->pid <= 0) unlink(toybuf); else { char *mailfile = xmprintf("/var/spool/cron/cron.%s.%d", cfile->username, (int)job->pid); rename(toybuf, mailfile); free(mailfile); } } loginfo(LOG_LEVEL8, "USER %s pid %3d cmd %s", cfile->username, job->pid, job->cmd); if (job->pid < 0) job->needstart = 1; else job->isrunning = 1; } } if ((job = job->next) == jstart) break; } if ((cfile = cfile->next) == gclist) break; } } // Identify jobs, which needs to be started at the given time interval. static void schedule_jobs(time_t ctime, time_t ptime) { time_t tm = ptime-ptime%60; for (; tm <= ctime; tm += 60) { struct tm *lt; CRONFILE *cfile = gclist; JOB *job, *jstart; if (tm <= ptime) continue; lt = localtime(&tm); while (cfile) { if (TT.flagd) loginfo(LOG_LEVEL5, "file %s:", cfile->username); if (cfile->invalid) goto NEXT_CRONFILE; job = jstart = (JOB *)cfile->job; while (job) { if (TT.flagd) loginfo(LOG_LEVEL5, " line %s", job->cmd); if (job->min[lt->tm_min] && job->hour[lt->tm_hour] && (job->dom[lt->tm_mday] || job->dow[lt->tm_wday]) && job->mon[lt->tm_mon-1]) { if (TT.flagd) loginfo(LOG_LEVEL5, " job: %d %s\n", (int)job->pid, job->cmd); if (job->pid > 0) { loginfo(LOG_LEVEL8, "user %s: process already running: %s", cfile->username, job->cmd); } else if (!job->pid) { job->pid = -1; job->needstart = 1; job->isrunning = 0; } } if ((job = job->next) == jstart) break; } NEXT_CRONFILE: if ((cfile = cfile->next) == gclist) break; } } } void crond_main(void) { time_t ctime, ptime; int sleepfor = 60; struct stat sb; TT.flagd = (toys.optflags & FLAG_d); // Setting default params. if (TT.flagd) TT.loglevel = TT.loglevel_d; if (!(toys.optflags & (FLAG_f | FLAG_b))) toys.optflags |= FLAG_b; if (!(toys.optflags & (FLAG_S | FLAG_L))) toys.optflags |= FLAG_S; if ((toys.optflags & FLAG_c) && (TT.crontabs_dir[strlen(TT.crontabs_dir)-1] != '/')) TT.crontabs_dir = xmprintf("%s/", TT.crontabs_dir); if (!TT.crontabs_dir) TT.crontabs_dir = xstrdup("/var/spool/cron/crontabs/"); if (toys.optflags & FLAG_b) daemon(0,0); if (!TT.flagd && !TT.logfile) openlog(toys.which->name, LOG_CONS | LOG_PID, LOG_CRON); // Set default shell once. if (setenv("SHELL", "/bin/sh", 1)) error_exit("Can't set default shell"); xchdir(TT.crontabs_dir); loginfo(LOG_LEVEL8, "crond started, log level %d", TT.loglevel); if (stat(TT.crontabs_dir, &sb)) sb.st_mtime = 0; TT.crontabs_dir_mtime = sb.st_mtime; scan_cronfiles(); ctime = time(NULL); while (1) { long tdiff; ptime = ctime; sleep(sleepfor - (ptime%sleepfor) +1); tdiff =(long) ((ctime = time(NULL)) - ptime); if (stat(TT.crontabs_dir, &sb)) sb.st_mtime = 0; if (TT.crontabs_dir_mtime != sb.st_mtime) { TT.crontabs_dir_mtime = sb.st_mtime; scan_cronfiles(); } if (TT.flagd) loginfo(LOG_LEVEL5, "wakeup diff=%ld\n", tdiff); if (tdiff < -60 * 60 || tdiff > 60 * 60) loginfo(LOG_LEVEL9, "time disparity of %ld minutes detected", tdiff / 60); else if (tdiff > 0) { schedule_jobs(ctime, ptime); execute_jobs(); if (count_running_jobs()) sleepfor = 10; else sleepfor = 60; } } }