* 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))
//usage: " -f Foreground"
//usage: "\n -b Background (default)"
//usage: "\n -S Log to syslog (default)"
-//usage: "\n -l N Set log level. Most verbose:0, default:8"
+//usage: "\n -l N Set log level. Most verbose 0, default 8"
//usage: IF_FEATURE_CROND_D(
//usage: "\n -d N Set log level, log to stderr"
//usage: )
#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
#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
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 */
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;
//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);
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);
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;
free(mailFile2);
}
}
+
+ return line->cl_pid;
}
/*
#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;
}
/* 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 */
pid = 0;
}
line->cl_pid = pid;
+ return pid;
}
#define process_finished_job(user, line) ((line)->cl_pid = 0)
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;
}
}
}
}
-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;
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) {
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)) {
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);
} 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 */