
/*
 *  DCRON.C  V1.05
 *
 *  Limitations:    The only RUN commands allowed are of the form:
 *
 *		    RUN >nil: <nil: <command>
 *
 *  -Wakeup once every minute & check the modify date for S:CRONTAB
 *   and to check if the DateStamp() has been re-set.
 *
 *  -Attempt to be smart about checking s:crontab (scan the file
 *   as little as possible).
 *
 *  -Handles massive Date changes and crontab modifications
 *
 *  DCRON [-d] logfile
 */

#include <stdio.h>
#include <local/typedefs.h>

#define CRONTAB "s:crontab"
#define SIGS	(SIGBREAKF_CTRL_C|SIGBREAKF_CTRL_D|SIGBREAKF_CTRL_E|SIGBREAKF_CTRL_F)

#define ACTION_READWRITE    1004
#define ACTION_FIND_INPUT   1005	    /* various ACTION's supported   */
#define ACTION_FIND_OUTPUT  1006
#define ACTION_END	    1007
#define ACTION_EXAMINE	    23
#define ACTION_EXAMINENEXT  24
#define ACTION_LOCATE	    8
#define ACTION_FREELOCK     15
#define ACTION_COPYDIR	    19

#define DOS_FALSE    0
#define DOS_TRUE     -1

#define BTOC(bptr)  ((void *)((long)(bptr) << 2))

typedef struct FileHandle FH;

typedef struct {
    short min;
    short hour;
    short day;
    short month;
    short dow;
} MHDMW;

typedef struct {
    short entry[5];
} MHDMWARY;

typedef struct {
    MHDMW   Beg;
    MHDMW   End;
} CDATE;

typedef struct {
    char min[60];
    char hour[24];
    char day[32];
    char month[13];
    char dow[7];
} CARRY;

extern	int	Enable_Abort;
extern	APTR	DeviceProc();

DATESTAMP   BegDate;	    /*	Start and end range for     */
DATESTAMP   EndDate;	    /*	 file compare		    */
DATESTAMP   ModDate;	    /*	Check if date modified	    */
DATESTAMP   TabDate;	    /*	Check if crontab modified   */

IOT	Iot;		    /*	Best guess at next timeout		*/
IOT	Iotmin; 	    /*	1 minute T.O. (date/file modified test  */
PORT	*TPort; 	    /*	Collector plate for IO requests 	*/
short	FatalError;	    /*	Uh oh, can't recover                    */
short	NextTimeout;	    /*	# minutes till next timeout		*/
char	*LogFile;
char	XDebug;
long	NilFH;
short	CronFileExists = 1; /*	Does the crontab file exist?		*/

void	logmessage();

