source: MondoRescue/branches/3.3/mindi-busybox/miscutils/crond.c@ 3865

Last change on this file since 3865 was 3621, checked in by Bruno Cornec, 10 years ago

New 3?3 banch for incorporation of latest busybox 1.25. Changing minor version to handle potential incompatibilities.

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