// cronsched - Print a formatted schedule of cronjobs to be run in the
//             specified time period

// COPYRIGHT(C) 1997 Dennis Lovelady
//                   http://www.lovelady.com/mailform/

// August, 2000 - Squashed bug associated with date change and time zones
//              corrected.  Thanks to Bob Vance for pointing this out.

// August, 2000 - Added capabaility of compiling without threadsafe routines
//                (Some compilers lack these libraries)

// November, 2001 - Discovered a bug that caused crash.  Problem was that
//                  a malloc() followed by strcpy did not account for the
//                  0x00 string terminator.
//                      Thanks to Larry Hatfield for pointing out the problem!

// This program will process the crontab entries for a given user or list of
// users and produce a printed report showing a sort of job schedule.  All of
// cron's valid syntax rules are accepted by this program.  The resultant
// report is ordered by date and time of execution.

// It ain't pretty, but it works.

// Basically, what happens is that the crontab entries are encoded into
// tables for month, for day, for weekday, for hour, and for minute.  If a
// job is to run on Sundays, for example, there will be a "1" entry in the 
// "Sunday" element of the weekday table.  A job that is to run when day-of-
// month equals 10, will have an entry in that element, etc.
// When the report is produced, all possible minutes from the report start-time
// to the report end-time will be checked.  When the appropriate table element
// entries evaluate to 1, the item is added to the report.

// There are probably ways that could be used to reduce the CPU requirement of
// this program, but it turns out not to be that costly after all.  I wouldn't
// want to run a report for, let's say, a whole century :^) - but that would be
// outside the design of this program (although it'll handle it).

// By default, this program will produce a report of the cronjobs that are to
// be run for the next 24 hours.

// OK, so I'm lazy... for a usage report, compile and enter:
//    progname -help


// #define THREADSAFE 1  /* Comment out for non-threadsafe (usualy OK) */
#include <ctype.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <libgen.h>

#ifndef TRUE
  #define TRUE (0==0)
  #define FALSE (0==1)
#endif

// Constants:
//      MAX_FILES = the maximum number of crontab files that can be processed
//      MAX_CMDS  = the maximum number of cronjobs that can be processed in
//                  one run
//      DFT_DIR   = The directory where crontab files are stored by default
//      PAGE_WID  = The maximum width of a print page

#define MAX_FILES 100
#define MAX_CMDS  1000
#define DFT_DIR   "/var/spool/cron" // /var/spool/cron/crontabs"
#define PAGE_WID  80
#define MIN(a,b) (a)<(b) ? (a) : (b)
#define MAX(a,b) (a)>(b) ? (a) : (b)

// Values for printing month and weekday names

