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