
/*
 * DATABASE.C
 *
 * Copyright 1994 Matthew Dillon (dillon@apollo.west.oic.com)
 * May be distributed under the GNU General Public License
 */

#include "defs.h"

Prototype void CheckUpdates(void);
Prototype void SynchronizeDir(const char *dirName);
Prototype int TestJobs(time_t t1, time_t t2);
Prototype void RunJobs(void);
Prototype int CheckJobs(void);

void SynchronizeFile(const  char *fileName);
void DeleteFile(const char *userName);
char *ParseField(char *userName, char *ary, int modvalue, int off, const char **names, char *ptr);
void FixDayDow(CronLine *line);

CronFile *FileBase;

const char *DowAry[] = {
    "sun",
    "mon",
    "tue",
    "wed",
    "thu",
    "fri",
    "sat",

    "Sun",
    "Mon",
    "Tue",
    "Wed",
    "Thu",
    "Fri",
    "Sat",
    NULL
};

const char *MonAry[] = {
    "jan",
    "feb",
    "mar",
    "apr",
    "may",
    "jun",
    "jul",
    "aug",
    "sep",
    "oct",
    "nov",
    "dec",

    "Jan",
    "Feb",
    "Mar",
    "Apr",
    "May",
    "Jun",
    "Jul",
    "Aug",
    "Sep",
    "Oct",
    "Nov",
    "Dec",
    NULL
};

void
CheckUpdates(void)
{
    FILE *fi;
    char buf[256];

    if ((fi = fopen(CRONUPDATE, "r")) != NULL) {
        remove(CRONUPDATE);
        while (fgets(buf, sizeof(buf), fi) != NULL) {
            SynchronizeFile(strtok(buf, " \t\r\n"));
        }
        fclose(fi);
    }
}

void
SynchronizeDir(const char *dirName)
{
    /*
     * Attempt to delete the database.  Note that we have to make a copy
     * of the string
     */

    for (;;) {
	CronFile *file;
	char *user;

	for (file = FileBase; file && file->cf_Deleted; file = file->cf_Next)
	    ;
	if (file == NULL)
	    break;
	user = strdup(file->cf_User);
	DeleteFile(user);
	free(user);
    }

    /*
     * Remove cron update file
     *
     * Re-chdir, in case directory was renamed & deleted, or otherwise
     * screwed up.
     *
     * scan directory and add associated users
     */

    remove(CRONUPDATE);
    if (chdir(CDir) < 0) {
        log9("unable to find %s\n", CDir);
        exit(20);
    }
    {
        DIR *dir;
        struct dirent *den;

        if ((dir = opendir("."))) {
            while ((den = readdir(dir))) {
                if (strchr(den->d_name, '.') != NULL)
                    continue;
		if (getpwnam(den->d_name))
		    SynchronizeFile(den->d_name);
		else
		    log(7, "ignoring %s\n", den->d_name);
            }
            closedir(dir);
        } else {
            log9("Unable to open current dir!\n");
            exit(20);
        }
    }
}

void
SynchronizeFile(const char *fileName)
{
    int maxEntries = MAXLINES;
    int maxLines;
    char buf[1024];

    if (strcmp(fileName, "root") == 0)
        maxEntries = 65535;
    maxLines = maxEntries * 10;

    if (fileName) {
	FILE *fi;

	DeleteFile(fileName);

        if ((fi = fopen(fileName, "r")) != NULL) {
	    struct stat sbuf;

	    if (fstat(fileno(fi), &sbuf) == 0 && sbuf.st_uid == DaemonUid) {
		CronFile *file = calloc(1, sizeof(CronFile));
		CronLine **pline;

		file->cf_User = strdup(fileName);
		pline = &file->cf_LineBase;

		while (fgets(buf, sizeof(buf), fi) != NULL && --maxLines) {
		    CronLine line;
		    char *ptr;

		    if (buf[0])
			buf[strlen(buf)-1] = 0;

		    if (buf[0] == 0 || buf[0] == '#' || buf[0] == ' ' || buf[0] == '\t')
			continue;

		    if (--maxEntries == 0)
		        break;

		    bzero(&line, sizeof(line));

		    if (DebugOpt)
			log9("User %s Entry %s\n", fileName, buf);

		    /*
		     * parse date ranges
		     */

		    ptr = ParseField(file->cf_User, line.cl_Mins, 60, 0, NULL, buf);
		    ptr = ParseField(file->cf_User, line.cl_Hrs,  24, 0, NULL, ptr);
		    ptr = ParseField(file->cf_User, line.cl_Days, 32, 0, NULL, ptr);
		    ptr = ParseField(file->cf_User, line.cl_Mons, 12, -1, MonAry, ptr);
		    ptr = ParseField(file->cf_User, line.cl_Dow, 7, 0, DowAry, ptr);

		    /*
		     * check failure
		     */

		    if (ptr == NULL)
			continue;

		    /*
		     * fix days and dow - if one is not * and the other
		     * is *, the other is set to 0, and vise-versa
		     */

		    FixDayDow(&line);

		    *pline = calloc(1, sizeof(CronLine));
		    **pline = line;

		    /*
		     * copy command
		     */

		    (*pline)->cl_Shell = strdup(ptr);

		    if (DebugOpt) {
			log9("    Command %s\n", ptr);
		    }

		    pline = &((*pline)->cl_Next);
		}
		*pline = NULL;

		file->cf_Next = FileBase;
		FileBase = file;

		if (maxLines == 0 || maxEntries == 0)
		    log9("Maximum number of lines reached for user %s\n", fileName);
	    }
	    fclose(fi);
	} 
    }
}

