From 86d96ab7ab8c8361d9e9b790b24c0691ce03a65d Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Thu, 23 Jul 2020 18:12:05 +0100 Subject: [PATCH 01/28] Propagate username to a per-job attribute Retain the username on a file level; this is used for error reporting. This patch prepares for users in some contexts (eg. root in /etc/cron.d) to specify which user each job runs as. --- database.c | 3 +++ defs.h | 1 + job.c | 8 ++++---- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/database.c b/database.c index 270ab9b..4f192c5 100644 --- a/database.c +++ b/database.c @@ -381,6 +381,7 @@ SynchronizeFile(const char *dpath, const char *fileName, const char *userName) break; memset(&line, 0, sizeof(line)); + line.cl_UserName = strdup(userName); if (DebugOpt) printlogf(LOG_DEBUG, "User %s Entry %s\n", userName, buf); @@ -913,6 +914,7 @@ DeleteFile(CronFile **pfile) } else { *pline = line->cl_Next; free(line->cl_Shell); + free(line->cl_UserName); if (line->cl_JobName) /* this frees both cl_Description and cl_JobName @@ -1281,6 +1283,7 @@ PrintLine(CronLine *line) printlogf(LOG_DEBUG, "CronLine:\n------------\n"); printlogf(LOG_DEBUG, " Command: %s\n", line->cl_Shell); + printlogf(LOG_DEBUG, " User: %s\n", line->cl_UserName); //printlogf(LOG_DEBUG, " Desc: %s\n", line->cl_Description); printlogf(LOG_DEBUG, " Freq: %s\n", (line->cl_Freq ? (line->cl_Freq == -1 ? "(noauto)" : "(startup") : "(use arrays)")); diff --git a/defs.h b/defs.h index 5903e6e..90f37a4 100644 --- a/defs.h +++ b/defs.h @@ -137,6 +137,7 @@ typedef struct CronFile { typedef struct CronLine { struct CronLine *cl_Next; char *cl_Shell; /* shell command */ + char *cl_UserName; /* to execute as */ char *cl_Description; /* either "" or "job " */ char *cl_JobName; /* job name, if any */ char *cl_Timestamp; /* path to timestamp file, if cl_Freq defined */ diff --git a/job.c b/job.c index a0201e7..0893e66 100644 --- a/job.c +++ b/job.c @@ -62,9 +62,9 @@ RunJob(CronFile *file, CronLine *line) * Change running state to the user in question */ - if (ChangeUser(file->cf_UserName, TempDir) < 0) { + if (ChangeUser(line->cl_UserName, TempDir) < 0) { printlogf(LOG_ERR, "unable to ChangeUser (user %s %s)\n", - file->cf_UserName, + line->cl_UserName, line->cl_Description ); exit(0); @@ -112,7 +112,7 @@ RunJob(CronFile *file, CronLine *line) * Complain to our log (now associated with fd 8) */ fdprintlogf(LOG_ERR, 8, "unable to exec (user %s cmd /bin/sh -c %s)\n", - file->cf_UserName, + line->cl_UserName, line->cl_Shell ); /* @@ -128,7 +128,7 @@ RunJob(CronFile *file, CronLine *line) * Complain to log (with regular fd 2) */ printlogf(LOG_ERR, "unable to fork (user %s %s)\n", - file->cf_UserName, + line->cl_UserName, line->cl_Description ); line->cl_Pid = 0; From e64b39029fa2bcc219c7cd9291d37cd929b4375b Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Thu, 23 Jul 2020 18:51:07 +0100 Subject: [PATCH 02/28] Optionally specify to use the parsing format that includes username --- database.c | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/database.c b/database.c index 4f192c5..f2d524b 100644 --- a/database.c +++ b/database.c @@ -26,7 +26,7 @@ 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 SynchronizeFile(const char *dpath, const char *fname, const char *uname, int parseUser); void DeleteFile(CronFile **pfile); char *ParseInterval(int *interval, char *ptr); char *ParseField(char *userName, char *ary, int modvalue, int offset, int onvalue, const char **names, char *ptr); @@ -126,11 +126,11 @@ CheckUpdates(const char *dpath, const char *user_override, time_t t1, time_t t2) fname = strtok_r(buf, " \t\n", &ptok); if (user_override) - SynchronizeFile(dpath, fname, user_override); + SynchronizeFile(dpath, fname, user_override, 0); 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); + SynchronizeFile(dpath, fname, fname, 0); ReadTimestamps(fname); } else { /* if fname is followed by whitespace, we prod any following jobs */ @@ -221,9 +221,9 @@ SynchronizeDir(const char *dpath, const char *user_override, int initial_scan) if (strcmp(den->d_name, CRONUPDATE) == 0) continue; if (user_override) { - SynchronizeFile(dpath, den->d_name, user_override); + SynchronizeFile(dpath, den->d_name, user_override, 0); } else if (getpwnam(den->d_name)) { - SynchronizeFile(dpath, den->d_name, den->d_name); + SynchronizeFile(dpath, den->d_name, den->d_name, 0); } else { printlogf(LOG_WARNING, "ignoring %s/%s (non-existent user)\n", dpath, den->d_name); @@ -309,7 +309,7 @@ ReadTimestamps(const char *user) } void -SynchronizeFile(const char *dpath, const char *fileName, const char *userName) +SynchronizeFile(const char *dpath, const char *fileName, const char *userName, int parseUser) { CronFile **pfile; CronFile *file; @@ -381,7 +381,6 @@ SynchronizeFile(const char *dpath, const char *fileName, const char *userName) break; memset(&line, 0, sizeof(line)); - line.cl_UserName = strdup(userName); if (DebugOpt) printlogf(LOG_DEBUG, "User %s Entry %s\n", userName, buf); @@ -629,6 +628,19 @@ SynchronizeFile(const char *dpath, const char *fileName, const char *userName) } /* now we've added any ID=... or AFTER=... */ + /* + * system crontabs (cron.d) have an extra field for username + */ + if (parseUser) + line.cl_UserName = strdup(strsep(&ptr, " \t")); + else + line.cl_UserName = strdup(userName); + + if (!ptr) { + printlogf(LOG_WARNING, "failed parsing system crontab: no username found for job"); + abort(); + } + /* * copy command string */ From fddbda1ffb28ad2234ccd1d5f543ac68b4e0eaf6 Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Thu, 23 Jul 2020 18:59:40 +0100 Subject: [PATCH 03/28] Use "system" crontab parsing in specific cases Maintain the existing behaviour in other cases, where the username is specified from the file. --- database.c | 21 ++++++++++----------- main.c | 12 ++++++------ 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/database.c b/database.c index f2d524b..5c36c36 100644 --- a/database.c +++ b/database.c @@ -17,8 +17,8 @@ #define LAST_DOW (1 << 5) #define ALL_DOW (FIRST_DOW|SECOND_DOW|THIRD_DOW|FOURTH_DOW|FIFTH_DOW|LAST_DOW) -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 CheckUpdates(const char *dpath, int is_system, time_t t1, time_t t2); +Prototype void SynchronizeDir(const char *dpath, int is_system, int initial_scan); Prototype void ReadTimestamps(const char *user); Prototype int TestJobs(time_t t1, time_t t2); Prototype int TestStartupJobs(void); @@ -96,12 +96,11 @@ const char *FreqAry[] = { }; /* - * 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. + * Check the cron.update file in the specified directory. System crontabs + * are 'owned' by root but parse a username to run each job as. */ void -CheckUpdates(const char *dpath, const char *user_override, time_t t1, time_t t2) +CheckUpdates(const char *dpath, int is_system, time_t t1, time_t t2) { FILE *fi; char buf[SMALL_BUFFER]; @@ -125,8 +124,8 @@ CheckUpdates(const char *dpath, const char *user_override, time_t t1, time_t t2) */ fname = strtok_r(buf, " \t\n", &ptok); - if (user_override) - SynchronizeFile(dpath, fname, user_override, 0); + if (is_system) + SynchronizeFile(dpath, fname, "root", 1); else if (!getpwnam(fname)) printlogf(LOG_WARNING, "ignoring %s/%s (non-existent user)\n", dpath, fname); else if (*ptok == 0 || *ptok == '\n') { @@ -174,7 +173,7 @@ CheckUpdates(const char *dpath, const char *user_override, time_t t1, time_t t2) } void -SynchronizeDir(const char *dpath, const char *user_override, int initial_scan) +SynchronizeDir(const char *dpath, int is_system, int initial_scan) { CronFile **pfile; CronFile *file; @@ -220,8 +219,8 @@ SynchronizeDir(const char *dpath, const char *user_override, int initial_scan) continue; if (strcmp(den->d_name, CRONUPDATE) == 0) continue; - if (user_override) { - SynchronizeFile(dpath, den->d_name, user_override, 0); + if (is_system) { + SynchronizeFile(dpath, den->d_name, "root", 1); } else if (getpwnam(den->d_name)) { SynchronizeFile(dpath, den->d_name, den->d_name, 0); } else { diff --git a/main.c b/main.c index 1fc5cd6..aa95533 100644 --- a/main.c +++ b/main.c @@ -290,8 +290,8 @@ main(int ac, char **av) */ printlogf(LOG_NOTICE,"%s " VERSION " dillon's cron daemon, started with loglevel %s\n", av[0], LevelAry[LogLevel]); - SynchronizeDir(CDir, NULL, 1); - SynchronizeDir(SCDir, "root", 1); + SynchronizeDir(CDir, 0, 1); + SynchronizeDir(SCDir, 1, 1); ReadTimestamps(NULL); TestStartupJobs(); /* @startup jobs only run when crond is started, not when their crontab is loaded */ @@ -334,14 +334,14 @@ main(int ac, char **av) rescan = 1; } else { rescan = 60; - SynchronizeDir(CDir, NULL, 0); - SynchronizeDir(SCDir, "root", 0); + SynchronizeDir(CDir, 0, 0); + SynchronizeDir(SCDir, 1, 0); ReadTimestamps(NULL); } } if (rescan < 60) { - CheckUpdates(CDir, NULL, t1, t2); - CheckUpdates(SCDir, "root", t1, t2); + CheckUpdates(CDir, 0, t1, t2); + CheckUpdates(SCDir, 1, t1, t2); } if (DebugOpt) printlogf(LOG_DEBUG, "Wakeup dt=%d\n", dt); From 5d3258c993af56b1afed19f8c49199c81082a07e Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Thu, 23 Jul 2020 19:27:35 +0100 Subject: [PATCH 04/28] Improve the display of parse errors The username is ambiguous when displaying errors, as the root user can have multiple files. Reporting errors with the full context and filename should make it much easier to work out what is happening when something goes wrong. This also helps identify places where the username is actually used for function and not just logging. --- database.c | 68 ++++++++++++++++++++++-------------------------------- 1 file changed, 28 insertions(+), 40 deletions(-) diff --git a/database.c b/database.c index 5c36c36..97d829f 100644 --- a/database.c +++ b/database.c @@ -29,7 +29,7 @@ Prototype int CheckJobs(void); void SynchronizeFile(const char *dpath, const char *fname, const char *uname, int parseUser); void DeleteFile(CronFile **pfile); char *ParseInterval(int *interval, char *ptr); -char *ParseField(char *userName, char *ary, int modvalue, int offset, int onvalue, const char **names, char *ptr); +char *ParseField(char *ary, int modvalue, int offset, int onvalue, const char **names, char *ptr); void FixDayDow(CronLine *line); void PrintLine(CronLine *line); void PrintFile(CronFile *file, char* loc, char* fname, int line); @@ -382,7 +382,7 @@ SynchronizeFile(const char *dpath, const char *fileName, const char *userName, i memset(&line, 0, sizeof(line)); if (DebugOpt) - printlogf(LOG_DEBUG, "User %s Entry %s\n", userName, buf); + printlogf(LOG_DEBUG, "%s as %s: %s\n", path, userName, buf); if (*ptr == '@') { /* @@ -429,7 +429,7 @@ SynchronizeFile(const char *dpath, const char *fileName, const char *userName, i } if (!line.cl_Freq || (*ptr != ' ' && *ptr != '\t')) { - printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s\n", userName, buf); + printlogf(LOG_WARNING, "%s: Failed parsing '@' interval: %s\n", path, buf); continue; } @@ -463,22 +463,20 @@ SynchronizeFile(const char *dpath, const char *fileName, const char *userName, i * parse date ranges */ - ptr = ParseField(file->cf_UserName, line.cl_Mins, FIELD_MINUTES, 0, 1, - NULL, ptr); - ptr = ParseField(file->cf_UserName, line.cl_Hrs, FIELD_HOURS, 0, 1, - NULL, ptr); - ptr = ParseField(file->cf_UserName, line.cl_Days, FIELD_M_DAYS, 0, 1, - NULL, ptr); - ptr = ParseField(file->cf_UserName, line.cl_Mons, FIELD_MONTHS, -1, 1, - MonAry, ptr); - ptr = ParseField(file->cf_UserName, line.cl_Dow, FIELD_W_DAYS, 0, ALL_DOW, - DowAry, ptr); + ptr = ParseField(line.cl_Mins, FIELD_MINUTES, 0, 1, NULL, ptr); + ptr = ParseField(line.cl_Hrs, FIELD_HOURS, 0, 1, NULL, ptr); + ptr = ParseField(line.cl_Days, FIELD_M_DAYS, 0, 1, NULL, ptr); + ptr = ParseField(line.cl_Mons, FIELD_MONTHS, -1, 1, MonAry, ptr); + ptr = ParseField(line.cl_Dow, FIELD_W_DAYS, 0, ALL_DOW, DowAry, ptr); + /* * check failure */ - if (ptr == NULL) + if (ptr == NULL) { + printlogf(LOG_WARNING, "%s: Failed to parse date/time specification: %s\n", path, buf); continue; + } /* * fix days and dow - if one is not * and the other @@ -493,7 +491,7 @@ SynchronizeFile(const char *dpath, const char *fileName, const char *userName, i 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); + printlogf(LOG_WARNING, "%s: Multiple use of " ID_TAG " is invalid: %s\n", path, buf); ptr = NULL; } else { ptr += strlen(ID_TAG); @@ -509,12 +507,12 @@ SynchronizeFile(const char *dpath, const char *fileName, const char *userName, i } 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); + printlogf(LOG_WARNING, "%s: Entry ends unexpectedly after " ID_TAG "%s: %s\n", path, line.cl_JobName, buf); } } 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); + printlogf(LOG_WARNING, "%s: Multiple use of " FREQ_TAG " is invalid: %s\n", path, buf); ptr = NULL; } else { char *base = ptr; @@ -524,17 +522,13 @@ SynchronizeFile(const char *dpath, const char *fileName, const char *userName, i 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; - } + if (!ptr) + printlogf(LOG_WARNING, "%s: Entry ends unexpectedly after " FREQ_TAG ": %s", path, buf); } } 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); + printlogf(LOG_WARNING, "%s: Multiple use of " WAIT_TAG " is invalid: %s\n", path, buf); ptr = NULL; } else { short more = 1; @@ -550,7 +544,7 @@ SynchronizeFile(const char *dpath, const char *fileName, const char *userName, i } 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); + printlogf(LOG_WARNING, "%s: Entry ends unexpectedly after " WAIT_TAG "%s: %s", path, name, buf); ptr = NULL; } else { int waitfor = 0; @@ -559,7 +553,7 @@ SynchronizeFile(const char *dpath, const char *fileName, const char *userName, i 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); + printlogf(LOG_WARNING, "%s: Could not parse interval for " WAIT_TAG "%s: %s\n", path, name, buf); ptr = NULL; } else /* truncate name */ @@ -586,7 +580,7 @@ SynchronizeFile(const char *dpath, const char *fileName, const char *userName, i pjob = &job->cl_Next; } if (!job) { - printlogf(LOG_WARNING, "failed parsing crontab for user %s: unknown job %s\n", userName, name); + printlogf(LOG_WARNING, "%s: Ignoring unknown job: " WAIT_TAG "%s: %s\n", path, name, buf); /* we can continue parsing this line, we just don't install any CronWaiter for the requested job */ } } @@ -608,7 +602,7 @@ SynchronizeFile(const char *dpath, const char *fileName, const char *userName, i 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); + printlogf(LOG_WARNING, "%s: Writing timestamp requries job to be named: %s\n", path, buf); ptr = NULL; } if (!ptr) { @@ -636,7 +630,7 @@ SynchronizeFile(const char *dpath, const char *fileName, const char *userName, i line.cl_UserName = strdup(userName); if (!ptr) { - printlogf(LOG_WARNING, "failed parsing system crontab: no username found for job"); + printlogf(LOG_WARNING, "%s: Could not parse system crontab; username expected per-job: %s\n", path, buf); abort(); } @@ -677,7 +671,7 @@ SynchronizeFile(const char *dpath, const char *fileName, const char *userName, i FileBase = file; if (maxLines == 0 || maxEntries == 0) - printlogf(LOG_WARNING, "maximum number of lines reached for user %s\n", userName); + printlogf(LOG_WARNING, "%s: Limit on number of entries has been reached\n", path); } fclose(fi); } @@ -713,7 +707,7 @@ ParseInterval(int *interval, char *ptr) } char * -ParseField(char *user, char *ary, int modvalue, int offset, int onvalue, const char **names, char *ptr) +ParseField(char *ary, int modvalue, int offset, int onvalue, const char **names, char *ptr) { char *base = ptr; int n1 = -1; @@ -762,10 +756,8 @@ ParseField(char *user, char *ary, int modvalue, int offset, int onvalue, const c * handle optional range '-' */ - if (skip == 0) { - printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s\n", user, base); + if (skip == 0) return(NULL); - } if (*ptr == '-' && n2 < 0) { ++ptr; continue; @@ -803,10 +795,8 @@ ParseField(char *user, char *ary, int modvalue, int offset, int onvalue, const c } } while (n1 != n2 && --failsafe); - if (failsafe == 0) { - printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s\n", user, base); + if (failsafe == 0) return(NULL); - } } if (*ptr != ',') break; @@ -815,10 +805,8 @@ ParseField(char *user, char *ary, int modvalue, int offset, int onvalue, const c n2 = -1; } - if (*ptr != ' ' && *ptr != '\t' && *ptr != '\n') { - printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s\n", user, base); + if (*ptr != ' ' && *ptr != '\t' && *ptr != '\n') return(NULL); - } while (*ptr == ' ' || *ptr == '\t' || *ptr == '\n') ++ptr; From e54dc45e45328d6612ed6985652d029e2b37740a Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Thu, 23 Jul 2020 21:30:44 +0100 Subject: [PATCH 05/28] Present a function for deallocating entries No functional change here, just moving to its own function. --- database.c | 68 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/database.c b/database.c index 97d829f..3f568da 100644 --- a/database.c +++ b/database.c @@ -28,6 +28,7 @@ Prototype int CheckJobs(void); void SynchronizeFile(const char *dpath, const char *fname, const char *uname, int parseUser); void DeleteFile(CronFile **pfile); +void DeleteLineContent(CronLine *line); char *ParseInterval(int *interval, char *ptr); char *ParseField(char *ary, int modvalue, int offset, int onvalue, const char **names, char *ptr); void FixDayDow(CronLine *line); @@ -900,8 +901,6 @@ 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; @@ -912,34 +911,7 @@ DeleteFile(CronFile **pfile) pline = &line->cl_Next; } else { *pline = line->cl_Next; - free(line->cl_Shell); - free(line->cl_UserName); - - 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); - } - + DeleteLineContent(line); free(line); } } @@ -952,6 +924,42 @@ DeleteFile(CronFile **pfile) } } +/* + * Free resources associated with the given line, which may be incomplete. + */ +void DeleteLineContent(CronLine *line) +{ + CronWaiter **pwaiters, *waiters; + CronNotifier **pnotifs, *notifs; + + free(line->cl_Shell); + free(line->cl_UserName); + + 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); + } +} /* * TestJobs() From 0986e7e4666536813a9d151bf0e4a066638e5a5a Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Thu, 23 Jul 2020 21:49:14 +0100 Subject: [PATCH 06/28] Parse in a self-contained function Start to attack the heavy indentation that is making it hard to understand the safety of the error handling. --- database.c | 151 ++++++++++++++++++++++++++++------------------------- 1 file changed, 81 insertions(+), 70 deletions(-) diff --git a/database.c b/database.c index 3f568da..a035305 100644 --- a/database.c +++ b/database.c @@ -308,6 +308,85 @@ ReadTimestamps(const char *user) } } +/* + * parse @hourly, etc + * + * Return: NULL on parse error + */ +static char* +ParseTimeInterval(CronLine *line, char *ptr) +{ + 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')) + return NULL; + + 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; + + return ptr; +} + void SynchronizeFile(const char *dpath, const char *fileName, const char *userName, int parseUser) { @@ -386,79 +465,11 @@ SynchronizeFile(const char *dpath, const char *fileName, const char *userName, i printlogf(LOG_DEBUG, "%s as %s: %s\n", path, 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')) { + ptr = ParseTimeInterval(&line, ptr); + if (!ptr) { printlogf(LOG_WARNING, "%s: Failed parsing '@' interval: %s\n", path, 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 From 48882349806276ff56f912bd13de677a05a0b3ec Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Thu, 23 Jul 2020 21:54:34 +0100 Subject: [PATCH 07/28] Move to its own function No functional change; just refactor. Error conditions still handled the same. --- database.c | 48 ++++++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/database.c b/database.c index a035305..1529377 100644 --- a/database.c +++ b/database.c @@ -387,6 +387,30 @@ ParseTimeInterval(CronLine *line, char *ptr) return ptr; } +/* + * parse time and date fields + */ +static char* +ParseTimeSpec(CronLine *line, char *ptr) +{ + ptr = ParseField(line->cl_Mins, FIELD_MINUTES, 0, 1, NULL, ptr); + ptr = ParseField(line->cl_Hrs, FIELD_HOURS, 0, 1, NULL, ptr); + ptr = ParseField(line->cl_Days, FIELD_M_DAYS, 0, 1, NULL, ptr); + ptr = ParseField(line->cl_Mons, FIELD_MONTHS, -1, 1, MonAry, ptr); + ptr = ParseField(line->cl_Dow, FIELD_W_DAYS, 0, ALL_DOW, DowAry, ptr); + + if (!ptr) + return NULL; + + /* + * fix days and dow - if one is not * and the other + * is *, the other is set to 0, and vise-versa + */ + FixDayDow(line); + + return ptr; +} + void SynchronizeFile(const char *dpath, const char *fileName, const char *userName, int parseUser) { @@ -471,31 +495,11 @@ SynchronizeFile(const char *dpath, const char *fileName, const char *userName, i continue; } } else { - /* - * parse date ranges - */ - - ptr = ParseField(line.cl_Mins, FIELD_MINUTES, 0, 1, NULL, ptr); - ptr = ParseField(line.cl_Hrs, FIELD_HOURS, 0, 1, NULL, ptr); - ptr = ParseField(line.cl_Days, FIELD_M_DAYS, 0, 1, NULL, ptr); - ptr = ParseField(line.cl_Mons, FIELD_MONTHS, -1, 1, MonAry, ptr); - ptr = ParseField(line.cl_Dow, FIELD_W_DAYS, 0, ALL_DOW, DowAry, ptr); - - /* - * check failure - */ - - if (ptr == NULL) { + ptr = ParseTimeSpec(&line, ptr); + if (!ptr) { printlogf(LOG_WARNING, "%s: Failed to parse date/time specification: %s\n", path, buf); 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=... */ From 55ff8b78eb7fd7f6d3b567ef4b31db9d442ee354 Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Thu, 23 Jul 2020 22:16:56 +0100 Subject: [PATCH 08/28] Parse KEY=VALUE attributes in their own function A slight change in function when to stop parsing. The previous code would stop if all attributes were populated; we don't need this. Just always consume input until something is encountered that does not look like an attribute. --- database.c | 242 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 135 insertions(+), 107 deletions(-) diff --git a/database.c b/database.c index 1529377..76bd02a 100644 --- a/database.c +++ b/database.c @@ -411,6 +411,140 @@ ParseTimeSpec(CronLine *line, char *ptr) return ptr; } +static char* +ParseOneAttribute(CronFile *file, CronLine *line, char *ptr, + const char *path, const char *buf) +{ + if (strncmp(ptr, ID_TAG, strlen(ID_TAG)) == 0) { + if (line->cl_JobName) { + /* only assign ID_TAG once */ + printlogf(LOG_WARNING, "%s: Multiple use of " ID_TAG " is invalid: %s\n", path, buf); + 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, "%s: Entry ends unexpectedly after " ID_TAG "%s: %s\n", path, line->cl_JobName, buf); + } + } else if (strncmp(ptr, FREQ_TAG, strlen(FREQ_TAG)) == 0) { + if (line->cl_Freq) { + /* only assign FREQ_TAG once */ + printlogf(LOG_WARNING, "%s: Multiple use of " FREQ_TAG " is invalid: %s\n", path, buf); + 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, "%s: Entry ends unexpectedly after " FREQ_TAG ": %s", path, buf); + } + } else if (strncmp(ptr, WAIT_TAG, strlen(WAIT_TAG)) == 0) { + if (line->cl_Waiters) { + /* only assign WAIT_TAG once */ + printlogf(LOG_WARNING, "%s: Multiple use of " WAIT_TAG " is invalid: %s\n", path, buf); + 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, "%s: Entry ends unexpectedly after " WAIT_TAG "%s: %s", path, name, buf); + 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, "%s: Could not parse interval for " WAIT_TAG "%s: %s\n", path, name, buf); + 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, "%s: Ignoring unknown job: " WAIT_TAG "%s: %s\n", path, name, buf); + /* we can continue parsing this line, we just don't install any CronWaiter for the requested job */ + } + } + } + } while (ptr && more); + } + } + + return ptr; +} + +/* + * Parse as many things as we can that look like attributes + * + * Parsing stops when we encounter something that isn't identifiable + * and its assumed to be the next field. + * + * Return NULL on error + */ +static char* +ParseAttributes(CronFile *file, CronLine *line, char *ptr, + const char *path, const char *buf) +{ + for (;;) { + char *prev = ptr; + + ptr = ParseOneAttribute(file, line, ptr, path, buf); + if (!ptr) + return NULL; /* error */ + if (ptr == prev) + return ptr; /* nothing consumed; not understood */ + + while (*ptr == ' ' || *ptr == '\t') + ++ptr; + } +} + void SynchronizeFile(const char *dpath, const char *fileName, const char *userName, int parseUser) { @@ -503,113 +637,7 @@ SynchronizeFile(const char *dpath, const char *fileName, const char *userName, i } /* 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, "%s: Multiple use of " ID_TAG " is invalid: %s\n", path, buf); - 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, "%s: Entry ends unexpectedly after " ID_TAG "%s: %s\n", path, line.cl_JobName, buf); - } - } else if (strncmp(ptr, FREQ_TAG, strlen(FREQ_TAG)) == 0) { - if (line.cl_Freq) { - /* only assign FREQ_TAG once */ - printlogf(LOG_WARNING, "%s: Multiple use of " FREQ_TAG " is invalid: %s\n", path, buf); - 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, "%s: Entry ends unexpectedly after " FREQ_TAG ": %s", path, buf); - } - } else if (strncmp(ptr, WAIT_TAG, strlen(WAIT_TAG)) == 0) { - if (line.cl_Waiters) { - /* only assign WAIT_TAG once */ - printlogf(LOG_WARNING, "%s: Multiple use of " WAIT_TAG " is invalid: %s\n", path, buf); - 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, "%s: Entry ends unexpectedly after " WAIT_TAG "%s: %s", path, name, buf); - 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, "%s: Could not parse interval for " WAIT_TAG "%s: %s\n", path, name, buf); - 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, "%s: Ignoring unknown job: " WAIT_TAG "%s: %s\n", path, name, buf); - /* 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); + ptr = ParseAttributes(file, &line, ptr, path, buf); if (line.cl_JobName && (!ptr || *line.cl_JobName == 0)) { /* we're aborting, or ID= was empty */ From d9a34f8bf3f37e4f8a46abcde70b0b33dcb10d32 Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Thu, 23 Jul 2020 22:34:36 +0100 Subject: [PATCH 09/28] A self-contained function for parsing a single line --- database.c | 205 ++++++++++++++++++++++++++++------------------------- 1 file changed, 110 insertions(+), 95 deletions(-) diff --git a/database.c b/database.c index 76bd02a..1def445 100644 --- a/database.c +++ b/database.c @@ -545,6 +545,115 @@ ParseAttributes(CronFile *file, CronLine *line, char *ptr, } } +/* + * Parse a single line in the file + * + * Return: 1 if "line" was fully populated, otherwise 0 + */ +int +ParseLine(CronFile *file, const char *userName, int parseUser, CronLine *line, char *buf, time_t tnow, const char *path) +{ + 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 == '#') + return 0; + + memset(line, 0, sizeof(*line)); + + if (DebugOpt) + printlogf(LOG_DEBUG, "%s as %s: %s\n", path, userName, buf); + + if (*ptr == '@') { + ptr = ParseTimeInterval(line, ptr); + if (!ptr) { + printlogf(LOG_WARNING, "%s: Failed parsing '@' interval: %s\n", path, buf); + return 0; + } + } else { + ptr = ParseTimeSpec(line, ptr); + if (!ptr) { + printlogf(LOG_WARNING, "%s: Failed to parse date/time specification: %s\n", path, buf); + return 0; + } + } + + /* check for ID=... and AFTER=... and FREQ=... */ + ptr = ParseAttributes(file, line, ptr, path, buf); + + 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, "%s: Writing timestamp requries job to be named: %s\n", path, buf); + 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); + } + } + return 0; + } + /* now we've added any ID=... or AFTER=... */ + + /* + * system crontabs (cron.d) have an extra field for username + */ + if (parseUser) + line->cl_UserName = strdup(strsep(&ptr, " \t")); + else + line->cl_UserName = strdup(userName); + + if (!ptr) { + printlogf(LOG_WARNING, "%s: Could not parse system crontab; username expected per-job: %s\n", path, buf); + abort(); + } + + /* + * 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\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\n", line->cl_Shell); + } + + return 1; +} + void SynchronizeFile(const char *dpath, const char *fileName, const char *userName, int parseUser) { @@ -601,106 +710,12 @@ SynchronizeFile(const char *dpath, const char *fileName, const char *userName, i /* 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, "%s as %s: %s\n", path, userName, buf); - - if (*ptr == '@') { - ptr = ParseTimeInterval(&line, ptr); - if (!ptr) { - printlogf(LOG_WARNING, "%s: Failed parsing '@' interval: %s\n", path, buf); - continue; - } - } else { - ptr = ParseTimeSpec(&line, ptr); - if (!ptr) { - printlogf(LOG_WARNING, "%s: Failed to parse date/time specification: %s\n", path, buf); - continue; - } - } - - /* check for ID=... and AFTER=... and FREQ=... */ - ptr = ParseAttributes(file, &line, ptr, path, buf); - - 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, "%s: Writing timestamp requries job to be named: %s\n", path, buf); - 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); - } - } + if (!ParseLine(file, userName, parseUser, &line, buf, tnow, path)) continue; - } - /* now we've added any ID=... or AFTER=... */ - - /* - * system crontabs (cron.d) have an extra field for username - */ - if (parseUser) - line.cl_UserName = strdup(strsep(&ptr, " \t")); - else - line.cl_UserName = strdup(userName); - - if (!ptr) { - printlogf(LOG_WARNING, "%s: Could not parse system crontab; username expected per-job: %s\n", path, buf); - abort(); - } - - /* - * 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\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\n", line.cl_Shell); - } *pline = calloc(1, sizeof(CronLine)); /* copy working CronLine to newly allocated one */ From 34ec36c5d713e06af45b07e85898bc9f3381db08 Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Thu, 23 Jul 2020 22:50:47 +0100 Subject: [PATCH 10/28] Correctly recover from errors Channel all failures through the same function to deallocate line content. This ensures missing commands to not leak waiters etc. --- database.c | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/database.c b/database.c index 1def445..0baaf57 100644 --- a/database.c +++ b/database.c @@ -587,32 +587,22 @@ ParseLine(CronFile *file, const char *userName, int parseUser, CronLine *line, c /* check for ID=... and AFTER=... and FREQ=... */ ptr = ParseAttributes(file, line, ptr, path, buf); + if (!ptr) + return 0; /* error already logged */ - if (line->cl_JobName && (!ptr || *line->cl_JobName == 0)) { - /* we're aborting, or ID= was empty */ + /* + * Filter out a job name which is the empty string + */ + if (line->cl_JobName && line->cl_JobName[0] == '\0') { free(line->cl_Description); line->cl_Description = NULL; line->cl_JobName = NULL; } - if (ptr && line->cl_Delay > 0 && !line->cl_JobName) { + + if (line->cl_Delay > 0 && !line->cl_JobName) { printlogf(LOG_WARNING, "%s: Writing timestamp requries job to be named: %s\n", path, buf); - 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); - } - } return 0; } - /* now we've added any ID=... or AFTER=... */ /* * system crontabs (cron.d) have an extra field for username @@ -624,7 +614,7 @@ ParseLine(CronFile *file, const char *userName, int parseUser, CronLine *line, c if (!ptr) { printlogf(LOG_WARNING, "%s: Could not parse system crontab; username expected per-job: %s\n", path, buf); - abort(); + return 0; } /* @@ -714,8 +704,10 @@ SynchronizeFile(const char *dpath, const char *fileName, const char *userName, i if (--maxEntries == 0) break; - if (!ParseLine(file, userName, parseUser, &line, buf, tnow, path)) + if (!ParseLine(file, userName, parseUser, &line, buf, tnow, path)) { + DeleteLineContent(&line); continue; + } *pline = calloc(1, sizeof(CronLine)); /* copy working CronLine to newly allocated one */ From f9480a629e27f23380becf700c7da64f2e6d4cf6 Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Thu, 23 Jul 2020 23:08:35 +0100 Subject: [PATCH 11/28] Clarify this message --- database.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database.c b/database.c index 0baaf57..5299560 100644 --- a/database.c +++ b/database.c @@ -613,7 +613,7 @@ ParseLine(CronFile *file, const char *userName, int parseUser, CronLine *line, c line->cl_UserName = strdup(userName); if (!ptr) { - printlogf(LOG_WARNING, "%s: Could not parse system crontab; username expected per-job: %s\n", path, buf); + printlogf(LOG_WARNING, "%s: Could not parse; a username and command is expected in a system crontab: %s\n", path, buf); return 0; } From 035421d3ede24f4504fa30e88cb412e2e72d0dba Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Thu, 23 Jul 2020 23:14:24 +0100 Subject: [PATCH 12/28] Remove unused variable --- database.c | 1 - 1 file changed, 1 deletion(-) diff --git a/database.c b/database.c index 5299560..ec26b99 100644 --- a/database.c +++ b/database.c @@ -442,7 +442,6 @@ ParseOneAttribute(CronFile *file, CronLine *line, char *ptr, printlogf(LOG_WARNING, "%s: Multiple use of " FREQ_TAG " is invalid: %s\n", path, buf); ptr = NULL; } else { - char *base = ptr; ptr += strlen(FREQ_TAG); ptr = ParseInterval(&line->cl_Freq, ptr); if (ptr && *ptr == '/') From b3cc628f2c3d1bb1e792c9e9629713a52da9467f Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Fri, 24 Jul 2020 10:25:10 +0100 Subject: [PATCH 13/28] Explain the need to call the destructor --- database.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/database.c b/database.c index ec26b99..aeee02e 100644 --- a/database.c +++ b/database.c @@ -547,6 +547,10 @@ ParseAttributes(CronFile *file, CronLine *line, char *ptr, /* * Parse a single line in the file * + * On completion the caller is always responsible for deallocating the + * content of "line", whether this function fully populated the line + * or not. + * * Return: 1 if "line" was fully populated, otherwise 0 */ int From 2951c2717e840d2ef56752df7d374c00e8f6beb4 Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Fri, 24 Jul 2020 10:55:37 +0100 Subject: [PATCH 14/28] In preparation for two "crontab" man pages We have "crontab" (the command) and "crontab" (the format description). --- Makefile | 4 ++-- crond.markdown => crond.8.markdown | 0 crontab.markdown => crontab.1.markdown | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename crond.markdown => crond.8.markdown (100%) rename crontab.markdown => crontab.1.markdown (100%) diff --git a/Makefile b/Makefile index c7cc359..abfe4c8 100644 --- a/Makefile +++ b/Makefile @@ -78,8 +78,8 @@ clean: force force: ; man: force - -pandoc -t man -f markdown -s crontab.markdown -o crontab.1 - -pandoc -t man -f markdown -s crond.markdown -o crond.8 + -pandoc -t man -f markdown -s crontab.1.markdown -o crontab.1 + -pandoc -t man -f markdown -s crond.8.markdown -o crond.8 # for maintainer's use only TARNAME = /home/abs/_dcron/dcron-$(VERSION).tar.gz diff --git a/crond.markdown b/crond.8.markdown similarity index 100% rename from crond.markdown rename to crond.8.markdown diff --git a/crontab.markdown b/crontab.1.markdown similarity index 100% rename from crontab.markdown rename to crontab.1.markdown From 6fdb4608b0a98a39e9cbd780962c557d70674bad Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Fri, 24 Jul 2020 11:02:52 +0100 Subject: [PATCH 15/28] Explain the file format in a separate file It needs further description of "system" crontabs which falls out of the scope of crontab.1. In general, it will benefit from being in a separate file where both pages can be broadened. --- Makefile | 2 + crond.8.markdown | 1 + crontab.1.markdown | 133 +--------------------------------------- crontab.5.markdown | 150 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 154 insertions(+), 132 deletions(-) create mode 100644 crontab.5.markdown diff --git a/Makefile b/Makefile index abfe4c8..e6f9d8f 100644 --- a/Makefile +++ b/Makefile @@ -66,6 +66,7 @@ install: $(INSTALL_PROGRAM) -m0700 -g root crond $(DESTDIR)$(SBINDIR)/crond $(INSTALL_PROGRAM) -m4750 -g $(CRONTAB_GROUP) crontab $(DESTDIR)$(BINDIR)/crontab $(INSTALL_DATA) crontab.1 $(DESTDIR)$(MANDIR)/man1/crontab.1 + $(INSTALL_DATA) crontab.5 $(DESTDIR)$(MANDIR)/man5/crontab.5 $(INSTALL_DATA) crond.8 $(DESTDIR)$(MANDIR)/man8/crond.8 $(INSTALL_DIR) $(DESTDIR)$(SCRONTABS) $(INSTALL_DIR) $(DESTDIR)$(CRONTABS) @@ -79,6 +80,7 @@ force: ; man: force -pandoc -t man -f markdown -s crontab.1.markdown -o crontab.1 + -pandoc -t man -f markdown -s crontab.5.markdown -o crontab.5 -pandoc -t man -f markdown -s crond.8.markdown -o crond.8 # for maintainer's use only diff --git a/crond.8.markdown b/crond.8.markdown index e52727b..225da3f 100644 --- a/crond.8.markdown +++ b/crond.8.markdown @@ -178,6 +178,7 @@ out of descriptors. SEE ALSO ======== **crontab**(1) +**crontab**(5) AUTHORS ======= diff --git a/crontab.1.markdown b/crontab.1.markdown index 3d56338..ed6ca86 100644 --- a/crontab.1.markdown +++ b/crontab.1.markdown @@ -45,138 +45,6 @@ the -u or -c switches to specify a different user and/or crontab directory. The superuser also has his or her own per-user crontab, saved as /var/spool/cron/crontabs/root. - -Unlike other cron daemons, this crond/crontab package doesn't try to do -everything under the sun. It doesn't try to keep track of user's preferred -shells; that would require special-casing users with no login shell. Instead, -it just runs all commands using `/bin/sh`. (Commands can of course be script -files written in any shell you like.) - -Nor does it do any special environment handling. A shell script is -better-suited to doing that than a cron daemon. This cron daemon sets up only -four environment variables: USER, LOGNAME, HOME, and SHELL. - - -Our crontab format is roughly similar to that used by vixiecron. Individual -fields may contain a time, a time range, a time range with a skip factor, a -symbolic range for the day of week and month in year, and additional subranges -delimited with commas. Blank lines in the crontab or lines that begin with a -hash (#) are ignored. If you specify both a day in the month and a day of week, -it will be interpreted as the Nth such day in the month. - -Some examples: - - # MIN HOUR DAY MONTH DAYOFWEEK COMMAND - # run `date` at 6:10 am every day - 10 6 * * * date - - # run every two hours at the top of the hour - 0 */2 * * * date - - # run every two hours between 11 pm and 7 am, and again at 8 am - 0 23-7/2,8 * * * date - - # run at 4:00 am on January 1st - 0 4 1 jan * date - - # run every day at 11 am, appending all output to a file - 0 11 * * * date >> /var/log/date-output 2>&1 - -To request the last Monday, etc. in a month, ask for the "5th" one. This will always match the last Monday, etc., even if there are only four Mondays in the month: - - # run at 11 am on the first and last Mon, Tue, Wed of each month - 0 11 1,5 * mon-wed date - -When the fourth Monday in a month is the last, it will match against both the "4th" and the "5th" (it will only run once if both are specified). - -The following formats are also recognized: - - # schedule this job only once, when crond starts up - @reboot date - - # schedule this job whenever crond is running, and sees that at least one - # hour has elapsed since it last ran - @hourly ID=job1 date - -The formats @hourly, @daily, @weekly, @monthly, and @yearly need to update -timestamp files when their jobs have been run. The timestamp files are saved as -/var/spool/cron/cronstamps/user.jobname. So for all of these formats, the cron -command needs a jobname, given by prefixing the command with `ID=jobname`. -(This syntax was chosen to maximize the chance that our crontab files will be -readable by other cron daemons as well. They might just interpret the -ID=jobname as a command-line environment variable assignment.) - -There's also this esoteric option, whose usefulness will be explained later: - - # don't ever schedule this job on its own; only run it when it's triggered - # as a "dependency" of another job (see below), or when the user explicitly - # requests it through the "cron.update" file (see crond(8)) - @noauto ID=namedjob date - -There's also a format available for finer-grained control of frequencies: - - # run whenever it's between 2-4 am, and at least one day (1d) - # has elapsed since this job ran - * 2-4 * * * ID=job2 FREQ=1d date - - # as before, but re-try every 10 minutes (10m) if my_command - # exits with code 11 (EAGAIN) - * 2-4 * * * ID=job3 FREQ=1d/10m my_command - -These formats also update timestamp files, and so also require their jobs to be assigned -IDs. - -Notice the technique used in the second example: jobs can exit with code 11 to -indicate they lacked the resources to run (for example, no network was -available), and so should be tried again after a brief delay. This works for -jobs using either @freq or FREQ=... formats; but the FREQ=.../10m syntax is the -only way to customize the length of the delay before re-trying. - -Jobs can be made to "depend" on, or wait until AFTER other jobs have -successfully completed. Consider the following crontab: - - * * * * * ID=job4 FREQ=1d first_command - * * * * * ID=job5 FREQ=1h AFTER=job4/30m second_command - -Here, whenever job5 is up to be run, if job4 is scheduled to run within the -next 30 minutes (30m), job5 will first wait for it to successfully complete. - -(What if job4 doesn't successfully complete? If job4 returns with exit code -EAGAIN, job5 will continue to wait until job4 is retried---even if that won't -be within the hour. If job4 returns with any other non-zero exit code, job5 -will be removed from the queue without running.) - -Jobs can be told to wait for multiple other jobs, as follows: - - 10 * * * * ID=job6 AFTER=job4/1h,job7 third_command - -The waiting job6 doesn't care what order job4 and job7 complete in. If job6 comes -up to be re-scheduled (an hour later) while an earlier instance is still waiting, only a -single instance of job6 will remain in the queue. It will have all of its -"waiting flags" reset: so each of job7 and job4 (supposing again that job4 would run within the -next 1h) will again have to complete before job6 will run. - -If a job waits on a @reboot or @noauto job, the target job being waited on will -also be scheduled to run. This technique can be used to have a common job scheduled as @noauto -that several other jobs depend on (and so call as a subroutine). - -The command portion of a cron job is run with `/bin/sh -c ...` and may -therefore contain any valid Bourne shell command. A common practice is to -prefix your command with **exec** to keep the process table uncluttered. It is -also common to redirect job output to a file or to /dev/null. If you do not, -and the command generates output on stdout or stderr, that output will be -mailed to the local user whose crontab the job comes from. If you have crontabs -for special users, such as uucp, who can't receive local mail, you may want to -create mail aliases for them or adjust this behavior. (See crond(8) for details -how to adjust it.) - -Whenever jobs return an exit code that's neither 0 nor 11 (EAGAIN), that event -will be logged, regardless of whether any stdout or stderr is generated. The job's -timestamp will also be updated, and it won't be run again until it would next -be normally scheduled. Any jobs waiting on the failed job will be canceled; they -won't be run until they're next scheduled. - - TODO ==== Ought to be able to have several crontab files for any given user, as @@ -185,6 +53,7 @@ an organizational tool. SEE ALSO ======== +**crontab**(5) **crond**(8) AUTHORS diff --git a/crontab.5.markdown b/crontab.5.markdown new file mode 100644 index 0000000..8b5b5cb --- /dev/null +++ b/crontab.5.markdown @@ -0,0 +1,150 @@ +% CRONTAB(5) +% +% 20 Nov 2019 + +NAME +==== +crontab - table of cron jobs + +DESCRIPTION +=========== + +Unlike other cron daemons, this crond/crontab package doesn't try to do +everything under the sun. It doesn't try to keep track of user's preferred +shells; that would require special-casing users with no login shell. Instead, +it just runs all commands using `/bin/sh`. (Commands can of course be script +files written in any shell you like.) + +Nor does it do any special environment handling. A shell script is +better-suited to doing that than a cron daemon. This cron daemon sets up only +four environment variables: USER, LOGNAME, HOME, and SHELL. + + +Our crontab format is roughly similar to that used by vixiecron. Individual +fields may contain a time, a time range, a time range with a skip factor, a +symbolic range for the day of week and month in year, and additional subranges +delimited with commas. Blank lines in the crontab or lines that begin with a +hash (#) are ignored. If you specify both a day in the month and a day of week, +it will be interpreted as the Nth such day in the month. + +Some examples: + + # MIN HOUR DAY MONTH DAYOFWEEK COMMAND + # run `date` at 6:10 am every day + 10 6 * * * date + + # run every two hours at the top of the hour + 0 */2 * * * date + + # run every two hours between 11 pm and 7 am, and again at 8 am + 0 23-7/2,8 * * * date + + # run at 4:00 am on January 1st + 0 4 1 jan * date + + # run every day at 11 am, appending all output to a file + 0 11 * * * date >> /var/log/date-output 2>&1 + +To request the last Monday, etc. in a month, ask for the "5th" one. This will always match the last Monday, etc., even if there are only four Mondays in the month: + + # run at 11 am on the first and last Mon, Tue, Wed of each month + 0 11 1,5 * mon-wed date + +When the fourth Monday in a month is the last, it will match against both the "4th" and the "5th" (it will only run once if both are specified). + +The following formats are also recognized: + + # schedule this job only once, when crond starts up + @reboot date + + # schedule this job whenever crond is running, and sees that at least one + # hour has elapsed since it last ran + @hourly ID=job1 date + +The formats @hourly, @daily, @weekly, @monthly, and @yearly need to update +timestamp files when their jobs have been run. The timestamp files are saved as +/var/spool/cron/cronstamps/user.jobname. So for all of these formats, the cron +command needs a jobname, given by prefixing the command with `ID=jobname`. +(This syntax was chosen to maximize the chance that our crontab files will be +readable by other cron daemons as well. They might just interpret the +ID=jobname as a command-line environment variable assignment.) + +There's also this esoteric option, whose usefulness will be explained later: + + # don't ever schedule this job on its own; only run it when it's triggered + # as a "dependency" of another job (see below), or when the user explicitly + # requests it through the "cron.update" file (see crond(8)) + @noauto ID=namedjob date + +There's also a format available for finer-grained control of frequencies: + + # run whenever it's between 2-4 am, and at least one day (1d) + # has elapsed since this job ran + * 2-4 * * * ID=job2 FREQ=1d date + + # as before, but re-try every 10 minutes (10m) if my_command + # exits with code 11 (EAGAIN) + * 2-4 * * * ID=job3 FREQ=1d/10m my_command + +These formats also update timestamp files, and so also require their jobs to be assigned +IDs. + +Notice the technique used in the second example: jobs can exit with code 11 to +indicate they lacked the resources to run (for example, no network was +available), and so should be tried again after a brief delay. This works for +jobs using either @freq or FREQ=... formats; but the FREQ=.../10m syntax is the +only way to customize the length of the delay before re-trying. + +Jobs can be made to "depend" on, or wait until AFTER other jobs have +successfully completed. Consider the following crontab: + + * * * * * ID=job4 FREQ=1d first_command + * * * * * ID=job5 FREQ=1h AFTER=job4/30m second_command + +Here, whenever job5 is up to be run, if job4 is scheduled to run within the +next 30 minutes (30m), job5 will first wait for it to successfully complete. + +(What if job4 doesn't successfully complete? If job4 returns with exit code +EAGAIN, job5 will continue to wait until job4 is retried---even if that won't +be within the hour. If job4 returns with any other non-zero exit code, job5 +will be removed from the queue without running.) + +Jobs can be told to wait for multiple other jobs, as follows: + + 10 * * * * ID=job6 AFTER=job4/1h,job7 third_command + +The waiting job6 doesn't care what order job4 and job7 complete in. If job6 comes +up to be re-scheduled (an hour later) while an earlier instance is still waiting, only a +single instance of job6 will remain in the queue. It will have all of its +"waiting flags" reset: so each of job7 and job4 (supposing again that job4 would run within the +next 1h) will again have to complete before job6 will run. + +If a job waits on a @reboot or @noauto job, the target job being waited on will +also be scheduled to run. This technique can be used to have a common job scheduled as @noauto +that several other jobs depend on (and so call as a subroutine). + +The command portion of a cron job is run with `/bin/sh -c ...` and may +therefore contain any valid Bourne shell command. A common practice is to +prefix your command with **exec** to keep the process table uncluttered. It is +also common to redirect job output to a file or to /dev/null. If you do not, +and the command generates output on stdout or stderr, that output will be +mailed to the local user whose crontab the job comes from. If you have crontabs +for special users, such as uucp, who can't receive local mail, you may want to +create mail aliases for them or adjust this behavior. (See crond(8) for details +how to adjust it.) + +Whenever jobs return an exit code that's neither 0 nor 11 (EAGAIN), that event +will be logged, regardless of whether any stdout or stderr is generated. The job's +timestamp will also be updated, and it won't be run again until it would next +be normally scheduled. Any jobs waiting on the failed job will be canceled; they +won't be run until they're next scheduled. + +SEE ALSO +======== +**crontab**(1) +**crond**(8) + +AUTHORS +======= +Matthew Dillon (dillon@apollo.backplane.com): original developer +James Pryor (dubiousjim@gmail.com): current developer From 141cc7f5dc8065de28acc87d9c6fc8b5d0726267 Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Fri, 24 Jul 2020 11:10:27 +0100 Subject: [PATCH 16/28] First explanation of "system" crontab --- crontab.5.markdown | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crontab.5.markdown b/crontab.5.markdown index 8b5b5cb..43b9236 100644 --- a/crontab.5.markdown +++ b/crontab.5.markdown @@ -23,9 +23,11 @@ four environment variables: USER, LOGNAME, HOME, and SHELL. Our crontab format is roughly similar to that used by vixiecron. Individual fields may contain a time, a time range, a time range with a skip factor, a symbolic range for the day of week and month in year, and additional subranges -delimited with commas. Blank lines in the crontab or lines that begin with a -hash (#) are ignored. If you specify both a day in the month and a day of week, -it will be interpreted as the Nth such day in the month. +delimited with commas. "System" crontabs (typically in /etc/cron.d) have an +additional field which specifies which user the job will run as. Blank lines +in the crontab or lines that begin with a hash (#) are ignored. If you +specify both a day in the month and a day of week, it will be +interpreted as the Nth such day in the month. Some examples: From ae5f6cd9209389da279ea3d58694351942f9e18d Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Fri, 24 Jul 2020 11:13:42 +0100 Subject: [PATCH 17/28] Introduce sub-sections There's quite a lot of information here. Some of it, such as dependencies, is quite obscure and benefits from being separated out. --- crontab.5.markdown | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crontab.5.markdown b/crontab.5.markdown index 43b9236..f62aacb 100644 --- a/crontab.5.markdown +++ b/crontab.5.markdown @@ -97,6 +97,9 @@ available), and so should be tried again after a brief delay. This works for jobs using either @freq or FREQ=... formats; but the FREQ=.../10m syntax is the only way to customize the length of the delay before re-trying. +Dependencies +------------ + Jobs can be made to "depend" on, or wait until AFTER other jobs have successfully completed. Consider the following crontab: @@ -125,6 +128,9 @@ If a job waits on a @reboot or @noauto job, the target job being waited on will also be scheduled to run. This technique can be used to have a common job scheduled as @noauto that several other jobs depend on (and so call as a subroutine). +Command execution +----------------- + The command portion of a cron job is run with `/bin/sh -c ...` and may therefore contain any valid Bourne shell command. A common practice is to prefix your command with **exec** to keep the process table uncluttered. It is From 4788048b4476ba9778db9648255f83b359a04758 Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Fri, 24 Jul 2020 11:15:12 +0100 Subject: [PATCH 18/28] Open with a list of what we do, not what we don't do These can be moved to a separate section. Following man-pages(7) convention I chose "NOTES". --- crontab.5.markdown | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/crontab.5.markdown b/crontab.5.markdown index f62aacb..3c2f34e 100644 --- a/crontab.5.markdown +++ b/crontab.5.markdown @@ -8,18 +8,6 @@ crontab - table of cron jobs DESCRIPTION =========== - -Unlike other cron daemons, this crond/crontab package doesn't try to do -everything under the sun. It doesn't try to keep track of user's preferred -shells; that would require special-casing users with no login shell. Instead, -it just runs all commands using `/bin/sh`. (Commands can of course be script -files written in any shell you like.) - -Nor does it do any special environment handling. A shell script is -better-suited to doing that than a cron daemon. This cron daemon sets up only -four environment variables: USER, LOGNAME, HOME, and SHELL. - - Our crontab format is roughly similar to that used by vixiecron. Individual fields may contain a time, a time range, a time range with a skip factor, a symbolic range for the day of week and month in year, and additional subranges @@ -147,6 +135,18 @@ timestamp will also be updated, and it won't be run again until it would next be normally scheduled. Any jobs waiting on the failed job will be canceled; they won't be run until they're next scheduled. +NOTES +===== +Unlike other cron daemons, this crond/crontab package doesn't try to do +everything under the sun. It doesn't try to keep track of user's preferred +shells; that would require special-casing users with no login shell. Instead, +it just runs all commands using `/bin/sh`. (Commands can of course be script +files written in any shell you like.) + +Nor does it do any special environment handling. A shell script is +better-suited to doing that than a cron daemon. This cron daemon sets up only +four environment variables: USER, LOGNAME, HOME, and SHELL. + SEE ALSO ======== **crontab**(1) From 281db5464a69274965b775151a94afaa26f280a6 Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Fri, 24 Jul 2020 12:03:14 +0100 Subject: [PATCH 19/28] Tackle the crontab man page Mainly a re-ordering of the existing information, with the addition of a summary at the beginning. Man page convention forces the EXAMPLES to the end. Use this as an incentive to provide a better and more concise summary up-front. --- crontab.5.markdown | 152 +++++++++++++++++++++++++++++++++------------ 1 file changed, 111 insertions(+), 41 deletions(-) diff --git a/crontab.5.markdown b/crontab.5.markdown index 3c2f34e..1b02d37 100644 --- a/crontab.5.markdown +++ b/crontab.5.markdown @@ -8,63 +8,89 @@ crontab - table of cron jobs DESCRIPTION =========== -Our crontab format is roughly similar to that used by vixiecron. Individual -fields may contain a time, a time range, a time range with a skip factor, a -symbolic range for the day of week and month in year, and additional subranges -delimited with commas. "System" crontabs (typically in /etc/cron.d) have an -additional field which specifies which user the job will run as. Blank lines -in the crontab or lines that begin with a hash (#) are ignored. If you -specify both a day in the month and a day of week, it will be -interpreted as the Nth such day in the month. -Some examples: +This page describes the crontab format used by **dcron**. Where +possible, the format follows the conventions of other cron +implementations, such as Vixie cron. - # MIN HOUR DAY MONTH DAYOFWEEK COMMAND - # run `date` at 6:10 am every day - 10 6 * * * date +You may wish to skip to the comprehensive section of **EXAMPLES**. - # run every two hours at the top of the hour - 0 */2 * * * date +A crontab consists of one row for each job. Blank lines in the crontab +are ignored, as are lines which begin with a hash (#) and are used for +comments. - # run every two hours between 11 pm and 7 am, and again at 8 am - 0 23-7/2,8 * * * date +A job consists of a time specification, and command to run: - # run at 4:00 am on January 1st - 0 4 1 jan * date + * * * * * date - # run every day at 11 am, appending all output to a file - 0 11 * * * date >> /var/log/date-output 2>&1 +Additional attributes may be specified: -To request the last Monday, etc. in a month, ask for the "5th" one. This will always match the last Monday, etc., even if there are only four Mondays in the month: + * * * * * ID=myjob date - # run at 11 am on the first and last Mon, Tue, Wed of each month - 0 11 1,5 * mon-wed date +Shorthand time specifications can be given, in which case cron will +decide when the job runs: -When the fourth Monday in a month is the last, it will match against both the "4th" and the "5th" (it will only run once if both are specified). + @hourly ID=myjob date -The following formats are also recognized: +"System" crontabs (typically in /etc/cron.d; see the section **System +crontabs**) have an extended format which includes the user to execute +a job as. - # schedule this job only once, when crond starts up - @reboot date + * * * * * matthew date + +Time specification +------------------ + +The time specification fields are, in order: - # schedule this job whenever crond is running, and sees that at least one - # hour has elapsed since it last ran +field values +------ ------- +minute 0-59 +hour 0-23 +day of month 1-31 +month 1-12; jan-dec +day of week 0-7; sun-sun + +For any time field, ranges can be given, and sub-ranges can be +separated with a comma: + + # run every two hours between 11pm and 7am, and again at 8am + 0 23-7/2,8 * * * date + +If you specify both a day in the month and a day of week, it will be +interpreted as the Nth such day in the month. + +The alternative time specifications of @hourly, @daily, @weekly, +@monthly, and @yearly may also be used. In this case cron will keep a +log of when the job was last run, and use this to decide when to next +run the job. This means it is necessary to set the **ID=** attribute +to uniquely identify the job: + + # at least one hour has elapsed since it last ran @hourly ID=job1 date -The formats @hourly, @daily, @weekly, @monthly, and @yearly need to update -timestamp files when their jobs have been run. The timestamp files are saved as -/var/spool/cron/cronstamps/user.jobname. So for all of these formats, the cron -command needs a jobname, given by prefixing the command with `ID=jobname`. -(This syntax was chosen to maximize the chance that our crontab files will be -readable by other cron daemons as well. They might just interpret the -ID=jobname as a command-line environment variable assignment.) +A special time specification @reboot will begin the job on startup, +and no idenfier is required: -There's also this esoteric option, whose usefulness will be explained later: + @reboot date - # don't ever schedule this job on its own; only run it when it's triggered - # as a "dependency" of another job (see below), or when the user explicitly - # requests it through the "cron.update" file (see crond(8)) - @noauto ID=namedjob date +System crontabs +--------------- + +"System" crontabs are files located in /etc/cron.d, and it is normal +for these files to be placed by a package manager. + +The jobs run as a user specified on a per-job basis, following the +convention of other cron implementations: + + # switch to user "matthew" and run the command + * * * * * matthew date + +System crontabs remain are "owned" by root, which will be the email +recipient of output or errors. + +Fine-grained control +-------------------- There's also a format available for finer-grained control of frequencies: @@ -116,6 +142,11 @@ If a job waits on a @reboot or @noauto job, the target job being waited on will also be scheduled to run. This technique can be used to have a common job scheduled as @noauto that several other jobs depend on (and so call as a subroutine). + # don't ever schedule this job on its own; only run it when it's triggered + # as a "dependency" of another job (see below), or when the user explicitly + # requests it through the "cron.update" file (see crond(8)) + @noauto ID=namedjob date + Command execution ----------------- @@ -135,6 +166,45 @@ timestamp will also be updated, and it won't be run again until it would next be normally scheduled. Any jobs waiting on the failed job will be canceled; they won't be run until they're next scheduled. +EXAMPLES +======== + +Examples of regular user's crontab entries: + + # MIN HOUR DAY MONTH DAYOFWEEK COMMAND + + # run `date` at 6:10 am every day + 10 6 * * * date + + # run every two hours at the top of the hour + 0 */2 * * * date + + # run every two hours between 11 pm and 7 am, and again at 8 am + 0 23-7/2,8 * * * date + + # run at 4:00 am on January 1st + 0 4 1 jan * date + + # run every day at 11 am, appending all output to a file + 0 11 * * * date >> /var/log/date-output 2>&1 + + # schedule this job only once, when crond starts up + @reboot date + + # at least one hour has elapsed since it last ran + @hourly ID=job1 date + +To request the last Monday, etc. in a month, ask for the "5th" +one. This will always match the last Monday, etc., even if there are +only four Mondays in the month: + + # run at 11 am on the first and last Mon, Tue, Wed of each month + 0 11 1,5 * mon-wed date + +When the fourth Monday in a month is the last, it will match against +both the "4th" and the "5th" (it will only run once if both are +specified). + NOTES ===== Unlike other cron daemons, this crond/crontab package doesn't try to do From 6a5289108207c057ee1315fe26f69738d1a6ed31 Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Fri, 24 Jul 2020 12:05:42 +0100 Subject: [PATCH 20/28] Text ranges can also be used --- crontab.5.markdown | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crontab.5.markdown b/crontab.5.markdown index 1b02d37..5b66b84 100644 --- a/crontab.5.markdown +++ b/crontab.5.markdown @@ -51,7 +51,8 @@ day of month 1-31 month 1-12; jan-dec day of week 0-7; sun-sun -For any time field, ranges can be given, and sub-ranges can be +For any field, ranges can be given either numerically (eg. "1-3") or +using the text equivalents (eg. "mon-wed"). Sub-ranges can be separated with a comma: # run every two hours between 11pm and 7am, and again at 8am From 18f7f1b3a5d6a70c28506c37103b270ab2bb96e1 Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Fri, 24 Jul 2020 12:07:29 +0100 Subject: [PATCH 21/28] These are mainly side notes --- crontab.1.markdown | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crontab.1.markdown b/crontab.1.markdown index ed6ca86..6a1805e 100644 --- a/crontab.1.markdown +++ b/crontab.1.markdown @@ -29,6 +29,9 @@ Generally the -e option is used to edit your crontab. **crontab** will use the editor specified by your EDITOR or VISUAL environment variable (or /usr/bin/vi) to edit the crontab. +NOTES +===== + **crontab** doesn't provide the kinds of protections that programs like **visudo** do against syntax errors and simultaneous edits. Errors won't be detected until **crond** reads the crontab file. What **crontab** does is provide a mechanism for From e47c3644c4512c53d0a0df6570c478d445878476 Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Fri, 24 Jul 2020 12:08:14 +0100 Subject: [PATCH 22/28] This location is abstracted by the software Users do not need to know about this location; they should use the comman dto edit the crontab. --- crontab.1.markdown | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crontab.1.markdown b/crontab.1.markdown index 6a1805e..c168807 100644 --- a/crontab.1.markdown +++ b/crontab.1.markdown @@ -29,6 +29,9 @@ Generally the -e option is used to edit your crontab. **crontab** will use the editor specified by your EDITOR or VISUAL environment variable (or /usr/bin/vi) to edit the crontab. +Just as a regular user, the superuser also has his or her own per-user +crontab. + NOTES ===== @@ -45,9 +48,6 @@ install crontabs even for users who don't have the privileges to install them themselves. (Even for users who don't have a login shell.) Only the superuser may use the -u or -c switches to specify a different user and/or crontab directory. -The superuser also has his or her own per-user crontab, saved as -/var/spool/cron/crontabs/root. - TODO ==== Ought to be able to have several crontab files for any given user, as From 717764e24d7ef596fe047dd27330e9f86768a8f8 Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Fri, 24 Jul 2020 12:09:03 +0100 Subject: [PATCH 23/28] This is rather wishful and out of date; keep the page more focused --- crontab.1.markdown | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crontab.1.markdown b/crontab.1.markdown index c168807..f06d272 100644 --- a/crontab.1.markdown +++ b/crontab.1.markdown @@ -48,12 +48,6 @@ install crontabs even for users who don't have the privileges to install them themselves. (Even for users who don't have a login shell.) Only the superuser may use the -u or -c switches to specify a different user and/or crontab directory. -TODO -==== -Ought to be able to have several crontab files for any given user, as -an organizational tool. - - SEE ALSO ======== **crontab**(5) From 16590d991ff30e89bf12bc5b31a7591bac381fd5 Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Fri, 24 Jul 2020 12:11:08 +0100 Subject: [PATCH 24/28] Point to the file specification --- crontab.1.markdown | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crontab.1.markdown b/crontab.1.markdown index f06d272..aae811e 100644 --- a/crontab.1.markdown +++ b/crontab.1.markdown @@ -23,7 +23,8 @@ SYNOPSIS DESCRIPTION =========== -**crontab** manipulates the per-user crontabs. +The **crontab** command manipulates the per-user crontabs. If you are +looking for the format of these files, see "man 5 crontab". Generally the -e option is used to edit your crontab. **crontab** will use the editor specified by your EDITOR or VISUAL environment From 9bdc964b11d2d1841907ab2519f943522f1390e7 Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Fri, 24 Jul 2020 12:13:57 +0100 Subject: [PATCH 25/28] Benefit from explicit example of the common case --- crontab.1.markdown | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crontab.1.markdown b/crontab.1.markdown index aae811e..611c55d 100644 --- a/crontab.1.markdown +++ b/crontab.1.markdown @@ -28,7 +28,9 @@ looking for the format of these files, see "man 5 crontab". Generally the -e option is used to edit your crontab. **crontab** will use the editor specified by your EDITOR or VISUAL environment -variable (or /usr/bin/vi) to edit the crontab. +variable (or /usr/bin/vi) to edit the crontab: + + $ crontab -e Just as a regular user, the superuser also has his or her own per-user crontab. From 9da39fae173fffb147de2d7363365e1ec9cd28ae Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Fri, 24 Jul 2020 13:10:13 +0100 Subject: [PATCH 26/28] Claim some contribution --- crontab.5.markdown | 1 + database.c | 1 + 2 files changed, 2 insertions(+) diff --git a/crontab.5.markdown b/crontab.5.markdown index 5b66b84..5a0c354 100644 --- a/crontab.5.markdown +++ b/crontab.5.markdown @@ -227,3 +227,4 @@ AUTHORS ======= Matthew Dillon (dillon@apollo.backplane.com): original developer James Pryor (dubiousjim@gmail.com): current developer +Mark Hills (mark@xwax.org): contributor diff --git a/database.c b/database.c index aeee02e..45794dd 100644 --- a/database.c +++ b/database.c @@ -4,6 +4,7 @@ * * Copyright 1994 Matthew Dillon (dillon@apollo.backplane.com) * Copyright 2009-2019 James Pryor + * Copyright 2020 Mark Hills * May be distributed under the GNU General Public License version 2 or any later version. */ From 5a4b3c53887957a83024ada4a1e499bc91266da7 Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Fri, 24 Jul 2020 13:10:55 +0100 Subject: [PATCH 27/28] Generate the man pages --- crond.8 | 262 ++++++++++++++++++++++----------------------- crontab.1 | 268 +++++++--------------------------------------- crontab.5 | 315 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 483 insertions(+), 362 deletions(-) create mode 100644 crontab.5 diff --git a/crond.8 b/crond.8 index 1582235..cb7eb86 100644 --- a/crond.8 +++ b/crond.8 @@ -1,193 +1,191 @@ -.TH CROND 8 "1 May 2011" +.\" Automatically generated by Pandoc 2.3.1 +.\" +.TH "CROND" "8" "20 Nov 2019" "" "" +.hy .SH NAME .PP -crond - dillon's lightweight cron daemon +crond \- dillon's lightweight cron daemon .SH SYNOPSIS .PP -\f[B]crond [-s dir] [-c dir] [-t dir] [-m user\@host] [-M mailhandler] [-S|-L file] [-l loglevel] [-b|-f|-d]\f[] +\f[B]crond [\-s dir] [\-c dir] [\-t dir] [\-m user\@host] [\-M +mailhandler] [\-S|\-L file] [\-l loglevel] [\-b|\-f|\-d]\f[] .SH OPTIONS .PP -\f[B]crond\f[] is a background daemon that parses individual -crontab files and executes commands on behalf of the users in -question. +\f[B]crond\f[] is a background daemon that parses individual crontab +files and executes commands on behalf of the users in question. .TP -.B -s dir +.B \-s dir directory of system crontabs (defaults to /etc/cron.d) .RS .RE .TP -.B -c dir -directory of per-user crontabs (defaults to -/var/spool/cron/crontabs) +.B \-c dir +directory of per\-user crontabs (defaults to /var/spool/cron/crontabs) .RS .RE .TP -.B -t dir +.B \-t dir directory of timestamps for \@freq and FREQ=\&... jobs (defaults to /var/spool/cron/cronstamps) .RS .RE .TP -.B -m user\@host -where should the output of cronjobs be directed? (defaults to local -user) Some mail handlers (like msmtp) can't route mail to local -users. -If that's what you're using, then you should supply a remote -address using this switch. +.B \-m user\@host +where should the output of cronjobs be directed? +(defaults to local user) Some mail handlers (like msmtp) can't route +mail to local users. +If that's what you're using, then you should supply a remote address +using this switch. Cron output for all users will be directed to that address. -Alternatively, you could supply a different mail handler using the --M switch, to log or otherwise process the messages instead of -mailing them. -Alternatively, you could just direct the stdout and stderr of your -cron jobs to /dev/null. +Alternatively, you could supply a different mail handler using the \-M +switch, to log or otherwise process the messages instead of mailing +them. +Alternatively, you could just direct the stdout and stderr of your cron +jobs to /dev/null. .RS .RE .TP -.B -M mailhandler -Any output that cronjobs print to stdout or stderr gets formatted -as an email and piped to \f[C]/usr/sbin/sendmail\ -t\ -oem\ -i\f[]. +.B \-M mailhandler +Any output that cronjobs print to stdout or stderr gets formatted as an +email and piped to \f[C]/usr/sbin/sendmail\ \-t\ \-oem\ \-i\f[]. Attempts to mail this are also logged. -This switch permits the user to substitute a different mailhandler, -or a script, for sendmail. -That custom mailhandler is called with no arguments, and with the -mail headers and cronjob output supplied to stdin. -When a custom mailhandler is used, mailing is no longer logged -(have your mailhandler do that if you want it). -When cron jobs generate no stdout or stderr, nothing is sent to -either sendmail or a custom mailhandler. +This switch permits the user to substitute a different mailhandler, or a +script, for sendmail. +That custom mailhandler is called with no arguments, and with the mail +headers and cronjob output supplied to stdin. +When a custom mailhandler is used, mailing is no longer logged (have +your mailhandler do that if you want it). +When cron jobs generate no stdout or stderr, nothing is sent to either +sendmail or a custom mailhandler. .RS .RE .TP -.B -S +.B \-S log events to syslog, using syslog facility LOG_CRON and identity `crond' (this is the default behavior). .RS .RE .TP -.B -L file +.B \-L file log to specified file instead of syslog. .RS .RE .TP -.B -l loglevel +.B \-l loglevel log events at the specified, or more important, loglevels. The default is `notice'. -Valid level names are as described in logger(1) and syslog(3): -alert, crit, debug, emerg, err, error (deprecated synonym for err), -info, notice, panic (deprecated synonym for emerg), warning, warn -(deprecated synonym for warning). +Valid level names are as described in logger(1) and syslog(3): alert, +crit, debug, emerg, err, error (deprecated synonym for err), info, +notice, panic (deprecated synonym for emerg), warning, warn (deprecated +synonym for warning). .RS .RE .TP -.B -b -run \f[B]crond\f[] in the background (default unless -d or -f is +.B \-b +run \f[B]crond\f[] in the background (default unless \-d or \-f is specified) .RS .RE .TP -.B -f +.B \-f run \f[B]crond\f[] in the foreground. -All log messages are sent to stderr instead of syslog or a -L file. +All log messages are sent to stderr instead of syslog or a \-L file. .RS .RE .TP -.B -d +.B \-d turn on debugging. -This option sets the logging level to `debug' and causes -\f[B]crond\f[] to run in the foreground. +This option sets the logging level to `debug' and causes \f[B]crond\f[] +to run in the foreground. .RS .RE .SH DESCRIPTION .PP -\f[B]crond\f[] is responsible for scanning the crontab files and -running their commands at the appropriate time. -It always synchronizes to the top of the minute, matching the -current time against its internal list of parsed crontabs. +\f[B]crond\f[] is responsible for scanning the crontab files and running +their commands at the appropriate time. +It always synchronizes to the top of the minute, matching the current +time against its internal list of parsed crontabs. That list is stored so that it can be scanned very quickly, and \f[B]crond\f[] can deal with several hundred crontabs with several thousand entries without using noticeable CPU. .PP -Cron jobs are not re-executed if a previous instance of them is -still running. -For example, if you have a crontab command \f[C]sleep\ 70\f[], that -you request to be run every minute, \f[B]crond\f[] will skip this -job when it sees it is still running. -So the job won't be run more frequently than once every two -minutes. +Cron jobs are not re\-executed if a previous instance of them is still +running. +For example, if you have a crontab command \f[C]sleep\ 70\f[], that you +request to be run every minute, \f[B]crond\f[] will skip this job when +it sees it is still running. +So the job won't be run more frequently than once every two minutes. If you do not like this feature, you can run your commands in the background with an \f[C]&\f[]. .PP -\f[B]crond\f[] automatically detects when the clock has been -changed, during its per-minute scans. -Backwards time-changes of an hour or less won't re-run cron jobs -from the intervening period. -\f[B]crond\f[] will effectively sleep until it catches back up to -the original time. -Forwards time-changes of an hour or less (or if the computer is -suspended and resumed again within an hour) will run any missed -jobs exactly once. -Changes greater than an hour in either direction cause -\f[B]crond\f[] to re-calculate when jobs should be run, and not -attempt to execute any missed commands. -This is effectively the same as if \f[B]crond\f[] had been stopped -and re-started. -.PP -For example, suppose it's 10 am, and a job is scheduled to run -every day at 10:30 am. -If you set the system's clock forward to 11 am, crond will -immediately run the 10:30 job. -If on the other hand you set the system's clock forward to noon, -the 10:30 am job will be skipped until the next day. +\f[B]crond\f[] automatically detects when the clock has been changed, +during its per\-minute scans. +Backwards time\-changes of an hour or less won't re\-run cron jobs from +the intervening period. +\f[B]crond\f[] will effectively sleep until it catches back up to the +original time. +Forwards time\-changes of an hour or less (or if the computer is +suspended and resumed again within an hour) will run any missed jobs +exactly once. +Changes greater than an hour in either direction cause \f[B]crond\f[] to +re\-calculate when jobs should be run, and not attempt to execute any +missed commands. +This is effectively the same as if \f[B]crond\f[] had been stopped and +re\-started. +.PP +For example, suppose it's 10 am, and a job is scheduled to run every day +at 10:30 am. +If you set the system's clock forward to 11 am, crond will immediately +run the 10:30 job. +If on the other hand you set the system's clock forward to noon, the +10:30 am job will be skipped until the next day. Jobs scheduled using \@daily and the like work differently; see crontab(1) for details. .PP -\f[B]crond\f[] has a number of built in limitations to reduce the -chance of it being ill-used. -Potentially infinite loops during parsing are dealt with via a -failsafe counter, and non-root crontabs are limited to 256 crontab -entries. +\f[B]crond\f[] has a number of built in limitations to reduce the chance +of it being ill\-used. +Potentially infinite loops during parsing are dealt with via a failsafe +counter, and non\-root crontabs are limited to 256 crontab entries. Crontab lines may not be longer than 1024 characters, including the newline. .PP -Whenever \f[B]crond\f[] must run a job, it first creates a -daemon-owned temporary file O_EXCL and O_APPEND to store any -output, then fork()s and changes its user and group permissions to -match that of the user the job is being run for, then -\f[B]exec\f[]s \f[B]/bin/sh -c \f[] to run the job. -The temporary file remains under the ownership of the daemon to -prevent the user from tampering with it. -Upon job completion, \f[B]crond\f[] verifies the secureness of the -mail file and, if it has been appended to, mails the file to the -specified address. -The \f[B]sendmail\f[] program (or custom mail handler, if supplied) -is run under the user's uid to prevent mail related security holes. +Whenever \f[B]crond\f[] must run a job, it first creates a daemon\-owned +temporary file O_EXCL and O_APPEND to store any output, then fork()s and +changes its user and group permissions to match that of the user the job +is being run for, then \f[B]exec\f[]s \f[B]/bin/sh \-c \f[] to run the +job. +The temporary file remains under the ownership of the daemon to prevent +the user from tampering with it. +Upon job completion, \f[B]crond\f[] verifies the secureness of the mail +file and, if it has been appended to, mails the file to the specified +address. +The \f[B]sendmail\f[] program (or custom mail handler, if supplied) is +run under the user's uid to prevent mail related security holes. .PP When a user edits their crontab, \f[B]crontab\f[] first copies the -crontab to a user owned file before running the user's preferred -editor. -The suid \f[B]crontab\f[] keeps an open descriptor to the file -which it later uses to copy the file back, thereby ensuring the -user has not tampered with the file type. -.PP -\f[B]crontab\f[] notifies \f[B]crond\f[] that a user's crontab file -has been modified (or created or deleted) through the -\[lq]cron.update\[rq] file, which resides in the per-user crontabs -directory (usually /var/spool/cron/crontabs). -\f[B]crontab\f[] appends the filename of the modified crontab file -to \[lq]cron.update\[rq]; and \f[B]crond\f[] inspects this file to +crontab to a user owned file before running the user's preferred editor. +The suid \f[B]crontab\f[] keeps an open descriptor to the file which it +later uses to copy the file back, thereby ensuring the user has not +tampered with the file type. +.PP +\f[B]crontab\f[] notifies \f[B]crond\f[] that a user's crontab file has +been modified (or created or deleted) through the \[lq]cron.update\[rq] +file, which resides in the per\-user crontabs directory (usually +/var/spool/cron/crontabs). +\f[B]crontab\f[] appends the filename of the modified crontab file to +\[lq]cron.update\[rq]; and \f[B]crond\f[] inspects this file to determine when to reparse or otherwise update its internal list of parsed crontabs. .PP Whenever a \[lq]cron.update\[rq] file is seen, \f[B]crond\f[] also -re-reads timestamp files from its timestamp directory (usually +re\-reads timestamp files from its timestamp directory (usually /var/spool/cron/cronstamps). Normally these will just mirror \f[B]crond\f[]'s own internal -representations, but this mechanism could be used to manually -notify \f[B]crond\f[] that you've externally updated the -timestamps. +representations, but this mechanism could be used to manually notify +\f[B]crond\f[] that you've externally updated the timestamps. .PP -The \[lq]cron.update\[rq] file can also be used to ask -\f[B]crond\f[] to schedule a \[lq]named\[rq] cron job. +The \[lq]cron.update\[rq] file can also be used to ask \f[B]crond\f[] to +schedule a \[lq]named\[rq] cron job. To do this, append a line of the form: .IP .nf @@ -197,40 +195,36 @@ clio\ job1\ !job2 .fi .PP to \[lq]cron.update\[rq]. -This request that user clio's job1 should be scheduled (waiting -first for the successful completion of any jobs named in job1's -AFTER= tag), and job2 should also be scheduled (without waiting for -other jobs). +This request that user clio's job1 should be scheduled (waiting first +for the successful completion of any jobs named in job1's AFTER= tag), +and job2 should also be scheduled (without waiting for other jobs). See crontab(1) for more about tags and named jobs. .PP -The directory of per-user crontabs is re-parsed once every hour in -any case. -Any crontabs in the system directory (usually /etc/cron.d) are -parsed at the same time. +The directory of per\-user crontabs is re\-parsed once every hour in any +case. +Any crontabs in the system directory (usually /etc/cron.d) are parsed at +the same time. This directory can be used by packaging systems. -When you install a package foo, it might write its own foo-specific +When you install a package foo, it might write its own foo\-specific crontab to /etc/cron.d/foo. .PP -The superuser has a per-user crontab along with other users. +The superuser has a per\-user crontab along with other users. It usually resides at /var/spool/cron/crontabs/root. .PP Users can only have a crontab if they have an entry in /etc/passwd; however they do not need to have login shell privileges. -Cron jobs are always run under /bin/sh; see crontab(1) for more -details. +Cron jobs are always run under /bin/sh; see crontab(1) for more details. .PP -Unlike \f[B]crontab\f[], the \f[B]crond\f[] program does not keep -open descriptors to crontab files while running their jobs, as this -could cause \f[B]crond\f[] to run out of descriptors. +Unlike \f[B]crontab\f[], the \f[B]crond\f[] program does not keep open +descriptors to crontab files while running their jobs, as this could +cause \f[B]crond\f[] to run out of descriptors. .SH SEE ALSO .PP -\f[B]crontab\f[](1) +\f[B]crontab\f[](1) \f[B]crontab\f[](5) .SH AUTHORS .PP -Matthew Dillon (dillon\@apollo.backplane.com): original -developer +Matthew Dillon (dillon\@apollo.backplane.com): original developer .PD 0 .P .PD -Jim Pryor (profjim\@jimpryor.net): current -developer +James Pryor (dubiousjim\@gmail.com): current developer diff --git a/crontab.1 b/crontab.1 index 1e2f59f..55a0bb4 100644 --- a/crontab.1 +++ b/crontab.1 @@ -1,254 +1,66 @@ -.TH CRONTAB 1 "1 May 2011" +.\" Automatically generated by Pandoc 2.3.1 +.\" +.TH "CRONTAB" "1" "20 Nov 2019" "" "" +.hy .SH NAME .PP -crontab - manipulate per-user crontabs (dillon's lightweight cron +crontab \- manipulate per\-user crontabs (dillon's lightweight cron daemon) .SH SYNOPSIS .PP -\f[B]crontab file [-u user]\f[] - replace crontab from file +\f[B]crontab file [\-u user]\f[] \- replace crontab from file .PP -\f[B]crontab - [-u user]\f[] - replace crontab from stdin +\f[B]crontab \- [\-u user]\f[] \- replace crontab from stdin .PP -\f[B]crontab -l [-u user]\f[] - list crontab for user +\f[B]crontab \-l [\-u user]\f[] \- list crontab for user .PP -\f[B]crontab -e [-u user]\f[] - edit crontab for user +\f[B]crontab \-e [\-u user]\f[] \- edit crontab for user .PP -\f[B]crontab -d [-u user]\f[] - delete crontab for user +\f[B]crontab \-d [\-u user]\f[] \- delete crontab for user .PP -\f[B]crontab -c dir\f[] - specify crontab directory +\f[B]crontab \-c dir\f[] \- specify crontab directory .SH DESCRIPTION .PP -\f[B]crontab\f[] manipulates the per-user crontabs. +The \f[B]crontab\f[] command manipulates the per\-user crontabs. +If you are looking for the format of these files, see \[lq]man 5 +crontab\[rq]. .PP -Generally the -e option is used to edit your crontab. -\f[B]crontab\f[] will use the editor specified by your EDITOR or -VISUAL environment variable (or /usr/bin/vi) to edit the crontab. -.PP -\f[B]crontab\f[] doesn't provide the kinds of protections that -programs like \f[B]visudo\f[] do against syntax errors and -simultaneous edits. -Errors won't be detected until \f[B]crond\f[] reads the crontab -file. -What \f[B]crontab\f[] does is provide a mechanism for users who may -not themselves have write privileges to the crontab folder to -nonetheless install or edit their crontabs. -It also notifies a running crond daemon of any changes to these -files. -.PP -Only users who belong to the same group as the \f[B]crontab\f[] -binary will be able to install or edit crontabs. -However it'll be possible for the superuser to install crontabs -even for users who don't have the privileges to install them -themselves. -(Even for users who don't have a login shell.) -Only the superuser may use the -u or -c switches to specify a -different user and/or crontab directory. -.PP -The superuser also has his or her own per-user crontab, saved as -/var/spool/cron/crontabs/root. -.PP -Unlike other cron daemons, this crond/crontab package doesn't try -to do everything under the sun. -It doesn't try to keep track of user's preferred shells; that would -require special-casing users with no login shell. -Instead, it just runs all commands using \f[C]/bin/sh\f[]. -(Commands can of course be script files written in any shell you -like.) -.PP -Nor does it do any special environment handling. -A shell script is better-suited to doing that than a cron daemon. -This cron daemon sets up only four environment variables: USER, -LOGNAME, HOME, and SHELL. -.PP -Our crontab format is roughly similar to that used by vixiecron. -Individual fields may contain a time, a time range, a time range -with a skip factor, a symbolic range for the day of week and month -in year, and additional subranges delimited with commas. -Blank lines in the crontab or lines that begin with a hash (#) are -ignored. -If you specify both a day in the month and a day of week, it will -be interpreted as the Nth such day in the month. -.PP -Some examples: -.IP -.nf -\f[C] -#\ MIN\ HOUR\ DAY\ MONTH\ DAYOFWEEK\ \ COMMAND -#\ run\ `date`\ at\ 6:10\ am\ every\ day -10\ 6\ *\ *\ *\ date - -#\ run\ every\ two\ hours\ at\ the\ top\ of\ the\ hour -0\ */2\ *\ *\ *\ date - -#\ run\ every\ two\ hours\ between\ 11\ pm\ and\ 7\ am,\ and\ again\ at\ 8\ am -0\ 23-7/2,8\ *\ *\ *\ date - -#\ run\ at\ 4:00\ am\ on\ January\ 1st -0\ 4\ 1\ jan\ *\ date - -#\ run\ every\ day\ at\ 11\ am,\ appending\ all\ output\ to\ a\ file -0\ 11\ *\ *\ *\ date\ >>\ /var/log/date-output\ 2>&1 -\f[] -.fi -.PP -To request the last Monday, etc. -in a month, ask for the \[lq]6th\[rq] one. -This will always match the last Monday, etc., even if there are -only four Mondays in the month: -.IP -.nf -\f[C] -#\ run\ at\ 11\ am\ on\ the\ first\ and\ last\ Mon,\ Tue,\ Wed\ of\ each\ month -0\ 11\ 1,6\ *\ mon-wed\ date - -#\ run\ at\ noon\ on\ the\ fourth\ and\ last\ Friday\ of\ each\ month -0\ 12\ 4,6\ *\ fri\ date -\f[] -.fi -.PP -When the fourth Monday in a month is also the last, this will match against -both the \[lq]4th\[rq] and the \[lq]6th\[rq] but the job is scheduled only -once. -.PP -The following formats are also recognized: -.IP -.nf -\f[C] -#\ schedule\ this\ job\ only\ once,\ when\ crond\ starts\ up -\@reboot\ date - -#\ schedule\ this\ job\ whenever\ crond\ is\ running,\ and\ sees\ that\ at\ least\ one -#\ hour\ has\ elapsed\ since\ it\ last\ ran -\@hourly\ ID=job1\ date -\f[] -.fi -.PP -The formats \@hourly, \@daily, \@weekly, \@monthly, and \@yearly -need to update timestamp files when their jobs have been run. -The timestamp files are saved as -/var/spool/cron/cronstamps/user.jobname. -So for all of these formats, the cron command needs a jobname, -given by prefixing the command with \f[C]ID=jobname\f[]. -(This syntax was chosen to maximize the chance that our crontab -files will be readable by other cron daemons as well. -They might just interpret the ID=jobname as a command-line -environment variable assignment.) -.PP -There's also this esoteric option, whose usefulness will be -explained later: +Generally the \-e option is used to edit your crontab. +\f[B]crontab\f[] will use the editor specified by your EDITOR or VISUAL +environment variable (or /usr/bin/vi) to edit the crontab: .IP .nf \f[C] -#\ don\[aq]t\ ever\ schedule\ this\ job\ on\ its\ own;\ only\ run\ it\ when\ it\[aq]s\ triggered -#\ as\ a\ "dependency"\ of\ another\ job\ (see\ below),\ or\ when\ the\ user\ explicitly -#\ requests\ it\ through\ the\ "cron.update"\ file\ (see\ crond(8)) -\@noauto\ ID=namedjob\ date +$\ crontab\ \-e \f[] .fi .PP -There's also a format available for finer-grained control of -frequencies: -.IP -.nf -\f[C] -#\ run\ whenever\ it\[aq]s\ between\ 2-4\ am,\ and\ at\ least\ one\ day\ (1d) -#\ has\ elapsed\ since\ this\ job\ ran -*\ 2-4\ *\ *\ *\ ID=job2\ FREQ=1d\ date - -#\ as\ before,\ but\ re-try\ every\ 10\ minutes\ (10m)\ if\ my_command -#\ exits\ with\ code\ 11\ (EAGAIN) -*\ 2-4\ *\ *\ *\ ID=job3\ FREQ=1d/10m\ my_command -\f[] -.fi -.PP -These formats also update timestamp files, and so also require -their jobs to be assigned IDs. -.PP -Notice the technique used in the second example: jobs can exit with -code 11 to indicate they lacked the resources to run (for example, -no network was available), and so should be tried again after a -brief delay. -This works for jobs using either \@freq or FREQ=\&... formats; but -the FREQ=\&.../10m syntax is the only way to customize the length -of the delay before re-trying. -.PP -Jobs can be made to \[lq]depend\[rq] on, or wait until AFTER other -jobs have successfully completed. -Consider the following crontab: -.IP -.nf -\f[C] -*\ *\ *\ *\ *\ ID=job4\ FREQ=1d\ first_command -*\ *\ *\ *\ *\ ID=job5\ FREQ=1h\ AFTER=job4/30m\ second_command -\f[] -.fi -.PP -Here, whenever job5 is up to be run, if job4 is scheduled to run -within the next 30 minutes (30m), job5 will first wait for it to -successfully complete. -.PP -(What if job4 doesn't successfully complete? If job4 returns with -exit code EAGAIN, job5 will continue to wait until job4 is -retried\[em]even if that won't be within the hour. -If job4 returns with any other non-zero exit code, job5 will be -removed from the queue without running.) -.PP -Jobs can be told to wait for multiple other jobs, as follows: -.IP -.nf -\f[C] -10\ *\ *\ *\ *\ ID=job6\ AFTER=job4/1h,job7\ third_command -\f[] -.fi -.PP -The waiting job6 doesn't care what order job4 and job7 complete in. -If job6 comes up to be re-scheduled (an hour later) while an -earlier instance is still waiting, only a single instance of job6 -will remain in the queue. -It will have all of its \[lq]waiting flags\[rq] reset: so each of -job7 and job4 (supposing again that job4 would run within the next -1h) will again have to complete before job6 will run. -.PP -If a job waits on a \@reboot or \@noauto job, the target job being -waited on will also be scheduled to run. -This technique can be used to have a common job scheduled as -\@noauto that several other jobs depend on (and so call as a -subroutine). -.PP -The command portion of a cron job is run with -\f[C]/bin/sh\ -c\ ...\f[] and may therefore contain any valid -Bourne shell command. -A common practice is to prefix your command with \f[B]exec\f[] to -keep the process table uncluttered. -It is also common to redirect job output to a file or to /dev/null. -If you do not, and the command generates output on stdout or -stderr, that output will be mailed to the local user whose crontab -the job comes from. -If you have crontabs for special users, such as uucp, who can't -receive local mail, you may want to create mail aliases for them or -adjust this behavior. -(See crond(8) for details how to adjust it.) -.PP -Whenever jobs return an exit code that's neither 0 nor 11 (EAGAIN), -that event will be logged, regardless of whether any stdout or -stderr is generated. -The job's timestamp will also be updated, and it won't be run again -until it would next be normally scheduled. -Any jobs waiting on the failed job will be canceled; they won't be -run until they're next scheduled. -.SH TODO -.PP -Ought to be able to have several crontab files for any given user, -as an organizational tool. +Just as a regular user, the superuser also has his or her own per\-user +crontab. +.SH NOTES +.PP +\f[B]crontab\f[] doesn't provide the kinds of protections that programs +like \f[B]visudo\f[] do against syntax errors and simultaneous edits. +Errors won't be detected until \f[B]crond\f[] reads the crontab file. +What \f[B]crontab\f[] does is provide a mechanism for users who may not +themselves have write privileges to the crontab folder to nonetheless +install or edit their crontabs. +It also notifies a running crond daemon of any changes to these files. +.PP +Only users who belong to the same group as the \f[B]crontab\f[] binary +will be able to install or edit crontabs. +However it'll be possible for the superuser to install crontabs even for +users who don't have the privileges to install them themselves. +(Even for users who don't have a login shell.) Only the superuser may +use the \-u or \-c switches to specify a different user and/or crontab +directory. .SH SEE ALSO .PP -\f[B]crond\f[](8) +\f[B]crontab\f[](5) \f[B]crond\f[](8) .SH AUTHORS .PP -Matthew Dillon (dillon\@apollo.backplane.com): original -developer +Matthew Dillon (dillon\@apollo.backplane.com): original developer .PD 0 .P .PD -Jim Pryor (profjim\@jimpryor.net): current -developer +James Pryor (dubiousjim\@gmail.com): current developer diff --git a/crontab.5 b/crontab.5 new file mode 100644 index 0000000..07435e6 --- /dev/null +++ b/crontab.5 @@ -0,0 +1,315 @@ +.\"t +.\" Automatically generated by Pandoc 2.3.1 +.\" +.TH "CRONTAB" "5" "20 Nov 2019" "" "" +.hy +.SH NAME +.PP +crontab \- table of cron jobs +.SH DESCRIPTION +.PP +This page describes the crontab format used by \f[B]dcron\f[]. +Where possible, the format follows the conventions of other cron +implementations, such as Vixie cron. +.PP +You may wish to skip to the comprehensive section of \f[B]EXAMPLES\f[]. +.PP +A crontab consists of one row for each job. +Blank lines in the crontab are ignored, as are lines which begin with a +hash (#) and are used for comments. +.PP +A job consists of a time specification, and command to run: +.IP +.nf +\f[C] +*\ *\ *\ *\ *\ date +\f[] +.fi +.PP +Additional attributes may be specified: +.IP +.nf +\f[C] +*\ *\ *\ *\ *\ ID=myjob\ date +\f[] +.fi +.PP +Shorthand time specifications can be given, in which case cron will +decide when the job runs: +.IP +.nf +\f[C] +\@hourly\ ID=myjob\ date +\f[] +.fi +.PP +\[lq]System\[rq] crontabs (typically in /etc/cron.d; see the section +\f[B]System crontabs\f[]) have an extended format which includes the +user to execute a job as. +.IP +.nf +\f[C] +*\ *\ *\ *\ *\ matthew\ date +\f[] +.fi +.SS Time specification +.PP +The time specification fields are, in order: +.PP +.TS +tab(@); +l l. +T{ +field +T}@T{ +values +T} +_ +T{ +minute +T}@T{ +0\-59 +T} +T{ +hour +T}@T{ +0\-23 +T} +T{ +day of month +T}@T{ +1\-31 +T} +T{ +month +T}@T{ +1\-12; jan\-dec +T} +T{ +day of week +T}@T{ +0\-7; sun\-sun +T} +.TE +.PP +For any field, ranges can be given either numerically (eg. +\[lq]1\-3\[rq]) or using the text equivalents (eg. +\[lq]mon\-wed\[rq]). +Sub\-ranges can be separated with a comma: +.IP +.nf +\f[C] +#\ run\ every\ two\ hours\ between\ 11pm\ and\ 7am,\ and\ again\ at\ 8am +0\ 23\-7/2,8\ *\ *\ *\ date +\f[] +.fi +.PP +If you specify both a day in the month and a day of week, it will be +interpreted as the Nth such day in the month. +.PP +The alternative time specifications of \@hourly, \@daily, \@weekly, +\@monthly, and \@yearly may also be used. +In this case cron will keep a log of when the job was last run, and use +this to decide when to next run the job. +This means it is necessary to set the \f[B]ID=\f[] attribute to uniquely +identify the job: +.IP +.nf +\f[C] +#\ at\ least\ one\ hour\ has\ elapsed\ since\ it\ last\ ran +\@hourly\ ID=job1\ date +\f[] +.fi +.PP +A special time specification \@reboot will begin the job on startup, and +no idenfier is required: +.IP +.nf +\f[C] +\@reboot\ date +\f[] +.fi +.SS System crontabs +.PP +\[lq]System\[rq] crontabs are files located in /etc/cron.d, and it is +normal for these files to be placed by a package manager. +.PP +The jobs run as a user specified on a per\-job basis, following the +convention of other cron implementations: +.IP +.nf +\f[C] +#\ switch\ to\ user\ "matthew"\ and\ run\ the\ command +*\ *\ *\ *\ *\ matthew\ date +\f[] +.fi +.PP +System crontabs remain are \[lq]owned\[rq] by root, which will be the +email recipient of output or errors. +.SS Fine\-grained control +.PP +There's also a format available for finer\-grained control of +frequencies: +.IP +.nf +\f[C] +#\ run\ whenever\ it\[aq]s\ between\ 2\-4\ am,\ and\ at\ least\ one\ day\ (1d) +#\ has\ elapsed\ since\ this\ job\ ran +*\ 2\-4\ *\ *\ *\ ID=job2\ FREQ=1d\ date + +#\ as\ before,\ but\ re\-try\ every\ 10\ minutes\ (10m)\ if\ my_command +#\ exits\ with\ code\ 11\ (EAGAIN) +*\ 2\-4\ *\ *\ *\ ID=job3\ FREQ=1d/10m\ my_command +\f[] +.fi +.PP +These formats also update timestamp files, and so also require their +jobs to be assigned IDs. +.PP +Notice the technique used in the second example: jobs can exit with code +11 to indicate they lacked the resources to run (for example, no network +was available), and so should be tried again after a brief delay. +This works for jobs using either \@freq or FREQ=\&... formats; but the +FREQ=\&.../10m syntax is the only way to customize the length of the +delay before re\-trying. +.SS Dependencies +.PP +Jobs can be made to \[lq]depend\[rq] on, or wait until AFTER other jobs +have successfully completed. +Consider the following crontab: +.IP +.nf +\f[C] +*\ *\ *\ *\ *\ ID=job4\ FREQ=1d\ first_command +*\ *\ *\ *\ *\ ID=job5\ FREQ=1h\ AFTER=job4/30m\ second_command +\f[] +.fi +.PP +Here, whenever job5 is up to be run, if job4 is scheduled to run within +the next 30 minutes (30m), job5 will first wait for it to successfully +complete. +.PP +(What if job4 doesn't successfully complete? +If job4 returns with exit code EAGAIN, job5 will continue to wait until +job4 is retried\[em]even if that won't be within the hour. +If job4 returns with any other non\-zero exit code, job5 will be removed +from the queue without running.) +.PP +Jobs can be told to wait for multiple other jobs, as follows: +.IP +.nf +\f[C] +10\ *\ *\ *\ *\ ID=job6\ AFTER=job4/1h,job7\ third_command +\f[] +.fi +.PP +The waiting job6 doesn't care what order job4 and job7 complete in. +If job6 comes up to be re\-scheduled (an hour later) while an earlier +instance is still waiting, only a single instance of job6 will remain in +the queue. +It will have all of its \[lq]waiting flags\[rq] reset: so each of job7 +and job4 (supposing again that job4 would run within the next 1h) will +again have to complete before job6 will run. +.PP +If a job waits on a \@reboot or \@noauto job, the target job being +waited on will also be scheduled to run. +This technique can be used to have a common job scheduled as \@noauto +that several other jobs depend on (and so call as a subroutine). +.IP +.nf +\f[C] +#\ don\[aq]t\ ever\ schedule\ this\ job\ on\ its\ own;\ only\ run\ it\ when\ it\[aq]s\ triggered +#\ as\ a\ "dependency"\ of\ another\ job\ (see\ below),\ or\ when\ the\ user\ explicitly +#\ requests\ it\ through\ the\ "cron.update"\ file\ (see\ crond(8)) +\@noauto\ ID=namedjob\ date +\f[] +.fi +.SS Command execution +.PP +The command portion of a cron job is run with \f[C]/bin/sh\ \-c\ ...\f[] +and may therefore contain any valid Bourne shell command. +A common practice is to prefix your command with \f[B]exec\f[] to keep +the process table uncluttered. +It is also common to redirect job output to a file or to /dev/null. +If you do not, and the command generates output on stdout or stderr, +that output will be mailed to the local user whose crontab the job comes +from. +If you have crontabs for special users, such as uucp, who can't receive +local mail, you may want to create mail aliases for them or adjust this +behavior. +(See crond(8) for details how to adjust it.) +.PP +Whenever jobs return an exit code that's neither 0 nor 11 (EAGAIN), that +event will be logged, regardless of whether any stdout or stderr is +generated. +The job's timestamp will also be updated, and it won't be run again +until it would next be normally scheduled. +Any jobs waiting on the failed job will be canceled; they won't be run +until they're next scheduled. +.SH EXAMPLES +.PP +Examples of regular user's crontab entries: +.IP +.nf +\f[C] +#\ MIN\ HOUR\ DAY\ MONTH\ DAYOFWEEK\ \ COMMAND + +#\ run\ `date`\ at\ 6:10\ am\ every\ day +10\ 6\ *\ *\ *\ date + +#\ run\ every\ two\ hours\ at\ the\ top\ of\ the\ hour +0\ */2\ *\ *\ *\ date + +#\ run\ every\ two\ hours\ between\ 11\ pm\ and\ 7\ am,\ and\ again\ at\ 8\ am +0\ 23\-7/2,8\ *\ *\ *\ date + +#\ run\ at\ 4:00\ am\ on\ January\ 1st +0\ 4\ 1\ jan\ *\ date + +#\ run\ every\ day\ at\ 11\ am,\ appending\ all\ output\ to\ a\ file +0\ 11\ *\ *\ *\ date\ >>\ /var/log/date\-output\ 2>&1 + +#\ schedule\ this\ job\ only\ once,\ when\ crond\ starts\ up +\@reboot\ date + +#\ at\ least\ one\ hour\ has\ elapsed\ since\ it\ last\ ran +\@hourly\ ID=job1\ date +\f[] +.fi +.PP +To request the last Monday, etc. +in a month, ask for the \[lq]5th\[rq] one. +This will always match the last Monday, etc., even if there are only +four Mondays in the month: +.IP +.nf +\f[C] +#\ run\ at\ 11\ am\ on\ the\ first\ and\ last\ Mon,\ Tue,\ Wed\ of\ each\ month +0\ 11\ 1,5\ *\ mon\-wed\ date +\f[] +.fi +.PP +When the fourth Monday in a month is the last, it will match against +both the \[lq]4th\[rq] and the \[lq]5th\[rq] (it will only run once if +both are specified). +.SH NOTES +.PP +Unlike other cron daemons, this crond/crontab package doesn't try to do +everything under the sun. +It doesn't try to keep track of user's preferred shells; that would +require special\-casing users with no login shell. +Instead, it just runs all commands using \f[C]/bin/sh\f[]. +(Commands can of course be script files written in any shell you like.) +.PP +Nor does it do any special environment handling. +A shell script is better\-suited to doing that than a cron daemon. +This cron daemon sets up only four environment variables: USER, LOGNAME, +HOME, and SHELL. +.SH SEE ALSO +.PP +\f[B]crontab\f[](1) \f[B]crond\f[](8) +.SH AUTHORS +.PP +Matthew Dillon (dillon\@apollo.backplane.com): original developer James +Pryor (dubiousjim\@gmail.com): current developer Mark Hills +(mark\@xwax.org): contributor From 227aada667516afb7480e0bd8404bad2af9a69a8 Mon Sep 17 00:00:00 2001 From: Mark Hills Date: Fri, 24 Jul 2020 14:02:40 +0100 Subject: [PATCH 28/28] Fix a bug where we could return with this not initialised If the file starts with non-job content, such as a comment or newline, we return an invalid line which the caller then takes responsibilty for cleaning up. --- database.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/database.c b/database.c index 45794dd..50d1de2 100644 --- a/database.c +++ b/database.c @@ -560,6 +560,9 @@ ParseLine(CronFile *file, const char *userName, int parseUser, CronLine *line, c char *ptr = buf; int len; + /* Always return with the parsed line in a defined state */ + memset(line, 0, sizeof(*line)); + while (*ptr == ' ' || *ptr == '\t' || *ptr == '\n') ++ptr; @@ -570,8 +573,6 @@ ParseLine(CronFile *file, const char *userName, int parseUser, CronLine *line, c if (*ptr == 0 || *ptr == '#') return 0; - memset(line, 0, sizeof(*line)); - if (DebugOpt) printlogf(LOG_DEBUG, "%s as %s: %s\n", path, userName, buf);