bc: convert all status codes, remove bc_err_msgs[], bc_vm_error(), bc_vm_posixError()
[oweals/busybox.git] / miscutils / crond.c
index 88e7b47b3ff13c43001bdd98e31836e7d1cbc4b9..f6580a9d41543427a0facecb0c169421bbfc1a13 100644 (file)
@@ -9,38 +9,54 @@
  * Licensed under GPLv2 or later, see file LICENSE in this source tree.
  */
 //config:config CROND
-//config:      bool "crond"
+//config:      bool "crond (13 kb)"
 //config:      default y
 //config:      select FEATURE_SYSLOG
 //config:      help
-//config:        Crond is a background daemon that parses individual crontab
-//config:        files and executes commands on behalf of the users in question.
-//config:        This is a port of dcron from slackware. It uses files of the
-//config:        format /var/spool/cron/crontabs/<username> files, for example:
-//config:            $ cat /var/spool/cron/crontabs/root
-//config:            # Run daily cron jobs at 4:40 every day:
-//config:            40 4 * * * /etc/cron/daily > /dev/null 2>&1
+//config:      Crond is a background daemon that parses individual crontab
+//config:      files and executes commands on behalf of the users in question.
+//config:      This is a port of dcron from slackware. It uses files of the
+//config:      format /var/spool/cron/crontabs/<username> files, for example:
+//config:              $ cat /var/spool/cron/crontabs/root
+//config:              # Run daily cron jobs at 4:40 every day:
+//config:              40 4 * * * /etc/cron/daily > /dev/null 2>&1
 //config:
 //config:config FEATURE_CROND_D
-//config:      bool "Support option -d to redirect output to stderr"
+//config:      bool "Support -d (redirect output to stderr)"
 //config:      depends on CROND
 //config:      default y
 //config:      help
-//config:        -d N sets loglevel (0:most verbose) and directs all output to stderr.
+//config:      -d N sets loglevel (0:most verbose) and directs all output to stderr.
 //config:
 //config:config FEATURE_CROND_CALL_SENDMAIL
 //config:      bool "Report command output via email (using sendmail)"
 //config:      default y
 //config:      depends on CROND
 //config:      help
-//config:        Command output will be sent to corresponding user via email.
+//config:      Command output will be sent to corresponding user via email.
+//config:
+//config:config FEATURE_CROND_SPECIAL_TIMES
+//config:      bool "Support special times (@reboot, @daily, etc) in crontabs"
+//config:      default y
+//config:      depends on CROND
+//config:      help
+//config:      string        meaning
+//config:      ------        -------
+//config:      @reboot       Run once, at startup
+//config:      @yearly       Run once a year:  "0 0 1 1 *"
+//config:      @annually     Same as @yearly:  "0 0 1 1 *"
+//config:      @monthly      Run once a month: "0 0 1 * *"
+//config:      @weekly       Run once a week:  "0 0 * * 0"
+//config:      @daily        Run once a day:   "0 0 * * *"
+//config:      @midnight     Same as @daily:   "0 0 * * *"
+//config:      @hourly       Run once an hour: "0 * * * *"
 //config:
 //config:config FEATURE_CROND_DIR
 //config:      string "crond spool directory"
 //config:      default "/var/spool/cron"
 //config:      depends on CROND || CRONTAB
 //config:      help
-//config:        Location of crond spool.
+//config:      Location of crond spool.
 
 //applet:IF_CROND(APPLET(crond, BB_DIR_USR_SBIN, BB_SUID_DROP))
 
@@ -63,9 +79,9 @@
 #include "common_bufsiz.h"
 #include <syslog.h>
 
-/* glibc frees previous setenv'ed value when we do next setenv()
- * of the same variable. uclibc does not do this! */
-#if (defined(__GLIBC__) && !defined(__UCLIBC__)) /* || OTHER_SAFE_LIBC... */
+#if 0
+/* If libc tracks and reuses setenv()-allocated memory, ok to set this to 0 */
+/* Neither glibc nor uclibc do that! */
 # define SETENV_LEAKS 0
 #else
 # define SETENV_LEAKS 1
