Radix cross Linux

The main Radix cross Linux repository contains the build scripts of packages, which have the most complete and common functionality for desktop machines

452 Commits   2 Branches   1 Tag
Index: database.c
===================================================================
--- database.c	(nonexistent)
+++ database.c	(revision 5)
@@ -0,0 +1,1250 @@
+
+/*
+ * DATABASE.C
+ *
+ * Copyright 1994 Matthew Dillon (dillon@apollo.backplane.com)
+ * Copyright 2009-2011 James Pryor <profjim@jimpryor.net>
+ * May be distributed under the GNU General Public License
+ */
+
+#include "defs.h"
+
+Prototype void CheckUpdates(const char *dpath, const char *user_override, time_t t1, time_t t2);
+Prototype void SynchronizeDir(const char *dpath, const char *user_override, int initial_scan);
+Prototype void ReadTimestamps(const char *user);
+Prototype int TestJobs(time_t t1, time_t t2);
+Prototype int TestStartupJobs(void);
+Prototype int ArmJob(CronFile *file, CronLine *line, time_t t1, time_t t2);
+Prototype void RunJobs(void);
+Prototype int CheckJobs(void);
+
+void SynchronizeFile(const char *dpath, const char *fname, const char *uname);
+void DeleteFile(CronFile **pfile);
+char *ParseInterval(int *interval, char *ptr);
+char *ParseField(char *userName, char *ary, int modvalue, int off, int onvalue, const char **names, char *ptr);
+void FixDayDow(CronLine *line);
+
+CronFile *FileBase = NULL;
+
+const char *DowAry[] = {
+	"sun",
+	"mon",
+	"tue",
+	"wed",
+	"thu",
+	"fri",
+	"sat",
+
+	"Sun",
+	"Mon",
+	"Tue",
+	"Wed",
+	"Thu",
+	"Fri",
+	"Sat",
+	NULL
+};
+
+const char *MonAry[] = {
+	"jan",
+	"feb",
+	"mar",
+	"apr",
+	"may",
+	"jun",
+	"jul",
+	"aug",
+	"sep",
+	"oct",
+	"nov",
+	"dec",
+
+	"Jan",
+	"Feb",
+	"Mar",
+	"Apr",
+	"May",
+	"Jun",
+	"Jul",
+	"Aug",
+	"Sep",
+	"Oct",
+	"Nov",
+	"Dec",
+	NULL
+};
+
+const char *FreqAry[] = {
+	"noauto",
+	"reboot",
+	"hourly",
+	"daily",
+	"weekly",
+	"monthly",
+	"yearly",
+	NULL
+};
+
+/*
+ * Check the cron.update file in the specified directory.  If user_override
+ * is NULL then the files in the directory belong to the user whose name is
+ * the file, otherwise they belong to the user_override user.
+ */
+void
+CheckUpdates(const char *dpath, const char *user_override, time_t t1, time_t t2)
+{
+	FILE *fi;
+	char buf[SMALL_BUFFER];
+	char *fname, *ptok, *job;
+	char *path;
+
+	if (!(path = concat(dpath, "/", CRONUPDATE, NULL))) {
+		errno = ENOMEM;
+		perror("CheckUpdates");
+		exit(1);
+	}
+	if ((fi = fopen(path, "r")) != NULL) {
+		remove(path);
+		printlogf(LOG_INFO, "reading %s/%s\n", dpath, CRONUPDATE);
+		while (fgets(buf, sizeof(buf), fi) != NULL) {
+			/*
+			 * if buf has only sep chars, return NULL and point ptok at buf's terminating 0
+			 * else return pointer to first non-sep of buf and
+			 * 		if there's a following sep, overwrite it to 0 and point ptok to next char
+			 * 		else point ptok at buf's terminating 0
+			 */
+			fname = strtok_r(buf, " \t\n", &ptok);
+
+			if (user_override)
+				SynchronizeFile(dpath, fname, user_override);
+			else if (!getpwnam(fname))
+				printlogf(LOG_WARNING, "ignoring %s/%s (non-existent user)\n", dpath, fname);
+			else if (*ptok == 0 || *ptok == '\n') {
+				SynchronizeFile(dpath, fname, fname);
+				ReadTimestamps(fname);
+			} else {
+				/* if fname is followed by whitespace, we prod any following jobs */
+				CronFile *file = FileBase;
+				while (file) {
+					if (strcmp(file->cf_UserName, fname) == 0)
+						break;
+					file = file->cf_Next;
+				}
+				if (!file)
+					printlogf(LOG_WARNING, "unable to prod for user %s: no crontab\n", fname);
+				else {
+					CronLine *line;
+					/* calling strtok(ptok...) then strtok(NULL) is equiv to calling strtok_r(NULL,..&ptok) */
+					while ((job = strtok(ptok, " \t\n")) != NULL) {
+						time_t force = t2;
+						ptok = NULL;
+						if (*job == '!') {
+							force = (time_t)-1;
+							++job;
+						}
+						line = file->cf_LineBase;
+						while (line) {
+							if (line->cl_JobName && strcmp(line->cl_JobName, job) == 0)
+								break;
+							line = line->cl_Next;
+						}
+						if (line)
+							ArmJob(file, line, t1, force);
+						else {
+							printlogf(LOG_WARNING, "unable to prod for user %s: unknown job %s\n", fname, job);
+							/* we can continue parsing this line, we just don't install any CronWaiter for the requested job */
+						}
+					}
+				}
+			}
+		}
+		fclose(fi);
+	}
+	free(path);
+}
+
+void
+SynchronizeDir(const char *dpath, const char *user_override, int initial_scan)
+{
+	CronFile **pfile;
+	CronFile *file;
+	struct dirent *den;
+	DIR *dir;
+	char *path;
+
+	if (DebugOpt)
+		printlogf(LOG_DEBUG, "Synchronizing %s\n", dpath);
+
+	/*
+	 * Delete all database CronFiles for this directory.  DeleteFile() will
+	 * free *pfile and relink the *pfile pointer, or in the alternative will
+	 * mark it as deleted.
+	 */
+	pfile = &FileBase;
+	while ((file = *pfile) != NULL) {
+		if (file->cf_Deleted == 0 && strcmp(file->cf_DPath, dpath) == 0) {
+			DeleteFile(pfile);
+		} else {
+			pfile = &file->cf_Next;
+		}
+	}
+
+	/*
+	 * Since we are resynchronizing the entire directory, remove the
+	 * the CRONUPDATE file.
+	 */
+	if (!(path = concat(dpath, "/", CRONUPDATE, NULL))) {
+		errno = ENOMEM;
+		perror("SynchronizeDir");
+		exit(1);
+	}
+	remove(path);
+	free(path);
+
+	/*
+	 * Scan the specified directory
+	 */
+	if ((dir = opendir(dpath)) != NULL) {
+		while ((den = readdir(dir)) != NULL) {
+			if (strchr(den->d_name, '.') != NULL)
+				continue;
+			if (strcmp(den->d_name, CRONUPDATE) == 0)
+				continue;
+			if (user_override) {
+				SynchronizeFile(dpath, den->d_name, user_override);
+			} else if (getpwnam(den->d_name)) {
+				SynchronizeFile(dpath, den->d_name, den->d_name);
+			} else {
+				printlogf(LOG_WARNING, "ignoring %s/%s (non-existent user)\n",
+						dpath, den->d_name);
+			}
+		}
+		closedir(dir);
+	} else {
+		if (initial_scan)
+			printlogf(LOG_ERR, "unable to scan directory %s\n", dpath);
+			/* softerror, do not exit the program */
+	}
+}
+
+
+void
+ReadTimestamps(const char *user)
+{
+	CronFile *file;
+	CronLine *line;
+	FILE *fi;
+	char buf[SMALL_BUFFER];
+	char *ptr;
+	struct tm tm = {0};
+	time_t sec, freq;
+
+	file = FileBase;
+	while (file != NULL) {
+		if (file->cf_Deleted == 0 && (!user || strcmp(user, file->cf_UserName) == 0)) {
+			line = file->cf_LineBase;
+			while (line != NULL) {
+				if (line->cl_Timestamp) {
+					if ((fi = fopen(line->cl_Timestamp, "r")) != NULL) {
+						if (fgets(buf, sizeof(buf), fi) != NULL) {
+							int fake = 0;
+							ptr = buf;
+							if (strncmp(buf, "after ", 6) == 0) {
+								fake = 1;
+								ptr += 6;
+							}
+							sec = (time_t)-1;
+							ptr = strptime(ptr, CRONSTAMP_FMT, &tm);
+							if (ptr && (*ptr == 0 || *ptr == '\n')) {
+								/* strptime uses current seconds when seconds not specified? anyway, we don't get round minutes */
+								tm.tm_sec = 0;
+								tm.tm_isdst = -1;
+								sec = mktime(&tm);
+							}
+							if (sec == (time_t)-1) {
+								printlogf(LOG_ERR, "unable to parse timestamp (user %s job %s)\n", file->cf_UserName, line->cl_JobName);
+								/* we continue checking other timestamps in this CronFile */
+							} else {
+								/* sec -= sec % 60; */
+								if (fake) {
+									line->cl_NotUntil = sec;
+								} else {
+									line->cl_LastRan = sec;
+									freq = (line->cl_Freq > 0) ? line->cl_Freq : line->cl_Delay;
+									/* if (line->cl_NotUntil < line->cl_LastRan + freq) */
+									line->cl_NotUntil = line->cl_LastRan + freq;
+								}
+							}
+						}
+						fclose(fi);
+					} else {
+						int succeeded = 0;
+						printlogf(LOG_NOTICE, "no timestamp found (user %s job %s)\n", file->cf_UserName, line->cl_JobName);
+						/* write a fake timestamp file so our initial NotUntil doesn't keep being reset every hour when crond does a SynchronizeDir */
+						if ((fi = fopen(line->cl_Timestamp, "w")) != NULL) {
+							if (strftime(buf, sizeof(buf), CRONSTAMP_FMT, localtime(&line->cl_NotUntil)))
+								if (fputs("after ", fi) >= 0)
+									if (fputs(buf,fi) >= 0)
+										succeeded = 1;
+							fclose(fi);
+						}
+						if (!succeeded)
+							printlogf(LOG_WARNING, "unable to write timestamp to %s (user %s %s)\n", line->cl_Timestamp, file->cf_UserName, line->cl_Description);
+					}
+				}
+				line = line->cl_Next;
+			}
+		}
+		file = file->cf_Next;
+	}
+}
+
+void
+SynchronizeFile(const char *dpath, const char *fileName, const char *userName)
+{
+	CronFile **pfile;
+	CronFile *file;
+	int maxEntries;
+	int maxLines;
+	char buf[RW_BUFFER]; /* max length for crontab lines */
+	char *path;
+	FILE *fi;
+
+	/*
+	 * Limit entries
+	 */
+	if (strcmp(userName, "root") == 0)
+		maxEntries = 65535;
+	else
+		maxEntries = MAXLINES;
+	maxLines = maxEntries * 10;
+
+	/*
+	 * Delete any existing copy of this CronFile
+	 */
+	pfile = &FileBase;
+	while ((file = *pfile) != NULL) {
+		if (file->cf_Deleted == 0 && strcmp(file->cf_DPath, dpath) == 0 &&
+				strcmp(file->cf_FileName, fileName) == 0
+		   ) {
+			DeleteFile(pfile);
+		} else {
+			pfile = &file->cf_Next;
+		}
+	}
+
+	if (!(path = concat(dpath, "/", fileName, NULL))) {
+		errno = ENOMEM;
+		perror("SynchronizeFile");
+		exit(1);
+	}
+	if ((fi = fopen(path, "r")) != NULL) {
+		struct stat sbuf;
+
+		if (fstat(fileno(fi), &sbuf) == 0 && sbuf.st_uid == DaemonUid) {
+			CronFile *file = calloc(1, sizeof(CronFile));
+			CronLine **pline;
+			time_t tnow = time(NULL);
+			tnow -= tnow % 60;
+
+			file->cf_UserName = strdup(userName);
+			file->cf_FileName = strdup(fileName);
+			file->cf_DPath = strdup(dpath);
+			pline = &file->cf_LineBase;
+
+			/* fgets reads at most size-1 chars until \n or EOF, then adds a\0; \n if present is stored in buf */
+			while (fgets(buf, sizeof(buf), fi) != NULL && --maxLines) {
+				CronLine line;
+				char *ptr = buf;
+				int len;
+
+				while (*ptr == ' ' || *ptr == '\t' || *ptr == '\n')
+					++ptr;
+
+				len = strlen(ptr);
+				if (len && ptr[len-1] == '\n')
+					ptr[--len] = 0;
+
+				if (*ptr == 0 || *ptr == '#')
+					continue;
+
+				if (--maxEntries == 0)
+					break;
+
+				memset(&line, 0, sizeof(line));
+
+				if (DebugOpt)
+					printlogf(LOG_DEBUG, "User %s Entry %s\n", userName, buf);
+
+				if (*ptr == '@') {
+					/*
+					 * parse @hourly, etc
+					 */
+					int	j;
+					line.cl_Delay = -1;
+					ptr += 1;
+					for (j = 0; FreqAry[j]; ++j) {
+						if (strncmp(ptr, FreqAry[j], strlen(FreqAry[j])) == 0) {
+							break;
+						}
+					}
+					if (FreqAry[j]) {
+						ptr += strlen(FreqAry[j]);
+						switch(j) {
+							case 0:
+								/* noauto */
+								line.cl_Freq = -2;
+								line.cl_Delay = 0;
+								break;
+							case 1:
+								/* reboot */
+								line.cl_Freq = -1;
+								line.cl_Delay = 0;
+								break;
+							case 2:
+								line.cl_Freq = HOURLY_FREQ;
+								break;
+							case 3:
+								line.cl_Freq = DAILY_FREQ;
+								break;
+							case 4:
+								line.cl_Freq = WEEKLY_FREQ;
+								break;
+							case 5:
+								line.cl_Freq = MONTHLY_FREQ;
+								break;
+							case 6:
+								line.cl_Freq = YEARLY_FREQ;
+								break;
+							/* else line.cl_Freq will remain 0 */
+						}
+					}
+
+					if (!line.cl_Freq || (*ptr != ' ' && *ptr != '\t')) {
+						printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s\n", userName, buf);
+						continue;
+					}
+
+					if (line.cl_Delay < 0) {
+						/*
+						 * delays on @daily, @hourly, etc are 1/20 of the frequency
+						 * so they don't all start at once
+						 * this also affects how they behave when the job returns EAGAIN
+						 */
+						line.cl_Delay = line.cl_Freq / 20;
+						line.cl_Delay -= line.cl_Delay % 60;
+						if (line.cl_Delay == 0)
+							line.cl_Delay = 60;
+						/* all minutes are permitted */
+						for (j=0; j<60; ++j)
+							line.cl_Mins[j] = 1;
+						for (j=0; j<24; ++j)
+							line.cl_Hrs[j] = 1;
+						for (j=1; j<32; ++j)
+							/* days are numbered 1..31 */
+							line.cl_Days[j] = 1;
+						for (j=0; j<12; ++j)
+							line.cl_Mons[j] = 1;
+					}
+
+					while (*ptr == ' ' || *ptr == '\t')
+						++ptr;
+
+				} else {
+					/*
+					 * parse date ranges
+					 */
+
+					ptr = ParseField(file->cf_UserName, line.cl_Mins, 60, 0, 1,
+							NULL, ptr);
+					ptr = ParseField(file->cf_UserName, line.cl_Hrs,  24, 0, 1,
+							NULL, ptr);
+					ptr = ParseField(file->cf_UserName, line.cl_Days, 32, 0, 1,
+							NULL, ptr);
+					ptr = ParseField(file->cf_UserName, line.cl_Mons, 12, -1, 1,
+							MonAry, ptr);
+					ptr = ParseField(file->cf_UserName, line.cl_Dow, 7, 0, 31,
+							DowAry, ptr);
+					/*
+					 * check failure
+					 */
+
+					if (ptr == NULL)
+						continue;
+
+					/*
+					 * fix days and dow - if one is not * and the other
+					 * is *, the other is set to 0, and vise-versa
+					 */
+
+					FixDayDow(&line);
+				}
+
+				/* check for ID=... and AFTER=... and FREQ=... */
+				do {
+					if (strncmp(ptr, ID_TAG, strlen(ID_TAG)) == 0) {
+						if (line.cl_JobName) {
+							/* only assign ID_TAG once */
+							printlogf(LOG_WARNING, "failed parsing crontab for user %s: repeated %s\n", userName, ptr);
+							ptr = NULL;
+						} else {
+							ptr += strlen(ID_TAG);
+							/*
+							 * name = strsep(&ptr, seps):
+							 * return name = ptr, and if ptr contains sep chars, overwrite first with 0 and point ptr to next char
+							 *                    else set ptr=NULL
+							 */
+							if (!(line.cl_Description = concat("job ", strsep(&ptr, " \t"), NULL))) {
+								errno = ENOMEM;
+								perror("SynchronizeFile");
+								exit(1);
+							}
+							line.cl_JobName = line.cl_Description + 4;
+							if (!ptr)
+								printlogf(LOG_WARNING, "failed parsing crontab for user %s: no command after %s%s\n", userName, ID_TAG, line.cl_JobName);
+						}
+					} else if (strncmp(ptr, FREQ_TAG, strlen(FREQ_TAG)) == 0) {
+						if (line.cl_Freq) {
+							/* only assign FREQ_TAG once */
+							printlogf(LOG_WARNING, "failed parsing crontab for user %s: repeated %s\n", userName, ptr);
+							ptr = NULL;
+						} else {
+							char *base = ptr;
+							ptr += strlen(FREQ_TAG);
+							ptr = ParseInterval(&line.cl_Freq, ptr);
+							if (ptr && *ptr == '/')
+								ptr = ParseInterval(&line.cl_Delay, ++ptr);
+							else
+								line.cl_Delay = line.cl_Freq;
+							if (!ptr) {
+								printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s\n", userName, base);
+							} else if (*ptr != ' ' && *ptr != '\t') {
+								printlogf(LOG_WARNING, "failed parsing crontab for user %s: no command after %s\n", userName, base);
+								ptr = NULL;
+							}
+						}
+					} else if (strncmp(ptr, WAIT_TAG, strlen(WAIT_TAG)) == 0) {
+						if (line.cl_Waiters) {
+							/* only assign WAIT_TAG once */
+							printlogf(LOG_WARNING, "failed parsing crontab for user %s: repeated %s\n", userName, ptr);
+							ptr = NULL;
+						} else {
+							short more = 1;
+							char *name;
+							ptr += strlen(WAIT_TAG);
+							do {
+								CronLine *job, **pjob;
+								if (strcspn(ptr,",") < strcspn(ptr," \t"))
+									name = strsep(&ptr, ",");
+								else {
+									more = 0;
+									name = strsep(&ptr, " \t");
+								}
+								if (!ptr || *ptr == 0) {
+									/* unexpectedly this was the last token in buf; so abort */
+									printlogf(LOG_WARNING, "failed parsing crontab for user %s: no command after %s%s\n", userName, WAIT_TAG, name);
+									ptr = NULL;
+								} else {
+									int waitfor = 0;
+									char *w, *wsave;
+									if ((w = strchr(name, '/')) != NULL) {
+										wsave = w++;
+										w = ParseInterval(&waitfor, w);
+										if (!w || *w != 0) {
+											printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s%s\n", userName, WAIT_TAG, name);
+											ptr = NULL;
+										} else
+											/* truncate name */
+											*wsave = 0;
+									}
+									if (ptr) {
+										/* look for a matching CronLine */
+										pjob = &file->cf_LineBase;
+										while ((job = *pjob) != NULL) {
+											if (job->cl_JobName && strcmp(job->cl_JobName, name) == 0) {
+												CronWaiter *waiter = malloc(sizeof(CronWaiter));
+												CronNotifier *notif = malloc(sizeof(CronNotifier));
+												waiter->cw_Flag = -1;
+												waiter->cw_MaxWait = waitfor;
+												waiter->cw_NotifLine = job;
+												waiter->cw_Notifier = notif;
+												waiter->cw_Next = line.cl_Waiters;	/* add to head of line.cl_Waiters */
+												line.cl_Waiters = waiter;
+												notif->cn_Waiter = waiter;
+												notif->cn_Next = job->cl_Notifs;	/* add to head of job->cl_Notifs */
+												job->cl_Notifs = notif;
+												break;
+											} else
+												pjob = &job->cl_Next;
+										}
+										if (!job) {
+											printlogf(LOG_WARNING, "failed parsing crontab for user %s: unknown job %s\n", userName, name);
+											/* we can continue parsing this line, we just don't install any CronWaiter for the requested job */
+										}
+									}
+								}
+							} while (ptr && more);
+						}
+					} else
+						break;
+					if (!ptr)
+						break;
+					while (*ptr == ' ' || *ptr == '\t')
+						++ptr;
+				} while (!line.cl_JobName || !line.cl_Waiters || !line.cl_Freq);
+
+				if (line.cl_JobName && (!ptr || *line.cl_JobName == 0)) {
+					/* we're aborting, or ID= was empty */
+					free(line.cl_Description);
+					line.cl_Description = NULL;
+					line.cl_JobName = NULL;
+				}
+				if (ptr && line.cl_Delay > 0 && !line.cl_JobName) {
+					printlogf(LOG_WARNING, "failed parsing crontab for user %s: writing timestamp requires job %s to be named\n", userName, ptr);
+					ptr = NULL;
+				}
+				if (!ptr) {
+					/* couldn't parse so we abort; free any cl_Waiters */
+					if (line.cl_Waiters) {
+						CronWaiter **pwaiters, *waiters;
+						pwaiters = &line.cl_Waiters;
+						while ((waiters = *pwaiters) != NULL) {
+							*pwaiters = waiters->cw_Next;
+							/* leave the Notifier allocated but disabled */
+							waiters->cw_Notifier->cn_Waiter = NULL;
+							free(waiters);
+						}
+					}
+					continue;
+				}
+				/* now we've added any ID=... or AFTER=... */
+
+				/*
+				 * copy command string
+				 */
+				line.cl_Shell = strdup(ptr);
+
+				if (line.cl_Delay > 0) {
+					if (!(line.cl_Timestamp = concat(TSDir, "/", userName, ".", line.cl_JobName, NULL))) {
+						errno = ENOMEM;
+						perror("SynchronizeFile");
+						exit(1);
+					}
+					line.cl_NotUntil = tnow + line.cl_Delay;
+				}
+
+				if (line.cl_JobName) {
+					if (DebugOpt)
+						printlogf(LOG_DEBUG, "    Command %s Job %s\n", line.cl_Shell, line.cl_JobName);
+				} else {
+					/* when cl_JobName is NULL, we point cl_Description to cl_Shell */
+					line.cl_Description = line.cl_Shell;
+					if (DebugOpt)
+						printlogf(LOG_DEBUG, "    Command %s\n", line.cl_Shell);
+				}
+
+				*pline = calloc(1, sizeof(CronLine));
+				/* copy working CronLine to newly allocated one */
+				**pline = line;
+
+				pline = &((*pline)->cl_Next);
+			}
+
+			*pline = NULL;
+
+			file->cf_Next = FileBase;
+			FileBase = file;
+
+			if (maxLines == 0 || maxEntries == 0)
+				printlogf(LOG_WARNING, "maximum number of lines reached for user %s\n", userName);
+		}
+		fclose(fi);
+	}
+	free(path);
+}
+
+char *
+ParseInterval(int *interval, char *ptr)
+{
+	int n = 0;
+	if (ptr && *ptr >= '0' && *ptr <= '9' && (n = strtol(ptr, &ptr, 10)) > 0)
+		switch (*ptr) {
+			case 'm':
+				n *= 60;
+				break;
+			case 'h':
+				n *= HOURLY_FREQ;
+				break;
+			case 'd':
+				n *= DAILY_FREQ;
+				break;
+			case 'w':
+				n *= WEEKLY_FREQ;
+				break;
+			default:
+				n = 0;
+		}
+	if (n > 0) {
+		*interval = n;
+		return (ptr+1);
+	} else
+		return (NULL);
+}
+
+char *
+ParseField(char *user, char *ary, int modvalue, int off, int onvalue, const char **names, char *ptr)
+{
+	char *base = ptr;
+	int n1 = -1;
+	int n2 = -1;
+
+	if (base == NULL)
+		return (NULL);
+
+	while (*ptr != ' ' && *ptr != '\t' && *ptr != '\n') {
+		int skip = 0;
+
+		/*
+		 * Handle numeric digit or symbol or '*'
+		 */
+
+		if (*ptr == '*') {
+			n1 = 0;			/* everything will be filled */
+			n2 = modvalue - 1;
+			skip = 1;
+			++ptr;
+		} else if (*ptr >= '0' && *ptr <= '9') {
+			if (n1 < 0)
+				n1 = strtol(ptr, &ptr, 10) + off;
+			else
+				n2 = strtol(ptr, &ptr, 10) + off;
+			skip = 1;
+		} else if (names) {
+			int i;
+
+			for (i = 0; names[i]; ++i) {
+				if (strncmp(ptr, names[i], strlen(names[i])) == 0) {
+					break;
+				}
+			}
+			if (names[i]) {
+				ptr += strlen(names[i]);
+				if (n1 < 0)
+					n1 = i;
+				else
+					n2 = i;
+				skip = 1;
+			}
+		}
+
+		/*
+		 * handle optional range '-'
+		 */
+
+		if (skip == 0) {
+			printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s\n", user, base);
+			return(NULL);
+		}
+		if (*ptr == '-' && n2 < 0) {
+			++ptr;
+			continue;
+		}
+
+		/*
+		 * collapse single-value ranges, handle skipmark, and fill
+		 * in the character array appropriately.
+		 */
+
+		if (n2 < 0)
+			n2 = n1;
+
+		n2 = n2 % modvalue;
+
+		if (*ptr == '/')
+			skip = strtol(ptr + 1, &ptr, 10);
+
+		/*
+		 * fill array, using a failsafe is the easiest way to prevent
+		 * an endless loop
+		 */
+
+		{
+			int s0 = 1;
+			int failsafe = 1024;
+
+			--n1;
+			do {
+				n1 = (n1 + 1) % modvalue;
+
+				if (--s0 == 0) {
+					ary[n1] = onvalue;
+					s0 = skip;
+				}
+			} while (n1 != n2 && --failsafe);
+
+			if (failsafe == 0) {
+				printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s\n", user, base);
+				return(NULL);
+			}
+		}
+		if (*ptr != ',')
+			break;
+		++ptr;
+		n1 = -1;
+		n2 = -1;
+	}
+
+	if (*ptr != ' ' && *ptr != '\t' && *ptr != '\n') {
+		printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s\n", user, base);
+		return(NULL);
+	}
+
+	while (*ptr == ' ' || *ptr == '\t' || *ptr == '\n')
+		++ptr;
+
+	if (DebugOpt) {
+		int i;
+
+		for (i = 0; i < modvalue; ++i)
+			if (modvalue == 7)
+				printlogf(LOG_DEBUG, "%2x ", ary[i]);
+			else
+				printlogf(LOG_DEBUG, "%d", ary[i]);
+		printlogf(LOG_DEBUG, "\n");
+	}
+
+	return(ptr);
+}
+
+void
+FixDayDow(CronLine *line)
+{
+	unsigned short i,j;
+	short weekUsed = 0;
+	short daysUsed = 0;
+
+	for (i = 0; i < arysize(line->cl_Dow); ++i) {
+		if (line->cl_Dow[i] == 0) {
+			weekUsed = 1;
+			break;
+		}
+	}
+	for (i = 0; i < arysize(line->cl_Days); ++i) {
+		if (line->cl_Days[i] == 0) {
+			if (weekUsed) {
+				if (!daysUsed) {
+					daysUsed = 1;
+					/* change from "every Mon" to "ith Mon"
+					 * 6th,7th... Dow are treated as 1st,2nd... */
+					for (j = 0; j < arysize(line->cl_Dow); ++j) {
+						line->cl_Dow[j] &= 1 << (i-1)%5;
+					}
+				} else {
+					/* change from "nth Mon" to "nth or ith Mon" */
+					for (j = 0; j < arysize(line->cl_Dow); ++j) {
+						if (line->cl_Dow[j])
+							line->cl_Dow[j] |= 1 << (i-1)%5;
+					}
+				}
+				/* continue cycling through cl_Days */
+			}
+			else {
+				daysUsed = 1;
+				break;
+			}
+		}
+	}
+	if (weekUsed) {
+		memset(line->cl_Days, 0, sizeof(line->cl_Days));
+	}
+	if (daysUsed && !weekUsed) {
+		memset(line->cl_Dow, 0, sizeof(line->cl_Dow));
+	}
+}
+
+/*
+ *  DeleteFile() - destroy a CronFile.
+ *
+ *  The CronFile (*pfile) is destroyed if possible, and marked cf_Deleted
+ *  if there are still active processes running on it.  *pfile is relinked
+ *  on success.
+ */
+void
+DeleteFile(CronFile **pfile)
+{
+	CronFile *file = *pfile;
+	CronLine **pline = &file->cf_LineBase;
+	CronLine *line;
+	CronWaiter **pwaiters, *waiters;
+	CronNotifier **pnotifs, *notifs;
+
+	file->cf_Running = 0;
+	file->cf_Deleted = 1;
+
+	while ((line = *pline) != NULL) {
+		if (line->cl_Pid > 0) {
+			file->cf_Running = 1;
+			pline = &line->cl_Next;
+		} else {
+			*pline = line->cl_Next;
+			free(line->cl_Shell);
+
+			if (line->cl_JobName)
+				/* this frees both cl_Description and cl_JobName
+				 * if cl_JobName is NULL, Description pointed to ch_Shell, which was already freed
+				 */
+				free(line->cl_Description);
+			if (line->cl_Timestamp)
+				free(line->cl_Timestamp);
+
+			pnotifs = &line->cl_Notifs;
+			while ((notifs = *pnotifs) != NULL) {
+				*pnotifs = notifs->cn_Next;
+				if (notifs->cn_Waiter) {
+					notifs->cn_Waiter->cw_NotifLine = NULL;
+					notifs->cn_Waiter->cw_Notifier = NULL;
+				}
+				free(notifs);
+			}
+			pwaiters = &line->cl_Waiters;
+			while ((waiters = *pwaiters) != NULL) {
+				*pwaiters = waiters->cw_Next;
+				if (waiters->cw_Notifier)
+					waiters->cw_Notifier->cn_Waiter = NULL;
+				free(waiters);
+			}
+
+			free(line);
+		}
+	}
+	if (file->cf_Running == 0) {
+		*pfile = file->cf_Next;
+		free(file->cf_DPath);
+		free(file->cf_FileName);
+		free(file->cf_UserName);
+		free(file);
+	}
+}
+
+
+/*
+ * TestJobs()
+ *
+ * determine which jobs need to be run.  Under normal conditions, the
+ * period is about a minute (one scan).  Worst case it will be one
+ * hour (60 scans).
+ */
+
+int
+TestJobs(time_t t1, time_t t2)
+{
+	short nJobs = 0;
+	time_t t;
+	CronFile *file;
+	CronLine *line;
+
+	for (file = FileBase; file; file = file->cf_Next) {
+		if (file->cf_Deleted)
+			continue;
+		for (line = file->cf_LineBase; line; line = line->cl_Next) {
+			struct CronWaiter *waiter;
+
+			if (line->cl_Pid == -2) {
+				/* can job stop waiting? */
+				int ready = 1;
+				waiter = line->cl_Waiters;
+				while (waiter != NULL) {
+					if (waiter->cw_Flag > 0) {
+						/* notifier exited unsuccessfully */
+						ready = 2;
+						break;
+					} else if (waiter->cw_Flag < 0)
+						/* still waiting, notifier hasn't run to completion */
+						ready = 0;
+					waiter = waiter->cw_Next;
+				}
+				if (ready == 2) {
+					if (DebugOpt)
+						printlogf(LOG_DEBUG, "cancelled waiting: user %s %s\n", file->cf_UserName, line->cl_Description);
+					line->cl_Pid = 0;
+				} else if (ready) {
+					if (DebugOpt)
+						printlogf(LOG_DEBUG, "finished waiting: user %s %s\n", file->cf_UserName, line->cl_Description);
+					nJobs += ArmJob(file, line, 0, -1);
+					/*
+					 if (line->cl_NotUntil)
+						 line->cl_NotUntil = t2;
+					*/
+				}
+			}
+		}
+	}
+
+	/*
+	 * Find jobs > t1 and <= t2
+	 */
+
+	for (t = t1 - t1 % 60; t <= t2; t += 60) {
+		if (t > t1) {
+			struct tm *tp = localtime(&t);
+
+			unsigned short n_wday = (tp->tm_mday - 1)%7 + 1;
+			if (n_wday >= 4) {
+				struct tm tnext = *tp;
+				tnext.tm_mday += 7;
+				if (mktime(&tnext) != (time_t)-1 && tnext.tm_mon != tp->tm_mon)
+					n_wday |= 16;	/* last dow in month is always recognized as 5th */
+			}
+
+			for (file = FileBase; file; file = file->cf_Next) {
+				if (file->cf_Deleted)
+					continue;
+				for (line = file->cf_LineBase; line; line = line->cl_Next) {
+					if ((line->cl_Pid == -2 || line->cl_Pid == 0) && (line->cl_Freq == 0 || (line->cl_Freq > 0 && t2 >= line->cl_NotUntil))) {
+						/* (re)schedule job? */
+						if (line->cl_Mins[tp->tm_min] &&
+								line->cl_Hrs[tp->tm_hour] &&
+								(line->cl_Days[tp->tm_mday] || (n_wday && line->cl_Dow[tp->tm_wday]) ) &&
+								line->cl_Mons[tp->tm_mon]
+						   ) {
+							if (line->cl_NotUntil)
+								line->cl_NotUntil = t2 - t2 % 60 + line->cl_Delay; /* save what minute this job was scheduled/started waiting, plus cl_Delay */
+							nJobs += ArmJob(file, line, t1, t2);
+						}
+					}
+				}
+			}
+		}
+	}
+	return(nJobs);
+}
+
+/*
+ * ArmJob: if t2 is (time_t)-1, we force-schedule the job without any waiting
+ * else it will wait on any of its declared notifiers who will run <= t2 + cw_MaxWait
+ */
+
+int
+ArmJob(CronFile *file, CronLine *line, time_t t1, time_t t2)
+{
+	struct CronWaiter *waiter;
+	if (line->cl_Pid > 0) {
+		printlogf(LOG_NOTICE, "process already running (%d): user %s %s\n",
+				line->cl_Pid,
+				file->cf_UserName,
+				line->cl_Description
+			);
+	} else if (t2 == -1 && line->cl_Pid != -1) {
+		line->cl_Pid = -1;
+		file->cf_Ready = 1;
+		return 1;
+	} else if (line->cl_Pid == 0) {
+		/* arming a waiting job (cl_Pid == -2) without forcing has no effect */
+		line->cl_Pid = -1;
+		/* if we have any waiters, zero them and arm cl_Pid=-2 */
+		waiter = line->cl_Waiters;
+		while (waiter != NULL) {
+			/* check if notifier will run <= t2 + cw_Max_Wait? */
+			if (!waiter->cw_NotifLine)
+				/* notifier deleted */
+				waiter->cw_Flag = 0;
+			else if (waiter->cw_NotifLine->cl_Pid != 0) {
+				/* if notifier is armed, or waiting, or running, we wait for it */
+				waiter->cw_Flag = -1;
+				line->cl_Pid = -2;
+			} else if (waiter->cw_NotifLine->cl_Freq < 0) {
+				/* arm any @noauto or @reboot jobs we're waiting on */
+				ArmJob(file, waiter->cw_NotifLine, t1, t2);
+				waiter->cw_Flag = -1;
+				line->cl_Pid = -2;
+			} else {
+				time_t t;
+				if (waiter->cw_MaxWait == 0)
+					/* when no MaxWait interval specified, we always wait */
+					waiter->cw_Flag = -1;
+				else if (waiter->cw_NotifLine->cl_Freq == 0 || (waiter->cw_NotifLine->cl_Freq > 0 && t2 + waiter->cw_MaxWait >= waiter->cw_NotifLine->cl_NotUntil)) {
+					/* default is don't wait */
+					waiter->cw_Flag = 0;
+					for (t = t1 - t1 % 60; t <= t2; t += 60) {
+						if (t > t1) {
+							struct tm *tp = localtime(&t);
+
+							unsigned short n_wday = (tp->tm_mday - 1)%7 + 1;
+							if (n_wday >= 4) {
+								struct tm tnext = *tp;
+								tnext.tm_mday += 7;
+								if (mktime(&tnext) != (time_t)-1 && tnext.tm_mon != tp->tm_mon)
+									n_wday |= 16;	/* last dow in month is always recognized as 5th */
+							}
+							if (line->cl_Mins[tp->tm_min] &&
+									line->cl_Hrs[tp->tm_hour] &&
+									(line->cl_Days[tp->tm_mday] || (n_wday && line->cl_Dow[tp->tm_wday]) ) &&
+									line->cl_Mons[tp->tm_mon]
+							   ) {
+								/* notifier will run soon enough, we wait for it */
+								waiter->cw_Flag = -1;
+								line->cl_Pid = -2;
+								break;
+							}
+						}
+					}
+				}
+			}
+			waiter = waiter->cw_Next;
+		}
+		if (line->cl_Pid == -1) {
+			/* job is ready to run */
+			file->cf_Ready = 1;
+			if (DebugOpt)
+				printlogf(LOG_DEBUG, "scheduled: user %s %s\n",
+						file->cf_UserName,
+						line->cl_Description
+					);
+			return 1;
+		} else if (DebugOpt)
+			printlogf(LOG_DEBUG, "waiting: user %s %s\n",
+					file->cf_UserName,
+					line->cl_Description
+				);
+	}
+	return 0;
+}
+
+int
+TestStartupJobs(void)
+{
+	short nJobs = 0;
+	time_t t1 = time(NULL);
+	CronFile *file;
+	CronLine *line;
+
+	t1 = t1 - t1 % 60 + 60;
+
+	for (file = FileBase; file; file = file->cf_Next) {
+		if (DebugOpt)
+			printlogf(LOG_DEBUG, "TestStartup for FILE %s/%s USER %s:\n",
+				file->cf_DPath, file->cf_FileName, file->cf_UserName);
+		for (line = file->cf_LineBase; line; line = line->cl_Next) {
+			struct CronWaiter *waiter;
+			if (DebugOpt) {
+				if (line->cl_JobName)
+					printlogf(LOG_DEBUG, "    LINE %s JOB %s\n", line->cl_Shell, line->cl_JobName);
+				else
+					printlogf(LOG_DEBUG, "    LINE %s\n", line->cl_Shell);
+			}
+
+			if (line->cl_Freq == -1) {
+				/* freq is @reboot */
+
+				line->cl_Pid = -1;
+				/* if we have any waiters, reset them and arm Pid = -2 */
+				waiter = line->cl_Waiters;
+				while (waiter != NULL) {
+					waiter->cw_Flag = -1;
+					line->cl_Pid = -2;
+					/* we only arm @noauto jobs we're waiting on, not other @reboot jobs */
+					if (waiter->cw_NotifLine && waiter->cw_NotifLine->cl_Freq == -2)
+						ArmJob(file, waiter->cw_NotifLine, t1, t1+60);
+					waiter = waiter->cw_Next;
+				}
+				if (line->cl_Pid == -1) {
+					/* job is ready to run */
+					file->cf_Ready = 1;
+					++nJobs;
+					if (DebugOpt)
+						printlogf(LOG_DEBUG, "    scheduled: %s\n", line->cl_Description);
+				} else if (DebugOpt)
+					printlogf(LOG_DEBUG, "    waiting: %s\n", line->cl_Description);
+
+			}
+
+		} /* for line */
+	}
+	return(nJobs);
+}
+
+void
+RunJobs(void)
+{
+	CronFile *file;
+	CronLine *line;
+
+	for (file = FileBase; file; file = file->cf_Next) {
+		if (file->cf_Ready) {
+			file->cf_Ready = 0;
+
+			for (line = file->cf_LineBase; line; line = line->cl_Next) {
+				if (line->cl_Pid == -1) {
+
+					RunJob(file, line);
+
+					printlogf(LOG_INFO, "FILE %s/%s USER %s PID %3d %s\n",
+							file->cf_DPath,
+							file->cf_FileName,
+							file->cf_UserName,
+							line->cl_Pid,
+							line->cl_Description
+						);
+					if (line->cl_Pid < 0)
+						/* QUESTION how could this happen? RunJob will leave cl_Pid set to 0 or the actual pid */
+						file->cf_Ready = 1;
+					else if (line->cl_Pid > 0)
+						file->cf_Running = 1;
+				}
+			}
+		}
+	}
+}
+
+/*
+ * CheckJobs() - check for job completion
+ *
+ * Check for job completion, return number of CronFiles still running after
+ * all done.
+ */
+
+int
+CheckJobs(void)
+{
+	CronFile *file;
+	CronLine *line;
+	int nStillRunning = 0;
+
+	for (file = FileBase; file; file = file->cf_Next) {
+		if (file->cf_Running) {
+			file->cf_Running = 0;
+
+			for (line = file->cf_LineBase; line; line = line->cl_Next) {
+				if (line->cl_Pid > 0) {
+					int status;
+					int r = waitpid(line->cl_Pid, &status, WNOHANG);
+
+					/* waitpid returns -1 for error, 0 if cl_Pid still running, cl_Pid if it's dead */
+
+					if (r < 0 || r == line->cl_Pid) {
+						if (r > 0 && WIFEXITED(status))
+							status = WEXITSTATUS(status);
+						else
+							status = 1;
+						EndJob(file, line, status);
+
+					} else if (r == 0) {
+						file->cf_Running = 1;
+					}
+				}
+			}
+			nStillRunning += file->cf_Running;
+		}
+		/* For the purposes of this check, increase the "still running" counter if a file has lines that are waiting */
+		if (file->cf_Running == 0) {
+			for (line = file->cf_LineBase; line; line = line->cl_Next) {
+				if (line->cl_Pid == -2) {
+					nStillRunning += 1;
+					break;
+				}
+			}
+		}
+	}
+	return(nStillRunning);
+}
+