crond: rename HumpBack names. Note two TODOs/FIXMEs
[oweals/busybox.git] / miscutils / crond.c
1 /* vi: set sw=4 ts=4: */
2 /*
3  * crond -d[#] -c <crondir> -f -b
4  *
5  * run as root, but NOT setuid root
6  *
7  * Copyright 1994 Matthew Dillon (dillon@apollo.west.oic.com)
8  * (version 2.3.2)
9  * Vladimir Oleynik <dzo@simtreas.ru> (C) 2002
10  *
11  * Licensed under the GPL v2 or later, see the file LICENSE in this tarball.
12  */
13
14 #include "libbb.h"
15 #include <syslog.h>
16
17 /* glibc frees previous setenv'ed value when we do next setenv()
18  * of the same variable. uclibc does not do this! */
19 #if (defined(__GLIBC__) && !defined(__UCLIBC__)) /* || OTHER_SAFE_LIBC... */
20 # define SETENV_LEAKS 0
21 #else
22 # define SETENV_LEAKS 1
23 #endif
24
25
26 #define TMPDIR          CONFIG_FEATURE_CROND_DIR
27 #define CRONTABS        CONFIG_FEATURE_CROND_DIR "/crontabs"
28 #ifndef SENDMAIL
29 # define SENDMAIL       "sendmail"
30 #endif
31 #ifndef SENDMAIL_ARGS
32 # define SENDMAIL_ARGS  "-ti", NULL
33 #endif
34 #ifndef CRONUPDATE
35 # define CRONUPDATE     "cron.update"
36 #endif
37 #ifndef MAXLINES
38 # define MAXLINES       256     /* max lines in non-root crontabs */
39 #endif
40
41
42 typedef struct CronFile {
43         struct CronFile *cf_next;
44         struct CronLine *cf_lines;
45         char *cf_username;
46         smallint cf_wants_starting;     /* bool: one or more jobs ready */
47         smallint cf_has_running;        /* bool: one or more jobs running */
48         smallint cf_deleted;            /* marked for deletion (but still has running jobs) */
49 } CronFile;
50
51 typedef struct CronLine {
52         struct CronLine *cl_next;
53         char *cl_cmd;                   /* shell command */
54         pid_t cl_pid;                   /* >0:running, <0:needs to be started in this minute, 0:dormant */
55 #if ENABLE_FEATURE_CROND_CALL_SENDMAIL
56         int cl_empty_mail_size;         /* size of mail header only */
57         char *cl_mailto;                /* whom to mail results, may be NULL */
58         smallint cl_mail_result;        /* mail file is created, need to send it on completion */
59 #endif
60         /* ordered by size, not in natural order. makes code smaller: */
61         char cl_Dow[7];                 /* 0-6, beginning sunday */
62         char cl_Mons[12];               /* 0-11 */
63         char cl_Hrs[24];                /* 0-23 */
64         char cl_Days[32];               /* 1-31 */
65         char cl_Mins[60];               /* 0-59 */
66 } CronLine;
67
68
69 #define DAEMON_UID 0
70
71
72 enum {
73         OPT_l = (1 << 0),
74         OPT_L = (1 << 1),
75         OPT_f = (1 << 2),
76         OPT_b = (1 << 3),
77         OPT_S = (1 << 4),
78         OPT_c = (1 << 5),
79         OPT_d = (1 << 6) * ENABLE_FEATURE_CROND_D,
80 };
81 #if ENABLE_FEATURE_CROND_D
82 # define DebugOpt (option_mask32 & OPT_d)
83 #else
84 # define DebugOpt 0
85 #endif
86
87
88 struct globals {
89         unsigned log_level; /* = 8; */
90         time_t crontab_dir_mtime;
91         const char *log_filename;
92         const char *crontab_dir_name; /* = CRONTABS; */
93         CronFile *cron_files;
94 #if SETENV_LEAKS
95         char *env_var_user;
96         char *env_var_home;
97 #endif
98 } FIX_ALIASING;
99 #define G (*(struct globals*)&bb_common_bufsiz1)
100 #define INIT_G() do { \
101         G.log_level = 8; \
102         G.crontab_dir_name = CRONTABS; \
103 } while (0)
104
105
106 /* 0 is the most verbose, default 8 */
107 #define LVL5  "\x05"
108 #define LVL7  "\x07"
109 #define LVL8  "\x08"
110 #define WARN9 "\x49"
111 #define DIE9  "\xc9"
112 /* level >= 20 is "error" */
113 #define ERR20 "\x14"
114
115 static void crondlog(const char *ctl, ...) __attribute__ ((format (printf, 1, 2)));
116 static void crondlog(const char *ctl, ...)
117 {
118         va_list va;
119         int level = (ctl[0] & 0x1f);
120
121         va_start(va, ctl);
122         if (level >= (int)G.log_level) {
123                 /* Debug mode: all to (non-redirected) stderr, */
124                 /* Syslog mode: all to syslog (logmode = LOGMODE_SYSLOG), */
125                 if (!DebugOpt && G.log_filename) {
126                         /* Otherwise (log to file): we reopen log file at every write: */
127                         int logfd = open3_or_warn(G.log_filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
128                         if (logfd >= 0)
129                                 xmove_fd(logfd, STDERR_FILENO);
130                 }
131                 /* When we log to syslog, level > 8 is logged at LOG_ERR
132                  * syslog level, level <= 8 is logged at LOG_INFO. */
133                 if (level > 8) {
134                         bb_verror_msg(ctl + 1, va, /* strerr: */ NULL);
135                 } else {
136                         char *msg = NULL;
137                         vasprintf(&msg, ctl + 1, va);
138                         bb_info_msg("%s: %s", applet_name, msg);
139                         free(msg);
140                 }
141         }
142         va_end(va);
143         if (ctl[0] & 0x80)
144                 exit(20);
145 }
146
147 static const char DowAry[] ALIGN1 =
148         "sun""mon""tue""wed""thu""fri""sat"
149         /* "Sun""Mon""Tue""Wed""Thu""Fri""Sat" */
150 ;
151
152 static const char MonAry[] ALIGN1 =
153         "jan""feb""mar""apr""may""jun""jul""aug""sep""oct""nov""dec"
154         /* "Jan""Feb""Mar""Apr""May""Jun""Jul""Aug""Sep""Oct""Nov""Dec" */
155 ;
156
157 static void ParseField(char *user, char *ary, int modvalue, int off,
158                                 const char *names, char *ptr)
159 /* 'names' is a pointer to a set of 3-char abbreviations */
160 {
161         char *base = ptr;
162         int n1 = -1;
163         int n2 = -1;
164
165         // this can't happen due to config_read()
166         /*if (base == NULL)
167                 return;*/
168
169         while (1) {
170                 int skip = 0;
171
172                 /* Handle numeric digit or symbol or '*' */
173                 if (*ptr == '*') {
174                         n1 = 0;         /* everything will be filled */
175                         n2 = modvalue - 1;
176                         skip = 1;
177                         ++ptr;
178                 } else if (isdigit(*ptr)) {
179                         char *endp;
180                         if (n1 < 0) {
181                                 n1 = strtol(ptr, &endp, 10) + off;
182                         } else {
183                                 n2 = strtol(ptr, &endp, 10) + off;
184                         }
185                         ptr = endp; /* gcc likes temp var for &endp */
186                         skip = 1;
187                 } else if (names) {
188                         int i;
189
190                         for (i = 0; names[i]; i += 3) {
191                                 /* was using strncmp before... */
192                                 if (strncasecmp(ptr, &names[i], 3) == 0) {
193                                         ptr += 3;
194                                         if (n1 < 0) {
195                                                 n1 = i / 3;
196                                         } else {
197                                                 n2 = i / 3;
198                                         }
199                                         skip = 1;
200                                         break;
201                                 }
202                         }
203                 }
204
205                 /* handle optional range '-' */
206                 if (skip == 0) {
207                         goto err;
208                 }
209                 if (*ptr == '-' && n2 < 0) {
210                         ++ptr;
211                         continue;
212                 }
213
214                 /*
215                  * collapse single-value ranges, handle skipmark, and fill
216                  * in the character array appropriately.
217                  */
218                 if (n2 < 0) {
219                         n2 = n1;
220                 }
221                 if (*ptr == '/') {
222                         char *endp;
223                         skip = strtol(ptr + 1, &endp, 10);
224                         ptr = endp; /* gcc likes temp var for &endp */
225                 }
226
227                 /*
228                  * fill array, using a failsafe is the easiest way to prevent
229                  * an endless loop
230                  */
231                 {
232                         int s0 = 1;
233                         int failsafe = 1024;
234
235                         --n1;
236                         do {
237                                 n1 = (n1 + 1) % modvalue;
238
239                                 if (--s0 == 0) {
240                                         ary[n1 % modvalue] = 1;
241                                         s0 = skip;
242                                 }
243                                 if (--failsafe == 0) {
244                                         goto err;
245                                 }
246                         } while (n1 != n2);
247
248                 }
249                 if (*ptr != ',') {
250                         break;
251                 }
252                 ++ptr;
253                 n1 = -1;
254                 n2 = -1;
255         }
256
257         if (*ptr) {
258  err:
259                 crondlog(WARN9 "user %s: parse error at %s", user, base);
260                 return;
261         }
262
263         if (DebugOpt && (G.log_level <= 5)) { /* like LVL5 */
264                 /* can't use crondlog, it inserts '\n' */
265                 int i;
266                 for (i = 0; i < modvalue; ++i)
267                         fprintf(stderr, "%d", (unsigned char)ary[i]);
268                 bb_putchar_stderr('\n');
269         }
270 }
271
272 static void FixDayDow(CronLine *line)
273 {
274         unsigned i;
275         int weekUsed = 0;
276         int daysUsed = 0;
277
278         for (i = 0; i < ARRAY_SIZE(line->cl_Dow); ++i) {
279                 if (line->cl_Dow[i] == 0) {
280                         weekUsed = 1;
281                         break;
282                 }
283         }
284         for (i = 0; i < ARRAY_SIZE(line->cl_Days); ++i) {
285                 if (line->cl_Days[i] == 0) {
286                         daysUsed = 1;
287                         break;
288                 }
289         }
290         if (weekUsed != daysUsed) {
291                 if (weekUsed)
292                         memset(line->cl_Days, 0, sizeof(line->cl_Days));
293                 else /* daysUsed */
294                         memset(line->cl_Dow, 0, sizeof(line->cl_Dow));
295         }
296 }
297
298 /*
299  * delete_cronfile() - delete user database
300  *
301  * Note: multiple entries for same user may exist if we were unable to
302  * completely delete a database due to running processes.
303  */
304 //FIXME: we will start a new job even if the old job is running
305 //if crontab was reloaded: crond thinks that "new" job is different from "old"
306 //even if they are in fact completely the same. Example
307 //Crontab was:
308 // 0-59 * * * job1
309 // 0-59 * * * long_running_job2
310 //User edits crontab to:
311 // 0-59 * * * job1_updated
312 // 0-59 * * * long_running_job2
313 //Bug: crond can now start another long_running_job2 even if old one
314 //is still running.
315 static void delete_cronfile(const char *userName)
316 {
317         CronFile **pfile = &G.cron_files;
318         CronFile *file;
319
320         while ((file = *pfile) != NULL) {
321                 if (strcmp(userName, file->cf_username) == 0) {
322                         CronLine **pline = &file->cf_lines;
323                         CronLine *line;
324
325                         file->cf_has_running = 0;
326                         file->cf_deleted = 1;
327
328                         while ((line = *pline) != NULL) {
329                                 if (line->cl_pid > 0) {
330                                         file->cf_has_running = 1;
331                                         pline = &line->cl_next;
332                                 } else {
333                                         *pline = line->cl_next;
334                                         free(line->cl_cmd);
335                                         free(line);
336                                 }
337                         }
338                         if (file->cf_has_running == 0) {
339                                 *pfile = file->cf_next;
340                                 free(file->cf_username);
341                                 free(file);
342                                 continue;
343                         }
344                 }
345                 pfile = &file->cf_next;
346         }
347 }
348
349 static void load_crontab(const char *fileName)
350 {
351         struct parser_t *parser;
352         struct stat sbuf;
353         int maxLines;
354         char *tokens[6];
355 #if ENABLE_FEATURE_CROND_CALL_SENDMAIL
356         char *mailTo = NULL;
357 #endif
358
359         delete_cronfile(fileName);
360
361         if (!getpwnam(fileName)) {
362                 crondlog(LVL7 "ignoring file '%s' (no such user)", fileName);
363                 return;
364         }
365
366         parser = config_open(fileName);
367         if (!parser)
368                 return;
369
370         maxLines = (strcmp(fileName, "root") == 0) ? 65535 : MAXLINES;
371
372         if (fstat(fileno(parser->fp), &sbuf) == 0 && sbuf.st_uid == DAEMON_UID) {
373                 CronFile *file = xzalloc(sizeof(CronFile));
374                 CronLine **pline;
375                 int n;
376
377                 file->cf_username = xstrdup(fileName);
378                 pline = &file->cf_lines;
379
380                 while (1) {
381                         CronLine *line;
382
383                         if (!--maxLines)
384                                 break;
385                         n = config_read(parser, tokens, 6, 1, "# \t", PARSE_NORMAL | PARSE_KEEP_COPY);
386                         if (!n)
387                                 break;
388
389                         if (DebugOpt)
390                                 crondlog(LVL5 "user:%s entry:%s", fileName, parser->data);
391
392                         /* check if line is setting MAILTO= */
393                         if (0 == strncmp(tokens[0], "MAILTO=", 7)) {
394 #if ENABLE_FEATURE_CROND_CALL_SENDMAIL
395                                 free(mailTo);
396                                 mailTo = (tokens[0][7]) ? xstrdup(&tokens[0][7]) : NULL;
397 #endif /* otherwise just ignore such lines */
398                                 continue;
399                         }
400                         /* check if a minimum of tokens is specified */
401                         if (n < 6)
402                                 continue;
403                         *pline = line = xzalloc(sizeof(*line));
404                         /* parse date ranges */
405                         ParseField(file->cf_username, line->cl_Mins, 60, 0, NULL, tokens[0]);
406                         ParseField(file->cf_username, line->cl_Hrs, 24, 0, NULL, tokens[1]);
407                         ParseField(file->cf_username, line->cl_Days, 32, 0, NULL, tokens[2]);
408                         ParseField(file->cf_username, line->cl_Mons, 12, -1, MonAry, tokens[3]);
409                         ParseField(file->cf_username, line->cl_Dow, 7, 0, DowAry, tokens[4]);
410                         /*
411                          * fix days and dow - if one is not "*" and the other
412                          * is "*", the other is set to 0, and vise-versa
413                          */
414                         FixDayDow(line);
415 #if ENABLE_FEATURE_CROND_CALL_SENDMAIL
416                         /* copy mailto (can be NULL) */
417                         line->cl_mailto = xstrdup(mailTo);
418 #endif
419                         /* copy command */
420                         line->cl_cmd = xstrdup(tokens[5]);
421                         if (DebugOpt) {
422                                 crondlog(LVL5 " command:%s", tokens[5]);
423                         }
424                         pline = &line->cl_next;
425 //bb_error_msg("M[%s]F[%s][%s][%s][%s][%s][%s]", mailTo, tokens[0], tokens[1], tokens[2], tokens[3], tokens[4], tokens[5]);
426                 }
427                 *pline = NULL;
428
429                 file->cf_next = G.cron_files;
430                 G.cron_files = file;
431
432                 if (maxLines == 0) {
433                         crondlog(WARN9 "user %s: too many lines", fileName);
434                 }
435         }
436         config_close(parser);
437 }
438
439 static void process_cron_update_file(void)
440 {
441         FILE *fi;
442         char buf[256];
443
444         fi = fopen_for_read(CRONUPDATE);
445         if (fi != NULL) {
446                 unlink(CRONUPDATE);
447                 while (fgets(buf, sizeof(buf), fi) != NULL) {
448                         /* use first word only */
449                         skip_non_whitespace(buf)[0] = '\0';
450                         load_crontab(buf);
451                 }
452                 fclose(fi);
453         }
454 }
455
456 static void rescan_crontab_dir(void)
457 {
458         CronFile *file;
459
460         /* Delete all files until we only have ones with running jobs (or none) */
461  again:
462         for (file = G.cron_files; file; file = file->cf_next) {
463                 if (!file->cf_deleted) {
464                         delete_cronfile(file->cf_username);
465                         goto again;
466                 }
467         }
468
469         /* Remove cron update file */
470         unlink(CRONUPDATE);
471         /* Re-chdir, in case directory was renamed & deleted */
472         if (chdir(G.crontab_dir_name) < 0) {
473                 crondlog(DIE9 "chdir(%s)", G.crontab_dir_name);
474         }
475
476         /* Scan directory and add associated users */
477         {
478                 DIR *dir = opendir(".");
479                 struct dirent *den;
480
481                 if (!dir)
482                         crondlog(DIE9 "chdir(%s)", "."); /* exits */
483                 while ((den = readdir(dir)) != NULL) {
484                         if (strchr(den->d_name, '.') != NULL) {
485                                 continue;
486                         }
487                         load_crontab(den->d_name);
488                 }
489                 closedir(dir);
490         }
491 }
492
493 #if SETENV_LEAKS
494 /* We set environment *before* vfork (because we want to use vfork),
495  * so we cannot use setenv() - repeated calls to setenv() may leak memory!
496  * Using putenv(), and freeing memory after unsetenv() won't leak */
497 static void safe_setenv(char **pvar_val, const char *var, const char *val)
498 {
499         char *var_val = *pvar_val;
500
501         if (var_val) {
502                 bb_unsetenv_and_free(var_val);
503         }
504         *pvar_val = xasprintf("%s=%s", var, val);
505         putenv(*pvar_val);
506 }
507 #endif
508
509 static void set_env_vars(struct passwd *pas)
510 {
511 #if SETENV_LEAKS
512         safe_setenv(&G.env_var_user, "USER", pas->pw_name);
513         safe_setenv(&G.env_var_home, "HOME", pas->pw_dir);
514         /* if we want to set user's shell instead: */
515         /*safe_setenv(G.env_var_shell, "SHELL", pas->pw_shell);*/
516 #else
517         xsetenv("USER", pas->pw_name);
518         xsetenv("HOME", pas->pw_dir);
519 #endif
520         /* currently, we use constant one: */
521         /*setenv("SHELL", DEFAULT_SHELL, 1); - done earlier */
522 }
523
524 static void change_user(struct passwd *pas)
525 {
526         /* careful: we're after vfork! */
527         change_identity(pas); /* - initgroups, setgid, setuid */
528         if (chdir(pas->pw_dir) < 0) {
529                 crondlog(WARN9 "chdir(%s)", pas->pw_dir);
530                 if (chdir(TMPDIR) < 0) {
531                         crondlog(DIE9 "chdir(%s)", TMPDIR); /* exits */
532                 }
533         }
534 }
535
536 // TODO: sendmail should be _run-time_ option, not compile-time!
537 #if ENABLE_FEATURE_CROND_CALL_SENDMAIL
538
539 //TODO: return pid (and stop passing line);
540 // stop passing mail_filename here but process it in caller
541 static void
542 fork_job(const char *user, CronLine *line, int mailFd,
543                 const char *prog, const char *cmd, const char *arg,
544                 const char *mail_filename)
545 {
546         struct passwd *pas;
547         pid_t pid;
548
549         /* prepare things before vfork */
550         pas = getpwnam(user);
551         if (!pas) {
552                 crondlog(WARN9 "can't get uid for %s", user);
553                 goto err;
554         }
555         set_env_vars(pas);
556
557         pid = vfork();
558         if (pid == 0) {
559                 /* CHILD */
560                 /* initgroups, setgid, setuid, and chdir to home or TMPDIR */
561                 change_user(pas);
562                 if (DebugOpt) {
563                         crondlog(LVL5 "child running %s", prog);
564                 }
565                 if (mailFd >= 0) {
566                         xmove_fd(mailFd, mail_filename ? 1 : 0);
567                         dup2(1, 2);
568                 }
569                 /* crond 3.0pl1-100 puts tasks in separate process groups */
570                 bb_setpgrp();
571                 execlp(prog, prog, cmd, arg, (char *) NULL);
572                 crondlog(ERR20 "can't exec, user %s cmd %s %s %s", user, prog, cmd, arg);
573                 if (mail_filename) {
574                         fdprintf(1, "Exec failed: %s -c %s\n", prog, arg);
575                 }
576                 _exit(EXIT_SUCCESS);
577         }
578
579         line->cl_pid = pid;
580         if (pid < 0) {
581                 /* FORK FAILED */
582                 crondlog(ERR20 "can't vfork");
583  err:
584                 line->cl_pid = 0;
585                 if (mail_filename) {
586                         unlink(mail_filename);
587                 }
588         } else {
589                 /* PARENT, FORK SUCCESS */
590                 if (mail_filename) {
591                         /* rename mail-file based on pid of process */
592                         char *mailFile2 = xasprintf("%s/cron.%s.%d", TMPDIR, user, (int)pid);
593                         rename(mail_filename, mailFile2); // TODO: xrename?
594                         free(mailFile2);
595                 }
596         }
597
598         /*
599          * Close the mail file descriptor.. we can't just leave it open in
600          * a structure, closing it later, because we might run out of descriptors
601          */
602         if (mailFd >= 0) {
603                 close(mailFd);
604         }
605 }
606
607 static void start_one_job(const char *user, CronLine *line)
608 {
609         char mailFile[128];
610         int mailFd = -1;
611
612         line->cl_pid = 0;
613         line->cl_mail_result = 0;
614
615         if (line->cl_mailto) {
616                 /* Open mail file (owner is root so nobody can screw with it) */
617                 snprintf(mailFile, sizeof(mailFile), "%s/cron.%s.%d", TMPDIR, user, getpid());
618                 mailFd = open(mailFile, O_CREAT | O_TRUNC | O_WRONLY | O_EXCL | O_APPEND, 0600);
619
620                 if (mailFd >= 0) {
621                         line->cl_mail_result = 1;
622                         fdprintf(mailFd, "To: %s\nSubject: cron: %s\n\n", line->cl_mailto,
623                                 line->cl_cmd);
624                         line->cl_empty_mail_size = lseek(mailFd, 0, SEEK_CUR);
625                 } else {
626                         crondlog(ERR20 "can't create mail file %s for user %s, "
627                                         "discarding output", mailFile, user);
628                 }
629         }
630
631         fork_job(user, line, mailFd, DEFAULT_SHELL, "-c", line->cl_cmd, mailFile);
632 }
633
634 /*
635  * process_finished_job - called when job terminates and when mail terminates
636  */
637 static void process_finished_job(const char *user, CronLine *line)
638 {
639         pid_t pid;
640         int mailFd;
641         char mailFile[128];
642         struct stat sbuf;
643
644         pid = line->cl_pid;
645         line->cl_pid = 0;
646         if (pid <= 0) {
647                 /* No job */
648                 return;
649         }
650         if (line->cl_mail_result == 0) {
651                 /* End of job and no mail file, or end of sendmail job */
652                 return;
653         }
654         line->cl_mail_result = 0;
655
656         /*
657          * End of primary job - check for mail file.
658          * If size has changed and the file is still valid, we send it.
659          */
660         snprintf(mailFile, sizeof(mailFile), "%s/cron.%s.%d", TMPDIR, user, (int)pid);
661         mailFd = open(mailFile, O_RDONLY);
662         unlink(mailFile);
663         if (mailFd < 0) {
664                 return;
665         }
666
667         if (fstat(mailFd, &sbuf) < 0
668          || sbuf.st_uid != DAEMON_UID
669          || sbuf.st_nlink != 0
670          || sbuf.st_size == line->cl_empty_mail_size
671          || !S_ISREG(sbuf.st_mode)
672         ) {
673                 close(mailFd);
674                 return;
675         }
676         /* if (line->cl_mailto) - always true if cl_mail_result was true */
677                 fork_job(user, line, mailFd, SENDMAIL, SENDMAIL_ARGS, NULL);
678 }
679
680 #else /* !ENABLE_FEATURE_CROND_CALL_SENDMAIL */
681
682 static void start_one_job(const char *user, CronLine *line)
683 {
684         struct passwd *pas;
685         pid_t pid;
686
687         pas = getpwnam(user);
688         if (!pas) {
689                 crondlog(WARN9 "can't get uid for %s", user);
690                 goto err;
691         }
692
693         /* Prepare things before vfork */
694         set_env_vars(pas);
695
696         /* Fork as the user in question and run program */
697         pid = vfork();
698         if (pid == 0) {
699                 /* CHILD */
700                 /* initgroups, setgid, setuid, and chdir to home or TMPDIR */
701                 change_user(pas);
702                 if (DebugOpt) {
703                         crondlog(LVL5 "child running %s", DEFAULT_SHELL);
704                 }
705                 /* crond 3.0pl1-100 puts tasks in separate process groups */
706                 bb_setpgrp();
707                 execl(DEFAULT_SHELL, DEFAULT_SHELL, "-c", line->cl_cmd, (char *) NULL);
708                 crondlog(ERR20 "can't execute '%s' for user %s", DEFAULT_SHELL, user);
709                 _exit(EXIT_SUCCESS);
710         }
711         if (pid < 0) {
712                 /* FORK FAILED */
713                 crondlog(ERR20 "can't vfork");
714  err:
715                 pid = 0;
716         }
717         line->cl_pid = pid;
718 }
719
720 #define process_finished_job(user, line)  ((line)->cl_pid = 0)
721
722 #endif /* !ENABLE_FEATURE_CROND_CALL_SENDMAIL */
723
724 /*
725  * Determine which jobs need to be run.  Under normal conditions, the
726  * period is about a minute (one scan).  Worst case it will be one
727  * hour (60 scans).
728  */
729 static void flag_starting_jobs(time_t t1, time_t t2)
730 {
731         time_t t;
732
733         /* Find jobs > t1 and <= t2 */
734
735         for (t = t1 - t1 % 60; t <= t2; t += 60) {
736                 struct tm *ptm;
737                 CronFile *file;
738                 CronLine *line;
739
740                 if (t <= t1)
741                         continue;
742
743                 ptm = localtime(&t);
744                 for (file = G.cron_files; file; file = file->cf_next) {
745                         if (DebugOpt)
746                                 crondlog(LVL5 "file %s:", file->cf_username);
747                         if (file->cf_deleted)
748                                 continue;
749                         for (line = file->cf_lines; line; line = line->cl_next) {
750                                 if (DebugOpt)
751                                         crondlog(LVL5 " line %s", line->cl_cmd);
752                                 if (line->cl_Mins[ptm->tm_min]
753                                  && line->cl_Hrs[ptm->tm_hour]
754                                  && (line->cl_Days[ptm->tm_mday] || line->cl_Dow[ptm->tm_wday])
755                                  && line->cl_Mons[ptm->tm_mon]
756                                 ) {
757                                         if (DebugOpt) {
758                                                 crondlog(LVL5 " job: %d %s",
759                                                         (int)line->cl_pid, line->cl_cmd);
760                                         }
761                                         if (line->cl_pid > 0) {
762                                                 crondlog(LVL8 "user %s: process already running: %s",
763                                                         file->cf_username, line->cl_cmd);
764                                         } else if (line->cl_pid == 0) {
765                                                 line->cl_pid = -1;
766                                                 file->cf_wants_starting = 1;
767                                         }
768                                 }
769                         }
770                 }
771         }
772 }
773
774 static void start_jobs(void)
775 {
776         CronFile *file;
777         CronLine *line;
778
779         for (file = G.cron_files; file; file = file->cf_next) {
780                 if (!file->cf_wants_starting)
781                         continue;
782
783                 file->cf_wants_starting = 0;
784                 for (line = file->cf_lines; line; line = line->cl_next) {
785                         pid_t pid;
786                         if (line->cl_pid >= 0)
787                                 continue;
788
789                         start_one_job(file->cf_username, line);
790                         pid = line->cl_pid;
791                         crondlog(LVL8 "USER %s pid %3d cmd %s",
792                                 file->cf_username, (int)pid, line->cl_cmd);
793                         if (pid < 0) {
794                                 file->cf_wants_starting = 1;
795                         }
796                         if (pid > 0) {
797                                 file->cf_has_running = 1;
798                         }
799                 }
800         }
801 }
802
803 /*
804  * Check for job completion, return number of jobs still running after
805  * all done.
806  */
807 static int check_completions(void)
808 {
809         CronFile *file;
810         CronLine *line;
811         int num_still_running = 0;
812
813         for (file = G.cron_files; file; file = file->cf_next) {
814                 if (!file->cf_has_running)
815                         continue;
816
817                 file->cf_has_running = 0;
818                 for (line = file->cf_lines; line; line = line->cl_next) {
819                         int r;
820
821                         if (line->cl_pid <= 0)
822                                 continue;
823
824                         r = waitpid(line->cl_pid, NULL, WNOHANG);
825                         if (r < 0 || r == line->cl_pid) {
826                                 process_finished_job(file->cf_username, line);
827                                 if (line->cl_pid == 0) {
828                                         /* sendmail was not started for it */
829                                         continue;
830                                 }
831                                 /* else: sendmail was started, job is still running, fall thru */
832                         }
833                         /* else: r == 0: "process is still running" */
834                         file->cf_has_running = 1;
835                 }
836 //FIXME: if !file->cf_has_running && file->deleted: delete it!
837 //otherwise deleted entries will stay forever, right?
838                 num_still_running += file->cf_has_running;
839         }
840         return num_still_running;
841 }
842
843 int crond_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
844 int crond_main(int argc UNUSED_PARAM, char **argv)
845 {
846         time_t t2;
847         int rescan;
848         int sleep_time;
849         unsigned opts;
850
851         INIT_G();
852
853         /* "-b after -f is ignored", and so on for every pair a-b */
854         opt_complementary = "f-b:b-f:S-L:L-S" IF_FEATURE_CROND_D(":d-l")
855                         ":l+:d+"; /* -l and -d have numeric param */
856         opts = getopt32(argv, "l:L:fbSc:" IF_FEATURE_CROND_D("d:"),
857                         &G.log_level, &G.log_filename, &G.crontab_dir_name
858                         IF_FEATURE_CROND_D(,&G.log_level));
859         /* both -d N and -l N set the same variable: G.log_level */
860
861         if (!(opts & OPT_f)) {
862                 /* close stdin, stdout, stderr.
863                  * close unused descriptors - don't need them. */
864                 bb_daemonize_or_rexec(DAEMON_CLOSE_EXTRA_FDS, argv);
865         }
866
867         if (!(opts & OPT_d) && G.log_filename == NULL) {
868                 /* logging to syslog */
869                 openlog(applet_name, LOG_CONS | LOG_PID, LOG_CRON);
870                 logmode = LOGMODE_SYSLOG;
871         }
872
873         xchdir(G.crontab_dir_name);
874         //signal(SIGHUP, SIG_IGN); /* ? original crond dies on HUP... */
875         xsetenv("SHELL", DEFAULT_SHELL); /* once, for all future children */
876         crondlog(LVL8 "crond (busybox "BB_VER") started, log level %d", G.log_level);
877         rescan_crontab_dir();
878         write_pidfile("/var/run/crond.pid");
879
880         /* Main loop */
881         t2 = time(NULL);
882         rescan = 60;
883         sleep_time = 60;
884         for (;;) {
885                 struct stat sbuf;
886                 time_t t1;
887                 long dt;
888
889                 t1 = t2;
890
891                 /* Synchronize to 1 minute, minimum 1 second */
892                 sleep(sleep_time - (time(NULL) % sleep_time) + 1);
893
894                 t2 = time(NULL);
895                 dt = (long)t2 - (long)t1;
896
897                 /*
898                  * The file 'cron.update' is checked to determine new cron
899                  * jobs.  The directory is rescanned once an hour to deal
900                  * with any screwups.
901                  *
902                  * Check for time jump.  Disparities over an hour either way
903                  * result in resynchronization.  A negative disparity
904                  * less than an hour causes us to effectively sleep until we
905                  * match the original time (i.e. no re-execution of jobs that
906                  * have just been run).  A positive disparity less than
907                  * an hour causes intermediate jobs to be run, but only once
908                  * in the worst case.
909                  *
910                  * When running jobs, the inequality used is greater but not
911                  * equal to t1, and less then or equal to t2.
912                  */
913                 if (stat(G.crontab_dir_name, &sbuf) != 0)
914                         sbuf.st_mtime = 0; /* force update (once) if dir was deleted */
915                 if (G.crontab_dir_mtime != sbuf.st_mtime) {
916                         G.crontab_dir_mtime = sbuf.st_mtime;
917                         rescan = 1;
918                 }
919                 if (--rescan == 0) {
920                         rescan = 60;
921                         rescan_crontab_dir();
922                 }
923                 process_cron_update_file();
924                 if (DebugOpt)
925                         crondlog(LVL5 "wakeup dt=%ld", dt);
926                 if (dt < -60 * 60 || dt > 60 * 60) {
927                         crondlog(WARN9 "time disparity of %ld minutes detected", dt / 60);
928                         /* and we do not run any jobs in this case */
929                 } else if (dt > 0) {
930                         /* Usual case: time advances forward, as expected */
931                         flag_starting_jobs(t1, t2);
932                         start_jobs();
933                         if (check_completions() > 0) {
934                                 /* some jobs are still running */
935                                 sleep_time = 10;
936                         } else {
937                                 sleep_time = 60;
938                         }
939                 }
940                 /* else: time jumped back, do not run any jobs */
941         } /* for (;;) */
942
943         return 0; /* not reached */
944 }