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#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"
  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
  42typedef 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
  51typedef 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, 0 if no mailfile */
  57        char *cl_mailto;                /* whom to mail results, may be NULL */
  58#endif
  59        /* ordered by size, not in natural order. makes code smaller: */
  60        char cl_Dow[7];                 /* 0-6, beginning sunday */
  61        char cl_Mons[12];               /* 0-11 */
  62        char cl_Hrs[24];                /* 0-23 */
  63        char cl_Days[32];               /* 1-31 */
  64        char cl_Mins[60];               /* 0-59 */
  65} CronLine;
  66
  67
  68#define DAEMON_UID 0
  69
  70
  71enum {
  72        OPT_l = (1 << 0),
  73        OPT_L = (1 << 1),
  74        OPT_f = (1 << 2),
  75        OPT_b = (1 << 3),
  76        OPT_S = (1 << 4),
  77        OPT_c = (1 << 5),
  78        OPT_d = (1 << 6) * ENABLE_FEATURE_CROND_D,
  79};
  80#if ENABLE_FEATURE_CROND_D
  81# define DebugOpt (option_mask32 & OPT_d)
  82#else
  83# define DebugOpt 0
  84#endif
  85
  86
  87struct globals {
  88        unsigned log_level; /* = 8; */
  89        time_t crontab_dir_mtime;
  90        const char *log_filename;
  91        const char *crontab_dir_name; /* = CRONTABS; */
  92        CronFile *cron_files;
  93#if SETENV_LEAKS
  94        char *env_var_user;
  95        char *env_var_home;
  96#endif
  97} FIX_ALIASING;
  98#define G (*(struct globals*)&bb_common_bufsiz1)
  99#define INIT_G() do { \
 100        G.log_level = 8; \
 101        G.crontab_dir_name = CRONTABS; \
 102} while (0)
 103
 104
 105/* 0 is the most verbose, default 8 */
 106#define LVL5  "\x05"
 107#define LVL7  "\x07"
 108#define LVL8  "\x08"
 109#define WARN9 "\x49"
 110#define DIE9  "\xc9"
 111/* level >= 20 is "error" */
 112#define ERR20 "\x14"
 113
 114static void crondlog(const char *ctl, ...) __attribute__ ((format (printf, 1, 2)));
 115static void crondlog(const char *ctl, ...)
 116{
 117        va_list va;
 118        int level = (ctl[0] & 0x1f);
 119
 120        va_start(va, ctl);
 121        if (level >= (int)G.log_level) {
 122                /* Debug mode: all to (non-redirected) stderr, */
 123                /* Syslog mode: all to syslog (logmode = LOGMODE_SYSLOG), */
 124                if (!DebugOpt && G.log_filename) {
 125                        /* Otherwise (log to file): we reopen log file at every write: */
 126                        int logfd = open_or_warn(G.log_filename, O_WRONLY | O_CREAT | O_APPEND);
 127                        if (logfd >= 0)
 128                                xmove_fd(logfd, STDERR_FILENO);
 129                }
 130                /* When we log to syslog, level > 8 is logged at LOG_ERR
 131                 * syslog level, level <= 8 is logged at LOG_INFO. */
 132                if (level > 8) {
 133                        bb_verror_msg(ctl + 1, va, /* strerr: */ NULL);
 134                } else {
 135                        char *msg = NULL;
 136                        vasprintf(&msg, ctl + 1, va);
 137                        bb_info_msg("%s: %s", applet_name, msg);
 138                        free(msg);
 139                }
 140        }
 141        va_end(va);
 142        if (ctl[0] & 0x80)
 143                exit(20);
 144}
 145
 146static const char DowAry[] ALIGN1 =
 147        "sun""mon""tue""wed""thu""fri""sat"
 148        /* "Sun""Mon""Tue""Wed""Thu""Fri""Sat" */
 149;
 150
 151static const char MonAry[] ALIGN1 =
 152        "jan""feb""mar""apr""may""jun""jul""aug""sep""oct""nov""dec"
 153        /* "Jan""Feb""Mar""Apr""May""Jun""Jul""Aug""Sep""Oct""Nov""Dec" */
 154;
 155
 156static void ParseField(char *user, char *ary, int modvalue, int off,
 157                                const char *names, char *ptr)
 158/* 'names' is a pointer to a set of 3-char abbreviations */
 159{
 160        char *base = ptr;
 161        int n1 = -1;
 162        int n2 = -1;
 163
 164        // this can't happen due to config_read()
 165        /*if (base == NULL)
 166                return;*/
 167
 168        while (1) {
 169                int skip = 0;
 170
 171                /* Handle numeric digit or symbol or '*' */
 172                if (*ptr == '*') {
 173                        n1 = 0;  /* everything will be filled */
 174                        n2 = modvalue - 1;
 175                        skip = 1;
 176                        ++ptr;
 177                } else if (isdigit(*ptr)) {
 178                        char *endp;
 179                        if (n1 < 0) {
 180                                n1 = strtol(ptr, &endp, 10) + off;
 181                        } else {
 182                                n2 = strtol(ptr, &endp, 10) + off;
 183                        }
 184                        ptr = endp; /* gcc likes temp var for &endp */
 185                        skip = 1;
 186                } else if (names) {
 187                        int i;
 188
 189                        for (i = 0; names[i]; i += 3) {
 190                                /* was using strncmp before... */
 191                                if (strncasecmp(ptr, &names[i], 3) == 0) {
 192                                        ptr += 3;
 193                                        if (n1 < 0) {
 194                                                n1 = i / 3;
 195                                        } else {
 196                                                n2 = i / 3;
 197                                        }
 198                                        skip = 1;
 199                                        break;
 200                                }
 201                        }
 202                }
 203
 204                /* handle optional range '-' */
 205                if (skip == 0) {
 206                        goto err;
 207                }
 208                if (*ptr == '-' && n2 < 0) {
 209                        ++ptr;
 210                        continue;
 211                }
 212
 213                /*
 214                 * collapse single-value ranges, handle skipmark, and fill
 215                 * in the character array appropriately.
 216                 */
 217                if (n2 < 0) {
 218                        n2 = n1;
 219                }
 220                if (*ptr == '/') {
 221                        char *endp;
 222                        skip = strtol(ptr + 1, &endp, 10);
 223                        ptr = endp; /* gcc likes temp var for &endp */
 224                }
 225
 226                /*
 227                 * fill array, using a failsafe is the easiest way to prevent
 228                 * an endless loop
 229                 */
 230                {
 231                        int s0 = 1;
 232                        int failsafe = 1024;
 233
 234                        --n1;
 235                        do {
 236                                n1 = (n1 + 1) % modvalue;
 237
 238                                if (--s0 == 0) {
 239                                        ary[n1 % modvalue] = 1;
 240                                        s0 = skip;
 241                                }
 242                                if (--failsafe == 0) {
 243                                        goto err;
 244                                }
 245                        } while (n1 != n2);
 246                }
 247                if (*ptr != ',') {
 248                        break;
 249                }
 250                ++ptr;
 251                n1 = -1;
 252                n2 = -1;
 253        }
 254
 255        if (*ptr) {
 256 err:
 257                crondlog(WARN9 "user %s: parse error at %s", user, base);
 258                return;
 259        }
 260
 261        if (DebugOpt && (G.log_level <= 5)) { /* like LVL5 */
 262                /* can't use crondlog, it inserts '\n' */
 263                int i;
 264                for (i = 0; i < modvalue; ++i)
 265                        fprintf(stderr, "%d", (unsigned char)ary[i]);
 266                bb_putchar_stderr('\n');
 267        }
 268}
 269
 270static void FixDayDow(CronLine *line)
 271{
 272        unsigned i;
 273        int weekUsed = 0;
 274        int daysUsed = 0;
 275
 276        for (i = 0; i < ARRAY_SIZE(line->cl_Dow); ++i) {
 277                if (line->cl_Dow[i] == 0) {
 278                        weekUsed = 1;
 279                        break;
 280                }
 281        }
 282        for (i = 0; i < ARRAY_SIZE(line->cl_Days); ++i) {
 283                if (line->cl_Days[i] == 0) {
 284                        daysUsed = 1;
 285                        break;
 286                }
 287        }
 288        if (weekUsed != daysUsed) {
 289                if (weekUsed)
 290                        memset(line->cl_Days, 0, sizeof(line->cl_Days));
 291                else /* daysUsed */
 292                        memset(line->cl_Dow, 0, sizeof(line->cl_Dow));
 293        }
 294}
 295
 296/*
 297 * delete_cronfile() - delete user database
 298 *
 299 * Note: multiple entries for same user may exist if we were unable to
 300 * completely delete a database due to running processes.
 301 */
 302//FIXME: we will start a new job even if the old job is running
 303//if crontab was reloaded: crond thinks that "new" job is different from "old"
 304//even if they are in fact completely the same. Example
 305//Crontab was:
 306// 0-59 * * * * job1
 307// 0-59 * * * * long_running_job2
 308//User edits crontab to:
 309// 0-59 * * * * job1_updated
 310// 0-59 * * * * long_running_job2
 311//Bug: crond can now start another long_running_job2 even if old one
 312//is still running.
 313//OTOH most other versions of cron do not wait for job termination anyway,
 314//they end up with multiple copies of jobs if they don't terminate soon enough.
 315static 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
 349static 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
 439static 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
 456static 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 */
 497static 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
 509static 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
 524static 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
 539static pid_t
 540fork_job(const char *user, int mailFd,
 541                const char *prog,
 542                const char *shell_cmd /* if NULL, we run sendmail */
 543) {
 544        struct passwd *pas;
 545        pid_t pid;
 546
 547        /* prepare things before vfork */
 548        pas = getpwnam(user);
 549        if (!pas) {
 550                crondlog(WARN9 "can't get uid for %s", user);
 551                goto err;
 552        }
 553        set_env_vars(pas);
 554
 555        pid = vfork();
 556        if (pid == 0) {
 557                /* CHILD */
 558                /* initgroups, setgid, setuid, and chdir to home or TMPDIR */
 559                change_user(pas);
 560                if (DebugOpt) {
 561                        crondlog(LVL5 "child running %s", prog);
 562                }
 563                if (mailFd >= 0) {
 564                        xmove_fd(mailFd, shell_cmd ? 1 : 0);
 565                        dup2(1, 2);
 566                }
 567                /* crond 3.0pl1-100 puts tasks in separate process groups */
 568                bb_setpgrp();
 569                execlp(prog, prog, (shell_cmd ? "-c" : SENDMAIL_ARGS), shell_cmd, (char *) NULL);
 570                crondlog(ERR20 "can't execute '%s' for user %s", prog, user);
 571                if (shell_cmd) {
 572                        fdprintf(1, "Exec failed: %s -c %s\n", prog, shell_cmd);
 573                }
 574                _exit(EXIT_SUCCESS);
 575        }
 576
 577        if (pid < 0) {
 578                /* FORK FAILED */
 579                crondlog(ERR20 "can't vfork");
 580 err:
 581                pid = 0;
 582        } /* else: PARENT, FORK SUCCESS */
 583
 584        /*
 585         * Close the mail file descriptor.. we can't just leave it open in
 586         * a structure, closing it later, because we might run out of descriptors
 587         */
 588        if (mailFd >= 0) {
 589                close(mailFd);
 590        }
 591        return pid;
 592}
 593
 594static void start_one_job(const char *user, CronLine *line)
 595{
 596        char mailFile[128];
 597        int mailFd = -1;
 598
 599        line->cl_pid = 0;
 600        line->cl_empty_mail_size = 0;
 601
 602        if (line->cl_mailto) {
 603                /* Open mail file (owner is root so nobody can screw with it) */
 604                snprintf(mailFile, sizeof(mailFile), "%s/cron.%s.%d", TMPDIR, user, getpid());
 605                mailFd = open(mailFile, O_CREAT | O_TRUNC | O_WRONLY | O_EXCL | O_APPEND, 0600);
 606
 607                if (mailFd >= 0) {
 608                        fdprintf(mailFd, "To: %s\nSubject: cron: %s\n\n", line->cl_mailto,
 609                                line->cl_cmd);
 610                        line->cl_empty_mail_size = lseek(mailFd, 0, SEEK_CUR);
 611                } else {
 612                        crondlog(ERR20 "can't create mail file %s for user %s, "
 613                                        "discarding output", mailFile, user);
 614                }
 615        }
 616
 617        line->cl_pid = fork_job(user, mailFd, DEFAULT_SHELL, line->cl_cmd);
 618        if (mailFd >= 0) {
 619                if (line->cl_pid <= 0) {
 620                        unlink(mailFile);
 621                } else {
 622                        /* rename mail-file based on pid of process */
 623                        char *mailFile2 = xasprintf("%s/cron.%s.%d", TMPDIR, user, (int)line->cl_pid);
 624                        rename(mailFile, mailFile2); // TODO: xrename?
 625                        free(mailFile2);
 626                }
 627        }
 628}
 629
 630/*
 631 * process_finished_job - called when job terminates and when mail terminates
 632 */
 633static void process_finished_job(const char *user, CronLine *line)
 634{
 635        pid_t pid;
 636        int mailFd;
 637        char mailFile[128];
 638        struct stat sbuf;
 639
 640        pid = line->cl_pid;
 641        line->cl_pid = 0;
 642        if (pid <= 0) {
 643                /* No job */
 644                return;
 645        }
 646        if (line->cl_empty_mail_size <= 0) {
 647                /* End of job and no mail file, or end of sendmail job */
 648                return;
 649        }
 650
 651        /*
 652         * End of primary job - check for mail file.
 653         * If size has changed and the file is still valid, we send it.
 654         */
 655        snprintf(mailFile, sizeof(mailFile), "%s/cron.%s.%d", TMPDIR, user, (int)pid);
 656        mailFd = open(mailFile, O_RDONLY);
 657        unlink(mailFile);
 658        if (mailFd < 0) {
 659                return;
 660        }
 661
 662        if (fstat(mailFd, &sbuf) < 0
 663         || sbuf.st_uid != DAEMON_UID
 664         || sbuf.st_nlink != 0
 665         || sbuf.st_size == line->cl_empty_mail_size
 666         || !S_ISREG(sbuf.st_mode)
 667        ) {
 668                close(mailFd);
 669                return;
 670        }
 671        line->cl_empty_mail_size = 0;
 672        /* if (line->cl_mailto) - always true if cl_empty_mail_size was nonzero */
 673                line->cl_pid = fork_job(user, mailFd, SENDMAIL, NULL);
 674}
 675
 676#else /* !ENABLE_FEATURE_CROND_CALL_SENDMAIL */
 677
 678static void start_one_job(const char *user, CronLine *line)
 679{
 680        struct passwd *pas;
 681        pid_t pid;
 682
 683        pas = getpwnam(user);
 684        if (!pas) {
 685                crondlog(WARN9 "can't get uid for %s", user);
 686                goto err;
 687        }
 688
 689        /* Prepare things before vfork */
 690        set_env_vars(pas);
 691
 692        /* Fork as the user in question and run program */
 693        pid = vfork();
 694        if (pid == 0) {
 695                /* CHILD */
 696                /* initgroups, setgid, setuid, and chdir to home or TMPDIR */
 697                change_user(pas);
 698                if (DebugOpt) {
 699                        crondlog(LVL5 "child running %s", DEFAULT_SHELL);
 700                }
 701                /* crond 3.0pl1-100 puts tasks in separate process groups */
 702                bb_setpgrp();
 703                execl(DEFAULT_SHELL, DEFAULT_SHELL, "-c", line->cl_cmd, (char *) NULL);
 704                crondlog(ERR20 "can't execute '%s' for user %s", DEFAULT_SHELL, user);
 705                _exit(EXIT_SUCCESS);
 706        }
 707        if (pid < 0) {
 708                /* FORK FAILED */
 709                crondlog(ERR20 "can't vfork");
 710 err:
 711                pid = 0;
 712        }
 713        line->cl_pid = pid;
 714}
 715
 716#define process_finished_job(user, line)  ((line)->cl_pid = 0)
 717
 718#endif /* !ENABLE_FEATURE_CROND_CALL_SENDMAIL */
 719
 720/*
 721 * Determine which jobs need to be run.  Under normal conditions, the
 722 * period is about a minute (one scan).  Worst case it will be one
 723 * hour (60 scans).
 724 */
 725static void flag_starting_jobs(time_t t1, time_t t2)
 726{
 727        time_t t;
 728
 729        /* Find jobs > t1 and <= t2 */
 730
 731        for (t = t1 - t1 % 60; t <= t2; t += 60) {
 732                struct tm *ptm;
 733                CronFile *file;
 734                CronLine *line;
 735
 736                if (t <= t1)
 737                        continue;
 738
 739                ptm = localtime(&t);
 740                for (file = G.cron_files; file; file = file->cf_next) {
 741                        if (DebugOpt)
 742                                crondlog(LVL5 "file %s:", file->cf_username);
 743                        if (file->cf_deleted)
 744                                continue;
 745                        for (line = file->cf_lines; line; line = line->cl_next) {
 746                                if (DebugOpt)
 747                                        crondlog(LVL5 " line %s", line->cl_cmd);
 748                                if (line->cl_Mins[ptm->tm_min]
 749                                 && line->cl_Hrs[ptm->tm_hour]
 750                                 && (line->cl_Days[ptm->tm_mday] || line->cl_Dow[ptm->tm_wday])
 751                                 && line->cl_Mons[ptm->tm_mon]
 752                                ) {
 753                                        if (DebugOpt) {
 754                                                crondlog(LVL5 " job: %d %s",
 755                                                        (int)line->cl_pid, line->cl_cmd);
 756                                        }
 757                                        if (line->cl_pid > 0) {
 758                                                crondlog(LVL8 "user %s: process already running: %s",
 759                                                        file->cf_username, line->cl_cmd);
 760                                        } else if (line->cl_pid == 0) {
 761                                                line->cl_pid = -1;
 762                                                file->cf_wants_starting = 1;
 763                                        }
 764                                }
 765                        }
 766                }
 767        }
 768}
 769
 770static void start_jobs(void)
 771{
 772        CronFile *file;
 773        CronLine *line;
 774
 775        for (file = G.cron_files; file; file = file->cf_next) {
 776                if (!file->cf_wants_starting)
 777                        continue;
 778
 779                file->cf_wants_starting = 0;
 780                for (line = file->cf_lines; line; line = line->cl_next) {
 781                        pid_t pid;
 782                        if (line->cl_pid >= 0)
 783                                continue;
 784
 785                        start_one_job(file->cf_username, line);
 786                        pid = line->cl_pid;
 787                        crondlog(LVL8 "USER %s pid %3d cmd %s",
 788                                file->cf_username, (int)pid, line->cl_cmd);
 789                        if (pid < 0) {
 790                                file->cf_wants_starting = 1;
 791                        }
 792                        if (pid > 0) {
 793                                file->cf_has_running = 1;
 794                        }
 795                }
 796        }
 797}
 798
 799/*
 800 * Check for job completion, return number of jobs still running after
 801 * all done.
 802 */
 803static int check_completions(void)
 804{
 805        CronFile *file;
 806        CronLine *line;
 807        int num_still_running = 0;
 808
 809        for (file = G.cron_files; file; file = file->cf_next) {
 810                if (!file->cf_has_running)
 811                        continue;
 812
 813                file->cf_has_running = 0;
 814                for (line = file->cf_lines; line; line = line->cl_next) {
 815                        int r;
 816
 817                        if (line->cl_pid <= 0)
 818                                continue;
 819
 820                        r = waitpid(line->cl_pid, NULL, WNOHANG);
 821                        if (r < 0 || r == line->cl_pid) {
 822                                process_finished_job(file->cf_username, line);
 823                                if (line->cl_pid == 0) {
 824                                        /* sendmail was not started for it */
 825                                        continue;
 826                                }
 827                                /* else: sendmail was started, job is still running, fall thru */
 828                        }
 829                        /* else: r == 0: "process is still running" */
 830                        file->cf_has_running = 1;
 831                }
 832//FIXME: if !file->cf_has_running && file->deleted: delete it!
 833//otherwise deleted entries will stay forever, right?
 834                num_still_running += file->cf_has_running;
 835        }
 836        return num_still_running;
 837}
 838
 839int crond_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
 840int crond_main(int argc UNUSED_PARAM, char **argv)
 841{
 842        time_t t2;
 843        int rescan;
 844        int sleep_time;
 845        unsigned opts;
 846
 847        INIT_G();
 848
 849        /* "-b after -f is ignored", and so on for every pair a-b */
 850        opt_complementary = "f-b:b-f:S-L:L-S" IF_FEATURE_CROND_D(":d-l")
 851                        ":l+:d+"; /* -l and -d have numeric param */
 852        opts = getopt32(argv, "l:L:fbSc:" IF_FEATURE_CROND_D("d:"),
 853                        &G.log_level, &G.log_filename, &G.crontab_dir_name
 854                        IF_FEATURE_CROND_D(,&G.log_level));
 855        /* both -d N and -l N set the same variable: G.log_level */
 856
 857        if (!(opts & OPT_f)) {
 858                /* close stdin, stdout, stderr.
 859                 * close unused descriptors - don't need them. */
 860                bb_daemonize_or_rexec(DAEMON_CLOSE_EXTRA_FDS, argv);
 861        }
 862
 863        if (!(opts & OPT_d) && G.log_filename == NULL) {
 864                /* logging to syslog */
 865                openlog(applet_name, LOG_CONS | LOG_PID, LOG_CRON);
 866                logmode = LOGMODE_SYSLOG;
 867        }
 868
 869        xchdir(G.crontab_dir_name);
 870        //signal(SIGHUP, SIG_IGN); /* ? original crond dies on HUP... */
 871        xsetenv("SHELL", DEFAULT_SHELL); /* once, for all future children */
 872        crondlog(LVL8 "crond (busybox "BB_VER") started, log level %d", G.log_level);
 873        rescan_crontab_dir();
 874        write_pidfile("/var/run/crond.pid");
 875
 876        /* Main loop */
 877        t2 = time(NULL);
 878        rescan = 60;
 879        sleep_time = 60;
 880        for (;;) {
 881                struct stat sbuf;
 882                time_t t1;
 883                long dt;
 884
 885                t1 = t2;
 886
 887                /* Synchronize to 1 minute, minimum 1 second */
 888                sleep(sleep_time - (time(NULL) % sleep_time) + 1);
 889
 890                t2 = time(NULL);
 891                dt = (long)t2 - (long)t1;
 892
 893                /*
 894                 * The file 'cron.update' is checked to determine new cron
 895                 * jobs.  The directory is rescanned once an hour to deal
 896                 * with any screwups.
 897                 *
 898                 * Check for time jump.  Disparities over an hour either way
 899                 * result in resynchronization.  A negative disparity
 900                 * less than an hour causes us to effectively sleep until we
 901                 * match the original time (i.e. no re-execution of jobs that
 902                 * have just been run).  A positive disparity less than
 903                 * an hour causes intermediate jobs to be run, but only once
 904                 * in the worst case.
 905                 *
 906                 * When running jobs, the inequality used is greater but not
 907                 * equal to t1, and less then or equal to t2.
 908                 */
 909                if (stat(G.crontab_dir_name, &sbuf) != 0)
 910                        sbuf.st_mtime = 0; /* force update (once) if dir was deleted */
 911                if (G.crontab_dir_mtime != sbuf.st_mtime) {
 912                        G.crontab_dir_mtime = sbuf.st_mtime;
 913                        rescan = 1;
 914                }
 915                if (--rescan == 0) {
 916                        rescan = 60;
 917                        rescan_crontab_dir();
 918                }
 919                process_cron_update_file();
 920                if (DebugOpt)
 921                        crondlog(LVL5 "wakeup dt=%ld", dt);
 922                if (dt < -60 * 60 || dt > 60 * 60) {
 923                        crondlog(WARN9 "time disparity of %ld minutes detected", dt / 60);
 924                        /* and we do not run any jobs in this case */
 925                } else if (dt > 0) {
 926                        /* Usual case: time advances forward, as expected */
 927                        flag_starting_jobs(t1, t2);
 928                        start_jobs();
 929                        if (check_completions() > 0) {
 930                                /* some jobs are still running */
 931                                sleep_time = 10;
 932                        } else {
 933                                sleep_time = 60;
 934                        }
 935                }
 936                /* else: time jumped back, do not run any jobs */
 937        } /* for (;;) */
 938
 939        return 0; /* not reached */
 940}
 941