>From 4d139e0cce2ea5133423c1d9844c355ec593d17a Mon Sep 17 00:00:00 2001
From: Tim Landscheidt <tim@tim-landscheidt.de>
Date: Wed, 2 May 2012 21:02:20 +0000
Subject: [PATCH] Add persistent database of last runs.

* man/crontab.5: Remove caveat about restarts.
* src/Makefile.am: Add last_run_db.c and -ldb.
* src/cron.c: Add database setup and close and note times of last runs.
* src/entry.c: Rename IS_* to RUN_*.  Look up time of last run.
* src/funcs.h: Add prototypes.
* src/job.c: Move last run management to cron.c.
* src/last_run_db.c: New.
* src/pathnames.h: Add _PATH_CRON_DB.
---
 man/crontab.5     |    6 --
 src/Makefile.am   |    8 ++--
 src/cron.c        |   30 ++++++++++-
 src/entry.c       |   13 +++--
 src/funcs.h       |   12 +++++
 src/job.c         |    8 ---
 src/last_run_db.c |  143 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/pathnames.h   |    4 ++
 8 files changed, 200 insertions(+), 24 deletions(-)
 create mode 100644 src/last_run_db.c

diff --git a/man/crontab.5 b/man/crontab.5
index 44c73ac..75b17fb 100644
--- a/man/crontab.5
+++ b/man/crontab.5
@@ -308,12 +308,6 @@ Run once per day (0:00 to 23:59).
 .TP
 .BI "%weekly" " minute hour"
 Run once per week (Monday to Sunday).
-.PP
-All time periods are reset at cronie start.  That means, if
-you (re-)start cronie every day, a
-.I
-%weekly
-job will be run seven times per week.
 .SH CAVEATS
 .BR crontab
 files have to be regular files or symlinks to regular files, they must not be executable
diff --git a/src/Makefile.am b/src/Makefile.am
index a3f57c8..92956e1 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -7,16 +7,16 @@ crond_SOURCES = \
 	cron.c database.c user.c job.c do_command.c popen.c \
 	$(common_src)
 crontab_SOURCES = crontab.c $(common_src)
-common_src = entry.c env.c misc.c pw_dup.c security.c cron.h \
-	externs.h funcs.h globals.h macros.h pathnames.h structs.h \
-	bitstring.h
+common_src = entry.c env.c last_run_db.c misc.c pw_dup.c	\
+	security.c cron.h externs.h funcs.h globals.h		\
+	macros.h pathnames.h structs.h bitstring.h
 common_nodist = cron-paths.h
 nodist_crond_SOURCES = $(common_nodist)
 nodist_crontab_SOURCES = $(common_nodist)
 BUILT_SOURCES = $(common_nodist)
 
 
-LDADD = $(LIBSELINUX) $(LIBPAM) $(LIBAUDIT)
+LDADD = $(LIBSELINUX) $(LIBPAM) $(LIBAUDIT) -ldb
 
 ## if DEBUG
 ## noinst_PROGRAMS = debug
diff --git a/src/cron.c b/src/cron.c
index bd78aba..ce7c314 100644
--- a/src/cron.c
+++ b/src/cron.c
@@ -28,6 +28,8 @@
 
 #include <cron.h>
 
+#include <stdlib.h>
+
 #if defined WITH_INOTIFY
 int inotify_enabled;
 #else
