Initial commit

This commit is contained in:
Ryan Eberhardt 2020-10-07 15:58:20 -07:00
commit e1aa7f4345
75 changed files with 3937 additions and 0 deletions

21
LICENSE.txt Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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 "$@"

View 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;
}

View File

@ -0,0 +1,5 @@
#include <stdlib.h>
int main() {
asm("syscall" :: "a"(60), "D"(0));
}

View 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);
}

View File

@ -0,0 +1,6 @@
#include <stdio.h>
int main() {
printf("Hello world!\n");
return 0;
}

View 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);
}

View 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;
}

View 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.");
}
}
}
}
}
}

View 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,
}
}
}

View 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)
}
}

View 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(())
}

View 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
View 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
View File

@ -0,0 +1,5 @@
/balancebeam/.cargo/
/balancebeam/target/
/balancebeam/Cargo.lock
.idea
.*.swp

18
proj-2/Dockerfile Normal file
View 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", "--"]

View 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"

View 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");
}
}

View 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())
}

View 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()
}

View 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 :)");
}

View 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 :)");
}

View 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
}
}

View 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()
}
}

View 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()
}
}

View 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();
});
}

View 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
View 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

View 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]

View File

@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}

View 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]

View 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]);
}
}

View File

@ -0,0 +1,7 @@
[package]
name = "hangman"
version = "0.1.0"
authors = ["Armin Namavari <arminn@stanford.edu>"]
[dependencies]
rand = "0.6.0"

View 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! :)
}

View File

@ -0,0 +1,9 @@
immutable
borrowed
shared
reference
aluminum
oxidation
lobster
starfish
crawfish

3
week1/part-4.txt Normal file
View File

@ -0,0 +1,3 @@
Survey code:
Thanks for being awesome! We appreciate you!

13
week2/.gitignore vendored Normal file
View 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
View 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
View File

@ -0,0 +1,6 @@
[package]
name = "rdiff"
version = "0.1.0"
authors = ["Armin Namavari <arminn@stanford.edu>"]
[dependencies]

View 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.

View 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
View File

@ -0,0 +1,5 @@
a
b
c
d
e

8
week2/rdiff/simple-b.txt Normal file
View File

@ -0,0 +1,8 @@
a
added
b
c
added
d
added
e

101
week2/rdiff/src/grid.rs Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
Survey code:

13
week3/.gitignore vendored Normal file
View 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

View 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"

View File

@ -0,0 +1,10 @@
SRCS = $(wildcard *.c)
PROGS = $(patsubst %.c,%,$(SRCS))
all: $(PROGS)
%: %.c
$(CC) $(CFLAGS) -o $@ $<
clean:
rm -f $(PROGS)

View 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;
}

View File

@ -0,0 +1 @@
int main() {}

View 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."
);
}
}

View 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();
}
}

View 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();
}
}

View 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"
);
}
}

View 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;
}

View 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]

View 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();
}
}
}

View 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
View File

@ -0,0 +1 @@
Survey code:

13
week5/.gitignore vendored Normal file
View 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
View 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
View 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
View File

@ -0,0 +1 @@
Survey code:

13
week6/.gitignore vendored Normal file
View 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

View 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"

View 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
View File

@ -0,0 +1 @@
Survey code: