Index: usr.sbin/cron/cron/cron.h =================================================================== --- usr.sbin/cron/cron/cron.h +++ usr.sbin/cron/cron/cron.h @@ -193,6 +193,7 @@ #define INTERVAL 0x40 #define DONT_LOG 0x80 #define MAIL_WHEN_ERR 0x100 +#define SINGLE_JOB 0x200 time_t lastrun; } entry; @@ -222,7 +223,8 @@ open_logfile(void), sigpipe_func(void), job_add(entry *, user *), - do_command(entry *, user *), + job_remove(entry *, user *), + job_exit(pid_t), link_user(cron_db *, user *), unlink_user(cron_db *, user *), free_user(user *), @@ -261,6 +263,8 @@ FILE *cron_popen(char *, char *, entry *, PID_T *); +pid_t do_command(entry *, user *); + /* in the C tradition, we only create * variables for the main program, just Index: usr.sbin/cron/cron/cron.c =================================================================== --- usr.sbin/cron/cron/cron.c +++ usr.sbin/cron/cron/cron.c @@ -501,6 +501,7 @@ return; default: find_interval_entry(pid); + job_exit(pid); Debug(DPROC, ("[%d] sigchld...pid #%d died, stat=%d\n", getpid(), pid, WEXITSTATUS(waiter))) Index: usr.sbin/cron/cron/do_command.c =================================================================== --- usr.sbin/cron/cron/do_command.c +++ usr.sbin/cron/cron/do_command.c @@ -44,7 +44,7 @@ extern char *environ; -void +pid_t do_command(e, u) entry *e; user *u; @@ -79,9 +79,12 @@ e->lastexit = 0; e->child = pid; } + if ((e->flags & SINGLE_JOB) == 0) + pid = -1; break; } Debug(DPROC, ("[%d] main process returning to work\n", getpid())) + return (pid); } Index: usr.sbin/cron/cron/job.c =================================================================== --- usr.sbin/cron/cron/job.c +++ usr.sbin/cron/cron/job.c @@ -21,18 +21,19 @@ #endif +#include #include "cron.h" typedef struct _job { - struct _job *next; - entry *e; - user *u; + STAILQ_ENTRY(_job) next; + entry *e; + user *u; + pid_t pid; } job; -static job *jhead = NULL, *jtail = NULL; - +static STAILQ_HEAD(job_queue, _job) jobs = STAILQ_HEAD_INITIALIZER(jobs); void job_add(e, u) @@ -42,35 +43,84 @@ register job *j; /* if already on queue, keep going */ - for (j=jhead; j; j=j->next) + STAILQ_FOREACH(j, &jobs, next) { if (j->e == e && j->u == u) { return; } + } /* build a job queue element */ if ((j = (job*)malloc(sizeof(job))) == NULL) return; - j->next = (job*) NULL; j->e = e; j->u = u; + j->pid = -1; /* add it to the tail */ - if (!jhead) { jhead=j; } - else { jtail->next=j; } - jtail = j; + STAILQ_INSERT_TAIL(&jobs, j, next); } +void +job_remove(entry *e, user *u) +{ + job *j, *prev = NULL; + + STAILQ_FOREACH(j, &jobs, next) { + if (j->e == e && j->u == u) { + if (prev == NULL) + STAILQ_REMOVE_HEAD(&jobs, next); + else + STAILQ_REMOVE_AFTER(&jobs, prev, next); + free(j); + break; + } + prev = j; + } +} + +void +job_exit(pid_t jobpid) +{ + job *j, *prev = NULL; + + /* If a singleton exited, remove and free it. */ + STAILQ_FOREACH(j, &jobs, next) { + if (jobpid == j->pid) { + if (prev == NULL) + STAILQ_REMOVE_HEAD(&jobs, next); + else + STAILQ_REMOVE_AFTER(&jobs, prev, next); + free(j); + break; + } + prev = j; + } +} int job_runqueue() { - register job *j, *jn; + register job *j; register int run = 0; + struct job_queue singletons = STAILQ_HEAD_INITIALIZER(singletons); + char buf[MAX_TEMPSTR]; + + while ((j = STAILQ_FIRST(&jobs))) { + STAILQ_REMOVE_HEAD(&jobs, next); + + /* Only start the job if it is not a running singleton. */ + if (j->pid == -1) { + j->pid = do_command(j->e, j->u); + run++; + } else if ((j->e->flags & DONT_LOG) == 0) { + snprintf(buf, sizeof(buf), "%s", j->e->cmd); + log_it(j->u->name, getpid(), "SKIPPING", buf); + } - for (j=jhead; j; j=jn) { - do_command(j->e, j->u); - jn = j->next; - free(j); - run++; + /* Singleton jobs persist in the queue until they exit. */ + if (j->pid != -1) + STAILQ_INSERT_TAIL(&singletons, j, next); + else + free(j); } - jhead = jtail = NULL; + STAILQ_CONCAT(&jobs, &singletons); return run; } Index: usr.sbin/cron/crontab/crontab.5 =================================================================== --- usr.sbin/cron/crontab/crontab.5 +++ usr.sbin/cron/crontab/crontab.5 @@ -17,7 +17,7 @@ .\" .\" $FreeBSD$ .\" -.Dd March 29, 2020 +.Dd July 11, 2021 .Dt CRONTAB 5 .Os .Sh NAME @@ -233,6 +233,13 @@ .Xr cron 8 . .It Fl q Execution will not be logged. +.It Fl s +Only a single instance of +.Ar command +will be run concurrently. +Additional instances of +.Ar command +will not be scheduled until the earlier one completes. .El .sp Duplicate options are not allowed. @@ -354,6 +361,10 @@ option does not mail on successful run. .Sh AUTHORS .An Paul Vixie Aq Mt paul@vix.com +.Pp +The +.Fl s +command option was inspired by similar functionality in OpenBSD. .Sh BUGS If you are in one of the 70-odd countries that observe Daylight Savings Time, jobs scheduled during the rollback or advance may be Index: usr.sbin/cron/lib/entry.c =================================================================== --- usr.sbin/cron/lib/entry.c +++ usr.sbin/cron/lib/entry.c @@ -456,6 +456,14 @@ } e->flags |= DONT_LOG; break; + case 's': + /* only allow the user to set the option once */ + if ((e->flags & SINGLE_JOB) == SINGLE_JOB) { + ecode = e_option; + goto eof; + } + e->flags |= SINGLE_JOB; + break; default: Debug(DPARS|DEXT, ("load_entry()...invalid option '%c'\n", ch)) ecode = e_option;