void
main(ac, av)
short ac;
char *av[];
{
    PROC *proc = FindTask(NULL);

    Enable_Abort = 0;
    {
	register short i;
	short j = 0;

	for (i = 1; i < ac; ++i) {
	    register char *ptr = av[i];
	    if (*ptr != '-') {
		switch(j++) {
		case 0:
		    LogFile = ptr;
		    break;
		}
		continue;
	    }
	    while (*++ptr) {
		switch(*ptr) {
		case 'd':
		    ++XDebug;
		    break;
		default:
		    WriteStr(Output(), "bad option\n");
		    goto fail;
		}
	    }
	}
    }
    if (!LogFile) {
fail:
	WriteStr(Output(), "DCron [-d] Logfile\n");
	WriteStr(Output(), "DCron, V1.05\n");
	exit(1);
    }
    if (OpenDevice("timer.device", 0, &Iot, UNIT_VBLANK)) {
	logmessage("Unable to open timer.device\n");
	exit(1);
    }
    if (!DeviceProc("NULL:")) {
	logmessage("NULL: device required for dcron to run\n");
	WriteStr(Output(), "NULL: device required to run\n");
	exit(1);
    }
    proc->pr_ConsoleTask = DeviceProc("NULL:");
    fclose(stderr);
    logmessage("Startup: V1.05 (dist: 22 December 1988)\n");

    NilFH = Open("null:", 1006);
    DateStamp(&EndDate);
    DateStamp(&ModDate);
    TPort = CreatePort(NULL,0);
    Iot.tr_time.tv_secs = 1;
    Iot.tr_time.tv_micro= 0;
    Iot.tr_node.io_Message.mn_ReplyPort = TPort;
    Iot.tr_node.io_Command = TR_ADDREQUEST;
    Iotmin = Iot;
    SendIO(&Iot);
    SendIO(&Iotmin);
    for (;;) {
	long mask;
	mask = Wait(SIGS | (1 << TPort->mp_SigBit));
	if (mask & (SIGBREAKF_CTRL_C|SIGBREAKF_CTRL_D)) {
	    logmessage("DCRON: Break\n");
	    break;
	}
	if (mask & (SIGBREAKF_CTRL_E|SIGBREAKF_CTRL_F)) {
	    logmessage("^E/F force scan\n");
	    AbortIO(&Iot);          /*  force execution                     */
	}
	if (FatalError)
	    break;
	if (CheckIO(&Iotmin)) {     /*  if file/date modified, force exec.  */
	    WaitIO(&Iotmin);
	    if (datemod() + filemod())
		AbortIO(&Iot);
	    Iotmin.tr_time.tv_secs = 60;
	    Iotmin.tr_time.tv_micro= 0;
	    SendIO(&Iotmin);
	}
	if (CheckIO(&Iot)) {
	    DATESTAMP Ds;
	    short secmin;

	    WaitIO(&Iot);
	    dostuff();
	    if (NextTimeout <= 0)
		NextTimeout = 1;
	    if (XDebug) {
		logmessage(" Next Timeout in %02ld:%02ld\n",
		    NextTimeout / 60,
		    NextTimeout % 60
		);
	    }
	    DateStamp(&Ds);
	    secmin = 61 - (Ds.ds_Tick / 50);    /*  1+Secs till next min. */
	    Iot.tr_time.tv_secs = secmin + (NextTimeout - 1) * 60;
	    Iot.tr_time.tv_micro= 0;
	    SendIO(&Iot);
	}
    }
    AbortIO(&Iot);
    AbortIO(&Iotmin);
    WaitIO(&Iot);
    WaitIO(&Iotmin);
    CloseDevice(&Iot);
    DeletePort(TPort);
    Close(NilFH);
}

/*
 *  If the current date is less than the previous date
 *  If the current date is more than +5 minutes the previous date
 */

datemod()
{
    DATESTAMP Date;
    long xold, xnew;

    DateStamp(&Date);
    xold = ModDate.ds_Days * 1440 + ModDate.ds_Minute;
    xnew = Date.ds_Days * 1440 + Date.ds_Minute;
    ModDate = Date;
    if (xnew < xold || xnew - 5 > xold) {
	DateStamp(&EndDate);
	logmessage("Date change noted\n");
	return(1);
    }
    return(0);
}

/*
 *  If the file modification time is different than when
 *  we last checked, we want to recalculate our timeout
 *  and rescan it.
 */

filemod()
{
    char buf[sizeof(FIB)+4];
    register FIB *fib = (FIB *)(((long)buf+3)&~3);
    long lock;
    long result = 0;

    if (lock = Lock(CRONTAB, SHARED_LOCK)) {
	if (Examine(lock, fib)) {
	    if (fib->fib_Date.ds_Tick   != TabDate.ds_Tick ||
		fib->fib_Date.ds_Minute != TabDate.ds_Minute ||
		fib->fib_Date.ds_Days	!= TabDate.ds_Days) {

		if (TabDate.ds_Days) {
		    logmessage("crontab modification noted\n");
		    result = 1;
		}
		TabDate = fib->fib_Date;
	    }
	}
	UnLock(lock);
    }
    return(result);
}

