CS 3733 Operating Systems, Fall 2006 Assignments 3 and 4


Parts 0-2 Assignment 3 due: Monday, October 30
Parts 3-5 Assignment 4 due: Monday, November 13

In this assignment we will continue the project started in Assignment 1. Our goal is to be able to synchronized files on two different machines. For now, we will work on synchronizing files on two directories on the same machine. We will do it in a way that will allow for easy conversion to a two-machine solution.

In this assignment you will write two programs: a server and a client. The server will receive a local directory (a string) and a remote file list. The remote file list (RFL) will be in the format of a NULL-terminated array of strings. The server will create a second file list from the local directory and create three lists using the create_lists function from assignment 1. For each file in the RFL that is not in the local directory, the server will ask for a copy of the file from the client. For each file that is in both the RFL and the directory, the server will first ask the client for the modification time of the file and ask for the new file if it is newer than the one in the local directory.

The client and server will communicate using named pipes.

We will organize this assignment in such a way as to isolate the communication from the rest of the processing. That way when we start communicating between processes on different machines we can reuse much of the code from this assignment.

This assignment has several parts. Each of the parts is fairly simple if you have completed the other parts successfully.

Part 0: Getting Started
Put all of the utility functions you wrote for Assignment 1 in a file called list_utility.c. Make a corresponding list_utility.h include file. As you add functions to list_utility.c, update list_utility.h. Get a copy of the restart library, restart.c and its include file restart.h from the USP web site. Use these functions in the following parts. This will make your program safe to use in the presence of signals.

Add the following function to list_utility.c:
int compare_lists(char **list1, char **list2);
returns true (1) if list1 and list2 are identical NULL-terminated arrays of strings, and false (0) otherwise. The lists are identical if they have the same length and the strings they point to have the same characters. Note that the lists contain pointers. The pointers do not have to be identical, but the strings they point to need to be equal. (Use strcmp.) Convince me that this function works correctly.

Part 1: Directory functions
Write the following functions:
char **get_regular_files(char *dir)
returns a NULL-terminated array of strings containing the regular files int the directory given by dir. Return NULL on error with errno set. Use the stat function to determine if a file is a regular file. You must allocate space for the array and the strings pointed to by the elements of the array in such a way the the memory can be freed with freemakeargv from USP. Look up this function in the program index of USP. Use a method similar to Example 5.10 of USP to determine if a file is a regular file. You will need to make two passes with readdir, one to count the number of regular files (so you can allocate space for the list) and one to put the entries in the list. Note that you cannot just copy pointers from the DIR structure into the list memory may be overwritten when you call readdir again.

int get_modification_time(char *filename)
returns an integer giving the number of seconds since the Epoch representing the modification tome of the file. Return -1 with errno set if an error occurs. Use the st_mtime field of the stat structure. You can test this function by using ctime and comparing the result generated by ls -l.

Convince me that these functions work correctly.

Part 2: Communication functions
In this part will will write a function to receive a file using a open file descriptor. There are at least three ways for the receiver to determine when the end of the file was transmitted. One is to close the file descriptor when the last byte has been sent. We do not want to do this since we need to send more than one file. We can put a special terminator byte at the end of the file, but this will not work for binary files. Lastly we can tell the receiver how many bytes to expect. We will use this method. The file size will be sent as ASCII digits followed by a newline. No string terminator will be sent. This will be immediately followed by the indicated number of bytes.
Write the following functions and put them in list_utility.c.

int get_integer(int fd);
That reads a non-negative integer as described above from the open file descriptor and returns its value. It returns -1 and sets error number if an error occurs. Use the readline function from the restart library. You can assume that a valid integer will not contain more than 64 digits, but you need to generate and error before a buffer overflow occurs if the input is invalid.

int get_file(int fd, int size, char *filename);
Reads the given number of bytes from the open file descriptor and stores them in the new file with the given name. If a file with the given name already exists, the file is overwritten. Return the size of the file on success and -1 on error with errno set. Decide on appropriate permissions for a new file.

int send_integer(int fd, int n);
that sends the second parameter (a non-negative integer) over the open file descriptor in the format described above, ASCII digits followed by a newline. Return the value sent on success and -1 with errno set on error.

int send_n_bytes(int fd, int n, char *buf);
Sends exactly n bytes from the buffer to the given open file descriptor. Return the number of bytes sent on success and -1 with errno set on error.

int send_file(int fd, char *filename)
Sends the given file over the open file descriptor using the format described above. First the size is sent as a single line and then the bytes of the file are sent. Return the size of the file on success and -1 with errno set on error. This function should use the send_integer and send_n_bytes functions above.