@@ -166,6 +168,7 @@ int main(int argc, char *argv[]) {
 	char *cs;
 	pid_t pid = getpid();
 	long oldGMToff;
+	int exitcode = EXIT_SUCCESS;
 
 	if ((ProgramName=strrchr(argv[0], '/')) == NULL) {
 		ProgramName = argv[0];
@@ -263,6 +266,11 @@ int main(int argc, char *argv[]) {
 	database.tail = NULL;
 	database.mtime = (time_t) 0;
 
+	if (init_last_run_db ()) {
+		exitcode = EXIT_FAILURE;
+		goto mainexit;
+	}
+
 	load_database(&database);
 
 	fd = -1;
@@ -433,9 +441,12 @@ int main(int argc, char *argv[]) {
 		log_it("CRON", pid, "INFO", "Inotify close failed", errno);
 #endif
 
+	mainexit:
+	close_last_run_db ();
+
 	(void) unlink(_PATH_CRON_PID);
 
-	return 0;
+	return exitcode;
 }
 
 static void run_reboot_jobs(cron_db * db) {
@@ -588,8 +599,23 @@ static void find_jobs(int vtime, cron_db * db, int doWild, int doNonWild, long v
 
 					if ((doNonWild &&
 							!(e->flags & (MIN_STAR | HR_STAR))) ||
-						(doWild && (e->flags & (MIN_STAR | HR_STAR))))
+						(doWild && (e->flags & (MIN_STAR | HR_STAR)))) {
 						job_add(e, u);	/*will add job, if it isn't in queue already for NOW. */
+
+						/* update last_run (even though we haven't actually run the job yet) */
+						if (e->period) {
+							time_t t;
+
+							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);
+
+							/* update last_run_db */
+							note_last_run (uname, e->cmd, &e->last_run, &e->last_run_monday);
+						}
+					}
 				}
 			}
 		}
