├── LICENSE.txt ├── README.md ├── proj-1 ├── .gitignore └── deet │ ├── Cargo.toml │ ├── Dockerfile │ ├── Makefile │ ├── container │ ├── samples │ ├── count.c │ ├── exit.c │ ├── function_calls.c │ ├── hello.c │ ├── segfault.c │ └── sleepy_print.c │ └── src │ ├── debugger.rs │ ├── debugger_command.rs │ ├── dwarf_data.rs │ ├── gimli_wrapper.rs │ ├── inferior.rs │ └── main.rs ├── proj-2 ├── .gitignore ├── Dockerfile └── balancebeam │ ├── Cargo.toml │ ├── src │ ├── main.rs │ ├── request.rs │ └── response.rs │ └── tests │ ├── 01_single_upstream_tests.rs │ ├── 02_multiple_upstream_tests.rs │ └── common │ ├── balancebeam.rs │ ├── echo_server.rs │ ├── error_server.rs │ ├── mod.rs │ └── server.rs ├── week1 ├── .gitignore ├── part-1-hello-world │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── part-2-warmup │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── part-3-hangman │ ├── Cargo.toml │ ├── src │ │ └── main.rs │ └── words.txt └── part-4.txt ├── week2 ├── .gitignore ├── ownership.txt ├── rdiff │ ├── Cargo.toml │ ├── handout-a.txt │ ├── handout-b.txt │ ├── simple-a.txt │ ├── simple-b.txt │ └── src │ │ ├── grid.rs │ │ └── main.rs ├── rwc │ ├── Cargo.toml │ └── src │ │ └── main.rs └── survey.txt ├── week3 ├── .gitignore ├── inspect-fds │ ├── Cargo.toml │ ├── Makefile │ ├── multi_pipe_test.c │ ├── nothing.c │ ├── src │ │ ├── main.rs │ │ ├── open_file.rs │ │ ├── process.rs │ │ └── ps_utils.rs │ └── zombie_test.c ├── linked_list │ ├── Cargo.toml │ └── src │ │ ├── linked_list.rs │ │ └── main.rs └── survey.txt ├── week5 ├── .gitignore ├── farm │ ├── Cargo.toml │ └── src │ │ └── main.rs └── survey.txt └── week6 ├── .gitignore ├── parallel_map ├── Cargo.toml └── src │ └── main.rs └── survey.txt /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CS 110L Spring 2020 starter code 2 | 3 | Assignment handouts are available [here](https://reberhardt.com/cs110l/spring-2020/). 4 | 5 | Trying out these assignments? Adapting these for a class? [Please let us 6 | know](mailto:ryan@reberhardt.com); we'd love to hear from you! 7 | 8 | Please don't post solution code publicly on the internet. We have plagiarism 9 | detection tools, but we'd rather make it hard to plagiarize in the first place. 10 | -------------------------------------------------------------------------------- /proj-1/.gitignore: -------------------------------------------------------------------------------- 1 | /deet/.cargo/ 2 | /deet/target/ 3 | /deet/Cargo.lock 4 | .*.swp 5 | .deet_history 6 | .bash_history 7 | /deet/samples/sleepy_print 8 | /deet/samples/segfault 9 | /deet/samples/hello 10 | /deet/samples/function_calls 11 | /deet/samples/exit 12 | /deet/samples/count 13 | .idea 14 | -------------------------------------------------------------------------------- /proj-1/deet/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deet" 3 | version = "0.1.0" 4 | authors = ["Ryan Eberhardt "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | nix = "0.17.0" 11 | libc = "0.2.68" 12 | rustyline = "6.1.2" 13 | gimli = { git = "https://github.com/gimli-rs/gimli", rev = "ad23cdb2", default-features = false, features = ["read"] } 14 | object = { version = "0.17", default-features = false, features = ["read"] } 15 | memmap = "0.7" 16 | addr2line = "0.11.0" 17 | -------------------------------------------------------------------------------- /proj-1/deet/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y build-essential make curl strace gdb 5 | 6 | # Install Rust. Don't use rustup, so we can install for all users (not just the 7 | # root user) 8 | RUN curl --proto '=https' --tlsv1.2 -sSf \ 9 | https://static.rust-lang.org/dist/rust-1.43.0-x86_64-unknown-linux-gnu.tar.gz \ 10 | -o rust.tar.gz && \ 11 | tar -xzf rust.tar.gz && \ 12 | rust-1.43.0-x86_64-unknown-linux-gnu/install.sh 13 | 14 | # Make .cargo writable by any user (so we can run the container as an 15 | # unprivileged user) 16 | RUN mkdir /.cargo && chmod 777 /.cargo 17 | 18 | WORKDIR /deet 19 | -------------------------------------------------------------------------------- /proj-1/deet/Makefile: -------------------------------------------------------------------------------- 1 | SRCS = $(wildcard samples/*.c) 2 | PROGS = $(patsubst %.c,%,$(SRCS)) 3 | 4 | all: $(PROGS) 5 | 6 | %: %.c 7 | $(CC) $(CFLAGS) -O0 -g -no-pie -fno-omit-frame-pointer -o $@ $< 8 | 9 | clean: 10 | rm -f $(PROGS) 11 | -------------------------------------------------------------------------------- /proj-1/deet/container: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Kill the existing deet container if one is running 4 | docker rm -f deet &>/dev/null 5 | 6 | # Start a container 7 | docker run \ 8 | `# Give the container a name (so that it's easier to attach to with "docker exec")` \ 9 | --name deet \ 10 | `# Mount the current directory inside of the container, so cargo can access it` \ 11 | -v "${PWD}":/deet \ 12 | `# Set the container user's home directory to our deet directory` \ 13 | -e HOME=/deet \ 14 | `# Run as the current user (instead of root)` \ 15 | -u $(id -u ${USER}):$(id -g ${USER}) \ 16 | `# Allow ptrace` \ 17 | --cap-add=SYS_PTRACE \ 18 | `# When the container exits, automatically clean up the files it leaves behind` \ 19 | --rm \ 20 | `# Get an interactive terminal` \ 21 | -it \ 22 | `# Run the deet image` \ 23 | deet "$@" 24 | -------------------------------------------------------------------------------- /proj-1/deet/samples/count.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | printf("1\n"); 5 | printf("2\n"); 6 | printf("3\n"); 7 | printf("4\n"); 8 | printf("5\n"); 9 | return 0; 10 | } 11 | -------------------------------------------------------------------------------- /proj-1/deet/samples/exit.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | asm("syscall" :: "a"(60), "D"(0)); 5 | } 6 | -------------------------------------------------------------------------------- /proj-1/deet/samples/function_calls.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int global = 5; 4 | 5 | void func3(int a) { 6 | printf("Hello from func3! %d\n", a); 7 | } 8 | 9 | void func2(int a, int b) { 10 | printf("func2(%d, %d) was called\n", a, b); 11 | int sum = a + b; 12 | printf("sum = %d\n", sum); 13 | func3(100); 14 | } 15 | 16 | void func1(int a) { 17 | printf("func1(%d) was called\n", a); 18 | func2(a, global); 19 | func3(100); 20 | printf("end of func1\n"); 21 | } 22 | 23 | int main() { 24 | func1(42); 25 | } 26 | -------------------------------------------------------------------------------- /proj-1/deet/samples/hello.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | printf("Hello world!\n"); 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /proj-1/deet/samples/segfault.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void func2(int a) { 4 | printf("About to segfault... a=%d\n", a); 5 | *(int*)0 = a; 6 | printf("Did segfault!\n"); 7 | } 8 | 9 | void func1(int a) { 10 | printf("Calling func2\n"); 11 | func2(a % 5); 12 | } 13 | 14 | int main() { 15 | func1(42); 16 | } 17 | -------------------------------------------------------------------------------- /proj-1/deet/samples/sleepy_print.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int main(int argc, char *argv[]) { 6 | unsigned long num_seconds; 7 | if (argc != 2 || (num_seconds = strtoul(argv[1], NULL, 10)) == 0) { 8 | fprintf(stderr, "Usage: %s \n", argv[0]); 9 | exit(1); 10 | } 11 | for (unsigned long i = 0; i < num_seconds; i++) { 12 | printf("%lu\n", i); 13 | sleep(1); 14 | } 15 | return 0; 16 | } 17 | -------------------------------------------------------------------------------- /proj-1/deet/src/debugger.rs: -------------------------------------------------------------------------------- 1 | use crate::debugger_command::DebuggerCommand; 2 | use crate::inferior::Inferior; 3 | use rustyline::error::ReadlineError; 4 | use rustyline::Editor; 5 | 6 | pub struct Debugger { 7 | target: String, 8 | history_path: String, 9 | readline: Editor<()>, 10 | inferior: Option, 11 | } 12 | 13 | impl Debugger { 14 | /// Initializes the debugger. 15 | pub fn new(target: &str) -> Debugger { 16 | // TODO (milestone 3): initialize the DwarfData 17 | 18 | let history_path = format!("{}/.deet_history", std::env::var("HOME").unwrap()); 19 | let mut readline = Editor::<()>::new(); 20 | // Attempt to load history from ~/.deet_history if it exists 21 | let _ = readline.load_history(&history_path); 22 | 23 | Debugger { 24 | target: target.to_string(), 25 | history_path, 26 | readline, 27 | inferior: None, 28 | } 29 | } 30 | 31 | pub fn run(&mut self) { 32 | loop { 33 | match self.get_next_command() { 34 | DebuggerCommand::Run(args) => { 35 | if let Some(inferior) = Inferior::new(&self.target, &args) { 36 | // Create the inferior 37 | self.inferior = Some(inferior); 38 | // TODO (milestone 1): make the inferior run 39 | // You may use self.inferior.as_mut().unwrap() to get a mutable reference 40 | // to the Inferior object 41 | } else { 42 | println!("Error starting subprocess"); 43 | } 44 | } 45 | DebuggerCommand::Quit => { 46 | return; 47 | } 48 | } 49 | } 50 | } 51 | 52 | /// This function prompts the user to enter a command, and continues re-prompting until the user 53 | /// enters a valid command. It uses DebuggerCommand::from_tokens to do the command parsing. 54 | /// 55 | /// You don't need to read, understand, or modify this function. 56 | fn get_next_command(&mut self) -> DebuggerCommand { 57 | loop { 58 | // Print prompt and get next line of user input 59 | match self.readline.readline("(deet) ") { 60 | Err(ReadlineError::Interrupted) => { 61 | // User pressed ctrl+c. We're going to ignore it 62 | println!("Type \"quit\" to exit"); 63 | } 64 | Err(ReadlineError::Eof) => { 65 | // User pressed ctrl+d, which is the equivalent of "quit" for our purposes 66 | return DebuggerCommand::Quit; 67 | } 68 | Err(err) => { 69 | panic!("Unexpected I/O error: {:?}", err); 70 | } 71 | Ok(line) => { 72 | if line.trim().len() == 0 { 73 | continue; 74 | } 75 | self.readline.add_history_entry(line.as_str()); 76 | if let Err(err) = self.readline.save_history(&self.history_path) { 77 | println!( 78 | "Warning: failed to save history file at {}: {}", 79 | self.history_path, err 80 | ); 81 | } 82 | let tokens: Vec<&str> = line.split_whitespace().collect(); 83 | if let Some(cmd) = DebuggerCommand::from_tokens(&tokens) { 84 | return cmd; 85 | } else { 86 | println!("Unrecognized command."); 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /proj-1/deet/src/debugger_command.rs: -------------------------------------------------------------------------------- 1 | pub enum DebuggerCommand { 2 | Quit, 3 | Run(Vec), 4 | } 5 | 6 | impl DebuggerCommand { 7 | pub fn from_tokens(tokens: &Vec<&str>) -> Option { 8 | match tokens[0] { 9 | "q" | "quit" => Some(DebuggerCommand::Quit), 10 | "r" | "run" => { 11 | let args = tokens[1..].to_vec(); 12 | Some(DebuggerCommand::Run( 13 | args.iter().map(|s| s.to_string()).collect(), 14 | )) 15 | } 16 | // Default case: 17 | _ => None, 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /proj-1/deet/src/dwarf_data.rs: -------------------------------------------------------------------------------- 1 | use crate::gimli_wrapper; 2 | use addr2line::Context; 3 | use object::Object; 4 | use std::convert::TryInto; 5 | use std::{fmt, fs}; 6 | 7 | #[derive(Debug)] 8 | pub enum Error { 9 | ErrorOpeningFile, 10 | DwarfFormatError(gimli_wrapper::Error), 11 | } 12 | 13 | pub struct DwarfData { 14 | files: Vec, 15 | addr2line: Context>, 16 | } 17 | 18 | impl fmt::Debug for DwarfData { 19 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 20 | write!(f, "DwarfData {{files: {:?}}}", self.files) 21 | } 22 | } 23 | 24 | impl From for Error { 25 | fn from(err: gimli_wrapper::Error) -> Self { 26 | Error::DwarfFormatError(err) 27 | } 28 | } 29 | 30 | impl DwarfData { 31 | pub fn from_file(path: &str) -> Result { 32 | let file = fs::File::open(path).or(Err(Error::ErrorOpeningFile))?; 33 | let mmap = unsafe { memmap::Mmap::map(&file).or(Err(Error::ErrorOpeningFile))? }; 34 | let object = object::File::parse(&*mmap) 35 | .or_else(|e| Err(gimli_wrapper::Error::ObjectError(e.to_string())))?; 36 | let endian = if object.is_little_endian() { 37 | gimli::RunTimeEndian::Little 38 | } else { 39 | gimli::RunTimeEndian::Big 40 | }; 41 | Ok(DwarfData { 42 | files: gimli_wrapper::load_file(&object, endian)?, 43 | addr2line: Context::new(&object).or_else(|e| Err(gimli_wrapper::Error::from(e)))?, 44 | }) 45 | } 46 | 47 | #[allow(dead_code)] 48 | fn get_target_file(&self, file: &str) -> Option<&File> { 49 | self.files.iter().find(|f| { 50 | f.name == file || (!file.contains("/") && f.name.ends_with(&format!("/{}", file))) 51 | }) 52 | } 53 | 54 | #[allow(dead_code)] 55 | pub fn get_addr_for_line(&self, file: Option<&str>, line_number: usize) -> Option { 56 | let target_file = match file { 57 | Some(filename) => self.get_target_file(filename)?, 58 | None => self.files.get(0)?, 59 | }; 60 | Some( 61 | target_file 62 | .lines 63 | .iter() 64 | .find(|line| line.number >= line_number)? 65 | .address, 66 | ) 67 | } 68 | 69 | #[allow(dead_code)] 70 | pub fn get_addr_for_function(&self, file: Option<&str>, func_name: &str) -> Option { 71 | match file { 72 | Some(filename) => Some( 73 | self.get_target_file(filename)? 74 | .functions 75 | .iter() 76 | .find(|func| func.name == func_name)? 77 | .address, 78 | ), 79 | None => { 80 | for file in &self.files { 81 | if let Some(func) = file.functions.iter().find(|func| func.name == func_name) { 82 | return Some(func.address); 83 | } 84 | } 85 | None 86 | } 87 | } 88 | } 89 | 90 | #[allow(dead_code)] 91 | pub fn get_line_from_addr(&self, curr_addr: usize) -> Option { 92 | let location = self 93 | .addr2line 94 | .find_location(curr_addr.try_into().unwrap()) 95 | .ok()??; 96 | Some(Line { 97 | file: location.file?.to_string(), 98 | number: location.line?.try_into().unwrap(), 99 | address: curr_addr, 100 | }) 101 | } 102 | 103 | #[allow(dead_code)] 104 | pub fn get_function_from_addr(&self, curr_addr: usize) -> Option { 105 | let frame = self 106 | .addr2line 107 | .find_frames(curr_addr.try_into().unwrap()) 108 | .ok()? 109 | .next() 110 | .ok()??; 111 | Some(frame.function?.raw_name().ok()?.to_string()) 112 | } 113 | 114 | #[allow(dead_code)] 115 | pub fn print(&self) { 116 | for file in &self.files { 117 | println!("------"); 118 | println!("{}", file.name); 119 | println!("------"); 120 | 121 | println!("Global variables:"); 122 | for var in &file.global_variables { 123 | println!( 124 | " * {} ({}, located at {}, declared at line {})", 125 | var.name, var.entity_type.name, var.location, var.line_number 126 | ); 127 | } 128 | 129 | println!("Functions:"); 130 | for func in &file.functions { 131 | println!( 132 | " * {} (declared on line {}, located at {:#x}, {} bytes long)", 133 | func.name, func.line_number, func.address, func.text_length 134 | ); 135 | for var in &func.variables { 136 | println!( 137 | " * Variable: {} ({}, located at {}, declared at line {})", 138 | var.name, var.entity_type.name, var.location, var.line_number 139 | ); 140 | } 141 | } 142 | 143 | println!("Line numbers:"); 144 | for line in &file.lines { 145 | println!(" * {} (at {:#x})", line.number, line.address); 146 | } 147 | } 148 | } 149 | } 150 | 151 | #[derive(Debug, Clone, Default)] 152 | pub struct Type { 153 | pub name: String, 154 | pub size: usize, 155 | } 156 | 157 | impl Type { 158 | pub fn new(name: String, size: usize) -> Self { 159 | Type { 160 | name: name, 161 | size: size, 162 | } 163 | } 164 | } 165 | 166 | #[derive(Clone)] 167 | pub enum Location { 168 | Address(usize), 169 | FramePointerOffset(isize), 170 | } 171 | 172 | impl fmt::Display for Location { 173 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 174 | match *self { 175 | Location::Address(addr) => write!(f, "Address({:#x})", addr), 176 | Location::FramePointerOffset(offset) => write!(f, "FramePointerOffset({})", offset), 177 | } 178 | } 179 | } 180 | 181 | impl fmt::Debug for Location { 182 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 183 | fmt::Display::fmt(self, f) 184 | } 185 | } 186 | 187 | // For variables and formal parameters 188 | #[derive(Debug, Clone)] 189 | pub struct Variable { 190 | pub name: String, 191 | pub entity_type: Type, 192 | pub location: Location, 193 | pub line_number: usize, // Line number in source file 194 | } 195 | 196 | #[derive(Debug, Default, Clone)] 197 | pub struct Function { 198 | pub name: String, 199 | pub address: usize, 200 | pub text_length: usize, 201 | pub line_number: usize, // Line number in source file 202 | pub variables: Vec, 203 | } 204 | 205 | #[derive(Debug, Default, Clone)] 206 | pub struct File { 207 | pub name: String, 208 | pub global_variables: Vec, 209 | pub functions: Vec, 210 | pub lines: Vec, 211 | } 212 | 213 | #[derive(Debug, Clone, PartialEq)] 214 | pub struct Line { 215 | pub file: String, 216 | pub number: usize, 217 | pub address: usize, 218 | } 219 | 220 | impl fmt::Display for Line { 221 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 222 | write!(f, "{}:{}", self.file, self.number) 223 | } 224 | } 225 | 226 | 227 | -------------------------------------------------------------------------------- /proj-1/deet/src/gimli_wrapper.rs: -------------------------------------------------------------------------------- 1 | //! This file contains code for using gimli to extract information from the DWARF section of an 2 | //! executable. The code is adapted from 3 | //! https://github.com/gimli-rs/gimli/blob/master/examples/simple.rs and 4 | //! https://github.com/gimli-rs/gimli/blob/master/examples/dwarfdump.rs. 5 | //! 6 | //! This code is a huge mess. Please don't read it unless you're trying to do an extension :) 7 | 8 | use gimli; 9 | use gimli::{UnitOffset, UnitSectionOffset}; 10 | use object::Object; 11 | use std::borrow; 12 | //use std::io::{BufWriter, Write}; 13 | use crate::dwarf_data::{File, Function, Line, Location, Type, Variable}; 14 | use std::collections::HashMap; 15 | use std::convert::TryInto; 16 | use std::fmt::Write; 17 | use std::{io, path}; 18 | 19 | pub fn load_file(object: &object::File, endian: gimli::RunTimeEndian) -> Result, Error> { 20 | // Load a section and return as `Cow<[u8]>`. 21 | let load_section = |id: gimli::SectionId| -> Result, gimli::Error> { 22 | Ok(object 23 | .section_data_by_name(id.name()) 24 | .unwrap_or(borrow::Cow::Borrowed(&[][..]))) 25 | }; 26 | // Load a supplementary section. We don't have a supplementary object file, 27 | // so always return an empty slice. 28 | let load_section_sup = |_| Ok(borrow::Cow::Borrowed(&[][..])); 29 | 30 | // Load all of the sections. 31 | let dwarf_cow = gimli::Dwarf::load(&load_section, &load_section_sup)?; 32 | 33 | // Borrow a `Cow<[u8]>` to create an `EndianSlice`. 34 | let borrow_section: &dyn for<'a> Fn( 35 | &'a borrow::Cow<[u8]>, 36 | ) -> gimli::EndianSlice<'a, gimli::RunTimeEndian> = 37 | &|section| gimli::EndianSlice::new(&*section, endian); 38 | 39 | // Create `EndianSlice`s for all of the sections. 40 | let dwarf = dwarf_cow.borrow(&borrow_section); 41 | 42 | // Define a mapping from type offsets to type structs 43 | let mut offset_to_type: HashMap = HashMap::new(); 44 | 45 | let mut compilation_units: Vec = Vec::new(); 46 | 47 | // Iterate over the compilation units. 48 | let mut iter = dwarf.units(); 49 | while let Some(header) = iter.next()? { 50 | let unit = dwarf.unit(header)?; 51 | 52 | // Iterate over the Debugging Information Entries (DIEs) in the unit. 53 | let mut depth = 0; 54 | let mut entries = unit.entries(); 55 | while let Some((delta_depth, entry)) = entries.next_dfs()? { 56 | depth += delta_depth; 57 | // Update the offset_to_type mapping for types 58 | // Update the variable list for formal params/variables 59 | match entry.tag() { 60 | gimli::DW_TAG_compile_unit => { 61 | let name = if let Ok(Some(attr)) = entry.attr(gimli::DW_AT_name) { 62 | if let Ok(DebugValue::Str(name)) = get_attr_value(&attr, &unit, &dwarf) { 63 | name 64 | } else { 65 | "".to_string() 66 | } 67 | } else { 68 | "".to_string() 69 | }; 70 | compilation_units.push(File { 71 | name, 72 | global_variables: Vec::new(), 73 | functions: Vec::new(), 74 | lines: Vec::new(), 75 | }); 76 | } 77 | gimli::DW_TAG_base_type => { 78 | let name = if let Ok(Some(attr)) = entry.attr(gimli::DW_AT_name) { 79 | if let Ok(DebugValue::Str(name)) = get_attr_value(&attr, &unit, &dwarf) { 80 | name 81 | } else { 82 | "".to_string() 83 | } 84 | } else { 85 | "".to_string() 86 | }; 87 | let byte_size = if let Ok(Some(attr)) = entry.attr(gimli::DW_AT_byte_size) { 88 | if let Ok(DebugValue::Uint(byte_size)) = 89 | get_attr_value(&attr, &unit, &dwarf) 90 | { 91 | byte_size 92 | } else { 93 | // TODO: report error? 94 | 0 95 | } 96 | } else { 97 | // TODO: report error? 98 | 0 99 | }; 100 | let type_offset = entry.offset().0; 101 | offset_to_type 102 | .insert(type_offset, Type::new(name, byte_size.try_into().unwrap())); 103 | } 104 | gimli::DW_TAG_subprogram => { 105 | let mut func: Function = Default::default(); 106 | let mut attrs = entry.attrs(); 107 | while let Some(attr) = attrs.next()? { 108 | let val = get_attr_value(&attr, &unit, &dwarf); 109 | //println!(" {}: {:?}", attr.name(), val); 110 | match attr.name() { 111 | gimli::DW_AT_name => { 112 | if let Ok(DebugValue::Str(name)) = val { 113 | func.name = name; 114 | } 115 | } 116 | gimli::DW_AT_high_pc => { 117 | if let Ok(DebugValue::Uint(high_pc)) = val { 118 | func.text_length = high_pc.try_into().unwrap(); 119 | } 120 | } 121 | gimli::DW_AT_low_pc => { 122 | //println!("low pc {:?}", attr.value()); 123 | if let Ok(DebugValue::Uint(low_pc)) = val { 124 | func.address = low_pc.try_into().unwrap(); 125 | } 126 | } 127 | gimli::DW_AT_decl_line => { 128 | if let Ok(DebugValue::Uint(line_number)) = val { 129 | func.line_number = line_number.try_into().unwrap(); 130 | } 131 | } 132 | _ => {} 133 | } 134 | } 135 | compilation_units.last_mut().unwrap().functions.push(func); 136 | } 137 | gimli::DW_TAG_formal_parameter | gimli::DW_TAG_variable => { 138 | let mut name = String::new(); 139 | let mut entity_type: Option = None; 140 | let mut location: Option = None; 141 | let mut line_number = 0; 142 | let mut attrs = entry.attrs(); 143 | while let Some(attr) = attrs.next()? { 144 | let val = get_attr_value(&attr, &unit, &dwarf); 145 | //println!(" {}: {:?}", attr.name(), val); 146 | match attr.name() { 147 | gimli::DW_AT_name => { 148 | if let Ok(DebugValue::Str(attr_name)) = val { 149 | name = attr_name; 150 | } 151 | } 152 | gimli::DW_AT_type => { 153 | if let Ok(DebugValue::Size(offset)) = val { 154 | if let Some(dtype) = offset_to_type.get(&offset).clone() { 155 | entity_type = Some(dtype.clone()); 156 | } 157 | } 158 | } 159 | gimli::DW_AT_location => { 160 | if let Some(loc) = get_location(&attr, &unit) { 161 | location = Some(loc); 162 | } 163 | } 164 | gimli::DW_AT_decl_line => { 165 | if let Ok(DebugValue::Uint(num)) = val { 166 | line_number = num; 167 | } 168 | } 169 | _ => {} 170 | } 171 | } 172 | if entity_type.is_some() && location.is_some() { 173 | let var = Variable { 174 | name, 175 | entity_type: entity_type.unwrap(), 176 | location: location.unwrap(), 177 | line_number: line_number.try_into().unwrap(), 178 | }; 179 | if depth == 1 { 180 | compilation_units 181 | .last_mut() 182 | .unwrap() 183 | .global_variables 184 | .push(var); 185 | } else if depth > 1 { 186 | compilation_units 187 | .last_mut() 188 | .unwrap() 189 | .functions 190 | .last_mut() 191 | .unwrap() 192 | .variables 193 | .push(var); 194 | } 195 | } 196 | } 197 | // NOTE: :You may consider supporting other types by extending this 198 | // match statement 199 | _ => {} 200 | } 201 | } 202 | 203 | // Get line numbers 204 | if let Some(program) = unit.line_program.clone() { 205 | // Iterate over the line program rows. 206 | let mut rows = program.rows(); 207 | while let Some((header, row)) = rows.next_row()? { 208 | if !row.end_sequence() { 209 | // Determine the path. Real applications should cache this for performance. 210 | let mut path = path::PathBuf::new(); 211 | if let Some(file) = row.file(header) { 212 | if let Some(dir) = file.directory(header) { 213 | path.push(dwarf.attr_string(&unit, dir)?.to_string_lossy().as_ref()); 214 | } 215 | path.push( 216 | dwarf 217 | .attr_string(&unit, file.path_name())? 218 | .to_string_lossy() 219 | .as_ref(), 220 | ); 221 | } 222 | 223 | // Get the File 224 | let file = compilation_units 225 | .iter_mut() 226 | .find(|f| f.name == path.as_os_str().to_str().unwrap()); 227 | 228 | // Determine line/column. DWARF line/column is never 0, so we use that 229 | // but other applications may want to display this differently. 230 | let line = row.line().unwrap_or(0); 231 | 232 | if let Some(file) = file { 233 | file.lines.push(Line { 234 | file: file.name.clone(), 235 | number: line.try_into().unwrap(), 236 | address: row.address().try_into().unwrap(), 237 | }); 238 | } 239 | } 240 | } 241 | } 242 | } 243 | Ok(compilation_units) 244 | } 245 | 246 | #[derive(Debug, Clone)] 247 | pub enum DebugValue { 248 | Str(String), 249 | Uint(u64), 250 | Int(i64), 251 | Size(usize), 252 | NoVal, 253 | } 254 | 255 | #[derive(Debug, Clone, PartialEq, Eq)] 256 | pub enum Error { 257 | GimliError(gimli::Error), 258 | Addr2lineError(addr2line::gimli::Error), 259 | ObjectError(String), 260 | IoError, 261 | } 262 | 263 | impl From for Error { 264 | fn from(err: gimli::Error) -> Self { 265 | Error::GimliError(err) 266 | } 267 | } 268 | 269 | impl From for Error { 270 | fn from(err: addr2line::gimli::Error) -> Self { 271 | Error::Addr2lineError(err) 272 | } 273 | } 274 | 275 | impl From for Error { 276 | fn from(_: io::Error) -> Self { 277 | Error::IoError 278 | } 279 | } 280 | 281 | impl From for Error { 282 | fn from(_: std::fmt::Error) -> Self { 283 | Error::IoError 284 | } 285 | } 286 | 287 | impl<'input, Endian> Reader for gimli::EndianSlice<'input, Endian> where 288 | Endian: gimli::Endianity + Send + Sync 289 | { 290 | } 291 | 292 | trait Reader: gimli::Reader + Send + Sync {} 293 | 294 | fn get_location(attr: &gimli::Attribute, unit: &gimli::Unit) -> Option { 295 | if let gimli::AttributeValue::Exprloc(ref data) = attr.value() { 296 | let encoding = unit.encoding(); 297 | let mut pc = data.0.clone(); 298 | if pc.len() > 0 { 299 | if let Ok(op) = gimli::Operation::parse(&mut pc, encoding) { 300 | match op { 301 | gimli::Operation::FrameOffset { offset } => { 302 | return Some(Location::FramePointerOffset(offset.try_into().unwrap())); 303 | } 304 | gimli::Operation::Address { address } => { 305 | return Some(Location::Address(address.try_into().unwrap())); 306 | } 307 | _ => {} 308 | } 309 | } 310 | } 311 | } 312 | None 313 | } 314 | 315 | // based on dwarf_dump.rs 316 | fn get_attr_value( 317 | attr: &gimli::Attribute, 318 | unit: &gimli::Unit, 319 | dwarf: &gimli::Dwarf, 320 | ) -> Result { 321 | let value = attr.value(); 322 | // TODO: get rid of w eventually 323 | let mut buf = String::new(); 324 | let w = &mut buf; 325 | match value { 326 | gimli::AttributeValue::Exprloc(ref data) => { 327 | dump_exprloc(w, unit.encoding(), data)?; 328 | Ok(DebugValue::Str(w.to_string())) 329 | } 330 | gimli::AttributeValue::UnitRef(offset) => { 331 | match offset.to_unit_section_offset(unit) { 332 | UnitSectionOffset::DebugInfoOffset(goff) => { 333 | Ok(DebugValue::Size(goff.0)) 334 | } 335 | UnitSectionOffset::DebugTypesOffset(goff) => { 336 | Ok(DebugValue::Size(goff.0)) 337 | } 338 | } 339 | } 340 | gimli::AttributeValue::DebugStrRef(offset) => { 341 | if let Ok(s) = dwarf.debug_str.get_str(offset) { 342 | Ok(DebugValue::Str(format!("{}", s.to_string_lossy()?))) 343 | } else { 344 | Ok(DebugValue::Str(format!("<.debug_str+0x{:08x}>", offset.0))) 345 | } 346 | } 347 | gimli::AttributeValue::Sdata(data) => Ok(DebugValue::Int(data)), 348 | gimli::AttributeValue::Addr(data) => Ok(DebugValue::Uint(data)), 349 | gimli::AttributeValue::Udata(data) => Ok(DebugValue::Uint(data)), 350 | 351 | gimli::AttributeValue::String(s) => { 352 | Ok(DebugValue::Str(format!("{}", s.to_string_lossy()?))) 353 | } 354 | gimli::AttributeValue::FileIndex(value) => { 355 | write!(w, "0x{:08x}", value)?; 356 | dump_file_index(w, value, unit, dwarf)?; 357 | Ok(DebugValue::Str(w.to_string())) 358 | } 359 | _ => { 360 | Ok(DebugValue::NoVal) 361 | } 362 | } 363 | } 364 | 365 | fn dump_file_index( 366 | w: &mut W, 367 | file: u64, 368 | unit: &gimli::Unit, 369 | dwarf: &gimli::Dwarf, 370 | ) -> Result<(), Error> { 371 | if file == 0 { 372 | return Ok(()); 373 | } 374 | let header = match unit.line_program { 375 | Some(ref program) => program.header(), 376 | None => return Ok(()), 377 | }; 378 | let file = match header.file(file) { 379 | Some(header) => header, 380 | None => { 381 | writeln!(w, "Unable to get header for file {}", file)?; 382 | return Ok(()); 383 | } 384 | }; 385 | write!(w, " ")?; 386 | if let Some(directory) = file.directory(header) { 387 | let directory = dwarf.attr_string(unit, directory)?; 388 | let directory = directory.to_string_lossy()?; 389 | if !directory.starts_with('/') { 390 | if let Some(ref comp_dir) = unit.comp_dir { 391 | write!(w, "{}/", comp_dir.to_string_lossy()?,)?; 392 | } 393 | } 394 | write!(w, "{}/", directory)?; 395 | } 396 | write!( 397 | w, 398 | "{}", 399 | dwarf 400 | .attr_string(unit, file.path_name())? 401 | .to_string_lossy()? 402 | )?; 403 | Ok(()) 404 | } 405 | 406 | fn dump_exprloc( 407 | w: &mut W, 408 | encoding: gimli::Encoding, 409 | data: &gimli::Expression, 410 | ) -> Result<(), Error> { 411 | let mut pc = data.0.clone(); 412 | let mut space = false; 413 | while pc.len() != 0 { 414 | let mut op_pc = pc.clone(); 415 | let dwop = gimli::DwOp(op_pc.read_u8()?); 416 | match gimli::Operation::parse(&mut pc, encoding) { 417 | Ok(op) => { 418 | if space { 419 | write!(w, " ")?; 420 | } else { 421 | space = true; 422 | } 423 | dump_op(w, encoding, dwop, op)?; 424 | } 425 | Err(gimli::Error::InvalidExpression(op)) => { 426 | writeln!(w, "WARNING: unsupported operation 0x{:02x}", op.0)?; 427 | return Ok(()); 428 | } 429 | Err(gimli::Error::UnsupportedRegister(register)) => { 430 | writeln!(w, "WARNING: unsupported register {}", register)?; 431 | return Ok(()); 432 | } 433 | Err(gimli::Error::UnexpectedEof(_)) => { 434 | writeln!(w, "WARNING: truncated or malformed expression")?; 435 | return Ok(()); 436 | } 437 | Err(e) => { 438 | writeln!(w, "WARNING: unexpected operation parse error: {}", e)?; 439 | return Ok(()); 440 | } 441 | } 442 | } 443 | Ok(()) 444 | } 445 | 446 | fn dump_op( 447 | w: &mut W, 448 | encoding: gimli::Encoding, 449 | dwop: gimli::DwOp, 450 | op: gimli::Operation, 451 | ) -> Result<(), Error> { 452 | write!(w, "{}", dwop)?; 453 | match op { 454 | gimli::Operation::Deref { 455 | base_type, size, .. 456 | } => { 457 | if dwop == gimli::DW_OP_deref_size || dwop == gimli::DW_OP_xderef_size { 458 | write!(w, " {}", size)?; 459 | } 460 | if base_type != UnitOffset(0) { 461 | write!(w, " type 0x{:08x}", base_type.0)?; 462 | } 463 | } 464 | gimli::Operation::Pick { index } => { 465 | if dwop == gimli::DW_OP_pick { 466 | write!(w, " {}", index)?; 467 | } 468 | } 469 | gimli::Operation::PlusConstant { value } => { 470 | write!(w, " {}", value as i64)?; 471 | } 472 | gimli::Operation::Bra { target } => { 473 | write!(w, " {}", target)?; 474 | } 475 | gimli::Operation::Skip { target } => { 476 | write!(w, " {}", target)?; 477 | } 478 | gimli::Operation::SignedConstant { value } => match dwop { 479 | gimli::DW_OP_const1s 480 | | gimli::DW_OP_const2s 481 | | gimli::DW_OP_const4s 482 | | gimli::DW_OP_const8s 483 | | gimli::DW_OP_consts => { 484 | write!(w, " {}", value)?; 485 | } 486 | _ => {} 487 | }, 488 | gimli::Operation::UnsignedConstant { value } => match dwop { 489 | gimli::DW_OP_const1u 490 | | gimli::DW_OP_const2u 491 | | gimli::DW_OP_const4u 492 | | gimli::DW_OP_const8u 493 | | gimli::DW_OP_constu => { 494 | write!(w, " {}", value)?; 495 | } 496 | _ => { 497 | // These have the value encoded in the operation, eg DW_OP_lit0. 498 | } 499 | }, 500 | gimli::Operation::Register { register } => { 501 | if dwop == gimli::DW_OP_regx { 502 | write!(w, " {}", register.0)?; 503 | } 504 | } 505 | gimli::Operation::RegisterOffset { 506 | register, 507 | offset, 508 | base_type, 509 | } => { 510 | if dwop >= gimli::DW_OP_breg0 && dwop <= gimli::DW_OP_breg31 { 511 | write!(w, "{:+}", offset)?; 512 | } else { 513 | write!(w, " {}", register.0)?; 514 | if offset != 0 { 515 | write!(w, "{:+}", offset)?; 516 | } 517 | if base_type != UnitOffset(0) { 518 | write!(w, " type 0x{:08x}", base_type.0)?; 519 | } 520 | } 521 | } 522 | gimli::Operation::FrameOffset { offset } => { 523 | write!(w, " {}", offset)?; 524 | } 525 | gimli::Operation::Call { offset } => match offset { 526 | gimli::DieReference::UnitRef(gimli::UnitOffset(offset)) => { 527 | write!(w, " 0x{:08x}", offset)?; 528 | } 529 | gimli::DieReference::DebugInfoRef(gimli::DebugInfoOffset(offset)) => { 530 | write!(w, " 0x{:08x}", offset)?; 531 | } 532 | }, 533 | gimli::Operation::Piece { 534 | size_in_bits, 535 | bit_offset: None, 536 | } => { 537 | write!(w, " {}", size_in_bits / 8)?; 538 | } 539 | gimli::Operation::Piece { 540 | size_in_bits, 541 | bit_offset: Some(bit_offset), 542 | } => { 543 | write!(w, " 0x{:08x} offset 0x{:08x}", size_in_bits, bit_offset)?; 544 | } 545 | gimli::Operation::ImplicitValue { data } => { 546 | let data = data.to_slice()?; 547 | write!(w, " 0x{:08x} contents 0x", data.len())?; 548 | for byte in data.iter() { 549 | write!(w, "{:02x}", byte)?; 550 | } 551 | } 552 | gimli::Operation::ImplicitPointer { value, byte_offset } => { 553 | write!(w, " 0x{:08x} {}", value.0, byte_offset)?; 554 | } 555 | gimli::Operation::EntryValue { expression } => { 556 | write!(w, "(")?; 557 | dump_exprloc(w, encoding, &gimli::Expression(expression))?; 558 | write!(w, ")")?; 559 | } 560 | gimli::Operation::ParameterRef { offset } => { 561 | write!(w, " 0x{:08x}", offset.0)?; 562 | } 563 | gimli::Operation::Address { address } => { 564 | write!(w, " 0x{:08x}", address)?; 565 | } 566 | gimli::Operation::AddressIndex { index } => { 567 | write!(w, " 0x{:08x}", index.0)?; 568 | } 569 | gimli::Operation::ConstantIndex { index } => { 570 | write!(w, " 0x{:08x}", index.0)?; 571 | } 572 | gimli::Operation::TypedLiteral { base_type, value } => { 573 | write!(w, " type 0x{:08x} contents 0x", base_type.0)?; 574 | for byte in value.to_slice()?.iter() { 575 | write!(w, "{:02x}", byte)?; 576 | } 577 | } 578 | gimli::Operation::Convert { base_type } => { 579 | write!(w, " type 0x{:08x}", base_type.0)?; 580 | } 581 | gimli::Operation::Reinterpret { base_type } => { 582 | write!(w, " type 0x{:08x}", base_type.0)?; 583 | } 584 | gimli::Operation::Drop 585 | | gimli::Operation::Swap 586 | | gimli::Operation::Rot 587 | | gimli::Operation::Abs 588 | | gimli::Operation::And 589 | | gimli::Operation::Div 590 | | gimli::Operation::Minus 591 | | gimli::Operation::Mod 592 | | gimli::Operation::Mul 593 | | gimli::Operation::Neg 594 | | gimli::Operation::Not 595 | | gimli::Operation::Or 596 | | gimli::Operation::Plus 597 | | gimli::Operation::Shl 598 | | gimli::Operation::Shr 599 | | gimli::Operation::Shra 600 | | gimli::Operation::Xor 601 | | gimli::Operation::Eq 602 | | gimli::Operation::Ge 603 | | gimli::Operation::Gt 604 | | gimli::Operation::Le 605 | | gimli::Operation::Lt 606 | | gimli::Operation::Ne 607 | | gimli::Operation::Nop 608 | | gimli::Operation::PushObjectAddress 609 | | gimli::Operation::TLS 610 | | gimli::Operation::CallFrameCFA 611 | | gimli::Operation::StackValue => {} 612 | }; 613 | Ok(()) 614 | } 615 | -------------------------------------------------------------------------------- /proj-1/deet/src/inferior.rs: -------------------------------------------------------------------------------- 1 | use nix::sys::ptrace; 2 | use nix::sys::signal; 3 | use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus}; 4 | use nix::unistd::Pid; 5 | use std::process::Child; 6 | 7 | pub enum Status { 8 | /// Indicates inferior stopped. Contains the signal that stopped the process, as well as the 9 | /// current instruction pointer that it is stopped at. 10 | Stopped(signal::Signal, usize), 11 | 12 | /// Indicates inferior exited normally. Contains the exit status code. 13 | Exited(i32), 14 | 15 | /// Indicates the inferior exited due to a signal. Contains the signal that killed the 16 | /// process. 17 | Signaled(signal::Signal), 18 | } 19 | 20 | /// This function calls ptrace with PTRACE_TRACEME to enable debugging on a process. You should use 21 | /// pre_exec with Command to call this in the child process. 22 | fn child_traceme() -> Result<(), std::io::Error> { 23 | ptrace::traceme().or(Err(std::io::Error::new( 24 | std::io::ErrorKind::Other, 25 | "ptrace TRACEME failed", 26 | ))) 27 | } 28 | 29 | pub struct Inferior { 30 | child: Child, 31 | } 32 | 33 | impl Inferior { 34 | /// Attempts to start a new inferior process. Returns Some(Inferior) if successful, or None if 35 | /// an error is encountered. 36 | pub fn new(target: &str, args: &Vec) -> Option { 37 | // TODO: implement me! 38 | println!( 39 | "Inferior::new not implemented! target={}, args={:?}", 40 | target, args 41 | ); 42 | None 43 | } 44 | 45 | /// Returns the pid of this inferior. 46 | pub fn pid(&self) -> Pid { 47 | nix::unistd::Pid::from_raw(self.child.id() as i32) 48 | } 49 | 50 | /// Calls waitpid on this inferior and returns a Status to indicate the state of the process 51 | /// after the waitpid call. 52 | pub fn wait(&self, options: Option) -> Result { 53 | Ok(match waitpid(self.pid(), options)? { 54 | WaitStatus::Exited(_pid, exit_code) => Status::Exited(exit_code), 55 | WaitStatus::Signaled(_pid, signal, _core_dumped) => Status::Signaled(signal), 56 | WaitStatus::Stopped(_pid, signal) => { 57 | let regs = ptrace::getregs(self.pid())?; 58 | Status::Stopped(signal, regs.rip as usize) 59 | } 60 | other => panic!("waitpid returned unexpected status: {:?}", other), 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /proj-1/deet/src/main.rs: -------------------------------------------------------------------------------- 1 | mod debugger; 2 | mod debugger_command; 3 | mod inferior; 4 | 5 | use crate::debugger::Debugger; 6 | use nix::sys::signal::{signal, SigHandler, Signal}; 7 | use std::env; 8 | 9 | fn main() { 10 | let args: Vec = env::args().collect(); 11 | if args.len() != 2 { 12 | println!("Usage: {} ", args[0]); 13 | std::process::exit(1); 14 | } 15 | let target = &args[1]; 16 | 17 | // Disable handling of ctrl+c in this process (so that ctrl+c only gets delivered to child 18 | // processes) 19 | unsafe { signal(Signal::SIGINT, SigHandler::SigIgn) }.expect("Error disabling SIGINT handling"); 20 | 21 | Debugger::new(target).run(); 22 | } 23 | -------------------------------------------------------------------------------- /proj-2/.gitignore: -------------------------------------------------------------------------------- 1 | /balancebeam/.cargo/ 2 | /balancebeam/target/ 3 | /balancebeam/Cargo.lock 4 | .idea 5 | .*.swp 6 | -------------------------------------------------------------------------------- /proj-2/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y build-essential python3 curl 5 | 6 | RUN useradd -ms /bin/bash balancebeam 7 | USER balancebeam 8 | WORKDIR /home/balancebeam 9 | 10 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y 11 | 12 | COPY balancebeam/Cargo.toml . 13 | RUN mkdir src && touch src/main.rs && ./.cargo/bin/cargo build --release || true 14 | 15 | COPY balancebeam/ ./ 16 | RUN ./.cargo/bin/cargo build --release 17 | 18 | ENTRYPOINT ["./.cargo/bin/cargo", "run", "--release", "--"] 19 | -------------------------------------------------------------------------------- /proj-2/balancebeam/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "balancebeam" 3 | version = "0.1.0" 4 | authors = ["Ryan Eberhardt "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | clap = "3.0.0-beta.1" 11 | httparse = "1.3" 12 | http = "0.2" 13 | log = "0.4" 14 | env_logger = "0.7" 15 | pretty_env_logger = "0.4" 16 | threadpool = "1.8" 17 | tokio = { version = "0.2", features = ["full"] } 18 | rand = "0.7" 19 | parking_lot = "0.10" 20 | 21 | [dev-dependencies] 22 | nix = "0.17" 23 | hyper = "0.13" 24 | reqwest = "0.10" 25 | async-trait = "0.1" 26 | -------------------------------------------------------------------------------- /proj-2/balancebeam/src/main.rs: -------------------------------------------------------------------------------- 1 | mod request; 2 | mod response; 3 | 4 | use clap::Clap; 5 | use rand::{Rng, SeedableRng}; 6 | use std::net::{TcpListener, TcpStream}; 7 | 8 | /// Contains information parsed from the command-line invocation of balancebeam. The Clap macros 9 | /// provide a fancy way to automatically construct a command-line argument parser. 10 | #[derive(Clap, Debug)] 11 | #[clap(about = "Fun with load balancing")] 12 | struct CmdOptions { 13 | #[clap( 14 | short, 15 | long, 16 | about = "IP/port to bind to", 17 | default_value = "0.0.0.0:1100" 18 | )] 19 | bind: String, 20 | #[clap(short, long, about = "Upstream host to forward requests to")] 21 | upstream: Vec, 22 | #[clap( 23 | long, 24 | about = "Perform active health checks on this interval (in seconds)", 25 | default_value = "10" 26 | )] 27 | active_health_check_interval: usize, 28 | #[clap( 29 | long, 30 | about = "Path to send request to for active health checks", 31 | default_value = "/" 32 | )] 33 | active_health_check_path: String, 34 | #[clap( 35 | long, 36 | about = "Maximum number of requests to accept per IP per minute (0 = unlimited)", 37 | default_value = "0" 38 | )] 39 | max_requests_per_minute: usize, 40 | } 41 | 42 | /// Contains information about the state of balancebeam (e.g. what servers we are currently proxying 43 | /// to, what servers have failed, rate limiting counts, etc.) 44 | /// 45 | /// You should add fields to this struct in later milestones. 46 | struct ProxyState { 47 | /// How frequently we check whether upstream servers are alive (Milestone 4) 48 | #[allow(dead_code)] 49 | active_health_check_interval: usize, 50 | /// Where we should send requests when doing active health checks (Milestone 4) 51 | #[allow(dead_code)] 52 | active_health_check_path: String, 53 | /// Maximum number of requests an individual IP can make in a minute (Milestone 5) 54 | #[allow(dead_code)] 55 | max_requests_per_minute: usize, 56 | /// Addresses of servers that we are proxying to 57 | upstream_addresses: Vec, 58 | } 59 | 60 | fn main() { 61 | // Initialize the logging library. You can print log messages using the `log` macros: 62 | // https://docs.rs/log/0.4.8/log/ You are welcome to continue using print! statements; this 63 | // just looks a little prettier. 64 | if let Err(_) = std::env::var("RUST_LOG") { 65 | std::env::set_var("RUST_LOG", "debug"); 66 | } 67 | pretty_env_logger::init(); 68 | 69 | // Parse the command line arguments passed to this program 70 | let options = CmdOptions::parse(); 71 | if options.upstream.len() < 1 { 72 | log::error!("At least one upstream server must be specified using the --upstream option."); 73 | std::process::exit(1); 74 | } 75 | 76 | // Start listening for connections 77 | let listener = match TcpListener::bind(&options.bind) { 78 | Ok(listener) => listener, 79 | Err(err) => { 80 | log::error!("Could not bind to {}: {}", options.bind, err); 81 | std::process::exit(1); 82 | } 83 | }; 84 | log::info!("Listening for requests on {}", options.bind); 85 | 86 | // Handle incoming connections 87 | let state = ProxyState { 88 | upstream_addresses: options.upstream, 89 | active_health_check_interval: options.active_health_check_interval, 90 | active_health_check_path: options.active_health_check_path, 91 | max_requests_per_minute: options.max_requests_per_minute, 92 | }; 93 | for stream in listener.incoming() { 94 | if let Ok(stream) = stream { 95 | // Handle the connection! 96 | handle_connection(stream, &state); 97 | } 98 | } 99 | } 100 | 101 | fn connect_to_upstream(state: &ProxyState) -> Result { 102 | let mut rng = rand::rngs::StdRng::from_entropy(); 103 | let upstream_idx = rng.gen_range(0, state.upstream_addresses.len()); 104 | let upstream_ip = &state.upstream_addresses[upstream_idx]; 105 | TcpStream::connect(upstream_ip).or_else(|err| { 106 | log::error!("Failed to connect to upstream {}: {}", upstream_ip, err); 107 | Err(err) 108 | }) 109 | // TODO: implement failover (milestone 3) 110 | } 111 | 112 | fn send_response(client_conn: &mut TcpStream, response: &http::Response>) { 113 | let client_ip = client_conn.peer_addr().unwrap().ip().to_string(); 114 | log::info!("{} <- {}", client_ip, response::format_response_line(&response)); 115 | if let Err(error) = response::write_to_stream(&response, client_conn) { 116 | log::warn!("Failed to send response to client: {}", error); 117 | return; 118 | } 119 | } 120 | 121 | fn handle_connection(mut client_conn: TcpStream, state: &ProxyState) { 122 | let client_ip = client_conn.peer_addr().unwrap().ip().to_string(); 123 | log::info!("Connection received from {}", client_ip); 124 | 125 | // Open a connection to a random destination server 126 | let mut upstream_conn = match connect_to_upstream(state) { 127 | Ok(stream) => stream, 128 | Err(_error) => { 129 | let response = response::make_http_error(http::StatusCode::BAD_GATEWAY); 130 | send_response(&mut client_conn, &response); 131 | return; 132 | } 133 | }; 134 | let upstream_ip = client_conn.peer_addr().unwrap().ip().to_string(); 135 | 136 | // The client may now send us one or more requests. Keep trying to read requests until the 137 | // client hangs up or we get an error. 138 | loop { 139 | // Read a request from the client 140 | let mut request = match request::read_from_stream(&mut client_conn) { 141 | Ok(request) => request, 142 | // Handle case where client closed connection and is no longer sending requests 143 | Err(request::Error::IncompleteRequest(0)) => { 144 | log::debug!("Client finished sending requests. Shutting down connection"); 145 | return; 146 | } 147 | // Handle I/O error in reading from the client 148 | Err(request::Error::ConnectionError(io_err)) => { 149 | log::info!("Error reading request from client stream: {}", io_err); 150 | return; 151 | } 152 | Err(error) => { 153 | log::debug!("Error parsing request: {:?}", error); 154 | let response = response::make_http_error(match error { 155 | request::Error::IncompleteRequest(_) 156 | | request::Error::MalformedRequest(_) 157 | | request::Error::InvalidContentLength 158 | | request::Error::ContentLengthMismatch => http::StatusCode::BAD_REQUEST, 159 | request::Error::RequestBodyTooLarge => http::StatusCode::PAYLOAD_TOO_LARGE, 160 | request::Error::ConnectionError(_) => http::StatusCode::SERVICE_UNAVAILABLE, 161 | }); 162 | send_response(&mut client_conn, &response); 163 | continue; 164 | } 165 | }; 166 | log::info!( 167 | "{} -> {}: {}", 168 | client_ip, 169 | upstream_ip, 170 | request::format_request_line(&request) 171 | ); 172 | 173 | // Add X-Forwarded-For header so that the upstream server knows the client's IP address. 174 | // (We're the ones connecting directly to the upstream server, so without this header, the 175 | // upstream server will only know our IP, not the client's.) 176 | request::extend_header_value(&mut request, "x-forwarded-for", &client_ip); 177 | 178 | // Forward the request to the server 179 | if let Err(error) = request::write_to_stream(&request, &mut upstream_conn) { 180 | log::error!("Failed to send request to upstream {}: {}", upstream_ip, error); 181 | let response = response::make_http_error(http::StatusCode::BAD_GATEWAY); 182 | send_response(&mut client_conn, &response); 183 | return; 184 | } 185 | log::debug!("Forwarded request to server"); 186 | 187 | // Read the server's response 188 | let response = match response::read_from_stream(&mut upstream_conn, request.method()) { 189 | Ok(response) => response, 190 | Err(error) => { 191 | log::error!("Error reading response from server: {:?}", error); 192 | let response = response::make_http_error(http::StatusCode::BAD_GATEWAY); 193 | send_response(&mut client_conn, &response); 194 | return; 195 | } 196 | }; 197 | // Forward the response to the client 198 | send_response(&mut client_conn, &response); 199 | log::debug!("Forwarded response to client"); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /proj-2/balancebeam/src/request.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::min; 2 | use std::io::{Read, Write}; 3 | use std::net::TcpStream; 4 | 5 | const MAX_HEADERS_SIZE: usize = 8000; 6 | const MAX_BODY_SIZE: usize = 10000000; 7 | const MAX_NUM_HEADERS: usize = 32; 8 | 9 | #[derive(Debug)] 10 | pub enum Error { 11 | /// Client hung up before sending a complete request. IncompleteRequest contains the number of 12 | /// bytes that were successfully read before the client hung up 13 | IncompleteRequest(usize), 14 | /// Client sent an invalid HTTP request. httparse::Error contains more details 15 | MalformedRequest(httparse::Error), 16 | /// The Content-Length header is present, but does not contain a valid numeric value 17 | InvalidContentLength, 18 | /// The Content-Length header does not match the size of the request body that was sent 19 | ContentLengthMismatch, 20 | /// The request body is bigger than MAX_BODY_SIZE 21 | RequestBodyTooLarge, 22 | /// Encountered an I/O error when reading/writing a TcpStream 23 | ConnectionError(std::io::Error), 24 | } 25 | 26 | /// Extracts the Content-Length header value from the provided request. Returns Ok(Some(usize)) if 27 | /// the Content-Length is present and valid, Ok(None) if Content-Length is not present, or 28 | /// Err(Error) if Content-Length is present but invalid. 29 | /// 30 | /// You won't need to touch this function. 31 | fn get_content_length(request: &http::Request>) -> Result, Error> { 32 | // Look for content-length header 33 | if let Some(header_value) = request.headers().get("content-length") { 34 | // If it exists, parse it as a usize (or return InvalidContentLength if it can't be parsed as such) 35 | Ok(Some( 36 | header_value 37 | .to_str() 38 | .or(Err(Error::InvalidContentLength))? 39 | .parse::() 40 | .or(Err(Error::InvalidContentLength))?, 41 | )) 42 | } else { 43 | // If it doesn't exist, return None 44 | Ok(None) 45 | } 46 | } 47 | 48 | /// This function appends to a header value (adding a new header if the header is not already 49 | /// present). This is used to add the client's IP address to the end of the X-Forwarded-For list, 50 | /// or to add a new X-Forwarded-For header if one is not already present. 51 | /// 52 | /// You won't need to touch this function. 53 | pub fn extend_header_value( 54 | request: &mut http::Request>, 55 | name: &'static str, 56 | extend_value: &str, 57 | ) { 58 | let new_value = match request.headers().get(name) { 59 | Some(existing_value) => { 60 | [existing_value.as_bytes(), b", ", extend_value.as_bytes()].concat() 61 | } 62 | None => extend_value.as_bytes().to_owned(), 63 | }; 64 | request 65 | .headers_mut() 66 | .insert(name, http::HeaderValue::from_bytes(&new_value).unwrap()); 67 | } 68 | 69 | /// Attempts to parse the data in the supplied buffer as an HTTP request. Returns one of the 70 | /// following: 71 | /// 72 | /// * If there is a complete and valid request in the buffer, returns Ok(Some(http::Request)) 73 | /// * If there is an incomplete but valid-so-far request in the buffer, returns Ok(None) 74 | /// * If there is data in the buffer that is definitely not a valid HTTP request, returns Err(Error) 75 | /// 76 | /// You won't need to touch this function. 77 | fn parse_request(buffer: &[u8]) -> Result>, usize)>, Error> { 78 | let mut headers = [httparse::EMPTY_HEADER; MAX_NUM_HEADERS]; 79 | let mut req = httparse::Request::new(&mut headers); 80 | let res = req.parse(buffer).or_else(|err| Err(Error::MalformedRequest(err)))?; 81 | 82 | if let httparse::Status::Complete(len) = res { 83 | let mut request = http::Request::builder() 84 | .method(req.method.unwrap()) 85 | .uri(req.path.unwrap()) 86 | .version(http::Version::HTTP_11); 87 | for header in req.headers { 88 | request = request.header(header.name, header.value); 89 | } 90 | let request = request.body(Vec::new()).unwrap(); 91 | Ok(Some((request, len))) 92 | } else { 93 | Ok(None) 94 | } 95 | } 96 | 97 | /// Reads an HTTP request from the provided stream, waiting until a complete set of headers is sent. 98 | /// This function only reads the request line and headers; the read_body function can subsequently 99 | /// be called in order to read the request body (for a POST request). 100 | /// 101 | /// Returns Ok(http::Request) if a valid request is received, or Error if not. 102 | /// 103 | /// You will need to modify this function in Milestone 2. 104 | fn read_headers(stream: &mut TcpStream) -> Result>, Error> { 105 | // Try reading the headers from the request. We may not receive all the headers in one shot 106 | // (e.g. we might receive the first few bytes of a request, and then the rest follows later). 107 | // Try parsing repeatedly until we read a valid HTTP request 108 | let mut request_buffer = [0_u8; MAX_HEADERS_SIZE]; 109 | let mut bytes_read = 0; 110 | loop { 111 | // Read bytes from the connection into the buffer, starting at position bytes_read 112 | let new_bytes = stream 113 | .read(&mut request_buffer[bytes_read..]) 114 | .or_else(|err| Err(Error::ConnectionError(err)))?; 115 | if new_bytes == 0 { 116 | // We didn't manage to read a complete request 117 | return Err(Error::IncompleteRequest(bytes_read)); 118 | } 119 | bytes_read += new_bytes; 120 | 121 | // See if we've read a valid request so far 122 | if let Some((mut request, headers_len)) = parse_request(&request_buffer[..bytes_read])? { 123 | // We've read a complete set of headers. However, if this was a POST request, a request 124 | // body might have been included as well, and we might have read part of the body out of 125 | // the stream into header_buffer. We need to add those bytes to the Request body so that 126 | // we don't lose them 127 | request 128 | .body_mut() 129 | .extend_from_slice(&request_buffer[headers_len..bytes_read]); 130 | return Ok(request); 131 | } 132 | } 133 | } 134 | 135 | /// This function reads the body for a request from the stream. The client only sends a body if the 136 | /// Content-Length header is present; this function reads that number of bytes from the stream. It 137 | /// returns Ok(()) if successful, or Err(Error) if Content-Length bytes couldn't be read. 138 | /// 139 | /// You will need to modify this function in Milestone 2. 140 | fn read_body( 141 | stream: &mut TcpStream, 142 | request: &mut http::Request>, 143 | content_length: usize, 144 | ) -> Result<(), Error> { 145 | // Keep reading data until we read the full body length, or until we hit an error. 146 | while request.body().len() < content_length { 147 | // Read up to 512 bytes at a time. (If the client only sent a small body, then only allocate 148 | // space to read that body.) 149 | let mut buffer = vec![0_u8; min(512, content_length)]; 150 | let bytes_read = stream.read(&mut buffer).or_else(|err| Err(Error::ConnectionError(err)))?; 151 | 152 | // Make sure the client is still sending us bytes 153 | if bytes_read == 0 { 154 | log::debug!( 155 | "Client hung up after sending a body of length {}, even though it said the content \ 156 | length is {}", 157 | request.body().len(), 158 | content_length 159 | ); 160 | return Err(Error::ContentLengthMismatch); 161 | } 162 | 163 | // Make sure the client didn't send us *too many* bytes 164 | if request.body().len() + bytes_read > content_length { 165 | log::debug!( 166 | "Client sent more bytes than we expected based on the given content length!" 167 | ); 168 | return Err(Error::ContentLengthMismatch); 169 | } 170 | 171 | // Store the received bytes in the request body 172 | request.body_mut().extend_from_slice(&buffer[..bytes_read]); 173 | } 174 | Ok(()) 175 | } 176 | 177 | /// This function reads and returns an HTTP request from a stream, returning an Error if the client 178 | /// closes the connection prematurely or sends an invalid request. 179 | /// 180 | /// You will need to modify this function in Milestone 2. 181 | pub fn read_from_stream(stream: &mut TcpStream) -> Result>, Error> { 182 | // Read headers 183 | let mut request = read_headers(stream)?; 184 | // Read body if the client supplied the Content-Length header (which it does for POST requests) 185 | if let Some(content_length) = get_content_length(&request)? { 186 | if content_length > MAX_BODY_SIZE { 187 | return Err(Error::RequestBodyTooLarge); 188 | } else { 189 | read_body(stream, &mut request, content_length)?; 190 | } 191 | } 192 | Ok(request) 193 | } 194 | 195 | /// This function serializes a request to bytes and writes those bytes to the provided stream. 196 | /// 197 | /// You will need to modify this function in Milestone 2. 198 | pub fn write_to_stream( 199 | request: &http::Request>, 200 | stream: &mut TcpStream, 201 | ) -> Result<(), std::io::Error> { 202 | stream.write(&format_request_line(request).into_bytes())?; 203 | stream.write(&['\r' as u8, '\n' as u8])?; // \r\n 204 | for (header_name, header_value) in request.headers() { 205 | stream.write(&format!("{}: ", header_name).as_bytes())?; 206 | stream.write(header_value.as_bytes())?; 207 | stream.write(&['\r' as u8, '\n' as u8])?; // \r\n 208 | } 209 | stream.write(&['\r' as u8, '\n' as u8])?; 210 | if request.body().len() > 0 { 211 | stream.write(request.body())?; 212 | } 213 | Ok(()) 214 | } 215 | 216 | pub fn format_request_line(request: &http::Request>) -> String { 217 | format!("{} {} {:?}", request.method(), request.uri(), request.version()) 218 | } 219 | -------------------------------------------------------------------------------- /proj-2/balancebeam/src/response.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | use std::net::TcpStream; 3 | 4 | const MAX_HEADERS_SIZE: usize = 8000; 5 | const MAX_BODY_SIZE: usize = 10000000; 6 | const MAX_NUM_HEADERS: usize = 32; 7 | 8 | #[derive(Debug)] 9 | pub enum Error { 10 | /// Client hung up before sending a complete request 11 | IncompleteResponse, 12 | /// Client sent an invalid HTTP request. httparse::Error contains more details 13 | MalformedResponse(httparse::Error), 14 | /// The Content-Length header is present, but does not contain a valid numeric value 15 | InvalidContentLength, 16 | /// The Content-Length header does not match the size of the request body that was sent 17 | ContentLengthMismatch, 18 | /// The request body is bigger than MAX_BODY_SIZE 19 | ResponseBodyTooLarge, 20 | /// Encountered an I/O error when reading/writing a TcpStream 21 | ConnectionError(std::io::Error), 22 | } 23 | 24 | /// Extracts the Content-Length header value from the provided response. Returns Ok(Some(usize)) if 25 | /// the Content-Length is present and valid, Ok(None) if Content-Length is not present, or 26 | /// Err(Error) if Content-Length is present but invalid. 27 | /// 28 | /// You won't need to touch this function. 29 | fn get_content_length(response: &http::Response>) -> Result, Error> { 30 | // Look for content-length header 31 | if let Some(header_value) = response.headers().get("content-length") { 32 | // If it exists, parse it as a usize (or return InvalidResponseFormat if it can't be parsed as such) 33 | Ok(Some( 34 | header_value 35 | .to_str() 36 | .or(Err(Error::InvalidContentLength))? 37 | .parse::() 38 | .or(Err(Error::InvalidContentLength))?, 39 | )) 40 | } else { 41 | // If it doesn't exist, return None 42 | Ok(None) 43 | } 44 | } 45 | 46 | /// Attempts to parse the data in the supplied buffer as an HTTP response. Returns one of the 47 | /// following: 48 | /// 49 | /// * If there is a complete and valid response in the buffer, returns Ok(Some(http::Request)) 50 | /// * If there is an incomplete but valid-so-far response in the buffer, returns Ok(None) 51 | /// * If there is data in the buffer that is definitely not a valid HTTP response, returns 52 | /// Err(Error) 53 | /// 54 | /// You won't need to touch this function. 55 | fn parse_response(buffer: &[u8]) -> Result>, usize)>, Error> { 56 | let mut headers = [httparse::EMPTY_HEADER; MAX_NUM_HEADERS]; 57 | let mut resp = httparse::Response::new(&mut headers); 58 | let res = resp 59 | .parse(buffer) 60 | .or_else(|err| Err(Error::MalformedResponse(err)))?; 61 | 62 | if let httparse::Status::Complete(len) = res { 63 | let mut response = http::Response::builder() 64 | .status(resp.code.unwrap()) 65 | .version(http::Version::HTTP_11); 66 | for header in resp.headers { 67 | response = response.header(header.name, header.value); 68 | } 69 | let response = response.body(Vec::new()).unwrap(); 70 | Ok(Some((response, len))) 71 | } else { 72 | Ok(None) 73 | } 74 | } 75 | 76 | /// Reads an HTTP response from the provided stream, waiting until a complete set of headers is 77 | /// sent. This function only reads the response line and headers; the read_body function can 78 | /// subsequently be called in order to read the response body. 79 | /// 80 | /// Returns Ok(http::Response) if a valid response is received, or Error if not. 81 | /// 82 | /// You will need to modify this function in Milestone 2. 83 | fn read_headers(stream: &mut TcpStream) -> Result>, Error> { 84 | // Try reading the headers from the response. We may not receive all the headers in one shot 85 | // (e.g. we might receive the first few bytes of a response, and then the rest follows later). 86 | // Try parsing repeatedly until we read a valid HTTP response 87 | let mut response_buffer = [0_u8; MAX_HEADERS_SIZE]; 88 | let mut bytes_read = 0; 89 | loop { 90 | // Read bytes from the connection into the buffer, starting at position bytes_read 91 | let new_bytes = stream 92 | .read(&mut response_buffer[bytes_read..]) 93 | .or_else(|err| Err(Error::ConnectionError(err)))?; 94 | if new_bytes == 0 { 95 | // We didn't manage to read a complete response 96 | return Err(Error::IncompleteResponse); 97 | } 98 | bytes_read += new_bytes; 99 | 100 | // See if we've read a valid response so far 101 | if let Some((mut response, headers_len)) = parse_response(&response_buffer[..bytes_read])? { 102 | // We've read a complete set of headers. We may have also read the first part of the 103 | // response body; take whatever is left over in the response buffer and save that as 104 | // the start of the response body. 105 | response 106 | .body_mut() 107 | .extend_from_slice(&response_buffer[headers_len..bytes_read]); 108 | return Ok(response); 109 | } 110 | } 111 | } 112 | 113 | /// This function reads the body for a response from the stream. If the Content-Length header is 114 | /// present, it reads that many bytes; otherwise, it reads bytes until the connection is closed. 115 | /// 116 | /// You will need to modify this function in Milestone 2. 117 | fn read_body(stream: &mut TcpStream, response: &mut http::Response>) -> Result<(), Error> { 118 | // The response may or may not supply a Content-Length header. If it provides the header, then 119 | // we want to read that number of bytes; if it does not, we want to keep reading bytes until 120 | // the connection is closed. 121 | let content_length = get_content_length(response)?; 122 | 123 | while content_length.is_none() || response.body().len() < content_length.unwrap() { 124 | let mut buffer = [0_u8; 512]; 125 | let bytes_read = stream 126 | .read(&mut buffer) 127 | .or_else(|err| Err(Error::ConnectionError(err)))?; 128 | if bytes_read == 0 { 129 | // The server has hung up! 130 | if content_length.is_none() { 131 | // We've reached the end of the response 132 | break; 133 | } else { 134 | // Content-Length was set, but the server hung up before we managed to read that 135 | // number of bytes 136 | return Err(Error::ContentLengthMismatch); 137 | } 138 | } 139 | 140 | // Make sure the server doesn't send more bytes than it promised to send 141 | if content_length.is_some() && response.body().len() + bytes_read > content_length.unwrap() 142 | { 143 | return Err(Error::ContentLengthMismatch); 144 | } 145 | 146 | // Make sure server doesn't send more bytes than we allow 147 | if response.body().len() + bytes_read > MAX_BODY_SIZE { 148 | return Err(Error::ResponseBodyTooLarge); 149 | } 150 | 151 | // Append received bytes to the response body 152 | response.body_mut().extend_from_slice(&buffer[..bytes_read]); 153 | } 154 | Ok(()) 155 | } 156 | 157 | /// This function reads and returns an HTTP response from a stream, returning an Error if the server 158 | /// closes the connection prematurely or sends an invalid response. 159 | /// 160 | /// You will need to modify this function in Milestone 2. 161 | pub fn read_from_stream( 162 | stream: &mut TcpStream, 163 | request_method: &http::Method, 164 | ) -> Result>, Error> { 165 | let mut response = read_headers(stream)?; 166 | // A response may have a body as long as it is not responding to a HEAD request and as long as 167 | // the response status code is not 1xx, 204 (no content), or 304 (not modified). 168 | if !(request_method == http::Method::HEAD 169 | || response.status().as_u16() < 200 170 | || response.status() == http::StatusCode::NO_CONTENT 171 | || response.status() == http::StatusCode::NOT_MODIFIED) 172 | { 173 | read_body(stream, &mut response)?; 174 | } 175 | Ok(response) 176 | } 177 | 178 | /// This function serializes a response to bytes and writes those bytes to the provided stream. 179 | /// 180 | /// You will need to modify this function in Milestone 2. 181 | pub fn write_to_stream( 182 | response: &http::Response>, 183 | stream: &mut TcpStream, 184 | ) -> Result<(), std::io::Error> { 185 | stream.write(&format_response_line(response).into_bytes())?; 186 | stream.write(&['\r' as u8, '\n' as u8])?; // \r\n 187 | for (header_name, header_value) in response.headers() { 188 | stream.write(&format!("{}: ", header_name).as_bytes())?; 189 | stream.write(header_value.as_bytes())?; 190 | stream.write(&['\r' as u8, '\n' as u8])?; // \r\n 191 | } 192 | stream.write(&['\r' as u8, '\n' as u8])?; 193 | if response.body().len() > 0 { 194 | stream.write(response.body())?; 195 | } 196 | Ok(()) 197 | } 198 | 199 | pub fn format_response_line(response: &http::Response>) -> String { 200 | format!( 201 | "{:?} {} {}", 202 | response.version(), 203 | response.status().as_str(), 204 | response.status().canonical_reason().unwrap_or("") 205 | ) 206 | } 207 | 208 | /// This is a helper function that creates an http::Response containing an HTTP error that can be 209 | /// sent to a client. 210 | pub fn make_http_error(status: http::StatusCode) -> http::Response> { 211 | let body = format!( 212 | "HTTP {} {}", 213 | status.as_u16(), 214 | status.canonical_reason().unwrap_or("") 215 | ) 216 | .into_bytes(); 217 | http::Response::builder() 218 | .status(status) 219 | .header("Content-Type", "text/plain") 220 | .header("Content-Length", body.len().to_string()) 221 | .version(http::Version::HTTP_11) 222 | .body(body) 223 | .unwrap() 224 | } 225 | -------------------------------------------------------------------------------- /proj-2/balancebeam/tests/01_single_upstream_tests.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use common::{init_logging, BalanceBeam, EchoServer, Server}; 4 | use std::sync::Arc; 5 | 6 | async fn setup() -> (BalanceBeam, EchoServer) { 7 | init_logging(); 8 | let upstream = EchoServer::new().await; 9 | let balancebeam = BalanceBeam::new(&[&upstream.address], None, None).await; 10 | (balancebeam, upstream) 11 | } 12 | 13 | /// Test the simple case: open a few connections, each with only a single request, and make sure 14 | /// things are delivered correctly. 15 | #[tokio::test] 16 | async fn test_simple_connections() { 17 | let (balancebeam, upstream) = setup().await; 18 | 19 | log::info!("Sending a GET request"); 20 | let response_text = balancebeam 21 | .get("/first_url") 22 | .await 23 | .expect("Error sending request to balancebeam"); 24 | assert!(response_text.contains("GET /first_url HTTP/1.1")); 25 | assert!(response_text.contains("x-sent-by: balancebeam-tests")); 26 | assert!(response_text.contains("x-forwarded-for: 127.0.0.1")); 27 | 28 | log::info!("Sending a POST request"); 29 | let response_text = balancebeam 30 | .post("/first_url", "Hello world!") 31 | .await 32 | .expect("Error sending request to balancebeam"); 33 | assert!(response_text.contains("POST /first_url HTTP/1.1")); 34 | assert!(response_text.contains("x-sent-by: balancebeam-tests")); 35 | assert!(response_text.contains("x-forwarded-for: 127.0.0.1")); 36 | assert!(response_text.contains("\n\nHello world!")); 37 | 38 | log::info!("Checking that the origin server received 2 requests"); 39 | let num_requests_received = Box::new(upstream).stop().await; 40 | assert_eq!( 41 | num_requests_received, 2, 42 | "Upstream server did not receive the expected number of requests" 43 | ); 44 | 45 | log::info!("All done :)"); 46 | } 47 | 48 | /// Test handling of multiple HTTP requests per connection to the server. Open three concurrent 49 | /// connections, and send four requests on each. 50 | #[tokio::test] 51 | async fn test_multiple_requests_per_connection() { 52 | let num_connections = 3; 53 | let requests_per_connection = 4; 54 | 55 | let (balancebeam, upstream) = setup().await; 56 | let balancebeam_shared = Arc::new(balancebeam); 57 | 58 | let mut tasks = Vec::new(); 59 | for task_num in 0..num_connections { 60 | let balancebeam_shared = balancebeam_shared.clone(); 61 | tasks.push(tokio::task::spawn(async move { 62 | let client = reqwest::Client::new(); 63 | for req_num in 0..requests_per_connection { 64 | log::info!( 65 | "Task {} sending request {} (connection {})", 66 | task_num, 67 | req_num, 68 | task_num 69 | ); 70 | let path = format!("/conn-{}/req-{}", task_num, req_num); 71 | let response_text = client 72 | .get(&format!("http://{}{}", balancebeam_shared.address, path)) 73 | .header("x-sent-by", "balancebeam-tests") 74 | .send() 75 | .await 76 | .expect("Failed to connect to balancebeam") 77 | .text() 78 | .await 79 | .expect("Balancebeam replied with a malformed response"); 80 | assert!(response_text.contains(&format!("GET {} HTTP/1.1", path))); 81 | } 82 | })); 83 | } 84 | 85 | for join_handle in tasks { 86 | join_handle.await.expect("Task panicked"); 87 | } 88 | 89 | log::info!( 90 | "Checking that the origin server received {} requests", 91 | num_connections * requests_per_connection 92 | ); 93 | let num_requests_received = Box::new(upstream).stop().await; 94 | assert_eq!( 95 | num_requests_received, 96 | num_connections * requests_per_connection, 97 | "Upstream server did not receive the expected number of requests" 98 | ); 99 | 100 | log::info!("All done :)"); 101 | } 102 | -------------------------------------------------------------------------------- /proj-2/balancebeam/tests/02_multiple_upstream_tests.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use common::{init_logging, BalanceBeam, EchoServer, ErrorServer, Server}; 4 | 5 | use std::time::Duration; 6 | use tokio::time::delay_for; 7 | 8 | async fn setup_with_params( 9 | n_upstreams: usize, 10 | active_health_check_interval: Option, 11 | max_requests_per_minute: Option, 12 | ) -> (BalanceBeam, Vec>) { 13 | init_logging(); 14 | let mut upstreams: Vec> = Vec::new(); 15 | for _ in 0..n_upstreams { 16 | upstreams.push(Box::new(EchoServer::new().await)); 17 | } 18 | let upstream_addresses: Vec = upstreams 19 | .iter() 20 | .map(|upstream| upstream.address()) 21 | .collect(); 22 | let upstream_addresses: Vec<&str> = upstream_addresses 23 | .iter() 24 | .map(|addr| addr.as_str()) 25 | .collect(); 26 | let balancebeam = BalanceBeam::new( 27 | &upstream_addresses, 28 | active_health_check_interval, 29 | max_requests_per_minute, 30 | ) 31 | .await; 32 | (balancebeam, upstreams) 33 | } 34 | 35 | async fn setup(n_upstreams: usize) -> (BalanceBeam, Vec>) { 36 | setup_with_params(n_upstreams, None, None).await 37 | } 38 | 39 | /// Send a bunch of requests to the load balancer, and ensure they are evenly distributed across the 40 | /// upstream servers 41 | #[tokio::test] 42 | async fn test_load_distribution() { 43 | let n_upstreams = 3; 44 | let n_requests = 90; 45 | let (balancebeam, mut upstreams) = setup(n_upstreams).await; 46 | 47 | for i in 0..n_requests { 48 | let path = format!("/request-{}", i); 49 | let response_text = balancebeam 50 | .get(&path) 51 | .await 52 | .expect("Error sending request to balancebeam"); 53 | assert!(response_text.contains(&format!("GET {} HTTP/1.1", path))); 54 | assert!(response_text.contains("x-sent-by: balancebeam-tests")); 55 | assert!(response_text.contains("x-forwarded-for: 127.0.0.1")); 56 | } 57 | 58 | let mut request_counters = Vec::new(); 59 | while let Some(upstream) = upstreams.pop() { 60 | request_counters.insert(0, upstream.stop().await); 61 | } 62 | log::info!( 63 | "Number of requests received by each upstream: {:?}", 64 | request_counters 65 | ); 66 | let avg_req_count = 67 | request_counters.iter().sum::() as f64 / request_counters.len() as f64; 68 | log::info!("Average number of requests per upstream: {}", avg_req_count); 69 | for upstream_req_count in request_counters { 70 | if (upstream_req_count as f64 - avg_req_count).abs() > 0.4 * avg_req_count { 71 | log::error!( 72 | "Upstream request count {} differs too much from the average! Load doesn't seem \ 73 | evenly distributed.", 74 | upstream_req_count 75 | ); 76 | panic!("Upstream request count differs too much"); 77 | } 78 | } 79 | 80 | log::info!("All done :)"); 81 | } 82 | 83 | async fn try_failover(balancebeam: &BalanceBeam, upstreams: &mut Vec>) { 84 | // Send some initial requests. Everything should work 85 | log::info!("Sending some initial requests. These should definitely work."); 86 | for i in 0..5 { 87 | let path = format!("/request-{}", i); 88 | let response_text = balancebeam 89 | .get(&path) 90 | .await 91 | .expect("Error sending request to balancebeam"); 92 | assert!(response_text.contains(&format!("GET {} HTTP/1.1", path))); 93 | assert!(response_text.contains("x-sent-by: balancebeam-tests")); 94 | assert!(response_text.contains("x-forwarded-for: 127.0.0.1")); 95 | } 96 | 97 | // Kill one of the upstreams 98 | log::info!("Killing one of the upstream servers"); 99 | upstreams.pop().unwrap().stop().await; 100 | 101 | // Make sure requests continue to work 102 | for i in 0..6 { 103 | log::info!("Sending request #{} after killing an upstream server", i); 104 | let path = format!("/failover-{}", i); 105 | let response_text = balancebeam 106 | .get(&path) 107 | .await 108 | .expect("Error sending request to balancebeam. Passive failover may not be working"); 109 | assert!( 110 | response_text.contains(&format!("GET {} HTTP/1.1", path)), 111 | "balancebeam returned unexpected response. Failover may not be working." 112 | ); 113 | assert!( 114 | response_text.contains("x-sent-by: balancebeam-tests"), 115 | "balancebeam returned unexpected response. Failover may not be working." 116 | ); 117 | assert!( 118 | response_text.contains("x-forwarded-for: 127.0.0.1"), 119 | "balancebeam returned unexpected response. Failover may not be working." 120 | ); 121 | } 122 | } 123 | 124 | /// Make sure passive health checks work. Send a few requests, then kill one of the upstreams and 125 | /// make sure requests continue to work 126 | #[tokio::test] 127 | async fn test_passive_health_checks() { 128 | let n_upstreams = 2; 129 | let (balancebeam, mut upstreams) = setup(n_upstreams).await; 130 | try_failover(&balancebeam, &mut upstreams).await; 131 | log::info!("All done :)"); 132 | } 133 | 134 | /// Verify that the active health checks are monitoring HTTP status, rather than simply depending 135 | /// on whether connections can be established to determine whether an upstream is up: 136 | /// 137 | /// * Send a few requests 138 | /// * Replace one of the upstreams with a server that only returns HTTP error 500s 139 | /// * Send some more requests. Make sure all the requests succeed 140 | #[tokio::test] 141 | async fn test_active_health_checks_check_http_status() { 142 | let n_upstreams = 2; 143 | let (balancebeam, mut upstreams) = setup_with_params(n_upstreams, Some(1), None).await; 144 | let failed_ip = upstreams[upstreams.len() - 1].address(); 145 | 146 | // Send some initial requests. Everything should work 147 | log::info!("Sending some initial requests. These should definitely work."); 148 | for i in 0..4 { 149 | let path = format!("/request-{}", i); 150 | let response_text = balancebeam 151 | .get(&path) 152 | .await 153 | .expect("Error sending request to balancebeam"); 154 | assert!(response_text.contains(&format!("GET {} HTTP/1.1", path))); 155 | assert!(response_text.contains("x-sent-by: balancebeam-tests")); 156 | assert!(response_text.contains("x-forwarded-for: 127.0.0.1")); 157 | } 158 | 159 | // Do a switcharoo with an upstream 160 | log::info!("Replacing one of the upstreams with a server that returns Error 500s..."); 161 | upstreams.pop().unwrap().stop().await; 162 | upstreams.push(Box::new(ErrorServer::new_at_address(failed_ip).await)); 163 | 164 | log::info!("Waiting for health checks to realize server is dead..."); 165 | delay_for(Duration::from_secs(3)).await; 166 | 167 | // Make sure we get back successful requests 168 | for i in 0..8 { 169 | log::info!( 170 | "Sending request #{} after swapping server for one that returns Error 500. We should \ 171 | get a successful response from the other upstream", 172 | i 173 | ); 174 | let path = format!("/failover-{}", i); 175 | let response_text = balancebeam.get(&path).await.expect( 176 | "Error sending request to balancebeam. Active health checks may not be working", 177 | ); 178 | assert!( 179 | response_text.contains(&format!("GET {} HTTP/1.1", path)), 180 | "balancebeam returned unexpected response. Active health checks may not be working." 181 | ); 182 | assert!( 183 | response_text.contains("x-sent-by: balancebeam-tests"), 184 | "balancebeam returned unexpected response. Active health checks may not be working." 185 | ); 186 | assert!( 187 | response_text.contains("x-forwarded-for: 127.0.0.1"), 188 | "balancebeam returned unexpected response. Active health checks may not be working." 189 | ); 190 | } 191 | } 192 | 193 | /// Make sure active health checks restore upstreams that were previously failed but are now 194 | /// working again: 195 | /// 196 | /// * Send a few requests 197 | /// * Kill one of the upstreams 198 | /// * Send some more requests 199 | /// * Bring the upstream back 200 | /// * Ensure requests are delivered again 201 | #[tokio::test] 202 | async fn test_active_health_checks_restore_failed_upstream() { 203 | let n_upstreams = 2; 204 | let (balancebeam, mut upstreams) = setup_with_params(n_upstreams, Some(1), None).await; 205 | let failed_ip = upstreams[upstreams.len() - 1].address(); 206 | try_failover(&balancebeam, &mut upstreams).await; 207 | 208 | log::info!("Re-starting the \"failed\" upstream server..."); 209 | upstreams.push(Box::new(EchoServer::new_at_address(failed_ip).await)); 210 | 211 | log::info!("Waiting a few seconds for the active health check to run..."); 212 | delay_for(Duration::from_secs(3)).await; 213 | 214 | log::info!("Sending some more requests"); 215 | for i in 0..5 { 216 | let path = format!("/after-restore-{}", i); 217 | let response_text = balancebeam 218 | .get(&path) 219 | .await 220 | .expect("Error sending request to balancebeam"); 221 | assert!(response_text.contains(&format!("GET {} HTTP/1.1", path))); 222 | assert!(response_text.contains("x-sent-by: balancebeam-tests")); 223 | assert!(response_text.contains("x-forwarded-for: 127.0.0.1")); 224 | } 225 | 226 | log::info!( 227 | "Verifying that the previously-dead upstream got some requests after being restored" 228 | ); 229 | let last_upstream_req_count = upstreams.pop().unwrap().stop().await; 230 | assert!( 231 | last_upstream_req_count > 0, 232 | "We killed an upstream, then brought it back, but it never got any more requests!" 233 | ); 234 | 235 | // Shut down 236 | while let Some(upstream) = upstreams.pop() { 237 | upstream.stop().await; 238 | } 239 | 240 | log::info!("All done :)"); 241 | } 242 | 243 | /// Enable rate limiting and ensure that requests fail after sending more than the threshold 244 | #[tokio::test] 245 | async fn test_rate_limiting() { 246 | let n_upstreams = 1; 247 | let rate_limit_threshold = 5; 248 | let num_extra_requests: usize = 3; 249 | let (balancebeam, mut upstreams) = 250 | setup_with_params(n_upstreams, None, Some(rate_limit_threshold)).await; 251 | 252 | log::info!( 253 | "Sending some basic requests to the server, within the rate limit threshold. These \ 254 | should succeed." 255 | ); 256 | for i in 0..rate_limit_threshold { 257 | let path = format!("/request-{}", i); 258 | let response_text = balancebeam 259 | .get(&path) 260 | .await 261 | .expect("Error sending request to balancebeam"); 262 | assert!(response_text.contains(&format!("GET {} HTTP/1.1", path))); 263 | assert!(response_text.contains("x-sent-by: balancebeam-tests")); 264 | assert!(response_text.contains("x-forwarded-for: 127.0.0.1")); 265 | } 266 | 267 | log::info!( 268 | "Sending more requests that exceed the rate limit threshold. The server should \ 269 | respond to these with an HTTP 429 (too many requests) error." 270 | ); 271 | for i in 0..num_extra_requests { 272 | let client = reqwest::Client::new(); 273 | let response = client 274 | .get(&format!("http://{}/overboard-{}", balancebeam.address, i)) 275 | .header("x-sent-by", "balancebeam-tests") 276 | .send() 277 | .await 278 | .expect( 279 | "Error sending rate limited request to balancebeam. You should be \ 280 | accepting the connection but sending back an HTTP error, rather than rejecting \ 281 | the connection outright.", 282 | ); 283 | log::info!("{:?}", response); 284 | log::info!("Checking to make sure the server responded with HTTP 429"); 285 | assert_eq!(response.status().as_u16(), 429); 286 | } 287 | 288 | log::info!("Ensuring the extra requests didn't go through to the upstream servers"); 289 | let mut total_request_count = 0; 290 | while let Some(upstream) = upstreams.pop() { 291 | total_request_count += upstream.stop().await; 292 | } 293 | assert_eq!(total_request_count, rate_limit_threshold); 294 | 295 | log::info!("All done :)"); 296 | } 297 | -------------------------------------------------------------------------------- /proj-2/balancebeam/tests/common/balancebeam.rs: -------------------------------------------------------------------------------- 1 | use rand::Rng; 2 | use std::time::Duration; 3 | use tokio::io::{AsyncBufReadExt, BufReader}; 4 | use tokio::process::{Child, Command}; 5 | use tokio::time::delay_for; 6 | 7 | pub struct BalanceBeam { 8 | #[allow(dead_code)] 9 | child: Child, // process is killed when dropped (Command::kill_on_drop) 10 | pub address: String, 11 | } 12 | 13 | impl BalanceBeam { 14 | fn target_bin_path() -> std::path::PathBuf { 15 | let mut path = std::env::current_exe().expect("Could not get current test executable path"); 16 | path.pop(); 17 | path.pop(); 18 | path.push("balancebeam"); 19 | path 20 | } 21 | 22 | pub async fn new( 23 | upstreams: &[&str], 24 | active_health_check_interval: Option, 25 | max_requests_per_minute: Option, 26 | ) -> BalanceBeam { 27 | let mut rng = rand::thread_rng(); 28 | let address = format!("127.0.0.1:{}", rng.gen_range(1024, 65535)); 29 | let mut cmd = Command::new(BalanceBeam::target_bin_path()); 30 | cmd.arg("--bind").arg(&address); 31 | for upstream in upstreams { 32 | cmd.arg("--upstream").arg(upstream); 33 | } 34 | if let Some(active_health_check_interval) = active_health_check_interval { 35 | cmd.arg("--active-health-check-interval") 36 | .arg(active_health_check_interval.to_string()); 37 | } 38 | if let Some(max_requests_per_minute) = max_requests_per_minute { 39 | cmd.arg("--max-requests-per-minute") 40 | .arg(max_requests_per_minute.to_string()); 41 | } 42 | cmd.kill_on_drop(true); 43 | cmd.stdout(std::process::Stdio::piped()); 44 | cmd.stderr(std::process::Stdio::piped()); 45 | let mut child = cmd.spawn().expect(&format!( 46 | "Could not execute balancebeam binary {}", 47 | BalanceBeam::target_bin_path().to_str().unwrap() 48 | )); 49 | 50 | // Print output from the child. We want to intercept and log this output (instead of letting 51 | // the child inherit stderr and print directly to the terminal) so that the output can be 52 | // suppressed if the test passes and displayed if it fails. 53 | let stdout = child 54 | .stdout 55 | .take() 56 | .expect("Child process somehow missing stdout pipe!"); 57 | tokio::spawn(async move { 58 | let mut stdout_reader = BufReader::new(stdout).lines(); 59 | while let Some(line) = stdout_reader 60 | .next_line() 61 | .await 62 | .expect("I/O error reading from child stdout") 63 | { 64 | println!("Balancebeam output: {}", line); 65 | } 66 | }); 67 | let stderr = child 68 | .stderr 69 | .take() 70 | .expect("Child process somehow missing stderr pipe!"); 71 | tokio::spawn(async move { 72 | let mut stderr_reader = BufReader::new(stderr).lines(); 73 | while let Some(line) = stderr_reader 74 | .next_line() 75 | .await 76 | .expect("I/O error reading from child stderr") 77 | { 78 | println!("Balancebeam output: {}", line); 79 | } 80 | }); 81 | 82 | // Hack: wait for executable to start running 83 | delay_for(Duration::from_secs(1)).await; 84 | BalanceBeam { child, address } 85 | } 86 | 87 | #[allow(dead_code)] 88 | pub async fn get(&self, path: &str) -> Result { 89 | let client = reqwest::Client::new(); 90 | client 91 | .get(&format!("http://{}{}", self.address, path)) 92 | .header("x-sent-by", "balancebeam-tests") 93 | .send() 94 | .await? 95 | .text() 96 | .await 97 | } 98 | 99 | #[allow(dead_code)] 100 | pub async fn post(&self, path: &str, body: &str) -> Result { 101 | let client = reqwest::Client::new(); 102 | client 103 | .post(&format!("http://{}{}", self.address, path)) 104 | .header("x-sent-by", "balancebeam-tests") 105 | .body(body.to_string()) 106 | .send() 107 | .await? 108 | .text() 109 | .await 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /proj-2/balancebeam/tests/common/echo_server.rs: -------------------------------------------------------------------------------- 1 | use crate::common::server::Server; 2 | use async_trait::async_trait; 3 | use hyper::service::{make_service_fn, service_fn}; 4 | use hyper::{Body, Request, Response}; 5 | use rand::Rng; 6 | use std::sync::{atomic, Arc}; 7 | use tokio::sync::oneshot; 8 | 9 | #[derive(Debug)] 10 | struct ServerState { 11 | pub requests_received: atomic::AtomicUsize, 12 | } 13 | 14 | async fn echo( 15 | server_state: Arc, 16 | req: Request, 17 | ) -> Result, hyper::Error> { 18 | server_state 19 | .requests_received 20 | .fetch_add(1, atomic::Ordering::SeqCst); 21 | let mut req_text = format!("{} {} {:?}\n", req.method(), req.uri(), req.version()); 22 | for (header_name, header_value) in req.headers() { 23 | req_text += &format!( 24 | "{}: {}\n", 25 | header_name.as_str(), 26 | header_value.to_str().unwrap_or("") 27 | ); 28 | } 29 | req_text += "\n"; 30 | let mut req_as_bytes = req_text.into_bytes(); 31 | req_as_bytes.extend(hyper::body::to_bytes(req.into_body()).await?); 32 | Ok(Response::new(Body::from(req_as_bytes))) 33 | } 34 | 35 | pub struct EchoServer { 36 | shutdown_signal_sender: oneshot::Sender<()>, 37 | server_task: tokio::task::JoinHandle<()>, 38 | pub address: String, 39 | state: Arc, 40 | } 41 | 42 | impl EchoServer { 43 | pub async fn new() -> EchoServer { 44 | let mut rng = rand::thread_rng(); 45 | EchoServer::new_at_address(format!("127.0.0.1:{}", rng.gen_range(1024, 65535))).await 46 | } 47 | 48 | pub async fn new_at_address(bind_addr_string: String) -> EchoServer { 49 | let bind_addr = bind_addr_string.parse().unwrap(); 50 | // Create a one-shot channel that can be used to tell the server to shut down 51 | let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); 52 | 53 | // Start a separate server task 54 | let server_state = Arc::new(ServerState { 55 | requests_received: atomic::AtomicUsize::new(0), 56 | }); 57 | let server_task_state = server_state.clone(); 58 | let server_task = tokio::spawn(async move { 59 | let service = make_service_fn(|_| { 60 | let server_task_state = server_task_state.clone(); 61 | async move { 62 | Ok::<_, hyper::Error>(service_fn(move |req| { 63 | let server_task_state = server_task_state.clone(); 64 | echo(server_task_state, req) 65 | })) 66 | } 67 | }); 68 | let server = hyper::Server::bind(&bind_addr) 69 | .serve(service) 70 | .with_graceful_shutdown(async { 71 | shutdown_rx.await.ok(); 72 | }); 73 | // Start serving and wait for the server to exit 74 | if let Err(e) = server.await { 75 | log::error!("Error in EchoServer: {}", e); 76 | } 77 | }); 78 | 79 | EchoServer { 80 | shutdown_signal_sender: shutdown_tx, 81 | server_task, 82 | state: server_state, 83 | address: bind_addr_string, 84 | } 85 | } 86 | } 87 | 88 | #[async_trait] 89 | impl Server for EchoServer { 90 | async fn stop(self: Box) -> usize { 91 | // Tell the hyper server to stop 92 | let _ = self.shutdown_signal_sender.send(()); 93 | // Wait for it to stop 94 | self.server_task 95 | .await 96 | .expect("ErrorServer server task panicked"); 97 | 98 | self.state.requests_received.load(atomic::Ordering::SeqCst) 99 | } 100 | 101 | fn address(&self) -> String { 102 | self.address.clone() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /proj-2/balancebeam/tests/common/error_server.rs: -------------------------------------------------------------------------------- 1 | use crate::common::server::Server; 2 | use async_trait::async_trait; 3 | use hyper::service::{make_service_fn, service_fn}; 4 | use hyper::{Body, Response}; 5 | use rand::Rng; 6 | use std::sync::{atomic, Arc}; 7 | use tokio::sync::oneshot; 8 | 9 | #[derive(Debug)] 10 | struct ServerState { 11 | pub requests_received: atomic::AtomicUsize, 12 | } 13 | 14 | #[allow(dead_code)] 15 | async fn return_error() -> Result, hyper::Error> { 16 | Ok(Response::builder() 17 | .status(http::StatusCode::INTERNAL_SERVER_ERROR) 18 | .body(Body::empty()) 19 | .unwrap()) 20 | } 21 | 22 | pub struct ErrorServer { 23 | shutdown_signal_sender: oneshot::Sender<()>, 24 | server_task: tokio::task::JoinHandle<()>, 25 | pub address: String, 26 | state: Arc, 27 | } 28 | 29 | impl ErrorServer { 30 | #[allow(dead_code)] 31 | pub async fn new() -> ErrorServer { 32 | let mut rng = rand::thread_rng(); 33 | ErrorServer::new_at_address(format!("127.0.0.1:{}", rng.gen_range(1024, 65535))).await 34 | } 35 | 36 | #[allow(dead_code)] 37 | pub async fn new_at_address(bind_addr_string: String) -> ErrorServer { 38 | let bind_addr = bind_addr_string.parse().unwrap(); 39 | // Create a one-shot channel that can be used to tell the server to shut down 40 | let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); 41 | 42 | // Start a separate server task 43 | let server_state = Arc::new(ServerState { 44 | requests_received: atomic::AtomicUsize::new(0), 45 | }); 46 | let server_task_state = server_state.clone(); 47 | let server_task = tokio::spawn(async move { 48 | let service = make_service_fn(|_| { 49 | let server_task_state = server_task_state.clone(); 50 | async move { 51 | Ok::<_, hyper::Error>(service_fn(move |_req| { 52 | server_task_state 53 | .requests_received 54 | .fetch_add(1, atomic::Ordering::SeqCst); 55 | return_error() 56 | })) 57 | } 58 | }); 59 | let server = hyper::Server::bind(&bind_addr) 60 | .serve(service) 61 | .with_graceful_shutdown(async { 62 | shutdown_rx.await.ok(); 63 | }); 64 | // Start serving and wait for the server to exit 65 | if let Err(e) = server.await { 66 | log::error!("Error in ErrorServer: {}", e); 67 | } 68 | }); 69 | 70 | ErrorServer { 71 | shutdown_signal_sender: shutdown_tx, 72 | server_task, 73 | state: server_state, 74 | address: bind_addr_string, 75 | } 76 | } 77 | } 78 | 79 | #[async_trait] 80 | impl Server for ErrorServer { 81 | async fn stop(self: Box) -> usize { 82 | // Tell the hyper server to stop 83 | let _ = self.shutdown_signal_sender.send(()); 84 | // Wait for it to stop 85 | self.server_task 86 | .await 87 | .expect("ErrorServer server task panicked"); 88 | 89 | self.state.requests_received.load(atomic::Ordering::SeqCst) 90 | } 91 | 92 | fn address(&self) -> String { 93 | self.address.clone() 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /proj-2/balancebeam/tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | mod balancebeam; 2 | mod echo_server; 3 | mod error_server; 4 | mod server; 5 | 6 | use std::sync; 7 | 8 | pub use balancebeam::BalanceBeam; 9 | pub use echo_server::EchoServer; 10 | pub use error_server::ErrorServer; 11 | pub use server::Server; 12 | 13 | static INIT_TESTS: sync::Once = sync::Once::new(); 14 | 15 | pub fn init_logging() { 16 | INIT_TESTS.call_once(|| { 17 | pretty_env_logger::formatted_builder() 18 | .is_test(true) 19 | .parse_filters("info") 20 | .init(); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /proj-2/balancebeam/tests/common/server.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | 3 | #[async_trait] 4 | pub trait Server { 5 | async fn stop(self: Box) -> usize; 6 | fn address(&self) -> String; 7 | } 8 | -------------------------------------------------------------------------------- /week1/.gitignore: -------------------------------------------------------------------------------- 1 | # Vim swap files 2 | .*.swp 3 | 4 | # Mac 5 | .DS_Store 6 | 7 | # Rust build artifacts 8 | /part-1-hello-world/target/ 9 | /part-2-warmup/target/ 10 | /part-3-hangman/target/ 11 | 12 | Cargo.lock 13 | 14 | # These are backup files generated by rustfmt 15 | **/*.rs.bk 16 | -------------------------------------------------------------------------------- /week1/part-1-hello-world/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello-world" 3 | version = "0.1.0" 4 | authors = ["Ryan Eberhardt "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /week1/part-1-hello-world/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /week1/part-2-warmup/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "warmup" 3 | version = "0.1.0" 4 | authors = ["Ryan Eberhardt "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /week1/part-2-warmup/src/main.rs: -------------------------------------------------------------------------------- 1 | /* The following exercises were borrowed from Will Crichton's CS 242 Rust lab. */ 2 | 3 | use std::collections::HashSet; 4 | 5 | fn main() { 6 | println!("Hi! Try running \"cargo test\" to run tests."); 7 | } 8 | 9 | fn add_n(v: Vec, n: i32) -> Vec { 10 | unimplemented!() 11 | } 12 | 13 | fn add_n_inplace(v: &mut Vec, n: i32) { 14 | unimplemented!() 15 | } 16 | 17 | fn dedup(v: &mut Vec) { 18 | unimplemented!() 19 | } 20 | 21 | #[cfg(test)] 22 | mod test { 23 | use super::*; 24 | 25 | #[test] 26 | fn test_add_n() { 27 | assert_eq!(add_n(vec![1], 2), vec![3]); 28 | } 29 | 30 | #[test] 31 | fn test_add_n_inplace() { 32 | let mut v = vec![1]; 33 | add_n_inplace(&mut v, 2); 34 | assert_eq!(v, vec![3]); 35 | } 36 | 37 | #[test] 38 | fn test_dedup() { 39 | let mut v = vec![3, 1, 0, 1, 4, 4]; 40 | dedup(&mut v); 41 | assert_eq!(v, vec![3, 1, 0, 4]); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /week1/part-3-hangman/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hangman" 3 | version = "0.1.0" 4 | authors = ["Armin Namavari "] 5 | 6 | [dependencies] 7 | rand = "0.6.0" -------------------------------------------------------------------------------- /week1/part-3-hangman/src/main.rs: -------------------------------------------------------------------------------- 1 | // Simple Hangman Program 2 | // User gets five incorrect guesses 3 | // Word chosen randomly from words.txt 4 | // Inspiration from: https://doc.rust-lang.org/book/ch02-00-guessing-game-tutorial.html 5 | // This assignment will introduce you to some fundamental syntax in Rust: 6 | // - variable declaration 7 | // - string manipulation 8 | // - conditional statements 9 | // - loops 10 | // - vectors 11 | // - files 12 | // - user input 13 | // We've tried to limit/hide Rust's quirks since we'll discuss those details 14 | // more in depth in the coming lectures. 15 | extern crate rand; 16 | use rand::Rng; 17 | use std::fs; 18 | use std::io; 19 | use std::io::Write; 20 | 21 | const NUM_INCORRECT_GUESSES: u32 = 5; 22 | const WORDS_PATH: &str = "words.txt"; 23 | 24 | fn pick_a_random_word() -> String { 25 | let file_string = fs::read_to_string(WORDS_PATH).expect("Unable to read file."); 26 | let words: Vec<&str> = file_string.split('\n').collect(); 27 | String::from(words[rand::thread_rng().gen_range(0, words.len())].trim()) 28 | } 29 | 30 | fn main() { 31 | let secret_word = pick_a_random_word(); 32 | // Note: given what you know about Rust so far, it's easier to pull characters out of a 33 | // vector than it is to pull them out of a string. You can get the ith character of 34 | // secret_word by doing secret_word_chars[i]. 35 | let secret_word_chars: Vec = secret_word.chars().collect(); 36 | // Uncomment for debugging: 37 | // println!("random word: {}", secret_word); 38 | 39 | // Your code here! :) 40 | } 41 | -------------------------------------------------------------------------------- /week1/part-3-hangman/words.txt: -------------------------------------------------------------------------------- 1 | immutable 2 | borrowed 3 | shared 4 | reference 5 | aluminum 6 | oxidation 7 | lobster 8 | starfish 9 | crawfish -------------------------------------------------------------------------------- /week1/part-4.txt: -------------------------------------------------------------------------------- 1 | Survey code: 2 | 3 | Thanks for being awesome! We appreciate you! 4 | -------------------------------------------------------------------------------- /week2/.gitignore: -------------------------------------------------------------------------------- 1 | # Vim swap files 2 | .*.swp 3 | 4 | # Mac 5 | .DS_Store 6 | 7 | # Rust build artifacts 8 | /*/target/ 9 | 10 | /*/Cargo.lock 11 | 12 | # These are backup files generated by rustfmt 13 | **/*.rs.bk 14 | -------------------------------------------------------------------------------- /week2/ownership.txt: -------------------------------------------------------------------------------- 1 | Example 1: 2 | ``` 3 | fn main() { 4 | let mut s = String::from("hello"); 5 | let ref1 = &s; 6 | let ref2 = &ref1; 7 | let ref3 = &ref2; 8 | s = String::from("goodbye"); 9 | println!("{}", ref3.to_uppercase()); 10 | } 11 | ``` 12 | 13 | 14 | 15 | Example 2: 16 | ``` 17 | fn drip_drop() -> &String { 18 | let s = String::from("hello world!"); 19 | return &s; 20 | } 21 | ``` 22 | 23 | 24 | 25 | Example 3: 26 | ``` 27 | fn main() { 28 | let s1 = String::from("hello"); 29 | let mut v = Vec::new(); 30 | v.push(s1); 31 | let s2: String = v[0]; 32 | println!("{}", s2); 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /week2/rdiff/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rdiff" 3 | version = "0.1.0" 4 | authors = ["Armin Namavari "] 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /week2/rdiff/handout-a.txt: -------------------------------------------------------------------------------- 1 | This week's exercises will continue easing you into Rust and will feature some 2 | components of object-oriented Rust that we're covering this week. You'll be 3 | writing some programs that have more sophisticated logic that what you saw last 4 | with last week's exercises. The first exercise is a warm-up: to implement the wc 5 | command line utility in Rust. The second exercise is more challenging: to 6 | implement the diff utility. In order to do so, you'll first find the longest 7 | common subsequence (LCS) of lines between the two files and use this to inform 8 | how you display your diff. 9 | -------------------------------------------------------------------------------- /week2/rdiff/handout-b.txt: -------------------------------------------------------------------------------- 1 | You'll be learning and practicing a lot of new Rust concepts this week by 2 | writing some programs that have more sophisticated logic that what you saw last 3 | with last week's exercises. The first exercise is a warm-up: to implement the wc 4 | command line utility in Rust. The second exercise is more challenging: to 5 | implement the diff utility. In order to do so, you'll first find the longest 6 | subsequence that is common. 7 | -------------------------------------------------------------------------------- /week2/rdiff/simple-a.txt: -------------------------------------------------------------------------------- 1 | a 2 | b 3 | c 4 | d 5 | e 6 | -------------------------------------------------------------------------------- /week2/rdiff/simple-b.txt: -------------------------------------------------------------------------------- 1 | a 2 | added 3 | b 4 | c 5 | added 6 | d 7 | added 8 | e 9 | -------------------------------------------------------------------------------- /week2/rdiff/src/grid.rs: -------------------------------------------------------------------------------- 1 | // Grid implemented as flat vector 2 | pub struct Grid { 3 | num_rows: usize, 4 | num_cols: usize, 5 | elems: Vec, 6 | } 7 | 8 | impl Grid { 9 | /// Returns a Grid of the specified size, with all elements pre-initialized to zero. 10 | pub fn new(num_rows: usize, num_cols: usize) -> Grid { 11 | Grid { 12 | num_rows: num_rows, 13 | num_cols: num_cols, 14 | // This syntax uses the vec! macro to create a vector of zeros, initialized to a 15 | // specific length 16 | // https://stackoverflow.com/a/29530932 17 | elems: vec![0; num_rows * num_cols], 18 | } 19 | } 20 | 21 | pub fn size(&self) -> (usize, usize) { 22 | (self.num_rows, self.num_cols) 23 | } 24 | 25 | /// Returns the element at the specified location. If the location is out of bounds, returns 26 | /// None. 27 | /// 28 | /// Note to students: this function also could have returned Result. It's a matter of taste in 29 | /// how you define the semantics; many languages raise exceptions for out-of-bounds exceptions, 30 | /// but others argue that makes code needlessly complex. Here, we decided to return Option to 31 | /// give you more practice with Option :) and because this similar library returns Option: 32 | /// https://docs.rs/array2d/0.2.1/array2d/struct.Array2D.html 33 | #[allow(unused)] // TODO: delete this line when you implement this function 34 | pub fn get(&self, row: usize, col: usize) -> Option { 35 | unimplemented!(); 36 | // Be sure to delete the #[allow(unused)] line above 37 | } 38 | 39 | /// Sets the element at the specified location to the specified value. If the location is out 40 | /// of bounds, returns Err with an error message. 41 | #[allow(unused)] // TODO: delete this line when you implement this function 42 | pub fn set(&mut self, row: usize, col: usize, val: usize) -> Result<(), &'static str> { 43 | unimplemented!(); 44 | // Be sure to delete the #[allow(unused)] line above 45 | } 46 | 47 | /// Prints a visual representation of the grid. You can use this for debugging. 48 | pub fn display(&self) { 49 | for row in 0..self.num_rows { 50 | let mut line = String::new(); 51 | for col in 0..self.num_cols { 52 | line.push_str(&format!("{}, ", self.get(row, col).unwrap())); 53 | } 54 | println!("{}", line); 55 | } 56 | } 57 | 58 | /// Resets all the elements to zero. 59 | pub fn clear(&mut self) { 60 | for i in self.elems.iter_mut() { 61 | *i = 0; 62 | } 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod test { 68 | use super::*; 69 | 70 | #[test] 71 | fn test_grid() { 72 | let n_rows = 4; 73 | let n_cols = 3; 74 | let mut grid = Grid::new(n_rows, n_cols); 75 | 76 | // Initialize grid 77 | for r in 0..n_rows { 78 | for c in 0..n_cols { 79 | assert!( 80 | grid.set(r, c, r * n_cols + c).is_ok(), 81 | "Grid::set returned Err even though the provided bounds are valid!" 82 | ); 83 | } 84 | } 85 | 86 | // Note: you need to run "cargo test -- --nocapture" in order to see output printed 87 | println!("Grid contents:"); 88 | grid.display(); 89 | 90 | // Make sure the values are what we expect 91 | for r in 0..n_rows { 92 | for c in 0..n_cols { 93 | assert!( 94 | grid.get(r, c).is_some(), 95 | "Grid::get returned None even though the provided bounds are valid!" 96 | ); 97 | assert_eq!(grid.get(r, c).unwrap(), r * n_cols + c); 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /week2/rdiff/src/main.rs: -------------------------------------------------------------------------------- 1 | use grid::Grid; // For lcs() 2 | use std::env; 3 | use std::fs::File; // For read_file_lines() 4 | use std::io::{self, BufRead}; // For read_file_lines() 5 | use std::process; 6 | 7 | pub mod grid; 8 | 9 | /// Reads the file at the supplied path, and returns a vector of strings. 10 | #[allow(unused)] // TODO: delete this line when you implement this function 11 | fn read_file_lines(filename: &String) -> Result, io::Error> { 12 | unimplemented!(); 13 | // Be sure to delete the #[allow(unused)] line above 14 | } 15 | 16 | #[allow(unused)] // TODO: delete this line when you implement this function 17 | fn lcs(seq1: &Vec, seq2: &Vec) -> Grid { 18 | // Note: Feel free to use unwrap() in this code, as long as you're basically certain it'll 19 | // never happen. Conceptually, unwrap() is justified here, because there's not really any error 20 | // condition you're watching out for (i.e. as long as your code is written correctly, nothing 21 | // external can go wrong that we would want to handle in higher-level functions). The unwrap() 22 | // calls act like having asserts in C code, i.e. as guards against programming error. 23 | unimplemented!(); 24 | // Be sure to delete the #[allow(unused)] line above 25 | } 26 | 27 | #[allow(unused)] // TODO: delete this line when you implement this function 28 | fn print_diff(lcs_table: &Grid, lines1: &Vec, lines2: &Vec, i: usize, j: usize) { 29 | unimplemented!(); 30 | // Be sure to delete the #[allow(unused)] line above 31 | } 32 | 33 | #[allow(unused)] // TODO: delete this line when you implement this function 34 | fn main() { 35 | let args: Vec = env::args().collect(); 36 | if args.len() < 3 { 37 | println!("Too few arguments."); 38 | process::exit(1); 39 | } 40 | let filename1 = &args[1]; 41 | let filename2 = &args[2]; 42 | 43 | unimplemented!(); 44 | // Be sure to delete the #[allow(unused)] line above 45 | } 46 | 47 | #[cfg(test)] 48 | mod test { 49 | use super::*; 50 | 51 | #[test] 52 | fn test_read_file_lines() { 53 | let lines_result = read_file_lines(&String::from("handout-a.txt")); 54 | assert!(lines_result.is_ok()); 55 | let lines = lines_result.unwrap(); 56 | assert_eq!(lines.len(), 8); 57 | assert_eq!( 58 | lines[0], 59 | "This week's exercises will continue easing you into Rust and will feature some" 60 | ); 61 | } 62 | 63 | #[test] 64 | fn test_lcs() { 65 | let mut expected = Grid::new(5, 4); 66 | expected.set(1, 1, 1).unwrap(); 67 | expected.set(1, 2, 1).unwrap(); 68 | expected.set(1, 3, 1).unwrap(); 69 | expected.set(2, 1, 1).unwrap(); 70 | expected.set(2, 2, 1).unwrap(); 71 | expected.set(2, 3, 2).unwrap(); 72 | expected.set(3, 1, 1).unwrap(); 73 | expected.set(3, 2, 1).unwrap(); 74 | expected.set(3, 3, 2).unwrap(); 75 | expected.set(4, 1, 1).unwrap(); 76 | expected.set(4, 2, 2).unwrap(); 77 | expected.set(4, 3, 2).unwrap(); 78 | 79 | println!("Expected:"); 80 | expected.display(); 81 | let result = lcs( 82 | &"abcd".chars().map(|c| c.to_string()).collect(), 83 | &"adb".chars().map(|c| c.to_string()).collect(), 84 | ); 85 | println!("Got:"); 86 | result.display(); 87 | assert_eq!(result.size(), expected.size()); 88 | for row in 0..expected.size().0 { 89 | for col in 0..expected.size().1 { 90 | assert_eq!(result.get(row, col), expected.get(row, col)); 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /week2/rwc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rwc" 3 | version = "0.1.0" 4 | authors = ["Ryan Eberhardt "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /week2/rwc/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::process; 3 | 4 | fn main() { 5 | let args: Vec = env::args().collect(); 6 | if args.len() < 2 { 7 | println!("Too few arguments."); 8 | process::exit(1); 9 | } 10 | let filename = &args[1]; 11 | // Your code here :) 12 | } 13 | -------------------------------------------------------------------------------- /week2/survey.txt: -------------------------------------------------------------------------------- 1 | Survey code: 2 | -------------------------------------------------------------------------------- /week3/.gitignore: -------------------------------------------------------------------------------- 1 | # Vim swap files 2 | .*.swp 3 | 4 | # Mac 5 | .DS_Store 6 | 7 | # Rust build artifacts 8 | /*/target/ 9 | 10 | /*/Cargo.lock 11 | 12 | # These are backup files generated by rustfmt 13 | **/*.rs.bk 14 | -------------------------------------------------------------------------------- /week3/inspect-fds/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "inspect-fds" 3 | version = "0.1.0" 4 | authors = ["Ryan Eberhardt "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | nix = "0.17.0" 11 | regex = "1.3.7" 12 | -------------------------------------------------------------------------------- /week3/inspect-fds/Makefile: -------------------------------------------------------------------------------- 1 | SRCS = $(wildcard *.c) 2 | PROGS = $(patsubst %.c,%,$(SRCS)) 3 | 4 | all: $(PROGS) 5 | 6 | %: %.c 7 | $(CC) $(CFLAGS) -o $@ $< 8 | 9 | clean: 10 | rm -f $(PROGS) 11 | -------------------------------------------------------------------------------- /week3/inspect-fds/multi_pipe_test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main() { 5 | int fds1[2]; 6 | int fds2[2]; 7 | pipe(fds1); 8 | pipe(fds2); 9 | pid_t pid = fork(); 10 | if (pid == 0) { 11 | dup2(fds1[0], STDIN_FILENO); 12 | dup2(fds2[1], STDOUT_FILENO); 13 | close(fds1[0]); 14 | close(fds1[1]); 15 | close(fds2[0]); 16 | close(fds2[1]); 17 | sleep(2); 18 | return 0; 19 | } 20 | close(fds1[0]); 21 | close(fds2[1]); 22 | waitpid(pid, NULL, 0); 23 | return 0; 24 | } 25 | -------------------------------------------------------------------------------- /week3/inspect-fds/nothing.c: -------------------------------------------------------------------------------- 1 | int main() {} 2 | -------------------------------------------------------------------------------- /week3/inspect-fds/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | mod open_file; 4 | mod process; 5 | mod ps_utils; 6 | 7 | fn main() { 8 | let args: Vec = env::args().collect(); 9 | if args.len() != 2 { 10 | println!("Usage: {} ", args[0]); 11 | std::process::exit(1); 12 | } 13 | #[allow(unused)] // TODO: delete this line for Milestone 1 14 | let target = &args[1]; 15 | 16 | // TODO: Milestone 1: Get the target Process using psutils::get_target() 17 | unimplemented!(); 18 | } 19 | 20 | #[cfg(test)] 21 | mod test { 22 | use std::process::{Child, Command}; 23 | 24 | fn start_c_program(program: &str) -> Child { 25 | Command::new(program) 26 | .spawn() 27 | .expect(&format!("Could not find {}. Have you run make?", program)) 28 | } 29 | 30 | #[test] 31 | fn test_exit_status_valid_target() { 32 | let mut subprocess = start_c_program("./multi_pipe_test"); 33 | assert_eq!( 34 | Command::new("./target/debug/inspect-fds") 35 | .args(&[&subprocess.id().to_string()]) 36 | .status() 37 | .expect("Could not find target/debug/inspect-fds. Is the binary compiled?") 38 | .code() 39 | .expect("Program was unexpectedly terminated by a signal"), 40 | 0, 41 | "We expected the program to exit normally, but it didn't." 42 | ); 43 | let _ = subprocess.kill(); 44 | } 45 | 46 | #[test] 47 | fn test_exit_status_invalid_target() { 48 | assert_eq!( 49 | Command::new("./target/debug/inspect-fds") 50 | .args(&["./nonexistent"]) 51 | .status() 52 | .expect("Could not find target/debug/inspect-fds. Is the binary compiled?") 53 | .code() 54 | .expect("Program was unexpectedly terminated by a signal"), 55 | 1, 56 | "Program exited with unexpected return code. Make sure you handle the case where \ 57 | ps_utils::get_target returns None and print an error message and return status \ 58 | 1." 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /week3/inspect-fds/src/open_file.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use std::collections::hash_map::DefaultHasher; 3 | use std::hash::{Hash, Hasher}; 4 | #[allow(unused_imports)] // TODO: delete this line for Milestone 4 5 | use std::{fmt, fs}; 6 | 7 | #[allow(unused)] // TODO: delete this line for Milestone 4 8 | const O_WRONLY: usize = 00000001; 9 | #[allow(unused)] // TODO: delete this line for Milestone 4 10 | const O_RDWR: usize = 00000002; 11 | #[allow(unused)] // TODO: delete this line for Milestone 4 12 | const COLORS: [&str; 6] = [ 13 | "\x1B[38;5;9m", 14 | "\x1B[38;5;10m", 15 | "\x1B[38;5;11m", 16 | "\x1B[38;5;12m", 17 | "\x1B[38;5;13m", 18 | "\x1B[38;5;14m", 19 | ]; 20 | #[allow(unused)] // TODO: delete this line for Milestone 4 21 | const CLEAR_COLOR: &str = "\x1B[0m"; 22 | 23 | /// This enum can be used to represent whether a file is read-only, write-only, or read/write. An 24 | /// enum is basically a value that can be one of some number of "things." 25 | #[allow(unused)] // TODO: delete this line for Milestone 4 26 | #[derive(Debug, Clone, PartialEq)] 27 | pub enum AccessMode { 28 | Read, 29 | Write, 30 | ReadWrite, 31 | } 32 | 33 | impl fmt::Display for AccessMode { 34 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 35 | // Match operators are very commonly used with enums in Rust. They function similar to 36 | // switch statements in other languages (but can be more expressive). 37 | match self { 38 | AccessMode::Read => write!(f, "{}", "read"), 39 | AccessMode::Write => write!(f, "{}", "write"), 40 | AccessMode::ReadWrite => write!(f, "{}", "read/write"), 41 | } 42 | } 43 | } 44 | 45 | /// Stores information about an open file on the system. Since the Linux kernel doesn't really 46 | /// expose much information about the open file table to userspace (cplayground uses a modified 47 | /// kernel), this struct contains info from both the open file table and the vnode table. 48 | #[derive(Debug, Clone, PartialEq)] 49 | pub struct OpenFile { 50 | pub name: String, 51 | pub cursor: usize, 52 | pub access_mode: AccessMode, 53 | } 54 | 55 | impl OpenFile { 56 | #[allow(unused)] // TODO: delete this line for Milestone 4 57 | pub fn new(name: String, cursor: usize, access_mode: AccessMode) -> OpenFile { 58 | OpenFile { 59 | name, 60 | cursor, 61 | access_mode, 62 | } 63 | } 64 | 65 | /// This function takes a path of an open file and returns a more human-friendly name for that 66 | /// file. 67 | /// 68 | /// * For regular files, this will simply return the supplied path. 69 | /// * For terminals (files starting with /dev/pts), this will return "". 70 | /// * For pipes (filenames formatted like pipe:[pipenum]), this will return "". 71 | #[allow(unused)] // TODO: delete this line for Milestone 4 72 | fn path_to_name(path: &str) -> String { 73 | if path.starts_with("/dev/pts/") { 74 | String::from("") 75 | } else if path.starts_with("pipe:[") && path.ends_with("]") { 76 | let pipe_num = &path[path.find('[').unwrap() + 1..path.find(']').unwrap()]; 77 | format!("", pipe_num) 78 | } else { 79 | String::from(path) 80 | } 81 | } 82 | 83 | /// This file takes the contents of /proc/{pid}/fdinfo/{fdnum} for some file descriptor and 84 | /// extracts the cursor position of that file descriptor (technically, the position of the 85 | /// open file table entry that the fd points to) using a regex. It returns None if the cursor 86 | /// couldn't be found in the fdinfo text. 87 | #[allow(unused)] // TODO: delete this line for Milestone 4 88 | fn parse_cursor(fdinfo: &str) -> Option { 89 | // Regex::new will return an Error if there is a syntactical error in our regular 90 | // expression. We call unwrap() here because that indicates there's an obvious problem with 91 | // our code, but if this were code for a critical system that needs to not crash, then 92 | // we would want to return an Error instead. 93 | let re = Regex::new(r"pos:\s*(\d+)").unwrap(); 94 | Some( 95 | re.captures(fdinfo)? 96 | .get(1)? 97 | .as_str() 98 | .parse::() 99 | .ok()?, 100 | ) 101 | } 102 | 103 | /// This file takes the contents of /proc/{pid}/fdinfo/{fdnum} for some file descriptor and 104 | /// extracts the access mode for that open file using the "flags:" field contained in the 105 | /// fdinfo text. It returns None if the "flags" field couldn't be found. 106 | #[allow(unused)] // TODO: delete this line for Milestone 4 107 | fn parse_access_mode(fdinfo: &str) -> Option { 108 | // Regex::new will return an Error if there is a syntactical error in our regular 109 | // expression. We call unwrap() here because that indicates there's an obvious problem with 110 | // our code, but if this were code for a critical system that needs to not crash, then 111 | // we would want to return an Error instead. 112 | let re = Regex::new(r"flags:\s*(\d+)").unwrap(); 113 | // Extract the flags field and parse it as octal 114 | let flags = usize::from_str_radix(re.captures(fdinfo)?.get(1)?.as_str(), 8).ok()?; 115 | if flags & O_WRONLY > 0 { 116 | Some(AccessMode::Write) 117 | } else if flags & O_RDWR > 0 { 118 | Some(AccessMode::ReadWrite) 119 | } else { 120 | Some(AccessMode::Read) 121 | } 122 | } 123 | 124 | /// Given a specified process and fd number, this function reads /proc/{pid}/fd/{fdnum} and 125 | /// /proc/{pid}/fdinfo/{fdnum} to populate an OpenFile struct. It returns None if the pid or fd 126 | /// are invalid, or if necessary information is unavailable. 127 | /// 128 | /// (Note: whether this function returns Option or Result is a matter of style and context. 129 | /// Some people might argue that you should return Result, so that you have finer grained 130 | /// control over possible things that could go wrong, e.g. you might want to handle things 131 | /// differently if this fails because the process doesn't have a specified fd, vs if it 132 | /// fails because it failed to read a /proc file. However, that significantly increases 133 | /// complexity of error handling. In our case, this does not need to be a super robust 134 | /// program and we don't need to do fine-grained error handling, so returning Option is a 135 | /// simple way to indicate that "hey, we weren't able to get the necessary information" 136 | /// without making a big deal of it.) 137 | #[allow(unused)] // TODO: delete this line for Milestone 4 138 | pub fn from_fd(pid: usize, fd: usize) -> Option { 139 | // TODO: implement for Milestone 4 140 | unimplemented!(); 141 | } 142 | 143 | /// This function returns the OpenFile's name with ANSI escape codes included to colorize 144 | /// pipe names. It hashes the pipe name so that the same pipe name will always result in the 145 | /// same color. This is useful for making program output more readable, since a user can 146 | /// quickly see all the fds that point to a particular pipe. 147 | #[allow(unused)] // TODO: delete this line for Milestone 5 148 | pub fn colorized_name(&self) -> String { 149 | if self.name.starts_with(" Child { 168 | Command::new(program) 169 | .spawn() 170 | .expect(&format!("Could not find {}. Have you run make?", program)) 171 | } 172 | 173 | #[test] 174 | fn test_openfile_from_fd() { 175 | let mut test_subprocess = start_c_program("./multi_pipe_test"); 176 | let process = ps_utils::get_target("multi_pipe_test").unwrap().unwrap(); 177 | // Get file descriptor 0, which should point to the terminal 178 | let open_file = OpenFile::from_fd(process.pid, 0) 179 | .expect("Expected to get open file data for multi_pipe_test, but OpenFile::from_fd returned None"); 180 | assert_eq!(open_file.name, ""); 181 | assert_eq!(open_file.cursor, 0); 182 | assert_eq!(open_file.access_mode, AccessMode::ReadWrite); 183 | let _ = test_subprocess.kill(); 184 | } 185 | 186 | #[test] 187 | fn test_openfile_from_fd_invalid_fd() { 188 | let mut test_subprocess = start_c_program("./multi_pipe_test"); 189 | let process = ps_utils::get_target("multi_pipe_test").unwrap().unwrap(); 190 | // Get file descriptor 30, which should be invalid 191 | assert!( 192 | OpenFile::from_fd(process.pid, 30).is_none(), 193 | "Expected None because file descriptor 30 is invalid" 194 | ); 195 | let _ = test_subprocess.kill(); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /week3/inspect-fds/src/process.rs: -------------------------------------------------------------------------------- 1 | use crate::open_file::OpenFile; 2 | #[allow(unused)] // TODO: delete this line for Milestone 3 3 | use std::fs; 4 | 5 | #[derive(Debug, Clone, PartialEq)] 6 | pub struct Process { 7 | pub pid: usize, 8 | pub ppid: usize, 9 | pub command: String, 10 | } 11 | 12 | impl Process { 13 | #[allow(unused)] // TODO: delete this line for Milestone 1 14 | pub fn new(pid: usize, ppid: usize, command: String) -> Process { 15 | Process { pid, ppid, command } 16 | } 17 | 18 | /// This function returns a list of file descriptor numbers for this Process, if that 19 | /// information is available (it will return None if the information is unavailable). The 20 | /// information will commonly be unavailable if the process has exited. (Zombie processes 21 | /// still have a pid, but their resources have already been freed, including the file 22 | /// descriptor table.) 23 | #[allow(unused)] // TODO: delete this line for Milestone 3 24 | pub fn list_fds(&self) -> Option> { 25 | // TODO: implement for Milestone 3 26 | unimplemented!(); 27 | } 28 | 29 | /// This function returns a list of (fdnumber, OpenFile) tuples, if file descriptor 30 | /// information is available (it returns None otherwise). The information is commonly 31 | /// unavailable if the process has already exited. 32 | #[allow(unused)] // TODO: delete this line for Milestone 4 33 | pub fn list_open_files(&self) -> Option> { 34 | let mut open_files = vec![]; 35 | for fd in self.list_fds()? { 36 | open_files.push((fd, OpenFile::from_fd(self.pid, fd)?)); 37 | } 38 | Some(open_files) 39 | } 40 | } 41 | 42 | #[cfg(test)] 43 | mod test { 44 | use crate::ps_utils; 45 | use std::process::{Child, Command}; 46 | 47 | fn start_c_program(program: &str) -> Child { 48 | Command::new(program) 49 | .spawn() 50 | .expect(&format!("Could not find {}. Have you run make?", program)) 51 | } 52 | 53 | #[test] 54 | fn test_list_fds() { 55 | let mut test_subprocess = start_c_program("./multi_pipe_test"); 56 | let process = ps_utils::get_target("multi_pipe_test").unwrap().unwrap(); 57 | assert_eq!( 58 | process 59 | .list_fds() 60 | .expect("Expected list_fds to find file descriptors, but it returned None"), 61 | vec![0, 1, 2, 4, 5] 62 | ); 63 | let _ = test_subprocess.kill(); 64 | } 65 | 66 | #[test] 67 | fn test_list_fds_zombie() { 68 | let mut test_subprocess = start_c_program("./nothing"); 69 | let process = ps_utils::get_target("nothing").unwrap().unwrap(); 70 | assert!( 71 | process.list_fds().is_none(), 72 | "Expected list_fds to return None for a zombie process" 73 | ); 74 | let _ = test_subprocess.kill(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /week3/inspect-fds/src/ps_utils.rs: -------------------------------------------------------------------------------- 1 | use crate::process::Process; 2 | use nix::unistd::getuid; 3 | use std::fmt; 4 | use std::process::Command; 5 | 6 | /// This enum represents the possible causes that an error might occur. It's useful because it 7 | /// allows a caller of an API to have fine-grained control over error handling based on the 8 | /// specifics of what went wrong. You'll find similar ideas in Rust libraries, such as std::io: 9 | /// https://doc.rust-lang.org/std/io/enum.ErrorKind.html However, you won't need to do anything 10 | /// with this (or like this) in your own code. 11 | #[derive(Debug)] 12 | pub enum Error { 13 | ExecutableError(std::io::Error), 14 | OutputFormatError(&'static str), 15 | } 16 | 17 | // Generate readable representations of Error 18 | impl fmt::Display for Error { 19 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 20 | match &self { 21 | Error::ExecutableError(err) => write!(f, "Error executing ps: {}", err), 22 | Error::OutputFormatError(err) => write!(f, "ps printed malformed output: {}", err), 23 | } 24 | } 25 | } 26 | 27 | // Make it possible to automatically convert std::io::Error to our Error type 28 | impl From for Error { 29 | fn from(error: std::io::Error) -> Error { 30 | Error::ExecutableError(error) 31 | } 32 | } 33 | 34 | // Make it possible to automatically convert std::string::FromUtf8Error to our Error type 35 | impl From for Error { 36 | fn from(_error: std::string::FromUtf8Error) -> Error { 37 | Error::OutputFormatError("Output is not utf-8") 38 | } 39 | } 40 | 41 | // Make it possible to automatically convert std::string::ParseIntError to our Error type 42 | impl From for Error { 43 | fn from(_error: std::num::ParseIntError) -> Error { 44 | Error::OutputFormatError("Error parsing integer") 45 | } 46 | } 47 | 48 | /// This function takes a line of ps output formatted with -o "pid= ppid= command=" and returns a 49 | /// Process struct initialized from the parsed output. 50 | /// 51 | /// Example line: 52 | /// " 578 577 emacs inode.c" 53 | #[allow(unused)] // TODO: delete this line for Milestone 1 54 | fn parse_ps_line(line: &str) -> Result { 55 | // ps doesn't output a very nice machine-readable output, so we do some wonky things here to 56 | // deal with variable amounts of whitespace. 57 | let mut remainder = line.trim(); 58 | let first_token_end = remainder 59 | .find(char::is_whitespace) 60 | .ok_or(Error::OutputFormatError("Missing second column"))?; 61 | let pid = remainder[0..first_token_end].parse::()?; 62 | remainder = remainder[first_token_end..].trim_start(); 63 | let second_token_end = remainder 64 | .find(char::is_whitespace) 65 | .ok_or(Error::OutputFormatError("Missing third column"))?; 66 | let ppid = remainder[0..second_token_end].parse::()?; 67 | remainder = remainder[second_token_end..].trim_start(); 68 | Ok(Process::new(pid, ppid, String::from(remainder))) 69 | } 70 | 71 | /// This function takes a pid and returns a Process struct for the specified process, or None if 72 | /// the specified pid doesn't exist. An Error is only returned if ps cannot be executed or 73 | /// produces unexpected output format. 74 | #[allow(unused)] // TODO: delete this line for Milestone 1 75 | fn get_process(pid: usize) -> Result, Error> { 76 | // Run ps to find the specified pid. We use the ? operator to return an Error if executing ps 77 | // fails, or if it returns non-utf-8 output. (The extra Error traits above are used to 78 | // automatically convert errors like std::io::Error or std::string::FromUtf8Error into our 79 | // custom error type.) 80 | let output = String::from_utf8( 81 | Command::new("ps") 82 | .args(&["--pid", &pid.to_string(), "-o", "pid= ppid= command="]) 83 | .output()? 84 | .stdout, 85 | )?; 86 | // Return Some if the process was found and output parsing succeeds, or None if ps produced no 87 | // output (indicating there is no matching process). Note the use of ? to propagate Error if an 88 | // error occured in parsing the output. 89 | if output.trim().len() > 0 { 90 | Ok(Some(parse_ps_line(output.trim())?)) 91 | } else { 92 | Ok(None) 93 | } 94 | } 95 | 96 | /// This function takes a pid and returns a list of Process structs for processes that have the 97 | /// specified pid as their parent process. An Error is returned if ps cannot be executed or 98 | /// produces unexpected output format. 99 | #[allow(unused)] // TODO: delete this line for Milestone 5 100 | pub fn get_child_processes(pid: usize) -> Result, Error> { 101 | let ps_output = Command::new("ps") 102 | .args(&["--ppid", &pid.to_string(), "-o", "pid= ppid= command="]) 103 | .output()?; 104 | let mut output = Vec::new(); 105 | for line in String::from_utf8(ps_output.stdout)?.lines() { 106 | output.push(parse_ps_line(line)?); 107 | } 108 | Ok(output) 109 | } 110 | 111 | /// This function takes a command name (e.g. "sort" or "./multi_pipe_test") and returns the first 112 | /// matching process's pid, or None if no matching process is found. It returns an Error if there 113 | /// is an error running pgrep or parsing pgrep's output. 114 | #[allow(unused)] // TODO: delete this line for Milestone 1 115 | fn get_pid_by_command_name(name: &str) -> Result, Error> { 116 | let output = String::from_utf8( 117 | Command::new("pgrep") 118 | .args(&["-xU", getuid().to_string().as_str(), name]) 119 | .output()? 120 | .stdout, 121 | )?; 122 | Ok(match output.lines().next() { 123 | Some(line) => Some(line.parse::()?), 124 | None => None, 125 | }) 126 | } 127 | 128 | /// This program finds a target process on the system. The specified query can either be a 129 | /// command name (e.g. "./subprocess_test") or a PID (e.g. "5612"). This function returns a 130 | /// Process struct if the specified process was found, None if no matching processes were found, or 131 | /// Error if an error was encountered in running ps or pgrep. 132 | #[allow(unused)] // TODO: delete this line for Milestone 1 133 | pub fn get_target(query: &str) -> Result, Error> { 134 | let pid_by_command = get_pid_by_command_name(query)?; 135 | if pid_by_command.is_some() { 136 | return get_process(pid_by_command.unwrap()); 137 | } 138 | // If searching for the query as a command name failed, let's see if it's a valid pid 139 | match query.parse() { 140 | Ok(pid) => return get_process(pid), 141 | Err(_) => return Ok(None), 142 | } 143 | } 144 | 145 | #[cfg(test)] 146 | mod test { 147 | use super::*; 148 | use std::process::Child; 149 | 150 | fn start_c_program(program: &str) -> Child { 151 | Command::new(program) 152 | .spawn() 153 | .expect(&format!("Could not find {}. Have you run make?", program)) 154 | } 155 | 156 | #[test] 157 | fn test_get_target_success() { 158 | let mut subprocess = start_c_program("./multi_pipe_test"); 159 | let found = get_target("multi_pipe_test") 160 | .expect("Passed valid \"multi_pipe_test\" to get_target, but it returned an error") 161 | .expect("Passed valid \"multi_pipe_test\" to get_target, but it returned None"); 162 | assert_eq!(found.command, "./multi_pipe_test"); 163 | let _ = subprocess.kill(); 164 | } 165 | 166 | #[test] 167 | fn test_get_target_invalid_command() { 168 | let found = get_target("asdflksadfasdf") 169 | .expect("get_target returned an error, even though ps and pgrep should be working"); 170 | assert!( 171 | found.is_none(), 172 | "Passed invalid target to get_target, but it returned Some" 173 | ); 174 | } 175 | 176 | #[test] 177 | fn test_get_target_invalid_pid() { 178 | let found = get_target("1234567890") 179 | .expect("get_target returned an error, even though ps and pgrep should be working"); 180 | assert!( 181 | found.is_none(), 182 | "Passed invalid target to get_target, but it returned Some" 183 | ); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /week3/inspect-fds/zombie_test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main() { 5 | int fds[2]; 6 | pipe(fds); 7 | pid_t pid = fork(); 8 | if (pid == 0) { 9 | return 0; 10 | } 11 | close(fds[0]); 12 | sleep(2); 13 | waitpid(pid, NULL, 0); 14 | return 0; 15 | } 16 | -------------------------------------------------------------------------------- /week3/linked_list/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "linked_list" 3 | version = "0.1.0" 4 | authors = ["Armin Namavari "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /week3/linked_list/src/linked_list.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::option::Option; 3 | 4 | pub struct LinkedList { 5 | head: Option>, 6 | size: usize, 7 | } 8 | 9 | struct Node { 10 | value: u32, 11 | next: Option>, 12 | } 13 | 14 | impl Node { 15 | pub fn new(value: u32, next: Option>) -> Node { 16 | Node {value: value, next: next} 17 | } 18 | } 19 | 20 | impl LinkedList { 21 | pub fn new() -> LinkedList { 22 | LinkedList {head: None, size: 0} 23 | } 24 | 25 | pub fn get_size(&self) -> usize { 26 | self.size 27 | } 28 | 29 | pub fn is_empty(&self) -> bool { 30 | self.get_size() == 0 31 | } 32 | 33 | pub fn push_front(&mut self, value: u32) { 34 | let new_node: Box = Box::new(Node::new(value, self.head.take())); 35 | self.head = Some(new_node); 36 | self.size += 1; 37 | } 38 | 39 | pub fn pop_front(&mut self) -> Option { 40 | let node: Box = self.head.take()?; 41 | self.head = node.next; 42 | self.size -= 1; 43 | Some(node.value) 44 | } 45 | } 46 | 47 | 48 | impl fmt::Display for LinkedList { 49 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 50 | let mut current: &Option> = &self.head; 51 | let mut result = String::new(); 52 | loop { 53 | match current { 54 | Some(node) => { 55 | result = format!("{} {}", result, node.value); 56 | current = &node.next; 57 | }, 58 | None => break, 59 | } 60 | } 61 | write!(f, "{}", result) 62 | } 63 | } 64 | 65 | impl Drop for LinkedList { 66 | fn drop(&mut self) { 67 | let mut current = self.head.take(); 68 | while let Some(mut node) = current { 69 | current = node.next.take(); 70 | } 71 | } 72 | } 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /week3/linked_list/src/main.rs: -------------------------------------------------------------------------------- 1 | use linked_list::LinkedList; 2 | pub mod linked_list; 3 | 4 | fn main() { 5 | let mut list: LinkedList = LinkedList::new(); 6 | assert!(list.is_empty()); 7 | assert_eq!(list.get_size(), 0); 8 | for i in 1..12 { 9 | list.push_front(i); 10 | } 11 | println!("{}", list); 12 | println!("list size: {}", list.get_size()); 13 | println!("top element: {}", list.pop_front().unwrap()); 14 | println!("{}", list); 15 | println!("size: {}", list.get_size()); 16 | println!("{}", list.to_string()); // ToString impl for anything impl Display 17 | 18 | // If you implement iterator trait: 19 | //for val in &list { 20 | // println!("{}", val); 21 | //} 22 | } 23 | -------------------------------------------------------------------------------- /week3/survey.txt: -------------------------------------------------------------------------------- 1 | Survey code: 2 | -------------------------------------------------------------------------------- /week5/.gitignore: -------------------------------------------------------------------------------- 1 | # Vim swap files 2 | .*.swp 3 | 4 | # Mac 5 | .DS_Store 6 | 7 | # Rust build artifacts 8 | /*/target/ 9 | 10 | /*/Cargo.lock 11 | 12 | # These are backup files generated by rustfmt 13 | **/*.rs.bk 14 | -------------------------------------------------------------------------------- /week5/farm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "farm" 3 | version = "0.1.0" 4 | authors = ["Ryan Eberhardt "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | num_cpus = "1.13.0" 11 | -------------------------------------------------------------------------------- /week5/farm/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | #[allow(unused_imports)] 3 | use std::sync::{Arc, Mutex}; 4 | use std::time::Instant; 5 | #[allow(unused_imports)] 6 | use std::{env, process, thread}; 7 | 8 | /// Determines whether a number is prime. This function is taken from CS 110 factor.py. 9 | /// 10 | /// You don't need to read or understand this code. 11 | #[allow(dead_code)] 12 | fn is_prime(num: u32) -> bool { 13 | if num <= 1 { 14 | return false; 15 | } 16 | for factor in 2..((num as f64).sqrt().floor() as u32) { 17 | if num % factor == 0 { 18 | return false; 19 | } 20 | } 21 | true 22 | } 23 | 24 | /// Determines the prime factors of a number and prints them to stdout. This function is taken 25 | /// from CS 110 factor.py. 26 | /// 27 | /// You don't need to read or understand this code. 28 | #[allow(dead_code)] 29 | fn factor_number(num: u32) { 30 | let start = Instant::now(); 31 | 32 | if num == 1 || is_prime(num) { 33 | println!("{} = {} [time: {:?}]", num, num, start.elapsed()); 34 | return; 35 | } 36 | 37 | let mut factors = Vec::new(); 38 | let mut curr_num = num; 39 | for factor in 2..num { 40 | while curr_num % factor == 0 { 41 | factors.push(factor); 42 | curr_num /= factor; 43 | } 44 | } 45 | factors.sort(); 46 | let factors_str = factors 47 | .into_iter() 48 | .map(|f| f.to_string()) 49 | .collect::>() 50 | .join(" * "); 51 | println!("{} = {} [time: {:?}]", num, factors_str, start.elapsed()); 52 | } 53 | 54 | /// Returns a list of numbers supplied via argv. 55 | #[allow(dead_code)] 56 | fn get_input_numbers() -> VecDeque { 57 | let mut numbers = VecDeque::new(); 58 | for arg in env::args().skip(1) { 59 | if let Ok(val) = arg.parse::() { 60 | numbers.push_back(val); 61 | } else { 62 | println!("{} is not a valid number", arg); 63 | process::exit(1); 64 | } 65 | } 66 | numbers 67 | } 68 | 69 | fn main() { 70 | let num_threads = num_cpus::get(); 71 | println!("Farm starting on {} CPUs", num_threads); 72 | let start = Instant::now(); 73 | 74 | // TODO: call get_input_numbers() and store a queue of numbers to factor 75 | 76 | // TODO: spawn `num_threads` threads, each of which pops numbers off the queue and calls 77 | // factor_number() until the queue is empty 78 | 79 | // TODO: join all the threads you created 80 | 81 | println!("Total execution time: {:?}", start.elapsed()); 82 | } 83 | -------------------------------------------------------------------------------- /week5/survey.txt: -------------------------------------------------------------------------------- 1 | Survey code: 2 | -------------------------------------------------------------------------------- /week6/.gitignore: -------------------------------------------------------------------------------- 1 | # Vim swap files 2 | .*.swp 3 | 4 | # Mac 5 | .DS_Store 6 | 7 | # Rust build artifacts 8 | /*/target/ 9 | 10 | /*/Cargo.lock 11 | 12 | # These are backup files generated by rustfmt 13 | **/*.rs.bk 14 | -------------------------------------------------------------------------------- /week6/parallel_map/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "parallel_map" 3 | version = "0.1.0" 4 | authors = ["Armin Namavari "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | crossbeam-channel = "0.4.2" -------------------------------------------------------------------------------- /week6/parallel_map/src/main.rs: -------------------------------------------------------------------------------- 1 | use crossbeam_channel; 2 | use std::{thread, time}; 3 | 4 | fn parallel_map(mut input_vec: Vec, num_threads: usize, f: F) -> Vec 5 | where 6 | F: FnOnce(T) -> U + Send + Copy + 'static, 7 | T: Send + 'static, 8 | U: Send + 'static + Default, 9 | { 10 | let mut output_vec: Vec = Vec::with_capacity(input_vec.len()); 11 | // TODO: implement parallel map! 12 | output_vec 13 | } 14 | 15 | fn main() { 16 | let v = vec![6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 12, 18, 11, 5, 20]; 17 | let squares = parallel_map(v, 10, |num| { 18 | println!("{} squared is {}", num, num * num); 19 | thread::sleep(time::Duration::from_millis(500)); 20 | num * num 21 | }); 22 | println!("squares: {:?}", squares); 23 | } 24 | -------------------------------------------------------------------------------- /week6/survey.txt: -------------------------------------------------------------------------------- 1 | Survey code: 2 | --------------------------------------------------------------------------------