COSC 519 - Programming Project 1

(project has been developed by Prof. Marvin Solomon – Univ. of Wisconsin Madison)

Due:

Sept. 27, at the beginning of the class. The project will be done in teams of at most 3 students. You will hand me a printout with the source program, the script file (see below)  and a floppy with the source file. Put everything in an envelope ( I cannot handle loose papers and disks).


Contents


Introduction

The purpose of this assignment is to introduce you to Java programming and some of the basic aspect of system programming. 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. Your shell will also support a service similar to the 'alias' utility of the UNIX shell. The alias utility creates or redefines alias definitions or writes the values of existing alias definitions to standard output. An alias definition provides a string value that replaces a command name when it is encountered.

You do not need to implement pipes or re-direction of standard input and standard output, but 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

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 under 300 lines, including comments. Don't forget, this project is meant primarily to be a "get acquainted with Java" exercise.

 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), replaces aliases, 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. Splitting the individual commands into words is almost trivial with the StringTokenizer class found in java.util. Once you have identified the word that contains the command string, you can look it up in the alias table the shell maintains. If found in the table, the command name will be replaced with the string value from the table.

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. Note that if the command is the 'alias' command, it has to be executed by the shell.

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 if 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. You do not need to be able to run commands that read from their standard input. By the way, notice 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. Don't blame me; it's not my fault!

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() and 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 carry out the commands on the line. It will then wait until the threads have finished before continuing its own execution. There are two ways to start threads in Java. The first is to derive your class from the Thread class and then override its run() function. The second is to use the Runnable interface (pp. 177-178). With this approach, you create a class that implements Runnable and pass an instance of this class into the constructor of a new thread object. Although the first approach seems simpler at first, it is has some pitfalls, so we recommend using the second approach. Any Java book or tutorial should have examples illustrating both approaches.

To handle 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, we recommend you 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 the Java tutorial 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 this project you will work in teams having at most 3 people. If you prefer you can also work individually. Of course all the members of the team should contribute. I may choose a team and student in the team to present the project (the general structure of the program and the code itself).

You will hand me in paper form a brief presentation of the project, the Java code, and the transcript file (see below). You will also hand me a floppy disk containing .java file that you obtain after compilation. Your program should run on triton.

The transcript of a terminal session demonstrates your shell's ability to perform as specified . To obtain it 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

Print the typescript file and hand it to me.

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 also give me the testcommands  file.

Be sure that you use test data adequate to exercise your program's capabilities. You should follow good-style programming including top-down design, good indentation, meaningful variable names, modularity, and helpful comments. You will be graded not only on the basis of correctness, but also programming style and completeness of test data.


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