@@ -74,6 +90,7 @@
 
 #define CRON_DIR        CONFIG_FEATURE_CROND_DIR
 #define CRONTABS        CONFIG_FEATURE_CROND_DIR "/crontabs"
+#define CRON_REBOOT     CONFIG_PID_FILE_PATH "/crond.reboot"
 #ifndef SENDMAIL
 # define SENDMAIL       "sendmail"
 #endif
@@ -101,6 +118,8 @@ typedef struct CronLine {
        struct CronLine *cl_next;
        char *cl_cmd;                   /* shell command */
        pid_t cl_pid;                   /* >0:running, <0:needs to be started in this minute, 0:dormant */
+#define START_ME_REBOOT -2
+#define START_ME_NORMAL -1
 #if ENABLE_FEATURE_CROND_CALL_SENDMAIL
        int cl_empty_mail_size;         /* size of mail header only, 0 if no mailfile */
        char *cl_mailto;                /* whom to mail results, may be NULL */
@@ -134,6 +153,7 @@ struct globals {
        const char *log_filename;
        const char *crontab_dir_name; /* = CRONTABS; */
        CronFile *cron_files;
+       char *default_shell;
 #if SETENV_LEAKS
        char *env_var_user;
        char *env_var_home;
@@ -465,21 +485,85 @@ static void load_crontab(const char *fileName)
 //line of the crontab's owner. HOME and SHELL may be overridden by settings
 //in the crontab; LOGNAME may not.
 
+#if ENABLE_FEATURE_CROND_SPECIAL_TIMES
+                       if (tokens[0][0] == '@') {
+                               /*
+                                * "@daily /a/script/to/run PARAM1 PARAM2..."
+                                */
+                               typedef struct SpecialEntry {
+                                       const char *name;
+                                       const char tokens[8];
+                               } SpecialEntry;
+                               static const SpecialEntry SpecAry[] = {
+                                       /*              hour  day   month weekday */
+                                       { "yearly",     "0\0" "1\0" "1\0" "*" },
+                                       { "annually",   "0\0" "1\0" "1\0" "*" },
+                                       { "monthly",    "0\0" "1\0" "*\0" "*" },
+                                       { "weekly",     "0\0" "*\0" "*\0" "0" },
+                                       { "daily",      "0\0" "*\0" "*\0" "*" },
+                                       { "midnight",   "0\0" "*\0" "*\0" "*" },
+                                       { "hourly",     "*\0" "*\0" "*\0" "*" },
+                                       { "reboot",     ""                    },
+                               };
+                               const SpecialEntry *e = SpecAry;
+
+                               if (n < 2)
+                                       continue;
+                               for (;;) {
+                                       if (strcmp(e->name, tokens[0] + 1) == 0) {
+                                               /*
+                                                * tokens[1] is only the first word of command,
+                                                * can'r use it.
+                                                * find the entire command in unmodified string:
+                                                */
+                                               tokens[5] = skip_whitespace(
+                                                       skip_non_whitespace(
+                                                       skip_whitespace(parser->data)));
+                                               if (e->tokens[0]) {
+                                                       char *et = (char*)e->tokens;
+                                                       /* minute is "0" for all specials */
+                                                       tokens[0] = (char*)"0";
+                                                       tokens[1] = et;
+                                                       tokens[2] = et + 2;
+                                                       tokens[3] = et + 4;
+                                                       tokens[4] = et + 6;
+                                               }
+                                               goto got_it;
+                                       }
+                                       if (!e->tokens[0])
+                                               break;
+                                       e++;
+                               }
+                               continue; /* bad line (unrecognized '@foo') */
+                       }
+#endif
                        /* check if a minimum of tokens is specified */
                        if (n < 6)
                                continue;
+ IF_FEATURE_CROND_SPECIAL_TIMES(
+  got_it:
+ )
                        *pline = line = xzalloc(sizeof(*line));
-                       /* parse date ranges */
-                       ParseField(file->cf_username, line->cl_Mins, 60, 0, NULL, tokens[0]);
-                       ParseField(file->cf_username, line->cl_Hrs, 24, 0, NULL, tokens[1]);
-                       ParseField(file->cf_username, line->cl_Days, 32, 0, NULL, tokens[2]);
-                       ParseField(file->cf_username, line->cl_Mons, 12, -1, MonAry, tokens[3]);
-                       ParseField(file->cf_username, line->cl_Dow, 7, 0, DowAry, tokens[4]);
-                       /*
-                        * fix days and dow - if one is not "*" and the other
-                        * is "*", the other is set to 0, and vise-versa
-                        */
-                       FixDayDow(line);
+#if ENABLE_FEATURE_CROND_SPECIAL_TIMES
+                       if (tokens[0][0] == '@') { /* "@reboot" line */
+                               file->cf_wants_starting = 1;
+                               line->cl_pid = START_ME_REBOOT; /* wants to start */
+                               /* line->cl_Mins/Hrs/etc stay zero: never match any time */
+                       } else
+#endif
+                       {
+                               /* parse date ranges */
+                               ParseField(file->cf_username, line->cl_Mins, 60, 0, NULL, tokens[0]);
+                               ParseField(file->cf_username, line->cl_Hrs, 24, 0, NULL, tokens[1]);
+                               ParseField(file->cf_username, line->cl_Days, 32, 0, NULL, tokens[2]);
+                               ParseField(file->cf_username, line->cl_Mons, 12, -1, MonAry, tokens[3]);
+                               ParseField(file->cf_username, line->cl_Dow, 7, 0, DowAry, tokens[4]);
+                               /*
+                                * fix days and dow - if one is not "*" and the other
+                                * is "*", the other is set to 0, and vise-versa
+                                */
+                               FixDayDow(line);
+                       }
 #if ENABLE_FEATURE_CROND_CALL_SENDMAIL
                        /* copy mailto (can be NULL) */
                        line->cl_mailto = xstrdup(mailTo);
@@ -617,7 +701,7 @@ fork_job(const char *user, int mailFd, CronLine *line, bool run_sendmail)
                goto err;
        }
 
-       shell = line->cl_shell ? line->cl_shell : DEFAULT_SHELL;
+       shell = line->cl_shell ? line->cl_shell : G.default_shell;
        prog = run_sendmail ? SENDMAIL : shell;
 
        set_env_vars(pas, shell);
@@ -664,7 +748,7 @@ fork_job(const char *user, int mailFd, CronLine *line, bool run_sendmail)
        return pid;
 }
 
