From b96861eb9b3e43f8958dfbf395a167d8839b8e00 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sun, 9 Jun 2019 12:49:53 -0400 Subject: start using snafu --- src/parser.rs | 12 ++--- src/process.rs | 97 +++++++++++++++++++++++------------- src/readline.rs | 149 +++++++++++++++++++++++++++++++++++--------------------- src/repl.rs | 40 ++++++++++----- 4 files changed, 192 insertions(+), 106 deletions(-) (limited to 'src') diff --git a/src/parser.rs b/src/parser.rs index cc86020..3a03d43 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,14 +1,14 @@ -#[derive(Debug)] +use snafu::{OptionExt, Snafu}; + +#[derive(Debug, Snafu)] pub enum Error { + #[snafu(display("No command given"))] CommandRequired, } pub fn parse(line: &str) -> Result<(String, Vec), Error> { // TODO let mut tokens = line.split_whitespace().map(|s| s.to_string()); - if let Some(cmd) = tokens.next() { - Ok((cmd, tokens.collect())) - } else { - Err(Error::CommandRequired) - } + let cmd = tokens.next().context(CommandRequired)?; + Ok((cmd, tokens.collect())) } diff --git a/src/process.rs b/src/process.rs index 5a82e5c..302c9ca 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,12 +1,52 @@ use futures::future::Future; +use snafu::{ResultExt, Snafu}; use std::io::{Read, Write}; use tokio::io::AsyncRead; use tokio_pty_process::CommandExt; -#[derive(Debug)] +#[derive(Debug, Snafu)] pub enum Error { - IOError(std::io::Error), - ParserError(crate::parser::Error), + #[snafu(display("failed to open a pty: {}", source))] + OpenPty { source: std::io::Error }, + + #[snafu(display( + "failed to spawn process for {} {}: {}", + cmd, + args.join(" "), + source + ))] + SpawnProcess { + cmd: String, + args: Vec, + source: std::io::Error, + }, + + #[snafu(display("failed to parse command line '{}': {}", line, source))] + ParserError { + line: String, + source: crate::parser::Error, + }, + + #[snafu(display("failed to write to pty: {}", source))] + WriteToPty { source: std::io::Error }, + + #[snafu(display("failed to read from terminal: {}", source))] + ReadFromTerminal { source: std::io::Error }, + + #[snafu(display( + "failed to clear ready state on pty for reading: {}", + source + ))] + PtyClearReadReady { source: std::io::Error }, + + #[snafu(display("failed to poll for process exit: {}", source))] + ProcessExitPoll { source: std::io::Error }, + + #[snafu(display( + "failed to put the terminal into raw mode: {}", + source + ))] + IntoRawMode { source: std::io::Error }, } pub fn spawn(line: &str) -> Result { @@ -32,15 +72,15 @@ pub struct RunningProcess { impl RunningProcess { fn new(line: &str) -> Result { - let pty = tokio_pty_process::AsyncPtyMaster::open() - .map_err(|e| Error::IOError(e))?; + let pty = + tokio_pty_process::AsyncPtyMaster::open().context(OpenPty)?; let (cmd, args) = - crate::parser::parse(line).map_err(|e| Error::ParserError(e))?; - let process = std::process::Command::new(cmd) + crate::parser::parse(line).context(ParserError { line })?; + let process = std::process::Command::new(cmd.clone()) .args(&args) .spawn_pty_async(&pty) - .map_err(|e| Error::IOError(e))?; + .context(SpawnProcess { cmd, args })?; // TODO: tokio::io::stdin is broken (it's blocking) // let input = tokio::io::stdin(); @@ -53,7 +93,8 @@ impl RunningProcess { buf: Vec::with_capacity(4096), output_done: false, exit_done: false, - _screen: crossterm::RawScreen::into_raw_mode().unwrap(), + _screen: crossterm::RawScreen::into_raw_mode() + .context(IntoRawMode)?, }) } } @@ -72,21 +113,12 @@ impl futures::stream::Stream for RunningProcess { let mut stdin = stdin.lock(); let mut buf = vec![0; 4096]; // TODO: async - match stdin.read(&mut buf) { - Ok(n) => { - if n > 0 { - let bytes = buf[..n].to_vec(); - - // TODO: async - let res = self.pty.write_all(&bytes); - if let Err(e) = res { - return Err(Error::IOError(e)); - } - } - } - Err(e) => { - return Err(Error::IOError(e)); - } + let n = stdin.read(&mut buf).context(ReadFromTerminal)?; + if n > 0 { + let bytes = buf[..n].to_vec(); + + // TODO: async + self.pty.write_all(&bytes).context(WriteToPty)?; } } _ => {} @@ -95,16 +127,13 @@ impl futures::stream::Stream for RunningProcess { // the buffer but we don't read it all in the previous read call, // since i think we won't get another notification until new bytes // actually arrive even if there are bytes in the buffer - if let Err(e) = self.input.clear_read_ready(ready) { - return Err(Error::IOError(e)); - } + self.input + .clear_read_ready(ready) + .context(PtyClearReadReady)?; if !self.output_done { self.buf.clear(); - let output_poll = self - .pty - .read_buf(&mut self.buf) - .map_err(|e| Error::IOError(e)); + let output_poll = self.pty.read_buf(&mut self.buf); match output_poll { Ok(futures::Async::Ready(n)) => { let bytes = self.buf[..n].to_vec(); @@ -128,14 +157,16 @@ impl futures::stream::Stream for RunningProcess { return Ok(futures::Async::NotReady); } Err(_) => { + // explicitly ignoring errors (for now?) because we + // always read off the end of the pty after the process + // is done self.output_done = true; } } } if !self.exit_done { - let exit_poll = - self.process.poll().map_err(|e| Error::IOError(e)); + let exit_poll = self.process.poll().context(ProcessExitPoll); match exit_poll { Ok(futures::Async::Ready(status)) => { self.exit_done = true; diff --git a/src/readline.rs b/src/readline.rs index af2b4fa..eaf29ec 100644 --- a/src/readline.rs +++ b/src/readline.rs @@ -1,9 +1,29 @@ +use snafu::{ensure, ResultExt, Snafu}; use std::io::Write; -#[derive(Debug)] +#[derive(Debug, Snafu)] pub enum Error { + #[snafu(display("failed to write to the terminal: {}", source))] + WriteToTerminal { source: std::io::Error }, + + #[snafu(display("end of input"))] EOF, - IOError(std::io::Error), + + #[snafu(display( + "failed to put the terminal into raw mode: {}", + source + ))] + IntoRawMode { source: std::io::Error }, + + #[snafu(display( + "failed to spawn a background thread to read terminal input: {}", + source + ))] + TerminalInputReadingThread { source: std::io::Error }, +} + +pub fn readline(prompt: &str, echo: bool) -> Result { + Readline::new(prompt, echo) } pub struct Readline { @@ -22,10 +42,11 @@ struct ReadlineState { } impl Readline { - fn new(prompt: &str, echo: bool) -> Self { - let screen = crossterm::RawScreen::into_raw_mode().unwrap(); + fn new(prompt: &str, echo: bool) -> Result { + let screen = + crossterm::RawScreen::into_raw_mode().context(IntoRawMode)?; - Readline { + Ok(Readline { reader: None, state: ReadlineState { prompt: prompt.to_string(), @@ -35,7 +56,20 @@ impl Readline { wrote_prompt: false, }, _raw_screen: screen, + }) + } + + fn with_reader(&mut self, f: F) -> Result + where + F: FnOnce(&KeyReader, &mut ReadlineState) -> Result, + { + let mut reader_opt = self.reader.take(); + if reader_opt.is_none() { + reader_opt = Some(KeyReader::new(futures::task::current())?); } + let ret = f(reader_opt.as_ref().unwrap(), &mut self.state); + self.reader = reader_opt; + ret } } @@ -60,9 +94,9 @@ impl ReadlineState { match event { crossterm::KeyEvent::Char(c) => { if self.cursor != self.buffer.len() && c != '\n' { - self.echo(b"\x1b[@").map_err(|e| Error::IOError(e))?; + self.echo(b"\x1b[@").context(WriteToTerminal)?; } - self.echo_char(c).map_err(|e| Error::IOError(e))?; + self.echo_char(c).context(WriteToTerminal)?; if c == '\n' { return Ok(futures::Async::Ready(self.buffer.clone())); @@ -73,16 +107,15 @@ impl ReadlineState { crossterm::KeyEvent::Ctrl(c) => { if c == 'd' { if self.buffer.is_empty() { - self.echo_char('\n') - .map_err(|e| Error::IOError(e))?; - return Err(Error::EOF); + self.echo_char('\n').context(WriteToTerminal)?; + ensure!(false, EOF); } } if c == 'c' { self.buffer = String::new(); self.cursor = 0; - self.echo_char('\n').map_err(|e| Error::IOError(e))?; - self.prompt().map_err(|e| Error::IOError(e))?; + self.echo_char('\n').context(WriteToTerminal)?; + self.prompt().context(WriteToTerminal)?; } } crossterm::KeyEvent::Backspace => { @@ -90,25 +123,23 @@ impl ReadlineState { self.cursor -= 1; self.buffer.remove(self.cursor); if self.cursor == self.buffer.len() { - self.echo(b"\x08 \x08") - .map_err(|e| Error::IOError(e))?; + self.echo(b"\x08 \x08").context(WriteToTerminal)?; } else { - self.echo(b"\x08\x1b[P") - .map_err(|e| Error::IOError(e))?; + self.echo(b"\x08\x1b[P").context(WriteToTerminal)?; } } } crossterm::KeyEvent::Left => { let cursor = 0.max(self.cursor - 1); if cursor != self.cursor { - self.write(b"\x1b[D").map_err(|e| Error::IOError(e))?; + self.write(b"\x1b[D").context(WriteToTerminal)?; self.cursor = cursor; } } crossterm::KeyEvent::Right => { let cursor = self.buffer.len().min(self.cursor + 1); if cursor != self.cursor { - self.write(b"\x1b[C").map_err(|e| Error::IOError(e))?; + self.write(b"\x1b[C").context(WriteToTerminal)?; self.cursor = cursor; } } @@ -159,67 +190,75 @@ impl futures::future::Future for Readline { fn poll(&mut self) -> futures::Poll { if !self.state.wrote_prompt { - self.state.prompt().map_err(|e| Error::IOError(e))?; + self.state.prompt().context(WriteToTerminal)?; self.state.wrote_prompt = true; } - let reader = self - .reader - .get_or_insert_with(|| KeyReader::new(futures::task::current())); - while let Some(event) = reader.poll() { - let a = self.state.process_event(event)?; - if a.is_ready() { - return Ok(a); + self.with_reader(|reader, state| { + loop { + match reader.try_recv() { + Ok(event) => { + let a = state.process_event(event)?; + if a.is_ready() { + return Ok(a); + } + } + Err(std::sync::mpsc::TryRecvError::Empty) => { + return Ok(futures::Async::NotReady) + } + Err(std::sync::mpsc::TryRecvError::Disconnected) => { + // is EOF correct here? + ensure!(false, EOF) + } + } } - } - Ok(futures::Async::NotReady) + }) } } -pub fn readline(prompt: &str, echo: bool) -> Readline { - Readline::new(prompt, echo) -} - struct KeyReader { events: std::sync::mpsc::Receiver, quit: std::sync::mpsc::Sender<()>, } impl KeyReader { - fn new(task: futures::task::Task) -> Self { + fn new(task: futures::task::Task) -> Result { let reader = crossterm::input().read_sync(); let (events_tx, events_rx) = std::sync::mpsc::channel(); let (quit_tx, quit_rx) = std::sync::mpsc::channel(); // TODO: this is pretty janky - it'd be better to build in more useful // support to crossterm directly - std::thread::spawn(move || { - for event in reader { - let newline = event - == crossterm::InputEvent::Keyboard( - crossterm::KeyEvent::Char('\n'), - ); - events_tx.send(event).unwrap(); - task.notify(); - if newline { - break; - } - if let Ok(_) = quit_rx.try_recv() { - break; + std::thread::Builder::new() + .spawn(move || { + for event in reader { + let newline = event + == crossterm::InputEvent::Keyboard( + crossterm::KeyEvent::Char('\n'), + ); + // unwrap is unpleasant, but so is figuring out how to + // propagate the error back to the main thread + events_tx.send(event).unwrap(); + task.notify(); + if newline { + break; + } + if let Ok(_) = quit_rx.try_recv() { + break; + } } - } - }); + }) + .context(TerminalInputReadingThread)?; - KeyReader { + Ok(KeyReader { events: events_rx, quit: quit_tx, - } + }) } - fn poll(&self) -> Option { - if let Ok(event) = self.events.try_recv() { - return Some(event); - } - None + fn try_recv( + &self, + ) -> Result { + self.events.try_recv() } } diff --git a/src/repl.rs b/src/repl.rs index e54eb22..749e0d6 100644 --- a/src/repl.rs +++ b/src/repl.rs @@ -1,12 +1,18 @@ use futures::future::{Future, IntoFuture}; use futures::stream::Stream; +use snafu::{ResultExt, Snafu}; use std::io::Write; -#[derive(Debug)] +#[derive(Debug, Snafu)] enum Error { - ReadError(crate::readline::Error), - EvalError(crate::process::Error), - PrintError(std::io::Error), + #[snafu(display("error during read: {}", source))] + ReadError { source: crate::readline::Error }, + + #[snafu(display("error during eval: {}", source))] + EvalError { source: crate::process::Error }, + + #[snafu(display("error during print: {}", source))] + PrintError { source: std::io::Error }, } pub fn repl() { @@ -39,17 +45,24 @@ pub fn repl() { eprint!("process exited weirdly?\r\n"); return Ok((done, false)); } - Err(Error::ReadError(crate::readline::Error::EOF)) => { + Err(Error::ReadError { + source: crate::readline::Error::EOF, + }) => { return Ok((done, true)); } - Err(Error::EvalError(crate::process::Error::ParserError( - crate::parser::Error::CommandRequired, - ))) => { + Err(Error::EvalError { + source: + crate::process::Error::ParserError { + source: crate::parser::Error::CommandRequired, + line: _, + }, + }) => { return Ok((done, false)); } Err(e) => { let stderr = std::io::stderr(); let mut stderr = stderr.lock(); + // panics seem fine for errors during error handling write!(stderr, "error: {:?}\r\n", e).unwrap(); stderr.flush().unwrap(); return Ok((done, false)); @@ -60,7 +73,10 @@ pub fn repl() { } fn read() -> impl futures::future::Future { - crate::readline::readline("$ ", true).map_err(|e| Error::ReadError(e)) + crate::readline::readline("$ ", true) + .into_future() + .flatten() + .map_err(|e| Error::ReadError { source: e }) } fn eval( @@ -70,12 +86,12 @@ fn eval( crate::process::spawn(line) .into_future() .flatten_stream() - .map_err(|e| Error::EvalError(e)) + .map_err(|e| Error::EvalError { source: e }) } fn print(out: &[u8]) -> Result<(), Error> { let stdout = std::io::stdout(); let mut stdout = stdout.lock(); - stdout.write(out).map_err(|e| Error::PrintError(e))?; - stdout.flush().map_err(|e| Error::PrintError(e)) + stdout.write(out).context(PrintError)?; + stdout.flush().context(PrintError) } -- cgit v1.2.3-54-g00ecf