#module Cron "Cron v1.11"
#pragma builtins
/*
			CRON.C

	This program is a mutation of the UNIX CRON for VMS.
	CRON (Commands Run On Notice), for those who don't know,
	is a sort of job scheduler.  It wakes up periodically
	and executes commands based on the current date/time.
	This makes it REAL handy for the harried system manager
	who has a lot of scutwork to do periodically.

	This version of CRON takes its input from SYS$INPUT.
	I decided on this approach for several reasons, the 
	primary being that it was the easiest.  Another was
	that this way you keep the commands and the procedure
	to run them together.  It also made testing the thing
	easier.

	Another difference 'tween this and other variants is
	the decision to read all the CRON command strings into
	dynaically acquired space at once, rather than reading
	a file every minute.  This avoids the overhead, I/O,
	etc., of the latter approach, and makes it possible
	to make the one minute timer pop.  

	Oh, yeah, this also uses my version of a "callable
	DCL Lexical function", F_EDIT.  Will eventually have
	most of the lexicals in this form.  This does exactly 
	what F$EDIT does, with the exception of actually 
	returning a string, and does it MUCH faster.

	Also, note the use of the "#pragma builtins", system
	services and RTL calls.  Not a UNIX-ism in the house,
	which is fine, since this is for VMS.  I HATE the
	"If it's C, it's gotta be UNIX" attitude, and, for
	the record, I think ANSI stinks for promulgating it.
	They didn't standardize	C, they standardized UNIX!  
	I wrote them several times about this and NEVER got a
	response; so much for the "public review"!

	OPERATION:
  	There are comment lines and command lines.  Comment lines
	are any lines the first character of which is a "!";
	they are totally ignored.

	Command lines have 6 fields, space delimited, as follows:

	min_spec hr_spec dom_spec mon_spec dow_spec command

	All fields MUST be present and LINES WITH ERRORS ARE 
	SIMPLY IGNORED. Each field (except command) has the
	following format:
		number
			Simply a number in the range appropriate
                        for that field. Matches only when field
			equals current time value for that field.
			example:   3
		number-number
			A range specification.  Matches if the 
			current time value for that field is within
			the specified range of values.  Ranges
			wrap around the end of their legal values, e.g.,
			"2-6" matches 2, 3, 4, 5, and 6, but "6-2"
			(using day-of-week) matches 6, 7, 1, and 2..
		num,num,num... 
			comma separated list of either of the
                        above. So 9,7,2-5,12, will match 
			2,3,4,5,7,9 and 12
		*	
			Asterisk is a wildcard, and matches anything.

	Valid values for each field are as follows:
		Minute		0-59
		hour		0-23
		day		1-31
		month		1-12
		dow		1-7	(1=sun,2=mon,etc.)

	an example command set follows:

0,5,10,15,20,25,30,35,40,45,50,55 * * * 2-6 submit/nolog sys$manager:quepridown
0 * * * * submit/nolog sys_node:job_sched
!  on the first of every month start new accounting file
0 0 1 * * set accounting/new_file
!  every week start new errorlog
0 0 * * 7 rename sys$errorlog:errlog.sys sys$errorlog:errlog.old
!  purge errlogs that are overly old
0 1 * * 7 purge/keep=3 sys$errorlog:errlog.old
! start Diskeeper on system disk only at night
0 18 * * * @sys$sysdevice:[diskeeper]diskeeper_startup zfa0:/multiple_passes=50
! stop the system disk Diskeeper in the morning
0 5 * * * @sys_node:dksys_shut
! clean up files in plotting scratch directories
0 1 * * * delete/noconfirm/before="today-14-" zfe3:[iplot]*.*;*
1 1 * * * delete/noconfirm/before="today-14-" zfe3:[oee]*.*;*
2 1 * * * delete/noconfirm/before="today-14-" zfe3:[cnuc_work]*.*;*
3 1 * * * delete/noconfirm/before="today-14-" zfe3:[wrk_vplot]*.*;*
4 1 * * * delete/noconfirm/before="today-14-" zfe3:[zeta]*.*;*
5 1 * * * delete/noconfirm/before="today-14-" zfe3:[plot]*.*;*
6 1 * * * delete/noconfirm/before="today-14-" zfe3:[geo]*.*;*
7 1 * * * delete/noconfirm/before="today-14-" zfe3:[oes]*.*;*



Version 1.1   First production version
        1.11   8-MAY-1991  Fix buglet regarding leap year.
                           Add logic to ensure correct number
                           of days in month
*/

#include "tools:lexicals.h"
#include descrip
#include ssdef
#include rms

typedef struct _cmnd {
			struct _cmnd *next, *prev;
			struct dsc$descriptor_s info;
		} NODE, *NODEPTR;

#define NULL ((NODEPTR) 0)

NODE header = { &header, &header, { 0, DSC$K_DTYPE_T, DSC$K_CLASS_D, 0 } };
NODEPTR hnode;

