The goal of this homework assignment is to allow you to practice using system calls involving processes and signals in C by implementing two new Unix utilities: moveit and timeit.

  1. moveit: The first utility allows users to interactively rename specified files using their favorite text $EDITOR.

  2. timeit: The second utility allows users to compute the elapsed time of an application while also enforcing a timeout (or cut off time).

Set us up the bomb (No!)

Be careful with the fork system call. To prevent a fork bomb, start off by always putting a sleep after a fork, that way you have time to kill the processes.

If you still manage to create a fork bomb, do not simply go to another machine and run the same program. Notify the csehelp@nd.edu and explain what happened.

For this assignment, record your source code and any responses to the following activities in the homework09 folder of your assignments GitLab repository and push your work by noon Saturday, April 13.

Activity 0: Preparation

Before starting this homework assignment, you should first perform a git pull to retrieve any changes in your remote GitLab repository:

$ cd path/to/repository                   # Go to assignments repository

$ git checkout master                     # Make sure we are in master branch

$ git pull --rebase                       # Get any remote changes not present locally

Next, create a new branch for this assignment:

$ git checkout -b homework09              # Create homework09 branch and check it out

Starter Code

To help you get started, the instructor has provided you with some starter code.

# Download starter code tarball
$ curl -LOk https://gitlab.com/nd-cse-20289-sp19/cse-20289-sp19-assignments/raw/master/homework09/homework09.tar.gz

# Extract starter code tarball
$ tar xzvf homework09.tar.gz

# Add and commit starter code
$ git add homework09
$ git commit -m "homework09: starter code"

Once downloaded and extracted, you should see the following files in your homework09 directory:

homework09
    \_ Makefile           # This is the Makefile for building all the project artifacts
    \_ README.md          # This is the README file for recording your responses
    \_ moveit.c           # This is the C99 source file for the moveit utility
    \_ timeit.c           # This is the C99 source file for the timeit utility
    \_ test_moveit.sh     # This is the Shell test script for the moveit utility
    \_ test_timeit.sh     # This is the Shell test script for the timeit utility

File: Makefile

Before you implement any of the utilities, you should first complete the provided Makefile. Remember, the Makefile contains all the rules or recipes for building the project artifacts (e.g. moveit and timeit). Although the provided Makefile contains most of the variable definitions and test recipes, you must add the appropriate rules for moveit, and timeit.

Makefile Variables

You must use the CC and CFLAGS variables when appropriate in your rules.

You can complete the Makefile with a pattern rule by compiling from the .c source directly into an executable.

Once you have a working Makefile, you should be able to run the following commands:

$ make                    # Build all targets
Compiling and Linking moveit...
Compiling and Linking timeit...

$ make clean              # Clean targets and intermediate files
Cleaning...

$ make -n                 # Simulate build with tracing output
echo Compiling and Linking moveit...
gcc -g -Wall -Werror -std=gnu99 -fPIC -o moveit moveit.c
echo Compiling and Linking timeit...
gcc -g -Wall -Werror -std=gnu99 -fPIC -o timeit timeit.c

$ make test               # Run all tests
...

Depending on your compiler, you may see some warnings with the initial starter code. Likewise, the test programs will all fail in some fashion.

Warnings

You must include the -Wall flag in your CFLAGS when you compile. This also means that your code must compile without any warnings, otherwise points will be deducted.

The details on what you need to implement are described in the following sections.

Activity 1: moveit

For the first activity, write a program, moveit, that reads in filenames from the command line arguments, stores them to a temporary file, opens an $EDITOR on the temporary file, allows the user to modify the filenames, and then renames the files based on the information in the temporary file as shown below:

Note: Your program must remove the temporary file with the unlink system call.

File: moveit.c

To implement the moveit utility, you are to complete the provided moveit.c source file which contains the following macros and functions:

Macros

#define streq(a, b) (strcmp(a, b) == 0)

The streq macro compares if two strings are equal.

##define strchomp(s) (s)[strlen(s) - 1] = 0

The strchomp macro removes the last character in a string.

Functions

/**
 * Display usage message and exit.
 * @param   progname    Program name.
 * @param   status      Exit status.
 */
void    usage(const char *progname, int status);

