commit e1aa7f4345da3857913b9da1812f0ce0a7b1c5cb Author: Ryan Eberhardt Date: Wed Oct 7 15:58:20 2020 -0700 Initial commit diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..8aa2645 --- /dev/null +++ b/LICENSE.txt @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4cb05bd --- /dev/null +++ b/README.md @@ -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! diff --git a/proj-1/.gitignore b/proj-1/.gitignore new file mode 100644 index 0000000..6877616 --- /dev/null +++ b/proj-1/.gitignore @@ -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 diff --git a/proj-1/deet/Cargo.toml b/proj-1/deet/Cargo.toml new file mode 100644 index 0000000..b42ed14 --- /dev/null +++ b/proj-1/deet/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "deet" +version = "0.1.0" +authors = ["Ryan Eberhardt "] +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" diff --git a/proj-1/deet/Dockerfile b/proj-1/deet/Dockerfile new file mode 100644 index 0000000..0256d5c --- /dev/null +++ b/proj-1/deet/Dockerfile @@ -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 diff --git a/proj-1/deet/Makefile b/proj-1/deet/Makefile new file mode 100644 index 0000000..a6b4d46 --- /dev/null +++ b/proj-1/deet/Makefile @@ -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) diff --git a/proj-1/deet/container b/proj-1/deet/container new file mode 100755 index 0000000..501ffd7 --- /dev/null +++ b/proj-1/deet/container @@ -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 "$@" diff --git a/proj-1/deet/samples/count.c b/proj-1/deet/samples/count.c new file mode 100644 index 0000000..3923147 --- /dev/null +++ b/proj-1/deet/samples/count.c @@ -0,0 +1,10 @@ +#include + +int main() { + printf("1\n"); + printf("2\n"); + printf("3\n"); + printf("4\n"); + printf("5\n"); + return 0; +} diff --git a/proj-1/deet/samples/exit.c b/proj-1/deet/samples/exit.c new file mode 100644 index 0000000..25e0716 --- /dev/null +++ b/proj-1/deet/samples/exit.c @@ -0,0 +1,5 @@ +#include + +int main() { + asm("syscall" :: "a"(60), "D"(0)); +} diff --git a/proj-1/deet/samples/function_calls.c b/proj-1/deet/samples/function_calls.c new file mode 100644 index 0000000..9357ce8 --- /dev/null +++ b/proj-1/deet/samples/function_calls.c @@ -0,0 +1,25 @@ +#include + +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); +} diff --git a/proj-1/deet/samples/hello.c b/proj-1/deet/samples/hello.c new file mode 100644 index 0000000..f8edd23 --- /dev/null +++ b/proj-1/deet/samples/hello.c @@ -0,0 +1,6 @@ +#include + +int main() { + printf("Hello world!\n"); + return 0; +} diff --git a/proj-1/deet/samples/segfault.c b/proj-1/deet/samples/segfault.c new file mode 100644 index 0000000..6a60c73 --- /dev/null +++ b/proj-1/deet/samples/segfault.c @@ -0,0 +1,16 @@ +#include + +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); +} diff --git a/proj-1/deet/samples/sleepy_print.c b/proj-1/deet/samples/sleepy_print.c new file mode 100644 index 0000000..2065afc --- /dev/null +++ b/proj-1/deet/samples/sleepy_print.c @@ -0,0 +1,16 @@ +#include +#include +#include + +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 \n", argv[0]); + exit(1); + } + for (unsigned long i = 0; i < num_seconds; i++) { + printf("%lu\n", i); + sleep(1); + } + return 0; +} diff --git a/proj-1/deet/src/debugger.rs b/proj-1/deet/src/debugger.rs new file mode 100644 index 0000000..42e1782 --- /dev/null +++ b/proj-1/deet/src/debugger.rs @@ -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, +} + +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."); + } + } + } + } + } +} diff --git a/proj-1/deet/src/debugger_command.rs b/proj-1/deet/src/debugger_command.rs new file mode 100644 index 0000000..4b111fc --- /dev/null +++ b/proj-1/deet/src/debugger_command.rs @@ -0,0 +1,20 @@ +pub enum DebuggerCommand { + Quit, + Run(Vec), +} + +impl DebuggerCommand { + pub fn from_tokens(tokens: &Vec<&str>) -> Option { + 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, + } + } +} diff --git a/proj-1/deet/src/dwarf_data.rs b/proj-1/deet/src/dwarf_data.rs new file mode 100644 index 0000000..954d4f3 --- /dev/null +++ b/proj-1/deet/src/dwarf_data.rs @@ -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, + addr2line: Context>, +} + +impl fmt::Debug for DwarfData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "DwarfData {{files: {:?}}}", self.files) + } +} + +impl From for Error { + fn from(err: gimli_wrapper::Error) -> Self { + Error::DwarfFormatError(err) + } +} + +impl DwarfData { + pub fn from_file(path: &str) -> Result { + 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 { + 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 { + 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 { + 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 { + 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, +} + +#[derive(Debug, Default, Clone)] +pub struct File { + pub name: String, + pub global_variables: Vec, + pub functions: Vec, + pub lines: Vec, +} + +#[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) + } +} + + diff --git a/proj-1/deet/src/gimli_wrapper.rs b/proj-1/deet/src/gimli_wrapper.rs new file mode 100644 index 0000000..95f49ff --- /dev/null +++ b/proj-1/deet/src/gimli_wrapper.rs @@ -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, Error> { + // Load a section and return as `Cow<[u8]>`. + let load_section = |id: gimli::SectionId| -> Result, 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 = HashMap::new(); + + let mut compilation_units: Vec = 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 { + "".to_string() + } + } else { + "".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 { + "".to_string() + } + } else { + "".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 = None; + let mut location: Option = 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 for Error { + fn from(err: gimli::Error) -> Self { + Error::GimliError(err) + } +} + +impl From for Error { + fn from(err: addr2line::gimli::Error) -> Self { + Error::Addr2lineError(err) + } +} + +impl From for Error { + fn from(_: io::Error) -> Self { + Error::IoError + } +} + +impl From 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 + Send + Sync {} + +fn get_location(attr: &gimli::Attribute, unit: &gimli::Unit) -> Option { + 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( + attr: &gimli::Attribute, + unit: &gimli::Unit, + dwarf: &gimli::Dwarf, +) -> Result { + 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( + w: &mut W, + file: u64, + unit: &gimli::Unit, + dwarf: &gimli::Dwarf, +) -> 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( + w: &mut W, + encoding: gimli::Encoding, + data: &gimli::Expression, +) -> 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( + w: &mut W, + encoding: gimli::Encoding, + dwop: gimli::DwOp, + op: gimli::Operation, +) -> 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(()) +} diff --git a/proj-1/deet/src/inferior.rs b/proj-1/deet/src/inferior.rs new file mode 100644 index 0000000..b87a142 --- /dev/null +++ b/proj-1/deet/src/inferior.rs @@ -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) -> Option { + // 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) -> Result { + 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), + }) + } +} diff --git a/proj-1/deet/src/main.rs b/proj-1/deet/src/main.rs new file mode 100644 index 0000000..a944ed0 --- /dev/null +++ b/proj-1/deet/src/main.rs @@ -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 = env::args().collect(); + if args.len() != 2 { + println!("Usage: {} ", 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(); +} diff --git a/proj-2/.gitignore b/proj-2/.gitignore new file mode 100644 index 0000000..0074f47 --- /dev/null +++ b/proj-2/.gitignore @@ -0,0 +1,5 @@ +/balancebeam/.cargo/ +/balancebeam/target/ +/balancebeam/Cargo.lock +.idea +.*.swp diff --git a/proj-2/Dockerfile b/proj-2/Dockerfile new file mode 100644 index 0000000..2e61114 --- /dev/null +++ b/proj-2/Dockerfile @@ -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", "--"] diff --git a/proj-2/balancebeam/Cargo.toml b/proj-2/balancebeam/Cargo.toml new file mode 100644 index 0000000..ce36631 --- /dev/null +++ b/proj-2/balancebeam/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "balancebeam" +version = "0.1.0" +authors = ["Ryan Eberhardt "] +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" diff --git a/proj-2/balancebeam/src/main.rs b/proj-2/balancebeam/src/main.rs new file mode 100644 index 0000000..ae7b533 --- /dev/null +++ b/proj-2/balancebeam/src/main.rs @@ -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, + #[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, +} + +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 { + 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>) { + 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"); + } +} diff --git a/proj-2/balancebeam/src/request.rs b/proj-2/balancebeam/src/request.rs new file mode 100644 index 0000000..d07897f --- /dev/null +++ b/proj-2/balancebeam/src/request.rs @@ -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>) -> Result, 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::() + .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>, + 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>, 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>, 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>, + 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>, 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>, + 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>) -> String { + format!("{} {} {:?}", request.method(), request.uri(), request.version()) +} diff --git a/proj-2/balancebeam/src/response.rs b/proj-2/balancebeam/src/response.rs new file mode 100644 index 0000000..a6dedca --- /dev/null +++ b/proj-2/balancebeam/src/response.rs @@ -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>) -> Result, 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::() + .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>, 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>, 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>) -> 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>, 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>, + 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>) -> 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> { + 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() +} diff --git a/proj-2/balancebeam/tests/01_single_upstream_tests.rs b/proj-2/balancebeam/tests/01_single_upstream_tests.rs new file mode 100644 index 0000000..76236fb --- /dev/null +++ b/proj-2/balancebeam/tests/01_single_upstream_tests.rs @@ -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 :)"); +} diff --git a/proj-2/balancebeam/tests/02_multiple_upstream_tests.rs b/proj-2/balancebeam/tests/02_multiple_upstream_tests.rs new file mode 100644 index 0000000..1536216 --- /dev/null +++ b/proj-2/balancebeam/tests/02_multiple_upstream_tests.rs @@ -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, + max_requests_per_minute: Option, +) -> (BalanceBeam, Vec>) { + init_logging(); + let mut upstreams: Vec> = Vec::new(); + for _ in 0..n_upstreams { + upstreams.push(Box::new(EchoServer::new().await)); + } + let upstream_addresses: Vec = 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>) { + 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::() 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>) { + // 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 :)"); +} diff --git a/proj-2/balancebeam/tests/common/balancebeam.rs b/proj-2/balancebeam/tests/common/balancebeam.rs new file mode 100644 index 0000000..043c3d0 --- /dev/null +++ b/proj-2/balancebeam/tests/common/balancebeam.rs @@ -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, + max_requests_per_minute: Option, + ) -> 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 { + 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 { + 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 + } +} diff --git a/proj-2/balancebeam/tests/common/echo_server.rs b/proj-2/balancebeam/tests/common/echo_server.rs new file mode 100644 index 0000000..f78837a --- /dev/null +++ b/proj-2/balancebeam/tests/common/echo_server.rs @@ -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, + req: Request, +) -> Result, 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("") + ); + } + 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, +} + +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) -> 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() + } +} diff --git a/proj-2/balancebeam/tests/common/error_server.rs b/proj-2/balancebeam/tests/common/error_server.rs new file mode 100644 index 0000000..210f567 --- /dev/null +++ b/proj-2/balancebeam/tests/common/error_server.rs @@ -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, 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, +} + +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) -> 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() + } +} diff --git a/proj-2/balancebeam/tests/common/mod.rs b/proj-2/balancebeam/tests/common/mod.rs new file mode 100644 index 0000000..dceca3d --- /dev/null +++ b/proj-2/balancebeam/tests/common/mod.rs @@ -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(); + }); +} diff --git a/proj-2/balancebeam/tests/common/server.rs b/proj-2/balancebeam/tests/common/server.rs new file mode 100644 index 0000000..0163115 --- /dev/null +++ b/proj-2/balancebeam/tests/common/server.rs @@ -0,0 +1,7 @@ +use async_trait::async_trait; + +#[async_trait] +pub trait Server { + async fn stop(self: Box) -> usize; + fn address(&self) -> String; +} diff --git a/week1/.gitignore b/week1/.gitignore new file mode 100644 index 0000000..e05bd8e --- /dev/null +++ b/week1/.gitignore @@ -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 diff --git a/week1/part-1-hello-world/Cargo.toml b/week1/part-1-hello-world/Cargo.toml new file mode 100644 index 0000000..9b4c8f0 --- /dev/null +++ b/week1/part-1-hello-world/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "hello-world" +version = "0.1.0" +authors = ["Ryan Eberhardt "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/week1/part-1-hello-world/src/main.rs b/week1/part-1-hello-world/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/week1/part-1-hello-world/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/week1/part-2-warmup/Cargo.toml b/week1/part-2-warmup/Cargo.toml new file mode 100644 index 0000000..90689b4 --- /dev/null +++ b/week1/part-2-warmup/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "warmup" +version = "0.1.0" +authors = ["Ryan Eberhardt "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/week1/part-2-warmup/src/main.rs b/week1/part-2-warmup/src/main.rs new file mode 100644 index 0000000..d5e231d --- /dev/null +++ b/week1/part-2-warmup/src/main.rs @@ -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, n: i32) -> Vec { + unimplemented!() +} + +fn add_n_inplace(v: &mut Vec, n: i32) { + unimplemented!() +} + +fn dedup(v: &mut Vec) { + 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]); + } +} diff --git a/week1/part-3-hangman/Cargo.toml b/week1/part-3-hangman/Cargo.toml new file mode 100644 index 0000000..98ce4d6 --- /dev/null +++ b/week1/part-3-hangman/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "hangman" +version = "0.1.0" +authors = ["Armin Namavari "] + +[dependencies] +rand = "0.6.0" \ No newline at end of file diff --git a/week1/part-3-hangman/src/main.rs b/week1/part-3-hangman/src/main.rs new file mode 100644 index 0000000..1d6cfe6 --- /dev/null +++ b/week1/part-3-hangman/src/main.rs @@ -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 = secret_word.chars().collect(); + // Uncomment for debugging: + // println!("random word: {}", secret_word); + + // Your code here! :) +} diff --git a/week1/part-3-hangman/words.txt b/week1/part-3-hangman/words.txt new file mode 100644 index 0000000..f22e405 --- /dev/null +++ b/week1/part-3-hangman/words.txt @@ -0,0 +1,9 @@ +immutable +borrowed +shared +reference +aluminum +oxidation +lobster +starfish +crawfish \ No newline at end of file diff --git a/week1/part-4.txt b/week1/part-4.txt new file mode 100644 index 0000000..800df26 --- /dev/null +++ b/week1/part-4.txt @@ -0,0 +1,3 @@ +Survey code: + +Thanks for being awesome! We appreciate you! diff --git a/week2/.gitignore b/week2/.gitignore new file mode 100644 index 0000000..bbaf2d6 --- /dev/null +++ b/week2/.gitignore @@ -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 diff --git a/week2/ownership.txt b/week2/ownership.txt new file mode 100644 index 0000000..e40b247 --- /dev/null +++ b/week2/ownership.txt @@ -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); +} +``` diff --git a/week2/rdiff/Cargo.toml b/week2/rdiff/Cargo.toml new file mode 100644 index 0000000..632e507 --- /dev/null +++ b/week2/rdiff/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "rdiff" +version = "0.1.0" +authors = ["Armin Namavari "] + +[dependencies] diff --git a/week2/rdiff/handout-a.txt b/week2/rdiff/handout-a.txt new file mode 100644 index 0000000..20a55a9 --- /dev/null +++ b/week2/rdiff/handout-a.txt @@ -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. diff --git a/week2/rdiff/handout-b.txt b/week2/rdiff/handout-b.txt new file mode 100644 index 0000000..5c4b04e --- /dev/null +++ b/week2/rdiff/handout-b.txt @@ -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. diff --git a/week2/rdiff/simple-a.txt b/week2/rdiff/simple-a.txt new file mode 100644 index 0000000..9405325 --- /dev/null +++ b/week2/rdiff/simple-a.txt @@ -0,0 +1,5 @@ +a +b +c +d +e diff --git a/week2/rdiff/simple-b.txt b/week2/rdiff/simple-b.txt new file mode 100644 index 0000000..36c6263 --- /dev/null +++ b/week2/rdiff/simple-b.txt @@ -0,0 +1,8 @@ +a +added +b +c +added +d +added +e diff --git a/week2/rdiff/src/grid.rs b/week2/rdiff/src/grid.rs new file mode 100644 index 0000000..5ed22a5 --- /dev/null +++ b/week2/rdiff/src/grid.rs @@ -0,0 +1,101 @@ +// Grid implemented as flat vector +pub struct Grid { + num_rows: usize, + num_cols: usize, + elems: Vec, +} + +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 { + 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); + } + } + } +} diff --git a/week2/rdiff/src/main.rs b/week2/rdiff/src/main.rs new file mode 100644 index 0000000..019c83c --- /dev/null +++ b/week2/rdiff/src/main.rs @@ -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, 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, seq2: &Vec) -> 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, lines2: &Vec, 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 = 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)); + } + } + } +} diff --git a/week2/rwc/Cargo.toml b/week2/rwc/Cargo.toml new file mode 100644 index 0000000..e471565 --- /dev/null +++ b/week2/rwc/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "rwc" +version = "0.1.0" +authors = ["Ryan Eberhardt "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/week2/rwc/src/main.rs b/week2/rwc/src/main.rs new file mode 100644 index 0000000..737a0af --- /dev/null +++ b/week2/rwc/src/main.rs @@ -0,0 +1,12 @@ +use std::env; +use std::process; + +fn main() { + let args: Vec = env::args().collect(); + if args.len() < 3 { + println!("Too few arguments."); + process::exit(1); + } + let filename1 = &args[1]; + // Your code here :) +} diff --git a/week2/survey.txt b/week2/survey.txt new file mode 100644 index 0000000..5f30df8 --- /dev/null +++ b/week2/survey.txt @@ -0,0 +1 @@ +Survey code: diff --git a/week3/.gitignore b/week3/.gitignore new file mode 100644 index 0000000..bbaf2d6 --- /dev/null +++ b/week3/.gitignore @@ -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 diff --git a/week3/inspect-fds/Cargo.toml b/week3/inspect-fds/Cargo.toml new file mode 100644 index 0000000..a5e570c --- /dev/null +++ b/week3/inspect-fds/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "inspect-fds" +version = "0.1.0" +authors = ["Ryan Eberhardt "] +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" diff --git a/week3/inspect-fds/Makefile b/week3/inspect-fds/Makefile new file mode 100644 index 0000000..d53b36b --- /dev/null +++ b/week3/inspect-fds/Makefile @@ -0,0 +1,10 @@ +SRCS = $(wildcard *.c) +PROGS = $(patsubst %.c,%,$(SRCS)) + +all: $(PROGS) + +%: %.c + $(CC) $(CFLAGS) -o $@ $< + +clean: + rm -f $(PROGS) diff --git a/week3/inspect-fds/multi_pipe_test.c b/week3/inspect-fds/multi_pipe_test.c new file mode 100644 index 0000000..bf25cdb --- /dev/null +++ b/week3/inspect-fds/multi_pipe_test.c @@ -0,0 +1,24 @@ +#include +#include + +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; +} diff --git a/week3/inspect-fds/nothing.c b/week3/inspect-fds/nothing.c new file mode 100644 index 0000000..237c8ce --- /dev/null +++ b/week3/inspect-fds/nothing.c @@ -0,0 +1 @@ +int main() {} diff --git a/week3/inspect-fds/src/main.rs b/week3/inspect-fds/src/main.rs new file mode 100644 index 0000000..5d523a0 --- /dev/null +++ b/week3/inspect-fds/src/main.rs @@ -0,0 +1,61 @@ +use std::env; + +mod open_file; +mod process; +mod ps_utils; + +fn main() { + let args: Vec = env::args().collect(); + if args.len() != 2 { + println!("Usage: {} ", 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." + ); + } +} diff --git a/week3/inspect-fds/src/open_file.rs b/week3/inspect-fds/src/open_file.rs new file mode 100644 index 0000000..a8d095d --- /dev/null +++ b/week3/inspect-fds/src/open_file.rs @@ -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 "". + /// * For pipes (filenames formatted like pipe:[pipenum]), this will return "". + #[allow(unused)] // TODO: delete this line for Milestone 4 + fn path_to_name(path: &str) -> String { + if path.starts_with("/dev/pts/") { + String::from("") + } else if path.starts_with("pipe:[") && path.ends_with("]") { + let pipe_num = &path[path.find('[').unwrap() + 1..path.find(']').unwrap()]; + format!("", 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 { + // 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::() + .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 { + // 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 { + // 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(" 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, ""); + 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(); + } +} diff --git a/week3/inspect-fds/src/process.rs b/week3/inspect-fds/src/process.rs new file mode 100644 index 0000000..60a70a9 --- /dev/null +++ b/week3/inspect-fds/src/process.rs @@ -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> { + // 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> { + 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(); + } +} diff --git a/week3/inspect-fds/src/ps_utils.rs b/week3/inspect-fds/src/ps_utils.rs new file mode 100644 index 0000000..55bafc9 --- /dev/null +++ b/week3/inspect-fds/src/ps_utils.rs @@ -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 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 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 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 { + // 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::()?; + 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::()?; + 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, 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, 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, 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::()?), + 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, 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" + ); + } +} diff --git a/week3/inspect-fds/zombie_test.c b/week3/inspect-fds/zombie_test.c new file mode 100644 index 0000000..bdc342f --- /dev/null +++ b/week3/inspect-fds/zombie_test.c @@ -0,0 +1,15 @@ +#include +#include + +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; +} diff --git a/week3/linked_list/Cargo.toml b/week3/linked_list/Cargo.toml new file mode 100644 index 0000000..f8ebe12 --- /dev/null +++ b/week3/linked_list/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "linked_list" +version = "0.1.0" +authors = ["Armin Namavari "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/week3/linked_list/src/linked_list.rs b/week3/linked_list/src/linked_list.rs new file mode 100644 index 0000000..c3a36d6 --- /dev/null +++ b/week3/linked_list/src/linked_list.rs @@ -0,0 +1,75 @@ +use std::fmt; +use std::option::Option; + +pub struct LinkedList { + head: Option>, + size: usize, +} + +struct Node { + value: u32, + next: Option>, +} + +impl Node { + pub fn new(value: u32, next: Option>) -> 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 = Box::new(Node::new(value, self.head.take())); + self.head = Some(new_node); + self.size += 1; + } + + pub fn pop_front(&mut self) -> Option { + let node: Box = 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> = &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(); + } + } +} + + + diff --git a/week3/linked_list/src/main.rs b/week3/linked_list/src/main.rs new file mode 100644 index 0000000..04226e9 --- /dev/null +++ b/week3/linked_list/src/main.rs @@ -0,0 +1,22 @@ +use linked_list::LinkedList; +pub mod linked_list; + +fn main() { + let mut list: LinkedList = 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); + //} +} diff --git a/week3/survey.txt b/week3/survey.txt new file mode 100644 index 0000000..5f30df8 --- /dev/null +++ b/week3/survey.txt @@ -0,0 +1 @@ +Survey code: diff --git a/week5/.gitignore b/week5/.gitignore new file mode 100644 index 0000000..bbaf2d6 --- /dev/null +++ b/week5/.gitignore @@ -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 diff --git a/week5/farm/Cargo.toml b/week5/farm/Cargo.toml new file mode 100644 index 0000000..a09ef1d --- /dev/null +++ b/week5/farm/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "farm" +version = "0.1.0" +authors = ["Ryan Eberhardt "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +num_cpus = "1.13.0" diff --git a/week5/farm/src/main.rs b/week5/farm/src/main.rs new file mode 100644 index 0000000..39d38d8 --- /dev/null +++ b/week5/farm/src/main.rs @@ -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::>() + .join(" * "); + println!("{} = {} [time: {:?}]", num, factors_str, start.elapsed()); +} + +/// Returns a list of numbers supplied via argv. +#[allow(dead_code)] +fn get_input_numbers() -> VecDeque { + let mut numbers = VecDeque::new(); + for arg in env::args().skip(1) { + if let Ok(val) = arg.parse::() { + 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()); +} diff --git a/week5/survey.txt b/week5/survey.txt new file mode 100644 index 0000000..5f30df8 --- /dev/null +++ b/week5/survey.txt @@ -0,0 +1 @@ +Survey code: diff --git a/week6/.gitignore b/week6/.gitignore new file mode 100644 index 0000000..bbaf2d6 --- /dev/null +++ b/week6/.gitignore @@ -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 diff --git a/week6/parallel_map/Cargo.toml b/week6/parallel_map/Cargo.toml new file mode 100644 index 0000000..3d5d931 --- /dev/null +++ b/week6/parallel_map/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "parallel_map" +version = "0.1.0" +authors = ["Armin Namavari "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +crossbeam-channel = "0.4.2" \ No newline at end of file diff --git a/week6/parallel_map/src/main.rs b/week6/parallel_map/src/main.rs new file mode 100644 index 0000000..b98cb3d --- /dev/null +++ b/week6/parallel_map/src/main.rs @@ -0,0 +1,23 @@ +use crossbeam_channel; +use std::{thread, time}; + +fn parallel_map(mut input_vec: Vec, num_threads: usize, f: F) -> Vec +where + F: FnOnce(T) -> U + Send + Copy + 'static, + T: Send + 'static, + U: Send + 'static + Default, +{ + let mut output_vec: Vec = 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); +} diff --git a/week6/survey.txt b/week6/survey.txt new file mode 100644 index 0000000..5f30df8 --- /dev/null +++ b/week6/survey.txt @@ -0,0 +1 @@ +Survey code: