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