diff --git a/usr.sbin/cron/cron/cron.8 b/usr.sbin/cron/cron/cron.8 index ec66af480e59..02d3aaf9ced5 100644 --- a/usr.sbin/cron/cron/cron.8 +++ b/usr.sbin/cron/cron/cron.8 @@ -26,6 +26,12 @@ .Sh SYNOPSIS .Nm .Oo +.Fl s +.Oc +.Oo +.Fl o +.Oc +.Oo .Fl x .Ar debugflag Ns Op , Ns Ar ... .Oc @@ -71,6 +77,51 @@ need not be restarted whenever a crontab file is modified. Note that the .Xr crontab 1 command updates the modtime of the spool directory whenever it changes a crontab. +.Pp +Available options: +.Bl -tag -width indent +.It Fl s +Enable special handling of situations when the GMT offset of the local +timezone changes, such as the switches between the standard time and +daylight saving time. +.Pp +The jobs run during the GMT offset changes time as +intuitively expected. If a job falls into a time interval that disappears +(for example, during the switch from +standard time) to daylight saving time or is +duplicated (for example, during the reverse switch), then it's handled +in one of two ways: +.Pp +The first case is for the jobs that run every at hour of a time interval +overlapping with the disappearing or duplicated interval. +In other words, if the job had run within one hour before the GMT offset change +(and cron was not restarted nor the +.Xr crontab 5 +changed after that) +or would run after the change at the next hour. +They work as always, skip the skipped time or run in the added +time as usual. +.Pp +The second case is for the jobs that run less frequently. +They are executed exactly once, they are not skipped nor +executed twice (unless cron is restarted or the user's +.Xr crontab 5 +is changed during such a time interval). If an interval disappears +due to the GMT offset change, such jobs are +executed at the same absolute point of time as they would be in the +old time zone. For example, if exactly one hour disappears, this +point would be during the next hour at the first minute that is +specified for them in +.Xr crontab 5 . +.It Fl o +Disable the special handling of situations when the GMT offset of the local +timezone changes, to be compatible with the old (default) behavior. +If both options +.Fl o +and +.Fl s +are specified, the option specified last wins. +.El .Sh SEE ALSO .Xr crontab 1 , .Xr crontab 5 diff --git a/usr.sbin/cron/cron/cron.c b/usr.sbin/cron/cron/cron.c index 8889b722d83c..5131645974af 100644 --- a/usr.sbin/cron/cron/cron.c +++ b/usr.sbin/cron/cron/cron.c @@ -36,19 +36,22 @@ static void usage __P((void)), run_reboot_jobs __P((cron_db *)), cron_tick __P((cron_db *)), cron_sync __P((void)), - cron_sleep __P((void)), + cron_sleep __P((cron_db *)), + cron_clean __P((cron_db *)), #ifdef USE_SIGCHLD sigchld_handler __P((int)), #endif sighup_handler __P((int)), parse_args __P((int c, char *v[])); +static time_t last_time = 0; +static int dst_enabled = 0; static void usage() { char **dflags; - fprintf(stderr, "usage: cron [-x debugflag[,...]]\n"); + fprintf(stderr, "usage: cron [-s] [-o] [-x debugflag[,...]]\n"); fprintf(stderr, "\ndebugflags: "); for(dflags = DebugFlagNames; *dflags; dflags++) { @@ -117,7 +120,7 @@ main(argc, argv) # if DEBUGGING /* if (!(DebugFlags & DTEST)) */ # endif /*DEBUGGING*/ - cron_sleep(); + cron_sleep(&database); load_database(&database); @@ -154,6 +157,11 @@ static void cron_tick(db) cron_db *db; { + static struct tm lasttm; + static time_t diff = 0, /* time difference in seconds from the last offset change */ + difflimit = 0; /* end point for the time zone correction */ + struct tm otztm; /* time in the old time zone */ + int otzminute, otzhour, otzdom, otzmonth, otzdow; register struct tm *tm = localtime(&TargetTime); register int minute, hour, dom, month, dow; register user *u; @@ -170,6 +178,65 @@ cron_tick(db) Debug(DSCH, ("[%d] tick(%d,%d,%d,%d,%d)\n", getpid(), minute, hour, dom, month, dow)) + if (dst_enabled && last_time != 0 + && TargetTime > last_time /* exclude stepping back */ + && tm->tm_gmtoff != lasttm.tm_gmtoff ) { + + diff = tm->tm_gmtoff - lasttm.tm_gmtoff; + + if ( diff > 0 ) { /* ST->DST */ + /* mark jobs for an earlier run */ + difflimit = TargetTime + diff; + for (u = db->head; u != NULL; u = u->next) { + for (e = u->crontab; e != NULL; e = e->next) { + e->flags &= ~NOT_UNTIL; + if ( e->lastrun >= TargetTime ) + e->lastrun = 0; + /* not include the ends of hourly ranges */ + if ( e->lastrun < TargetTime - 3600 ) + e->flags |= RUN_AT; + else + e->flags &= ~RUN_AT; + } + } + } else { /* diff < 0 : DST->ST */ + /* mark jobs for skipping */ + difflimit = TargetTime - diff; + for (u = db->head; u != NULL; u = u->next) { + for (e = u->crontab; e != NULL; e = e->next) { + e->flags |= NOT_UNTIL; + e->flags &= ~RUN_AT; + } + } + } + } + + if (diff != 0) { + /* if the time was reset of the end of special zone is reached */ + if (last_time == 0 || TargetTime >= difflimit) { + /* disable the TZ switch checks */ + diff = 0; + difflimit = 0; + for (u = db->head; u != NULL; u = u->next) { + for (e = u->crontab; e != NULL; e = e->next) { + e->flags &= ~(RUN_AT|NOT_UNTIL); + } + } + } else { + /* get the time in the old time zone */ + time_t difftime = TargetTime + tm->tm_gmtoff - diff; + gmtime_r(&difftime, &otztm); + + /* make 0-based values out of these so we can use them as indicies + */ + otzminute = otztm.tm_min -FIRST_MINUTE; + otzhour = otztm.tm_hour -FIRST_HOUR; + otzdom = otztm.tm_mday -FIRST_DOM; + otzmonth = otztm.tm_mon +1 /* 0..11 -> 1..12 */ -FIRST_MONTH; + otzdow = otztm.tm_wday -FIRST_DOW; + } + } + /* the dom/dow situation is odd. '* * 1,15 * Sun' will run on the * first and fifteenth AND every Sunday; '* * * * Sun' will run *only* * on Sundays; '* * 1,15 * *' will run *only* the 1st and 15th. this @@ -181,6 +248,27 @@ cron_tick(db) Debug(DSCH|DEXT, ("user [%s:%d:%d:...] cmd=\"%s\"\n", env_get("LOGNAME", e->envp), e->uid, e->gid, e->cmd)) + + if ( diff != 0 && (e->flags & (RUN_AT|NOT_UNTIL)) ) { + if (bit_test(e->minute, otzminute) + && bit_test(e->hour, otzhour) + && bit_test(e->month, otzmonth) + && ( ((e->flags & DOM_STAR) || (e->flags & DOW_STAR)) + ? (bit_test(e->dow,otzdow) && bit_test(e->dom,otzdom)) + : (bit_test(e->dow,otzdow) || bit_test(e->dom,otzdom)) + ) + ) { + if ( e->flags & RUN_AT ) { + e->flags &= ~RUN_AT; + e->lastrun = TargetTime; + job_add(e, u); + continue; + } else + e->flags &= ~NOT_UNTIL; + } else if ( e->flags & NOT_UNTIL ) + continue; + } + if (bit_test(e->minute, minute) && bit_test(e->hour, hour) && bit_test(e->month, month) @@ -189,10 +277,15 @@ cron_tick(db) : (bit_test(e->dow,dow) || bit_test(e->dom,dom)) ) ) { + e->flags &= ~RUN_AT; + e->lastrun = TargetTime; job_add(e, u); } } } + + last_time = TargetTime; + lasttm = *tm; } @@ -216,7 +309,9 @@ cron_sync() { static void -cron_sleep() { +cron_sleep(db) + cron_db *db; +{ int seconds_to_wait = 0; /* @@ -231,6 +326,7 @@ cron_sleep() { */ if (seconds_to_wait < -600 || seconds_to_wait > 600) { + cron_clean(db); cron_sync(); continue; } @@ -255,6 +351,26 @@ cron_sleep() { } +/* if the time was changed abruptly, clear the flags related + * to the daylight time switch handling to avoid strange effects + */ + +static void +cron_clean(db) + cron_db *db; +{ + user *u; + entry *e; + + last_time = 0; + + for (u = db->head; u != NULL; u = u->next) { + for (e = u->crontab; e != NULL; e = e->next) { + e->flags &= ~(RUN_AT|NOT_UNTIL); + } + } +} + #ifdef USE_SIGCHLD static void sigchld_handler(x) { @@ -299,8 +415,14 @@ parse_args(argc, argv) { int argch; - while ((argch = getopt(argc, argv, "x:")) != -1) { + while ((argch = getopt(argc, argv, "osx:")) != -1) { switch (argch) { + case 'o': + dst_enabled = 0; + break; + case 's': + dst_enabled = 1; + break; case 'x': if (!set_debug_flags(optarg)) usage(); @@ -310,3 +432,4 @@ parse_args(argc, argv) } } } + diff --git a/usr.sbin/cron/cron/cron.h b/usr.sbin/cron/cron/cron.h index 9d7b8d3bd518..33bdf926502b 100644 --- a/usr.sbin/cron/cron/cron.h +++ b/usr.sbin/cron/cron/cron.h @@ -172,6 +172,9 @@ typedef struct _entry { #define DOM_STAR 0x01 #define DOW_STAR 0x02 #define WHEN_REBOOT 0x04 +#define RUN_AT 0x08 +#define NOT_UNTIL 0x10 + time_t lastrun; } entry; /* the crontab database will be a list of the