Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make client timeout if daemon is not running #58

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions client/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ 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,
}
58 changes: 58 additions & 0 deletions client/src/fifo.rs
Original file line number Diff line number Diff line change
@@ -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::<DaemonToClientMsg>(&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(())
}
}
150 changes: 14 additions & 136 deletions client/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
use anyhow::{bail, Context, Result};
use args::Opts;
use common::client_daemon::{ClientToDaemonMsg, DaemonToClientMsg};
use crossterm::{
cursor,
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
terminal::{self, ClearType},
tty::IsTty,
ExecutableCommand, QueueableCommand,
};
use args::{ChatOpts, Opts};
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,
time::Duration,
};
use structopt::StructOpt;

mod args;
mod fifo;
mod tui;

const PROMPT: &str = "> ";

Expand All @@ -35,41 +31,15 @@ struct State<'a> {
}

impl<'a> State<'a> {
fn new() -> Result<Self> {
let mut stdout = io::stdout().lock();
fn new(opts: ChatOpts) -> Result<Self> {
let stdout = io::stdout().lock();
if !stdout.is_tty() {
bail!("stdout is not a tty");
}

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,
Expand Down Expand Up @@ -134,116 +104,24 @@ 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<F>(&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::<DaemonToClientMsg>(&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<ControlFlow<()>> {
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<ControlFlow<()>> {
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() -> Result<()> {
let mut state = State::new()?;
fn chat(opts: ChatOpts) -> Result<()> {
let mut state = State::new(opts)?;
state.start()?;

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::get() {
Opts::Chat => chat(),
match Opts::from_args() {
Opts::Chat(chatopts) => chat(chatopts),
}
}

fn main() {
if let Err(e) = go() {
cleanup();
tui::cleanup();
println!("rclc client error: {e:?}");
}
}
85 changes: 85 additions & 0 deletions client/src/tui.rs
Original file line number Diff line number Diff line change
@@ -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<F>(&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<ControlFlow<()>> {
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<ControlFlow<()>> {
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));
}
}