struct dsc$descriptor_s string = { 0, DSC$K_DTYPE_T, DSC$K_CLASS_D, 0 };
struct dsc$descriptor_s element = { 0, DSC$K_DTYPE_T, DSC$K_CLASS_D, 0 };
struct dsc$descriptor_s field = { 0, DSC$K_DTYPE_T, DSC$K_CLASS_D, 0 };
int status;

unsigned long del_1[2]; /* binary delta time for wakeup call */
$DESCRIPTOR(one_dsc,"0 00:01:00.00");  /* 1 minute delta time for wakeup call */
$DESCRIPTOR(nullstr,"");
$DESCRIPTOR(space, " ");
$DESCRIPTOR(dash,"-");
$DESCRIPTOR(comma,",");
$DESCRIPTOR(slash,"/");

 
main()
{
	unsigned short time_vec[8];
	unsigned short lenwrd;
	unsigned long timel[2], work, istatus;
	NODEPTR alloc_node();

	while(1)
	    {
		if ((istatus = LIB$GET_INPUT(&string,0,0)) == RMS$_EOF)
		    break;
		f_edit(&string,
		     &string,
		     ED_UNCOMMENT | ED_COMPRESS | ED_TRIM | ED_UPCASE);
		if (string.dsc$w_length < 12)
		    continue;
		hnode = alloc_node();
		STR$COPY_DX(&hnode->info, &string);
		_INSQUE(hnode, &header);
	    }

	/* Now set the base priority of Cron to 17 (yes, the real-time  */
        /* range).  The reason is to keep the timing accurate even against */
	/* something such as BACKUP.  If it doesn't work, I assume you don't */
	/* have this running with altpri for some reason.  */


#ifndef DEBUG
	SYS$SETPRI(0,0,17,0);                                
#endif
	/* set delta time between wakeups */
	SYS$BINTIM(&one_dsc,del_1);

	while (1)        /* main loop -- never exit this thing */
	    {
		t_tick();
		SYS$GETTIM(timel);
		LIB$DAY_OF_WEEK(timel,&work);	
		SYS$NUMTIM(time_vec,timel);
		time_vec[7] = (unsigned short) (work%7 + 1);
		if (time_vec[4] == 0) /* every hour, generate time stamp message */
		    timestamp(timel);
		cron_main(time_vec);
	    }
}

/*
	The workhorse function.   Executes commands based on comparisons
	of time between command records and the current time. The argument is 
	tha address of an array of 8 short ints: The array returned by
	the $NUMTIM service + one word containing the day of week number (1=Sunday,
	..., 7= Saturday).
*/

cron_main(cur_time)
    unsigned short cur_time[];		
{
	hnode = &header;
	while(hnode -> next != &header)
	    {
		hnode = hnode -> next;
		if ((status=check_line(cur_time, hnode)) > 0)
		    status = do_line(hnode);
		if (status < 0)     /* in error, so free it! */
		    {
			hnode = hnode -> prev;	/* backup one */
			free_node(hnode -> next);   /* and free the bad one */
		    }
 	    }
}

