From 4ac1653bc87999075b5ebf7ab60c2d981c989c16 Mon Sep 17 00:00:00 2001 From: Jacob Henn Date: Tue, 16 Aug 2022 14:32:14 -0700 Subject: [PATCH 1/4] add timeout to client --- client/src/args.rs | 12 ++++---- client/src/main.rs | 72 +++++++++++++++++++++++++--------------------- 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/client/src/args.rs b/client/src/args.rs index 6fe7573..595509b 100644 --- a/client/src/args.rs +++ b/client/src/args.rs @@ -2,11 +2,13 @@ use structopt::StructOpt; #[derive(StructOpt)] pub enum Opts { - Chat, + Chat(ChatOpts), } -impl Opts { - pub fn get() -> Self { - Self::from_args() - } + +#[derive(StructOpt)] +pub struct ChatOpts { + /// whether or not to give up on connecting to the daemon after 10 ms + #[structopt(long)] + pub no_timeout: bool, } diff --git a/client/src/main.rs b/client/src/main.rs index ca23c30..080819d 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,5 +1,5 @@ use anyhow::{bail, Context, Result}; -use args::Opts; +use args::{Opts, ChatOpts}; use common::client_daemon::{ClientToDaemonMsg, DaemonToClientMsg}; use crossterm::{ cursor, @@ -9,11 +9,14 @@ use crossterm::{ ExecutableCommand, QueueableCommand, }; use mio::{unix::SourceFd, Events, Interest, Poll, Token}; +use structopt::StructOpt; use std::{ fs::File, io::{self, Read, StdoutLock, Write}, ops::ControlFlow, os::unix::prelude::AsRawFd, + panic, + thread, time::Duration, }; @@ -35,7 +38,7 @@ struct State<'a> { } impl<'a> State<'a> { - fn new() -> Result { + fn new(opts: ChatOpts) -> Result { let mut stdout = io::stdout().lock(); if !stdout.is_tty() { bail!("stdout is not a tty"); @@ -43,33 +46,7 @@ impl<'a> State<'a> { terminal::enable_raw_mode().context("couldn't enable raw mode")?; - let home_dir = dirs::home_dir().context("couldn't get home directory")?; - - print!("waiting for daemon connection..."); - stdout.flush()?; - // aquire a write handle on the ctod fifo - // this blocks the client until the daemon opens the file to read - let ctodbuf = File::options() - .write(true) - .open(home_dir.join(".rclc/ctodbuf")) - .context("couldn't open client->daemon fifo")?; - - // clear waiting message - stdout - .queue(terminal::Clear(ClearType::CurrentLine))? - .execute(cursor::MoveToColumn(0))?; - - print!("waiting for daemon response..."); - stdout.flush()?; - // aquire a read handle on the dtoc file - // this blocks until the daemon opens the file to write - let dtocbuf = File::open(home_dir.join(".rclc/dtocbuf")) - .context("couldn't open daemon->client fifo")?; - - // clear waiting message - stdout - .queue(terminal::Clear(ClearType::CurrentLine))? - .execute(cursor::MoveToColumn(0))?; + let (ctodbuf, dtocbuf) = Self::try_get_fifos(&opts)?; Ok(Self { stdout, @@ -79,6 +56,35 @@ impl<'a> State<'a> { }) } + fn try_get_fifos(opts: &ChatOpts) -> Result<(File, File)> { + let thread = thread::spawn(|| { + let home_dir = dirs::home_dir().context("couldn't get home directory")?; + + let ctodbuf = File::options() + .write(true) + .open(home_dir.join(".rclc/ctodbuf")) + .context("couldn't open client->daemon fifo")?; + + let dtocbuf = File::open(home_dir.join(".rclc/dtocbuf")) + .context("couldn't open daemon->client fifo")?; + + Ok((ctodbuf, dtocbuf)) + }); + + if !opts.no_timeout { + thread::sleep(Duration::from_millis(10)); + } + + if opts.no_timeout || thread.is_finished() { + match thread.join() { + Ok(o) => o, + Err(e) => panic::resume_unwind(e), + } + } else { + bail!("daemon is not running (10 ms timeout reached)"); + } + } + /// start the event loop fn start(&mut self) -> Result<()> { // a mio poll lets you monitor for readiness events from multiple sources. @@ -213,8 +219,8 @@ impl<'a> State<'a> { } } -fn chat() -> Result<()> { - let mut state = State::new()?; +fn chat(opts: ChatOpts) -> Result<()> { + let mut state = State::new(opts)?; state.start()?; Ok(()) @@ -236,8 +242,8 @@ fn cleanup() { } fn go() -> Result<()> { - match Opts::get() { - Opts::Chat => chat(), + match Opts::from_args() { + Opts::Chat(chatopts) => chat(chatopts), } } From 0c0b92722768fce110df1aec4dd2c97442d95a03 Mon Sep 17 00:00:00 2001 From: Jacob Henn Date: Tue, 16 Aug 2022 14:54:31 -0700 Subject: [PATCH 2/4] fix format & amend --- client/src/main.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/client/src/main.rs b/client/src/main.rs index 080819d..a01b1b7 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,5 +1,5 @@ use anyhow::{bail, Context, Result}; -use args::{Opts, ChatOpts}; +use args::{ChatOpts, Opts}; use common::client_daemon::{ClientToDaemonMsg, DaemonToClientMsg}; use crossterm::{ cursor, @@ -9,16 +9,15 @@ use crossterm::{ ExecutableCommand, QueueableCommand, }; use mio::{unix::SourceFd, Events, Interest, Poll, Token}; -use structopt::StructOpt; use std::{ fs::File, io::{self, Read, StdoutLock, Write}, ops::ControlFlow, os::unix::prelude::AsRawFd, - panic, - thread, + panic, thread, time::Duration, }; +use structopt::StructOpt; mod args; @@ -39,7 +38,7 @@ struct State<'a> { impl<'a> State<'a> { fn new(opts: ChatOpts) -> Result { - let mut stdout = io::stdout().lock(); + let stdout = io::stdout().lock(); if !stdout.is_tty() { bail!("stdout is not a tty"); } From 2d55085b43a680772e2149cd50f693f387418bc8 Mon Sep 17 00:00:00 2001 From: Jacob Henn Date: Tue, 16 Aug 2022 14:57:25 -0700 Subject: [PATCH 3/4] amend again :( --- client/src/args.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/args.rs b/client/src/args.rs index 595509b..d7b020b 100644 --- a/client/src/args.rs +++ b/client/src/args.rs @@ -5,7 +5,6 @@ pub enum Opts { Chat(ChatOpts), } - #[derive(StructOpt)] pub struct ChatOpts { /// whether or not to give up on connecting to the daemon after 10 ms From 507dfe94833f0c63cfa05fa1b16c25a37aafda26 Mon Sep 17 00:00:00 2001 From: Jacob Henn Date: Tue, 16 Aug 2022 16:50:35 -0700 Subject: [PATCH 4/4] move some stuff out of main.rs --- client/src/fifo.rs | 58 +++++++++++++++++++ client/src/main.rs | 137 ++------------------------------------------- client/src/tui.rs | 85 ++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 132 deletions(-) create mode 100644 client/src/fifo.rs create mode 100644 client/src/tui.rs diff --git a/client/src/fifo.rs b/client/src/fifo.rs new file mode 100644 index 0000000..39eb88f --- /dev/null +++ b/client/src/fifo.rs @@ -0,0 +1,58 @@ +use crate::{args::ChatOpts, State}; +use anyhow::{bail, Context, Result}; +use common::client_daemon::{ClientToDaemonMsg, DaemonToClientMsg}; +use std::{fs::File, io::Read, panic, thread, time::Duration}; + +impl<'a> State<'a> { + /// try to open the two fifo files in their respective modes. timeout after 10 ms, unless the + /// `--no-timeout` option was provided. + pub(crate) fn try_get_fifos(opts: &ChatOpts) -> Result<(File, File)> { + let thread = thread::spawn(|| { + let home_dir = dirs::home_dir().context("couldn't get home directory")?; + + let ctodbuf = File::options() + .write(true) + .open(home_dir.join(".rclc/ctodbuf")) + .context("couldn't open client->daemon fifo")?; + + let dtocbuf = File::open(home_dir.join(".rclc/dtocbuf")) + .context("couldn't open daemon->client fifo")?; + + Ok((ctodbuf, dtocbuf)) + }); + + if !opts.no_timeout { + thread::sleep(Duration::from_millis(10)); + } + + if opts.no_timeout || thread.is_finished() { + match thread.join() { + Ok(o) => o, + Err(e) => panic::resume_unwind(e), + } + } else { + bail!("daemon is not running (10 ms timeout reached)"); + } + } + + pub(crate) fn send_fifo_msg(&mut self, msg: &ClientToDaemonMsg) -> Result<()> { + rmp_serde::encode::write(&mut self.ctodbuf, &msg) + .with_context(|| format!("couldn't write message {msg:?} to fifo")) + } + + pub(crate) fn handle_fifo_msg(&mut self) -> Result<()> { + let mut data = Vec::new(); + self.dtocbuf + .read_to_end(&mut data) + .context("couldn't read from fifo")?; + match rmp_serde::from_slice::(&data) { + Ok(msg) => print!("got fifo msg: {msg:?}\n\r"), + Err(e) => self.print_upwards(|| { + bunt::print!("{$red}invalid message from daemon: {}{/$}\n\r", e); + Ok(()) + })?, + } + + Ok(()) + } +} diff --git a/client/src/main.rs b/client/src/main.rs index a01b1b7..2648d3d 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,25 +1,19 @@ use anyhow::{bail, Context, Result}; use args::{ChatOpts, Opts}; -use common::client_daemon::{ClientToDaemonMsg, DaemonToClientMsg}; -use crossterm::{ - cursor, - event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, - terminal::{self, ClearType}, - tty::IsTty, - ExecutableCommand, QueueableCommand, -}; +use crossterm::{event, terminal, tty::IsTty}; use mio::{unix::SourceFd, Events, Interest, Poll, Token}; use std::{ fs::File, - io::{self, Read, StdoutLock, Write}, + io::{self, StdoutLock, Write}, ops::ControlFlow, os::unix::prelude::AsRawFd, - panic, thread, time::Duration, }; use structopt::StructOpt; mod args; +mod fifo; +mod tui; const PROMPT: &str = "> "; @@ -55,35 +49,6 @@ impl<'a> State<'a> { }) } - fn try_get_fifos(opts: &ChatOpts) -> Result<(File, File)> { - let thread = thread::spawn(|| { - let home_dir = dirs::home_dir().context("couldn't get home directory")?; - - let ctodbuf = File::options() - .write(true) - .open(home_dir.join(".rclc/ctodbuf")) - .context("couldn't open client->daemon fifo")?; - - let dtocbuf = File::open(home_dir.join(".rclc/dtocbuf")) - .context("couldn't open daemon->client fifo")?; - - Ok((ctodbuf, dtocbuf)) - }); - - if !opts.no_timeout { - thread::sleep(Duration::from_millis(10)); - } - - if opts.no_timeout || thread.is_finished() { - match thread.join() { - Ok(o) => o, - Err(e) => panic::resume_unwind(e), - } - } else { - bail!("daemon is not running (10 ms timeout reached)"); - } - } - /// start the event loop fn start(&mut self) -> Result<()> { // a mio poll lets you monitor for readiness events from multiple sources. @@ -139,83 +104,6 @@ impl<'a> State<'a> { Ok(()) } - - /// clear the prompt line, perform a function (function is assumed to print something - /// followed by `"\n\r"`), then restore the prompt line. stdout is not flushed. - fn print_upwards(&mut self, f: F) -> Result<()> - where - F: Fn() -> Result<()>, - { - self.stdout - .queue(terminal::Clear(ClearType::CurrentLine))? - .queue(cursor::MoveToColumn(0))?; - f()?; - print!("{}{}", PROMPT, self.input); - Ok(()) - } - - fn send_fifo_msg(&mut self, msg: &ClientToDaemonMsg) -> Result<()> { - rmp_serde::encode::write(&mut self.ctodbuf, &msg) - .with_context(|| format!("couldn't write message {msg:?} to fifo")) - } - - fn handle_fifo_msg(&mut self) -> Result<()> { - let mut data = Vec::new(); - self.dtocbuf - .read_to_end(&mut data) - .context("couldn't read from fifo")?; - match rmp_serde::from_slice::(&data) { - Ok(msg) => print!("got fifo msg: {msg:?}\n\r"), - Err(e) => self.print_upwards(|| { - bunt::print!("{$red}invalid message from daemon: {}{/$}\n\r", e); - Ok(()) - })?, - } - - Ok(()) - } - - fn handle_term_evt(&mut self) -> Result> { - let event = event::read().context("couldn't read next terminal event")?; - match event { - Event::Key(kev) => self.handle_kev(kev), - _ => Ok(ControlFlow::Continue(())), - } - } - - fn handle_kev(&mut self, kev: KeyEvent) -> Result> { - match kev.code { - KeyCode::Char('c') if kev.modifiers == KeyModifiers::CONTROL => { - // notify the event loop to break - return Ok(ControlFlow::Break(())); - } - KeyCode::Backspace => { - if !self.input.is_empty() { - self.input.pop(); - print!("\x08 \x08"); - } - } - KeyCode::Enter => { - if self.input.is_empty() { - return Ok(ControlFlow::Continue(())); - } - - self.send_fifo_msg(&ClientToDaemonMsg::Send(self.input.clone()))?; - self.input.clear(); - self.stdout - .queue(terminal::Clear(ClearType::CurrentLine))? - .queue(cursor::MoveToColumn(0))?; - print!("{}", PROMPT); - } - KeyCode::Char(c) => { - self.input.push(c); - print!("{c}"); - } - _ => (), - } - - Ok(ControlFlow::Continue(())) - } } fn chat(opts: ChatOpts) -> Result<()> { @@ -225,21 +113,6 @@ fn chat(opts: ChatOpts) -> Result<()> { Ok(()) } -#[allow(unused_must_use)] -/// Try our best to clean up the terminal state; if too many errors happen, just print some newlines and call it good. -fn cleanup() { - let mut stdout = io::stdout().lock(); - if stdout.is_tty() { - stdout.execute(cursor::Show); - if terminal::disable_raw_mode().is_ok() { - print!("\n\n"); - } else { - print!("\n\r\n\r"); - } - stdout.execute(terminal::Clear(ClearType::CurrentLine)); - } -} - fn go() -> Result<()> { match Opts::from_args() { Opts::Chat(chatopts) => chat(chatopts), @@ -248,7 +121,7 @@ fn go() -> Result<()> { fn main() { if let Err(e) = go() { - cleanup(); + tui::cleanup(); println!("rclc client error: {e:?}"); } } diff --git a/client/src/tui.rs b/client/src/tui.rs new file mode 100644 index 0000000..a42cffb --- /dev/null +++ b/client/src/tui.rs @@ -0,0 +1,85 @@ +use std::{io, ops::ControlFlow}; + +use crate::{State, PROMPT}; +use anyhow::{Context, Result}; +use common::client_daemon::ClientToDaemonMsg; +use crossterm::{ + cursor, + event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, + terminal::{self, ClearType}, + tty::IsTty, + ExecutableCommand, QueueableCommand, +}; + +impl<'a> State<'a> { + /// clear the prompt line, perform a function (function is assumed to print something + /// followed by `"\n\r"`), then restore the prompt line. stdout is not flushed. + pub(crate) fn print_upwards(&mut self, f: F) -> Result<()> + where + F: Fn() -> Result<()>, + { + self.stdout + .queue(terminal::Clear(ClearType::CurrentLine))? + .queue(cursor::MoveToColumn(0))?; + f()?; + print!("{}{}", PROMPT, self.input); + Ok(()) + } + + pub(crate) fn handle_term_evt(&mut self) -> Result> { + let event = event::read().context("couldn't read next terminal event")?; + match event { + Event::Key(kev) => self.handle_kev(kev), + _ => Ok(ControlFlow::Continue(())), + } + } + + pub(crate) fn handle_kev(&mut self, kev: KeyEvent) -> Result> { + match kev.code { + KeyCode::Char('c') if kev.modifiers == KeyModifiers::CONTROL => { + // notify the event loop to break + return Ok(ControlFlow::Break(())); + } + KeyCode::Backspace => { + if !self.input.is_empty() { + self.input.pop(); + print!("\x08 \x08"); + } + } + KeyCode::Enter => { + if self.input.is_empty() { + return Ok(ControlFlow::Continue(())); + } + + self.send_fifo_msg(&ClientToDaemonMsg::Send(self.input.clone()))?; + self.input.clear(); + self.stdout + .queue(terminal::Clear(ClearType::CurrentLine))? + .queue(cursor::MoveToColumn(0))?; + print!("{}", PROMPT); + } + KeyCode::Char(c) => { + self.input.push(c); + print!("{c}"); + } + _ => (), + } + + Ok(ControlFlow::Continue(())) + } +} + +#[allow(unused_must_use)] +/// Try our best to clean up the terminal state; if too many errors happen, just print some newlines and call it good. +pub fn cleanup() { + let mut stdout = io::stdout().lock(); + if stdout.is_tty() { + stdout.execute(cursor::Show); + if terminal::disable_raw_mode().is_ok() { + print!("\n\n"); + } else { + print!("\n\r\n\r"); + } + stdout.execute(terminal::Clear(ClearType::CurrentLine)); + } +}