Test all of these functions. Write appropriate test programs to test them and generate output that demonstrates your success. It is your responsibility to convince me that these function are working properly.

Part 3: File list utilities.
The server will need to receive a directory and a file list from the client. We will send the directory as a line rather than as a string. This means that it will be terminated by a newline rather than a string terminator. We do not want to send pointers when we send our list of strings over the file descriptor. We will send each string as a line (as above) and send an empty line to represent the end of the list. Write the following functions and put them in list_utility.

int send_string_as_line(int fd, char *buf);
takes an open file descriptor and a string as parameters and sends the characters of the string (but not the string terminator) to the file descriptor followed by a newline character. Return the total number of bytes sent on success or -1 with errno set on error. You may assume that the original string does not contain a newline character.

int get_string_as_line(int fd, char *buf, int bufsize);
reads a line from fd and stores the result (without the newline) in buf followed by a string terminator. The last parameter is the size of the buffer and an error should be returned before a buffer overflow occurs. Return the total number of bytes read on success or -1 with errno set on error. To allow you to use the readline function of the restart library, it is OK to return an error if the buffer is close to being too small.

int send_string_list_as_lines(int fd, char **list);
The first parameter is an open file descriptor and the second parameter is a NULL-terminated array of strings (like from Assignment 1). Each string is converted to a line and sent to the file descriptor using send_string_as_line. When the NULL pointer is reached an empty line is sent (just a newline character). Return the total number of bytes sent on success or -1 with errno set on error. We assume that none of the original strings contains a newline characterer or is empty. This is a reasonable assumption when the strings represent file names.

char **get_string_list_as_lines(int fd):
This reads lines in the format of send_string_list_as_lines and converts them to a NULL-terminated array of strings using get_string_as_line. Return the list on success and a NULL pointer with errno set on failure. The returned list should consist of allocated space that can be freed with freemakeargv from USP. Implementing this is simpler if you can assume a maximum length of the strings that are being sent and a maximum number of strings to be sent. You may assume a maximum string length of 1024 and a maximum number os strings of 100. Use automatic storage for temporary space. Remember that you do not want your returned list to be in automatic storage since this goes away when the function returns. You should not allow a buffer overflow to occur, no matter what input is received, even if it contains more than 100 string or a string is longer than 1024.

Test these methods and convince me that they are working correctly. One way to test them is to create a list using makeargv, send the list to a pipe using send_string_list_as_lines and then reading it back with get_string_list_as_lines. Compare the result with the original. Write a method to do the comparison rather than just looking at the results. This will make sure no non-printing characters have crept into your list. Be sure to do some testing of send_string_as_line and get_string_as_line before writing the methods to transmit lists.

Part 4: Write the client.
The client takes 3 command-line parameters, the names of two FIFOs, the first for reading and the second for writing and a directory. The client should assume that these FIFOs already exist. It sends the directory to the write pipe using send_string_as_line. It creates a list of the regular files in the directory using get_regular_files and sends this to the write pipe using send_string_list_as_lines. The client then waits for requests from the server be reading lines from the read pipe.

Three types of requests can be generated by the server. Each request is a line whose first character determines the type of request. The first character is an ASCII digit, '0', or '1', or '2'. For the first two requests, this is directly followed by the name of a file. For the last type, it is followed by a message.

You can test the client without having written the server. Open two terminal windows, one in which you can write to the read pipe (use cat < readpipe) and one in which you display what was written to the write pipe (cat > writepipe). Since all communication is done as lines of printing characters, you should be able to see exactly what is being transmitted.

Part 5: Write the server.
The server takes two command-line parameters, the names of two FIFOs, the first for reading and the second for writing. The server should assume that these FIFOs already exist. It will use get_string_as_line to read a file name from the read FIFO and get_string_list_as_lines to get a file list from the read FIFO. The file name will be interpreted as a local directory and the file list will be interpreted as a list of files on the remote directory. Get a list of regular files on the local directory (use get_regular_files) and use create_lists to compare the local and remote directories.

For each file that is on the remote directory but not on the local one, request the file by sending the file name preceded by a '1' character. Get the file and save it in the local directory using get_file. For each file that is in both directories, get the modification time of the remote file and compare it to the modification time of the local one. If the remote one is newer, get it and overwrite the local one. Make sure this works even if the new version is smaller than the old one.

When all of this has been completed, send a message to the client preceded by a '2' character. The message should indicate the number of files successfully transferred if all were successful, or an appropriate error message if not.

Handing in your assignment
Use this cover sheet for Assignment 3 and this one for Assignment 4.