check_line(time, cmd)
    unsigned short time[];
    NODEPTR cmd;
{
	int monthlim[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
	int check;
        int leap;

	check = ((time[0]%400 == 0) || (time[0]%100 != 0 && time[0]%4 == 0)) && time[1] == 2;
	leap = monthlim[time[1]-1] + check;
        
	if ((check = match(0,0,59,time[4],cmd)) > 0 /* minutes match? */ 
            && (check = match(1,0,23,time[3],cmd)) > 0 /* hours match? */
	    && (check = match(2,1,leap,time[2],cmd)) > 0 /* day match? */
	    && (check = match(3,1,12,time[1],cmd)) > 0 /* month match? */
	    && (check = match(4,1,7,time[7],cmd)) > 0) /* day of week match? */
       		return (1);  /* We have liftoff! */
	return(check);  /* no, not yet */
}

/*
	separates the command from the rest of the line and 
	does it.
*/

do_line(cmd_node)
    NODEPTR cmd_node;
{
        int pos;
	struct dsc$descriptor_s *cmd;
	cmd = &cmd_node -> info;

	if ((status = STR$ELEMENT(&string,&5,&space,cmd)) == SS$_NORMAL)
	    {
		pos = STR$POSITION(cmd,&string,&11); /* 11 is least pos for a valid command */
		STR$RIGHT(&string,cmd,&pos);
#ifdef DEBUG
		LIB$PUT_OUTPUT(&string);
#else
		status = LIB$SPAWN(&string,0,0,&7,&nullstr);  /* spawn/nowait/nologi/nokeyp/nosym/process="" */
		if (!(status & 1) ) lib$signal(status);
#endif
		return(1);		
	    }
	else
	    return(errx(cmd_node,6));
}

/*
	return 1 if the crontab field number is a match for currrent
	date and time, 0 if not, and -1 on error.
*/

match(fldno,fldmin,fldmax,value,dsc)
    int fldno, fldmin, fldmax;
    unsigned short value;
    NODEPTR dsc;
{
	unsigned short val1, val2;
	int stt, cnt, pos;

 	if (STR$ELEMENT(&element,&fldno,&space,&dsc->info) !=  SS$_NORMAL)
	    return(errx(dsc));
	if ((stt = _LOCC('*', element.dsc$w_length,element.dsc$a_pointer)) != 0) /* there's an asterix */
	    if(stt != 1 || element.dsc$w_length != 1) /* good if that's all there is */
	        return(errx(dsc,fldno));
	    else
		return(1);
	cnt=0;
	while (1)
	    {
		stt = STR$ELEMENT(&field,&cnt,&comma,&element);
		if (stt == SS$_NORMAL)		
		    {   /* comma delimited list element: value or value-value */
			if ((stt = _LOCC('-', field.dsc$w_length, field.dsc$a_pointer)) == 0) /* no dash, just a value */
			    {
				if (OTS$CVT_TU_L(&field,&val1,2,1) != SS$_NORMAL || val1 < fldmin || val1 > fldmax)
				    return(errx(dsc,fldno));
				if (val1 == value)
				    return(1);
			    }
			else  /* ok, we have a range specifier */
			    {
				/* convert to string index */
				stt = field.dsc$w_length - stt + 1;
				if (stt == 1 || field.dsc$w_length < 3)  /* must be at least x-y and no "-x" */
				    return(errx(dsc,fldno));
		    		--stt;
				STR$LEFT(&string,&field,&stt);
				if (OTS$CVT_TU_L(&string,&val1,2,1) != SS$_NORMAL || val1 < fldmin || val1 > fldmax)
				    return(errx(dsc,fldno));
				stt += 2;
				STR$RIGHT(&string,&field,&stt);
				if (OTS$CVT_TU_L(&string,&val2,2,1) != SS$_NORMAL || val2 < fldmin || val2 > fldmax)
				    return(errx(dsc,fldno));
				if (val1 <= val2)	/* standard range */
				    return( (val1 <= value) & (value <= val2) );
				/* now it gets a little wierd, to guarantee wraparound */
				return( ((val1 <= value) & (value <= fldmax)) |
					((fldmin <= value) & (value <= val2)) );
			    }
			++cnt;  /* next field */
                    }
		else
		    return(0);		
	    }		
}	

/*
	calculate next 1-minute boundary time, schedule a wakeup,
	and hibernate till then.  Yes, I realize that if all the commands
	in the table take more than one minute, I'm in trouble.  But if 
	I've got that many, I'm in trouble anyway!
*/

t_tick()
{
	unsigned long next[2], work[2];
	unsigned short ct_work[7]; 

	SYS$GETTIM(next);
	LIB$ADD_TIMES(next,del_1,work);
	SYS$NUMTIM(ct_work,work);
	ct_work[6] = ct_work[5] = 0;
       	LIB$CVT_VECTIM(ct_work, next);
	SYS$SCHDWK(0,0,next,0);
	SYS$HIBER();
}

/*
	handles field errors.  Formats and prints a message,
	and returns a status that makes the main loop 
	deallocate the node, so we only see it once.
*/
errx(cmd,fldno)
    NODEPTR cmd;
    int fldno;
{	
	unsigned short outlen;
	char msgbuf[500];
	struct dsc$descriptor_s out = {500, DSC$K_DTYPE_T, DSC$K_CLASS_S, msgbuf};
	$DESCRIPTOR(errmess, "Following command has error in field !UL, will be ignored from now on:!/!AS");

	SYS$FAO(&errmess, &outlen, &out, fldno, &cmd->info);
	out.dsc$w_length = outlen;
	LIB$PUT_OUTPUT(&out);
	return(-1);
}

timestamp(time)
    unsigned long time[];
{	
	unsigned short outlen;
	char msgbuf[50];
	struct dsc$descriptor_s out = {50, DSC$K_DTYPE_T, DSC$K_CLASS_S, msgbuf};
	$DESCRIPTOR(actmess, "Cron active at !%D");

	SYS$FAO(&actmess, &outlen, &out, time);
	out.dsc$w_length = outlen;
	LIB$PUT_OUTPUT(&out);
}

/* 
	allocate a node for a command.  Note that errors here are FATAL!
*/

NODEPTR alloc_node()
{
 	int numbytes = sizeof(NODE), sts;
	NODEPTR	temp;

 	if ((sts = LIB$GET_VM(&numbytes,&temp)) != SS$_NORMAL)
	    LIB$STOP(sts);
	temp -> info = header.info;
	return(temp);
}
/*
	frees a command node.  Used when commands are in error and 
	need ignoring. free's  info first. Again, errors are fatal!
*/

free_node(target)
    NODEPTR target;
{
	int numbytes = sizeof(NODE), sts;
	NODEPTR	temp;

 	if (_REMQUE(target,&temp) != 0)
	    LIB$STOP(SS$_BADQUEUEHDR);
	STR$FREE1_DX(&temp->info);
	if((sts = LIB$FREE_VM(&numbytes, &temp)) != SS$_NORMAL)
	    LIB$STOP(sts);
}

