crond: support @daily etc

function                                             old     new   delta
start_jobs                                             -     348    +348
load_crontab                                         766     936    +170
static.SpecAry                                         -      96     +96
crond_main                                          1424    1134    -290
------------------------------------------------------------------------------
(add/remove: 2/0 grow/shrink: 1/1 up/down: 614/-290)          Total: 324 bytes

Based on patch by Jonathan Kolb <kolbyjack@gmail.com>

Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
diff --git a/miscutils/crond.c b/miscutils/crond.c
index 88e7b47..8a39944 100644
--- a/miscutils/crond.c
+++ b/miscutils/crond.c
@@ -35,6 +35,22 @@
 //config:	help
 //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"
@@ -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 @@
 	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 */
@@ -452,6 +471,59 @@
 				shell = xstrdup(&tokens[0][6]);
 				continue;
 			}
+#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,
+						 * find the entire command in unmodified string:
+						 */
+						tokens[5] = strstr(
+							skip_non_whitespace(skip_whitespace(parser->data)),
+							/* ^^^^ avoids mishandling e.g. "@daily aily PARAM" */
+							tokens[1]
+						);
+						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
 //TODO: handle HOME= too? "man crontab" says:
 //name = value
 //
@@ -468,18 +540,30 @@
 			/* 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);
@@ -664,7 +748,7 @@
 	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 @@
 			free(mailFile2);
 		}
 	}
+
+	return line->cl_pid;
 }
 
 /*
@@ -748,7 +834,7 @@
 
 #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;
@@ -782,6 +868,7 @@
 		pid = 0;
 	}
 	line->cl_pid = pid;
+	return pid;
 }
 
 #define process_finished_job(user, line)  ((line)->cl_pid = 0)
@@ -825,7 +912,7 @@
 						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 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 @@
 		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) {
@@ -950,6 +1049,10 @@
 	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 +1105,7 @@
 		} 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 */