diff --git a/src/entry.c b/src/entry.c
index f728c28..3af64ed 100644
--- a/src/entry.c
+++ b/src/entry.c
@@ -188,11 +188,11 @@ entry *load_entry(FILE * file, void (*error_func) (), struct passwd *pw,
 		}
 	}
 	else if (ch == '%') {
-    int skiphours = 0;
+		int skiphours = 0;
 
 		ch = get_string(cmd, MAX_COMMAND, file, " \t\n");
 		if (!strcmp("weekly", cmd)) {
-			e->period = IS_WEEKLY;
+			e->period = RUN_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);
@@ -200,7 +200,7 @@ entry *load_entry(FILE * file, void (*error_func) (), struct passwd *pw,
 			skiphours = 0;
 		}
 		else if (!strcmp("daily", cmd)) {
-			e->period = IS_DAILY;
+			e->period = RUN_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);
@@ -208,7 +208,7 @@ entry *load_entry(FILE * file, void (*error_func) (), struct passwd *pw,
 			skiphours = 0;
 		}
 		else if (!strcmp("hourly", cmd)) {
-			e->period = IS_HOURLY;
+			e->period = RUN_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);
@@ -439,6 +439,11 @@ entry *load_entry(FILE * file, void (*error_func) (), struct passwd *pw,
 		goto eof;
 	}
 
+	/* For periodic jobs, look up the time of last run
+	 * and the Monday before. */
+	if (e->period)
+		look_up_last_run (e->pwd->pw_name, e->cmd, &e->last_run, &e->last_run_monday);
+
 	Debug(DPARS, ("load_entry()...returning successfully\n"))
 
 		/* success, fini, return pointer to the entry we just created...
diff --git a/src/funcs.h b/src/funcs.h
index aee4a99..9998aec 100644
--- a/src/funcs.h
+++ b/src/funcs.h
@@ -111,3 +111,15 @@ int crontab_security_access(void);
 /* PAM */
 int cron_start_pam(struct passwd *pw);
 void cron_close_pam(void);
+
+/* Database of last runs */
+int init_last_run_db (void);
+void note_last_run (const char *username,
+		    const char *command,
+		    struct tm *last_run,
+		    struct tm *last_run_monday);
+int look_up_last_run (const char *username,
+                      const char *command,
+                      struct tm *last_run,
+                      struct tm *last_run_monday);
+int close_last_run_db (void);
diff --git a/src/job.c b/src/job.c
index 57160a9..5ea7d98 100644
--- a/src/job.c
+++ b/src/job.c
@@ -31,7 +31,6 @@ 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)
@@ -45,13 +44,6 @@ 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/last_run_db.c b/src/last_run_db.c
new file mode 100644
index 0000000..c992904
--- /dev/null
+++ b/src/last_run_db.c
@@ -0,0 +1,143 @@
+/* last_run_db.c - On-disk database of jobs' last runs. */
+
+#include "cron.h"
+
+#include <db.h>
+#include <stdlib.h>
+#include <string.h>
+
+typedef struct
+{
+  struct tm last_run, last_run_monday;
+} last_run_data;
+
+static DB *last_run_db;
+
+/* The key of an entry consists of the username, a NUL byte, the
+ * command, and another NUL byte. */
+static DBT *
+make_last_run_key (DBT *key, const char *username, const char *command)
+{
+  memset (key, 0, sizeof (*key));
+  key->data = malloc (key->size = strlen (username) + 1 +
+                                  strlen (command) + 1);
+  if (!key->data)
+    {
+      log_it ("CRON", getpid (), "INFO", "Couldn't malloc in make_last_run_key", errno);
+
+      return NULL;
+    }
+  strcpy (key->data,                         username);
+  strcpy (key->data + strlen (username) + 1, command);
+
+  return key;
+}
+
+/* The data of an entry consists of a struct tm of the last run and
+ * another struct tm of the Monday preceding that. */
+static DBT *
+make_last_run_data (DBT *data, const struct tm *last_run,
+                    const struct tm *last_run_monday)
+{
+  memset (data, 0, sizeof (*data));
+  last_run_data *l = malloc (data->size = sizeof (*l));
+  if (!l)
+    {
+      log_it ("CRON", getpid (), "INFO", "Couldn't malloc in make_last_run_data", errno);
+
+      return NULL;
+    }
+  l->last_run        = *last_run;
+  l->last_run_monday = *last_run_monday;
+  data->data         = l;
+
+  return data;
+}
+
+int
+init_last_run_db (void)
+{
+  int ret = db_create (&last_run_db, NULL, 0);
+  if (ret != 0)
+    {
+      log_it ("CRON", getpid (), "DEATH", db_strerror (ret), ret);
+
+      return 1;
+    }
+
+  /* Open the database. */
+  ret = last_run_db->open (last_run_db, NULL, _PATH_CRON_DB, NULL,
+                           DB_BTREE, DB_CREATE, 0);
+  if (ret != 0)
+    {
+      log_it ("CRON", getpid (), "DEATH", db_strerror (ret), ret);
+
+      return 1;
+    }
+
+  /* TODO: Purge entries older than one week, the maximum time period
+   * supported.  Do this now and not on exit as time might be short
+   * between SIGTERM and SIGKILL. */
+
+  return 0;
+}
+
+void
+note_last_run (const char *username, const char *command,
+               struct tm *last_run, struct tm *last_run_monday)
+{
+  DBT key, data;
+  int ret;
+
+  if (!make_last_run_key (&key, username, command)           ||
+      !make_last_run_data (&data, last_run, last_run_monday))
+    return;
+  if ((ret = last_run_db->put (last_run_db, NULL, &key, &data, 0)))
+    log_it ("CRON", getpid (), "INFO", db_strerror (ret), ret);
+  free (key.data);
+  free (data.data);
+}
+
+int
+look_up_last_run (const char *username, const char *command,
+                  struct tm *last_run, struct tm *last_run_monday)
+{
+  DBT key, data;
+  last_run_data lrd;
+  int ret;
+
+  /* If invoked as crontab, last_run_db won't be set. */
+  if (!last_run_db)
+    return 1;
+
+  if (!make_last_run_key (&key, username, command))
+    return 2;
+  memset (&data, 0, sizeof (data));
+  data.flags = DB_DBT_USERMEM;
+  data.ulen  = sizeof (lrd);
+  data.data  = &lrd;
+  if ((ret = last_run_db->get (last_run_db, NULL, &key, &data, 0)))
+    {
+      if (ret != DB_NOTFOUND)
+        log_it ("CRON", getpid (), "INFO", db_strerror (ret), ret);
+
+      return 1;
+    }
+  *last_run        = lrd.last_run;
+  *last_run_monday = lrd.last_run_monday;
+  free (key.data);
+
+  return 0;
+}
+
+int
+close_last_run_db (void)
+{
+  if (last_run_db)
+    {
+      last_run_db->close (last_run_db, 0);
+      last_run_db = NULL;
+    }
+
+  return 0;
+}
diff --git a/src/pathnames.h b/src/pathnames.h
index b5d5c70..a049e9a 100644
--- a/src/pathnames.h
+++ b/src/pathnames.h
@@ -70,4 +70,8 @@
 # define _PATH_DEVNULL "/dev/null"
 #endif
 
+#ifndef _PATH_CRON_DB
+# define _PATH_CRON_DB "/var/lib/misc/cronie.db"
+#endif
+
 #endif /* _PATHNAMES_H_ */
-- 
1.7.7.6

