Skip to content

Project05 - Octox User Programs and System Calls

Due Mon May 11th by 11:59pm in your Project05 GitHub repo

Tests: https://github.com/USF-CS631-S26/tests

Background

You have just walked through the UNIX system call interface in the UNIX System Calls lecture and the nine ex_* example programs in src/user/bin/. This project is the follow-on: write your own Octox user programs that use those same syscalls to build small versions of familiar UNIX tools.

You will write four new user programs and modify the Octox shell to add a history builtin. Each program is a .rs file under src/user/bin/ plus a matching [[bin]] entry in src/user/Cargo.toml. The Octox Guide walks through how to add a user program from scratch if you have not done so before.

Setup

Clone the Octox repo into your Project05 GitHub repo. Build it once to confirm your toolchain works:

$ cd octox
$ cargo build --target riscv64gc-unknown-none-elf
$ cargo run --target riscv64gc-unknown-none-elf

You should land at the $ shell prompt. Ctrl-A x exits QEMU.

Requirements

  1. Write four new user programs: seq, tail, diff, and pipecount.
  2. Modify src/user/bin/sh.rs to add a history builtin.
  3. Each new program must have a matching [[bin]] entry in src/user/Cargo.toml so the build system picks it up. The file system strips the leading _ so the binary appears at /bin/<name>.
  4. All programs must produce output matching the automated tests exactly.
  5. Use the ulib::sys syscalls and helpers (File, BufReader, Command, etc.) — do not pull in additional crates.

Section 1: seq N

Print the integers 1 through N, one per line. If N is 0 the program produces no output. The argument is a non-negative decimal integer.

Examples:

$ seq 5
1
2
3
4
5

$ seq 1
1

$ seq 0

Hints: Look at src/user/bin/echo.rs for argv parsing and src/user/bin/ex_args.rs from the lecture for the simplest possible write-loop pattern. The println! macro from ulib ultimately calls sys::write(STDOUT_FILENO, ...).

Section 2: tail [-n N] FILE

Print the last N lines of FILE. The default for N is 10. If the file has fewer than N lines, print the entire file.

Examples:

$ seq 12 > /t
$ tail /t
3
4
5
6
7
8
9
10
11
12

$ tail -n 3 /t
10
11
12

$ tail -n 1 /t
12

$ tail -n 20 /t
1
2
3
4
5
6
7
8
9
10
11
12

Hints: Mirror src/user/bin/head.rs. Use BufReader::new(File::open(path)) and either read_line or the lines() iterator from ulib::io::BufRead. The simplest implementation reads every line into a Vec<String> then prints the suffix.

Section 3: diff FILE1 FILE2

Read both files line-by-line and emit a record for every line index where the two files differ. The record format is:

line N
< {line from FILE1}
---
> {line from FILE2}

If one file has more lines than the other, treat the missing lines as empty. When a side is empty, print < or > alone (no trailing space). If the two files are identical, produce no output.

Examples:

$ seq 3 > /a
$ seq 3 > /b
$ diff /a /b

$ seq 3 > /a
$ seq 5 > /b
$ diff /a /b
line 4
<
---
> 4
line 5
<
---
> 5

$ seq 5 > /a
$ seq 3 > /b
$ diff /a /b
line 4
< 4
---
>
line 5
< 5
---
>

Hints: Open both files, read all lines from each into a Vec<String>, then iterate up to the longer of the two. The pattern from head.rs / tail.rs carries over — you are doing it twice.

Section 4: pipecount CMD1... : CMD2...

Run CMD1 | CMD2, but with the parent process sitting in the middle counting bytes:

CMD1  ──►  pipe1  ──►  parent  ──►  pipe2  ──►  CMD2
                       (counts)

After both children exit, print pipecount: N bytes to your stdout where N is the total number of bytes that flowed from CMD1 into the parent (equivalently, the bytes the parent forwarded into CMD2).

The two commands are separated by a single literal : argument. Each side may have its own arguments. Examples:

$ pipecount seq 5 : wc
l=5, w=5, c=10
pipecount: 10 bytes