/*
 *  DOSTUFF()
 *
 *  Scan the CRONTAB and execute any entries that fall between
 *  BegDate and EndDate.  Specifically, >= BegDate and < EndDate.
 *
 *  This is done as little as possible.  While scanning, we calculate the
 *  timeout period till the next entry and will attempt to sleep for that
 *  period of time before comming back to this routine.  Checks are still
 *  done every 60 seconds for radical date changes and crontab modification
 *  (routines above).
 *
 *  The calculation of the timeout period is not entirely accurate.
 *  Specifically, in cases similar to:
 *
 *	"30 16 * * *"
 *
 *  If the current time is 16:xx, the timeout period calculated for the hours
 *  will 0 instead of 24 (if the minutes had been '*', this would be
 *  correct.  But if it is 16:31 the system will wait 29 minutes instead of
 *  23 hours and 30 minutes.  Of course, it will re-scan at +29 minutes and
 *  not find anything to do, but you get the point.
 */

static char dim[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
static CARRY CArry;
static short CRanStart[] = {  0,  0,  1  ,  1, 0 };
static short CRanEnd[]	 = { 59, 23, 31  , 12, 6 };
static long  CWeight[]	 = {  1, 60, 1440, 28*1440, 1440 };
static char *CBase[]	 = { CArry.min, CArry.hour, CArry.day, CArry.month, CArry.dow };

dostuff()
{
    CDATE cdate;
    long fh;
    char buf[256];
    int ie;

    NextTimeout = -1;

    BegDate = EndDate;
    DateStamp(&EndDate);
    breakup(&EndDate, &cdate.End);          /*  Breakup the time            */
    breakup(&BegDate, &cdate.Beg);          /*  note: dim[FEB] is modified  */
    bzero(&CArry, sizeof(CArry));           /*  range array                 */

    /*
     *	Generate the range for the delta time that has occured since the
     *	last reading.  (A) This is used to ensure we don't miss anything
     *		       (B) Ensure we don't repeat anything
     */

    ie = makerange(CArry.min  , 0, 59, cdate.Beg.min  , cdate.End.min  ,  1);
    ie = makerange(CArry.hour , 0, 23, cdate.Beg.hour , cdate.End.hour , ie);
    ie = makerange(CArry.day  , 1, dim[cdate.Beg.month-1],
				       cdate.Beg.day  , cdate.End.day  , ie);
    ie = makerange(CArry.month, 1, 12, cdate.Beg.month, cdate.End.month, ie);
    ie = makerange(CArry.dow  , 0,  6, cdate.Beg.dow  , cdate.End.dow  , ie);

    /*
     *	Note:	if ie is set after this then dostuff() was called
     *		within the same minute segment and would yield a
     *		repeation of commands.
     */

    if (ie == 0) {
	ReadLn(NULL, NULL, 0);
	if (fh = Open(CRONTAB, 1005)) {
	    long whennext;				/*  in minutes, estimate    */

	    if (CronFileExists == 0) {
		CronFileExists = 1;
		logmessage("DCron File %s exists\n", CRONTAB);
	    }
	    while (ReadLn(fh, buf, 256)) {
		if (buf[0] == 0 || buf[0] == '#')
		    continue;
		whennext = handleline(buf, &cdate.End); /*  take smallest           */
		if (XDebug > 1)
		    logmessage("whennext == %ld\n", whennext);
		if (NextTimeout < 0 || whennext < NextTimeout)
		    NextTimeout = whennext;
	    }
	    Close(fh);
	} else {
	    if (CronFileExists == 1) {
		CronFileExists = 0;
		logmessage("Unable to open %s\n", CRONTAB);
	    }
	}
    }

    /*
     *	You can't trust the Amiga's clock vs the timer.device ... this is to
     *	ensure that they don't get too far off... no more than an hour timeout
     *	is allowed.  Frankly, anything above 30 minutes will yield unnoticeable
     *	results.
     */

    if (--NextTimeout > 60)
	NextTimeout = 60;
}

breakup(date, mhd)
DATESTAMP *date;
MHDMW *mhd;
{
    long days;
    long years;
    char leap;
    short month;

    days = date->ds_Days + 731; 	    /*	1976	    */
    years = days / (365*3+366);             /*  #quad yrs   */
    days -= years * (365*3+366);
    leap = (days < 366);                    /*  is a leap yr*/
    years = 1976 + 4 * years;
    dim[1] = 29;
    if (!leap) {
	dim[1] = 28;
	days -= 366;
	++years;
    }
    years += days / 365;
    days -= (days / 365) * 365;
    for (month = 0; (month==1) ? (days >= 28 + leap) : (days >= dim[month]); ++month)
	days -= (month==1) ? (28 + leap) : dim[month];
    mhd->min	= date->ds_Minute % 60;
    mhd->hour	= date->ds_Minute / 60;
    mhd->day	= days + 1;
    mhd->month	= month + 1;
    mhd->dow	= date->ds_Days % 7;	/*  0 = sunday	 */
}

/*
 *  S < RANGE <= E	(if previous entries were equal)
 *	    note: if S == E, E is still included
 *  S <= RANGE <= E	(if previous entries were not equal)
 */

makerange(ptr, cs, ce, s, e, isequal)
register char *ptr;
{
    long error = 5000;
    short oldisequal = isequal;

    isequal = (isequal && s == e);
    if (oldisequal && s != e) {
	if (++s > ce)
	    s = cs;
    }
    while (--error && s != e) {
	ptr[s++] = 1;
	if (s > ce)
	    s = cs;
    }
    ptr[e] = 1;
    if (error == 0) {
	logmessage("DCRON,makerange(): software error\n");
	FatalError = 1;
    }
    return(isequal);
}

/*
 *  This is the core of the routine.  The fields are interpreted, times
 *  estimated, ready entries run, etc..
 *
 *  order:  minutes hours days months dayofweek
 *
 *  Each field may be a * indicating all-times, and a combination of a
 *  range and comma delimited numbers:	0-3,45,46-49	    (MUST BE SORTED)
 *
 *  Each field is separated by one or spaces+tabs
 */

handleline(buf, endt)
char *buf;
MHDMWARY *endt;
{
    register char *ptr = buf;
    register short i;
    char *gnum();
    short sumok = 0;
    long howsoon = 0;		    /*	how soon until next entry */

    for (i = 0; i < 5; ++i) {       /*  5 arguments     */
	char ok = 0;
	short nextunit = -1;	    /*	count, in units */

	while (*ptr == ' ' || *ptr == 9)
	    ++ptr;
	while (*ptr && *ptr != 9 && *ptr != ' ') {
	    short start;
	    short finish;
	    if (*ptr == '*') {
		++ptr;
		ok = 1;
		break;
	    }
	    ptr = gnum(ptr, &start);
	    finish = start;
	    if (*ptr == '-')
		ptr = gnum(ptr+1, &finish);
	    if (*ptr == ',')
		++ptr;

	    /*
	     *	Determine if current date is within time range.  nextunit
	     *	is the number of time units for this index till the next
	     *	entry will be executed.
	     */

	    if (endt->entry[i] < start) {
		register short newnextunit = start - endt->entry[i];
		if (nextunit < 0 || newnextunit < nextunit)
		    nextunit = newnextunit;
	    } else
	    if (endt->entry[i] > finish) {
		register short newnextunit = 1 + CRanEnd[i] - endt->entry[i] + start - CRanStart[i];
		if (nextunit < 0 || newnextunit < nextunit)
		    nextunit = newnextunit;
	    } else
		nextunit = 0;  /*  Inside time range   */

	    if (start < CRanStart[i] || finish < CRanStart[i] || start > CRanEnd[i] || finish > CRanEnd[i]) {
		logmessage("Illegal Bounds in: %s\n", buf);
		logmessage(" Value %ld & %ld bounds %ld-%ld ", start, finish, CRanStart[i], CRanEnd[i]);
		logmessage("  AT: '%s'\n", ptr);
		continue;
	    } else {
		do {
		    if (start > CRanEnd[i])
			start = CRanStart[i];
		    if (CBase[i][start]) {
			ok = 1;
			break;
		    }
		    ++start;
		} while (start != finish + 1);
	    }
	}
	if (ok)
	    ++sumok;
	if (nextunit > 0)
	    howsoon += nextunit * CWeight[i];
    }

    /*
     *	This is tricky.  Since this program also handles the dummy-null
     *	device, it CANNOT block on the execute.  The only way to do that
     *	is to run <nil: >nil: so run itself does not attempt to access
     *	"*".  It is ok if the program run runs accesses "*" as we will not
     *	be frozen then.
     */

    if (sumok == i) {
	register char *p2 = malloc(strlen(ptr)+32);

	logmessage("%s\n", buf);
	while (*ptr == ' ' || *ptr == 9)
	    ++ptr;
    /*	  strcpy(p2, "run <nil: >nil: ");   */
	strcpy(p2, "run ");
	strcat(p2, ptr);
	Execute(p2, NilFH, NilFH);
    }
    if (XDebug > 2) {
	logmessage("FOR %-20s   In %ld %02ld:%02ld\n",
	    buf, howsoon / 1440, howsoon / 60 % 24, howsoon % 60
	);
    }
    return(howsoon);
}

/*
 *  Poor man's log.  Note that the log file is not left open ... this allows
 *  one to read or tail it at any time.
 */

void
logmessage(ptr, a, b, c, d, e)
char *ptr;
{
    char *buf = malloc(512);
    DATESTAMP date;
    MHDMW   cb;
    long    fh;
    static char *Dow[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
    static char *Miy[] = { "---", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
			   "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };

    if (!buf)
	return;

    DateStamp(&date);
    breakup(&date, &cb);

    sprintf(buf, "dcron: %s %2ld %s %02ld:%02ld   ",
	Dow[cb.dow], cb.day, Miy[cb.month], cb.hour, cb.min
    );
    sprintf(buf+strlen(buf), ptr, a, b, c, d, e);
    if ((fh = Open(LogFile, 1005)) == NULL)
	fh = Open(LogFile, 1006);
    if (fh) {
	Seek(fh, 0L, 1);
	WriteStr(fh, buf);
	Close(fh);
    }
    free(buf);
}

/*
 *  Scan a string for a value, return the new pointer position and set the
 *  specified short pointer to the value.
 */

char *
gnum(ptr, pval)
register char *ptr;
register short *pval;
{
    short val = 0;
    if (*ptr < '0' || *ptr > '9')
	logmessage("Not a number: %s\n", ptr);
    while (*ptr >= '0' && *ptr <= '9') {
	val = val * 10 + *ptr - '0';
	++ptr;
    }
    *pval = val;
    return(ptr);
}

WriteStr(fh, buf)
long fh;
char *buf;
{
    Write(fh, buf, strlen(buf));
}

ReadLn(fh, buf, max)
long fh;
char *buf;
short max;
{
    static char Data[1024];
    static short RIdx, RLen;
    register short i;

    if (fh == NULL) {
	RIdx = RLen = 0;
	return(0);
    }
    for (--max, i = 0; i < max; ++i) {
	if (RIdx == RLen) {
	    RLen = Read(fh, Data, 1024);
	    RIdx = 0;
	    if (RLen <= 0) {
		buf[i] = 0;
		return(0);
	    }
	}
	if ((buf[i] = Data[RIdx++]) == '\n')
	    break;
    }
    buf[i] = 0;
    return(1);
}

