From 86990efcf8fa63decb05f462d745b95e4992dc77 Mon Sep 17 00:00:00 2001 From: Irene Knapp Date: Thu, 11 Mar 2021 20:15:47 -0800 Subject: refactor everything into smaller modules; move to using async-std; make the read loop async. some stuff doesn't work yet but it needed to be done and this is as clean a state as it's likely to get in, so we're committing it as a base to build on. --- src/error.rs | 33 +++----- src/main.rs | 61 +++++++------ src/path.rs | 21 ++--- src/path/error.rs | 28 ++++++ src/path/prelude.rs | 5 ++ src/prelude.rs | 5 +- src/result.rs | 2 + src/terminal.rs | 216 ++++++++++++++++++++++++++++++++++------------- src/terminal/decoding.rs | 151 +++++++++++++++++++++++---------- src/terminal/error.rs | 14 +++ src/terminal/prelude.rs | 4 + 11 files changed, 372 insertions(+), 168 deletions(-) create mode 100644 src/path/prelude.rs create mode 100644 src/terminal/prelude.rs (limited to 'src') diff --git a/src/error.rs b/src/error.rs index b8289f6..1d291ed 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,7 @@ +#![forbid(unsafe_code)] + use crate::path::GenericPath; -use crate::path::error::{FileNameError, DirectoryNameError}; +use crate::path::error::{FileNameError, DirectoryNameError, PathError}; use crate::terminal::error::TerminalError; @@ -12,10 +14,7 @@ pub enum Error { Parse(String), FileName(FileNameError), DirectoryName(DirectoryNameError), - PathListHasEmptyComponents(String), - PathLexicallyDirectory(GenericPath), - PathLexicallyRelative(GenericPath), - PathLexicallyInvalid(GenericPath), + Path(PathError), PathEmpiricallyFile(GenericPath), Terminal(TerminalError), } @@ -29,23 +28,7 @@ impl std::fmt::Display for Error { Error::Parse(e) => e.fmt(f), Error::FileName(e) => e.fmt(f), Error::DirectoryName(e) => e.fmt(f), - Error::PathListHasEmptyComponents(path_list) => - f.write_fmt(format_args!( - "Path list has empty components: {}", - path_list)), - Error::PathLexicallyDirectory(path) => - f.write_fmt(format_args!( - "The path {} ends in a slash, but is supposed to refer to a file, \ - not a directory.", - path)), - Error::PathLexicallyRelative(path) => - f.write_fmt(format_args!( - "The path {} is relative, not absolute.", - path)), - Error::PathLexicallyInvalid(path) => - f.write_fmt(format_args!( - "This isn't a valid path. {}", - path)), + Error::Path(e) => e.fmt(f), Error::PathEmpiricallyFile(path) => f.write_fmt(format_args!( "There's a file at {}, not a directory.", @@ -85,6 +68,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: PathError) -> Error { + Error::Path(e) + } +} + impl From for Error { fn from(e: TerminalError) -> Error { Error::Terminal(e) diff --git a/src/main.rs b/src/main.rs index d7a2aff..935155c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,15 @@ +#![forbid(unsafe_code)] use crate::prelude::*; + +use crate::result::Result; +use crate::terminal::{Input, Terminal}; + +use async_std::io; use std::collections::HashMap; use std::collections::HashSet; -use std::io; -use std::io::prelude::*; +use std::env; use std::os::unix::fs::PermissionsExt; -use std::process::Command; +use std::process::{self, Command}; #[macro_use] extern crate lalrpop_util; @@ -16,14 +21,9 @@ pub mod result; pub mod terminal; -pub enum Input { - String(String), - End, -} - - fn main() -> Result<()> { - std::process::exit(match repl() { + let result = async_std::task::block_on(async { repl().await }); + process::exit(match result { Ok(()) => 0, Err(ref e) => { eprintln!("{}", e); @@ -33,45 +33,42 @@ fn main() -> Result<()> { } -fn repl() -> Result<()> { +async fn repl() -> Result<()> { println!("Hello, terminal!"); + let mut terminal = Terminal::init(io::stdin())?; + loop { - prompt()?; + prompt().await?; + + let input = terminal.handle_input().await?; - terminal::handle_input_terminal(io::stdin())?; - let input = read()?; match input { - Input::String(string) => execute(&string)?, + Input::String(string) => { + println!("{:?} {}", string, string.len()); + execute(&string).await? + }, Input::End => break, } + + break; } + terminal.cleanup()?; + Ok(()) } -fn prompt() -> Result<()> { +async fn prompt() -> Result<()> { print!("\n$ "); - io::stdout().flush()?; + io::stdout().flush().await?; Ok(()) } -fn read() -> Result { - let mut input = String::new(); - let n_bytes = io::stdin().read_line(&mut input)?; - - if n_bytes == 0 { - Ok(Input::End) - } else { - Ok(Input::String(input)) - } -} - - -fn execute(input: &str) -> Result<()> { +async fn execute(input: &str) -> Result<()> { let invocation = commandline::InvocationParser::new().parse(input)?; match invocation.as_slice() { @@ -114,12 +111,12 @@ fn execute(input: &str) -> Result<()> { fn read_environment() -> Result> { - Ok(std::env::vars().collect()) + Ok(env::vars().collect()) } fn get_environment(variable_name: &str) -> Result> { - Ok(std::env::vars() + Ok(env::vars() .find(|(key, _)| key == variable_name) .map(|(_, value)| value)) } diff --git a/src/path.rs b/src/path.rs index 570b8ef..7df10b2 100644 --- a/src/path.rs +++ b/src/path.rs @@ -6,14 +6,15 @@ * to get the benefit of its functionality for handling non-Unicode filenames. */ -use crate::prelude::*; -use crate::path::error::*; +#![forbid(unsafe_code)] +use crate::path::prelude::*; use std::str::FromStr; lalrpop_mod!(pub parser, "/path/parser.rs"); pub mod error; +pub mod prelude; #[derive(Clone,Debug,Eq,Hash,Ord,PartialEq,PartialOrd)] @@ -230,10 +231,10 @@ pub fn parse_path_list(path_list: &str) .parse(path_list) { Ok(_) => { - Err(Error::PathListHasEmptyComponents(path_list.to_string())) + Err(PathError::PathListHasEmptyComponents(path_list.to_string())) }, Err(_) => { - Err(Error::Parse(original_error.to_string())) + Err(PathError::Parse(original_error.to_string())) }, } }, @@ -245,7 +246,7 @@ pub fn absolute_directory_path(generic_path: GenericPath) -> Result { if !generic_path.starts_with_slash { - return Err(Error::PathLexicallyRelative(generic_path)); + return Err(PathError::PathLexicallyRelative(generic_path)); } let mut flattened_components = Vec::new(); @@ -256,7 +257,7 @@ pub fn absolute_directory_path(generic_path: GenericPath) if flattened_components.len() > 0 { flattened_components.pop(); } else { - return Err(Error::PathLexicallyInvalid(generic_path)); + return Err(PathError::PathLexicallyInvalid(generic_path)); } }, GenericPathComponent::FileOrDirectoryName(name) => { @@ -275,11 +276,11 @@ pub fn absolute_file_path(generic_path: GenericPath) -> Result { if !generic_path.starts_with_slash { - return Err(Error::PathLexicallyRelative(generic_path)); + return Err(PathError::PathLexicallyRelative(generic_path)); } if generic_path.ends_with_slash { - return Err(Error::PathLexicallyDirectory(generic_path)); + return Err(PathError::PathLexicallyDirectory(generic_path)); } let mut iterator = generic_path.components.iter(); @@ -289,7 +290,7 @@ pub fn absolute_file_path(generic_path: GenericPath) FileName(name.to_string()) } _ => { - return Err(Error::PathLexicallyInvalid(generic_path)); + return Err(PathError::PathLexicallyInvalid(generic_path)); } }; @@ -301,7 +302,7 @@ pub fn absolute_file_path(generic_path: GenericPath) if flattened_components.len() > 0 { flattened_components.pop(); } else { - return Err(Error::PathLexicallyInvalid(generic_path)); + return Err(PathError::PathLexicallyInvalid(generic_path)); } }, GenericPathComponent::FileOrDirectoryName(name) => { diff --git a/src/path/error.rs b/src/path/error.rs index 0ee5423..aee6e9f 100644 --- a/src/path/error.rs +++ b/src/path/error.rs @@ -1,3 +1,10 @@ +#![forbid(unsafe_code)] + +use crate::path::GenericPath; + +pub type Result = std::result::Result; + + #[derive(Clone,Debug,Eq,Hash,Ord,PartialEq,PartialOrd)] pub enum FileNameError { ContainsSlash(String), @@ -11,6 +18,10 @@ pub enum DirectoryNameError { #[derive(Clone,Debug,Eq,Hash,Ord,PartialEq,PartialOrd)] pub enum PathError { Parse(String), + PathLexicallyDirectory(GenericPath), + PathLexicallyRelative(GenericPath), + PathLexicallyInvalid(GenericPath), + PathListHasEmptyComponents(String), } @@ -45,6 +56,23 @@ impl std::fmt::Display for PathError { match self { PathError::Parse(s) => f.write_fmt(format_args!("Syntax error in path: {}", s)), + PathError::PathLexicallyDirectory(path) => + f.write_fmt(format_args!( + "The path {} ends in a slash, but is supposed to refer to a file, \ + not a directory.", + path)), + PathError::PathLexicallyRelative(path) => + f.write_fmt(format_args!( + "The path {} is relative, not absolute.", + path)), + PathError::PathLexicallyInvalid(path) => + f.write_fmt(format_args!( + "This isn't a valid path. {}", + path)), + PathError::PathListHasEmptyComponents(path_list) => + f.write_fmt(format_args!( + "Path list has empty components: {}", + path_list)), } } } diff --git a/src/path/prelude.rs b/src/path/prelude.rs new file mode 100644 index 0000000..f7b23dd --- /dev/null +++ b/src/path/prelude.rs @@ -0,0 +1,5 @@ +#![forbid(unsafe_code)] + +pub use crate::path::error::{ + Result, DirectoryNameError, FileNameError, PathError}; + diff --git a/src/prelude.rs b/src/prelude.rs index b44fe02..f678ba1 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,2 +1,3 @@ -pub use crate::error::Error; -pub use crate::result::Result; +#![forbid(unsafe_code)] +pub use async_std::prelude::*; + diff --git a/src/result.rs b/src/result.rs index 8207680..b757636 100644 --- a/src/result.rs +++ b/src/result.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + use crate::error::Error; pub type Result = std::result::Result; diff --git a/src/terminal.rs b/src/terminal.rs index 298cc8c..73ba035 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -1,12 +1,19 @@ +#![forbid(unsafe_code)] +use crate::prelude::*; +use crate::terminal::prelude::*; + use crate::terminal::decoding::CharBufReader; -use crate::terminal::error::TerminalError; -use nix::sys::termios; -use std::io::Read; -use std::os::unix::io::AsRawFd; +use async_std::io::{self, Read}; +use async_std::os::unix::io::{AsRawFd, RawFd}; +use async_std::sync::{Arc, Mutex}; +use nix::sys::termios::{self, Termios}; +use signal_hook::consts::TERM_SIGNALS; +use std::sync::atomic::{AtomicBool, Ordering}; pub mod decoding; pub mod error; +pub mod prelude; #[derive(Clone,Debug,Eq,Hash,Ord,PartialEq,PartialOrd)] @@ -60,6 +67,16 @@ impl LineBuffer { first_line.push_str(&second_line); } } + + pub fn as_string(&self) -> String { + self.lines.join("\n") + } +} + + +pub enum Input { + String(String), + End, } @@ -70,83 +87,164 @@ enum InputAction { } -pub fn handle_input_terminal(input_stream: impl Read + AsRawFd) - -> std::result::Result<(), TerminalError> -{ - let fd = input_stream.as_raw_fd(); - let mut termios = termios::tcgetattr(fd) - .map_err(error::mode_setting)?; - termios.local_flags.insert(termios::LocalFlags::ECHO); - termios.local_flags.remove(termios::LocalFlags::ECHOCTL); - termios.local_flags.remove(termios::LocalFlags::ICANON); - termios.control_chars[termios::SpecialCharacterIndices::VMIN as usize] = 1; - termios.control_chars[termios::SpecialCharacterIndices::VTIME as usize] = 0; - termios::tcsetattr(fd, termios::SetArg::TCSANOW, &termios) - .map_err(error::mode_setting)?; - - handle_input(input_stream) +pub struct Terminal { + reader: Arc>>, + //reader: Arc>, + line_buffer: LineBuffer, + file_descriptor: RawFd, + initial_termios: Termios, + is_interrupted: Arc, } -pub fn handle_input(input_stream: impl Read) - -> std::result::Result<(), TerminalError> -{ - let mut reader = CharBufReader::new(input_stream); - let mut line_buffer = LineBuffer::new(); +impl Terminal { + pub fn init(input_stream: InputStream) -> Result> { + let is_interrupted = Arc::new(AtomicBool::new(false)); - loop { - let mut action: Option = None; + for signal in TERM_SIGNALS { + signal_hook::flag::register(*signal, Arc::clone(&is_interrupted)) + .map_err(error::internal)?; + } - let string = reader.fill_buf().map_err(error::input)?; + let fd = input_stream.as_raw_fd(); + let termios = termios::tcgetattr(fd).map_err(error::mode_setting)?; + let reader = Arc::new(Mutex::new(CharBufReader::new( + input_stream, Arc::clone(&is_interrupted)))); + //let reader = Arc::new(CharBufReader::new( + //input_stream, Arc::clone(&is_interrupted))); + let line_buffer = LineBuffer::new(); + + let terminal = Terminal { + reader: reader, + line_buffer: line_buffer, + file_descriptor: fd, + initial_termios: termios, + is_interrupted: is_interrupted, + }; - let mut chars = string.char_indices(); + terminal.init_modes()?; - for (_, c) in &mut chars { - match c { - '\n' => { - action = Some(InputAction::Execute); - } - '\u{7f}' => { - action = Some(InputAction::Backspace); - } - _ => { - line_buffer.insert(&c.to_string()); - } - } + Ok(terminal) + } - if action.is_some() { - break; - } - } - let n_to_consume = match chars.next() { - Some((offset, _)) => offset, - None => string.len(), - }; + pub fn cleanup(self) -> Result<()> { + self.cleanup_modes()?; + + Ok(()) + } + + + fn init_modes(&self) -> Result<()> { + let mut termios = self.initial_termios.clone(); + + termios.local_flags.remove(termios::LocalFlags::ECHO); + termios.local_flags.remove(termios::LocalFlags::ECHONL); + termios.local_flags.remove(termios::LocalFlags::ECHOCTL); + termios.local_flags.remove(termios::LocalFlags::ICANON); + + termios.local_flags.insert(termios::LocalFlags::ISIG); + + termios.control_chars[termios::SpecialCharacterIndices::VMIN as usize] = 1; + termios.control_chars[termios::SpecialCharacterIndices::VTIME as usize] = 0; + + termios::tcsetattr(self.file_descriptor, + termios::SetArg::TCSANOW, + &termios) + .map_err(error::mode_setting)?; + + Ok(()) + } - reader.consume(n_to_consume); - match action { - Some(InputAction::Execute) => { + pub fn cleanup_modes(&self) -> Result<()> { + println!("de-initializing"); // DO NOT SUBMIT + + let termios = self.initial_termios.clone(); + + termios::tcsetattr(self.file_descriptor, + termios::SetArg::TCSANOW, + &termios) + .map_err(error::mode_setting)?; + + Ok(()) + } + + + pub fn is_exiting(&self) -> bool { + self.is_interrupted.load(Ordering::Relaxed) + } + + + pub async fn handle_input(&mut self) -> Result + { + let is_interrupted = Arc::clone(&self.is_interrupted); + + loop { + let mut action: Option = None; + + let string = CharBufReader::fill_buf(Arc::clone(&self.reader)).await.map_err(error::input)?; + + if is_interrupted.load(Ordering::Relaxed) { break; } - Some(InputAction::Backspace) => { - line_buffer.backspace(); + + let mut chars = string.char_indices(); + + for (_, c) in &mut chars { + match c { + '\n' => { + action = Some(InputAction::Execute); + } + '\u{7f}' => { + action = Some(InputAction::Backspace); + } + _ => { + self.line_buffer.insert(&c.to_string()); + print!("{}", c); + io::stdout().flush(); + } + } + + if action.is_some() { + break; + } + } + + let n_to_consume = match chars.next() { + Some((offset, _)) => offset, + None => string.len(), + }; + + Arc::get_mut(&mut self.reader).unwrap().lock().await.consume(n_to_consume); + + match action { + Some(InputAction::Execute) => { + break; + } + Some(InputAction::Backspace) => { + self.line_buffer.backspace(); + } + None => { } } - None => { } } - } - println!("line buffer {:?}", line_buffer); - - Ok(()) + println!("line buffer {:?}", self.line_buffer); + if self.is_interrupted.load(Ordering::Relaxed) { + println!("exiting 3"); + Ok(Input::End) + } else { + let input = Input::String(self.line_buffer.as_string()); + Ok(input) + } + } } #[cfg(test)] mod tests { use super::*; - use std::io::Cursor; + use io::Cursor; #[test] fn test_empty_input() { diff --git a/src/terminal/decoding.rs b/src/terminal/decoding.rs index e2654fb..11f6046 100644 --- a/src/terminal/decoding.rs +++ b/src/terminal/decoding.rs @@ -1,63 +1,128 @@ -use crate::terminal::error::{self, TerminalError}; +#![forbid(unsafe_code)] +use crate::terminal::prelude::*; -use std::io::{BufRead, BufReader, Read}; +use crate::terminal::error; + +use async_std::future::Future; +use async_std::io::{BufRead, BufReader, Read}; +use async_std::sync::{Arc, Mutex}; +use async_std::task::{Context, Poll}; +use pin_project::pin_project; +use pin_utils::pin_mut; +use std::pin::Pin; use std::str; +use std::sync::atomic::{AtomicBool, Ordering}; -pub struct CharBufReader { - byte_reader: BufReader, - char_buffer: String, +#[pin_project] +pub struct CharBufReader { + #[pin] byte_reader: BufReader, + #[pin] char_buffer: String, + is_interrupted: Arc, } -impl CharBufReader { - pub fn new(input_stream: R) -> CharBufReader { - let byte_reader = BufReader::new(input_stream); +#[pin_project] +struct FillBufFuture { + char_reader: Arc>>, + //char_reader: Arc>, +} - CharBufReader { - byte_reader: byte_reader, - char_buffer: String::new(), - } - } - pub fn fill_buf(&mut self) - -> std::result::Result<&str, TerminalError> +impl Future for FillBufFuture { + type Output = Result; + + fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) + -> Poll { - loop { - let byte_buffer = self.byte_reader.fill_buf().map_err(error::input)?; - - match str::from_utf8(byte_buffer) { - Err(error) => { - let n_valid = error.valid_up_to(); - if n_valid == 0 { - self.byte_reader.consume(1); - } else { - match str::from_utf8(&byte_buffer[..n_valid]) { - Err(_) => { - self.byte_reader.consume(1); - }, - Ok(chars) => { - self.char_buffer.push_str(chars); - - self.byte_reader.consume(n_valid); - - break; - }, - } + let future = self.project(); + let char_reader: &mut Arc>> = future.char_reader; + let char_reader_future = char_reader.lock(); + pin_mut!(char_reader_future); + match char_reader_future.poll(context) { + Poll::Ready(mut char_reader) => { + let char_reader = &mut *char_reader; + let mut byte_reader: Pin<&mut BufReader> = Pin::new(&mut char_reader.byte_reader); + + loop { + if char_reader.is_interrupted.load(Ordering::Relaxed) { + println!("char reader got interrupt"); + return Poll::Pending; } - }, - Ok(chars) => { - self.char_buffer.push_str(chars); - let n_to_consume = byte_buffer.len(); - self.byte_reader.consume(n_to_consume); + println!("about to fill_buf"); + match byte_reader.as_mut().poll_fill_buf(context).map_err(error::input)? + { + Poll::Ready(byte_buffer) => { + println!("done with fill_buf"); + match str::from_utf8(&byte_buffer) { + Err(error) => { + let n_valid = error.valid_up_to(); + if n_valid == 0 { + byte_reader.as_mut().consume(1); + } else { + match str::from_utf8(&byte_buffer[..n_valid]) { + Err(_) => { + byte_reader.as_mut().consume(1); + }, + Ok(chars) => { + char_reader.char_buffer.push_str(chars); + + byte_reader.as_mut().consume(n_valid); + + break; + }, + } + } + } + Ok(chars) => { + char_reader.char_buffer.push_str(chars); - break; + let n_to_consume = byte_buffer.len(); + byte_reader.as_mut().consume(n_to_consume); + + break; + } + } + } + Poll::Pending => { + println!("fill_buf pending"); + return Poll::Pending; + } + } } + + return Poll::Ready(Ok(char_reader.char_buffer.to_string())); + } + Poll::Pending => { + println!("char_reader mutex pending"); + return Poll::Pending; } } + } +} + + +impl CharBufReader { + pub fn new(input_stream: R, is_interrupted: Arc) + -> CharBufReader + { + let byte_reader = BufReader::new(input_stream); + + CharBufReader { + byte_reader: byte_reader, + char_buffer: String::new(), + is_interrupted: is_interrupted, + } + } - return Ok(&self.char_buffer); + pub fn fill_buf(reader: Arc>) + //pub fn fill_buf(reader: Arc) + -> impl Future> + { + FillBufFuture { + char_reader: reader, + } } pub fn consume(&mut self, amount: usize) { diff --git a/src/terminal/error.rs b/src/terminal/error.rs index 6666e49..795f973 100644 --- a/src/terminal/error.rs +++ b/src/terminal/error.rs @@ -1,7 +1,13 @@ +#![forbid(unsafe_code)] + +pub type Result = std::result::Result; + + #[derive(Clone,Debug,Eq,Hash,Ord,PartialEq,PartialOrd)] pub enum TerminalError { Input(String), ModeSetting(String), + Internal(String), } impl std::error::Error for TerminalError { } @@ -15,6 +21,9 @@ impl std::fmt::Display for TerminalError { TerminalError::ModeSetting(s) => f.write_fmt(format_args!( "Can't set terminal mode: {}", s)), + TerminalError::Internal(s) => + f.write_fmt(format_args!( + "Internal error regarding the terminal: {}", s)), } } } @@ -29,3 +38,8 @@ pub fn mode_setting(e: impl std::error::Error) -> TerminalError { TerminalError::ModeSetting(format!("{}", e)) } + +pub fn internal(e: impl std::error::Error) -> TerminalError { + TerminalError::Internal(format!("{}", e)) +} + diff --git a/src/terminal/prelude.rs b/src/terminal/prelude.rs new file mode 100644 index 0000000..bada817 --- /dev/null +++ b/src/terminal/prelude.rs @@ -0,0 +1,4 @@ +#![forbid(unsafe_code)] + +pub use crate::terminal::error::{Result, TerminalError}; + -- cgit 1.4.1