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.