-static void start_one_job(const char *user, CronLine *line)
+static pid_t start_one_job(const char *user, CronLine *line)
 {
        char mailFile[128];
        int mailFd = -1;
@@ -698,6 +782,8 @@ static void start_one_job(const char *user, CronLine *line)
                        free(mailFile2);
                }
        }
+
+       return line->cl_pid;
 }
 
 /*
@@ -748,7 +834,7 @@ static void process_finished_job(const char *user, CronLine *line)
 
 #else /* !ENABLE_FEATURE_CROND_CALL_SENDMAIL */
 
-static void start_one_job(const char *user, CronLine *line)
+static pid_t start_one_job(const char *user, CronLine *line)
 {
        const char *shell;
        struct passwd *pas;
@@ -761,7 +847,7 @@ static void start_one_job(const char *user, CronLine *line)
        }
 
        /* Prepare things before vfork */
-       shell = line->cl_shell ? line->cl_shell : DEFAULT_SHELL;
+       shell = line->cl_shell ? line->cl_shell : G.default_shell;
        set_env_vars(pas, shell);
 
        /* Fork as the user in question and run program */
@@ -782,6 +868,7 @@ static void start_one_job(const char *user, CronLine *line)
                pid = 0;
        }
        line->cl_pid = pid;
+       return pid;
 }
 
 #define process_finished_job(user, line)  ((line)->cl_pid = 0)
@@ -825,7 +912,7 @@ static void flag_starting_jobs(time_t t1, time_t t2)
                                                log8("user %s: process already running: %s",
                                                        file->cf_username, line->cl_cmd);
                                        } else if (line->cl_pid == 0) {
-                                               line->cl_pid = -1;
+                                               line->cl_pid = START_ME_NORMAL;
                                                file->cf_wants_starting = 1;
                                        }
                                }
