Hi all,
The below patch adds a new kind of time specifier: an interval (in
minutes). When used, cron(8) will schedule the next instance of a job
after the previous job has completed and a full interval has passed.
A crontab(5) configured as following:
$ crontab -l
@3 sleep 100
Will result in this schedule:
Jul 10 13:38:17 vurt cron[96937]: (CRON) STARTUP (V5.0)
Jul 10 13:42:01 vurt cron[79385]: (job) CMD (sleep 100)
Jul 10 13:47:01 vurt cron[3165]: (job) CMD (sleep 100)
Jul 10 13:52:01 vurt cron[40539]: (job) CMD (sleep 100)
Jul 10 13:57:01 vurt cron[84504]: (job) CMD (sleep 100)
A use case could be running rpki-client more frequently than once an
hour:
@15 -n rpki-client && bgpctl reload
The above is equivalent to:
* * * * * -sn sleep 900 && rpki-client && bgpctl reload
I borrowed the idea from FreeBSD's cron [1]. A difference between the
below changeset and the freebsd implementation is that they specify the
interval in seconds, while the below specifies in minutes. I was able
to leverage the 'singleton' infrastructure. And removed a comment that
reads like a TODO nobody is going to do.
Thoughts?
Kind regards,
Job
[1]:
https://github.com/freebsd/freebsd-src/commit/a08d12d3f2d4f4dabfc01953be696fdc0750da9c#
Index: cron.c
===================================================================
RCS file: /cvs/src/usr.sbin/cron/cron.c,v
retrieving revision 1.79
diff -u -p -r1.79 cron.c
--- cron.c 16 Apr 2020 17:51:56 -0000 1.79
+++ cron.c 10 Jul 2021 13:38:13 -0000
@@ -273,6 +273,8 @@ run_reboot_jobs(cron_db *db)
SLIST_FOREACH(e, &u->crontab, entries) {
if (e->flags & WHEN_REBOOT)
job_add(e, u);
+ if (e->flags & INTERVAL)
+ e->lastexit = StartTime;
}
}
(void) job_runqueue();
@@ -303,6 +305,12 @@ find_jobs(time_t vtime, cron_db *db, int
*/
TAILQ_FOREACH(u, &db->users, entries) {
SLIST_FOREACH(e, &u->crontab, entries) {
+ if (e->flags & INTERVAL) {
+ if (e->lastexit > 0 &&
+ virtualSecond >= e->lastexit + e->interval)
+ job_add(e, u);
+ continue;
+ }
if (bit_test(e->minute, minute) &&
bit_test(e->hour, hour) &&
bit_test(e->month, month) &&
Index: crontab.5
===================================================================
RCS file: /cvs/src/usr.sbin/cron/crontab.5,v
retrieving revision 1.41
diff -u -p -r1.41 crontab.5
--- crontab.5 18 Apr 2020 17:11:40 -0000 1.41
+++ crontab.5 10 Jul 2021 13:38:13 -0000
@@ -265,7 +265,7 @@ For example,
would cause a command to be run at 4:30 am on the 1st and 15th of each
month, plus every Friday.
.Pp
-Instead of the first five fields, one of eight special strings may appear:
+Instead of the first five fields, one of nine special strings may appear:
.Bl -column "@midnight" "meaning" -offset indent
.It Sy string Ta Sy meaning
.It @reboot Ta Run once, at startup.
@@ -276,6 +276,14 @@ Instead of the first five fields, one of
.It @daily Ta Run every midnight (0 0 * * *).
.It @midnight Ta The same as @daily.
.It @hourly Ta Run every hour, on the hour (0 * * * *).
+.It Pf @ Ar minutes Ta The
+.Sq @
+symbol followed by a numeric value has a special notion of running a job
+after an interval specified in minutes has passed, and the previous
+instance has completed.
+The first run is scheduled at a full interval after
+.Xr cron 8
+started.
.El
.Sh ENVIRONMENT
.Bl -tag -width "LOGNAMEXXX"
@@ -346,7 +354,9 @@ MAILTO=paul
5 4 * * sun echo "run at 5 after 4 every sunday"
# run hourly at a random time within the first 30 minutes of the hour
-0~30 * * * * /usr/libexec/spamd-setup
+0~30 * * * * /usr/libexec/spamd-setup
+
+@10 sleep 180 && echo "starts every 13 minutes, implies -s"
.Ed
.Sh SEE ALSO
.Xr crontab 1 ,
@@ -372,6 +382,8 @@ Random intervals are supported using the
character.
.It
Months or days of the week can be specified by name.
+.It
+Interval mode.
.It
Environment variables can be set in a crontab.
.It
Index: do_command.c
===================================================================
RCS file: /cvs/src/usr.sbin/cron/do_command.c,v
retrieving revision 1.61
diff -u -p -r1.61 do_command.c
--- do_command.c 16 Apr 2020 17:51:56 -0000 1.61
+++ do_command.c 10 Jul 2021 13:38:13 -0000
@@ -54,7 +54,8 @@ do_command(entry *e, user *u)
/* fork to become asynchronous -- parent process is done immediately,
* and continues to run the normal cron code, which means return to
- * tick(). the child and grandchild don't leave this function, alive.
+ * find_jobs().
+ * The child and grandchild don't leave this function, alive.
*
* vfork() is unsuitable, since we have much to do, and the parent
* needs to be able to run off and fork other processes.
@@ -62,6 +63,8 @@ do_command(entry *e, user *u)
switch ((pid = fork())) {
case -1:
syslog(LOG_ERR, "(CRON) CAN'T FORK (%m)");
+ if (e->flags & INTERVAL)
+ e->lastexit = time(NULL);
break;
case 0:
/* child process */
@@ -70,6 +73,8 @@ do_command(entry *e, user *u)
break;
default:
/* parent process */
+ if (e->flags & INTERVAL)
+ e->lastexit = 0;
if ((e->flags & SINGLE_JOB) == 0)
pid = -1;
break;
Index: entry.c
===================================================================
RCS file: /cvs/src/usr.sbin/cron/entry.c,v
retrieving revision 1.52
diff -u -p -r1.52 entry.c
--- entry.c 18 Apr 2020 16:19:02 -0000 1.52
+++ entry.c 10 Jul 2021 13:38:13 -0000
@@ -126,18 +126,9 @@ load_entry(FILE *file, void (*error_func
}
if (ch == '@') {
- /* all of these should be flagged and load-limited; i.e.,
- * instead of @hourly meaning "0 * * * *" it should mean
- * "close to the front of every hour but not 'til the
- * system load is low". Problems are: how do you know
- * what "low" means? (save me from /etc/cron.conf!) and:
- * how to guarantee low variance (how low is low?), which
- * means how to we run roughly every hour -- seems like
- * we need to keep a history or let the first hour set
- * the schedule, which means we aren't load-limited
- * anymore. too much for my overloaded brain. (vix, jan90)
- * HINT
- */
+ char *endptr;
+ long interval;
+
ch = get_string(cmd, MAX_COMMAND, file, " \t\n");
if (!strcmp("reboot", cmd)) {
e->flags |= WHEN_REBOOT;
@@ -175,6 +166,11 @@ load_entry(FILE *file, void (*error_func
bit_nset(e->month, 0, (LAST_MONTH-FIRST_MONTH+1));
bit_nset(e->dow, 0, (LAST_DOW-FIRST_DOW+1));
e->flags |= HR_STAR;
+ } else if (*cmd != '\0' &&
+ (interval = strtol(cmd, &endptr, 10)) > 0 &&
+ *endptr == '\0') {
+ e->interval = interval * SECONDS_PER_MINUTE;
+ e->flags |= INTERVAL | SINGLE_JOB;
} else {
ecode = e_timespec;
goto eof;
Index: job.c
===================================================================
RCS file: /cvs/src/usr.sbin/cron/job.c,v
retrieving revision 1.15
diff -u -p -r1.15 job.c
--- job.c 17 Apr 2020 02:12:56 -0000 1.15
+++ job.c 10 Jul 2021 13:38:13 -0000
@@ -92,6 +92,8 @@ job_exit(pid_t jobpid)
/* If a singleton exited, remove and free it. */
SIMPLEQ_FOREACH(j, &jobs, entries) {
if (jobpid == j->pid) {
+ if (j->e->flags & INTERVAL)
+ j->e->lastexit = time(NULL);
if (prev == NULL)
SIMPLEQ_REMOVE_HEAD(&jobs, entries);
else
Index: structs.h
===================================================================
RCS file: /cvs/src/usr.sbin/cron/structs.h,v
retrieving revision 1.10
diff -u -p -r1.10 structs.h
--- structs.h 16 Apr 2020 17:51:56 -0000 1.10
+++ structs.h 10 Jul 2021 13:38:13 -0000
@@ -26,11 +26,19 @@ typedef struct _entry {
struct passwd *pwd;
char **envp;
char *cmd;
- bitstr_t bit_decl(minute, MINUTE_COUNT);
- bitstr_t bit_decl(hour, HOUR_COUNT);
- bitstr_t bit_decl(dom, DOM_COUNT);
- bitstr_t bit_decl(month, MONTH_COUNT);
- bitstr_t bit_decl(dow, DOW_COUNT);
+ union {
+ struct {
+ bitstr_t bit_decl(minute, MINUTE_COUNT);
+ bitstr_t bit_decl(hour, HOUR_COUNT);
+ bitstr_t bit_decl(dom, DOM_COUNT);
+ bitstr_t bit_decl(month, MONTH_COUNT);
+ bitstr_t bit_decl(dow, DOW_COUNT);
+ };
+ struct {
+ time_t lastexit;
+ time_t interval;
+ };
+ };
int flags;
#define MIN_STAR 0x01
#define HR_STAR 0x02
@@ -40,6 +48,7 @@ typedef struct _entry {
#define DONT_LOG 0x20
#define MAIL_WHEN_ERR 0x40
#define SINGLE_JOB 0x80
+#define INTERVAL 0x100
} entry;
/* the crontab database will be a list of the