Initial commit
This commit is contained in:
commit
e1aa7f4345
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) [year] [fullname]
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
6
README.md
Normal file
6
README.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# CS 110L Spring 2020 starter code
|
||||||
|
|
||||||
|
Assignment handouts are available [here](https://reberhardt.com/cs110l/spring-2020/).
|
||||||
|
|
||||||
|
Trying out these assignments? Adapting these for a class? [Please let us
|
||||||
|
know](mailto:ryan@reberhardt.com); we'd love to hear from you!
|
||||||
13
proj-1/.gitignore
vendored
Normal file
13
proj-1/.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/deet/.cargo/
|
||||||
|
/deet/target/
|
||||||
|
/deet/Cargo.lock
|
||||||
|
.*.swp
|
||||||
|
.deet_history
|
||||||
|
.bash_history
|
||||||
|
/deet/samples/sleepy_print
|
||||||
|
/deet/samples/segfault
|
||||||
|
/deet/samples/hello
|
||||||
|
/deet/samples/function_calls
|
||||||
|
/deet/samples/exit
|
||||||
|
/deet/samples/count
|
||||||
|
.idea
|
||||||
16
proj-1/deet/Cargo.toml
Normal file
16
proj-1/deet/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "deet"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Ryan Eberhardt <reberhardt7@gmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nix = "0.17.0"
|
||||||
|
libc = "0.2.68"
|
||||||
|
rustyline = "6.1.2"
|
||||||
|
gimli = { git = "https://github.com/gimli-rs/gimli", rev = "ad23cdb2", default-features = false, features = ["read"] }
|
||||||
|
object = { version = "0.17", default-features = false, features = ["read"] }
|
||||||
|
memmap = "0.7"
|
||||||
|
addr2line = "0.11.0"
|
||||||
18
proj-1/deet/Dockerfile
Normal file
18
proj-1/deet/Dockerfile
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
FROM ubuntu:18.04
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y build-essential make curl strace gdb
|
||||||
|
|
||||||
|
# Install Rust. Don't use rustup, so we can install for all users (not just the
|
||||||
|
# root user)
|
||||||
|
RUN curl --proto '=https' --tlsv1.2 -sSf \
|
||||||
|
https://static.rust-lang.org/dist/rust-1.43.0-x86_64-unknown-linux-gnu.tar.gz \
|
||||||
|
-o rust.tar.gz && \
|
||||||
|
tar -xzf rust.tar.gz && \
|
||||||
|
rust-1.43.0-x86_64-unknown-linux-gnu/install.sh
|
||||||
|
|
||||||
|
# Make .cargo writable by any user (so we can run the container as an
|
||||||
|
# unprivileged user)
|
||||||
|
RUN mkdir /.cargo && chmod 777 /.cargo
|
||||||
|
|
||||||
|
WORKDIR /deet
|
||||||
10
proj-1/deet/Makefile
Normal file
10
proj-1/deet/Makefile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
SRCS = $(wildcard samples/*.c)
|
||||||
|
PROGS = $(patsubst %.c,%,$(SRCS))
|
||||||
|
|
||||||
|
all: $(PROGS)
|
||||||
|
|
||||||
|
%: %.c
|
||||||
|
$(CC) $(CFLAGS) -O0 -g -no-pie -fno-omit-frame-pointer -o $@ $<
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(PROGS)
|
||||||
23
proj-1/deet/container
Executable file
23
proj-1/deet/container
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
# Kill the existing deet container if one is running
|
||||||
|
docker rm -f deet &>/dev/null
|
||||||
|
|
||||||
|
# Start a container
|
||||||
|
docker run \
|
||||||
|
`# Give the container a name (so that it's easier to attach to with "docker exec")` \
|
||||||
|
--name deet \
|
||||||
|
`# Mount the current directory inside of the container, so cargo can access it` \
|
||||||
|
-v "${PWD}":/deet \
|
||||||
|
`# Set the container user's home directory to our deet directory` \
|
||||||
|
-e HOME=/deet \
|
||||||
|
`# Run as the current user (instead of root)` \
|
||||||
|
-u $(id -u ${USER}):$(id -g ${USER}) \
|
||||||
|
`# Allow ptrace` \
|
||||||
|
--cap-add=SYS_PTRACE \
|
||||||
|
`# When the container exits, automatically clean up the files it leaves behind` \
|
||||||
|
--rm \
|
||||||
|
`# Get an interactive terminal` \
|
||||||
|
-it \
|
||||||
|
`# Run the deet image` \
|
||||||
|
deet "$@"
|
||||||
10
proj-1/deet/samples/count.c
Normal file
10
proj-1/deet/samples/count.c
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
printf("1\n");
|
||||||
|
printf("2\n");
|
||||||
|
printf("3\n");
|
||||||
|
printf("4\n");
|
||||||
|
printf("5\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
5
proj-1/deet/samples/exit.c
Normal file
5
proj-1/deet/samples/exit.c
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
asm("syscall" :: "a"(60), "D"(0));
|
||||||
|
}
|
||||||
25
proj-1/deet/samples/function_calls.c
Normal file
25
proj-1/deet/samples/function_calls.c
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
int global = 5;
|
||||||
|
|
||||||
|
void func3(int a) {
|
||||||
|
printf("Hello from func3! %d\n", a);
|
||||||
|
}
|
||||||
|
|
||||||
|
void func2(int a, int b) {
|
||||||
|
printf("func2(%d, %d) was called\n", a, b);
|
||||||
|
int sum = a + b;
|
||||||
|
printf("sum = %d\n", sum);
|
||||||
|
func3(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
void func1(int a) {
|
||||||
|
printf("func1(%d) was called\n", a);
|
||||||
|
func2(a, global);
|
||||||
|
func3(100);
|
||||||
|
printf("end of func1\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
func1(42);
|
||||||
|
}
|
||||||
6
proj-1/deet/samples/hello.c
Normal file
6
proj-1/deet/samples/hello.c
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
printf("Hello world!\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
16
proj-1/deet/samples/segfault.c
Normal file
16
proj-1/deet/samples/segfault.c
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
void func2(int a) {
|
||||||
|
printf("About to segfault... a=%d\n", a);
|
||||||
|
*(int*)0 = a;
|
||||||
|
printf("Did segfault!\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
void func1(int a) {
|
||||||
|
printf("Calling func2\n");
|
||||||
|
func2(a % 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
func1(42);
|
||||||
|
}
|
||||||
16
proj-1/deet/samples/sleepy_print.c
Normal file
16
proj-1/deet/samples/sleepy_print.c
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
unsigned long num_seconds;
|
||||||
|
if (argc != 2 || (num_seconds = strtoul(argv[1], NULL, 10)) == 0) {
|
||||||
|
fprintf(stderr, "Usage: %s <seconds to sleep>\n", argv[0]);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
for (unsigned long i = 0; i < num_seconds; i++) {
|
||||||
|
printf("%lu\n", i);
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
92
proj-1/deet/src/debugger.rs
Normal file
92
proj-1/deet/src/debugger.rs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
use crate::debugger_command::DebuggerCommand;
|
||||||
|
use crate::inferior::Inferior;
|
||||||
|
use rustyline::error::ReadlineError;
|
||||||
|
use rustyline::Editor;
|
||||||
|
|
||||||
|
pub struct Debugger {
|
||||||
|
target: String,
|
||||||
|
history_path: String,
|
||||||
|
readline: Editor<()>,
|
||||||
|
inferior: Option<Inferior>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debugger {
|
||||||
|
/// Initializes the debugger.
|
||||||
|
pub fn new(target: &str) -> Debugger {
|
||||||
|
// TODO (milestone 3): initialize the DwarfData
|
||||||
|
|
||||||
|
let history_path = format!("{}/.deet_history", std::env::var("HOME").unwrap());
|
||||||
|
let mut readline = Editor::<()>::new();
|
||||||
|
// Attempt to load history from ~/.deet_history if it exists
|
||||||
|
let _ = readline.load_history(&history_path);
|
||||||
|
|
||||||
|
Debugger {
|
||||||
|
target: target.to_string(),
|
||||||
|
history_path,
|
||||||
|
readline,
|
||||||
|
inferior: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(&mut self) {
|
||||||
|
loop {
|
||||||
|
match self.get_next_command() {
|
||||||
|
DebuggerCommand::Run(args) => {
|
||||||
|
if let Some(inferior) = Inferior::new(&self.target, &args) {
|
||||||
|
// Create the inferior
|
||||||
|
self.inferior = Some(inferior);
|
||||||
|
// TODO (milestone 1): make the inferior run
|
||||||
|
// You may use self.inferior.as_mut().unwrap() to get a mutable reference
|
||||||
|
// to the Inferior object
|
||||||
|
} else {
|
||||||
|
println!("Error starting subprocess");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DebuggerCommand::Quit => {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function prompts the user to enter a command, and continues re-prompting until the user
|
||||||
|
/// enters a valid command. It uses DebuggerCommand::from_tokens to do the command parsing.
|
||||||
|
///
|
||||||
|
/// You don't need to read, understand, or modify this function.
|
||||||
|
fn get_next_command(&mut self) -> DebuggerCommand {
|
||||||
|
loop {
|
||||||
|
// Print prompt and get next line of user input
|
||||||
|
match self.readline.readline("(deet) ") {
|
||||||
|
Err(ReadlineError::Interrupted) => {
|
||||||
|
// User pressed ctrl+c. We're going to ignore it
|
||||||
|
println!("Type \"quit\" to exit");
|
||||||
|
}
|
||||||
|
Err(ReadlineError::Eof) => {
|
||||||
|
// User pressed ctrl+d, which is the equivalent of "quit" for our purposes
|
||||||
|
return DebuggerCommand::Quit;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
panic!("Unexpected I/O error: {:?}", err);
|
||||||
|
}
|
||||||
|
Ok(line) => {
|
||||||
|
if line.trim().len() == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.readline.add_history_entry(line.as_str());
|
||||||
|
if let Err(err) = self.readline.save_history(&self.history_path) {
|
||||||
|
println!(
|
||||||
|
"Warning: failed to save history file at {}: {}",
|
||||||
|
self.history_path, err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let tokens: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
if let Some(cmd) = DebuggerCommand::from_tokens(&tokens) {
|
||||||
|
return cmd;
|
||||||
|
} else {
|
||||||
|
println!("Unrecognized command.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
proj-1/deet/src/debugger_command.rs
Normal file
20
proj-1/deet/src/debugger_command.rs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
pub enum DebuggerCommand {
|
||||||
|
Quit,
|
||||||
|
Run(Vec<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DebuggerCommand {
|
||||||
|
pub fn from_tokens(tokens: &Vec<&str>) -> Option<DebuggerCommand> {
|
||||||
|
match tokens[0] {
|
||||||
|
"q" | "quit" => Some(DebuggerCommand::Quit),
|
||||||
|
"r" | "run" => {
|
||||||
|
let args = tokens[1..].to_vec();
|
||||||
|
Some(DebuggerCommand::Run(
|
||||||
|
args.iter().map(|s| s.to_string()).collect(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
// Default case:
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
226
proj-1/deet/src/dwarf_data.rs
Normal file
226
proj-1/deet/src/dwarf_data.rs
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
use crate::gimli_wrapper;
|
||||||
|
use addr2line::Context;
|
||||||
|
use object::Object;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use std::{fmt, fs};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
ErrorOpeningFile,
|
||||||
|
DwarfFormatError(gimli_wrapper::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DwarfData {
|
||||||
|
files: Vec<File>,
|
||||||
|
addr2line: Context<addr2line::gimli::EndianRcSlice<addr2line::gimli::RunTimeEndian>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for DwarfData {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "DwarfData {{files: {:?}}}", self.files)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<gimli_wrapper::Error> for Error {
|
||||||
|
fn from(err: gimli_wrapper::Error) -> Self {
|
||||||
|
Error::DwarfFormatError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DwarfData {
|
||||||
|
pub fn from_file(path: &str) -> Result<DwarfData, Error> {
|
||||||
|
let file = fs::File::open(path).or(Err(Error::ErrorOpeningFile))?;
|
||||||
|
let mmap = unsafe { memmap::Mmap::map(&file).or(Err(Error::ErrorOpeningFile))? };
|
||||||
|
let object = object::File::parse(&*mmap)
|
||||||
|
.or_else(|e| Err(gimli_wrapper::Error::ObjectError(e.to_string())))?;
|
||||||
|
let endian = if object.is_little_endian() {
|
||||||
|
gimli::RunTimeEndian::Little
|
||||||
|
} else {
|
||||||
|
gimli::RunTimeEndian::Big
|
||||||
|
};
|
||||||
|
Ok(DwarfData {
|
||||||
|
files: gimli_wrapper::load_file(&object, endian)?,
|
||||||
|
addr2line: Context::new(&object).or_else(|e| Err(gimli_wrapper::Error::from(e)))?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn get_target_file(&self, file: &str) -> Option<&File> {
|
||||||
|
self.files.iter().find(|f| {
|
||||||
|
f.name == file || (!file.contains("/") && f.name.ends_with(&format!("/{}", file)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn get_addr_for_line(&self, file: Option<&str>, line_number: usize) -> Option<usize> {
|
||||||
|
let target_file = match file {
|
||||||
|
Some(filename) => self.get_target_file(filename)?,
|
||||||
|
None => self.files.get(0)?,
|
||||||
|
};
|
||||||
|
Some(
|
||||||
|
target_file
|
||||||
|
.lines
|
||||||
|
.iter()
|
||||||
|
.find(|line| line.number >= line_number)?
|
||||||
|
.address,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn get_addr_for_function(&self, file: Option<&str>, func_name: &str) -> Option<usize> {
|
||||||
|
match file {
|
||||||
|
Some(filename) => Some(
|
||||||
|
self.get_target_file(filename)?
|
||||||
|
.functions
|
||||||
|
.iter()
|
||||||
|
.find(|func| func.name == func_name)?
|
||||||
|
.address,
|
||||||
|
),
|
||||||
|
None => {
|
||||||
|
for file in &self.files {
|
||||||
|
if let Some(func) = file.functions.iter().find(|func| func.name == func_name) {
|
||||||
|
return Some(func.address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn get_line_from_addr(&self, curr_addr: usize) -> Option<Line> {
|
||||||
|
let location = self
|
||||||
|
.addr2line
|
||||||
|
.find_location(curr_addr.try_into().unwrap())
|
||||||
|
.ok()??;
|
||||||
|
Some(Line {
|
||||||
|
file: location.file?.to_string(),
|
||||||
|
number: location.line?.try_into().unwrap(),
|
||||||
|
address: curr_addr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn get_function_from_addr(&self, curr_addr: usize) -> Option<String> {
|
||||||
|
let frame = self
|
||||||
|
.addr2line
|
||||||
|
.find_frames(curr_addr.try_into().unwrap())
|
||||||
|
.ok()?
|
||||||
|
.next()
|
||||||
|
.ok()??;
|
||||||
|
Some(frame.function?.raw_name().ok()?.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn print(&self) {
|
||||||
|
for file in &self.files {
|
||||||
|
println!("------");
|
||||||
|
println!("{}", file.name);
|
||||||
|
println!("------");
|
||||||
|
|
||||||
|
println!("Global variables:");
|
||||||
|
for var in &file.global_variables {
|
||||||
|
println!(
|
||||||
|
" * {} ({}, located at {}, declared at line {})",
|
||||||
|
var.name, var.entity_type.name, var.location, var.line_number
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Functions:");
|
||||||
|
for func in &file.functions {
|
||||||
|
println!(
|
||||||
|
" * {} (declared on line {}, located at {:#x}, {} bytes long)",
|
||||||
|
func.name, func.line_number, func.address, func.text_length
|
||||||
|
);
|
||||||
|
for var in &func.variables {
|
||||||
|
println!(
|
||||||
|
" * Variable: {} ({}, located at {}, declared at line {})",
|
||||||
|
var.name, var.entity_type.name, var.location, var.line_number
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Line numbers:");
|
||||||
|
for line in &file.lines {
|
||||||
|
println!(" * {} (at {:#x})", line.number, line.address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct Type {
|
||||||
|
pub name: String,
|
||||||
|
pub size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Type {
|
||||||
|
pub fn new(name: String, size: usize) -> Self {
|
||||||
|
Type {
|
||||||
|
name: name,
|
||||||
|
size: size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum Location {
|
||||||
|
Address(usize),
|
||||||
|
FramePointerOffset(isize),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Location {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Location::Address(addr) => write!(f, "Address({:#x})", addr),
|
||||||
|
Location::FramePointerOffset(offset) => write!(f, "FramePointerOffset({})", offset),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Location {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
fmt::Display::fmt(self, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For variables and formal parameters
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Variable {
|
||||||
|
pub name: String,
|
||||||
|
pub entity_type: Type,
|
||||||
|
pub location: Location,
|
||||||
|
pub line_number: usize, // Line number in source file
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct Function {
|
||||||
|
pub name: String,
|
||||||
|
pub address: usize,
|
||||||
|
pub text_length: usize,
|
||||||
|
pub line_number: usize, // Line number in source file
|
||||||
|
pub variables: Vec<Variable>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct File {
|
||||||
|
pub name: String,
|
||||||
|
pub global_variables: Vec<Variable>,
|
||||||
|
pub functions: Vec<Function>,
|
||||||
|
pub lines: Vec<Line>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Line {
|
||||||
|
pub file: String,
|
||||||
|
pub number: usize,
|
||||||
|
pub address: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Line {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}:{}", self.file, self.number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
614
proj-1/deet/src/gimli_wrapper.rs
Normal file
614
proj-1/deet/src/gimli_wrapper.rs
Normal file
@ -0,0 +1,614 @@
|
|||||||
|
//! This file contains code for using gimli to extract information from the DWARF section of an
|
||||||
|
//! executable. The code is adapted from
|
||||||
|
//! https://github.com/gimli-rs/gimli/blob/master/examples/simple.rs and
|
||||||
|
//! https://github.com/gimli-rs/gimli/blob/master/examples/dwarfdump.rs.
|
||||||
|
//!
|
||||||
|
//! This code is a huge mess. Please don't read it unless you're trying to do an extension :)
|
||||||
|
|
||||||
|
use gimli;
|
||||||
|
use gimli::{UnitOffset, UnitSectionOffset};
|
||||||
|
use object::Object;
|
||||||
|
use std::borrow;
|
||||||
|
//use std::io::{BufWriter, Write};
|
||||||
|
use crate::dwarf_data::{File, Function, Line, Location, Type, Variable};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use std::fmt::Write;
|
||||||
|
use std::{io, path};
|
||||||
|
|
||||||
|
pub fn load_file(object: &object::File, endian: gimli::RunTimeEndian) -> Result<Vec<File>, Error> {
|
||||||
|
// Load a section and return as `Cow<[u8]>`.
|
||||||
|
let load_section = |id: gimli::SectionId| -> Result<borrow::Cow<[u8]>, gimli::Error> {
|
||||||
|
Ok(object
|
||||||
|
.section_data_by_name(id.name())
|
||||||
|
.unwrap_or(borrow::Cow::Borrowed(&[][..])))
|
||||||
|
};
|
||||||
|
// Load a supplementary section. We don't have a supplementary object file,
|
||||||
|
// so always return an empty slice.
|
||||||
|
let load_section_sup = |_| Ok(borrow::Cow::Borrowed(&[][..]));
|
||||||
|
|
||||||
|
// Load all of the sections.
|
||||||
|
let dwarf_cow = gimli::Dwarf::load(&load_section, &load_section_sup)?;
|
||||||
|
|
||||||
|
// Borrow a `Cow<[u8]>` to create an `EndianSlice`.
|
||||||
|
let borrow_section: &dyn for<'a> Fn(
|
||||||
|
&'a borrow::Cow<[u8]>,
|
||||||
|
) -> gimli::EndianSlice<'a, gimli::RunTimeEndian> =
|
||||||
|
&|section| gimli::EndianSlice::new(&*section, endian);
|
||||||
|
|
||||||
|
// Create `EndianSlice`s for all of the sections.
|
||||||
|
let dwarf = dwarf_cow.borrow(&borrow_section);
|
||||||
|
|
||||||
|
// Define a mapping from type offsets to type structs
|
||||||
|
let mut offset_to_type: HashMap<usize, Type> = HashMap::new();
|
||||||
|
|
||||||
|
let mut compilation_units: Vec<File> = Vec::new();
|
||||||
|
|
||||||
|
// Iterate over the compilation units.
|
||||||
|
let mut iter = dwarf.units();
|
||||||
|
while let Some(header) = iter.next()? {
|
||||||
|
let unit = dwarf.unit(header)?;
|
||||||
|
|
||||||
|
// Iterate over the Debugging Information Entries (DIEs) in the unit.
|
||||||
|
let mut depth = 0;
|
||||||
|
let mut entries = unit.entries();
|
||||||
|
while let Some((delta_depth, entry)) = entries.next_dfs()? {
|
||||||
|
depth += delta_depth;
|
||||||
|
// Update the offset_to_type mapping for types
|
||||||
|
// Update the variable list for formal params/variables
|
||||||
|
match entry.tag() {
|
||||||
|
gimli::DW_TAG_compile_unit => {
|
||||||
|
let name = if let Ok(Some(attr)) = entry.attr(gimli::DW_AT_name) {
|
||||||
|
if let Ok(DebugValue::Str(name)) = get_attr_value(&attr, &unit, &dwarf) {
|
||||||
|
name
|
||||||
|
} else {
|
||||||
|
"<unknown>".to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"<unknown>".to_string()
|
||||||
|
};
|
||||||
|
compilation_units.push(File {
|
||||||
|
name,
|
||||||
|
global_variables: Vec::new(),
|
||||||
|
functions: Vec::new(),
|
||||||
|
lines: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
gimli::DW_TAG_base_type => {
|
||||||
|
let name = if let Ok(Some(attr)) = entry.attr(gimli::DW_AT_name) {
|
||||||
|
if let Ok(DebugValue::Str(name)) = get_attr_value(&attr, &unit, &dwarf) {
|
||||||
|
name
|
||||||
|
} else {
|
||||||
|
"<unknown>".to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"<unknown>".to_string()
|
||||||
|
};
|
||||||
|
let byte_size = if let Ok(Some(attr)) = entry.attr(gimli::DW_AT_byte_size) {
|
||||||
|
if let Ok(DebugValue::Uint(byte_size)) =
|
||||||
|
get_attr_value(&attr, &unit, &dwarf)
|
||||||
|
{
|
||||||
|
byte_size
|
||||||
|
} else {
|
||||||
|
// TODO: report error?
|
||||||
|
0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: report error?
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let type_offset = entry.offset().0;
|
||||||
|
offset_to_type
|
||||||
|
.insert(type_offset, Type::new(name, byte_size.try_into().unwrap()));
|
||||||
|
}
|
||||||
|
gimli::DW_TAG_subprogram => {
|
||||||
|
let mut func: Function = Default::default();
|
||||||
|
let mut attrs = entry.attrs();
|
||||||
|
while let Some(attr) = attrs.next()? {
|
||||||
|
let val = get_attr_value(&attr, &unit, &dwarf);
|
||||||
|
//println!(" {}: {:?}", attr.name(), val);
|
||||||
|
match attr.name() {
|
||||||
|
gimli::DW_AT_name => {
|
||||||
|
if let Ok(DebugValue::Str(name)) = val {
|
||||||
|
func.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gimli::DW_AT_high_pc => {
|
||||||
|
if let Ok(DebugValue::Uint(high_pc)) = val {
|
||||||
|
func.text_length = high_pc.try_into().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gimli::DW_AT_low_pc => {
|
||||||
|
//println!("low pc {:?}", attr.value());
|
||||||
|
if let Ok(DebugValue::Uint(low_pc)) = val {
|
||||||
|
func.address = low_pc.try_into().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gimli::DW_AT_decl_line => {
|
||||||
|
if let Ok(DebugValue::Uint(line_number)) = val {
|
||||||
|
func.line_number = line_number.try_into().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compilation_units.last_mut().unwrap().functions.push(func);
|
||||||
|
}
|
||||||
|
gimli::DW_TAG_formal_parameter | gimli::DW_TAG_variable => {
|
||||||
|
let mut name = String::new();
|
||||||
|
let mut entity_type: Option<Type> = None;
|
||||||
|
let mut location: Option<Location> = None;
|
||||||
|
let mut line_number = 0;
|
||||||
|
let mut attrs = entry.attrs();
|
||||||
|
while let Some(attr) = attrs.next()? {
|
||||||
|
let val = get_attr_value(&attr, &unit, &dwarf);
|
||||||
|
//println!(" {}: {:?}", attr.name(), val);
|
||||||
|
match attr.name() {
|
||||||
|
gimli::DW_AT_name => {
|
||||||
|
if let Ok(DebugValue::Str(attr_name)) = val {
|
||||||
|
name = attr_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gimli::DW_AT_type => {
|
||||||
|
if let Ok(DebugValue::Size(offset)) = val {
|
||||||
|
if let Some(dtype) = offset_to_type.get(&offset).clone() {
|
||||||
|
entity_type = Some(dtype.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gimli::DW_AT_location => {
|
||||||
|
if let Some(loc) = get_location(&attr, &unit) {
|
||||||
|
location = Some(loc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gimli::DW_AT_decl_line => {
|
||||||
|
if let Ok(DebugValue::Uint(num)) = val {
|
||||||
|
line_number = num;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if entity_type.is_some() && location.is_some() {
|
||||||
|
let var = Variable {
|
||||||
|
name,
|
||||||
|
entity_type: entity_type.unwrap(),
|
||||||
|
location: location.unwrap(),
|
||||||
|
line_number: line_number.try_into().unwrap(),
|
||||||
|
};
|
||||||
|
if depth == 1 {
|
||||||
|
compilation_units
|
||||||
|
.last_mut()
|
||||||
|
.unwrap()
|
||||||
|
.global_variables
|
||||||
|
.push(var);
|
||||||
|
} else if depth > 1 {
|
||||||
|
compilation_units
|
||||||
|
.last_mut()
|
||||||
|
.unwrap()
|
||||||
|
.functions
|
||||||
|
.last_mut()
|
||||||
|
.unwrap()
|
||||||
|
.variables
|
||||||
|
.push(var);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// NOTE: :You may consider supporting other types by extending this
|
||||||
|
// match statement
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get line numbers
|
||||||
|
if let Some(program) = unit.line_program.clone() {
|
||||||
|
// Iterate over the line program rows.
|
||||||
|
let mut rows = program.rows();
|
||||||
|
while let Some((header, row)) = rows.next_row()? {
|
||||||
|
if !row.end_sequence() {
|
||||||
|
// Determine the path. Real applications should cache this for performance.
|
||||||
|
let mut path = path::PathBuf::new();
|
||||||
|
if let Some(file) = row.file(header) {
|
||||||
|
if let Some(dir) = file.directory(header) {
|
||||||
|
path.push(dwarf.attr_string(&unit, dir)?.to_string_lossy().as_ref());
|
||||||
|
}
|
||||||
|
path.push(
|
||||||
|
dwarf
|
||||||
|
.attr_string(&unit, file.path_name())?
|
||||||
|
.to_string_lossy()
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the File
|
||||||
|
let file = compilation_units
|
||||||
|
.iter_mut()
|
||||||
|
.find(|f| f.name == path.as_os_str().to_str().unwrap());
|
||||||
|
|
||||||
|
// Determine line/column. DWARF line/column is never 0, so we use that
|
||||||
|
// but other applications may want to display this differently.
|
||||||
|
let line = row.line().unwrap_or(0);
|
||||||
|
|
||||||
|
if let Some(file) = file {
|
||||||
|
file.lines.push(Line {
|
||||||
|
file: file.name.clone(),
|
||||||
|
number: line.try_into().unwrap(),
|
||||||
|
address: row.address().try_into().unwrap(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(compilation_units)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum DebugValue {
|
||||||
|
Str(String),
|
||||||
|
Uint(u64),
|
||||||
|
Int(i64),
|
||||||
|
Size(usize),
|
||||||
|
NoVal,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Error {
|
||||||
|
GimliError(gimli::Error),
|
||||||
|
Addr2lineError(addr2line::gimli::Error),
|
||||||
|
ObjectError(String),
|
||||||
|
IoError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<gimli::Error> for Error {
|
||||||
|
fn from(err: gimli::Error) -> Self {
|
||||||
|
Error::GimliError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<addr2line::gimli::Error> for Error {
|
||||||
|
fn from(err: addr2line::gimli::Error) -> Self {
|
||||||
|
Error::Addr2lineError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for Error {
|
||||||
|
fn from(_: io::Error) -> Self {
|
||||||
|
Error::IoError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::fmt::Error> for Error {
|
||||||
|
fn from(_: std::fmt::Error) -> Self {
|
||||||
|
Error::IoError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'input, Endian> Reader for gimli::EndianSlice<'input, Endian> where
|
||||||
|
Endian: gimli::Endianity + Send + Sync
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
trait Reader: gimli::Reader<Offset = usize> + Send + Sync {}
|
||||||
|
|
||||||
|
fn get_location<R: Reader>(attr: &gimli::Attribute<R>, unit: &gimli::Unit<R>) -> Option<Location> {
|
||||||
|
if let gimli::AttributeValue::Exprloc(ref data) = attr.value() {
|
||||||
|
let encoding = unit.encoding();
|
||||||
|
let mut pc = data.0.clone();
|
||||||
|
if pc.len() > 0 {
|
||||||
|
if let Ok(op) = gimli::Operation::parse(&mut pc, encoding) {
|
||||||
|
match op {
|
||||||
|
gimli::Operation::FrameOffset { offset } => {
|
||||||
|
return Some(Location::FramePointerOffset(offset.try_into().unwrap()));
|
||||||
|
}
|
||||||
|
gimli::Operation::Address { address } => {
|
||||||
|
return Some(Location::Address(address.try_into().unwrap()));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// based on dwarf_dump.rs
|
||||||
|
fn get_attr_value<R: Reader>(
|
||||||
|
attr: &gimli::Attribute<R>,
|
||||||
|
unit: &gimli::Unit<R>,
|
||||||
|
dwarf: &gimli::Dwarf<R>,
|
||||||
|
) -> Result<DebugValue, Error> {
|
||||||
|
let value = attr.value();
|
||||||
|
// TODO: get rid of w eventually
|
||||||
|
let mut buf = String::new();
|
||||||
|
let w = &mut buf;
|
||||||
|
match value {
|
||||||
|
gimli::AttributeValue::Exprloc(ref data) => {
|
||||||
|
dump_exprloc(w, unit.encoding(), data)?;
|
||||||
|
Ok(DebugValue::Str(w.to_string()))
|
||||||
|
}
|
||||||
|
gimli::AttributeValue::UnitRef(offset) => {
|
||||||
|
match offset.to_unit_section_offset(unit) {
|
||||||
|
UnitSectionOffset::DebugInfoOffset(goff) => {
|
||||||
|
Ok(DebugValue::Size(goff.0))
|
||||||
|
}
|
||||||
|
UnitSectionOffset::DebugTypesOffset(goff) => {
|
||||||
|
Ok(DebugValue::Size(goff.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gimli::AttributeValue::DebugStrRef(offset) => {
|
||||||
|
if let Ok(s) = dwarf.debug_str.get_str(offset) {
|
||||||
|
Ok(DebugValue::Str(format!("{}", s.to_string_lossy()?)))
|
||||||
|
} else {
|
||||||
|
Ok(DebugValue::Str(format!("<.debug_str+0x{:08x}>", offset.0)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gimli::AttributeValue::Sdata(data) => Ok(DebugValue::Int(data)),
|
||||||
|
gimli::AttributeValue::Addr(data) => Ok(DebugValue::Uint(data)),
|
||||||
|
gimli::AttributeValue::Udata(data) => Ok(DebugValue::Uint(data)),
|
||||||
|
|
||||||
|
gimli::AttributeValue::String(s) => {
|
||||||
|
Ok(DebugValue::Str(format!("{}", s.to_string_lossy()?)))
|
||||||
|
}
|
||||||
|
gimli::AttributeValue::FileIndex(value) => {
|
||||||
|
write!(w, "0x{:08x}", value)?;
|
||||||
|
dump_file_index(w, value, unit, dwarf)?;
|
||||||
|
Ok(DebugValue::Str(w.to_string()))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
Ok(DebugValue::NoVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dump_file_index<R: Reader, W: Write>(
|
||||||
|
w: &mut W,
|
||||||
|
file: u64,
|
||||||
|
unit: &gimli::Unit<R>,
|
||||||
|
dwarf: &gimli::Dwarf<R>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if file == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let header = match unit.line_program {
|
||||||
|
Some(ref program) => program.header(),
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
let file = match header.file(file) {
|
||||||
|
Some(header) => header,
|
||||||
|
None => {
|
||||||
|
writeln!(w, "Unable to get header for file {}", file)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
write!(w, " ")?;
|
||||||
|
if let Some(directory) = file.directory(header) {
|
||||||
|
let directory = dwarf.attr_string(unit, directory)?;
|
||||||
|
let directory = directory.to_string_lossy()?;
|
||||||
|
if !directory.starts_with('/') {
|
||||||
|
if let Some(ref comp_dir) = unit.comp_dir {
|
||||||
|
write!(w, "{}/", comp_dir.to_string_lossy()?,)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write!(w, "{}/", directory)?;
|
||||||
|
}
|
||||||
|
write!(
|
||||||
|
w,
|
||||||
|
"{}",
|
||||||
|
dwarf
|
||||||
|
.attr_string(unit, file.path_name())?
|
||||||
|
.to_string_lossy()?
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dump_exprloc<R: Reader, W: Write>(
|
||||||
|
w: &mut W,
|
||||||
|
encoding: gimli::Encoding,
|
||||||
|
data: &gimli::Expression<R>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut pc = data.0.clone();
|
||||||
|
let mut space = false;
|
||||||
|
while pc.len() != 0 {
|
||||||
|
let mut op_pc = pc.clone();
|
||||||
|
let dwop = gimli::DwOp(op_pc.read_u8()?);
|
||||||
|
match gimli::Operation::parse(&mut pc, encoding) {
|
||||||
|
Ok(op) => {
|
||||||
|
if space {
|
||||||
|
write!(w, " ")?;
|
||||||
|
} else {
|
||||||
|
space = true;
|
||||||
|
}
|
||||||
|
dump_op(w, encoding, dwop, op)?;
|
||||||
|
}
|
||||||
|
Err(gimli::Error::InvalidExpression(op)) => {
|
||||||
|
writeln!(w, "WARNING: unsupported operation 0x{:02x}", op.0)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(gimli::Error::UnsupportedRegister(register)) => {
|
||||||
|
writeln!(w, "WARNING: unsupported register {}", register)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(gimli::Error::UnexpectedEof(_)) => {
|
||||||
|
writeln!(w, "WARNING: truncated or malformed expression")?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
writeln!(w, "WARNING: unexpected operation parse error: {}", e)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dump_op<R: Reader, W: Write>(
|
||||||
|
w: &mut W,
|
||||||
|
encoding: gimli::Encoding,
|
||||||
|
dwop: gimli::DwOp,
|
||||||
|
op: gimli::Operation<R>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
write!(w, "{}", dwop)?;
|
||||||
|
match op {
|
||||||
|
gimli::Operation::Deref {
|
||||||
|
base_type, size, ..
|
||||||
|
} => {
|
||||||
|
if dwop == gimli::DW_OP_deref_size || dwop == gimli::DW_OP_xderef_size {
|
||||||
|
write!(w, " {}", size)?;
|
||||||
|
}
|
||||||
|
if base_type != UnitOffset(0) {
|
||||||
|
write!(w, " type 0x{:08x}", base_type.0)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gimli::Operation::Pick { index } => {
|
||||||
|
if dwop == gimli::DW_OP_pick {
|
||||||
|
write!(w, " {}", index)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gimli::Operation::PlusConstant { value } => {
|
||||||
|
write!(w, " {}", value as i64)?;
|
||||||
|
}
|
||||||
|
gimli::Operation::Bra { target } => {
|
||||||
|
write!(w, " {}", target)?;
|
||||||
|
}
|
||||||
|
gimli::Operation::Skip { target } => {
|
||||||
|
write!(w, " {}", target)?;
|
||||||
|
}
|
||||||
|
gimli::Operation::SignedConstant { value } => match dwop {
|
||||||
|
gimli::DW_OP_const1s
|
||||||
|
| gimli::DW_OP_const2s
|
||||||
|
| gimli::DW_OP_const4s
|
||||||
|
| gimli::DW_OP_const8s
|
||||||
|
| gimli::DW_OP_consts => {
|
||||||
|
write!(w, " {}", value)?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
gimli::Operation::UnsignedConstant { value } => match dwop {
|
||||||
|
gimli::DW_OP_const1u
|
||||||
|
| gimli::DW_OP_const2u
|
||||||
|
| gimli::DW_OP_const4u
|
||||||
|
| gimli::DW_OP_const8u
|
||||||
|
| gimli::DW_OP_constu => {
|
||||||
|
write!(w, " {}", value)?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// These have the value encoded in the operation, eg DW_OP_lit0.
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gimli::Operation::Register { register } => {
|
||||||
|
if dwop == gimli::DW_OP_regx {
|
||||||
|
write!(w, " {}", register.0)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gimli::Operation::RegisterOffset {
|
||||||
|
register,
|
||||||
|
offset,
|
||||||
|
base_type,
|
||||||
|
} => {
|
||||||
|
if dwop >= gimli::DW_OP_breg0 && dwop <= gimli::DW_OP_breg31 {
|
||||||
|
write!(w, "{:+}", offset)?;
|
||||||
|
} else {
|
||||||
|
write!(w, " {}", register.0)?;
|
||||||
|
if offset != 0 {
|
||||||
|
write!(w, "{:+}", offset)?;
|
||||||
|
}
|
||||||
|
if base_type != UnitOffset(0) {
|
||||||
|
write!(w, " type 0x{:08x}", base_type.0)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gimli::Operation::FrameOffset { offset } => {
|
||||||
|
write!(w, " {}", offset)?;
|
||||||
|
}
|
||||||
|
gimli::Operation::Call { offset } => match offset {
|
||||||
|
gimli::DieReference::UnitRef(gimli::UnitOffset(offset)) => {
|
||||||
|
write!(w, " 0x{:08x}", offset)?;
|
||||||
|
}
|
||||||
|
gimli::DieReference::DebugInfoRef(gimli::DebugInfoOffset(offset)) => {
|
||||||
|
write!(w, " 0x{:08x}", offset)?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gimli::Operation::Piece {
|
||||||
|
size_in_bits,
|
||||||
|
bit_offset: None,
|
||||||
|
} => {
|
||||||
|
write!(w, " {}", size_in_bits / 8)?;
|
||||||
|
}
|
||||||
|
gimli::Operation::Piece {
|
||||||
|
size_in_bits,
|
||||||
|
bit_offset: Some(bit_offset),
|
||||||
|
} => {
|
||||||
|
write!(w, " 0x{:08x} offset 0x{:08x}", size_in_bits, bit_offset)?;
|
||||||
|
}
|
||||||
|
gimli::Operation::ImplicitValue { data } => {
|
||||||
|
let data = data.to_slice()?;
|
||||||
|
write!(w, " 0x{:08x} contents 0x", data.len())?;
|
||||||
|
for byte in data.iter() {
|
||||||
|
write!(w, "{:02x}", byte)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gimli::Operation::ImplicitPointer { value, byte_offset } => {
|
||||||
|
write!(w, " 0x{:08x} {}", value.0, byte_offset)?;
|
||||||
|
}
|
||||||
|
gimli::Operation::EntryValue { expression } => {
|
||||||
|
write!(w, "(")?;
|
||||||
|
dump_exprloc(w, encoding, &gimli::Expression(expression))?;
|
||||||
|
write!(w, ")")?;
|
||||||
|
}
|
||||||
|
gimli::Operation::ParameterRef { offset } => {
|
||||||
|
write!(w, " 0x{:08x}", offset.0)?;
|
||||||
|
}
|
||||||
|
gimli::Operation::Address { address } => {
|
||||||
|
write!(w, " 0x{:08x}", address)?;
|
||||||
|
}
|
||||||
|
gimli::Operation::AddressIndex { index } => {
|
||||||
|
write!(w, " 0x{:08x}", index.0)?;
|
||||||
|
}
|
||||||
|
gimli::Operation::ConstantIndex { index } => {
|
||||||
|
write!(w, " 0x{:08x}", index.0)?;
|
||||||
|
}
|
||||||
|
gimli::Operation::TypedLiteral { base_type, value } => {
|
||||||
|
write!(w, " type 0x{:08x} contents 0x", base_type.0)?;
|
||||||
|
for byte in value.to_slice()?.iter() {
|
||||||
|
write!(w, "{:02x}", byte)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gimli::Operation::Convert { base_type } => {
|
||||||
|
write!(w, " type 0x{:08x}", base_type.0)?;
|
||||||
|
}
|
||||||
|
gimli::Operation::Reinterpret { base_type } => {
|
||||||
|
write!(w, " type 0x{:08x}", base_type.0)?;
|
||||||
|
}
|
||||||
|
gimli::Operation::Drop
|
||||||
|
| gimli::Operation::Swap
|
||||||
|
| gimli::Operation::Rot
|
||||||
|
| gimli::Operation::Abs
|
||||||
|
| gimli::Operation::And
|
||||||
|
| gimli::Operation::Div
|
||||||
|
| gimli::Operation::Minus
|
||||||
|
| gimli::Operation::Mod
|
||||||
|
| gimli::Operation::Mul
|
||||||
|
| gimli::Operation::Neg
|
||||||
|
| gimli::Operation::Not
|
||||||
|
| gimli::Operation::Or
|
||||||
|
| gimli::Operation::Plus
|
||||||
|
| gimli::Operation::Shl
|
||||||
|
| gimli::Operation::Shr
|
||||||
|
| gimli::Operation::Shra
|
||||||
|
| gimli::Operation::Xor
|
||||||
|
| gimli::Operation::Eq
|
||||||
|
| gimli::Operation::Ge
|
||||||
|
| gimli::Operation::Gt
|
||||||
|
| gimli::Operation::Le
|
||||||
|
| gimli::Operation::Lt
|
||||||
|
| gimli::Operation::Ne
|
||||||
|
| gimli::Operation::Nop
|
||||||
|
| gimli::Operation::PushObjectAddress
|
||||||
|
| gimli::Operation::TLS
|
||||||
|
| gimli::Operation::CallFrameCFA
|
||||||
|
| gimli::Operation::StackValue => {}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
63
proj-1/deet/src/inferior.rs
Normal file
63
proj-1/deet/src/inferior.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
use nix::sys::ptrace;
|
||||||
|
use nix::sys::signal;
|
||||||
|
use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
|
||||||
|
use nix::unistd::Pid;
|
||||||
|
use std::process::Child;
|
||||||
|
|
||||||
|
pub enum Status {
|
||||||
|
/// Indicates inferior stopped. Contains the signal that stopped the process, as well as the
|
||||||
|
/// current instruction pointer that it is stopped at.
|
||||||
|
Stopped(signal::Signal, usize),
|
||||||
|
|
||||||
|
/// Indicates inferior exited normally. Contains the exit status code.
|
||||||
|
Exited(i32),
|
||||||
|
|
||||||
|
/// Indicates the inferior exited due to a signal. Contains the signal that killed the
|
||||||
|
/// process.
|
||||||
|
Signaled(signal::Signal),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function calls ptrace with PTRACE_TRACEME to enable debugging on a process. You should use
|
||||||
|
/// pre_exec with Command to call this in the child process.
|
||||||
|
fn child_traceme() -> Result<(), std::io::Error> {
|
||||||
|
ptrace::traceme().or(Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::Other,
|
||||||
|
"ptrace TRACEME failed",
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Inferior {
|
||||||
|
child: Child,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Inferior {
|
||||||
|
/// Attempts to start a new inferior process. Returns Some(Inferior) if successful, or None if
|
||||||
|
/// an error is encountered.
|
||||||
|
pub fn new(target: &str, args: &Vec<String>) -> Option<Inferior> {
|
||||||
|
// TODO: implement me!
|
||||||
|
println!(
|
||||||
|
"Inferior::new not implemented! target={}, args={:?}",
|
||||||
|
target, args
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the pid of this inferior.
|
||||||
|
pub fn pid(&self) -> Pid {
|
||||||
|
nix::unistd::Pid::from_raw(self.child.id() as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calls waitpid on this inferior and returns a Status to indicate the state of the process
|
||||||
|
/// after the waitpid call.
|
||||||
|
pub fn wait(&self, options: Option<WaitPidFlag>) -> Result<Status, nix::Error> {
|
||||||
|
Ok(match waitpid(self.pid(), options)? {
|
||||||
|
WaitStatus::Exited(_pid, exit_code) => Status::Exited(exit_code),
|
||||||
|
WaitStatus::Signaled(_pid, signal, _core_dumped) => Status::Signaled(signal),
|
||||||
|
WaitStatus::Stopped(_pid, signal) => {
|
||||||
|
let regs = ptrace::getregs(self.pid())?;
|
||||||
|
Status::Stopped(signal, regs.rip as usize)
|
||||||
|
}
|
||||||
|
other => panic!("waitpid returned unexpected status: {:?}", other),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
22
proj-1/deet/src/main.rs
Normal file
22
proj-1/deet/src/main.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
mod debugger;
|
||||||
|
mod debugger_command;
|
||||||
|
mod inferior;
|
||||||
|
|
||||||
|
use crate::debugger::Debugger;
|
||||||
|
use nix::sys::signal::{signal, SigHandler, Signal};
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
if args.len() != 2 {
|
||||||
|
println!("Usage: {} <target program>", args[0]);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
let target = &args[1];
|
||||||
|
|
||||||
|
// Disable handling of ctrl+c in this process (so that ctrl+c only gets delivered to child
|
||||||
|
// processes)
|
||||||
|
unsafe { signal(Signal::SIGINT, SigHandler::SigIgn) }.expect("Error disabling SIGINT handling");
|
||||||
|
|
||||||
|
Debugger::new(target).run();
|
||||||
|
}
|
||||||
5
proj-2/.gitignore
vendored
Normal file
5
proj-2/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/balancebeam/.cargo/
|
||||||
|
/balancebeam/target/
|
||||||
|
/balancebeam/Cargo.lock
|
||||||
|
.idea
|
||||||
|
.*.swp
|
||||||
18
proj-2/Dockerfile
Normal file
18
proj-2/Dockerfile
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
FROM ubuntu:18.04
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y build-essential python3 curl
|
||||||
|
|
||||||
|
RUN useradd -ms /bin/bash balancebeam
|
||||||
|
USER balancebeam
|
||||||
|
WORKDIR /home/balancebeam
|
||||||
|
|
||||||
|
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||||
|
|
||||||
|
COPY balancebeam/Cargo.toml .
|
||||||
|
RUN mkdir src && touch src/main.rs && ./.cargo/bin/cargo build --release || true
|
||||||
|
|
||||||
|
COPY balancebeam/ ./
|
||||||
|
RUN ./.cargo/bin/cargo build --release
|
||||||
|
|
||||||
|
ENTRYPOINT ["./.cargo/bin/cargo", "run", "--release", "--"]
|
||||||
25
proj-2/balancebeam/Cargo.toml
Normal file
25
proj-2/balancebeam/Cargo.toml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "balancebeam"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Ryan Eberhardt <reberhardt7@gmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = "3.0.0-beta.1"
|
||||||
|
httparse = "1.3"
|
||||||
|
http = "0.2"
|
||||||
|
log = "0.4"
|
||||||
|
env_logger = "0.7"
|
||||||
|
pretty_env_logger = "0.4"
|
||||||
|
threadpool = "1.8"
|
||||||
|
tokio = { version = "0.2", features = ["full"] }
|
||||||
|
rand = "0.7"
|
||||||
|
parking_lot = "0.10"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
nix = "0.17"
|
||||||
|
hyper = "0.13"
|
||||||
|
reqwest = "0.10"
|
||||||
|
async-trait = "0.1"
|
||||||
201
proj-2/balancebeam/src/main.rs
Normal file
201
proj-2/balancebeam/src/main.rs
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
mod request;
|
||||||
|
mod response;
|
||||||
|
|
||||||
|
use clap::Clap;
|
||||||
|
use rand::{Rng, SeedableRng};
|
||||||
|
use std::net::{TcpListener, TcpStream};
|
||||||
|
|
||||||
|
/// Contains information parsed from the command-line invocation of balancebeam. The Clap macros
|
||||||
|
/// provide a fancy way to automatically construct a command-line argument parser.
|
||||||
|
#[derive(Clap, Debug)]
|
||||||
|
#[clap(about = "Fun with load balancing")]
|
||||||
|
struct CmdOptions {
|
||||||
|
#[clap(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
about = "IP/port to bind to",
|
||||||
|
default_value = "0.0.0.0:1100"
|
||||||
|
)]
|
||||||
|
bind: String,
|
||||||
|
#[clap(short, long, about = "Upstream host to forward requests to")]
|
||||||
|
upstream: Vec<String>,
|
||||||
|
#[clap(
|
||||||
|
long,
|
||||||
|
about = "Perform active health checks on this interval (in seconds)",
|
||||||
|
default_value = "10"
|
||||||
|
)]
|
||||||
|
active_health_check_interval: usize,
|
||||||
|
#[clap(
|
||||||
|
long,
|
||||||
|
about = "Path to send request to for active health checks",
|
||||||
|
default_value = "/"
|
||||||
|
)]
|
||||||
|
active_health_check_path: String,
|
||||||
|
#[clap(
|
||||||
|
long,
|
||||||
|
about = "Maximum number of requests to accept per IP per minute (0 = unlimited)",
|
||||||
|
default_value = "0"
|
||||||
|
)]
|
||||||
|
max_requests_per_minute: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains information about the state of balancebeam (e.g. what servers we are currently proxying
|
||||||
|
/// to, what servers have failed, rate limiting counts, etc.)
|
||||||
|
///
|
||||||
|
/// You should add fields to this struct in later milestones.
|
||||||
|
struct ProxyState {
|
||||||
|
/// How frequently we check whether upstream servers are alive (Milestone 4)
|
||||||
|
#[allow(dead_code)]
|
||||||
|
active_health_check_interval: usize,
|
||||||
|
/// Where we should send requests when doing active health checks (Milestone 4)
|
||||||
|
#[allow(dead_code)]
|
||||||
|
active_health_check_path: String,
|
||||||
|
/// Maximum number of requests an individual IP can make in a minute (Milestone 5)
|
||||||
|
#[allow(dead_code)]
|
||||||
|
max_requests_per_minute: usize,
|
||||||
|
/// Addresses of servers that we are proxying to
|
||||||
|
upstream_addresses: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Initialize the logging library. You can print log messages using the `log` macros:
|
||||||
|
// https://docs.rs/log/0.4.8/log/ You are welcome to continue using print! statements; this
|
||||||
|
// just looks a little prettier.
|
||||||
|
if let Err(_) = std::env::var("RUST_LOG") {
|
||||||
|
std::env::set_var("RUST_LOG", "debug");
|
||||||
|
}
|
||||||
|
pretty_env_logger::init();
|
||||||
|
|
||||||
|
// Parse the command line arguments passed to this program
|
||||||
|
let options = CmdOptions::parse();
|
||||||
|
if options.upstream.len() < 1 {
|
||||||
|
log::error!("At least one upstream server must be specified using the --upstream option.");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start listening for connections
|
||||||
|
let listener = match TcpListener::bind(&options.bind) {
|
||||||
|
Ok(listener) => listener,
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Could not bind to {}: {}", options.bind, err);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
log::info!("Listening for requests on {}", options.bind);
|
||||||
|
|
||||||
|
// Handle incoming connections
|
||||||
|
let state = ProxyState {
|
||||||
|
upstream_addresses: options.upstream,
|
||||||
|
active_health_check_interval: options.active_health_check_interval,
|
||||||
|
active_health_check_path: options.active_health_check_path,
|
||||||
|
max_requests_per_minute: options.max_requests_per_minute,
|
||||||
|
};
|
||||||
|
for stream in listener.incoming() {
|
||||||
|
if let Ok(stream) = stream {
|
||||||
|
// Handle the connection!
|
||||||
|
handle_connection(stream, &state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connect_to_upstream(state: &ProxyState) -> Result<TcpStream, std::io::Error> {
|
||||||
|
let mut rng = rand::rngs::StdRng::from_entropy();
|
||||||
|
let upstream_idx = rng.gen_range(0, state.upstream_addresses.len());
|
||||||
|
let upstream_ip = &state.upstream_addresses[upstream_idx];
|
||||||
|
TcpStream::connect(upstream_ip).or_else(|err| {
|
||||||
|
log::error!("Failed to connect to upstream {}: {}", upstream_ip, err);
|
||||||
|
Err(err)
|
||||||
|
})
|
||||||
|
// TODO: implement failover (milestone 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_response(client_conn: &mut TcpStream, response: &http::Response<Vec<u8>>) {
|
||||||
|
let client_ip = client_conn.peer_addr().unwrap().ip().to_string();
|
||||||
|
log::info!("{} <- {}", client_ip, response::format_response_line(&response));
|
||||||
|
if let Err(error) = response::write_to_stream(&response, client_conn) {
|
||||||
|
log::warn!("Failed to send response to client: {}", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_connection(mut client_conn: TcpStream, state: &ProxyState) {
|
||||||
|
let client_ip = client_conn.peer_addr().unwrap().ip().to_string();
|
||||||
|
log::info!("Connection received from {}", client_ip);
|
||||||
|
|
||||||
|
// Open a connection to a random destination server
|
||||||
|
let mut upstream_conn = match connect_to_upstream(state) {
|
||||||
|
Ok(stream) => stream,
|
||||||
|
Err(_error) => {
|
||||||
|
let response = response::make_http_error(http::StatusCode::BAD_GATEWAY);
|
||||||
|
send_response(&mut client_conn, &response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let upstream_ip = client_conn.peer_addr().unwrap().ip().to_string();
|
||||||
|
|
||||||
|
// The client may now send us one or more requests. Keep trying to read requests until the
|
||||||
|
// client hangs up or we get an error.
|
||||||
|
loop {
|
||||||
|
// Read a request from the client
|
||||||
|
let mut request = match request::read_from_stream(&mut client_conn) {
|
||||||
|
Ok(request) => request,
|
||||||
|
// Handle case where client closed connection and is no longer sending requests
|
||||||
|
Err(request::Error::IncompleteRequest(0)) => {
|
||||||
|
log::debug!("Client finished sending requests. Shutting down connection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Handle I/O error in reading from the client
|
||||||
|
Err(request::Error::ConnectionError(io_err)) => {
|
||||||
|
log::info!("Error reading request from client stream: {}", io_err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::debug!("Error parsing request: {:?}", error);
|
||||||
|
let response = response::make_http_error(match error {
|
||||||
|
request::Error::IncompleteRequest(_)
|
||||||
|
| request::Error::MalformedRequest(_)
|
||||||
|
| request::Error::InvalidContentLength
|
||||||
|
| request::Error::ContentLengthMismatch => http::StatusCode::BAD_REQUEST,
|
||||||
|
request::Error::RequestBodyTooLarge => http::StatusCode::PAYLOAD_TOO_LARGE,
|
||||||
|
request::Error::ConnectionError(_) => http::StatusCode::SERVICE_UNAVAILABLE,
|
||||||
|
});
|
||||||
|
send_response(&mut client_conn, &response);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
log::info!(
|
||||||
|
"{} -> {}: {}",
|
||||||
|
client_ip,
|
||||||
|
upstream_ip,
|
||||||
|
request::format_request_line(&request)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add X-Forwarded-For header so that the upstream server knows the client's IP address.
|
||||||
|
// (We're the ones connecting directly to the upstream server, so without this header, the
|
||||||
|
// upstream server will only know our IP, not the client's.)
|
||||||
|
request::extend_header_value(&mut request, "x-forwarded-for", &client_ip);
|
||||||
|
|
||||||
|
// Forward the request to the server
|
||||||
|
if let Err(error) = request::write_to_stream(&request, &mut upstream_conn) {
|
||||||
|
log::error!("Failed to send request to upstream {}: {}", upstream_ip, error);
|
||||||
|
let response = response::make_http_error(http::StatusCode::BAD_GATEWAY);
|
||||||
|
send_response(&mut client_conn, &response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log::debug!("Forwarded request to server");
|
||||||
|
|
||||||
|
// Read the server's response
|
||||||
|
let response = match response::read_from_stream(&mut upstream_conn, request.method()) {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(error) => {
|
||||||
|
log::error!("Error reading response from server: {:?}", error);
|
||||||
|
let response = response::make_http_error(http::StatusCode::BAD_GATEWAY);
|
||||||
|
send_response(&mut client_conn, &response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Forward the response to the client
|
||||||
|
send_response(&mut client_conn, &response);
|
||||||
|
log::debug!("Forwarded response to client");
|
||||||
|
}
|
||||||
|
}
|
||||||
218
proj-2/balancebeam/src/request.rs
Normal file
218
proj-2/balancebeam/src/request.rs
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
use std::cmp::min;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::net::TcpStream;
|
||||||
|
|
||||||
|
const MAX_HEADERS_SIZE: usize = 8000;
|
||||||
|
const MAX_BODY_SIZE: usize = 10000000;
|
||||||
|
const MAX_NUM_HEADERS: usize = 32;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
/// Client hung up before sending a complete request. IncompleteRequest contains the number of
|
||||||
|
/// bytes that were successfully read before the client hung up
|
||||||
|
IncompleteRequest(usize),
|
||||||
|
/// Client sent an invalid HTTP request. httparse::Error contains more details
|
||||||
|
MalformedRequest(httparse::Error),
|
||||||
|
/// The Content-Length header is present, but does not contain a valid numeric value
|
||||||
|
InvalidContentLength,
|
||||||
|
/// The Content-Length header does not match the size of the request body that was sent
|
||||||
|
ContentLengthMismatch,
|
||||||
|
/// The request body is bigger than MAX_BODY_SIZE
|
||||||
|
RequestBodyTooLarge,
|
||||||
|
/// Encountered an I/O error when reading/writing a TcpStream
|
||||||
|
ConnectionError(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts the Content-Length header value from the provided request. Returns Ok(Some(usize)) if
|
||||||
|
/// the Content-Length is present and valid, Ok(None) if Content-Length is not present, or
|
||||||
|
/// Err(Error) if Content-Length is present but invalid.
|
||||||
|
///
|
||||||
|
/// You won't need to touch this function.
|
||||||
|
fn get_content_length(request: &http::Request<Vec<u8>>) -> Result<Option<usize>, Error> {
|
||||||
|
// Look for content-length header
|
||||||
|
if let Some(header_value) = request.headers().get("content-length") {
|
||||||
|
// If it exists, parse it as a usize (or return InvalidContentLength if it can't be parsed as such)
|
||||||
|
Ok(Some(
|
||||||
|
header_value
|
||||||
|
.to_str()
|
||||||
|
.or(Err(Error::InvalidContentLength))?
|
||||||
|
.parse::<usize>()
|
||||||
|
.or(Err(Error::InvalidContentLength))?,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
// If it doesn't exist, return None
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function appends to a header value (adding a new header if the header is not already
|
||||||
|
/// present). This is used to add the client's IP address to the end of the X-Forwarded-For list,
|
||||||
|
/// or to add a new X-Forwarded-For header if one is not already present.
|
||||||
|
///
|
||||||
|
/// You won't need to touch this function.
|
||||||
|
pub fn extend_header_value(
|
||||||
|
request: &mut http::Request<Vec<u8>>,
|
||||||
|
name: &'static str,
|
||||||
|
extend_value: &str,
|
||||||
|
) {
|
||||||
|
let new_value = match request.headers().get(name) {
|
||||||
|
Some(existing_value) => {
|
||||||
|
[existing_value.as_bytes(), b", ", extend_value.as_bytes()].concat()
|
||||||
|
}
|
||||||
|
None => extend_value.as_bytes().to_owned(),
|
||||||
|
};
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.insert(name, http::HeaderValue::from_bytes(&new_value).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to parse the data in the supplied buffer as an HTTP request. Returns one of the
|
||||||
|
/// following:
|
||||||
|
///
|
||||||
|
/// * If there is a complete and valid request in the buffer, returns Ok(Some(http::Request))
|
||||||
|
/// * If there is an incomplete but valid-so-far request in the buffer, returns Ok(None)
|
||||||
|
/// * If there is data in the buffer that is definitely not a valid HTTP request, returns Err(Error)
|
||||||
|
///
|
||||||
|
/// You won't need to touch this function.
|
||||||
|
fn parse_request(buffer: &[u8]) -> Result<Option<(http::Request<Vec<u8>>, usize)>, Error> {
|
||||||
|
let mut headers = [httparse::EMPTY_HEADER; MAX_NUM_HEADERS];
|
||||||
|
let mut req = httparse::Request::new(&mut headers);
|
||||||
|
let res = req.parse(buffer).or_else(|err| Err(Error::MalformedRequest(err)))?;
|
||||||
|
|
||||||
|
if let httparse::Status::Complete(len) = res {
|
||||||
|
let mut request = http::Request::builder()
|
||||||
|
.method(req.method.unwrap())
|
||||||
|
.uri(req.path.unwrap())
|
||||||
|
.version(http::Version::HTTP_11);
|
||||||
|
for header in req.headers {
|
||||||
|
request = request.header(header.name, header.value);
|
||||||
|
}
|
||||||
|
let request = request.body(Vec::new()).unwrap();
|
||||||
|
Ok(Some((request, len)))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads an HTTP request from the provided stream, waiting until a complete set of headers is sent.
|
||||||
|
/// This function only reads the request line and headers; the read_body function can subsequently
|
||||||
|
/// be called in order to read the request body (for a POST request).
|
||||||
|
///
|
||||||
|
/// Returns Ok(http::Request) if a valid request is received, or Error if not.
|
||||||
|
///
|
||||||
|
/// You will need to modify this function in Milestone 2.
|
||||||
|
fn read_headers(stream: &mut TcpStream) -> Result<http::Request<Vec<u8>>, Error> {
|
||||||
|
// Try reading the headers from the request. We may not receive all the headers in one shot
|
||||||
|
// (e.g. we might receive the first few bytes of a request, and then the rest follows later).
|
||||||
|
// Try parsing repeatedly until we read a valid HTTP request
|
||||||
|
let mut request_buffer = [0_u8; MAX_HEADERS_SIZE];
|
||||||
|
let mut bytes_read = 0;
|
||||||
|
loop {
|
||||||
|
// Read bytes from the connection into the buffer, starting at position bytes_read
|
||||||
|
let new_bytes = stream
|
||||||
|
.read(&mut request_buffer[bytes_read..])
|
||||||
|
.or_else(|err| Err(Error::ConnectionError(err)))?;
|
||||||
|
if new_bytes == 0 {
|
||||||
|
// We didn't manage to read a complete request
|
||||||
|
return Err(Error::IncompleteRequest(bytes_read));
|
||||||
|
}
|
||||||
|
bytes_read += new_bytes;
|
||||||
|
|
||||||
|
// See if we've read a valid request so far
|
||||||
|
if let Some((mut request, headers_len)) = parse_request(&request_buffer[..bytes_read])? {
|
||||||
|
// We've read a complete set of headers. However, if this was a POST request, a request
|
||||||
|
// body might have been included as well, and we might have read part of the body out of
|
||||||
|
// the stream into header_buffer. We need to add those bytes to the Request body so that
|
||||||
|
// we don't lose them
|
||||||
|
request
|
||||||
|
.body_mut()
|
||||||
|
.extend_from_slice(&request_buffer[headers_len..bytes_read]);
|
||||||
|
return Ok(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function reads the body for a request from the stream. The client only sends a body if the
|
||||||
|
/// Content-Length header is present; this function reads that number of bytes from the stream. It
|
||||||
|
/// returns Ok(()) if successful, or Err(Error) if Content-Length bytes couldn't be read.
|
||||||
|
///
|
||||||
|
/// You will need to modify this function in Milestone 2.
|
||||||
|
fn read_body(
|
||||||
|
stream: &mut TcpStream,
|
||||||
|
request: &mut http::Request<Vec<u8>>,
|
||||||
|
content_length: usize,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
// Keep reading data until we read the full body length, or until we hit an error.
|
||||||
|
while request.body().len() < content_length {
|
||||||
|
// Read up to 512 bytes at a time. (If the client only sent a small body, then only allocate
|
||||||
|
// space to read that body.)
|
||||||
|
let mut buffer = vec![0_u8; min(512, content_length)];
|
||||||
|
let bytes_read = stream.read(&mut buffer).or_else(|err| Err(Error::ConnectionError(err)))?;
|
||||||
|
|
||||||
|
// Make sure the client is still sending us bytes
|
||||||
|
if bytes_read == 0 {
|
||||||
|
log::debug!(
|
||||||
|
"Client hung up after sending a body of length {}, even though it said the content \
|
||||||
|
length is {}",
|
||||||
|
request.body().len(),
|
||||||
|
content_length
|
||||||
|
);
|
||||||
|
return Err(Error::ContentLengthMismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the client didn't send us *too many* bytes
|
||||||
|
if request.body().len() + bytes_read > content_length {
|
||||||
|
log::debug!(
|
||||||
|
"Client sent more bytes than we expected based on the given content length!"
|
||||||
|
);
|
||||||
|
return Err(Error::ContentLengthMismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the received bytes in the request body
|
||||||
|
request.body_mut().extend_from_slice(&buffer[..bytes_read]);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function reads and returns an HTTP request from a stream, returning an Error if the client
|
||||||
|
/// closes the connection prematurely or sends an invalid request.
|
||||||
|
///
|
||||||
|
/// You will need to modify this function in Milestone 2.
|
||||||
|
pub fn read_from_stream(stream: &mut TcpStream) -> Result<http::Request<Vec<u8>>, Error> {
|
||||||
|
// Read headers
|
||||||
|
let mut request = read_headers(stream)?;
|
||||||
|
// Read body if the client supplied the Content-Length header (which it does for POST requests)
|
||||||
|
if let Some(content_length) = get_content_length(&request)? {
|
||||||
|
if content_length > MAX_BODY_SIZE {
|
||||||
|
return Err(Error::RequestBodyTooLarge);
|
||||||
|
} else {
|
||||||
|
read_body(stream, &mut request, content_length)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function serializes a request to bytes and writes those bytes to the provided stream.
|
||||||
|
///
|
||||||
|
/// You will need to modify this function in Milestone 2.
|
||||||
|
pub fn write_to_stream(
|
||||||
|
request: &http::Request<Vec<u8>>,
|
||||||
|
stream: &mut TcpStream,
|
||||||
|
) -> Result<(), std::io::Error> {
|
||||||
|
stream.write(&format_request_line(request).into_bytes())?;
|
||||||
|
stream.write(&['\r' as u8, '\n' as u8])?; // \r\n
|
||||||
|
for (header_name, header_value) in request.headers() {
|
||||||
|
stream.write(&format!("{}: ", header_name).as_bytes())?;
|
||||||
|
stream.write(header_value.as_bytes())?;
|
||||||
|
stream.write(&['\r' as u8, '\n' as u8])?; // \r\n
|
||||||
|
}
|
||||||
|
stream.write(&['\r' as u8, '\n' as u8])?;
|
||||||
|
if request.body().len() > 0 {
|
||||||
|
stream.write(request.body())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_request_line(request: &http::Request<Vec<u8>>) -> String {
|
||||||
|
format!("{} {} {:?}", request.method(), request.uri(), request.version())
|
||||||
|
}
|
||||||
224
proj-2/balancebeam/src/response.rs
Normal file
224
proj-2/balancebeam/src/response.rs
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::net::TcpStream;
|
||||||
|
|
||||||
|
const MAX_HEADERS_SIZE: usize = 8000;
|
||||||
|
const MAX_BODY_SIZE: usize = 10000000;
|
||||||
|
const MAX_NUM_HEADERS: usize = 32;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
/// Client hung up before sending a complete request
|
||||||
|
IncompleteResponse,
|
||||||
|
/// Client sent an invalid HTTP request. httparse::Error contains more details
|
||||||
|
MalformedResponse(httparse::Error),
|
||||||
|
/// The Content-Length header is present, but does not contain a valid numeric value
|
||||||
|
InvalidContentLength,
|
||||||
|
/// The Content-Length header does not match the size of the request body that was sent
|
||||||
|
ContentLengthMismatch,
|
||||||
|
/// The request body is bigger than MAX_BODY_SIZE
|
||||||
|
ResponseBodyTooLarge,
|
||||||
|
/// Encountered an I/O error when reading/writing a TcpStream
|
||||||
|
ConnectionError(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts the Content-Length header value from the provided response. Returns Ok(Some(usize)) if
|
||||||
|
/// the Content-Length is present and valid, Ok(None) if Content-Length is not present, or
|
||||||
|
/// Err(Error) if Content-Length is present but invalid.
|
||||||
|
///
|
||||||
|
/// You won't need to touch this function.
|
||||||
|
fn get_content_length(response: &http::Response<Vec<u8>>) -> Result<Option<usize>, Error> {
|
||||||
|
// Look for content-length header
|
||||||
|
if let Some(header_value) = response.headers().get("content-length") {
|
||||||
|
// If it exists, parse it as a usize (or return InvalidResponseFormat if it can't be parsed as such)
|
||||||
|
Ok(Some(
|
||||||
|
header_value
|
||||||
|
.to_str()
|
||||||
|
.or(Err(Error::InvalidContentLength))?
|
||||||
|
.parse::<usize>()
|
||||||
|
.or(Err(Error::InvalidContentLength))?,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
// If it doesn't exist, return None
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to parse the data in the supplied buffer as an HTTP response. Returns one of the
|
||||||
|
/// following:
|
||||||
|
///
|
||||||
|
/// * If there is a complete and valid response in the buffer, returns Ok(Some(http::Request))
|
||||||
|
/// * If there is an incomplete but valid-so-far response in the buffer, returns Ok(None)
|
||||||
|
/// * If there is data in the buffer that is definitely not a valid HTTP response, returns
|
||||||
|
/// Err(Error)
|
||||||
|
///
|
||||||
|
/// You won't need to touch this function.
|
||||||
|
fn parse_response(buffer: &[u8]) -> Result<Option<(http::Response<Vec<u8>>, usize)>, Error> {
|
||||||
|
let mut headers = [httparse::EMPTY_HEADER; MAX_NUM_HEADERS];
|
||||||
|
let mut resp = httparse::Response::new(&mut headers);
|
||||||
|
let res = resp
|
||||||
|
.parse(buffer)
|
||||||
|
.or_else(|err| Err(Error::MalformedResponse(err)))?;
|
||||||
|
|
||||||
|
if let httparse::Status::Complete(len) = res {
|
||||||
|
let mut response = http::Response::builder()
|
||||||
|
.status(resp.code.unwrap())
|
||||||
|
.version(http::Version::HTTP_11);
|
||||||
|
for header in resp.headers {
|
||||||
|
response = response.header(header.name, header.value);
|
||||||
|
}
|
||||||
|
let response = response.body(Vec::new()).unwrap();
|
||||||
|
Ok(Some((response, len)))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads an HTTP response from the provided stream, waiting until a complete set of headers is
|
||||||
|
/// sent. This function only reads the response line and headers; the read_body function can
|
||||||
|
/// subsequently be called in order to read the response body.
|
||||||
|
///
|
||||||
|
/// Returns Ok(http::Response) if a valid response is received, or Error if not.
|
||||||
|
///
|
||||||
|
/// You will need to modify this function in Milestone 2.
|
||||||
|
fn read_headers(stream: &mut TcpStream) -> Result<http::Response<Vec<u8>>, Error> {
|
||||||
|
// Try reading the headers from the response. We may not receive all the headers in one shot
|
||||||
|
// (e.g. we might receive the first few bytes of a response, and then the rest follows later).
|
||||||
|
// Try parsing repeatedly until we read a valid HTTP response
|
||||||
|
let mut response_buffer = [0_u8; MAX_HEADERS_SIZE];
|
||||||
|
let mut bytes_read = 0;
|
||||||
|
loop {
|
||||||
|
// Read bytes from the connection into the buffer, starting at position bytes_read
|
||||||
|
let new_bytes = stream
|
||||||
|
.read(&mut response_buffer[bytes_read..])
|
||||||
|
.or_else(|err| Err(Error::ConnectionError(err)))?;
|
||||||
|
if new_bytes == 0 {
|
||||||
|
// We didn't manage to read a complete response
|
||||||
|
return Err(Error::IncompleteResponse);
|
||||||
|
}
|
||||||
|
bytes_read += new_bytes;
|
||||||
|
|
||||||
|
// See if we've read a valid response so far
|
||||||
|
if let Some((mut response, headers_len)) = parse_response(&response_buffer[..bytes_read])? {
|
||||||
|
// We've read a complete set of headers. We may have also read the first part of the
|
||||||
|
// response body; take whatever is left over in the response buffer and save that as
|
||||||
|
// the start of the response body.
|
||||||
|
response
|
||||||
|
.body_mut()
|
||||||
|
.extend_from_slice(&response_buffer[headers_len..bytes_read]);
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function reads the body for a response from the stream. If the Content-Length header is
|
||||||
|
/// present, it reads that many bytes; otherwise, it reads bytes until the connection is closed.
|
||||||
|
///
|
||||||
|
/// You will need to modify this function in Milestone 2.
|
||||||
|
fn read_body(stream: &mut TcpStream, response: &mut http::Response<Vec<u8>>) -> Result<(), Error> {
|
||||||
|
// The response may or may not supply a Content-Length header. If it provides the header, then
|
||||||
|
// we want to read that number of bytes; if it does not, we want to keep reading bytes until
|
||||||
|
// the connection is closed.
|
||||||
|
let content_length = get_content_length(response)?;
|
||||||
|
|
||||||
|
while content_length.is_none() || response.body().len() < content_length.unwrap() {
|
||||||
|
let mut buffer = [0_u8; 512];
|
||||||
|
let bytes_read = stream
|
||||||
|
.read(&mut buffer)
|
||||||
|
.or_else(|err| Err(Error::ConnectionError(err)))?;
|
||||||
|
if bytes_read == 0 {
|
||||||
|
// The server has hung up!
|
||||||
|
if content_length.is_none() {
|
||||||
|
// We've reached the end of the response
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
// Content-Length was set, but the server hung up before we managed to read that
|
||||||
|
// number of bytes
|
||||||
|
return Err(Error::ContentLengthMismatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the server doesn't send more bytes than it promised to send
|
||||||
|
if content_length.is_some() && response.body().len() + bytes_read > content_length.unwrap()
|
||||||
|
{
|
||||||
|
return Err(Error::ContentLengthMismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure server doesn't send more bytes than we allow
|
||||||
|
if response.body().len() + bytes_read > MAX_BODY_SIZE {
|
||||||
|
return Err(Error::ResponseBodyTooLarge);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append received bytes to the response body
|
||||||
|
response.body_mut().extend_from_slice(&buffer[..bytes_read]);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function reads and returns an HTTP response from a stream, returning an Error if the server
|
||||||
|
/// closes the connection prematurely or sends an invalid response.
|
||||||
|
///
|
||||||
|
/// You will need to modify this function in Milestone 2.
|
||||||
|
pub fn read_from_stream(
|
||||||
|
stream: &mut TcpStream,
|
||||||
|
request_method: &http::Method,
|
||||||
|
) -> Result<http::Response<Vec<u8>>, Error> {
|
||||||
|
let mut response = read_headers(stream)?;
|
||||||
|
// A response may have a body as long as it is not responding to a HEAD request and as long as
|
||||||
|
// the response status code is not 1xx, 204 (no content), or 304 (not modified).
|
||||||
|
if !(request_method == http::Method::HEAD
|
||||||
|
|| response.status().as_u16() < 200
|
||||||
|
|| response.status() == http::StatusCode::NO_CONTENT
|
||||||
|
|| response.status() == http::StatusCode::NOT_MODIFIED)
|
||||||
|
{
|
||||||
|
read_body(stream, &mut response)?;
|
||||||
|
}
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function serializes a response to bytes and writes those bytes to the provided stream.
|
||||||
|
///
|
||||||
|
/// You will need to modify this function in Milestone 2.
|
||||||
|
pub fn write_to_stream(
|
||||||
|
response: &http::Response<Vec<u8>>,
|
||||||
|
stream: &mut TcpStream,
|
||||||
|
) -> Result<(), std::io::Error> {
|
||||||
|
stream.write(&format_response_line(response).into_bytes())?;
|
||||||
|
stream.write(&['\r' as u8, '\n' as u8])?; // \r\n
|
||||||
|
for (header_name, header_value) in response.headers() {
|
||||||
|
stream.write(&format!("{}: ", header_name).as_bytes())?;
|
||||||
|
stream.write(header_value.as_bytes())?;
|
||||||
|
stream.write(&['\r' as u8, '\n' as u8])?; // \r\n
|
||||||
|
}
|
||||||
|
stream.write(&['\r' as u8, '\n' as u8])?;
|
||||||
|
if response.body().len() > 0 {
|
||||||
|
stream.write(response.body())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_response_line(response: &http::Response<Vec<u8>>) -> String {
|
||||||
|
format!(
|
||||||
|
"{:?} {} {}",
|
||||||
|
response.version(),
|
||||||
|
response.status().as_str(),
|
||||||
|
response.status().canonical_reason().unwrap_or("")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is a helper function that creates an http::Response containing an HTTP error that can be
|
||||||
|
/// sent to a client.
|
||||||
|
pub fn make_http_error(status: http::StatusCode) -> http::Response<Vec<u8>> {
|
||||||
|
let body = format!(
|
||||||
|
"HTTP {} {}",
|
||||||
|
status.as_u16(),
|
||||||
|
status.canonical_reason().unwrap_or("")
|
||||||
|
)
|
||||||
|
.into_bytes();
|
||||||
|
http::Response::builder()
|
||||||
|
.status(status)
|
||||||
|
.header("Content-Type", "text/plain")
|
||||||
|
.header("Content-Length", body.len().to_string())
|
||||||
|
.version(http::Version::HTTP_11)
|
||||||
|
.body(body)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
101
proj-2/balancebeam/tests/01_single_upstream_tests.rs
Normal file
101
proj-2/balancebeam/tests/01_single_upstream_tests.rs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
mod common;
|
||||||
|
|
||||||
|
use common::{init_logging, BalanceBeam, EchoServer, Server};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
async fn setup() -> (BalanceBeam, EchoServer) {
|
||||||
|
init_logging();
|
||||||
|
let upstream = EchoServer::new().await;
|
||||||
|
let balancebeam = BalanceBeam::new(&[&upstream.address], None, None).await;
|
||||||
|
(balancebeam, upstream)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test the simple case: open a few connections, each with only a single request, and make sure
|
||||||
|
/// things are delivered correctly.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_simple_connections() {
|
||||||
|
let (balancebeam, upstream) = setup().await;
|
||||||
|
|
||||||
|
log::info!("Sending a GET request");
|
||||||
|
let response_text = balancebeam
|
||||||
|
.get("/first_url")
|
||||||
|
.await
|
||||||
|
.expect("Error sending request to balancebeam");
|
||||||
|
assert!(response_text.contains("GET /first_url HTTP/1.1"));
|
||||||
|
assert!(response_text.contains("x-sent-by: balancebeam-tests"));
|
||||||
|
assert!(response_text.contains("x-forwarded-for: 127.0.0.1"));
|
||||||
|
|
||||||
|
log::info!("Sending a POST request");
|
||||||
|
let response_text = balancebeam
|
||||||
|
.post("/first_url", "Hello world!")
|
||||||
|
.await
|
||||||
|
.expect("Error sending request to balancebeam");
|
||||||
|
assert!(response_text.contains("POST /first_url HTTP/1.1"));
|
||||||
|
assert!(response_text.contains("x-sent-by: balancebeam-tests"));
|
||||||
|
assert!(response_text.contains("x-forwarded-for: 127.0.0.1"));
|
||||||
|
assert!(response_text.contains("\n\nHello world!"));
|
||||||
|
|
||||||
|
log::info!("Checking that the origin server received 2 requests");
|
||||||
|
let num_requests_received = Box::new(upstream).stop().await;
|
||||||
|
assert_eq!(
|
||||||
|
num_requests_received, 2,
|
||||||
|
"Upstream server did not receive the expected number of requests"
|
||||||
|
);
|
||||||
|
|
||||||
|
log::info!("All done :)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test handling of multiple HTTP requests per connection to the server. Open three concurrent
|
||||||
|
/// connections, and send four requests on each.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_multiple_requests_per_connection() {
|
||||||
|
let num_connections = 3;
|
||||||
|
let requests_per_connection = 4;
|
||||||
|
|
||||||
|
let (balancebeam, upstream) = setup().await;
|
||||||
|
let balancebeam_shared = Arc::new(balancebeam);
|
||||||
|
|
||||||
|
let mut tasks = Vec::new();
|
||||||
|
for task_num in 0..num_connections {
|
||||||
|
let balancebeam_shared = balancebeam_shared.clone();
|
||||||
|
tasks.push(tokio::task::spawn(async move {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
for req_num in 0..requests_per_connection {
|
||||||
|
log::info!(
|
||||||
|
"Task {} sending request {} (connection {})",
|
||||||
|
task_num,
|
||||||
|
req_num,
|
||||||
|
task_num
|
||||||
|
);
|
||||||
|
let path = format!("/conn-{}/req-{}", task_num, req_num);
|
||||||
|
let response_text = client
|
||||||
|
.get(&format!("http://{}{}", balancebeam_shared.address, path))
|
||||||
|
.header("x-sent-by", "balancebeam-tests")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to connect to balancebeam")
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.expect("Balancebeam replied with a malformed response");
|
||||||
|
assert!(response_text.contains(&format!("GET {} HTTP/1.1", path)));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
for join_handle in tasks {
|
||||||
|
join_handle.await.expect("Task panicked");
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Checking that the origin server received {} requests",
|
||||||
|
num_connections * requests_per_connection
|
||||||
|
);
|
||||||
|
let num_requests_received = Box::new(upstream).stop().await;
|
||||||
|
assert_eq!(
|
||||||
|
num_requests_received,
|
||||||
|
num_connections * requests_per_connection,
|
||||||
|
"Upstream server did not receive the expected number of requests"
|
||||||
|
);
|
||||||
|
|
||||||
|
log::info!("All done :)");
|
||||||
|
}
|
||||||
296
proj-2/balancebeam/tests/02_multiple_upstream_tests.rs
Normal file
296
proj-2/balancebeam/tests/02_multiple_upstream_tests.rs
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
mod common;
|
||||||
|
|
||||||
|
use common::{init_logging, BalanceBeam, EchoServer, ErrorServer, Server};
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::time::delay_for;
|
||||||
|
|
||||||
|
async fn setup_with_params(
|
||||||
|
n_upstreams: usize,
|
||||||
|
active_health_check_interval: Option<usize>,
|
||||||
|
max_requests_per_minute: Option<usize>,
|
||||||
|
) -> (BalanceBeam, Vec<Box<dyn Server>>) {
|
||||||
|
init_logging();
|
||||||
|
let mut upstreams: Vec<Box<dyn Server>> = Vec::new();
|
||||||
|
for _ in 0..n_upstreams {
|
||||||
|
upstreams.push(Box::new(EchoServer::new().await));
|
||||||
|
}
|
||||||
|
let upstream_addresses: Vec<String> = upstreams
|
||||||
|
.iter()
|
||||||
|
.map(|upstream| upstream.address())
|
||||||
|
.collect();
|
||||||
|
let upstream_addresses: Vec<&str> = upstream_addresses
|
||||||
|
.iter()
|
||||||
|
.map(|addr| addr.as_str())
|
||||||
|
.collect();
|
||||||
|
let balancebeam = BalanceBeam::new(
|
||||||
|
&upstream_addresses,
|
||||||
|
active_health_check_interval,
|
||||||
|
max_requests_per_minute,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
(balancebeam, upstreams)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup(n_upstreams: usize) -> (BalanceBeam, Vec<Box<dyn Server>>) {
|
||||||
|
setup_with_params(n_upstreams, None, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a bunch of requests to the load balancer, and ensure they are evenly distributed across the
|
||||||
|
/// upstream servers
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_load_distribution() {
|
||||||
|
let n_upstreams = 3;
|
||||||
|
let n_requests = 90;
|
||||||
|
let (balancebeam, mut upstreams) = setup(n_upstreams).await;
|
||||||
|
|
||||||
|
for i in 0..n_requests {
|
||||||
|
let path = format!("/request-{}", i);
|
||||||
|
let response_text = balancebeam
|
||||||
|
.get(&path)
|
||||||
|
.await
|
||||||
|
.expect("Error sending request to balancebeam");
|
||||||
|
assert!(response_text.contains(&format!("GET {} HTTP/1.1", path)));
|
||||||
|
assert!(response_text.contains("x-sent-by: balancebeam-tests"));
|
||||||
|
assert!(response_text.contains("x-forwarded-for: 127.0.0.1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut request_counters = Vec::new();
|
||||||
|
while let Some(upstream) = upstreams.pop() {
|
||||||
|
request_counters.insert(0, upstream.stop().await);
|
||||||
|
}
|
||||||
|
log::info!(
|
||||||
|
"Number of requests received by each upstream: {:?}",
|
||||||
|
request_counters
|
||||||
|
);
|
||||||
|
let avg_req_count =
|
||||||
|
request_counters.iter().sum::<usize>() as f64 / request_counters.len() as f64;
|
||||||
|
log::info!("Average number of requests per upstream: {}", avg_req_count);
|
||||||
|
for upstream_req_count in request_counters {
|
||||||
|
if (upstream_req_count as f64 - avg_req_count).abs() > 0.4 * avg_req_count {
|
||||||
|
log::error!(
|
||||||
|
"Upstream request count {} differs too much from the average! Load doesn't seem \
|
||||||
|
evenly distributed.",
|
||||||
|
upstream_req_count
|
||||||
|
);
|
||||||
|
panic!("Upstream request count differs too much");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("All done :)");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn try_failover(balancebeam: &BalanceBeam, upstreams: &mut Vec<Box<dyn Server>>) {
|
||||||
|
// Send some initial requests. Everything should work
|
||||||
|
log::info!("Sending some initial requests. These should definitely work.");
|
||||||
|
for i in 0..5 {
|
||||||
|
let path = format!("/request-{}", i);
|
||||||
|
let response_text = balancebeam
|
||||||
|
.get(&path)
|
||||||
|
.await
|
||||||
|
.expect("Error sending request to balancebeam");
|
||||||
|
assert!(response_text.contains(&format!("GET {} HTTP/1.1", path)));
|
||||||
|
assert!(response_text.contains("x-sent-by: balancebeam-tests"));
|
||||||
|
assert!(response_text.contains("x-forwarded-for: 127.0.0.1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill one of the upstreams
|
||||||
|
log::info!("Killing one of the upstream servers");
|
||||||
|
upstreams.pop().unwrap().stop().await;
|
||||||
|
|
||||||
|
// Make sure requests continue to work
|
||||||
|
for i in 0..6 {
|
||||||
|
log::info!("Sending request #{} after killing an upstream server", i);
|
||||||
|
let path = format!("/failover-{}", i);
|
||||||
|
let response_text = balancebeam
|
||||||
|
.get(&path)
|
||||||
|
.await
|
||||||
|
.expect("Error sending request to balancebeam. Passive failover may not be working");
|
||||||
|
assert!(
|
||||||
|
response_text.contains(&format!("GET {} HTTP/1.1", path)),
|
||||||
|
"balancebeam returned unexpected response. Failover may not be working."
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
response_text.contains("x-sent-by: balancebeam-tests"),
|
||||||
|
"balancebeam returned unexpected response. Failover may not be working."
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
response_text.contains("x-forwarded-for: 127.0.0.1"),
|
||||||
|
"balancebeam returned unexpected response. Failover may not be working."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make sure passive health checks work. Send a few requests, then kill one of the upstreams and
|
||||||
|
/// make sure requests continue to work
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_passive_health_checks() {
|
||||||
|
let n_upstreams = 2;
|
||||||
|
let (balancebeam, mut upstreams) = setup(n_upstreams).await;
|
||||||
|
try_failover(&balancebeam, &mut upstreams).await;
|
||||||
|
log::info!("All done :)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify that the active health checks are monitoring HTTP status, rather than simply depending
|
||||||
|
/// on whether connections can be established to determine whether an upstream is up:
|
||||||
|
///
|
||||||
|
/// * Send a few requests
|
||||||
|
/// * Replace one of the upstreams with a server that only returns HTTP error 500s
|
||||||
|
/// * Send some more requests. Make sure all the requests succeed
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_active_health_checks_check_http_status() {
|
||||||
|
let n_upstreams = 2;
|
||||||
|
let (balancebeam, mut upstreams) = setup_with_params(n_upstreams, Some(1), None).await;
|
||||||
|
let failed_ip = upstreams[upstreams.len() - 1].address();
|
||||||
|
|
||||||
|
// Send some initial requests. Everything should work
|
||||||
|
log::info!("Sending some initial requests. These should definitely work.");
|
||||||
|
for i in 0..4 {
|
||||||
|
let path = format!("/request-{}", i);
|
||||||
|
let response_text = balancebeam
|
||||||
|
.get(&path)
|
||||||
|
.await
|
||||||
|
.expect("Error sending request to balancebeam");
|
||||||
|
assert!(response_text.contains(&format!("GET {} HTTP/1.1", path)));
|
||||||
|
assert!(response_text.contains("x-sent-by: balancebeam-tests"));
|
||||||
|
assert!(response_text.contains("x-forwarded-for: 127.0.0.1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do a switcharoo with an upstream
|
||||||
|
log::info!("Replacing one of the upstreams with a server that returns Error 500s...");
|
||||||
|
upstreams.pop().unwrap().stop().await;
|
||||||
|
upstreams.push(Box::new(ErrorServer::new_at_address(failed_ip).await));
|
||||||
|
|
||||||
|
log::info!("Waiting for health checks to realize server is dead...");
|
||||||
|
delay_for(Duration::from_secs(3)).await;
|
||||||
|
|
||||||
|
// Make sure we get back successful requests
|
||||||
|
for i in 0..8 {
|
||||||
|
log::info!(
|
||||||
|
"Sending request #{} after swapping server for one that returns Error 500. We should \
|
||||||
|
get a successful response from the other upstream",
|
||||||
|
i
|
||||||
|
);
|
||||||
|
let path = format!("/failover-{}", i);
|
||||||
|
let response_text = balancebeam.get(&path).await.expect(
|
||||||
|
"Error sending request to balancebeam. Active health checks may not be working",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
response_text.contains(&format!("GET {} HTTP/1.1", path)),
|
||||||
|
"balancebeam returned unexpected response. Active health checks may not be working."
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
response_text.contains("x-sent-by: balancebeam-tests"),
|
||||||
|
"balancebeam returned unexpected response. Active health checks may not be working."
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
response_text.contains("x-forwarded-for: 127.0.0.1"),
|
||||||
|
"balancebeam returned unexpected response. Active health checks may not be working."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make sure active health checks restore upstreams that were previously failed but are now
|
||||||
|
/// working again:
|
||||||
|
///
|
||||||
|
/// * Send a few requests
|
||||||
|
/// * Kill one of the upstreams
|
||||||
|
/// * Send some more requests
|
||||||
|
/// * Bring the upstream back
|
||||||
|
/// * Ensure requests are delivered again
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_active_health_checks_restore_failed_upstream() {
|
||||||
|
let n_upstreams = 2;
|
||||||
|
let (balancebeam, mut upstreams) = setup_with_params(n_upstreams, Some(1), None).await;
|
||||||
|
let failed_ip = upstreams[upstreams.len() - 1].address();
|
||||||
|
try_failover(&balancebeam, &mut upstreams).await;
|
||||||
|
|
||||||
|
log::info!("Re-starting the \"failed\" upstream server...");
|
||||||
|
upstreams.push(Box::new(EchoServer::new_at_address(failed_ip).await));
|
||||||
|
|
||||||
|
log::info!("Waiting a few seconds for the active health check to run...");
|
||||||
|
delay_for(Duration::from_secs(3)).await;
|
||||||
|
|
||||||
|
log::info!("Sending some more requests");
|
||||||
|
for i in 0..5 {
|
||||||
|
let path = format!("/after-restore-{}", i);
|
||||||
|
let response_text = balancebeam
|
||||||
|
.get(&path)
|
||||||
|
.await
|
||||||
|
.expect("Error sending request to balancebeam");
|
||||||
|
assert!(response_text.contains(&format!("GET {} HTTP/1.1", path)));
|
||||||
|
assert!(response_text.contains("x-sent-by: balancebeam-tests"));
|
||||||
|
assert!(response_text.contains("x-forwarded-for: 127.0.0.1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Verifying that the previously-dead upstream got some requests after being restored"
|
||||||
|
);
|
||||||
|
let last_upstream_req_count = upstreams.pop().unwrap().stop().await;
|
||||||
|
assert!(
|
||||||
|
last_upstream_req_count > 0,
|
||||||
|
"We killed an upstream, then brought it back, but it never got any more requests!"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Shut down
|
||||||
|
while let Some(upstream) = upstreams.pop() {
|
||||||
|
upstream.stop().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("All done :)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable rate limiting and ensure that requests fail after sending more than the threshold
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_rate_limiting() {
|
||||||
|
let n_upstreams = 1;
|
||||||
|
let rate_limit_threshold = 5;
|
||||||
|
let num_extra_requests: usize = 3;
|
||||||
|
let (balancebeam, mut upstreams) =
|
||||||
|
setup_with_params(n_upstreams, None, Some(rate_limit_threshold)).await;
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Sending some basic requests to the server, within the rate limit threshold. These \
|
||||||
|
should succeed."
|
||||||
|
);
|
||||||
|
for i in 0..rate_limit_threshold {
|
||||||
|
let path = format!("/request-{}", i);
|
||||||
|
let response_text = balancebeam
|
||||||
|
.get(&path)
|
||||||
|
.await
|
||||||
|
.expect("Error sending request to balancebeam");
|
||||||
|
assert!(response_text.contains(&format!("GET {} HTTP/1.1", path)));
|
||||||
|
assert!(response_text.contains("x-sent-by: balancebeam-tests"));
|
||||||
|
assert!(response_text.contains("x-forwarded-for: 127.0.0.1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Sending more requests that exceed the rate limit threshold. The server should \
|
||||||
|
respond to these with an HTTP 429 (too many requests) error."
|
||||||
|
);
|
||||||
|
for i in 0..num_extra_requests {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let response = client
|
||||||
|
.get(&format!("http://{}/overboard-{}", balancebeam.address, i))
|
||||||
|
.header("x-sent-by", "balancebeam-tests")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect(
|
||||||
|
"Error sending rate limited request to balancebeam. You should be \
|
||||||
|
accepting the connection but sending back an HTTP error, rather than rejecting \
|
||||||
|
the connection outright.",
|
||||||
|
);
|
||||||
|
log::info!("{:?}", response);
|
||||||
|
log::info!("Checking to make sure the server responded with HTTP 429");
|
||||||
|
assert_eq!(response.status().as_u16(), 429);
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Ensuring the extra requests didn't go through to the upstream servers");
|
||||||
|
let mut total_request_count = 0;
|
||||||
|
while let Some(upstream) = upstreams.pop() {
|
||||||
|
total_request_count += upstream.stop().await;
|
||||||
|
}
|
||||||
|
assert_eq!(total_request_count, rate_limit_threshold);
|
||||||
|
|
||||||
|
log::info!("All done :)");
|
||||||
|
}
|
||||||
111
proj-2/balancebeam/tests/common/balancebeam.rs
Normal file
111
proj-2/balancebeam/tests/common/balancebeam.rs
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
use rand::Rng;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||||
|
use tokio::process::{Child, Command};
|
||||||
|
use tokio::time::delay_for;
|
||||||
|
|
||||||
|
pub struct BalanceBeam {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
child: Child, // process is killed when dropped (Command::kill_on_drop)
|
||||||
|
pub address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BalanceBeam {
|
||||||
|
fn target_bin_path() -> std::path::PathBuf {
|
||||||
|
let mut path = std::env::current_exe().expect("Could not get current test executable path");
|
||||||
|
path.pop();
|
||||||
|
path.pop();
|
||||||
|
path.push("balancebeam");
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new(
|
||||||
|
upstreams: &[&str],
|
||||||
|
active_health_check_interval: Option<usize>,
|
||||||
|
max_requests_per_minute: Option<usize>,
|
||||||
|
) -> BalanceBeam {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let address = format!("127.0.0.1:{}", rng.gen_range(1024, 65535));
|
||||||
|
let mut cmd = Command::new(BalanceBeam::target_bin_path());
|
||||||
|
cmd.arg("--bind").arg(&address);
|
||||||
|
for upstream in upstreams {
|
||||||
|
cmd.arg("--upstream").arg(upstream);
|
||||||
|
}
|
||||||
|
if let Some(active_health_check_interval) = active_health_check_interval {
|
||||||
|
cmd.arg("--active-health-check-interval")
|
||||||
|
.arg(active_health_check_interval.to_string());
|
||||||
|
}
|
||||||
|
if let Some(max_requests_per_minute) = max_requests_per_minute {
|
||||||
|
cmd.arg("--max-requests-per-minute")
|
||||||
|
.arg(max_requests_per_minute.to_string());
|
||||||
|
}
|
||||||
|
cmd.kill_on_drop(true);
|
||||||
|
cmd.stdout(std::process::Stdio::piped());
|
||||||
|
cmd.stderr(std::process::Stdio::piped());
|
||||||
|
let mut child = cmd.spawn().expect(&format!(
|
||||||
|
"Could not execute balancebeam binary {}",
|
||||||
|
BalanceBeam::target_bin_path().to_str().unwrap()
|
||||||
|
));
|
||||||
|
|
||||||
|
// Print output from the child. We want to intercept and log this output (instead of letting
|
||||||
|
// the child inherit stderr and print directly to the terminal) so that the output can be
|
||||||
|
// suppressed if the test passes and displayed if it fails.
|
||||||
|
let stdout = child
|
||||||
|
.stdout
|
||||||
|
.take()
|
||||||
|
.expect("Child process somehow missing stdout pipe!");
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut stdout_reader = BufReader::new(stdout).lines();
|
||||||
|
while let Some(line) = stdout_reader
|
||||||
|
.next_line()
|
||||||
|
.await
|
||||||
|
.expect("I/O error reading from child stdout")
|
||||||
|
{
|
||||||
|
println!("Balancebeam output: {}", line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let stderr = child
|
||||||
|
.stderr
|
||||||
|
.take()
|
||||||
|
.expect("Child process somehow missing stderr pipe!");
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut stderr_reader = BufReader::new(stderr).lines();
|
||||||
|
while let Some(line) = stderr_reader
|
||||||
|
.next_line()
|
||||||
|
.await
|
||||||
|
.expect("I/O error reading from child stderr")
|
||||||
|
{
|
||||||
|
println!("Balancebeam output: {}", line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hack: wait for executable to start running
|
||||||
|
delay_for(Duration::from_secs(1)).await;
|
||||||
|
BalanceBeam { child, address }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn get(&self, path: &str) -> Result<String, reqwest::Error> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
client
|
||||||
|
.get(&format!("http://{}{}", self.address, path))
|
||||||
|
.header("x-sent-by", "balancebeam-tests")
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn post(&self, path: &str, body: &str) -> Result<String, reqwest::Error> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
client
|
||||||
|
.post(&format!("http://{}{}", self.address, path))
|
||||||
|
.header("x-sent-by", "balancebeam-tests")
|
||||||
|
.body(body.to_string())
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
104
proj-2/balancebeam/tests/common/echo_server.rs
Normal file
104
proj-2/balancebeam/tests/common/echo_server.rs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
use crate::common::server::Server;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use hyper::service::{make_service_fn, service_fn};
|
||||||
|
use hyper::{Body, Request, Response};
|
||||||
|
use rand::Rng;
|
||||||
|
use std::sync::{atomic, Arc};
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ServerState {
|
||||||
|
pub requests_received: atomic::AtomicUsize,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn echo(
|
||||||
|
server_state: Arc<ServerState>,
|
||||||
|
req: Request<Body>,
|
||||||
|
) -> Result<Response<Body>, hyper::Error> {
|
||||||
|
server_state
|
||||||
|
.requests_received
|
||||||
|
.fetch_add(1, atomic::Ordering::SeqCst);
|
||||||
|
let mut req_text = format!("{} {} {:?}\n", req.method(), req.uri(), req.version());
|
||||||
|
for (header_name, header_value) in req.headers() {
|
||||||
|
req_text += &format!(
|
||||||
|
"{}: {}\n",
|
||||||
|
header_name.as_str(),
|
||||||
|
header_value.to_str().unwrap_or("<binary value>")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
req_text += "\n";
|
||||||
|
let mut req_as_bytes = req_text.into_bytes();
|
||||||
|
req_as_bytes.extend(hyper::body::to_bytes(req.into_body()).await?);
|
||||||
|
Ok(Response::new(Body::from(req_as_bytes)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EchoServer {
|
||||||
|
shutdown_signal_sender: oneshot::Sender<()>,
|
||||||
|
server_task: tokio::task::JoinHandle<()>,
|
||||||
|
pub address: String,
|
||||||
|
state: Arc<ServerState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EchoServer {
|
||||||
|
pub async fn new() -> EchoServer {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
EchoServer::new_at_address(format!("127.0.0.1:{}", rng.gen_range(1024, 65535))).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new_at_address(bind_addr_string: String) -> EchoServer {
|
||||||
|
let bind_addr = bind_addr_string.parse().unwrap();
|
||||||
|
// Create a one-shot channel that can be used to tell the server to shut down
|
||||||
|
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
|
||||||
|
|
||||||
|
// Start a separate server task
|
||||||
|
let server_state = Arc::new(ServerState {
|
||||||
|
requests_received: atomic::AtomicUsize::new(0),
|
||||||
|
});
|
||||||
|
let server_task_state = server_state.clone();
|
||||||
|
let server_task = tokio::spawn(async move {
|
||||||
|
let service = make_service_fn(|_| {
|
||||||
|
let server_task_state = server_task_state.clone();
|
||||||
|
async move {
|
||||||
|
Ok::<_, hyper::Error>(service_fn(move |req| {
|
||||||
|
let server_task_state = server_task_state.clone();
|
||||||
|
echo(server_task_state, req)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let server = hyper::Server::bind(&bind_addr)
|
||||||
|
.serve(service)
|
||||||
|
.with_graceful_shutdown(async {
|
||||||
|
shutdown_rx.await.ok();
|
||||||
|
});
|
||||||
|
// Start serving and wait for the server to exit
|
||||||
|
if let Err(e) = server.await {
|
||||||
|
log::error!("Error in EchoServer: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
EchoServer {
|
||||||
|
shutdown_signal_sender: shutdown_tx,
|
||||||
|
server_task,
|
||||||
|
state: server_state,
|
||||||
|
address: bind_addr_string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Server for EchoServer {
|
||||||
|
async fn stop(self: Box<Self>) -> usize {
|
||||||
|
// Tell the hyper server to stop
|
||||||
|
let _ = self.shutdown_signal_sender.send(());
|
||||||
|
// Wait for it to stop
|
||||||
|
self.server_task
|
||||||
|
.await
|
||||||
|
.expect("ErrorServer server task panicked");
|
||||||
|
|
||||||
|
self.state.requests_received.load(atomic::Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn address(&self) -> String {
|
||||||
|
self.address.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
95
proj-2/balancebeam/tests/common/error_server.rs
Normal file
95
proj-2/balancebeam/tests/common/error_server.rs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
use crate::common::server::Server;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use hyper::service::{make_service_fn, service_fn};
|
||||||
|
use hyper::{Body, Response};
|
||||||
|
use rand::Rng;
|
||||||
|
use std::sync::{atomic, Arc};
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ServerState {
|
||||||
|
pub requests_received: atomic::AtomicUsize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
async fn return_error() -> Result<Response<Body>, hyper::Error> {
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(http::StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ErrorServer {
|
||||||
|
shutdown_signal_sender: oneshot::Sender<()>,
|
||||||
|
server_task: tokio::task::JoinHandle<()>,
|
||||||
|
pub address: String,
|
||||||
|
state: Arc<ServerState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorServer {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn new() -> ErrorServer {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
ErrorServer::new_at_address(format!("127.0.0.1:{}", rng.gen_range(1024, 65535))).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn new_at_address(bind_addr_string: String) -> ErrorServer {
|
||||||
|
let bind_addr = bind_addr_string.parse().unwrap();
|
||||||
|
// Create a one-shot channel that can be used to tell the server to shut down
|
||||||
|
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
|
||||||
|
|
||||||
|
// Start a separate server task
|
||||||
|
let server_state = Arc::new(ServerState {
|
||||||
|
requests_received: atomic::AtomicUsize::new(0),
|
||||||
|
});
|
||||||
|
let server_task_state = server_state.clone();
|
||||||
|
let server_task = tokio::spawn(async move {
|
||||||
|
let service = make_service_fn(|_| {
|
||||||
|
let server_task_state = server_task_state.clone();
|
||||||
|
async move {
|
||||||
|
Ok::<_, hyper::Error>(service_fn(move |_req| {
|
||||||
|
server_task_state
|
||||||
|
.requests_received
|
||||||
|
.fetch_add(1, atomic::Ordering::SeqCst);
|
||||||
|
return_error()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let server = hyper::Server::bind(&bind_addr)
|
||||||
|
.serve(service)
|
||||||
|
.with_graceful_shutdown(async {
|
||||||
|
shutdown_rx.await.ok();
|
||||||
|
});
|
||||||
|
// Start serving and wait for the server to exit
|
||||||
|
if let Err(e) = server.await {
|
||||||
|
log::error!("Error in ErrorServer: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ErrorServer {
|
||||||
|
shutdown_signal_sender: shutdown_tx,
|
||||||
|
server_task,
|
||||||
|
state: server_state,
|
||||||
|
address: bind_addr_string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Server for ErrorServer {
|
||||||
|
async fn stop(self: Box<Self>) -> usize {
|
||||||
|
// Tell the hyper server to stop
|
||||||
|
let _ = self.shutdown_signal_sender.send(());
|
||||||
|
// Wait for it to stop
|
||||||
|
self.server_task
|
||||||
|
.await
|
||||||
|
.expect("ErrorServer server task panicked");
|
||||||
|
|
||||||
|
self.state.requests_received.load(atomic::Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn address(&self) -> String {
|
||||||
|
self.address.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
22
proj-2/balancebeam/tests/common/mod.rs
Normal file
22
proj-2/balancebeam/tests/common/mod.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
mod balancebeam;
|
||||||
|
mod echo_server;
|
||||||
|
mod error_server;
|
||||||
|
mod server;
|
||||||
|
|
||||||
|
use std::sync;
|
||||||
|
|
||||||
|
pub use balancebeam::BalanceBeam;
|
||||||
|
pub use echo_server::EchoServer;
|
||||||
|
pub use error_server::ErrorServer;
|
||||||
|
pub use server::Server;
|
||||||
|
|
||||||
|
static INIT_TESTS: sync::Once = sync::Once::new();
|
||||||
|
|
||||||
|
pub fn init_logging() {
|
||||||
|
INIT_TESTS.call_once(|| {
|
||||||
|
pretty_env_logger::formatted_builder()
|
||||||
|
.is_test(true)
|
||||||
|
.parse_filters("info")
|
||||||
|
.init();
|
||||||
|
});
|
||||||
|
}
|
||||||
7
proj-2/balancebeam/tests/common/server.rs
Normal file
7
proj-2/balancebeam/tests/common/server.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Server {
|
||||||
|
async fn stop(self: Box<Self>) -> usize;
|
||||||
|
fn address(&self) -> String;
|
||||||
|
}
|
||||||
15
week1/.gitignore
vendored
Normal file
15
week1/.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Vim swap files
|
||||||
|
.*.swp
|
||||||
|
|
||||||
|
# Mac
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Rust build artifacts
|
||||||
|
/part-1-hello-world/target/
|
||||||
|
/part-2-warmup/target/
|
||||||
|
/part-3-hangman/target/
|
||||||
|
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
9
week1/part-1-hello-world/Cargo.toml
Normal file
9
week1/part-1-hello-world/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "hello-world"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Ryan Eberhardt <reberhardt7@gmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
3
week1/part-1-hello-world/src/main.rs
Normal file
3
week1/part-1-hello-world/src/main.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("Hello, world!");
|
||||||
|
}
|
||||||
9
week1/part-2-warmup/Cargo.toml
Normal file
9
week1/part-2-warmup/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "warmup"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Ryan Eberhardt <reberhardt7@gmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
43
week1/part-2-warmup/src/main.rs
Normal file
43
week1/part-2-warmup/src/main.rs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/* The following exercises were borrowed from Will Crichton's CS 242 Rust lab. */
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("Hi! Try running \"cargo test\" to run tests.");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_n(v: Vec<i32>, n: i32) -> Vec<i32> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_n_inplace(v: &mut Vec<i32>, n: i32) {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dedup(v: &mut Vec<i32>) {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_n() {
|
||||||
|
assert_eq!(add_n(vec![1], 2), vec![3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_n_inplace() {
|
||||||
|
let mut v = vec![1];
|
||||||
|
add_n_inplace(&mut v, 2);
|
||||||
|
assert_eq!(v, vec![3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dedup() {
|
||||||
|
let mut v = vec![3, 1, 0, 1, 4, 4];
|
||||||
|
dedup(&mut v);
|
||||||
|
assert_eq!(v, vec![3, 1, 0, 4]);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
week1/part-3-hangman/Cargo.toml
Normal file
7
week1/part-3-hangman/Cargo.toml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "hangman"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Armin Namavari <arminn@stanford.edu>"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rand = "0.6.0"
|
||||||
40
week1/part-3-hangman/src/main.rs
Normal file
40
week1/part-3-hangman/src/main.rs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// Simple Hangman Program
|
||||||
|
// User gets five incorrect guesses
|
||||||
|
// Word chosen randomly from words.txt
|
||||||
|
// Inspiration from: https://doc.rust-lang.org/book/ch02-00-guessing-game-tutorial.html
|
||||||
|
// This assignment will introduce you to some fundamental syntax in Rust:
|
||||||
|
// - variable declaration
|
||||||
|
// - string manipulation
|
||||||
|
// - conditional statements
|
||||||
|
// - loops
|
||||||
|
// - vectors
|
||||||
|
// - files
|
||||||
|
// - user input
|
||||||
|
// We've tried to limit/hide Rust's quirks since we'll discuss those details
|
||||||
|
// more in depth in the coming lectures.
|
||||||
|
extern crate rand;
|
||||||
|
use rand::Rng;
|
||||||
|
use std::fs;
|
||||||
|
use std::io;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
const NUM_INCORRECT_GUESSES: u32 = 5;
|
||||||
|
const WORDS_PATH: &str = "words.txt";
|
||||||
|
|
||||||
|
fn pick_a_random_word() -> String {
|
||||||
|
let file_string = fs::read_to_string(WORDS_PATH).expect("Unable to read file.");
|
||||||
|
let words: Vec<&str> = file_string.split('\n').collect();
|
||||||
|
String::from(words[rand::thread_rng().gen_range(0, words.len())].trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let secret_word = pick_a_random_word();
|
||||||
|
// Note: given what you know about Rust so far, it's easier to pull characters out of a
|
||||||
|
// vector than it is to pull them out of a string. You can get the ith character of
|
||||||
|
// secret_word by doing secret_word_chars[i].
|
||||||
|
let secret_word_chars: Vec<char> = secret_word.chars().collect();
|
||||||
|
// Uncomment for debugging:
|
||||||
|
// println!("random word: {}", secret_word);
|
||||||
|
|
||||||
|
// Your code here! :)
|
||||||
|
}
|
||||||
9
week1/part-3-hangman/words.txt
Normal file
9
week1/part-3-hangman/words.txt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
immutable
|
||||||
|
borrowed
|
||||||
|
shared
|
||||||
|
reference
|
||||||
|
aluminum
|
||||||
|
oxidation
|
||||||
|
lobster
|
||||||
|
starfish
|
||||||
|
crawfish
|
||||||
3
week1/part-4.txt
Normal file
3
week1/part-4.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Survey code:
|
||||||
|
|
||||||
|
Thanks for being awesome! We appreciate you!
|
||||||
13
week2/.gitignore
vendored
Normal file
13
week2/.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Vim swap files
|
||||||
|
.*.swp
|
||||||
|
|
||||||
|
# Mac
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Rust build artifacts
|
||||||
|
/*/target/
|
||||||
|
|
||||||
|
/*/Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
34
week2/ownership.txt
Normal file
34
week2/ownership.txt
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
Example 1:
|
||||||
|
```
|
||||||
|
fn main() {
|
||||||
|
let mut s = String::from("hello");
|
||||||
|
let ref1 = &s;
|
||||||
|
let ref2 = &ref1;
|
||||||
|
let ref3 = &ref2;
|
||||||
|
s = String::from("goodbye");
|
||||||
|
println!("{}", ref3.to_uppercase());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
```
|
||||||
|
fn drip_drop() -> &String {
|
||||||
|
let s = String::from("hello world!");
|
||||||
|
return &s;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
```
|
||||||
|
fn main() {
|
||||||
|
let s1 = String::from("hello");
|
||||||
|
let mut v = Vec::new();
|
||||||
|
v.push(s1);
|
||||||
|
let s2: String = v[0];
|
||||||
|
println!("{}", s2);
|
||||||
|
}
|
||||||
|
```
|
||||||
6
week2/rdiff/Cargo.toml
Normal file
6
week2/rdiff/Cargo.toml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "rdiff"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Armin Namavari <arminn@stanford.edu>"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
8
week2/rdiff/handout-a.txt
Normal file
8
week2/rdiff/handout-a.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
This week's exercises will continue easing you into Rust and will feature some
|
||||||
|
components of object-oriented Rust that we're covering this week. You'll be
|
||||||
|
writing some programs that have more sophisticated logic that what you saw last
|
||||||
|
with last week's exercises. The first exercise is a warm-up: to implement the wc
|
||||||
|
command line utility in Rust. The second exercise is more challenging: to
|
||||||
|
implement the diff utility. In order to do so, you'll first find the longest
|
||||||
|
common subsequence (LCS) of lines between the two files and use this to inform
|
||||||
|
how you display your diff.
|
||||||
6
week2/rdiff/handout-b.txt
Normal file
6
week2/rdiff/handout-b.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
You'll be learning and practicing a lot of new Rust concepts this week by
|
||||||
|
writing some programs that have more sophisticated logic that what you saw last
|
||||||
|
with last week's exercises. The first exercise is a warm-up: to implement the wc
|
||||||
|
command line utility in Rust. The second exercise is more challenging: to
|
||||||
|
implement the diff utility. In order to do so, you'll first find the longest
|
||||||
|
subsequence that is common.
|
||||||
5
week2/rdiff/simple-a.txt
Normal file
5
week2/rdiff/simple-a.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
a
|
||||||
|
b
|
||||||
|
c
|
||||||
|
d
|
||||||
|
e
|
||||||
8
week2/rdiff/simple-b.txt
Normal file
8
week2/rdiff/simple-b.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
a
|
||||||
|
added
|
||||||
|
b
|
||||||
|
c
|
||||||
|
added
|
||||||
|
d
|
||||||
|
added
|
||||||
|
e
|
||||||
101
week2/rdiff/src/grid.rs
Normal file
101
week2/rdiff/src/grid.rs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
// Grid implemented as flat vector
|
||||||
|
pub struct Grid {
|
||||||
|
num_rows: usize,
|
||||||
|
num_cols: usize,
|
||||||
|
elems: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Grid {
|
||||||
|
/// Returns a Grid of the specified size, with all elements pre-initialized to zero.
|
||||||
|
pub fn new(num_rows: usize, num_cols: usize) -> Grid {
|
||||||
|
Grid {
|
||||||
|
num_rows: num_rows,
|
||||||
|
num_cols: num_cols,
|
||||||
|
// This syntax uses the vec! macro to create a vector of zeros, initialized to a
|
||||||
|
// specific length
|
||||||
|
// https://stackoverflow.com/a/29530932
|
||||||
|
elems: vec![0; num_rows * num_cols],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> (usize, usize) {
|
||||||
|
(self.num_rows, self.num_cols)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the element at the specified location. If the location is out of bounds, returns
|
||||||
|
/// None.
|
||||||
|
///
|
||||||
|
/// Note to students: this function also could have returned Result. It's a matter of taste in
|
||||||
|
/// how you define the semantics; many languages raise exceptions for out-of-bounds exceptions,
|
||||||
|
/// but others argue that makes code needlessly complex. Here, we decided to return Option to
|
||||||
|
/// give you more practice with Option :) and because this similar library returns Option:
|
||||||
|
/// https://docs.rs/array2d/0.2.1/array2d/struct.Array2D.html
|
||||||
|
#[allow(unused)] // TODO: delete this line when you implement this function
|
||||||
|
pub fn get(&self, row: usize, col: usize) -> Option<usize> {
|
||||||
|
unimplemented!();
|
||||||
|
// Be sure to delete the #[allow(unused)] line above
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the element at the specified location to the specified value. If the location is out
|
||||||
|
/// of bounds, returns Err with an error message.
|
||||||
|
#[allow(unused)] // TODO: delete this line when you implement this function
|
||||||
|
pub fn set(&mut self, row: usize, col: usize, val: usize) -> Result<(), &'static str> {
|
||||||
|
unimplemented!();
|
||||||
|
// Be sure to delete the #[allow(unused)] line above
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prints a visual representation of the grid. You can use this for debugging.
|
||||||
|
pub fn display(&self) {
|
||||||
|
for row in 0..self.num_rows {
|
||||||
|
let mut line = String::new();
|
||||||
|
for col in 0..self.num_cols {
|
||||||
|
line.push_str(&format!("{}, ", self.get(row, col).unwrap()));
|
||||||
|
}
|
||||||
|
println!("{}", line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets all the elements to zero.
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
for i in self.elems.iter_mut() {
|
||||||
|
*i = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_grid() {
|
||||||
|
let n_rows = 4;
|
||||||
|
let n_cols = 3;
|
||||||
|
let mut grid = Grid::new(n_rows, n_cols);
|
||||||
|
|
||||||
|
// Initialize grid
|
||||||
|
for r in 0..n_rows {
|
||||||
|
for c in 0..n_cols {
|
||||||
|
assert!(
|
||||||
|
grid.set(r, c, r * n_cols + c).is_ok(),
|
||||||
|
"Grid::set returned Err even though the provided bounds are valid!"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: you need to run "cargo test -- --nocapture" in order to see output printed
|
||||||
|
println!("Grid contents:");
|
||||||
|
grid.display();
|
||||||
|
|
||||||
|
// Make sure the values are what we expect
|
||||||
|
for r in 0..n_rows {
|
||||||
|
for c in 0..n_cols {
|
||||||
|
assert!(
|
||||||
|
grid.get(r, c).is_some(),
|
||||||
|
"Grid::get returned None even though the provided bounds are valid!"
|
||||||
|
);
|
||||||
|
assert_eq!(grid.get(r, c).unwrap(), r * n_cols + c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
week2/rdiff/src/main.rs
Normal file
94
week2/rdiff/src/main.rs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
use grid::Grid; // For lcs()
|
||||||
|
use std::env;
|
||||||
|
use std::fs::File; // For read_file_lines()
|
||||||
|
use std::io::{self, BufRead}; // For read_file_lines()
|
||||||
|
use std::process;
|
||||||
|
|
||||||
|
pub mod grid;
|
||||||
|
|
||||||
|
/// Reads the file at the supplied path, and returns a vector of strings.
|
||||||
|
#[allow(unused)] // TODO: delete this line when you implement this function
|
||||||
|
fn read_file_lines(filename: &String) -> Result<Vec<String>, io::Error> {
|
||||||
|
unimplemented!();
|
||||||
|
// Be sure to delete the #[allow(unused)] line above
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)] // TODO: delete this line when you implement this function
|
||||||
|
fn lcs(seq1: &Vec<String>, seq2: &Vec<String>) -> Grid {
|
||||||
|
// Note: Feel free to use unwrap() in this code, as long as you're basically certain it'll
|
||||||
|
// never happen. Conceptually, unwrap() is justified here, because there's not really any error
|
||||||
|
// condition you're watching out for (i.e. as long as your code is written correctly, nothing
|
||||||
|
// external can go wrong that we would want to handle in higher-level functions). The unwrap()
|
||||||
|
// calls act like having asserts in C code, i.e. as guards against programming error.
|
||||||
|
unimplemented!();
|
||||||
|
// Be sure to delete the #[allow(unused)] line above
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)] // TODO: delete this line when you implement this function
|
||||||
|
fn print_diff(lcs_table: &Grid, lines1: &Vec<String>, lines2: &Vec<String>, i: usize, j: usize) {
|
||||||
|
unimplemented!();
|
||||||
|
// Be sure to delete the #[allow(unused)] line above
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)] // TODO: delete this line when you implement this function
|
||||||
|
fn main() {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
if args.len() < 3 {
|
||||||
|
println!("Too few arguments.");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
let filename1 = &args[1];
|
||||||
|
let filename2 = &args[2];
|
||||||
|
|
||||||
|
unimplemented!();
|
||||||
|
// Be sure to delete the #[allow(unused)] line above
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_read_file_lines() {
|
||||||
|
let lines_result = read_file_lines(&String::from("handout-a.txt"));
|
||||||
|
assert!(lines_result.is_ok());
|
||||||
|
let lines = lines_result.unwrap();
|
||||||
|
assert_eq!(lines.len(), 8);
|
||||||
|
assert_eq!(
|
||||||
|
lines[0],
|
||||||
|
"This week's exercises will continue easing you into Rust and will feature some"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lcs() {
|
||||||
|
let mut expected = Grid::new(5, 4);
|
||||||
|
expected.set(1, 1, 1).unwrap();
|
||||||
|
expected.set(1, 2, 1).unwrap();
|
||||||
|
expected.set(1, 3, 1).unwrap();
|
||||||
|
expected.set(2, 1, 1).unwrap();
|
||||||
|
expected.set(2, 2, 1).unwrap();
|
||||||
|
expected.set(2, 3, 2).unwrap();
|
||||||
|
expected.set(3, 1, 1).unwrap();
|
||||||
|
expected.set(3, 2, 1).unwrap();
|
||||||
|
expected.set(3, 3, 2).unwrap();
|
||||||
|
expected.set(4, 1, 1).unwrap();
|
||||||
|
expected.set(4, 2, 2).unwrap();
|
||||||
|
expected.set(4, 3, 2).unwrap();
|
||||||
|
|
||||||
|
println!("Expected:");
|
||||||
|
expected.display();
|
||||||
|
let result = lcs(
|
||||||
|
&"abcd".chars().map(|c| c.to_string()).collect(),
|
||||||
|
&"adb".chars().map(|c| c.to_string()).collect(),
|
||||||
|
);
|
||||||
|
println!("Got:");
|
||||||
|
result.display();
|
||||||
|
assert_eq!(result.size(), expected.size());
|
||||||
|
for row in 0..expected.size().0 {
|
||||||
|
for col in 0..expected.size().1 {
|
||||||
|
assert_eq!(result.get(row, col), expected.get(row, col));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
week2/rwc/Cargo.toml
Normal file
9
week2/rwc/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "rwc"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Ryan Eberhardt <reberhardt7@gmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
12
week2/rwc/src/main.rs
Normal file
12
week2/rwc/src/main.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::process;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
if args.len() < 3 {
|
||||||
|
println!("Too few arguments.");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
let filename1 = &args[1];
|
||||||
|
// Your code here :)
|
||||||
|
}
|
||||||
1
week2/survey.txt
Normal file
1
week2/survey.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Survey code:
|
||||||
13
week3/.gitignore
vendored
Normal file
13
week3/.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Vim swap files
|
||||||
|
.*.swp
|
||||||
|
|
||||||
|
# Mac
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Rust build artifacts
|
||||||
|
/*/target/
|
||||||
|
|
||||||
|
/*/Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
11
week3/inspect-fds/Cargo.toml
Normal file
11
week3/inspect-fds/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "inspect-fds"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Ryan Eberhardt <reberhardt7@gmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nix = "0.17.0"
|
||||||
|
regex = "1.3.7"
|
||||||
10
week3/inspect-fds/Makefile
Normal file
10
week3/inspect-fds/Makefile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
SRCS = $(wildcard *.c)
|
||||||
|
PROGS = $(patsubst %.c,%,$(SRCS))
|
||||||
|
|
||||||
|
all: $(PROGS)
|
||||||
|
|
||||||
|
%: %.c
|
||||||
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(PROGS)
|
||||||
24
week3/inspect-fds/multi_pipe_test.c
Normal file
24
week3/inspect-fds/multi_pipe_test.c
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
int fds1[2];
|
||||||
|
int fds2[2];
|
||||||
|
pipe(fds1);
|
||||||
|
pipe(fds2);
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
dup2(fds1[0], STDIN_FILENO);
|
||||||
|
dup2(fds2[1], STDOUT_FILENO);
|
||||||
|
close(fds1[0]);
|
||||||
|
close(fds1[1]);
|
||||||
|
close(fds2[0]);
|
||||||
|
close(fds2[1]);
|
||||||
|
sleep(2);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
close(fds1[0]);
|
||||||
|
close(fds2[1]);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
1
week3/inspect-fds/nothing.c
Normal file
1
week3/inspect-fds/nothing.c
Normal file
@ -0,0 +1 @@
|
|||||||
|
int main() {}
|
||||||
61
week3/inspect-fds/src/main.rs
Normal file
61
week3/inspect-fds/src/main.rs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
use std::env;
|
||||||
|
|
||||||
|
mod open_file;
|
||||||
|
mod process;
|
||||||
|
mod ps_utils;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
if args.len() != 2 {
|
||||||
|
println!("Usage: {} <name or pid of target>", args[0]);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 1
|
||||||
|
let target = &args[1];
|
||||||
|
|
||||||
|
// TODO: Milestone 1: Get the target Process using psutils::get_target()
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use std::process::{Child, Command};
|
||||||
|
|
||||||
|
fn start_c_program(program: &str) -> Child {
|
||||||
|
Command::new(program)
|
||||||
|
.spawn()
|
||||||
|
.expect(&format!("Could not find {}. Have you run make?", program))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_exit_status_valid_target() {
|
||||||
|
let mut subprocess = start_c_program("./multi_pipe_test");
|
||||||
|
assert_eq!(
|
||||||
|
Command::new("./target/debug/inspect-fds")
|
||||||
|
.args(&[&subprocess.id().to_string()])
|
||||||
|
.status()
|
||||||
|
.expect("Could not find target/debug/inspect-fds. Is the binary compiled?")
|
||||||
|
.code()
|
||||||
|
.expect("Program was unexpectedly terminated by a signal"),
|
||||||
|
0,
|
||||||
|
"We expected the program to exit normally, but it didn't."
|
||||||
|
);
|
||||||
|
let _ = subprocess.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_exit_status_invalid_target() {
|
||||||
|
assert_eq!(
|
||||||
|
Command::new("./target/debug/inspect-fds")
|
||||||
|
.args(&["./nonexistent"])
|
||||||
|
.status()
|
||||||
|
.expect("Could not find target/debug/inspect-fds. Is the binary compiled?")
|
||||||
|
.code()
|
||||||
|
.expect("Program was unexpectedly terminated by a signal"),
|
||||||
|
1,
|
||||||
|
"Program exited with unexpected return code. Make sure you handle the case where \
|
||||||
|
ps_utils::get_target returns None and print an error message and return status \
|
||||||
|
1."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
197
week3/inspect-fds/src/open_file.rs
Normal file
197
week3/inspect-fds/src/open_file.rs
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
use regex::Regex;
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
#[allow(unused_imports)] // TODO: delete this line for Milestone 4
|
||||||
|
use std::{fmt, fs};
|
||||||
|
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 4
|
||||||
|
const O_WRONLY: usize = 00000001;
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 4
|
||||||
|
const O_RDWR: usize = 00000002;
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 4
|
||||||
|
const COLORS: [&str; 6] = [
|
||||||
|
"\x1B[38;5;9m",
|
||||||
|
"\x1B[38;5;10m",
|
||||||
|
"\x1B[38;5;11m",
|
||||||
|
"\x1B[38;5;12m",
|
||||||
|
"\x1B[38;5;13m",
|
||||||
|
"\x1B[38;5;14m",
|
||||||
|
];
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 4
|
||||||
|
const CLEAR_COLOR: &str = "\x1B[0m";
|
||||||
|
|
||||||
|
/// This enum can be used to represent whether a file is read-only, write-only, or read/write. An
|
||||||
|
/// enum is basically a value that can be one of some number of "things."
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 4
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum AccessMode {
|
||||||
|
Read,
|
||||||
|
Write,
|
||||||
|
ReadWrite,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for AccessMode {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
// Match operators are very commonly used with enums in Rust. They function similar to
|
||||||
|
// switch statements in other languages (but can be more expressive).
|
||||||
|
match self {
|
||||||
|
AccessMode::Read => write!(f, "{}", "read"),
|
||||||
|
AccessMode::Write => write!(f, "{}", "write"),
|
||||||
|
AccessMode::ReadWrite => write!(f, "{}", "read/write"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores information about an open file on the system. Since the Linux kernel doesn't really
|
||||||
|
/// expose much information about the open file table to userspace (cplayground uses a modified
|
||||||
|
/// kernel), this struct contains info from both the open file table and the vnode table.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct OpenFile {
|
||||||
|
pub name: String,
|
||||||
|
pub cursor: usize,
|
||||||
|
pub access_mode: AccessMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenFile {
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 4
|
||||||
|
pub fn new(name: String, cursor: usize, access_mode: AccessMode) -> OpenFile {
|
||||||
|
OpenFile {
|
||||||
|
name,
|
||||||
|
cursor,
|
||||||
|
access_mode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function takes a path of an open file and returns a more human-friendly name for that
|
||||||
|
/// file.
|
||||||
|
///
|
||||||
|
/// * For regular files, this will simply return the supplied path.
|
||||||
|
/// * For terminals (files starting with /dev/pts), this will return "<terminal>".
|
||||||
|
/// * For pipes (filenames formatted like pipe:[pipenum]), this will return "<pipe #pipenum>".
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 4
|
||||||
|
fn path_to_name(path: &str) -> String {
|
||||||
|
if path.starts_with("/dev/pts/") {
|
||||||
|
String::from("<terminal>")
|
||||||
|
} else if path.starts_with("pipe:[") && path.ends_with("]") {
|
||||||
|
let pipe_num = &path[path.find('[').unwrap() + 1..path.find(']').unwrap()];
|
||||||
|
format!("<pipe #{}>", pipe_num)
|
||||||
|
} else {
|
||||||
|
String::from(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This file takes the contents of /proc/{pid}/fdinfo/{fdnum} for some file descriptor and
|
||||||
|
/// extracts the cursor position of that file descriptor (technically, the position of the
|
||||||
|
/// open file table entry that the fd points to) using a regex. It returns None if the cursor
|
||||||
|
/// couldn't be found in the fdinfo text.
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 4
|
||||||
|
fn parse_cursor(fdinfo: &str) -> Option<usize> {
|
||||||
|
// Regex::new will return an Error if there is a syntactical error in our regular
|
||||||
|
// expression. We call unwrap() here because that indicates there's an obvious problem with
|
||||||
|
// our code, but if this were code for a critical system that needs to not crash, then
|
||||||
|
// we would want to return an Error instead.
|
||||||
|
let re = Regex::new(r"pos:\s*(\d+)").unwrap();
|
||||||
|
Some(
|
||||||
|
re.captures(fdinfo)?
|
||||||
|
.get(1)?
|
||||||
|
.as_str()
|
||||||
|
.parse::<usize>()
|
||||||
|
.ok()?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This file takes the contents of /proc/{pid}/fdinfo/{fdnum} for some file descriptor and
|
||||||
|
/// extracts the access mode for that open file using the "flags:" field contained in the
|
||||||
|
/// fdinfo text. It returns None if the "flags" field couldn't be found.
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 4
|
||||||
|
fn parse_access_mode(fdinfo: &str) -> Option<AccessMode> {
|
||||||
|
// Regex::new will return an Error if there is a syntactical error in our regular
|
||||||
|
// expression. We call unwrap() here because that indicates there's an obvious problem with
|
||||||
|
// our code, but if this were code for a critical system that needs to not crash, then
|
||||||
|
// we would want to return an Error instead.
|
||||||
|
let re = Regex::new(r"flags:\s*(\d+)").unwrap();
|
||||||
|
// Extract the flags field and parse it as octal
|
||||||
|
let flags = usize::from_str_radix(re.captures(fdinfo)?.get(1)?.as_str(), 8).ok()?;
|
||||||
|
if flags & O_WRONLY > 0 {
|
||||||
|
Some(AccessMode::Write)
|
||||||
|
} else if flags & O_RDWR > 0 {
|
||||||
|
Some(AccessMode::ReadWrite)
|
||||||
|
} else {
|
||||||
|
Some(AccessMode::Read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a specified process and fd number, this function reads /proc/{pid}/fd/{fdnum} and
|
||||||
|
/// /proc/{pid}/fdinfo/{fdnum} to populate an OpenFile struct. It returns None if the pid or fd
|
||||||
|
/// are invalid, or if necessary information is unavailable.
|
||||||
|
///
|
||||||
|
/// (Note: whether this function returns Option or Result is a matter of style and context.
|
||||||
|
/// Some people might argue that you should return Result, so that you have finer grained
|
||||||
|
/// control over possible things that could go wrong, e.g. you might want to handle things
|
||||||
|
/// differently if this fails because the process doesn't have a specified fd, vs if it
|
||||||
|
/// fails because it failed to read a /proc file. However, that significantly increases
|
||||||
|
/// complexity of error handling. In our case, this does not need to be a super robust
|
||||||
|
/// program and we don't need to do fine-grained error handling, so returning Option is a
|
||||||
|
/// simple way to indicate that "hey, we weren't able to get the necessary information"
|
||||||
|
/// without making a big deal of it.)
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 4
|
||||||
|
pub fn from_fd(pid: usize, fd: usize) -> Option<OpenFile> {
|
||||||
|
// TODO: implement for Milestone 4
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function returns the OpenFile's name with ANSI escape codes included to colorize
|
||||||
|
/// pipe names. It hashes the pipe name so that the same pipe name will always result in the
|
||||||
|
/// same color. This is useful for making program output more readable, since a user can
|
||||||
|
/// quickly see all the fds that point to a particular pipe.
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 5
|
||||||
|
pub fn colorized_name(&self) -> String {
|
||||||
|
if self.name.starts_with("<pipe") {
|
||||||
|
let mut hash = DefaultHasher::new();
|
||||||
|
self.name.hash(&mut hash);
|
||||||
|
let hash_val = hash.finish();
|
||||||
|
let color = COLORS[(hash_val % COLORS.len() as u64) as usize];
|
||||||
|
format!("{}{}{}", color, self.name, CLEAR_COLOR)
|
||||||
|
} else {
|
||||||
|
format!("{}", self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::ps_utils;
|
||||||
|
use std::process::{Child, Command};
|
||||||
|
|
||||||
|
fn start_c_program(program: &str) -> Child {
|
||||||
|
Command::new(program)
|
||||||
|
.spawn()
|
||||||
|
.expect(&format!("Could not find {}. Have you run make?", program))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_openfile_from_fd() {
|
||||||
|
let mut test_subprocess = start_c_program("./multi_pipe_test");
|
||||||
|
let process = ps_utils::get_target("multi_pipe_test").unwrap().unwrap();
|
||||||
|
// Get file descriptor 0, which should point to the terminal
|
||||||
|
let open_file = OpenFile::from_fd(process.pid, 0)
|
||||||
|
.expect("Expected to get open file data for multi_pipe_test, but OpenFile::from_fd returned None");
|
||||||
|
assert_eq!(open_file.name, "<terminal>");
|
||||||
|
assert_eq!(open_file.cursor, 0);
|
||||||
|
assert_eq!(open_file.access_mode, AccessMode::ReadWrite);
|
||||||
|
let _ = test_subprocess.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_openfile_from_fd_invalid_fd() {
|
||||||
|
let mut test_subprocess = start_c_program("./multi_pipe_test");
|
||||||
|
let process = ps_utils::get_target("multi_pipe_test").unwrap().unwrap();
|
||||||
|
// Get file descriptor 30, which should be invalid
|
||||||
|
assert!(
|
||||||
|
OpenFile::from_fd(process.pid, 30).is_none(),
|
||||||
|
"Expected None because file descriptor 30 is invalid"
|
||||||
|
);
|
||||||
|
let _ = test_subprocess.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
76
week3/inspect-fds/src/process.rs
Normal file
76
week3/inspect-fds/src/process.rs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
use crate::open_file::OpenFile;
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 3
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Process {
|
||||||
|
pub pid: usize,
|
||||||
|
pub ppid: usize,
|
||||||
|
pub command: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Process {
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 1
|
||||||
|
pub fn new(pid: usize, ppid: usize, command: String) -> Process {
|
||||||
|
Process { pid, ppid, command }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function returns a list of file descriptor numbers for this Process, if that
|
||||||
|
/// information is available (it will return None if the information is unavailable). The
|
||||||
|
/// information will commonly be unavailable if the process has exited. (Zombie processes
|
||||||
|
/// still have a pid, but their resources have already been freed, including the file
|
||||||
|
/// descriptor table.)
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 3
|
||||||
|
pub fn list_fds(&self) -> Option<Vec<usize>> {
|
||||||
|
// TODO: implement for Milestone 3
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function returns a list of (fdnumber, OpenFile) tuples, if file descriptor
|
||||||
|
/// information is available (it returns None otherwise). The information is commonly
|
||||||
|
/// unavailable if the process has already exited.
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 4
|
||||||
|
pub fn list_open_files(&self) -> Option<Vec<(usize, OpenFile)>> {
|
||||||
|
let mut open_files = vec![];
|
||||||
|
for fd in self.list_fds()? {
|
||||||
|
open_files.push((fd, OpenFile::from_fd(self.pid, fd)?));
|
||||||
|
}
|
||||||
|
Some(open_files)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::ps_utils;
|
||||||
|
use std::process::{Child, Command};
|
||||||
|
|
||||||
|
fn start_c_program(program: &str) -> Child {
|
||||||
|
Command::new(program)
|
||||||
|
.spawn()
|
||||||
|
.expect(&format!("Could not find {}. Have you run make?", program))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_fds() {
|
||||||
|
let mut test_subprocess = start_c_program("./multi_pipe_test");
|
||||||
|
let process = ps_utils::get_target("multi_pipe_test").unwrap().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
process
|
||||||
|
.list_fds()
|
||||||
|
.expect("Expected list_fds to find file descriptors, but it returned None"),
|
||||||
|
vec![0, 1, 2, 4, 5]
|
||||||
|
);
|
||||||
|
let _ = test_subprocess.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_fds_zombie() {
|
||||||
|
let mut test_subprocess = start_c_program("./nothing");
|
||||||
|
let process = ps_utils::get_target("nothing").unwrap().unwrap();
|
||||||
|
assert!(
|
||||||
|
process.list_fds().is_none(),
|
||||||
|
"Expected list_fds to return None for a zombie process"
|
||||||
|
);
|
||||||
|
let _ = test_subprocess.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
185
week3/inspect-fds/src/ps_utils.rs
Normal file
185
week3/inspect-fds/src/ps_utils.rs
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
use crate::process::Process;
|
||||||
|
use nix::unistd::getuid;
|
||||||
|
use std::fmt;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// This enum represents the possible causes that an error might occur. It's useful because it
|
||||||
|
/// allows a caller of an API to have fine-grained control over error handling based on the
|
||||||
|
/// specifics of what went wrong. You'll find similar ideas in Rust libraries, such as std::io:
|
||||||
|
/// https://doc.rust-lang.org/std/io/enum.ErrorKind.html However, you won't need to do anything
|
||||||
|
/// with this (or like this) in your own code.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
ExecutableError(std::io::Error),
|
||||||
|
OutputFormatError(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate readable representations of Error
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match &self {
|
||||||
|
Error::ExecutableError(err) => write!(f, "Error executing ps: {}", err),
|
||||||
|
Error::OutputFormatError(err) => write!(f, "ps printed malformed output: {}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make it possible to automatically convert std::io::Error to our Error type
|
||||||
|
impl From<std::io::Error> for Error {
|
||||||
|
fn from(error: std::io::Error) -> Error {
|
||||||
|
Error::ExecutableError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make it possible to automatically convert std::string::FromUtf8Error to our Error type
|
||||||
|
impl From<std::string::FromUtf8Error> for Error {
|
||||||
|
fn from(_error: std::string::FromUtf8Error) -> Error {
|
||||||
|
Error::OutputFormatError("Output is not utf-8")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make it possible to automatically convert std::string::ParseIntError to our Error type
|
||||||
|
impl From<std::num::ParseIntError> for Error {
|
||||||
|
fn from(_error: std::num::ParseIntError) -> Error {
|
||||||
|
Error::OutputFormatError("Error parsing integer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function takes a line of ps output formatted with -o "pid= ppid= command=" and returns a
|
||||||
|
/// Process struct initialized from the parsed output.
|
||||||
|
///
|
||||||
|
/// Example line:
|
||||||
|
/// " 578 577 emacs inode.c"
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 1
|
||||||
|
fn parse_ps_line(line: &str) -> Result<Process, Error> {
|
||||||
|
// ps doesn't output a very nice machine-readable output, so we do some wonky things here to
|
||||||
|
// deal with variable amounts of whitespace.
|
||||||
|
let mut remainder = line.trim();
|
||||||
|
let first_token_end = remainder
|
||||||
|
.find(char::is_whitespace)
|
||||||
|
.ok_or(Error::OutputFormatError("Missing second column"))?;
|
||||||
|
let pid = remainder[0..first_token_end].parse::<usize>()?;
|
||||||
|
remainder = remainder[first_token_end..].trim_start();
|
||||||
|
let second_token_end = remainder
|
||||||
|
.find(char::is_whitespace)
|
||||||
|
.ok_or(Error::OutputFormatError("Missing third column"))?;
|
||||||
|
let ppid = remainder[0..second_token_end].parse::<usize>()?;
|
||||||
|
remainder = remainder[second_token_end..].trim_start();
|
||||||
|
Ok(Process::new(pid, ppid, String::from(remainder)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function takes a pid and returns a Process struct for the specified process, or None if
|
||||||
|
/// the specified pid doesn't exist. An Error is only returned if ps cannot be executed or
|
||||||
|
/// produces unexpected output format.
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 1
|
||||||
|
fn get_process(pid: usize) -> Result<Option<Process>, Error> {
|
||||||
|
// Run ps to find the specified pid. We use the ? operator to return an Error if executing ps
|
||||||
|
// fails, or if it returns non-utf-8 output. (The extra Error traits above are used to
|
||||||
|
// automatically convert errors like std::io::Error or std::string::FromUtf8Error into our
|
||||||
|
// custom error type.)
|
||||||
|
let output = String::from_utf8(
|
||||||
|
Command::new("ps")
|
||||||
|
.args(&["--pid", &pid.to_string(), "-o", "pid= ppid= command="])
|
||||||
|
.output()?
|
||||||
|
.stdout,
|
||||||
|
)?;
|
||||||
|
// Return Some if the process was found and output parsing succeeds, or None if ps produced no
|
||||||
|
// output (indicating there is no matching process). Note the use of ? to propagate Error if an
|
||||||
|
// error occured in parsing the output.
|
||||||
|
if output.trim().len() > 0 {
|
||||||
|
Ok(Some(parse_ps_line(output.trim())?))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function takes a pid and returns a list of Process structs for processes that have the
|
||||||
|
/// specified pid as their parent process. An Error is returned if ps cannot be executed or
|
||||||
|
/// produces unexpected output format.
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 5
|
||||||
|
pub fn get_child_processes(pid: usize) -> Result<Vec<Process>, Error> {
|
||||||
|
let ps_output = Command::new("ps")
|
||||||
|
.args(&["--ppid", &pid.to_string(), "-o", "pid= ppid= command="])
|
||||||
|
.output()?;
|
||||||
|
let mut output = Vec::new();
|
||||||
|
for line in String::from_utf8(ps_output.stdout)?.lines() {
|
||||||
|
output.push(parse_ps_line(line)?);
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function takes a command name (e.g. "sort" or "./multi_pipe_test") and returns the first
|
||||||
|
/// matching process's pid, or None if no matching process is found. It returns an Error if there
|
||||||
|
/// is an error running pgrep or parsing pgrep's output.
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 1
|
||||||
|
fn get_pid_by_command_name(name: &str) -> Result<Option<usize>, Error> {
|
||||||
|
let output = String::from_utf8(
|
||||||
|
Command::new("pgrep")
|
||||||
|
.args(&["-xU", getuid().to_string().as_str(), name])
|
||||||
|
.output()?
|
||||||
|
.stdout,
|
||||||
|
)?;
|
||||||
|
Ok(match output.lines().next() {
|
||||||
|
Some(line) => Some(line.parse::<usize>()?),
|
||||||
|
None => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This program finds a target process on the system. The specified query can either be a
|
||||||
|
/// command name (e.g. "./subprocess_test") or a PID (e.g. "5612"). This function returns a
|
||||||
|
/// Process struct if the specified process was found, None if no matching processes were found, or
|
||||||
|
/// Error if an error was encountered in running ps or pgrep.
|
||||||
|
#[allow(unused)] // TODO: delete this line for Milestone 1
|
||||||
|
pub fn get_target(query: &str) -> Result<Option<Process>, Error> {
|
||||||
|
let pid_by_command = get_pid_by_command_name(query)?;
|
||||||
|
if pid_by_command.is_some() {
|
||||||
|
return get_process(pid_by_command.unwrap());
|
||||||
|
}
|
||||||
|
// If searching for the query as a command name failed, let's see if it's a valid pid
|
||||||
|
match query.parse() {
|
||||||
|
Ok(pid) => return get_process(pid),
|
||||||
|
Err(_) => return Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use std::process::Child;
|
||||||
|
|
||||||
|
fn start_c_program(program: &str) -> Child {
|
||||||
|
Command::new(program)
|
||||||
|
.spawn()
|
||||||
|
.expect(&format!("Could not find {}. Have you run make?", program))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_target_success() {
|
||||||
|
let mut subprocess = start_c_program("./multi_pipe_test");
|
||||||
|
let found = get_target("multi_pipe_test")
|
||||||
|
.expect("Passed valid \"multi_pipe_test\" to get_target, but it returned an error")
|
||||||
|
.expect("Passed valid \"multi_pipe_test\" to get_target, but it returned None");
|
||||||
|
assert_eq!(found.command, "./multi_pipe_test");
|
||||||
|
let _ = subprocess.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_target_invalid_command() {
|
||||||
|
let found = get_target("asdflksadfasdf")
|
||||||
|
.expect("get_target returned an error, even though ps and pgrep should be working");
|
||||||
|
assert!(
|
||||||
|
found.is_none(),
|
||||||
|
"Passed invalid target to get_target, but it returned Some"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_target_invalid_pid() {
|
||||||
|
let found = get_target("1234567890")
|
||||||
|
.expect("get_target returned an error, even though ps and pgrep should be working");
|
||||||
|
assert!(
|
||||||
|
found.is_none(),
|
||||||
|
"Passed invalid target to get_target, but it returned Some"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
week3/inspect-fds/zombie_test.c
Normal file
15
week3/inspect-fds/zombie_test.c
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
int fds[2];
|
||||||
|
pipe(fds);
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
close(fds[0]);
|
||||||
|
sleep(2);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
9
week3/linked_list/Cargo.toml
Normal file
9
week3/linked_list/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "linked_list"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Armin Namavari <arminn@stanford.edu>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
75
week3/linked_list/src/linked_list.rs
Normal file
75
week3/linked_list/src/linked_list.rs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
use std::fmt;
|
||||||
|
use std::option::Option;
|
||||||
|
|
||||||
|
pub struct LinkedList {
|
||||||
|
head: Option<Box<Node>>,
|
||||||
|
size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Node {
|
||||||
|
value: u32,
|
||||||
|
next: Option<Box<Node>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node {
|
||||||
|
pub fn new(value: u32, next: Option<Box<Node>>) -> Node {
|
||||||
|
Node {value: value, next: next}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LinkedList {
|
||||||
|
pub fn new() -> LinkedList {
|
||||||
|
LinkedList {head: None, size: 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_size(&self) -> usize {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.get_size() == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_front(&mut self, value: u32) {
|
||||||
|
let new_node: Box<Node> = Box::new(Node::new(value, self.head.take()));
|
||||||
|
self.head = Some(new_node);
|
||||||
|
self.size += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop_front(&mut self) -> Option<u32> {
|
||||||
|
let node: Box<Node> = self.head.take()?;
|
||||||
|
self.head = node.next;
|
||||||
|
self.size -= 1;
|
||||||
|
Some(node.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl fmt::Display for LinkedList {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let mut current: &Option<Box<Node>> = &self.head;
|
||||||
|
let mut result = String::new();
|
||||||
|
loop {
|
||||||
|
match current {
|
||||||
|
Some(node) => {
|
||||||
|
result = format!("{} {}", result, node.value);
|
||||||
|
current = &node.next;
|
||||||
|
},
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write!(f, "{}", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for LinkedList {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let mut current = self.head.take();
|
||||||
|
while let Some(mut node) = current {
|
||||||
|
current = node.next.take();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
22
week3/linked_list/src/main.rs
Normal file
22
week3/linked_list/src/main.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
use linked_list::LinkedList;
|
||||||
|
pub mod linked_list;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut list: LinkedList<u32> = LinkedList::new();
|
||||||
|
assert!(list.is_empty());
|
||||||
|
assert_eq!(list.get_size(), 0);
|
||||||
|
for i in 1..12 {
|
||||||
|
list.push_front(i);
|
||||||
|
}
|
||||||
|
println!("{}", list);
|
||||||
|
println!("list size: {}", list.get_size());
|
||||||
|
println!("top element: {}", list.pop_front().unwrap());
|
||||||
|
println!("{}", list);
|
||||||
|
println!("size: {}", list.get_size());
|
||||||
|
println!("{}", list.to_string()); // ToString impl for anything impl Display
|
||||||
|
|
||||||
|
// If you implement iterator trait:
|
||||||
|
//for val in &list {
|
||||||
|
// println!("{}", val);
|
||||||
|
//}
|
||||||
|
}
|
||||||
1
week3/survey.txt
Normal file
1
week3/survey.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Survey code:
|
||||||
13
week5/.gitignore
vendored
Normal file
13
week5/.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Vim swap files
|
||||||
|
.*.swp
|
||||||
|
|
||||||
|
# Mac
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Rust build artifacts
|
||||||
|
/*/target/
|
||||||
|
|
||||||
|
/*/Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
10
week5/farm/Cargo.toml
Normal file
10
week5/farm/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "farm"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Ryan Eberhardt <reberhardt7@gmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
num_cpus = "1.13.0"
|
||||||
82
week5/farm/src/main.rs
Normal file
82
week5/farm/src/main.rs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
use std::collections::VecDeque;
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Instant;
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use std::{env, process, thread};
|
||||||
|
|
||||||
|
/// Determines whether a number is prime. This function is taken from CS 110 factor.py.
|
||||||
|
///
|
||||||
|
/// You don't need to read or understand this code.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn is_prime(num: u32) -> bool {
|
||||||
|
if num <= 1 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for factor in 2..((num as f64).sqrt().floor() as u32) {
|
||||||
|
if num % factor == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the prime factors of a number and prints them to stdout. This function is taken
|
||||||
|
/// from CS 110 factor.py.
|
||||||
|
///
|
||||||
|
/// You don't need to read or understand this code.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn factor_number(num: u32) {
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
if num == 1 || is_prime(num) {
|
||||||
|
println!("{} = {} [time: {:?}]", num, num, start.elapsed());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut factors = Vec::new();
|
||||||
|
let mut curr_num = num;
|
||||||
|
for factor in 2..num {
|
||||||
|
while curr_num % factor == 0 {
|
||||||
|
factors.push(factor);
|
||||||
|
curr_num /= factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
factors.sort();
|
||||||
|
let factors_str = factors
|
||||||
|
.into_iter()
|
||||||
|
.map(|f| f.to_string())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(" * ");
|
||||||
|
println!("{} = {} [time: {:?}]", num, factors_str, start.elapsed());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a list of numbers supplied via argv.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn get_input_numbers() -> VecDeque<u32> {
|
||||||
|
let mut numbers = VecDeque::new();
|
||||||
|
for arg in env::args().skip(1) {
|
||||||
|
if let Ok(val) = arg.parse::<u32>() {
|
||||||
|
numbers.push_back(val);
|
||||||
|
} else {
|
||||||
|
println!("{} is not a valid number", arg);
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
numbers
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let num_threads = num_cpus::get();
|
||||||
|
println!("Farm starting on {} CPUs", num_threads);
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
// TODO: call get_input_numbers() and store a queue of numbers to factor
|
||||||
|
|
||||||
|
// TODO: spawn `num_threads` threads, each of which pops numbers off the queue and calls
|
||||||
|
// factor_number() until the queue is empty
|
||||||
|
|
||||||
|
// TODO: join all the threads you created
|
||||||
|
|
||||||
|
println!("Total execution time: {:?}", start.elapsed());
|
||||||
|
}
|
||||||
1
week5/survey.txt
Normal file
1
week5/survey.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Survey code:
|
||||||
13
week6/.gitignore
vendored
Normal file
13
week6/.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Vim swap files
|
||||||
|
.*.swp
|
||||||
|
|
||||||
|
# Mac
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Rust build artifacts
|
||||||
|
/*/target/
|
||||||
|
|
||||||
|
/*/Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
10
week6/parallel_map/Cargo.toml
Normal file
10
week6/parallel_map/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "parallel_map"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Armin Namavari <arminn@stanford.edu>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
crossbeam-channel = "0.4.2"
|
||||||
23
week6/parallel_map/src/main.rs
Normal file
23
week6/parallel_map/src/main.rs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
use crossbeam_channel;
|
||||||
|
use std::{thread, time};
|
||||||
|
|
||||||
|
fn parallel_map<T, U, F>(mut input_vec: Vec<T>, num_threads: usize, f: F) -> Vec<U>
|
||||||
|
where
|
||||||
|
F: FnOnce(T) -> U + Send + Copy + 'static,
|
||||||
|
T: Send + 'static,
|
||||||
|
U: Send + 'static + Default,
|
||||||
|
{
|
||||||
|
let mut output_vec: Vec<U> = Vec::with_capacity(input_vec.len());
|
||||||
|
// TODO: implement parallel map!
|
||||||
|
output_vec
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let v = vec![6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 12, 18, 11, 5, 20];
|
||||||
|
let squares = parallel_map(v, 10, |num| {
|
||||||
|
println!("{} squared is {}", num, num * num);
|
||||||
|
thread::sleep(time::Duration::from_millis(500));
|
||||||
|
num * num
|
||||||
|
});
|
||||||
|
println!("squares: {:?}", squares);
|
||||||
|
}
|
||||||
1
week6/survey.txt
Normal file
1
week6/survey.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Survey code:
|
||||||
Loading…
Reference in New Issue
Block a user