@@ -834,7 +921,20 @@ static void flag_starting_jobs(time_t t1, time_t t2)
        }
 }
 
-static void start_jobs(void)
+#if ENABLE_FEATURE_CROND_SPECIAL_TIMES
+static int touch_reboot_file(void)
+{
+       int fd = open(CRON_REBOOT, O_WRONLY | O_CREAT | O_EXCL | O_TRUNC, 0000);
+       if (fd >= 0) {
+               close(fd);
+               return 1;
+       }
+       /* File (presumably) exists - this is not the first run after reboot */
+       return 0;
+}
+#endif
+
+static void start_jobs(int wants_start)
 {
        CronFile *file;
        CronLine *line;
@@ -846,11 +946,10 @@ static void start_jobs(void)
                file->cf_wants_starting = 0;
                for (line = file->cf_lines; line; line = line->cl_next) {
                        pid_t pid;
-                       if (line->cl_pid >= 0)
+                       if (line->cl_pid != wants_start)
                                continue;
 
-                       start_one_job(file->cf_username, line);
-                       pid = line->cl_pid;
+                       pid = start_one_job(file->cf_username, line);
                        log8("USER %s pid %3d cmd %s",
                                file->cf_username, (int)pid, line->cl_cmd);
                        if (pid < 0) {
@@ -922,13 +1021,17 @@ int crond_main(int argc UNUSED_PARAM, char **argv)
 
        INIT_G();
 
-       /* "-b after -f is ignored", and so on for every pair a-b */
-       opt_complementary = "f-b:b-f:S-L:L-S" IF_FEATURE_CROND_D(":d-l")
+       opts = getopt32(argv, "^"
+                       "l:L:fbSc:" IF_FEATURE_CROND_D("d:")
+                       "\0"
+                       /* "-b after -f is ignored", and so on for every pair a-b */
+                       "f-b:b-f:S-L:L-S" IF_FEATURE_CROND_D(":d-l")
                        /* -l and -d have numeric param */
-                       ":l+" IF_FEATURE_CROND_D(":d+");
-       opts = getopt32(argv, "l:L:fbSc:" IF_FEATURE_CROND_D("d:"),
+                       ":l+" IF_FEATURE_CROND_D(":d+")
+                       ,
                        &G.log_level, &G.log_filename, &G.crontab_dir_name
-                       IF_FEATURE_CROND_D(,&G.log_level));
+                       IF_FEATURE_CROND_D(,&G.log_level)
+       );
        /* both -d N and -l N set the same variable: G.log_level */
 
        if (!(opts & OPT_f)) {
@@ -947,9 +1050,17 @@ int crond_main(int argc UNUSED_PARAM, char **argv)
 
        reopen_logfile_to_stderr();
        xchdir(G.crontab_dir_name);
+       /* $SHELL, or current UID's shell, or DEFAULT_SHELL */
+       /* Useful on Android where DEFAULT_SHELL /bin/sh may not exist */
+       G.default_shell = xstrdup(get_shell_name());
+
        log8("crond (busybox "BB_VER") started, log level %d", G.log_level);
        rescan_crontab_dir();
        write_pidfile(CONFIG_PID_FILE_PATH "/crond.pid");
+#if ENABLE_FEATURE_CROND_SPECIAL_TIMES
+       if (touch_reboot_file())
+               start_jobs(START_ME_REBOOT); /* start @reboot entries, if any */
+#endif
 
        /* Main loop */
        t2 = time(NULL);
@@ -1002,7 +1113,7 @@ int crond_main(int argc UNUSED_PARAM, char **argv)
                } else if (dt > 0) {
                        /* Usual case: time advances forward, as expected */
                        flag_starting_jobs(t1, t2);
-                       start_jobs();
+                       start_jobs(START_ME_NORMAL);
                        sleep_time = 60;
                        if (check_completions() > 0) {
                                /* some jobs are still running */