MMotor OS
Developing

Hello Motor OS

In this example we compile the final project of the Rust Book, a multithreaded web server, for Motor OS, with one small change, copy it into a running Motor OS VM, and open it from the host.

You need a Motor OS build with its toolchain and a VM started with run-qemu.sh; see Building Motor OS and Running Motor OS.

The program

Create the project inside the Motor OS checkout, so that the repository's rust-toolchain.toml selects the Motor toolchain for it (build/ is ignored by git):

cd "$MOTORH/motor-os"
mkdir -p build/examples
cargo new build/examples/hello
cd build/examples/hello

Replace src/main.rs with this:

use hello::ThreadPool;
use std::fs;
use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;
use std::thread;
use std::time::Duration;

fn main() {
    let listener = TcpListener::bind("0.0.0.0:5542").unwrap();
    let pool = ThreadPool::new(4);

    for stream in listener.incoming().take(2) {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }

    println!("Shutting down.");
}

fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 1024];
    stream.read(&mut buffer).unwrap();

    let get = b"GET / HTTP/1.1\r\n";
    let sleep = b"GET /sleep HTTP/1.1\r\n";

    let (status_line, filename) = if buffer.starts_with(get) {
        ("HTTP/1.1 200 OK", "hello.html")
    } else if buffer.starts_with(sleep) {
        thread::sleep(Duration::from_secs(5));
        ("HTTP/1.1 200 OK", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND", "404.html")
    };

    let contents = fs::read_to_string(filename).unwrap();

    let response = format!(
        "{}\r\nContent-Length: {}\r\n\r\n{}",
        status_line,
        contents.len(),
        contents
    );

    stream.write_all(response.as_bytes()).unwrap();
    stream.flush().unwrap();
}

This is the Rust Book's code with a single change: instead of TcpListener::bind("127.0.0.1:7878") we call TcpListener::bind("0.0.0.0:5542"), so that the listener is reachable on the VM's network address, not only on loopback.

Then create src/lib.rs, exactly as in the Rust Book:

use std::{
    sync::{mpsc, Arc, Mutex},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: Option<mpsc::Sender<Job>>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool {
            workers,
            sender: Some(sender),
        }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.as_ref().unwrap().send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        drop(self.sender.take());

        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || loop {
            let message = receiver.lock().unwrap().recv();

            match message {
                Ok(job) => {
                    println!("Worker {id} got a job; executing.");

                    job();
                }
                Err(_) => {
                    println!("Worker {id} disconnected; shutting down.");
                    break;
                }
            }
        });

        Worker {
            id,
            thread: Some(thread),
        }
    }
}

And the two pages the server reads, next to Cargo.toml. hello.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello!</title>
  </head>
  <body>
    <h1>Hello!</h1>
    <p>Hi from Motor OS</p>
  </body>
</html>

404.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello!</title>
  </head>
  <body>
    <h1>Oops!</h1>
    <p>Sorry, I don't know what you're asking for.</p>
  </body>
</html>

Build it for Motor OS

cargo build --release --target x86_64-unknown-motor

No toolchain selector is needed: the checkout's rust-toolchain.toml names the exact Motor toolchain the build installed. For a project outside the checkout, copy that file next to its Cargo.toml. See Toolchains. The result is target/x86_64-unknown-motor/release/hello.

Copy it into the VM

With the VM running, use SFTP through the SSH server. /user/bin is where an interactive session may install programs, and /user/tmp is scratch space; the server reads its two pages from the current directory, so give it a directory of its own:

KEY="$MOTORH/motor-os/vm_images/release/test.key"
ssh -p 2222 -o IdentitiesOnly=yes -i "$KEY" [email protected] /system/bin/mkdir /user/tmp/hello
scp -P 2222 -o IdentitiesOnly=yes -i "$KEY" \
    target/x86_64-unknown-motor/release/hello hello.html 404.html \
    [email protected]:/user/tmp/hello/
ssh -p 2222 -o IdentitiesOnly=yes -i "$KEY" [email protected] /system/bin/chmod 755 /user/tmp/hello/hello

test.key is in the image directory next to run-qemu.sh (src/tests/test.key is the same key). scp carries the executable bit over; the chmod makes sure of it.

Run it

On the VM's console, or over ssh-into-motor-os-vm.sh:

rush:/$ cd /user/tmp/hello
rush:/user/tmp/hello$ ./hello

Or start it from the host in one command:

ssh -p 2222 -o IdentitiesOnly=yes -i "$KEY" [email protected] \
    /system/bin/rush -c 'cd /user/tmp/hello && ./hello'

On the host, open http://192.168.4.2:5542. You should see the hello.html page above, served from Motor OS. A request to /sleep is answered after five seconds. As in the book, the server accepts two connections and then shuts down its pool and exits; a browser may open more than one connection per page, so it can exit sooner than you expect.

To ship a program with an image instead of copying it in, add the files to the image description in src/imager/ (or to a directory under img_files/ that the description already includes) and rebuild the image.