Part 5. Controlling terminals (40 points)

The final thing to add to ps is printing a human-readable description of the controlling terminal (the TTY column).

In this part, you’re going to create a new DeviceMap struct which you will use to convert the “controlling terminal” integer to a string like pts/3.

The controlling terminal integer has two parts, a minor number that’s in the range [0, 255] and a major number. The integer is computed as major * 256 + minor. The major number identifies the type of “character device.”

Linux exposes the mapping between major number and device name in the file /proc/devices. Go ahead and take a look at that file to see how it is structured.

Your task

Create a new file src/device_map.rs (and add a module declaration in lib.rs) and define a new DeviceMap struct. This struct should contain a std::collections::HashMap<i32, String> field. (You’ll probably want to use std::collections::HashMap.) Don’t forget to make DeviceMap public.

You’re going to implement a public constructor named new() and a public lookup() method.

#![allow(unused)]
fn main() {
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
struct DeviceMap;
impl DeviceMap {
    pub fn new() -> Result<Self> {
        todo!()
    }

    pub fn lookup(&self, tty_nr: i32) -> String {
        let major = tty_nr >> 8;
        let minor = tty_nr & 0xFF;
        todo!()
    }
}
}

(You will need to use super::Result; to have access to your Result type).

The new() function should open /proc/devices and read lines corresponding to the Character devices section of the file and insert the names of the devices into the hash map using the integer value as the key. See the HashMap documentation for the methods you can use.

Tip

To simplify your implementation, you may want to use this code

let devices = std::fs::read_to_string("/proc/devices")
    .map_err(|err| format!("/proc/devices: {err}"))?;

for line in devices
    .lines()
    .skip_while(|&line| line != "Character devices:")
    .skip(1)
    .take_while(|&line| !line.is_empty())
{
    todo!("Split the line on whitespace and insert into the map")
}

This reads /proc/devices into a String named devices. Then it iterates over the lines by (1) skipping lines until Character devices: is found; (2) skipping that line; and (3) only returning the lines that are nonempty.

There are duplicate numbers in /proc/devices. That’s okay, just insert them into the hash map one at a time. The earlier values will be replaced by the later values.

What remains is to finish the implementation of lookup(). The provided code above extracts the major and minor number. Look up the major number in the hash map. If it’s not found, return the string "?". If it is found, then return format!("{device}/{minor}") (where device is the returned value from the hash map).

The final step is to update ps to create a DeviceMap in run() and when printing the TTY column, call the lookup() method to get the name of the TTY.

Here’s some sample output as compared to the real ps.

$ cargo run --bin ps --quiet
       PID TTY             TIME CMD
   1521440 pts/3       00:00:00 bash
   1523501 pts/3       00:00:00 ps

$ ps
    PID TTY          TIME CMD
1521440 pts/3    00:00:00 bash
1523768 pts/3    00:00:00 ps