char *
ParseField(char *user, char *ary, int modvalue, int off, const char **names, char *ptr)
{
    char *base = ptr;
    int n1 = -1;
    int n2 = -1;

    if (base == NULL)
    	return(NULL);

    while (*ptr != ' ' && *ptr != '\t' && *ptr != '\n') {
        int skip = 0;

	/*
	 * Handle numeric digit or symbol or '*'
	 */

        if (*ptr == '*') {
	    n1 = 0;			/* everything will be filled */
	    n2 = modvalue - 1;
            skip = 1;
            ++ptr;
	} else if (*ptr >= '0' && *ptr <= '9') {
	    if (n1 < 0)
	        n1 = strtol(ptr, &ptr, 10) + off;
	    else
	        n2 = strtol(ptr, &ptr, 10) + off;
	    skip = 1;
	} else if (names) {
	    int i;

	    for (i = 0; names[i]; ++i) {
	        if (strncmp(ptr, names[i], strlen(names[i])) == 0) {
	            break;
	        }
	    }
	    if (names[i]) {
	        ptr += strlen(names[i]);
	        if (n1 < 0)
	            n1 = i;
		else
		    n2 = i;
		skip = 1;
	    }
	}

	/*
	 * handle optional range '-'
	 */

	if (skip == 0) {
	    log9("failed user %s parsing %s\n", user, base);
	    return(NULL);
	}
	if (*ptr == '-' && n2 < 0) {
	    ++ptr;
	    continue;
	}

	/*
	 * collapse single-value ranges, handle skipmark, and fill
	 * in the character array appropriately.
	 */

	if (n2 < 0)
	    n2 = n1;

	if (*ptr == '/')
	    skip = strtol(ptr + 1, &ptr, 10);

	/*
	 * fill array, using a failsafe is the easiest way to prevent
	 * an endless loop
	 */

        {
            int s0 = 1;
	    int failsafe = 1024;

            --n1;
            do {
                n1 = (n1 + 1) % modvalue;

	        if (--s0 == 0) {
		    ary[n1 % modvalue] = 1;
		    s0 = skip;
		}
	    } while (n1 != n2 && --failsafe);

	    if (failsafe == 0) {
		log9("failed user %s parsing %s\n", user, base);
		return(NULL);
	    }
	}
	if (*ptr != ',')
	    break;
	++ptr;
	n1 = -1;
	n2 = -1;
    }

    if (*ptr != ' ' && *ptr != '\t' && *ptr != '\n') {
	log9("failed user %s parsing %s\n", user, base);
        return(NULL);
    }

    while (*ptr == ' ' || *ptr == '\t' || *ptr == '\n')
        ++ptr;

    if (DebugOpt) {
        int i;

        for (i = 0; i < modvalue; ++i)
            log(5, "%d", ary[i]);
	log(5, "\n");
    }

    return(ptr);
}

void
FixDayDow(CronLine *line)
{
    short i;
    short weekUsed = 0;
    short daysUsed = 0;

    for (i = 0; i < arysize(line->cl_Dow); ++i) {
        if (line->cl_Dow[i] == 0) {
            weekUsed = 1;
            break;
	}
    }
    for (i = 0; i < arysize(line->cl_Days); ++i) {
        if (line->cl_Days[i] == 0) {
            daysUsed = 1;
            break;
	}
    }
    if (weekUsed && !daysUsed) {
        memset(line->cl_Days, 0, sizeof(line->cl_Days));
    }
    if (daysUsed && !weekUsed) {
        memset(line->cl_Dow, 0, sizeof(line->cl_Dow));
    }
}

/*
 *  DeleteFile() - delete user database
 *
 *  Note: multiple entries for same user may exist if we were unable to 
 *  completely delete a database due to running processes.
 */

void
DeleteFile(const char *userName)
{
    CronFile **pfile = &FileBase;
    CronFile *file;

    while ((file = *pfile) != NULL) {
    	if (strcmp(userName, file->cf_User) == 0) {
    	    CronLine **pline = &file->cf_LineBase;
    	    CronLine *line;

    	    file->cf_Running = 0;
    	    file->cf_Deleted = 1;

    	    while ((line = *pline) != NULL) {
    	        if (line->cl_Pid > 0) {
    	            file->cf_Running = 1;
    	            pline = &line->cl_Next;
		} else {
		    *pline = line->cl_Next;
		    free(line->cl_Shell);
		    free(line);
		}
	    }
	    if (file->cf_Running == 0) {
		*pfile = file->cf_Next;
		free(file->cf_User);
		free(file);
	    } else {
	        pfile = &file->cf_Next;
	    }
    	} else {
    	    pfile = &file->cf_Next;
    	}
    }
}

/*
 * TestJobs()
 *
 * determine which jobs need to be run.  Under normal conditions, the
 * period is about a minute (one scan).  Worst case it will be one
 * hour (60 scans).
 */

int
TestJobs(time_t t1, time_t t2)
{
    short nJobs = 0;
    time_t t;

    /*
     * Find jobs > t1 and <= t2
     */

    for (t = t1 - t1 % 60; t <= t2; t += 60) {
	if (t > t1) {
	    struct tm *tp = localtime(&t);
	    CronFile *file;
	    CronLine *line;

	    for (file = FileBase; file; file = file->cf_Next) {
		if (DebugOpt)
		    log(5, "FILE %s:\n", file->cf_User);
		if (file->cf_Deleted)
		    continue;
		for (line = file->cf_LineBase; line; line = line->cl_Next) {
		    if (DebugOpt)
		        log(5, "    LINE %s\n", line->cl_Shell);
		    if (line->cl_Mins[tp->tm_min] &&
		        line->cl_Hrs[tp->tm_hour] &&
		        (line->cl_Days[tp->tm_mday] || line->cl_Dow[tp->tm_wday]) &&
		        line->cl_Mons[tp->tm_mon]
		    ) {
		        if (DebugOpt)
		            log(5, "    JobToDo: %d %s\n", line->cl_Pid, line->cl_Shell);
		        if (line->cl_Pid > 0) {
		            log(8, "    process already running: %s %s\n", 
		                file->cf_User,
		                line->cl_Shell
		            );
		        } else if (line->cl_Pid == 0) {
		            line->cl_Pid = -1;
		            file->cf_Ready = 1;
		            ++nJobs;
		        }
		    }
		}
	    }
	}
    }
    return(nJobs);
}

void
RunJobs(void)
{
    CronFile *file;
    CronLine *line;

    for (file = FileBase; file; file = file->cf_Next) {
	if (file->cf_Ready) {
	    file->cf_Ready = 0;

	    for (line = file->cf_LineBase; line; line = line->cl_Next) {
		if (line->cl_Pid < 0) {

		    RunJob(file, line);

		    log(8, "USER %s pid %3d cmd %s\n",
			file->cf_User,
			line->cl_Pid,
			line->cl_Shell
		    );
		    if (line->cl_Pid < 0)
		        file->cf_Ready = 1;
		    else if (line->cl_Pid > 0)
			file->cf_Running = 1;
		}
	    }
	}
    }
}

/*
 * CheckJobs() - check for job completion
 *
 * Check for job completion, return number of jobs still running after
 * all done.
 */

int
CheckJobs(void)
{
    CronFile *file;
    CronLine *line;
    int nStillRunning = 0;

    for (file = FileBase; file; file = file->cf_Next) {
	if (file->cf_Running) {
	    file->cf_Running = 0;

	    for (line = file->cf_LineBase; line; line = line->cl_Next) {
		if (line->cl_Pid > 0) {
		    int status;
		    int r = wait4(line->cl_Pid, &status, WNOHANG, NULL);

		    if (r < 0 || r == line->cl_Pid) {
		        EndJob(file, line);
		        if (line->cl_Pid)
			    file->cf_Running = 1;
		    } else if (r == 0) {
		        file->cf_Running = 1;
		    }
		}
	    }
	}
	nStillRunning += file->cf_Running;
    }
    return(nStillRunning);
}