char *Wdays[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
char *Mths[]  = {"Jan","Feb","Mar","Apr","May","Jun",
                "Jul","Aug","Sep","Oct","Nov","Dec"};

char *CronDir=DFT_DIR ;		// Default crontabs directory

typedef struct
    {

    // This structure occurs once for each cronjob.  The arrays
    // (Minutes, etc) contain zeros for entries that the job is
    // ineligible to run, and 1 for entries that it is eligible 
    // to run.

    short FileNbr;
    char  Minutes[60];
    char  Hours[24];
    char  Days[31];
    char  Months[12];
    char  Weekdays[7];
    char  *Cmd;
    } FormattedCmd_t;


FormattedCmd_t *pCmd[MAX_CMDS] ;		// Array of cronjob structs
char LineBuffer[2048], *pLineBuffer=LineBuffer ;// Input line from crontabs
char *FileName[MAX_FILES] = {NULL,NULL};	// cronjob filename
time_t StartTime, StopTime;			// Report start & stop time
int NbrJobs=0, NbrFiles=0;			// Totals for report
short PageWidth=PAGE_WID;			// Print width


short GetWord(char *buffer)
    {

    // This function will process a "word" from the crontab file.  GetWord
    // is called to decipher the crontab entries for Month, Day, Hour, Minute,
    // and Weekday.  For simple numbers, the number is returned; for ranges,
    // sets, and "*" a -1 is returned, indicating further processing is
    // necessary.

    short rc = 0, done=FALSE, o=0;

    while (isspace(*pLineBuffer))
        pLineBuffer++;				// Skip leading spaces
    while (! done)
        {
        buffer[o++] = *pLineBuffer ;		// Next character
        switch (*pLineBuffer) 
            {
            case 0x00: done = TRUE; break;	// End of line.  Done.
            case '*':  rc = -1;			// Asterisk found.
			// Asterisk found.  Skip any other characters in this
			// "word" (there shouldn't be any), and return -1,
			// indicating that further processing for this field
			// is necessary.
                       while (! isspace(*(++pLineBuffer)))
			  {} ;
                       done = TRUE;
                       break;
            default:
                if (! isdigit(*pLineBuffer))	// Non-numeric found
                    rc = -1;			// Indicate further processing
                break;
            }					// switch()
        if (! done)
            {
            ++pLineBuffer;
            if (isspace(*pLineBuffer))
                done = TRUE;
            }					// Stop when space found
        }					// while not done
    if (strlen(buffer) < 1)			// No data found.
        rc = -2; 					// Error
    if (rc == 0)				// All numeric.
        rc = atoi(buffer);				// Return number
    return rc ;
    }



short CipherValue(char *array, short nMin, short nMax)
    {

    // CipherValue is called upon to fill in the appropriate 1's for the
    // array currently being processed.  The nMin and nMax values indicate
    // how many elements are available in the current array (for example,
    // the weekday array has nMin=0 and nMax=6).  GetWord() is called to
    // perform a minimal check on the current value.  If GetWord returns
    // a value >= 0, the corresponding array value is set to 1.  -2,
    // remember, indicates an error.  For -1 return from GetWord, the
    // complex expression is evaluated here, and the appropriate elements
    // are set to 1, if specified, or 2 if "*".

    char word[256], *w=word, oper;
    short rc = 0, m=0, prev_m=99, i, done;

    for (m=nMin; m<=nMax; m++) array[m] = 0;    /* Initialize             */
    memset(word, 0x00, sizeof(word));
    rc = GetWord(word);    /* Get the next parameter value        */

    if (rc >= 0)
        {
                               // rc will contain the numeric value,
                               // if the parameter value is a simple
                               // number.  Otherwise, rc will contain
                               // -1.
        if ((rc > nMax) ||     // Value too high for this parameter.
            (rc < nMin))       // Value too low for this parameter.
            rc = -1;           // Error.
        else                   // Contains specific value
            {
            array[rc] = 1;     // Set this array element to 1.
            rc = 0;
            }
        }
    else if (rc == -1)         // Special value.  (-2=error)
        {
	if (word[0] == '*')    // Set all values
            {
            for (m=nMin; m<=nMax; m++)
                array[m] = 2 ;
            rc = 0;
            }
        else		       // Handle ranges/sets of values
            {
            done = FALSE;
            while (! done)
                {
                m=atoi(w);
                if ((m >= nMin) && (m <= nMax))
                    {
                    array[m] = 1;
                    prev_m = m;
                    }
                while (isdigit(*w))
                    w++;
                oper=*w;
                w++;
                switch (oper)
                    {
                    case 0x00: rc=0; done=TRUE; break;
                    case ',':  rc=0; break;
                    case '-':
                        m = atoi(w);
                        if ((m < prev_m) || (m > nMax))
                            {
                            rc = -1;
                            done=TRUE;
                            break;
                            }
                        for (i=prev_m; i<=m; i++)
                            array[i]=1;
                        prev_m=99;
                        while (isdigit(*w)) w++;
                        switch (*w)
                            {
                            case 0x00:  rc=0; done=TRUE; break;
                            case ',': w++; break;
                            default: rc=-1; done=TRUE; break;
                            }
                        break;
                    default:
                        rc = -1;
                        break;
                    }			// switch()
                }			// while not done
            }				// if word = '*'
        }				// if GetWord() = -1
    return rc;
    }




int GetLine(FILE *fp, FormattedCmd_t *pCmd)
    {

    // Read the next line from the crontab file, decipher the entries, and
    // add the results to the pCmd array.

    short rc = 0, n=0;
    void *p;

    pLineBuffer=fgets(LineBuffer, sizeof(LineBuffer), fp) ;
    if (pLineBuffer==NULL)			// EOF - all done
       return -100;
    while (isspace(*pLineBuffer))
        pLineBuffer++;				// Skip leading spaces
    if (*pLineBuffer == '#')			// Skip comment lines
        rc = -1;
    if (rc >= 0)				// Evaluate Minute
        rc = CipherValue(pCmd->Minutes,  0, 59) ;
    if (rc >= 0)				// Evaluate Hour
        rc = CipherValue(pCmd->Hours,    0, 23) ;
    if (rc >= 0)				// Evaluate Day of month
        rc = CipherValue(pCmd->Days,     1, 31) ;
    if (rc >= 0)				// Evaluate Month
        rc = CipherValue(pCmd->Months,   1, 12) ;
    if (rc >= 0)				// Evaluate Weekday
        rc = CipherValue(pCmd->Weekdays, 0,  6) ;
    if (rc >= 0)
        {
        while (isspace(*pLineBuffer))
            pLineBuffer++;			// Skip spaces

	// Trim off trailing spaces from the command

        while (isspace(pLineBuffer[strlen(pLineBuffer)-1]))
            pLineBuffer[strlen(pLineBuffer)-1] = 0x00;
        if (strlen(pLineBuffer) < 1)
            rc = -1;				// No command found
        else
            {					// Allocate mem for command
            if ((pCmd->Cmd = malloc(strlen(pLineBuffer)+1)) == NULL)
                {
                printf("\aError: could not allocate memory to store cmd %s\n",
                       pLineBuffer) ;
                rc = -1;
                }
            else
                {
                strcpy(pCmd->Cmd, pLineBuffer) ;// Store cmd address in array
                }
            }
        }
    return rc;
    }


int BuildStructures(FILE *fp, FormattedCmd_t *pCmd[])
    {

    // BuildStructures() is called for each crontab file.  Each line of the
    // crontab file is read in and processed, building the pCmd array with
    // the values required to produce the report.

    int rc=0, Nbr=0;

    while ((rc != -100) && (NbrJobs < MAX_CMDS))
        {
        pCmd[NbrJobs] = malloc(sizeof(FormattedCmd_t));  // Get space
        if (pCmd[NbrJobs] == NULL)
            {
            printf("Could not allocate memory to store cmd struct #%d\n", 
                   NbrJobs);
            }
        else					// Memory acquired.  Continue.
            {
            pCmd[NbrJobs]->FileNbr = NbrFiles ; // Job counter
            rc = GetLine(fp, pCmd[NbrJobs]) ;	// Process a line
            if (rc < 0)				// Error or comment
                {
                free(pCmd[NbrJobs]);
                pCmd[NbrJobs] = NULL ;
                }
            else				// Bump job count
                {
                Nbr++;
                NbrJobs++;
                }
            }       // successful memory allocation
        }           // until end of file
    return Nbr;
    }



int LoadFile(char *InFile)
    {

    // Open file and process each line from it.

    int rc = 0;
    FILE *fp;
    char *ShortName, *p, DirName[255], LongName[255] ;

    // Set the crontab directory name, if no directory name was specified

    strcpy(DirName, dirname(InFile)) ;
    ShortName = basename(InFile) ;
    if (strcmp(ShortName, InFile) == 0)		// No directory specified
	strcpy(DirName, CronDir) ;		// Set default directory

    sprintf(LongName, "%s/%s", DirName, ShortName) ;
    fp=fopen(LongName, "r");
    if (fp==NULL)
        {
        printf("\t\aError: Could not open file %s.  File ignored.\n",
               InFile) ;
        return -1 ;
        }
    printf("%s\n", LongName) ;

    FileName[NbrFiles] = malloc(strlen(ShortName));
    if (FileName[NbrFiles] == NULL)
        {
        printf("\a\nCould not allocate memory to store filename %s\n\n", 
               ShortName) ;
        }
    else
        {
        strcpy(FileName[NbrFiles], ShortName);

    /* Build the array of commands to execute */

        rc = BuildStructures(fp, pCmd);
        NbrFiles++;
        }
    fclose(fp);
    return rc;
    }




void Usage(char *Pgm)
    {

    // Print a summary of usage for the user

    char *p, *PgmName=Pgm;

    while ((p=strstr(PgmName, "/")) != NULL)
        {
        PgmName=p+1;
        }
    printf("\n\nUsage:\n\t%s [-b BeginDate][-d Days][-s Subdir][-w width] File [File2 ...]\n",
            PgmName) ;
    printf("\nDefault begin date is current date at current time\n") ;
    printf("Default \'Days\' value is 1 (24-hour report)\n") ;
    printf("Valid date formats:  MoDa  MoDaCcYy  MoDaCcYyHrMi\n") ;
    printf("\nExamples\n") ;
    printf("\t%s root\n\t\t(To produce a listing of root's cronjobs over the\n",
           PgmName) ;
    printf("\t\t next 24 hours)\n") ;
    printf("\t%s -b0401 -d30 -w132 root oracle\n", PgmName) ;
    printf("\t\t(To produce a listing of all cron jobs to be run in April\n");
    printf("\t\t for users root and oracle.  Report width is 132 chars.)\n") ;
    printf("\t%s -b0503 *\n", PgmName) ;
    printf("\t\t(To produce a listing of all cronjobs that will run for all\n") ;
    printf("\t\t users on May 3 of current year.\n") ;
    printf("\t%s -b0503 -s/var/backup/cron/crontabs  *\n", PgmName) ;
    printf("\t\t(Same as above, but use all files from directory /var/backup,\n") ;
    printf("\t\t instead of " DFT_DIR ")\n") ;
    }



int ConvertTime(char *CharIn, time_t *TimeOut)
    {

    // Convert the start-time parameter from the command line, if specified

    time_t ltime;
    struct tm WorkTime;
    char szTimeWork[14] ;
    char work4[5], work2[3];
    short i;

    switch (strlen(CharIn))
        {
        case 4 :	// MMDD
        case 8 :	// MMDDYYYY

	    // Fill out the szTimeWork with the current date/time, then
	    // overlay the user-specified portion with the user values

            memset(szTimeWork, 0x00, sizeof(szTimeWork)) ;
            time(&ltime) ;
#ifdef THREADSAFE
            localtime_r(&ltime, &WorkTime) ;
#else
            memcpy(&WorkTime, localtime(&ltime), sizeof(WorkTime));
#endif
            strftime(szTimeWork, sizeof(szTimeWork), "%m%d%Y000000",
                     &WorkTime) ;
            memcpy(szTimeWork, CharIn, strlen(CharIn)) ;
            break ;
        case 12 :	// MMDDYYYYHHMM
            strcpy(szTimeWork, CharIn) ;
            break ;
        default:
            return -1 ; // Date format not decipherable
        }

    for (i=0; i<strlen(szTimeWork); i++)
        {		// Check for numeric vals; assume if numeric, it's OK
        if ((szTimeWork[i] < '0') || (szTimeWork[i] > '9'))
            {
            return -1;
            }
        }

    // Convert the specified values into a valid time_t value

    memset(work4, 0x00, sizeof(work4));
    memset(work2, 0x00, sizeof(work2));
    memcpy(work2, szTimeWork, 2);
    time(&ltime);
#ifdef THREADSAFE
    localtime_r(&ltime, &WorkTime) ;
#else
    memcpy(&WorkTime, localtime(&ltime), sizeof(WorkTime));
#endif
    WorkTime.tm_mon = atoi(work2) - 1;
    memcpy(work2, szTimeWork+2, 2);
    WorkTime.tm_mday = atoi(work2);
    memcpy(work4, szTimeWork+4, 4);
    WorkTime.tm_year = atoi(work4)-1900 ;
    memcpy(work2, szTimeWork+8, 2);
    WorkTime.tm_hour = atoi(work2) ;
    memcpy(work2, szTimeWork+10, 2);
    WorkTime.tm_min = atoi(work2) ;
    WorkTime.tm_isdst = -1;             /* Figure out if DST applies */
    ltime = mktime(&WorkTime) ;
    memcpy(TimeOut, &ltime, sizeof(ltime));
    return 0;
    }


short PrintJobs(struct tm *CurrTime)
    {

    // Here's where all our preparation pays off.  PrintJobs() is called for
    // each minute within the specified time range, and checks all stored
    // jobs for eligibility.  If found to be eligible, an entry is printed.

    short i, rc=0, CmdWidth = PageWidth - 27 ;
    FormattedCmd_t *xCmd;
    char pbuff[2048], *p ;

    for (i=0; i<NbrJobs; i++)
        {
        xCmd=pCmd[i];
        if ((xCmd->Minutes [CurrTime->tm_min]     != 0)	    // Match minute
         && (xCmd->Hours   [CurrTime->tm_hour]    != 0)     // Match hour
         && (xCmd->Months  [CurrTime->tm_mon - 1] != 0))    // Match month
            if  ((xCmd->Weekdays[CurrTime->tm_wday]    == 1)   // either...
              || (xCmd->Days    [CurrTime->tm_mday]    == 1)   //   specified
             || ((xCmd->Days    [CurrTime->tm_mday]    == 2)   // or both ...
              && (xCmd->Weekdays[CurrTime->tm_wday]    == 2))) //   unspecified
            {
            printf("%-10s %3s %2d/%02d %2d:%02d "
                  ,FileName[xCmd->FileNbr]
                  ,Wdays[CurrTime->tm_wday]
                  ,CurrTime->tm_mon+1
                  ,CurrTime->tm_mday
                  ,CurrTime->tm_hour
                  ,CurrTime->tm_min
                  );
             p = xCmd->Cmd ;
             while (strlen(p) > 0)
                 {
                 memset(pbuff, 0x00, sizeof(pbuff)) ;
                 memcpy(pbuff, p, MIN(strlen(p), CmdWidth)) ;
                 printf("%s\n", pbuff) ;
                 if (strlen(p) > CmdWidth)
                     {
                     p += CmdWidth ;
                     printf("%-27s", " ") ;
                     }
                 else
                     p += strlen(p);
                 }
             }
        }
    return rc;
    }




int main(int argc, char *argv[])
    {
    short Nbr, rc ;		// Work
    int i, Opt, ReturnOpt;	// Work
    struct tm WorkTime;		// A time work area
    time_t CurrTime;		// Current date/time
    int Duration ;		// Number of days to print on the report

    time(&StartTime);		// Default report start time = now
    Duration = 1 ;		// Default report duration = 1 day
    if (argc < 2)		// No parameters passed
        {
	Usage(argv[0]) ;	// User help.  No crontab files specified.
	exit(1) ;
	}
    else			// Parameters passed.
        if (strcmp(argv[1], "-help") == 0)
            {
            Usage(argv[0]) ;	// User help.
            exit(0) ;
            }
    while ((Opt = getopt(argc, argv, "b:d:s:w:")) != EOF)
        {
        switch (Opt)
            {
            case 'b' :		// Report begin time
                if ((ConvertTime(optarg, &StartTime)) < 0)
                    {
                    Usage(argv[0]);
                    printf("Could not convert start time\n") ;
                    exit(10);
                    }
                break ;
            case 'd' :		// Report duration
                Duration = atoi(optarg) ;
                break ;
            case 's' :
                CronDir=optarg ;
                break ;
            case 'w' :
                PageWidth=atoi(optarg) ;
                break ;
            }
        }

    StopTime = (unsigned long int) StartTime + (Duration * 86400) - 60 ;

    if ((argc - optind) > MAX_FILES)
        {
        printf("\n\aWARNING: using only first %d files.\n\n", MAX_FILES);
        argc = MAX_FILES+2;
        }

    // "Headings" (such as it is)

#ifdef THREADSAFE
    localtime_r(&StartTime, &WorkTime);
#else
    memcpy(&WorkTime, localtime(&StartTime), sizeof(WorkTime));
#endif
    printf("\nJobs to be run between %s %d-%s-%04d @ %d:%02d and",
            Wdays[WorkTime.tm_wday], WorkTime.tm_mday,
            Mths[WorkTime.tm_mon], WorkTime.tm_year+1900,
            WorkTime.tm_hour, WorkTime.tm_min) ;
#ifdef THREADSAFE
    localtime_r(&StopTime, &WorkTime);
#else
    memcpy(&WorkTime, localtime(&StopTime), sizeof(WorkTime));
#endif
    printf(" %s %d-%s-%04d @ %d:%02d\n\n",
            Wdays[WorkTime.tm_wday], WorkTime.tm_mday,
            Mths[WorkTime.tm_mon], WorkTime.tm_year+1900,
            WorkTime.tm_hour, WorkTime.tm_min) ;
    printf("Files processed:\n") ;
    for (i=optind; i<argc; i++)
        LoadFile(argv[i]) ;
    printf("\n");

    /* Find the appropriate execution dates & times; report   */
    /* any within user parameters.                            */

    memcpy(&CurrTime, &StartTime, sizeof(CurrTime));
    while (difftime(StopTime, CurrTime) >= 0)
        {
#ifdef THREADSAFE
        localtime_r(&CurrTime, &WorkTime);
#else
        memcpy(&WorkTime, localtime(&CurrTime), sizeof(WorkTime));
#endif
        PrintJobs(&WorkTime);
        WorkTime.tm_min ++;
        WorkTime.tm_isdst = -1;             /* Figure out if DST applies */
        CurrTime = mktime(&WorkTime);
        }

    /* Finished with the report.  Now, free all of the       */
    /* memory blocks we have allocated.                      */

    Nbr=0;
    while (pCmd[Nbr] != NULL)
        {
        free(pCmd[Nbr]->Cmd);
        free(pCmd[Nbr]) ;
        Nbr++;
        }
    Nbr=0;
    for (Nbr=0; Nbr < MAX_FILES && FileName[Nbr] != NULL; Nbr++)
        free(FileName[Nbr]);

    return rc;
    }
