>From 6f941c541501e140d3d53a8f36d45346630adc6f Mon Sep 17 00:00:00 2001
From: Tim Landscheidt <tim@tim-landscheidt.de>
Date: Thu, 26 Apr 2012 11:55:39 +0000
Subject: [PATCH] Add basic fcron-like functionality.

* src/cron.c: Handle periodic jobs.
* src/entry.c: Parse "%hourly", "%daily" and "%weekly" jobs.
* src/job.c: Save time of last run.
* src/structs.h: Add field for last run.
---
 src/cron.c    |   48 +++++++++++++++++++++++++++++++++++++++
 src/entry.c   |   70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/job.c     |    8 ++++++
 src/structs.h |    7 +++++
 4 files changed, 133 insertions(+), 0 deletions(-)

diff --git a/src/cron.c b/src/cron.c
index d1c9d80..9089477 100644
--- a/src/cron.c
+++ b/src/cron.c
@@ -515,8 +515,56 @@ static void find_jobs(int vtime, cron_db * db, int doWild, int doNonWild, long v
 				uname = e->pwd->pw_name;
 			/* check if user exists in time of job is being run f.e. ldap */
 			if (getpwnam(uname) != NULL) {
+        struct tm this_weeks_monday;
+
 				job_tz = env_get("CRON_TZ", e->envp);
 				maketime(job_tz, orig_tz);
+
+				/* Check if a periodically run job hasn't been run
+         * for its period.  We're using the tm variable
+         * instead of hour/minute/etc. as last_run is also a
+         * struct tm. */
+
+				/* There's certainly the possibility to calculate
+				 * the time of next execution and then use simple
+				 * epoch arithmetics, but it's a) very complicated
+				 * to cover all corner cases and b) for most systems
+				 * not worth the micro-optimization. */
+				switch (e->period) {
+					case RUN_HOURLY:
+						/* Don't compare just the hours because in the
+						 * future we might want to save the time of the
+						 * last run so that it survives crond restarts.
+						 * If we only compared hours, we would miss if
+						 * crond was shutdown at 1:20 on Monday and
+						 * restarted 1:25 on Tuesday. */
+						if (tm->tm_hour == e->last_run.tm_hour &&
+								tm->tm_yday == e->last_run.tm_yday &&
+								tm->tm_year == e->last_run.tm_year)
+							continue;
+						break;
+
+					case RUN_DAILY:
+						/* Dito for days. */
+						if (tm->tm_yday == e->last_run.tm_yday &&
+								tm->tm_year == e->last_run.tm_year)
+							continue;
+						break;
+
+					case RUN_WEEKLY:
+						/* We've saved the Monday of the week the last
+             * time a job was run in last_time_monday.  That
+             * week hasn't ended if this week's Monday is
+             * identical. */
+						this_weeks_monday = *tm;
+						this_weeks_monday.tm_mday -= this_weeks_monday.tm_wday ? this_weeks_monday.tm_wday - 1 : 6;
+						mktime (&this_weeks_monday);
+						if (this_weeks_monday.tm_yday == e->last_run_monday.tm_yday &&
+								this_weeks_monday.tm_year == e->last_run_monday.tm_year)
+							continue;
+						break;
+				}
+
 				/* here we test whether time is NOW */
 				if (bit_test(e->minute, minute) &&
 					bit_test(e->hour, hour) &&
diff --git a/src/entry.c b/src/entry.c
index 1dc9b9d..f728c28 100644
--- a/src/entry.c
+++ b/src/entry.c
@@ -187,6 +187,76 @@ entry *load_entry(FILE * file, void (*error_func) (), struct passwd *pw,
 			goto eof;
 		}
 	}
+	else if (ch == '%') {
+    int skiphours = 0;
+
+		ch = get_string(cmd, MAX_COMMAND, file, " \t\n");
+		if (!strcmp("weekly", cmd)) {
+			e->period = IS_WEEKLY;
+			bit_nset(e->dom, 0, LAST_DOM - FIRST_DOM);
+			bit_nset(e->month, 0, LAST_MONTH - FIRST_MONTH);
+			bit_nset(e->dow, 0, LAST_DOW - FIRST_DOW);
+			e->flags |= DOM_STAR | DOW_STAR;
+			skiphours = 0;
+		}
+		else if (!strcmp("daily", cmd)) {
+			e->period = IS_DAILY;
+			bit_nset(e->dom, 0, LAST_DOM - FIRST_DOM);
+			bit_nset(e->month, 0, LAST_MONTH - FIRST_MONTH);
+			bit_nset(e->dow, 0, LAST_DOW - FIRST_DOW);
+			e->flags |= DOM_STAR | DOW_STAR;
+			skiphours = 0;
+		}
+		else if (!strcmp("hourly", cmd)) {
+			e->period = IS_HOURLY;
+			bit_nset(e->hour, 0, LAST_HOUR - FIRST_HOUR);
+			bit_nset(e->dom, 0, LAST_DOM - FIRST_DOM);
+			bit_nset(e->month, 0, LAST_MONTH - FIRST_MONTH);
+			bit_nset(e->dow, 0, LAST_DOW - FIRST_DOW);
+			e->flags |= HR_STAR | DOM_STAR | DOW_STAR;
+			skiphours = 1;
+		}
+		else {
+			ecode = e_timespec;
+			goto eof;
+		}
+
+		/* minutes
+		 */
+		Skip_Blanks(ch, file);
+		if (ch == EOF || ch == '\n') {
+			ecode = e_cmd;
+			goto eof;
+		}
+		if (ch == '*')
+			e->flags |= MIN_STAR;
+		ch = get_list(e->minute, FIRST_MINUTE, LAST_MINUTE, PPC_NULL, ch, file);
+		if (ch == EOF) {
+			ecode = e_minute;
+			goto eof;
+		}
+
+		/* hours
+		 */
+    if (skiphours)
+      goto usercmd;
+		if (ch == '*')
+			e->flags |= HR_STAR;
+		ch = get_list(e->hour, FIRST_HOUR, LAST_HOUR, PPC_NULL, ch, file);
+		if (ch == EOF) {
+			ecode = e_hour;
+			goto eof;
+		}
+		/* Advance past whitespace between shortcut and
+		 * username/command.
+		 */
+    usercmd:
+		Skip_Blanks(ch, file);
+		if (ch == EOF || ch == '\n') {
+			ecode = e_cmd;
+			goto eof;
+		}
+	}
 	else {
 		Debug(DPARS, ("load_entry()...about to parse numerics\n"))
 
diff --git a/src/job.c b/src/job.c
index 5ea7d98..57160a9 100644
--- a/src/job.c
+++ b/src/job.c
@@ -31,6 +31,7 @@ static job *jhead = NULL, *jtail = NULL;
 
 void job_add(entry * e, user * u) {
 	job *j;
+	time_t t;
 
 	/* if already on queue, keep going */
 	for (j = jhead; j != NULL; j = j->next)
@@ -44,6 +45,13 @@ void job_add(entry * e, user * u) {
 	j->e = e;
 	j->u = u;
 
+	/* update last_run (even though we haven't run the job yet) */
+        time (&t);
+        localtime_r (&t, &e->last_run);
+        localtime_r (&t, &e->last_run_monday);
+        e->last_run_monday.tm_mday -= e->last_run_monday.tm_wday ? e->last_run_monday.tm_wday - 1 : 6;
+        mktime (&e->last_run_monday);
+
 	/* add it to the tail */
 	if (jhead == NULL)
 		jhead = j;
diff --git a/src/structs.h b/src/structs.h
index 16cd180..9e95ecd 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -19,6 +19,8 @@
  * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <time.h>
+
 typedef	struct _entry {
 	struct _entry	*next;
 	struct passwd	*pwd;
@@ -36,6 +38,11 @@ typedef	struct _entry {
 #define	DOW_STAR	0x08
 #define	WHEN_REBOOT	0x10
 #define	DONT_LOG	0x20
+	int period;
+#define RUN_HOURLY	0x01
+#define RUN_DAILY	0x02
+#define RUN_WEEKLY	0x03
+	struct tm last_run, last_run_monday;
 } entry;
 
 			/* the crontab database will be a list of the
-- 
1.7.7.6

