From thompson@squirrel.tgsoft.com  Thu Apr 17 06:33:23 1997
Received: from squirrel.tgsoft.com (squirrel.tgsoft.com [207.167.64.183])
          by freefall.freebsd.org (8.8.5/8.8.5) with SMTP id GAA01705
          for <FreeBSD-gnats-submit@freebsd.org>; Thu, 17 Apr 1997 06:33:17 -0700 (PDT)
Received: (qmail 7324 invoked by uid 128); 17 Apr 1997 13:33:15 -0000
Message-Id: <19970417133315.7323.qmail@squirrel.tgsoft.com>
Date: 17 Apr 1997 13:33:15 -0000
From: thompson@squirrel.tgsoft.com
Reply-To: thompson@squirrel.tgsoft.com
To: FreeBSD-gnats-submit@freebsd.org
Subject: /etc/daily did not run on April 6, 1997
X-Send-Pr-Version: 3.2

>Number:         3314
>Category:       bin
>Synopsis:       [PATCH] /etc/daily did not run on April 6, 1997
>Confidential:   no
>Severity:       non-critical
>Priority:       low
>Responsible:    freebsd-bugs
>State:          closed
>Quarter:        
>Keywords:       
>Date-Required:  
>Class:          sw-bug
>Submitter-Id:   current-users
>Arrival-Date:   Thu Apr 17 06:40:01 PDT 1997
>Closed-Date:    Sun Nov 19 11:39:45 PST 2000
>Last-Modified:  Sun Nov 19 11:42:28 PST 2000
>Originator:     mark thompson
>Release:        FreeBSD 2.2.1-RELEASE i386
>Organization:
tgsoft
>Environment:

Standard 2.2.1, and many BSDs before it

>Description:

cron is simply not equipped to handle time changes caused by the
arrival and departure of daylight savings time. Since this is a
common event around the world, it seems that it should be.

>How-To-Repeat:

Submit a cron job that executes a few minutes hence, then adjust your system
clock to an hour from now.

>Fix:
	
Simple patch enclosed

diff -c -r -b ./cron/cron.8 cron/NEW/cron/cron.8
*** ./cron/cron.8	Sun Jun 30 15:11:50 1996
--- cron/NEW/cron/cron.8	Thu Apr 17 06:17:38 1997
***************
*** 13,18 ****
--- 13,21 ----
  .\" * Send bug reports, bug fixes, enhancements, requests, flames, etc., and
  .\" * I'll try to keep a version up to date.  I can be reached as follows:
  .\" * Paul Vixie          <paul@vix.com>          uunet!decwrl!vixie!paul
+ .\"
+ .\" * new timekeeping (handle daylight savings time) by
+ .\" * mark thompson       <thompson@tgsoft.com> ... so don't blame Paul
  .\" */
  .\" 
  .\" $Id: cron.8,v 1.2 1996/06/30 22:11:50 wosch Exp $
***************
*** 54,59 ****
--- 57,77 ----
  .IR Crontab (1)
  command updates the modtime of the spool directory whenever it changes a
  crontab.
+ .PP
+ Special considerations exist when the clock is changed by less than 3
+ hours, for example at the beginning and end of daylight savings
+ time. If the time has moved forwards, those jobs which would have
+ run in the time that was skipped will be run soon after the change. 
+ Conversely, if the time has moved backwards by less than 3 hours,
+ those jobs that fall into the repeated time will not be run.
+ .PP
+ Only jobs that run at a particular time (not specified as
+ @hourly, nor with '*' in the hour or minute specifier) are
+ affected. Jobs which are specified with wildcards are run based on the
+ new time immediately.
+ .PP
+ Clock changes of more than 3 hours are considered to be corrections to
+ the clock, and the new time is used immediately.
  .SH "SEE ALSO"
  crontab(1), crontab(5)
  .SH AUTHOR
diff -c -r -b ./cron/cron.c cron/NEW/cron/cron.c
*** ./cron/cron.c	Sun Jun 30 15:11:51 1996
--- cron/NEW/cron/cron.c	Thu Apr 17 06:05:25 1997
***************
*** 13,18 ****
--- 13,21 ----
   * Send bug reports, bug fixes, enhancements, requests, flames, etc., and
   * I'll try to keep a version up to date.  I can be reached as follows:
   * Paul Vixie          <paul@vix.com>          uunet!decwrl!vixie!paul
