In this project, you will build a multi-process telnet-like client and server. Part A of the project (this part) can be broken up into two major steps:
You will be extending the same code in Project 1B, so make sure it is readable and easy to modify.
This lab will build on the process and exception discussions in the lecture on processes and exceptions, but it is really about researching and exploiting APIs.
A single tarball (.tar.gz) containing:
You will write a program that compiles to an executable named
lab1a, that accepts the command line argument --shell (explained below).
c_iflag = ISTRIP; /* only lower 7 bits */ c_oflag = 0; /* no processing */ c_lflag = 0; /* no processing */and these changes should be made with the TCSANOW option. Note that this sort of mode change may result in the flushing of queued input or output characters. Thus the modes should be set immediately on start-up (before any characters are entered) and reset as the last thing before shutdown (after all returned output has been processed).
Note: You will surely have your program exit without properly restoring modes, after which your terminal session may seem unusable. Get familiar with the stty sane option of stty(1). If your program exists with your terminal modes in some unusable state, you can usually fix it by typing ^J stty sane ^J, where ^J is a control-J (or linefeed character).
On most systems the most appropriate shell is probably /bin/bash.
A read(2) on a pipe will not return an end of file (returned count of 0) until the write side of the pipe has been closed and all of the previously bufferred data has been returned. You will have (at least) three file descriptors for the write side of the pipe from the shell back to your process:
The same warning applies to the write file descriptor from your process to the shell. There must also be only one remaining copy of that open file reference, so that when you close it, the shell will read an end of file.
We type one character at a time, and so each read(2) from the keyboard is likely to return only one character, even if we do the read for a much larger number of characters. But a shell command may generate many lines, so reads from the shell are likely to return many more characters. It is suggested that you should do a large (e.g., 256 byte) read, and then process however many characters you actually receive.
One trick here is that will not be a strict alternation between input from the keyboard and input from the shell. Either is capable of generating input at any time. If we do a read(2) from the keyboard, we will block until keyboard input becomes available, even if there is input from the shell pipe waiting for us! The poll(2) system call enable us to solve this problem:
In this stage you will be introducing four significant new elements to your program:
That is a lot of new non-trivial code, and I don't like trying to debug four new things at the same time. You might find it easier to add and debug these features one at a time:
At this point every character should echo twice: once from your keyboard input handler, and again when it is re-played back from the child process. Once this works, you know that data is flowing through both of your pipes.
You should now be able to type shell commands to your program, see them echoed as you type them, and then see the output from the shell (and any command you run under it).
Because we have disabled all of the normal input processing, the interrupt character (^C) becomes just another character. When your program reads a ^C (0x03) from the keyboard, it should use kill(2) to send a SIGINT to the shell process. Note that the shell will not likely die as an immediate result of receiving this signal, so you should continue processing input from the keyboard to the shell and output from the shell to the screen.
You may find it easier to tell if your program has recognized a control-C or control-D if, rather than echoing them directly back to the screen, you translate each into a standard graphical representation (echoing a caret in front of the letter: ^C or ^D). The grading program will not look at how these characters are echoed.
You may find debugging complicated by the fact that it is not obvious which process did or did not receive or generate which character when. If you add a --debug option to your program that enables copious logging (e.g., to stderr) of every processed character and event, you may find that this greatly facilitates the debugging process.
WARNING:
As you are testing communication between the parent and sub-process
you will surely have situations where the child process (for one
reason or another) hangs without exiting. As you try to debug the
problem you may have dozens of hung processes in the background.
This can become a problem (e.g. when you exceed your quota of maximum
allowed processes per user). You should periodically run a
process-status command (ps) and see if you have any
lab1a (or pty_test) processes running in
the background. If so: kill them!
% ps
PID TTY TIME CMD
2718 pts/1 00:00:00 bash
184180 pts/1 00:00:01 xterm
218758 pts/1 00:00:00 pty_test
218759 pts/1 00:00:00 lab1a
218761 pts/1 00:00:00 bash
263956 pts/1 00:00:00 ps
% kill 218759
% kill 218758
%
NOTE that a reader (e.g., your shell) will not get an end-of-file from a pipe until (all copies of) the write file descriptor (in all processes that share it) are closed. This means you have to close pipe file descriptors:
The same is true of the pipe back from the shell to your process. Your poll will not return an error and your read will not return an EOF until the last write file descriptor associated with that pipe has been closed.
where the first # is the low order 7-bits (0x007f) of the shell's exit status and the second # is the next higher order byte (0xff00) of the shell's exit status (both decimal integers).
Note that the three normal shut-down scenarios (closing the pipe from the keyboard reader, receiving a SIGPIPE from the keyboard reader, and receiving an EOF from the shell reader) are not mutually independent. It is likely that, no matter how the shut-down is initiated, multiple of these events will occur in a non-deterministic order.
Summary of exit codes:
Your README file must include lines of the form:
Your tarball should have a name of the form lab1a-studentID.tar.gz. You can sanity check your submission with this test script.
We will test it on a departmental Linux server. You would be well advised to test your submission on that platform before submitting it. Depending on the TA policy for your quarter, submissions that do not work on these servers may lose points.
Long ago, when interaction was through mechanical teletypes with moving print-heads and paper rolls, these ASCII codes had the following meanings:
| 0x0D | Carriage Return (or '\r' in C) meant move the printing head back to the left edge of the paper. |
| 0x0A | Line Feed (or '\n' in C) meant move the paper upwards one line. |
| 0x04 | End Of File meant there is no more input. |
So every line of text ended with <cr><lf> or 0x0D 0x0A. This is still the case in Windows. Other people felt it was archaic foolishness to require two characters to indicate the end of line, and suggested that <lf> or 0x0A (renamed newline) should be all that was required. This is how files are represented in UNIX descendants. But when output is sent to a virtual terminal, the newline is still translated into the two distinct motion characters <cr> and <lf>. Thus:
DOS systems used to put a 0x18 (^Z) as the last character of a file to indicate END OF FILE, while most other operating systems simply ended the data (subsequent reads return nothing).
Points for this project will be awarded:
| value | feature |
|---|---|
| Packaging and build (15% total) | |
| 5% | un-tars expected contents |
| 5% | clean build w/default action (no warnings) |
| 3% | Makefile has clean and dist targets |
| 2% | reasonableness of README contents |
| Basic Functionality (35% total) | |
| 5% | correctly detects/reports bad arguments |
| 5% | correctly detects/reports system call failures |
| 5% | correctly changes console to character-at-a-time, no-echo mode |
| 5% | correctly restores terminal modes on exit |
| 5% | keyboard input echoed one character at a time |
| 5% | keyboard input written to screen one character at a time |
| 5% | correct <cr> and <lf> handling |
| --shell Functionality (50% total) | |
| 10% | forks a process and execs specified shell |
| 10% | correctly passes keyboard input to screen or shell |
| 10% | correctly passes output from shell to screen |
| 5% | ^D sends EOF to shell |
| 5% | ^C sends SIGINT to shell |
| 5% | correct <cr> and <lf> handling |
| 5% | report shell exit status on exit |