CSc 422 Assignment 2

Run Better

Assigned
Due

Sep 22
70 pts
Oct 8

This assignment involves making three changes the the posted runner example. You should be able to solve it in whatever environment you used for the first assignment: Unix, Linux (including VMs or WSL), or Mac should work fine. Start by grabbing the program and compiling it on your platform.

To get started, download this starting code. Unpack it into its own directory in the Unixy environment where asst1 can compile. Go there on the command line, and say make to build two executable files, the original runner and lbrun which we'll talk about later. (You may need to say instead gmake on Mac, but I don't think it will matter.) If you want to build the files using an IDE, that's fine, but it's your problem to make it work. You always have the option use it to edit, and build from the command line. Then, make the three improvements.

Error Reporting

Improve the reporting of errors by the program.

  1. Unix system calls generally return negative values (or sometimes NULL) to indicate failure. You should capture these, report the errors, and generally exit. Some very useful tools are errno and strerror. For instance, the existing fork call should become something like this:
    pid_t kidpid = fork(); if(kidpid < 0) { cerr << "Fork failed: " << strerror(errno) << endl; exit(10); }
    This will whine and exit if the call fails. Afterwards, test the variable kidpid to see if you are the child or parent. (Don't call fork again). You can use a similar pattern to improve the existing wait call, and use strerror() to report the reason for an exec failure.
  2. The wait call can also return the reason why the child process terminated. It does this through its parameter. So your wait call will look something like

    int status; kidpid = wait(&status);
    Use the return value to tell if the wait succeeded. If it did, then status will tell you what happened to the child process. Read the wait documentation for details. If the process was signaled, that means it crashed. If it exited, that means it didn't, though it also reports a status code to say if something went wrong. Zero is normal termination, nonzero is an error. Determine if the process exited or crashed. If exited, say if it exited normally or gave a code and print the code. For crashed, extract the signal number which gives the cause, and use the strsignal utility to get a nice message for printing it.

    The wait manual documents several tests that you can apply to the status after wait returns to see what happened. If instance, say if(WIFEXITED(status) to see if the process exited, rather than crashed. If so, the WEXITSTATUS(status) will tell you code it exited with.

    extrunner$ ./runner Your command? /bin/ls -l missing.txt ls: cannot access 'missing.txt': No such file or directory Abnormal exit code 2 extrunner$ ./runner Your command? /bin/ls -l runner.cpp -rw-rw-r--. 1 bennet bennet 1302 Sep 19 14:57 runner.cpp Normal exit extrunner$ ./runner Your command? boomer stk I'm going down!!!!! Killed: Segmentation fault

Parse An Input Line

The current runner asks separately for a command and a parameter, and can have only one. A command interpreter generally takes a line of input (a command) and breaks it up into pieces to provide the parameters for the command. You should do this. To simplify the task, you are given a class LineBreaker2. (Yes, there is an older version.) This object takes a C++ string and breaks it up into words and stores them as plain C strings in its peculiar way. There are three files, the header, linebreaker2.h, the implementation, linebreaker2.cpp, and a test driver lbrun.cpp. Read the comments in linebreaker2.h to see what the methods do, look at lbrun.cpp to see them used. Run lbrun and follow your nose to see some execution examples.

The purpose of this class is to help you modify the runner so it acts more like a command interpreter. Let your program ask for and read a single line, which is a command to run, then break it into separate parameters and run it.
tom@fedora:~/courses/cs422/asst/extrunner$ ./runner Your command? /bin/ls -l runner.cpp lbrun -rwxr-xr-x. 1 tom tom 182320 Sep 21 20:48 lbrun -rw-r--r--. 1 tom tom 1302 Sep 21 14:05 runner.cpp Normal exit

When you exec the program, you should use the execv form of exec. This takes two parameters, the full file name to execute, and an array of strings which are the parameters for the execution. This is an array of plain C strings, rather than C++ ones. By design, LineBreaker2 provides exactly what is needed. Subscript at 0 for the file name, and send the whole C array as the second parameter. This effectively sends the file name twice, but that is what exec expects.

Providing Input

The third change is to provide a file for the child process to write in place of the console. This a Unix (or perhaps originally Multics) invention which makes command use much more flexible. If the output of a program is long, you can direct the shell to save it in a file instead of printing it, so you can examine the file later. The feature is implemented on Unix and friends, and also Windows using the >, but, just to be different, we'll use a colon instead.
extrunner$ ./runner Your command? /bin/ls jobs.txt ls: cannot access 'jobs.txt': No such file or directory Abnormal exit code 2 extrunner$ ./runner Your command? /usr/bin/ps -e PID TTY TIME CMD 1 ? 00:00:24 systemd 2 ? 00:00:00 kthreadd 3 ? 00:00:00 pool_workqueue_release 4 ? 00:00:00 kworker/R-rcu_gp ... many lines omitted ... 155842 ? 00:00:00 kworker/u16:4-ext4-rsv-conversion 155843 ? 00:00:00 kworker/u16:5-ext4-rsv-conversion 155875 pts/5 00:00:00 runner 155877 pts/5 00:00:00 ps Normal exit extrunner$ ./runner Your command? /usr/bin/ps -e :jobs.txt Normal exit extrunner$ head -5 jobs.txt PID TTY TIME CMD 1 ? 00:00:24 systemd 2 ? 00:00:00 kthreadd 3 ? 00:00:00 pool_workqueue_release 4 ? 00:00:00 kworker/R-rcu_gp extrunner$
So, the first time I run the process listing command, it types many lines to the console. The second time, I specified an output file. The runner program arranged that output from ps will go into that file, which I can then examine more carefully with a text editor or other tools. So:
  1. After entering and parsing the command, see if the last string on the line starts with a colon.
  2. If it does, remove the file name from the parameter list, then take the (former) last parameter and remove the colon to extract the file name. Assuming your LineBreaker2 object is called lb, I recommend this procedure:
    char *fn = lb.pop_back(); fn++;
    This uses the fact that we are using the plain C representation of a string as a pointer to the first character. (The actual storage space is allocated inside the LineBreaker2 object.) Then, simply incrementing the pointer makes it point to the second character, omitting the first from the string.
  3. If the last word on the line was an input file, perform kernel file open. This is neither the plain C fopen call, nor the C++ ifstream::open. It looks like this:
    int fd = open(fn, O_WRONLY|O_CREAT|O_TRUNC, 0644);
    You will also need to #include fcntl.h. The returned value fd is called a file descriptor, and refers to the record of the created stream within the PCB for the process. The open may fail, so if fd is negative, print an error and exit.

    The second parameter specifies write only, create the file if it does not already exist, and discard any existing contents if it does. This is what most any higher-level open would send for you. The third parameter limits the final permissions of the file, subject to the setting for your login, which is undoubtedly some default no one ever asked you about.

    Be careful with this. When the above open call runs with the name of an existing file, the file will be clobbered without asking. Watch out while testing.

  4. Open the file before running fork. In the child portion of your code, after testing the fork return but before the exec, change the process output stream to be the file you opened. The required incantation is
    dup2(fd, 1); close(fd);
    The numbers fd and 1 refer to open file streams. Traditionally, they index a table in the process control block. The number 1 is the standard output stream, fd is the number assigned to the file when you opened earlier. The first call, makes the #1 entry an alias for this file, and the close removes the old entry which we no longer need.

    Make sure not to do any of this if no input file was specified; don't break the simpler case.

  5. Since this change applies to the child process created by fork, and the program you run with exec runs inside this process, the child will run with its output changed to the file you opened.
  6. On the parent side, when fork() returns nonzero, close the file descriptor. The parent will not be using it.

Enough improvements. Test these to make sure they work.

When your program works correctly and looks nice, submit it here.