$ seq 5 > /a
$ pipecount cat /a : wc
l=5, w=5, c=10
pipecount: 10 bytes

Required syscalls (use these directly from ulib::sys):

  • sys::pipe — create the two pipes
  • sys::fork — twice, once per child
  • sys::dup2 — wire each child's stdout/stdin to the right pipe end
  • sys::close — close every pipe end you do not use, in every process
  • sys::exec — start each child program
  • sys::read / sys::write — forward bytes through the parent
  • sys::wait — twice, to reap both children

Hints: src/user/bin/ex_pipe2.rs is the recipe for a 1-pipe pipeline (ls | wc). Your pipecount is the same idea with two pipes — the parent sits between them, looping read from pipe1[0] and write into pipe2[1]. After CMD1 closes its write end, your read on pipe1[0] returns Ok(0) (EOF) and you must close(pipe2[1]) so CMD2 sees its own EOF on stdin. Forgetting any close will deadlock the pipeline.

To exec a command from a vector of args, build the path "/bin/<name>" and pass the original &[&str] argv directly to sys::exec. (Octox's mkfs strips the leading _, so _seq lands at /bin/seq.)

Section 5: Add a history builtin to sh

Modify src/user/bin/sh.rs so that the shell records every non-empty command line the user enters and provides a history builtin that prints those lines, numbered starting at 1, in entry order. The history command itself counts as an entry — it appears as the most recent line in its own output.

Example:

$ echo hello
hello
$ echo world
world
$ history
1 echo hello
2 echo world
3 history

Hints: The existing shell already special-cases cd, export, and exit (around line 49 of sh.rs). Add history alongside them. Track entries in a local Vec<String> declared just before the main loop and push to it after each read_line. Only the parent process should print the history (so it does not run inside a piped subprocess) — guard with the existing if num == 0 check used by the other builtins.

Building and Running

In your project repo:

# build everything (kernel + user programs)
$ cargo build --target riscv64gc-unknown-none-elf

# boot the OS in QEMU; lands at the $ prompt
$ cargo run --target riscv64gc-unknown-none-elf

# exit QEMU
Ctrl-A x

To run a single command end-to-end (the way the autograder tests it), use runoctox.py:

$ python3 runoctox.py "seq 5"
$ seq 5
1
2
3
4
5

Each argument to runoctox.py is one shell command. Tests that need fixture files chain them in a single invocation:

$ python3 runoctox.py "seq 5 > /t" "tail -n 2 /t"
$ seq 5 > /t
$ tail -n 2 /t
4
5

Tips and Pitfalls

  • println! needs print! in scope. Add both to your use line: use ulib::{print, println, ...};. The macro expands to a print! call.
  • Read until EOF. sys::read may return fewer bytes than the buffer holds. Ok(0) is the only EOF signal. Loop until you see it. (ex_count in the lecture is the canonical example.)
  • Close every pipe end you don't use. This is the most common source of "my pipeline hangs forever" bugs. The parent in pipecount holds both ends of both pipes after pipe(); if you forget to close pipe2[1] after EOF, CMD2 will wait for input that never comes.
  • exec does not return on success. If you reach the line after sys::exec, exec failed. Always follow it with sys::exit(1) (or a panic) so the path is well-defined.
  • The shell does not handle quotes. Tokens are split on whitespace only. Do not write tests that depend on quoted strings as arguments.

Grading

Tests: https://github.com/USF-CS631-S26/tests

Grading is based on automated tests (100 points total):

Tests Points Description
seq-01, seq-02 10 Basic seq cases (5 each)
tail-01..03 15 Default N, custom N, N larger than file (5 each)
diff-01..03 15 Identical, /a longer, /b longer (5 each)
pipecount-01..03 30 seq through wc, cat through wc, larger seq (10 each)
history-01, -02 30 Basic history, longer history (15 each)
Total 100

Code Quality

Code quality deductions may be applied and can be earned back. We are looking for:

  • Consistent spacing and indentation
  • Consistent naming and commenting
  • No commented-out ("dead") code
  • No redundant or overly complicated code
  • A clean repo, that is no build products, extra files, etc.