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

Reply via email to