Lab 10 - File-based Chat System

From now on, we’ll assume that you start lab by connecting to the CS portal and that you are familiar with the command line environment. If you have not been practicing with the terminal, we strongly encourage reviewing Lab 1.

We will also assume that you ran the setup script from that lab and have all modules (including clang and git) by default.

In this lab, we will be writing a chat system so that you can send messages to your friends and classmates and read messages they send you. We will be experimenting with reading and writing files as well as continuing to work with strings in C (char *).

Lab Goals

After this lab, you should:

  1. Gain increasing familiariarity with C
  2. Understand more fully how strings (char *) work in C
  3. Understand how files work in C
  4. Gain experience writing and debugging C

Lab Overview

We have reserved some mailbox space on the CS portal in the directory /p/cso1/mailbox. For this lab, you will be writing a C program that will read a file in the mailbox (for your computingID) and display any messages you have. Then, your program will ask if the user wants to message another user and then write the message to that user’s mailbox file. Note: you should write your C code in your cso1-code folder so that you can continue to check it into git.

More specifically, you will write a program in C that will do the following:

  1. If there is a file named /p/cso1/mailbox/mst3k.chat, where mst3k is your ID, the program will show that file’s contents to the user and then empty the file. That is, it will print the contents of the file to the screen, then update the file to be empty.
    • If the user has no messages, you may want to display a message stating that they have no messages.
  2. The program will ask the user for the computing ID of the person they want to send a message to. If the user just presses return (without entering a computing id), then the program will end and not ask them what message to send.
  3. If the user gave a computing id, the program will ask the user what message to send to that person.
  4. Lastly, the program will append your computing ID, followed by a colon, followed by the message, to the file /p/cso1/mailbox/tj4uva.chat, where tj4uva is replaced by the recipient’s ID, and change its permissions so others can read and write it.

You are welcome to assume that no string will be bigger than 4096 characters and use a global char buffer[4096] instead of trying to handle malloc, etc, in this lab. We’ll be talking about malloc shortly after the exam.

You’ll probably find it useful to work in pairs so you can send each other messages.

Do NOT write or save any C code in the /p/cso1/mailbox directory.

Useful Hints

Creating Your Chat File

You need to create your own chat file so that it can be read and written to by others.

First, you will need to navigate to the mailbox directory. Then, you can use either vim or nano to create your file.

cd /p/cso1/mailbox
nano mst3k.chat

Type “hello” in your chat file to ensure it can be saved.

Do NOT write or save any C code in the /p/cso1/mailbox directory.

To return to your home directory:

cd ~

Feel free to write your code in any location where you usually work.

Getting your ID

Linux provides access to several “environment variables”. One of them (USER) has, as its value, your current login ID. (For us, that is your computing ID). You can retrieve this using the getenv function in stdlib.h:

Example: The following will print your ID to the terminal, duplicating the behavior of the built-in whoami command.

int main() {
    char *me = getenv("USER");
    puts(me);
    return 0;
}

Building a string

We will need to concatenate and build larger strings than we have so far. There are two basic tools for building a string out of component parts. Either one will work fine for this task, and both of them require a pre-allocated destination buffer. Hint: you may want to create a buffer for the string you build by defining a char array that is big enough, such as char buff[4096];.

strcat

The strcat function from string.h is similar to += for strings in Java or Python.

Example: After running the following

buffer[0] = '\0';
strcat(buffer, "/p/cso1/mailbox/");
strcat(buffer, "mst3k");
strcat(buffer, ".chat");

the array buffer contains a string with the following value: "/p/cso1/mailbox/mst3k.chat".

Hint: This might be helpful in putting together the messages to send and building the full path (directory) to read and write messages.

sprintf

The snprintf function from stdio.h does a formatted string construction, like a String’s .format() method in Java or Python. It uses the same formatting specifiers as printf, with both the power and complexity that that entails.

Example: After running the following

snprintf(buffer, 4096, "/p/%s/mailbox/%s.chat",
    "cso1",
    "mst3k",
);

the array buffer contains "/p/cso1/mailbox/mst3k.chat". Because of the second parameter, it will not overrun 4096 characters, even if it were given very large strings to format.

Hint: This might be helpful in putting together the messages to send and building the full path (directory) to read and write messages.

Working with files

To work with files, you want to use the fopen() function to open them, then access their contents, and then call fclose() to close them. The following components will help:

Opening a file

  • FILE *inbox = fopen(filepath, "r"); returns NULL if filepath is not the path to a real file, otherwise it returns a pointer to a FILE structure opened in read-only (for "r") mode.

    There is a special FILE * named stdin that is always open and it reads from what the user types into the terminal window.

  • FILE *outbox = fopen(filepath, "a"); returns NULL if filepath exists but you are not allowed to write to it, otherwise it returns a pointer to a FILE structure opened in append-only (for "a") mode.

    There is a special FILE * named stdout that is always open and displays to the terminal window.

Writing to a file

  • fwrite(buffer, sizeof(char), 23, outbox); writes 23 char-sized values from buffer to FILE *outbox. Use it if you know how many bytes you want to write.

  • fprintf(outbox, "%s: %d\n", "mst3k", 2501); writes mst3k: 2501 and a newline to FILE *outbox. Like snprintf, this gives a lot of power (along with corresponding complexity).