This provided usage function displays the help message and terminates the process with the specified status code.

/**
 * Save list of file paths to temporary file.
 * @param   files       Array of path strings.
 * @param   n           Number of path strings.
 * @return  Newly allocated path to temporary file.
 */
char *  save_files(char **files, size_t n);

This save_files function writes each of the files specified in the files array to a temporary file (one file per line) and returns the path of this temporary file.


/**
 * Run $EDITOR on specified path.
 * @param   path        Path to file to edit.
 * @return  Whether or not the $EDITOR process terminated successfully.
 */
bool    edit_files(const char *path);

This edit_files function executes the users $EDITOR on the specified file and returns the exit status of the $EDITOR process.

Fork It!

For this assignment, you must use low-level system calls for processes such as fork, exec, wait, and kill.

You may not use system or popen.


/**
 * Rename files as specified in contents of path.
 * @param   files       Array of old path names.
 * @param   n           Number of old path names.
 * @param   path        Path to file with new names.
 * @return  Whether or not all rename operations were successful.
 */
bool    move_files(char **files, size_t n, const char *path);

This move_files function renames old files specified in the files array with the new names contained in the path file (ie. files[0] corresponds to the first line in path).

Failure is an Option

You must check if the system calls you use fail and handle those situations appropriately.

File: test_moveit.sh

As you implement moveit, you can test it by running the test-moveit target:

$ make test-moveit
Testing moveit...
 system calls                                                             ... Success
 usage                                                                    ... Success
 usage (no arguments)                                                     ... Success
 usage (valgrind)                                                         ... Success
 deadpool spidey rogue -> deadpool spidey rogue                           ... Success
 deadpool spidey rogue -> deadpool spidey rogue (valgrind)                ... Success
 deadpool spidey rogue -> deadpool spidey rogue (vim)                     ... Success
 deadpool spidey rogue -> deadpool spidey rogue (vim, valgrind)           ... Success
 deadpool spidey rogue -> deadpool spidey rogue (nano)                    ... Success
 deadpool spidey rogue -> deadpool spidey rogue (nano, valgrind)          ... Success
 deadpool spidey rogue -> deadpool spidey rogue (emacs)                   ... Success
 deadpool spidey rogue -> deadpool spidey rogue (emacs, valgrind)         ... Success
 deadpool spidey rogue -> deadpool spidey rogue (NOPE)                    ... Success
 deadpool spidey rogue -> deadpool spidey rogue (NOPE, valgrind)          ... Success
 deadpool spidey rogue -> batman superman wonderwoman                     ... Success
 deadpool spidey rogue -> batman superman wonderwoman (valgrind)          ... Success
 deadpool spidey rogue -> batman superman wonderwoman (NOPE)              ... Success
 deadpool spidey rogue -> batman superman wonderwoman (NOPE, valgrind)    ... Success
 batman superman wonderwoman -> deadpool batman rogue                     ... Success
 batman superman wonderwoman -> deadpool batman rogue (valgrind)          ... Success
 batman superman wonderwoman -> deadpool batman rogue (NOPE)              ... Success
 batman superman wonderwoman -> deadpool batman rogue (NOPE, valgrind)    ... Success
 batman superman -> deadpool spidey rogue                                 ... Success
 batman superman -> deadpool spidey rogue (valgrind)                      ... Success
 batman superman -> deadpool spidey rogue (NOPE)                          ... Success
 batman superman -> deadpool spidey rogue (NOPE, valgrind)                ... Success
 deadpool spidey rogue -> batman superman                                 ... Success
 deadpool spidey rogue -> batman superman (valgrind)                      ... Success
 deadpool spidey rogue -> batman superman (NOPE)                          ... Success
 deadpool spidey rogue -> batman superman (NOPE, valgrind)                ... Success
 doom -> thing                                                            ... Success
 doom -> thing (valgrind)                                                 ... Success
 doom -> thing (rm)                                                       ... Success
 doom -> thing (rm, valgrind)                                             ... Success
 doom -> thing (false)                                                    ... Success
 doom -> thing (false, valgrind)                                          ... Success
   Score 6.00

Activity 2: timeit

For the second activity, write a program, timeit, that executes the given command until a timeout is reached or the program terminates as show below:

If the verbose mode is enabled, your timeit should display the following sort of debugging messages:

$ ./timeit -v
timeit.c:65:parse_options: Timeout = 10
timeit.c:66:parse_options: Verbose = 1
Usage: ./timeit [options] command...
Options:
    -t SECONDS  Timeout duration before killing command (default is 10)
    -v          Display verbose debugging output

$ ./timeit -t 5 -v sleep 1
timeit.c:65:parse_options: Timeout = 5
timeit.c:66:parse_options: Verbose = 1
timeit.c:80:parse_options: Command = sleep 1
timeit.c:107:main: Registering handlers...
timeit.c:110:main: Grabbing start time...
timeit.c:125:main: Sleeping for 5 seconds...
timeit.c:119:main: Executing child...
timeit.c:95:handle_signal: Received interrupt: 17
timeit.c:134:main: Waiting for child 32151...
timeit.c:139:main: Child exit status: 0
timeit.c:141:main: Grabbing end time...
Time Elapsed: 1.0

$ ./timeit -t 1 -v sleep 2
timeit.c:65:parse_options: Timeout = 1
timeit.c:66:parse_options: Verbose = 1
timeit.c:80:parse_options: Command = sleep 2
timeit.c:107:main: Registering handlers...
timeit.c:110:main: Grabbing start time...
timeit.c:125:main: Sleeping for 1 seconds...
timeit.c:119:main: Executing child...
timeit.c:127:main: Killing child 32162...
timeit.c:134:main: Waiting for child 32162...
timeit.c:95:handle_signal: Received interrupt: 17
timeit.c:139:main: Child exit status: 9
timeit.c:141:main: Grabbing end time...
Time Elapsed: 1.0

Note: If the time limit is exceeded, the parent should kill the child and wait for it. Moreover, the parent should always return the child's exit status as its own exit status.

File: timeit.c

To implement the timeit utility, you are to complete the provided timeit.c source file which contains the following macros and functions:

Macros

