use anyhow::{anyhow, Context, Result}; use std::{ io::{ErrorKind, Read, Write}, str::SplitWhitespace, thread, }; use crate::{client::Position, config::DEFAULT_PORT}; use super::{FrontendEvent, FrontendRequest}; pub fn run() -> Result<()> { let Ok(mut tx) = super::wait_for_service() else { return Err(anyhow!("Could not connect to lan-mouse-socket")); }; let mut rx = tx.try_clone()?; let reader = thread::Builder::new() .name("cli-frontend".to_string()) .spawn(move || { // all further prompts prompt(); loop { let mut buf = String::new(); match std::io::stdin().read_line(&mut buf) { Ok(0) => return, Ok(len) => { if let Some(events) = parse_cmd(buf, len) { for event in events.iter() { let json = serde_json::to_string(&event).unwrap(); let bytes = json.as_bytes(); let len = bytes.len().to_be_bytes(); if let Err(e) = tx.write(&len) { log::error!("error sending message: {e}"); }; if let Err(e) = tx.write(bytes) { log::error!("error sending message: {e}"); }; if *event == FrontendRequest::Terminate() { return; } } // prompt is printed after the server response is received } else { prompt(); } } Err(e) => { if e.kind() != ErrorKind::UnexpectedEof { log::error!("error reading from stdin: {e}"); } return; } } } })?; let _ = thread::Builder::new() .name("cli-frontend-notify".to_string()) .spawn(move || { loop { // read len let mut len = [0u8; 8]; match rx.read_exact(&mut len) { Ok(()) => (), Err(e) if e.kind() == ErrorKind::UnexpectedEof => break, Err(e) => break log::error!("{e}"), }; let len = usize::from_be_bytes(len); // read payload let mut buf: Vec = vec![0u8; len]; match rx.read_exact(&mut buf[..len]) { Ok(()) => (), Err(e) if e.kind() == ErrorKind::UnexpectedEof => break, Err(e) => break log::error!("{e}"), }; let notify: FrontendEvent = match serde_json::from_slice(&buf) { Ok(n) => n, Err(e) => break log::error!("{e}"), }; match notify { FrontendEvent::Activated(handle, active) => { if active { log::info!("client {handle} activated"); } else { log::info!("client {handle} deactivated"); } } FrontendEvent::Created(handle, client) => { let port = client.port; let pos = client.pos; let hostname = client.hostname.as_deref().unwrap_or(""); log::info!("new client ({handle}): {hostname}:{port} - {pos}"); } FrontendEvent::Updated(handle, client) => { let port = client.port; let pos = client.pos; let hostname = client.hostname.as_deref().unwrap_or(""); log::info!("client ({handle}) updated: {hostname}:{port} - {pos}"); } FrontendEvent::Deleted(client) => { log::info!("client ({client}) deleted."); } FrontendEvent::Error(e) => { log::warn!("{e}"); } FrontendEvent::Enumerate(clients) => { for (handle, client, active) in clients.into_iter() { log::info!( "client ({}) [{}]: active: {}, associated addresses: [{}]", handle, client.hostname.as_deref().unwrap_or(""), if active { "yes" } else { "no" }, client .ips .into_iter() .map(|a| a.to_string()) .collect::>() .join(", ") ); } } FrontendEvent::PortChanged(port, msg) => match msg { Some(msg) => log::info!("could not change port: {msg}"), None => log::info!("port changed: {port}"), }, } prompt(); } })?; match reader.join() { Ok(_) => {} Err(e) => { let msg = match (e.downcast_ref::<&str>(), e.downcast_ref::()) { (Some(&s), _) => s, (_, Some(s)) => s, _ => "no panic info", }; log::error!("reader thread paniced: {msg}"); } } Ok(()) } fn prompt() { eprint!("lan-mouse > "); std::io::stderr().flush().unwrap(); } fn parse_cmd(s: String, len: usize) -> Option> { if len == 0 { return Some(vec![FrontendRequest::Terminate()]); } let mut l = s.split_whitespace(); let cmd = l.next()?; let res = match cmd { "help" => { log::info!("list list clients"); log::info!("connect left|right|top|bottom [port] add a new client"); log::info!("disconnect remove a client"); log::info!("activate activate a client"); log::info!("deactivate deactivate a client"); log::info!("exit exit lan-mouse"); log::info!("setport change port"); None } "exit" => return Some(vec![FrontendRequest::Terminate()]), "list" => return Some(vec![FrontendRequest::Enumerate()]), "connect" => Some(parse_connect(l)), "disconnect" => Some(parse_disconnect(l)), "activate" => Some(parse_activate(l)), "deactivate" => Some(parse_deactivate(l)), "setport" => Some(parse_port(l)), _ => { log::error!("unknown command: {s}"); None } }; match res { Some(Ok(e)) => Some(e), Some(Err(e)) => { log::warn!("{e}"); None } _ => None, } } fn parse_connect(mut l: SplitWhitespace) -> Result> { let usage = "usage: connect left|right|top|bottom [port]"; let host = l.next().context(usage)?.to_owned(); let pos = match l.next().context(usage)? { "right" => Position::Right, "top" => Position::Top, "bottom" => Position::Bottom, _ => Position::Left, }; let port = if let Some(p) = l.next() { p.parse()? } else { DEFAULT_PORT }; Ok(vec![ FrontendRequest::Create(Some(host), port, pos), FrontendRequest::Enumerate(), ]) } fn parse_disconnect(mut l: SplitWhitespace) -> Result> { let client = l.next().context("usage: disconnect ")?.parse()?; Ok(vec![ FrontendRequest::Delete(client), FrontendRequest::Enumerate(), ]) } fn parse_activate(mut l: SplitWhitespace) -> Result> { let client = l.next().context("usage: activate ")?.parse()?; Ok(vec![ FrontendRequest::Activate(client, true), FrontendRequest::Enumerate(), ]) } fn parse_deactivate(mut l: SplitWhitespace) -> Result> { let client = l.next().context("usage: deactivate ")?.parse()?; Ok(vec![ FrontendRequest::Activate(client, false), FrontendRequest::Enumerate(), ]) } fn parse_port(mut l: SplitWhitespace) -> Result> { let port = l.next().context("usage: setport ")?.parse()?; Ok(vec![FrontendRequest::ChangePort(port)]) }