| /* 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; |
| } |
| } |
| } |