CS 537 - Spring 2004
Programming Assignment 1

Due: Wednesday, Feb 4 at 1:00 am.


Note: There is a Frequently Asked Questions (FAQ) page for this project. It should be considered an integral part of the specifications. Check it from time to time, since new items may be added.

Contents


Introduction

The purpose of this assignment is to refresh your skills in Java programming and introduce you to threads. You are to implement a simple shell (command interpreter) that behaves similarly (but not identically) to the UNIX shell. When you type in a command (in response to its prompt), it will create a thread that will execute the command you entered. Multiple commands can be entered on a single line, separated by `&' (ampersand) characters. Your shell will run them all concurrently and prompt for more user input when they have all finished.

You do not need to implement pipes or re-direction of standard input and standard output, and & at the end of a command has no special meaning (it does not mean to run the command in the "background"). You must be able to handle an arbitrary number of commands per line -- each with an arbitrary number of arguments separated by arbitrary amounts of white space (blanks or tabs). Your program should recover gracefully from such errors as unknown commands by printing an error message and continuing.

Suggestions

Note: Addtional hints and explanations may be found on the FAQ (Frequently Asked Questions) page for this project. You should get in the habit of checking this page daily, as new hints and clarifications may be added at any time.

This project is not as hard as it may seem at first reading, because most of the hard parts have already been done for you by the standard Java library. It is simply a matter of finding the relevant library routines and calling them properly. Your finished program will probably be about 200 lines, including comments. Don't forget, this project is meant primarily to be a "get acquainted with Java and threads" exercise. It is worth only 2% of your grade for the course.

The public static void main() procedure in your primary class will be quite simple. It will be an infinite loop1 that prints a prompt, reads a line, parses it (breaks it up into its constituent commands), starts a new thread to handle each of the different commands, and then waits for all the threads to finish before printing the next prompt.

Parsing

For parsing, you may find it easier to read the entire line into a String object. The stream System.in, which gets the keyboard input, is of type InputStream, so it can read either single bytes or arrays of bytes. You could represent an input line as an array of bytes, but you will find it much easier to use a String instead. You may want to look up the class BufferedReader to figure out how to read a line into a String. Splitting the line into commands separated by & characters is easily accomplished with the methods indexOf and substring in class String, or with StringTokenizer(str,delim). Splitting the individual commands into words is almost trivial with StringTokenizer.

Commands

Once you have split a command into words, it is easy to get the system to execute it with the aid of the classes Runtime and Process. Use r = Runtime.getRuntime() to get a reference to a Runtime object, and then call p = r.exec(argv) to run a command. Here argv is an array of Strings containing the words of the command (the command name itself is argv[0]) and the result p is a reference to a Process object.

There's one small catch. The output of the command will disappear down a black hole unless you go to the trouble of getting it and printing it to the screen. The way to get the standard output of the command is to call p.getInputStream(), where p is the Process reference returned by Runtime.exec(). When you read from the resulting stream, you get the standard output from the child process. Some commands send some of their output to an alternative output stream called the "standard error stream". For example, the command cat foo sends the contents of file foo to standard output, but the file foo doesn't exist, cat prints an error message to its standard error stream. You can get the standard error output by calling getErrorStream().

You can also feed characters to the standard input of the process with getOutputStream(), but that is not required for this project.2 You do not need to be able to run commands that read from their standard input.

A process may produce both standard and error output and they may come in any order, even interleaved, so you will need to use two threads (per command) to print them both out.

The exit command should cause your program to terminate immediately. It will not work to use Runtime.exec for exit (why not?), so you must look for it explicitly and use System.exit() to terminate the program. As with any other concurrent execution, if the exit command is run concurrently with other commands, the exact ordering of events is unpredictable. For example, in

    cat foo & exit 
your shell may terminate before or after displaying the contents of file foo or even half-way through.

Using Threads

Your primary class will read a command line from a user and create threads to carry out the commands on the line. It will then wait until all the threads have finished before continuing its own execution. For this purpose, you will need a Runnable class Command so that you can create a Thread using code such as 3

    Thread t = new Thread(new Command(/* whatever */));
    t.start();
    /* and later ... */
    try {
        t.join();
    } catch (Interrupted Exception e) {
        e.printStackTrace():
    }

To display both the standard and error output of the commands you will need at least two threads per command. You may find it simpler to use three threads: one thread for standard output, one for standard error, and a master thread to create them and wait for them to finish. If you find all of this very confusing, forget about standard error, only dump standard output, and only create one thread per command. Once you get this version debugged, you will probably not have much trouble modifying your program to handle standard error properly.

Exceptions

Java requires you to place within a try block any methods that might cause an exception. Following the try block is a catch clause (or catch clauses) that will be used to catch any exceptions that have been thrown See Java for C++ Programmers and Sections 1.13 and 10.12 of the Java book for more information about exceptions. Your code should deal with exceptions in an appropriate manner. For example, exceptions such as attempting to open a file that does not exist should result in a message to the user and the continuation of the program. More serious exceptions may require an error message followed by program termination (using System.exit()).

Grading

Note: For the remaining projects in this course, students will be working in two-person teams, but for this project, each person should work alone.

For this project, the grading will be 50% correctness (correctly implmenting the specifications), 40% style, and 10% testing. Later projects will have less emphasis on style, but we want to get you started off on the right foot and break any bad habits you may have. In particular,

These are the highlights, but there are more conventions you should use to make your code readable to a experienced Java programmer who is used to the conventions, not to your personal favorite style. For more advice on Java programming style, see Java for C++ Programmers and Code Conventions for the Java Programming Language.

Be sure that you use test data adequate to exercise your program's capabilities. Test both normal situtations and "boundary" cases: empty commands, very long commands, etc. In some cases, poor testing can cost you points not only for "testing", but also for "correctness"; if a feature is never tested, the grader may assume it doesn't work.

You may debug your program on any computer you have access to that supports Java, but you should test that it works correctly on the tux and nova machines. Despite the Java hype about "Write once, run anywhereTM", Java implementations on these machines do behave somewhat differently.

If you are registered for this class, you will find that you have access to a directory ~cs537-2/handin/NAME, where NAME is your login name. For example, if your login is lab, your handin directory is ~cs537-2/handin/lab. Your handin directory has six subdirectories: P1, P2, P3, P4, P5, and late. For this assignment, use directory P1. Copy all your .java source files and any other required files into the handin directory. Do not submit any .class files. After the deadline for this project you will be prevented from making any changes in this directory. If your assignment is late, put it into the late subdirectory and send email to me for permission to have it accepted.

Hand in your source program and a transcript of a terminal session which demonstrates your shell's ability to perform as specified (use the Unix command script(1)): Simply type the command "script". You will see the message

    Script started, file is typescript 
After that, everything you type in and everything sent to the screen will be saved in the file "typescript". When you're done with your demo, type "exit" (to the Unix shell, not to your program!), and you should get the message
    Script done, file is typescript 
Copy the typescript file to the handin directory.

Since it is tedious to type the same sequence of commands to you shell over and over, you might want to create a file of commands, say testcommands, and use it to drive your program

    java Project1 < testcommands 
Note that if you do that, the commands themselves will not appear in the output, only prompts and the results of running the commands. That's ok, but don't forget to copy the file testcommands to the handin directory. Be careful that your program correctly handles end-of-file as explained in the FAQ.

When a command line has multiple commands, they must be run concurrently. If they produce output, the output may be interleaved in arbitrary ways. However, with commands like cat, you may not see any interleaving unless the files being printed are very long. To make the concurrency easier to see, we have provided a program GenOutput that produces output on both the standard output and standard error streams, randomly switching back and forth and introducing delays. To use it, visit the page GenOutput.java with your web browser and save the file as GenOutput.java with the "Save As..." command from the File menu, or copy the file /u/c/s/cs537-2/public/html/examples/GenOutput.java to your directory. Compile it with the command

    jikes GenOutput.java 
Then type
    java GenOutput junk 1000
as an input line to your command interpreter. You should see stdout and stderr output randomly mixed. Similarly,
    java GenOutput one 1000 & java GenOutput two 1000 
should mix the outputs of the two commands together.

1In other courses, a program with an infinite loop is considered a bad thing, but in Operating systems, it's the norm!

2Notice that the names are backwards: You use getOutputStream to connect to the standard input of the process and getInputStream to connect to the standard output. The names were apparently chosed because getInputStream returns an InputStream. Then again, so does getErrorStream. Go figure.

3It is also possible to declare that Command extends Thread and use Thread t = new Command(/* whatever */), but we do not recommend this way of creating threads for reasons too complicated to go into here.


ganoop@cs.wisc.edu
Tue Jan 20 23:19:31 CST 2004