Assignment 3 FAQ

NOTE: this website is out of date. This is the course web site from a past quarter. If you are a current student taking the course, you should visit the current class web site instead. If the current website is not yet visible by going to cs111.stanford.edu, it may be accessible by visiting this link until the new page is mounted at this address. Please be advised that courses' policies change with each new quarter and instructor, and any information on this out-of-date page may not apply to you.

How do I use GDB for debugging on this assignment?

You can run your shell under GDB like this:

gdb stsh

Then you can set breakpoints and run it with run, at which point you'll see your shell prompt and can enter commands. You can also do run < SCRIPTFILE, specifying a script file, and it will read input from there instead.

Debugging with multiple processes requires some additional steps because GDB isn't always sure whether you want to step through the parent process or a child process's execution. Make sure to enter the following 2 commands right when you start GDB:

set detach-on-fork off
set follow-fork-mode child

The first tells GDB to capture any fork-ed processes and pause them on fork. The second says, when a child process is forked, to switch to tracing that child process. You can list all processes GDB has captured like this:

info inferiors

And switch to a different one by specifying its number, like this:

inferior 2

You can stop watching a process and continue it by specifying its number, like this:

detach inferior 2

Click here for sample GDB run.

An easy way to step through the execution for a particular process is to put a breakpoint in code that only that process will execute. For instance, if you are trying to debug the second child in a two-process pipeline, try putting a breakpoint in code only the second child executes.

How do I use Valgrind on this assignment?

Valgrind isn't supported on this assignment to check for memory leaks or errors, so you can ignore any output related to those (though you should still not have memory leaks or errors :) ). But you can use Valgrind to track open file descriptors, like this:

valgrind --track-fds=yes --trace-children=yes ./stsh

You can then enter commands, enter "quit" when you're done, and valgrind will show any open file descriptors. You can run it with a script as well:

valgrind --track-fds=yes --trace-children=yes ./stsh < samples/scripts/one-command.txt

You are not required to close STDIN, STDOUT or STDERR.

Note that if you're using VSCode, you may see additional file descriptors open that VSCode is using to connect to the myth machines. To confirm if an open file descriptor is coming from VSCode, you can either run the sample solution under valgrind to see if it also reports those same open file descriptors (meaning they are fine), or you can try running valgrind while SSHing from a separate terminal program outside of VSCode to see if those file descriptors no longer appear.

What's a good way to debug issues with connecting pipes or redirecting STDIN/STDOUT?

The samples/inspect-fds.py tool is an extremely useful tool that displays all currently-open file descriptors for all the shell processes.

To use it, you need to run it in a second terminal window connected to the same myth machine as the one where you are running stsh. You can do ssh SUNET@mythXX.stanford.edu (eg. ssh SUNET@myth55.stanford.edu) to log into a particular myth. Then, run ./samples/inspect-fds.py stsh to view the file descriptors in use at that time by your running shell program. It will look something like this, labeling and color-coding any pipes to show you how they are connected. Here is example output for the command sleep 100 | ./conduit | cat > out.txt:

========== ./samples/stsh_soln (pid 3677600, ppid 3652398) ========== 
0   (read/write)      <terminal>
1   (read/write)      <terminal>
2   (read/write)      <terminal>
========== sleep (pid 3677601, ppid 3677600) ========== 
0   (read/write)      <terminal>
1   (write)           <pipe #46283622> [red]
2   (read/write)      <terminal>
========== ./conduit (pid 3677602, ppid 3677600) ========== 
0   (read)            <pipe #46283622> [red]
1   (write)           <pipe #46283623> [green]
2   (read/write)      <terminal>
========== cat (pid 3677603, ppid 3677600) ========== 
0   (read)            <pipe #46283623> [green]
1   (write)           out.txt
2   (read/write)      <terminal>

Here, we see output for each of the 4 processes (parent shell + 3 children) with the following information displayed:

  • the shell's STDIN/STDOUT/STDERR are all connected to the terminal
  • the first child process, running sleep, has its STDIN/STDERR conencted to the terminal, and its STDOUT connected to the "red" pipe
  • the second child process, running conduit, has its STDERR connected to the terminal, its STDIN connected to the "red" pipe, and its STDOUT connected to the "green" pipe
  • the third child process, running cat, has its STDIN connected to the "green" pipe, its STDOUT outputting to the file out.txt, and its STDERR connected to the terminal.

You may need to run a long-running pipeline (e.g sleep 100 | ./conduit | cat > out.txt) to give sufficient time to run inspect-fds.py and see open file descriptors. Feel free to try this out on the sample solution too! Run samples/stsh_soln and enter sleep 100 | ./conduit | cat > out.txt, and in another terminal window connected to the same myth machine, run samples/inspect-fds.py stsh_soln.

What could cause an issue where the shell exits on its own after running a command?

This commonly happens when the shell's STDIN is closed; when this happens, the prompting loop exits, as it cannot get any more input from the user. Double check that you aren't accidentally closing the parent's STDIN.

What does the open-fds.py program do?

This program prints out file descriptors that are currently open (used). The way it does this is by creating several pipes, and then checking what file descriptor numbers in a certain range are not included in the file descriptors for those pipes; those must be already-open file descriptors, and those are printed out. You can use this for testing by running the program by itself or in a pipeline - if it inherits any file descriptors that remain open, it will print those out.

What could cause the issue "write error: bad file descriptor?"

This means that the STDOUT of that process isn't configured properly. For instance, STDOUT is connected to a file descriptor that is garbage (eg. uninitialized), or it's connected to a file descriptor that is set up only for reading.

What are some recommendations for thorough testing?

Our biggest recommendation is to test as you go, and test thoroughly at each milestone before moving on. Consider writing some tests before you write code for each milestone, as a way to document the intended behavior, and then write additional tests as you go. This not only helps to understand the milestone requirements prior to writing code, but can help you write tests that are grounded in the milestone requirements rather than your knowledge of how your already-written code works.

Beyond this, we recommend focusing on varying behaviors when you run pipelines of different lengths - for instance, things that print or don't print output, things that stall or don't stall, things that crash or don't crash, and thinking about how the different features you have implemented could be combined in various ways.

What are the error-checking requirements?

The three main error checking requirements are checking if execvp fails, checking if open fails for input or output redirection, and checking if a child process segfaulted. You are not required to error check e.g. the return values of other functions like fork, dup2, pipe, etc.

Do we need to close file descriptors or call exit if an error occurs in the child?

Nope! But you should make sure to throw an STSHException; in a child process, this causes the process to terminate, because it will immediately jump to this catch statement in main:

} catch (const STSHException& e) {
    cerr << e.what() << endl;
    if (getpid() != stshpid) exit(0);  // if exception is thrown from child process, kill it
}

What could cause the shell to print multiple copies of output that should only be printed once?

This can sometimes be caused by spawning too many processes, so you have more processes than expected running a command. The inspect-fds.py tool is a great way to see how many processes are currently running.

Do we need to keep our code for each milestone separate?

Nope - and in fact, we encourage you to unify code across milestones! The milestones are provided to help break down the assignment, but they are cumulative; in other words, your implementation of general multi-process pipelines will also support 2 process pipelines.