#define debug(M, ...) \
    if (Verbose) { \
        fprintf(stderr, "%s:%d:%s: " M, __FILE__, __LINE__, __func__, ##__VA_ARGS__); \
}

This debug macro displays the specified formatted message only if the Verbose global variable is true. The message includes the name of the file, the line number, and the function at which the macro is called.

Functions

/**
 * Display usage message and exit.
 * @param   progname    Program name.
 * @param   status      Exit status.
 */
void    usage(const char *progname, int status);

This provided usage function displays the help message and terminates the process with the specified status code.


/**
 * Parse command line options.
 * @param   argc        Number of command line arguments.
 * @param   argv        Array of command line argument strings.
 * @return  Array of strings representing command to execute.
 */
char ** parse_options(int argc, char **argv);

This parse_options function processes the command line arguments by setting the Timeout and Verbose global variables and by constructing an array of strings to represent the specified command.


/**
 * Handle signal.
 * @param   signum      Signal number.
 */
void    handle_signal(int signum);

This provided handle_signal function simply displays a message about the signum whenever a signal is triggered.

Main Execution

The main logic for timeit will be in the main function. There are multiple approaches on how to accomplish the task of running a program with a time limit and two such methods are shown below:

In both approaches, the main function registers some signals using either signal or sigaction. Then the parent process performs a fork and the child process executes the specified command using one of the exec variants.

  1. In the first approach, after the fork, the parent process simply performs a nanosleep on the specified Timeout. It will then either complete and thus must kill the child process, or it will be interrupted by the SIGCHLD signal. Afterwards, it will wait to get the child's exit status.

  2. In the second approach, after the fork, the parent process sets an alarm based on the specified Timeout. It then simply calls wait. If the alarm triggers, then the handler should kill the child process. Otherwise, if the child terminates before the Timeout then the parent cancels the alarm and completes its wait to retrieve the child's exit status.

Either approach is acceptable, although the first is recommended as it is simpler and less error-prone.

To measure the elapsed time, you must use the clock_gettime function with the CLOCK_MONOTONIC clock identifier (ie. older documentation may suggest gettimeofday, but use of this function is now discouraged).

Undefined Reference

If you get an "undefined reference to clock_gettime" linker error when compiling timeit, then you need to append the -lrt library flag to your compiler command as noted in the manual page for clock_gettime.

File: test_timeit.sh

As you implement timeit, you can test it by running the test-timeit target:

$ make test-timeit
Testing timeit...
 system calls                                                             ... Success
 usage (-h)                                                               ... Success
 usage (-h, valgrind)                                                     ... Success
 usage (no arguments)                                                     ... Success
 usage (no arguments, valgrind)                                           ... Success
 usage (-v, no command)                                                   ... Success
 usage (-v, no command, valgrind)                                         ... Success
 usage (-t 5 -v, no command)                                              ... Success
 usage (-t 5 -v, no command, valgrind)                                    ... Success
 sleep                                                                    ... Success
 sleep (valgrind)                                                         ... Success
 sleep 1                                                                  ... Success
 sleep 1 (output)                                                         ... Success
 sleep 1 (valgrind)                                                       ... Success
 -v sleep 1                                                               ... Success
 -v sleep 1 (output)                                                      ... Success
 -v sleep 1 (valgrind)                                                    ... Success
 -t 5 -v sleep 1                                                          ... Success
 -t 5 -v sleep 1 (output)                                                 ... Success
 -t 5 -v sleep 1 (valgrind)                                               ... Success
 sleep 5                                                                  ... Success
 sleep 5 (output)                                                         ... Success
 sleep 5 (valgrind)                                                       ... Success
 -v sleep 5                                                               ... Success
 -v sleep 5 (output)                                                      ... Success
 -v sleep 5 (valgrind)                                                    ... Success
 -t 1 sleep 5                                                             ... Success
 -t 1 sleep 5 (output)                                                    ... Success
 -t 1 sleep 5 (valgrind)                                                  ... Success
 -t 1 -v sleep 5                                                          ... Success
 -t 1 -v sleep 5 (output)                                                 ... Success
 -t 1 -v sleep 5 (valgrind)                                               ... Success
   Score 6.00

Activity 3: Reflection

In your README.md, answer the following questions:

  1. As discussed in class, working with system calls can be tricky because unlike most normal functions, they can fail. For each of the following scenarios involving moveit, identify which system calls would fail (if any) and described you handled that situation:

    • No arguments passed to moveit.

    • $EDITOR environmental variable is not set.

    • User has run out of processes.

    • $EDITOR program is not found.

    • Temporary file is deleted before moving files.

    • Destination path is not writable.

  2. As described in the project write-up, the parent is doing most of the work in timeit since it forks, times, and waits for the child (and possibly kills it), while the child simply calls exec. To distribute the work more evenly, Logan proposes the following change:

    Have the child set an alarm that goes off after the specified timeout. In the signal handler for the alarm, simply call exit, to terminate the child. This way, the parent just needs to wait and doesn't need to perform a kill (since the child will terminate itself after the timeout).

    Is this a good idea? Explain why or why not.

Guru Point: Cron (1 Point)

Professor Tim Weninger, Notre Dame's resident Reddit expert, lamblasted the instructor for failing to introduce students to cron in previous course sections. To rectify this, you are to write a simple shell or Python script does something interesting and runs periodically at some time interval using cron. Here are some resources regarding cron:

To receive credit, simply demonstrate the cronjob triggering to the instructor or a TA.

Submission

To submit your assignment, please commit your work to the homework09 folder of your homework09 branch in your assignments GitLab repository. Your homework09 folder should only contain the following files:

Note: Don't include any object files or executables.

#--------------------------------------------------
# BE SURE TO DO THE PREPARATION STEPS IN ACTIVITY 0
#--------------------------------------------------

$ cd homework09                           # Go to Homework 07 directory
...
$ git add Makefile                        # Mark changes for commit
$ git add moveit.c                        # Mark changes for commit
$ git commit -m "homework09: moveit"      # Record changes
...
$ git add Makefile                        # Mark changes for commit
$ git add timeit.c                        # Mark changes for commit
$ git commit -m "homework09: timeit"      # Record changes
...
$ git add README.md                       # Mark changes for commit
$ git commit -m "homework09: README"      # Record changes
...
$ git push -u origin homework09           # Push branch to GitLab

Remember to create a merge request and assign the appropriate TA from the Reading 11 TA List.