aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2019-06-09 12:49:53 -0400
committerJesse Luehrs <doy@tozt.net>2019-06-09 13:30:31 -0400
commitb96861eb9b3e43f8958dfbf395a167d8839b8e00 (patch)
treedf8cc6517817584a1dbee8cf2d21be23ff8a2e37 /src
parent035c598646d54ac0dc55fb9161e2f42e1863529e (diff)
downloadnbsh-old-b96861eb9b3e43f8958dfbf395a167d8839b8e00.tar.gz
nbsh-old-b96861eb9b3e43f8958dfbf395a167d8839b8e00.zip
start using snafu
Diffstat (limited to 'src')
-rw-r--r--src/parser.rs12
-rw-r--r--src/process.rs97
-rw-r--r--src/readline.rs149
-rw-r--r--src/repl.rs40
4 files changed, 192 insertions, 106 deletions
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<String>), 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<String>,
+ 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<RunningProcess, Error> {
@@ -32,15 +72,15 @@ pub struct RunningProcess {
impl RunningProcess {
fn new(line: &str) -> Result<Self, Error> {
- 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, Error> {
+ 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<Self, Error> {
+ 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<F, T>(&mut self, f: F) -> Result<T, Error>
+ where
+ F: FnOnce(&KeyReader, &mut ReadlineState) -> Result<T, Error>,
+ {
+ 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<Self::Item, Self::Error> {
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<crossterm::InputEvent>,
quit: std::sync::mpsc::Sender<()>,
}
impl KeyReader {
- fn new(task: futures::task::Task) -> Self {
+ fn new(task: futures::task::Task) -> Result<Self, Error> {
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<crossterm::InputEvent> {
- if let Ok(event) = self.events.try_recv() {
- return Some(event);
- }
- None
+ fn try_recv(
+ &self,
+ ) -> Result<crossterm::InputEvent, std::sync::mpsc::TryRecvError> {
+ 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<Item = String, Error = Error> {
- 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)
}