Reading from a file

  • size_t got = fread(buffer, sizeof(char), 4096, inbox); reads up to 4096 char-sized values from FILE *inbox into buffer, and returns the number of char-sized values read (which is often less than 4096 if inbox did not have that many characters).

    This does not work well for stdin as the inbox parameter, as you don’t generally know how many characters the user will type. fgets is better for most user interactions.

  • char *line = fgets(buffer, 4096, inbox); reads one line of text from FILE *inbox, or 4096 characters, whichever is less. It returns buffer on success and NULL on failure. The returned string includes the newline, as e.g. "mst3k\n"

    This works well for stdin as the inbox parameter, as users generally type one line at a time.

Handling file permissions, etc

In general, the files we write are owned by our user id and no one else can read or write to them. For this lab, we want students in our group to be able to write their messages to our mailboxes, so we’ll need to change permissions on the files we create. But we don’t want other people outside of uva to be able write to out messages.

  • chmod("/p/cso1/mailbox/mst3k.chat", 0660); sets the permissions for mst3k.chat based on a bit-vector of flags 0660, specified in octal (hence the leading 0):

    • The three octal digits define permissions (in written order) for the owner, group, and others.
    • If we consider the value of one octal digit in binary (e.g. change 06 to 1102), the bits mean may-read, may-write, and may-not-execute, in order. (A 1 means the permission is granted, a 0 means it is not granted).
    • We want the files used in the chat to be writeable by any user, so 0660 is a reasonable permission set.
  • truncate("/p/cso1/mailbox/mst3k.chat", 0); truncates the mst3k.chat file so its new size is 0 bytes. This is useful in re-setting a chat file after the user has read its contents.

Testing tips

You can manually check for the existance and permissions of a file in the terminal by running

ls -l /p/cso1/mailbox/mst3k.chat

Note that many bugs end up creating the wrong file name; the following will list all files in the directory with the newest file last

ls -ltr /p/cso1/mailbox/

You can force-add a message to a mailbox by appending some text directly to a file in the terminal. You can do that with the echo program and having bash redirect output so that it is appended to the file by running

echo "This is a message" >> /p/cso1/mailbox/mst3k.chat
chmod 660 /p/cso1/mailbox/mst3k.chat

You can read all messages in the terminal by using the less program to display the contents of the file. less works similarly to vim, so type q to exit the program. Open the mst3k.chat mailbox file by running

less /p/cso1/mailbox/mst3k.chat

You may also want to just print out the contents of a mailbox file directly to the terminal. You can do this with the cat command by running

cat /p/cso1/mailbox/mst3k.chat

NOTE: These can be useful in testing different aspects of your program independently. But don’t mess with other student’s mailboxes in this way without their permission.

If you try to read from a non-existent file, open a file in a non-existent directory, or access a file for which you do not have permissions, the functions you use will fail (and set errno).

Example run

The following shows an example of how a pair of students, aa1a and tj0uva might chat with one another. In this example, the program gives some additional information, such as displaying if they have no messages and displaying a prompt for what they are asking of the user. What the user types is shown like this.

User aa1a doesUser tj0uva does
$ ./a.out
You have no new messages
Who would you like to message?
tj0uva
What do you want to say?
Hello, is this working?
$ ./a.out
Your messages:
aa1a: Hello, is this working?
Who would you like to message?
aa1a
What do you want to say?
Yes, works well!
$ ./a.out
You have no new messages
Who would you like to message?
aa1a
What do you want to say?
How are you, by the way?
$ ./a.out
Your messages:
tj0uva: Yes, works well!
tj0uva: How are you, by the way?
Who would you like to message?

The Big Picture

Here are some high-level notes and hints for the ideal implementation:

  1. Use getenv to figure out the name of the user running the program. Don’t hard-code it or ask them.
  2. After showing the user their messages, truncate their mailbox file (empty it)
  3. Ask the user for who they want to message and their message to send
    1. If the user does not type an ID when asked, don’t ask for a message afterwards.
  4. Append the message to the recipient’s mailbox. Don’t erase the file first.
    1. Prepend the current user’s ID and a ": " to each message you put in a mailbox so the recipient can see who wrote it.
  5. chmod each file you touch to be 0660 so that others can work with it too.

The following flowchart may also be useful:

Flowchart diagram of fchat

Check-off with a TA

There is no autograder for this lab.

To check-off for this lab, show a TA

  • Your currently implemented file chat program. Your program should be able to do the basics:
    • read and display the current user’s messages
    • prompt the user for a computing id and a message, then append that to the other user’s mailbox file <!–
  • Note: you must show the TA your code, compiling your code, and then running it on the portal (providing the basic functionality above) for full credit.
    • The TA may ask you to run your code multiple times to see the full functionality of your program, such as running with no messages, messaging yourself, messaging another computing id, etc. Please follow their instructions to check off your program for full credit. –>

Copyright © 2023 Daniel Graham, John Hott and Luther Tychonievich.
Released under the CC-BY-NC-SA 4.0 license.
Creative Commons License