+  *
+  * New timekeeping (main after first load_database, last if in find_jobs,
+  * set_time and cron_sleep) by mark thompson <thompson@tgsoft.com>
   */
  
  #if !defined(lint) && !defined(LINT)
***************
*** 34,42 ****
  
  static	void	usage __P((void)),
  		run_reboot_jobs __P((cron_db *)),
! 		cron_tick __P((cron_db *)),
! 		cron_sync __P((void)),
! 		cron_sleep __P((void)),
  #ifdef USE_SIGCHLD
  		sigchld_handler __P((int)),
  #endif
--- 37,45 ----
  
  static	void	usage __P((void)),
  		run_reboot_jobs __P((cron_db *)),
! 		find_jobs __P((time_min, cron_db *, int, int)),
!                 set_time __P((void)),
!                 cron_sleep __P((time_min)),
  #ifdef USE_SIGCHLD
  		sigchld_handler __P((int)),
  #endif
***************
*** 93,104 ****
  
  	/* if there are no debug flags turned on, fork as a daemon should.
  	 */
! # if DEBUGGING
  	if (DebugFlags) {
! # else
! 	if (0) {
! # endif
  		(void) fprintf(stderr, "[%d] cron started\n", getpid());
  	} else {
  		switch (fork()) {
  		case -1:
--- 96,106 ----
  
  	/* if there are no debug flags turned on, fork as a daemon should.
  	 */
! 
  	if (DebugFlags) {
! #if DEBUGGING
  	    (void) fprintf(stderr, "[%d] cron started\n", getpid());
+ #endif
  	} else {
  		switch (fork()) {
  		case -1:
***************
*** 121,143 ****
  	database.tail = NULL;
  	database.mtime = (time_t) 0;
  	load_database(&database);
  	run_reboot_jobs(&database);
! 	cron_sync();
  	while (TRUE) {
! # if DEBUGGING
! 	    /* if (!(DebugFlags & DTEST)) */
! # endif /*DEBUGGING*/
! 			cron_sleep();
  
  		load_database(&database);
  
! 		/* do this iteration
! 		 */
! 		cron_tick(&database);
  
! 		/* sleep 1 minute
! 		 */
! 		TargetTime += 60;
  	}
  }
  
--- 123,232 ----
  	database.tail = NULL;
  	database.mtime = (time_t) 0;
  	load_database(&database);
+ 
+ 	set_time();
  	run_reboot_jobs(&database);
! 	timeRunning = virtualTime = clockTime;
! 
! /*
!  * too many clocks, not enough time (Al. Einstein)
!  * These clocks are in minutes since the epoch (time()/60).
!  * virtualTime is the time it *would* be if we woke up promptly and nobody
!  *             ever changed the clock. It is monotonically increasing...
!  *             unless a timejump happens.
!  *             At the top of the loop, all jobs for 'virtualTime' have run.
!  * timeRunning is the time we last awakened.
!  * clockTime   is the time when set_time was last called.
!  */
  	while (TRUE) {
! 	    time_min timeDiff;
! 	    int wakeupKind;
  
  	    load_database(&database);
  
! 	    /* ... wait for the time (in minutes) to change ... */
! 	    do {
! 		cron_sleep(timeRunning + 1);
! 		set_time();
! 	    } while (clockTime == timeRunning);
! 	    timeRunning = clockTime;
! 
! 	    /* ... calculate how the current time differs from our virtual  */
! 	    /* clock. Classify the change into one of 4 cases               */
! 	    timeDiff = timeRunning - virtualTime;
! 
! 	    /* shortcut for the most common case */
! 	    if (timeDiff == 1) {
! 		virtualTime = timeRunning;
! 		find_jobs(virtualTime, &database, TRUE, TRUE);
! 	    }
! 	    else {
! 		wakeupKind = -1;
! 		if (timeDiff > -(3*MINUTE_COUNT)) wakeupKind = 0;
! 		if (timeDiff >  0) wakeupKind = 1;
! 		if (timeDiff >  5) wakeupKind = 2;
! 		if (timeDiff > (3*MINUTE_COUNT)) wakeupKind = 3;
! 
! 		switch (wakeupKind) {
! 		/* case 1: timeDiff is a small positive number (wokeup late) */
! 		/* run jobs for each virtual minute until caught up.         */
! 		case 1:
! 		    Debug(DSCH, ("[%d], normal case %d minutes to go\n",
! 				 getpid(), timeRunning - virtualTime))
! 		    do {
! 			if (job_runqueue()) sleep(10);
  
! 			virtualTime++;
! 			find_jobs(virtualTime, &database, TRUE, TRUE);
! 		    } while (virtualTime < timeRunning);
! 		    break;
! 
! 		/* case 2: timeDiff is a medium-sized positive number,      */
! 		/* for example because we went to DST                       */
! 		/* run wildcard jobs once, then run any fixed-time jobs that*/
! 		/* would otherwise be skipped                               */
! 		/* if we use up our minute (possible, if there are a lot of */
! 		/* jobs to run) go around the loop again so that wildcard   */
! 		/* jobs have a chance to run, and we do our housekeeping    */
! 		case 2:
! 		    Debug(DSCH, ("[%d], DST begins %d minutes to go\n",
! 				 getpid(), timeRunning - virtualTime))
! 		    /* run wildcard jobs for current minute */
! 		    find_jobs(timeRunning, &database, TRUE, FALSE);
! 
! 		    /* run fixed-time jobs for each minute missed */ 
! 		    do {
! 			if (job_runqueue()) sleep(10);
! 
! 			virtualTime++;
! 			find_jobs(virtualTime, &database, FALSE, TRUE);
! 
! 			set_time();
! 		    } while (virtualTime < timeRunning &&
! 				 clockTime == timeRunning);
! 		    break;
! 
! 		/* case 3: timeDiff is a small or medium-sized negative num. */
! 		/* eg. because of DST ending */
! 		/* just run the wildcard jobs. The fixed-time jobs probably  */
! 		/* have already run, and should not be repeated              */
! 		/* virtual time does not change until we are caught up       */
! 		case 0:
! 		    Debug(DSCH, ("[%d], DST ends %d minutes to go\n",
! 				 getpid(), virtualTime - timeRunning))
! 		    find_jobs(timeRunning, &database, TRUE, FALSE);
! 		    break;
! 
! 		/* other: time has changed a *lot* */
! 		/* jump virtual time, and run everything */
! 		default:
! 		    Debug(DSCH, ("[%d], clock jumped\n", getpid()))
! 		    virtualTime = timeRunning;
! 		    find_jobs(timeRunning, &database, TRUE, TRUE);
! 		}
! 	    }
! 	    /* jobs to be run (if any) are loaded. clear the queue */
! 	    job_runqueue();
  	}
  }
  
***************
*** 161,170 ****
  
  
  static void
! cron_tick(db)
  	cron_db	*db;
  {
!  	register struct tm	*tm = localtime(&TargetTime);
  	register int		minute, hour, dom, month, dow;
  	register user		*u;
  	register entry		*e;
--- 250,263 ----
  
  
  static void
! find_jobs(vtime, db, doWild, doNonWild)
!         time_min vtime;
  	cron_db	*db;
+ 	int     doWild;
+ 	int     doNonWild;
  {
!         time_t   virtualSecond  = vtime * SECONDS_PER_MINUTE;
!  	register struct tm	*tm = localtime(&virtualSecond);
  	register int		minute, hour, dom, month, dow;
  	register user		*u;
  	register entry		*e;
***************
*** 177,184 ****
  	month = tm->tm_mon +1 /* 0..11 -> 1..12 */ -FIRST_MONTH;
  	dow = tm->tm_wday -FIRST_DOW;
  
! 	Debug(DSCH, ("[%d] tick(%d,%d,%d,%d,%d)\n",
! 		getpid(), minute, hour, dom, month, dow))
  
  	/* the dom/dow situation is odd.  '* * 1,15 * Sun' will run on the
  	 * first and fifteenth AND every Sunday;  '* * * * Sun' will run *only*
--- 270,278 ----
  	month = tm->tm_mon +1 /* 0..11 -> 1..12 */ -FIRST_MONTH;
  	dow = tm->tm_wday -FIRST_DOW;
  
! 	Debug(DSCH, ("[%d] tick(%d,%d,%d,%d,%d) %s %s\n",
! 		getpid(), minute, hour, dom, month, dow,
! 		doWild?" ":"No wildcard",doNonWild?" ":"Wildcard only"))
  
  	/* the dom/dow situation is odd.  '* * 1,15 * Sun' will run on the
  	 * first and fifteenth AND every Sunday;  '* * * * Sun' will run *only*
***************
*** 199,204 ****
--- 293,300 ----
  			      : (bit_test(e->dow,dow) || bit_test(e->dom,dom))
  			    )
  			   ) {
+ 			    if ((doNonWild && !(e->flags & (MIN_STAR|HR_STAR)))
+ 				|| (doWild && (e->flags & (MIN_STAR|HR_STAR))))
  				job_add(e, u);
  			}
  		}
***************
*** 206,255 ****
  }
  
  
! /* the task here is to figure out how long it's going to be until :00 of the
!  * following minute and initialize TargetTime to this value.  TargetTime
!  * will subsequently slide 60 seconds at a time, with correction applied
!  * implicitly in cron_sleep().  it would be nice to let cron execute in
!  * the "current minute" before going to sleep, but by restarting cron you
!  * could then get it to execute a given minute's jobs more than once.
!  * instead we have the chance of missing a minute's jobs completely, but
!  * that's something sysadmin's know to expect what with crashing computers..
   */
  static void
! cron_sync() {
!  	register struct tm	*tm;
! 
! 	TargetTime = time((time_t*)0);
! 	tm = localtime(&TargetTime);
! 	TargetTime += (60 - tm->tm_sec);
  }
  
! 
  static void
! cron_sleep() {
  	register int	seconds_to_wait;
  
! 	do {
! 		seconds_to_wait = (int) (TargetTime - time((time_t*)0));
  		Debug(DSCH, ("[%d] TargetTime=%ld, sec-to-wait=%d\n",
! 			getpid(), TargetTime, seconds_to_wait))
  
! 		/* if we intend to sleep, this means that it's finally
! 		 * time to empty the job queue (execute it).
! 		 *
! 		 * if we run any jobs, we'll probably screw up our timing,
! 		 * so go recompute.
! 		 *
! 		 * note that we depend here on the left-to-right nature
! 		 * of &&, and the short-circuiting.
! 		 */
! 	} while (seconds_to_wait > 0 && job_runqueue());
! 
! 	while (seconds_to_wait > 0) {
! 		Debug(DSCH, ("[%d] sleeping for %d seconds\n",
! 			getpid(), seconds_to_wait))
! 		seconds_to_wait = (int) sleep((unsigned int) seconds_to_wait);
! 	}
  }
  
  
--- 302,334 ----
  }
  
  
! /*
!  * set StartTime and clockTime to the current time.
!  * these are used for computing what time it really is right now.
!  * note that clockTime is a unix wallclock time converted to minutes
   */
  static void
! set_time() {
!     StartTime = time((time_t *)0);
!     clockTime = StartTime / (unsigned long)SECONDS_PER_MINUTE;
  }
  
! /*
!  * try to just hit the next minute
!  */
  static void
! cron_sleep(target)
!     time_min target;
! {
      register int	seconds_to_wait;
+     register struct tm *tm;
  
!     seconds_to_wait = (int)(target*SECONDS_PER_MINUTE - time((time_t*)0)) + 1;
      Debug(DSCH, ("[%d] TargetTime=%ld, sec-to-wait=%d\n",
! 		 getpid(),  target*SECONDS_PER_MINUTE, seconds_to_wait))
  
!     if (seconds_to_wait > 0 && seconds_to_wait < 65)
! 	sleep((unsigned int) seconds_to_wait);
  }
  
  
diff -c -r -b ./cron/cron.h cron/NEW/cron/cron.h
*** ./cron/cron.h	Sun Aug  4 17:31:24 1996
--- cron/NEW/cron/cron.h	Wed Apr 16 06:28:15 1997
***************
*** 120,125 ****
--- 120,129 ----
  			 LineNumber = ln; \
  			}
  
+ typedef long time_min;
+ 
+ #define SECONDS_PER_MINUTE 60
+ 
  #define	FIRST_MINUTE	0
  #define	LAST_MINUTE	59
  #define	MINUTE_COUNT	(LAST_MINUTE - FIRST_MINUTE + 1)
***************
*** 162,167 ****
--- 166,173 ----
  #define	DOM_STAR	0x01
  #define	DOW_STAR	0x02
  #define	WHEN_REBOOT	0x04
+ #define MIN_STAR        0x08
+ #define HR_STAR         0x10
  } entry;
  
  			/* the crontab database will be a list of the
***************
*** 256,262 ****
  
  char	*ProgramName;
  int	LineNumber;
! time_t	TargetTime;
  
  # if DEBUGGING
  int	DebugFlags;
--- 262,271 ----
  
  char	*ProgramName;
  int	LineNumber;
! time_t	StartTime;
! time_min timeRunning;
! time_min virtualTime;
! time_min clockTime;
  
  # if DEBUGGING
  int	DebugFlags;
***************
*** 264,269 ****
--- 273,280 ----
  		"ext", "sch", "proc", "pars", "load", "misc", "test", "bit",
  		NULL		/* NULL must be last element */
  	};
+ # else
+ #define DebugFlags 0
  # endif /* DEBUGGING */
  #else /*MAIN_PROGRAM*/
  extern	char	*copyright[],
***************
*** 271,277 ****
  		*DowNames[],
  		*ProgramName;
  extern	int	LineNumber;
! extern	time_t	TargetTime;
  # if DEBUGGING
  extern	int	DebugFlags;
  extern	char	*DebugFlagNames[];
--- 282,291 ----
  		*DowNames[],
  		*ProgramName;
  extern	int	LineNumber;
! extern	time_t	StartTime;
! extern  time_min timeRunning;
! extern  time_min virtualTime;
! extern  time_min clockTime;
  # if DEBUGGING
  extern	int	DebugFlags;
  extern	char	*DebugFlagNames[];
diff -c -r -b ./cron/database.c cron/NEW/cron/database.c
*** ./cron/database.c	Mon Sep  9 20:38:20 1996
--- cron/NEW/cron/database.c	Wed Apr 16 08:33:51 1997
***************
*** 13,18 ****
--- 13,22 ----
   * Send bug reports, bug fixes, enhancements, requests, flames, etc., and
   * I'll try to keep a version up to date.  I can be reached as follows:
   * Paul Vixie          <paul@vix.com>          uunet!decwrl!vixie!paul
+  *
+  * stored database time is hash of time of SPOOL_DIR and of /etc/crontab
+  * so that clock fluctuations will be correctly detected
+  * mark thompson <thompson@tgsoft.com>
   */
  
  #if !defined(lint) && !defined(LINT)
***************
*** 30,36 ****
  
  
  #define TMAX(a,b) ((a)>(b)?(a):(b))
! 
  
  static	void		process_crontab __P((char *, char *, char *,
  					     struct stat *,
--- 34,40 ----
  
  
  #define TMAX(a,b) ((a)>(b)?(a):(b))
! #define HASH(a,b) ((a)+(b))
  
  static	void		process_crontab __P((char *, char *, char *,
  					     struct stat *,
***************
*** 71,77 ****
  	 * so is guaranteed to be different than the stat() mtime the first
  	 * time this function is called.
  	 */
! 	if (old_db->mtime == TMAX(statbuf.st_mtime, syscron_stat.st_mtime)) {
  		Debug(DLOAD, ("[%d] spool dir mtime unch, no load needed.\n",
  			      getpid()))
  		return;
--- 75,81 ----
  	 * so is guaranteed to be different than the stat() mtime the first
  	 * time this function is called.
  	 */
! 	if (old_db->mtime == HASH(statbuf.st_mtime, syscron_stat.st_mtime)) {
  		Debug(DLOAD, ("[%d] spool dir mtime unch, no load needed.\n",
  			      getpid()))
  		return;
***************
*** 82,88 ****
  	 * actually changed.  Whatever is left in the old database when
  	 * we're done is chaff -- crontabs that disappeared.
  	 */
! 	new_db.mtime = TMAX(statbuf.st_mtime, syscron_stat.st_mtime);
  	new_db.head = new_db.tail = NULL;
  
  	if (syscron_stat.st_mtime) {
--- 86,92 ----
  	 * actually changed.  Whatever is left in the old database when
  	 * we're done is chaff -- crontabs that disappeared.
  	 */
! 	new_db.mtime = HASH(statbuf.st_mtime, syscron_stat.st_mtime);
  	new_db.head = new_db.tail = NULL;
  
  	if (syscron_stat.st_mtime) {
diff -c -r -b ./cron/do_command.c cron/NEW/cron/do_command.c
*** ./cron/do_command.c	Sun Sep 10 06:02:56 1995
--- cron/NEW/cron/do_command.c	Wed Apr 16 05:25:23 1997
***************
*** 388,394 ****
  					e->cmd);
  # if defined(MAIL_DATE)
  				fprintf(mail, "Date: %s\n",
! 					arpadate(&TargetTime));
  # endif /* MAIL_DATE */
  				for (env = e->envp;  *env;  env++)
  					fprintf(mail, "X-Cron-Env: <%s>\n",
--- 388,394 ----
  					e->cmd);
  # if defined(MAIL_DATE)
  				fprintf(mail, "Date: %s\n",
! 					arpadate(&StartTime));
  # endif /* MAIL_DATE */
  				for (env = e->envp;  *env;  env++)
  					fprintf(mail, "X-Cron-Env: <%s>\n",
diff -c -r -b ./lib/entry.c cron/NEW/lib/entry.c
*** ./lib/entry.c	Mon Aug 28 14:30:46 1995
--- cron/NEW/lib/entry.c	Wed Apr 16 08:30:14 1997
***************
*** 19,25 ****
  static char rcsid[] = "$Id: entry.c,v 1.4 1995/08/28 21:30:46 mpp Exp $";
  #endif
  
! /* vix 26jan87 [RCS'd; rest of log is in RCS file]
   * vix 01jan87 [added line-level error recovery]
   * vix 31dec86 [added /step to the from-to range, per bob@acornrc]
   * vix 30dec86 [written]
--- 19,27 ----
  static char rcsid[] = "$Id: entry.c,v 1.4 1995/08/28 21:30:46 mpp Exp $";
  #endif
  
! /*
!  * -mt 15Apr97 add HR_STAR and MIN_STAR [<thompson@tgsoft.com>]
!  * vix 26jan87 [RCS'd; rest of log is in RCS file]
   * vix 01jan87 [added line-level error recovery]
   * vix 31dec86 [added /step to the from-to range, per bob@acornrc]
   * vix 30dec86 [written]
***************
*** 153,158 ****
--- 155,161 ----
  			bit_nset(e->dom, 0, (LAST_DOM-FIRST_DOM+1));
  			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 {
  			ecode = e_timespec;
  			goto eof;
***************
*** 160,165 ****
--- 163,170 ----
  	} else {
  		Debug(DPARS, ("load_entry()...about to parse numerics\n"))
  
+ 	        if (ch == '*')
+ 		    e->flags |= MIN_STAR;
  		ch = get_list(e->minute, FIRST_MINUTE, LAST_MINUTE,
  			      PPC_NULL, ch, file);
  		if (ch == EOF) {
***************
*** 170,175 ****
--- 175,182 ----
  		/* hours
  		 */
  
+ 	        if (ch == '*')
+ 		    e->flags |= HR_STAR;
  		ch = get_list(e->hour, FIRST_HOUR, LAST_HOUR,
  			      PPC_NULL, ch, file);
  		if (ch == EOF) {

>Release-Note:
>Audit-Trail:
State-Changed-From-To: open->closed 
State-Changed-By: dougb 
State-Changed-When: Sun Nov 19 11:39:45 PST 2000 
State-Changed-Why:  

While your patch is very well done, the solutions to the 
problems which it addresses are by no means clear. In 
particular, for various reasons it may not be desirable 
to run a job anyway that got skipped by DST.  

The best advice is that given in crontab(5), namely don't 
schedule mission-critical jobs during that time period. 

http://www.freebsd.org/cgi/query-pr.cgi?pr=3314 
>Unformatted:
