CSCI 415/515: Fall 2022
Systems Programming
Project 3: mcron

Due: Thu, Oct 13, 7:00am


In this project, you will implement mcron -- a mock version of the cron utility. cron executes commands based on a user-supplied schedule; for instance, you can schedule a backup job to run every day at midnight.

Our mock utility will be quite a bit simpler: instead of running an actual command, we'll just log that it should be run. Additionally, whereas cron has a sophisticated syntax for specifying a schedule (see crontab), our tool is much more limited: the user can specify an interval in seconds, and the command "runs" at the end of each interval.

Name

mcron - execute scheduled commands (mock)

Synopsis

mcron [-h] [-l LOG_FILE] CONFIG_FILE

Options

-h, --help

Print a usage statement to stdout and exit with status 0.

-l, --log-file LOG_FILE

Use LOG_FILE as the log file. If LOG_FILE already exists, it is truncated and overwritten. If LOG_FILE is a path, the intermediate directories must already exist. If this option is not specified, then the default is to create a file called mcron.log in the current working directory.

Config File

Each line of the configuration file has the following format:

CONFIG_LINE := INTERVAL SPACE COMMAND {NEWLINE|EOF}

INTERVAL is an unsigned integer representing the number of seconds, and COMMAND is the command to "run" (in our case, log). COMMAND extends up to but not including the new line character (or, in the case of the last command, the end-of-file). The COMMAND "runs" every INTERVAL number of seconds (that is, mcron waits INTERVAL seconds and logs the COMMAND, then waits INTERVAL seconds and logs the COMMAND again, and so forth).

Each line of the configuration file also corresponds to a job id. The command on the first line has job id 0, the command on the second line has job id 1, and so forth.

The following is sample code to parse a configuration line. You'll want to read the config file line-by-line, and for each line, call job_from_config_line to parse the line to a struct job. You can then add the struct job to a linked list of all of the jobs. As you develop your program, you will most likely want to modify and add fields to the struct job.


#include <ctype.h>

#include "list.h"
#include "mu.h"


struct job {
    struct list_head list;
    char *cmd;
    int secs;
};


static struct job *
job_new(const char *cmd, unsigned int secs)
{
    struct job *job = mu_zalloc(sizeof(struct job));

    job->cmd = mu_strdup(cmd);
    job->secs = secs;

    return job;
}


static void
job_free(struct job *job)
{
    free(job->cmd);
    free(job);
}


/* Return NULL on invalid configuration line */
static struct job *
job_from_config_line(char *line)
{
    char *p = line;
    char *cmd;
    bool found_space = false;
    unsigned int secs;
    int err;

    mu_str_chomp(line);

    while (*p) {
        if (isspace(*p)) {
            found_space = true;
            break;
        }
        p++;
    }

    if (!found_space)
        return NULL;

    *p = '\0';
    cmd = p+1;

    err = mu_str_to_uint(line, 10, &secs);
    if (err != 0)
        return NULL;

    return job_new(cmd, secs);
}
        

Log File

Each log file line has the following format:

LOG_LINE := TIMESTAMP SPACE JOB_ID SPACE COMMAND NEWLINE
TIMESTAMP := YYYY/MM/DD HH:MM:SS UTC

The following code shows how to create a timestamp for the current time:


        #include <time.h>

        #include "mu.h"

        /* 
         * Write the UTC timestamp for the current time into `buf`.  `buf_size` is
         * the size in bytes of `buf`, which must be large enough to hold the
         * timestamp and its terminating nul-byte.  If `buf` is not large enough,
         * the function terminates the process.
         */
        void
        timestamp_utc(void *buf, size_t buf_size)
        {
            time_t t;
            struct tm tm;
            size_t n;

            time(&t);
            gmtime_r(&t &tm);
            n = strftime(buf, buf_size, "%Y/%m/%d %H:%M:%S UTC", &tm);
            if (n == 0)
                mu_die("strftime");
        }
        

PID File

On startup, mcron creates a file in its current working directory called mcron.pid that contains the process ID (PID) of the mcron process.

The format of the PID file is:

PID NEWLINE

A PID is an int (though libc typedef's it as a pid_t); a process can retrieve its PID using the getpid system call.

If mcron.pid already exists, then mcron overwrites it.

Signals

SIGTERM
On receiving a SIGTERM signal, mcron deletes the PID file and exits with a zero status.
SIGINT
Same as SIGTERM.
SIGUSR1
On receiving a SIGUSR1 signal, mcron rotates its log file. When rotating a log file, the log file is renamed to LOG_FILE-N, and the active log file is truncated to 0-length. For instance, if the name of the log file is mcron.log, then on first rotation, the log file is saved as mcron.log-0, on the second rotation it is saved as mcron.log-1, and so on. Note that the active log file is always mcron.log.
SIGHUP
On receiving a SIGHUP signal, mcron clears its list of jobs and re-reads the config file.

Bonus 1

Implement a -d, --delay SECS option that delays the start of mcron for SECS seconds. The delay only applies to the launch of mcron; it does not affect other operations, such as re-reading the configuration file on a SIGHUP.

Bonus 2

For Bonus 2, mcron should handle the realtime signal 35 (SIGRTMIN + 1). The integer value that the sender attaches to this signal is the job ID to run. mcron should lookup this job ID and run it (that is, log it) immediately. The job's schedule, however, should remain unchanged. If the job ID does not exist, mcron should log a one line message to the log file that starts with the timestamp and then includes the word error or Error somewhere in the rest of the line.

To send a realtime signal to mcron, you can compile and use the send_sigqueue sample program. This is merely for your testing purposes; you do not need to include send_sigqueue in your submission.

Utility code (mu.c, mu.h, and list.h)

As we progress through the course, we are slowly adding new functions to our utility files, mu.c and mu.h. Here is the most up-to-date version of those files, along with the linked list header file that you used in the sgrep project:

Submitting

Submit your project as a zip file via gradescope. Your project must include a Makefile that builds an executable called mcron. Please refer to the instructions for submitting an assignment for details on how to login to gradescope and properly zip your project.

Rubric

Config Files

a.conf


      
b.conf


      
a-new.conf



      

-h, --help


1.1 Print a usage statement (2 pts)


        ./mcron --help
        

Prints a usage statement to stdout. The statement must start with either Usage or usage; you decide the rest of the message. Conventionally, this option either prints the synopsis or a more verbose statement that also includes a description of the options.

1.2 Zero exit status (2 pts)


        ./mcron -h
        echo $?
        0
        

The exit status is zero.

Bad Usage


2.1 Missing config file (2 pts)


        ./mcron
        

Exits with a nonzero status.

2.2 Config file does not exist (2 pts)


        ./mcron nonexistent.conf
        

Exits with a nonzero status.

2.3 Unknown option (2 pts)


        ./mcron -f a.conf
        

Exits with a nonzero status.

PID File


3.1 PID file exists (7 pts)


        ./mcron a.conf
        

Produces a PID file called mcron.pid.

3.2 PID file contains PID (5 pts)


        ./mcron a.conf
        

Produces a PID file called mcron.pid that contains the process's PID followed by a newline (\n).

SIGTERM


4.1 SIGTERM kills the proces (7 pts)


        ./mcron a.conf &
        kill -TERM $(cat mcron.pid)
        

SIGTERM kills the process.

4.2 SIGTERM deletes PID file (5 pts)


        ./mcron a.conf &
        kill -TERM $(cat mcron.pid)
        ls mcron.pid
        ls: cannot access 'mcron.pid': No such file or directory
        

SIGTERM causes the process to delete its pid file before it exits.

SIGINT


5.1 SIGINT kills the proces (7 pts)


        ./mcron a.conf &
        kill -INT $(cat mcron.pid)
        

SIGINT kills the process.

5.2 SIGINT deletes PID file (5 pts)


        ./mcron a.conf &
        kill -INT $(cat mcron.pid)
        ls mcron.pid
        ls: cannot access 'mcron.pid': No such file or directory
        

SIGINT causes the process to delete its pid file before it exits.

Log


6.1 Logging a.conf (5 pts)


        ./mcron a.conf
        

Logs two commands to mcron.log withing first 7.5 seconds.

6.2 Logging format (5 pts)


        ./mcron a.conf
        

Logs one command within first 4.5 seconds with correct format (YYYY/MM/DD UTC 0 /bin/ls\n).

6.3 Logging b.conf (5 pts)


        ./mcron b.conf
        

After 11 seconds, the log has entries for: /bin/ls, /bin/netstat, /bin/ls, /bin/ls, /bin/netstat.

6.4 --log-file option (5 pts)


        ./mcron --log-file test.log a.conf
        

Logs one entry to test.log within first 4 seconds.

SIGUSR1


7.1 One rotation (5 pts)


        ./mcron a.conf &
        # wait 4 seconds
        kill -USR1 $(cat mcron.pid)
        

SIGUSR1 rotates the log to mcron.log-0.

7.2 mcron.log-0 contents (5 pts)


        ./mcron a.conf &
        # wait 4 seconds
        kill -USR1 $(cat mcron.pid)
        

SIGUSR1 rotates the log to mcron.log-0, which has one entry.

7.3 mcron.log contents (5 pts)


        ./mcron a.conf &
        # wait 4 seconds
        kill -USR1 $(cat mcron.pid)
        # wait 3 seconds
        

After 4 seconds, SIGUSR1 rotates the log to mcron.log-0 After another 3 seconds, mcron.log should have only one entry.

7.4 Two rotations (5 pts)


        ./mcron a.conf &
        # wait 4 seconds
        kill -USR1 $(cat mcron.pid)
        # wait 4 seconds
        kill -USR1 $(cat mcron.pid)
        

SIGUSR1 rotates the log to mcron.log-0, and then to mcron.log-1.

SIGHUP


8.1 Re-read configuration (14 pts)


        ./mcron a.conf &
        # wait 4 seconds
        cp a-new.conf a.conf
        kill -HUP $(cat mcron.pid)
        # wait 7 seconds
        

After running mcron.conf for 4 seconds, overwrite a.conf with a-new.conf and send a SIGHUP to re-read the configuration. After another 7 seconds, verify that the log as one entry for /bin/ls and one for /bin/netstat.

Bonus 1: -d, --delay Option


100.1 Delay (10 pts)


        ./mcron -d 3 a.conf
        # wait 7 seconds
        

After 7 seconds have elapsed, mcron.log should have only a single entry for /bin/ls.

Bonus 2: sigqueue


200.1 Run job (5 pts)


        ./mcron b.conf
        # wait 7 seconds
        

After 7 seconds have elapsed, send realtime signal #35 with a value of 1 and check that mcron.log has four entries: /bin/ls, /bin/netstat, /bin/ls, and /bin/netstat. (In other words, the signal caused the last /bin/netstat entry.) For testing mcron, you can compile and use the send_sigqueue sample program.

200.1 Non-existent job (5 pts)


        ./mcron b.conf
        # wait 7 seconds
        

After 7 seconds have elapsed, send realtime signal #35 with a value of 2 and check that mcron.log has four entries: /bin/ls, /bin/netstat, /bin/ls, and error. The error entry is simply a line that starts with a timestamp and then contains the word error or Error somewhere in the rest of the line. The error is due to the fact that b.conf only has two jobs (which correspond to job IDs 0 and 1); there is no job with an ID of 2.