add interactive mode to veilid-cli

This commit is contained in:
Christien Rioux 2024-03-02 23:49:12 -05:00
parent 931d145719
commit e98877fc71
9 changed files with 1762 additions and 1404 deletions

46
Cargo.lock generated
View File

@ -1255,6 +1255,23 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "crossterm"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
dependencies = [
"bitflags 2.4.2",
"crossterm_winapi",
"futures-core",
"libc",
"mio",
"parking_lot 0.12.1",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]] [[package]]
name = "crossterm_winapi" name = "crossterm_winapi"
version = "0.9.1" version = "0.9.1"
@ -1314,7 +1331,7 @@ dependencies = [
"async-std", "async-std",
"cfg-if 1.0.0", "cfg-if 1.0.0",
"crossbeam-channel", "crossbeam-channel",
"crossterm", "crossterm 0.25.0",
"cursive_core", "cursive_core",
"lazy_static", "lazy_static",
"libc", "libc",
@ -4324,6 +4341,22 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "rustyline-async"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6eb06391513b2184f0a5405c11a4a0a5302e8be442f4c5c35267187c2b37d5"
dependencies = [
"crossterm 0.27.0",
"futures-channel",
"futures-util",
"pin-project",
"thingbuf",
"thiserror",
"unicode-segmentation",
"unicode-width",
]
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.16" version = "1.0.16"
@ -4917,6 +4950,16 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "thingbuf"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4706f1bfb859af03f099ada2de3cea3e515843c2d3e93b7893f16d94a37f9415"
dependencies = [
"parking_lot 0.12.1",
"pin-project",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.56" version = "1.0.56"
@ -5641,6 +5684,7 @@ dependencies = [
"lru", "lru",
"owning_ref", "owning_ref",
"parking_lot 0.12.1", "parking_lot 0.12.1",
"rustyline-async",
"serde", "serde",
"serde_derive", "serde_derive",
"serial_test", "serial_test",

View File

@ -12,6 +12,7 @@ resolver = "2"
[patch.crates-io] [patch.crates-io]
cursive = { git = "https://gitlab.com/veilid/cursive.git" } cursive = { git = "https://gitlab.com/veilid/cursive.git" }
cursive_core = { git = "https://gitlab.com/veilid/cursive.git" } cursive_core = { git = "https://gitlab.com/veilid/cursive.git" }
nom = { git = "https://gitlab.com/emixa-d/ansi-parser.git" }
# For local development # For local development
# keyvaluedb = { path = "../keyvaluedb/keyvaluedb" } # keyvaluedb = { path = "../keyvaluedb/keyvaluedb" }

View File

@ -55,7 +55,7 @@ flexi_logger = { version = "^0", features = ["use_chrono_for_offset"] }
thiserror = "^1" thiserror = "^1"
crossbeam-channel = "^0" crossbeam-channel = "^0"
hex = "^0" hex = "^0"
veilid-tools = { version = "0.2.5", path = "../veilid-tools", default-features = false} veilid-tools = { version = "0.2.5", path = "../veilid-tools", default-features = false }
json = "^0" json = "^0"
stop-token = { version = "^0", default-features = false } stop-token = { version = "^0", default-features = false }
@ -67,6 +67,7 @@ chrono = "0.4.31"
owning_ref = "0.4.1" owning_ref = "0.4.1"
unicode-width = "0.1.11" unicode-width = "0.1.11"
lru = "0.10.1" lru = "0.10.1"
rustyline-async = "0.4.2"
[dev-dependencies] [dev-dependencies]
serial_test = "^2" serial_test = "^2"

View File

@ -80,7 +80,7 @@ impl ClientApiConnection {
async fn process_veilid_update(&self, update: json::JsonValue) { async fn process_veilid_update(&self, update: json::JsonValue) {
let comproc = self.inner.lock().comproc.clone(); let comproc = self.inner.lock().comproc.clone();
let Some(kind) = update["kind"].as_str() else { let Some(kind) = update["kind"].as_str() else {
comproc.log_message(Level::Error, format!("missing update kind: {}", update)); comproc.log_message(Level::Error, &format!("missing update kind: {}", update));
return; return;
}; };
match kind { match kind {
@ -110,7 +110,7 @@ impl ClientApiConnection {
comproc.update_value_change(&update); comproc.update_value_change(&update);
} }
_ => { _ => {
comproc.log_message(Level::Error, format!("unknown update kind: {}", update)); comproc.log_message(Level::Error, &format!("unknown update kind: {}", update));
} }
} }
} }

View File

@ -41,7 +41,7 @@ impl ConnectionState {
} }
struct CommandProcessorInner { struct CommandProcessorInner {
ui_sender: UISender, ui_sender: Box<dyn UISender>,
capi: Option<ClientApiConnection>, capi: Option<ClientApiConnection>,
reconnect: bool, reconnect: bool,
finished: bool, finished: bool,
@ -60,7 +60,7 @@ pub struct CommandProcessor {
} }
impl CommandProcessor { impl CommandProcessor {
pub fn new(ui_sender: UISender, settings: &Settings) -> Self { pub fn new(ui_sender: Box<dyn UISender>, settings: &Settings) -> Self {
Self { Self {
inner: Arc::new(Mutex::new(CommandProcessorInner { inner: Arc::new(Mutex::new(CommandProcessorInner {
ui_sender, ui_sender,
@ -86,8 +86,8 @@ impl CommandProcessor {
fn inner_mut(&self) -> MutexGuard<CommandProcessorInner> { fn inner_mut(&self) -> MutexGuard<CommandProcessorInner> {
self.inner.lock() self.inner.lock()
} }
fn ui_sender(&self) -> UISender { fn ui_sender(&self) -> Box<dyn UISender> {
self.inner.lock().ui_sender.clone() self.inner.lock().ui_sender.clone_uisender()
} }
fn capi(&self) -> ClientApiConnection { fn capi(&self) -> ClientApiConnection {
self.inner.lock().capi.as_ref().unwrap().clone() self.inner.lock().capi.as_ref().unwrap().clone()
@ -126,7 +126,7 @@ impl CommandProcessor {
ui.add_node_event( ui.add_node_event(
Level::Info, Level::Info,
format!( &format!(
r#"Client Commands: r#"Client Commands:
exit/quit exit the client exit/quit exit the client
disconnect disconnect the client from the Veilid node disconnect disconnect the client from the Veilid node
@ -190,11 +190,11 @@ Server Debug Commands:
spawn_detached_local(async move { spawn_detached_local(async move {
match capi.server_debug(command_line).await { match capi.server_debug(command_line).await {
Ok(output) => { Ok(output) => {
ui.add_node_event(Level::Info, output); ui.add_node_event(Level::Info, &output);
ui.send_callback(callback); ui.send_callback(callback);
} }
Err(e) => { Err(e) => {
ui.add_node_event(Level::Error, e.to_string()); ui.add_node_event(Level::Error, &e);
ui.send_callback(callback); ui.send_callback(callback);
} }
} }
@ -215,7 +215,7 @@ Server Debug Commands:
let log_level = match convert_loglevel(&rest.unwrap_or_default()) { let log_level = match convert_loglevel(&rest.unwrap_or_default()) {
Ok(v) => v, Ok(v) => v,
Err(e) => { Err(e) => {
ui.add_node_event(Level::Error, format!("Failed to change log level: {}", e)); ui.add_node_event(Level::Error, &format!("Failed to change log level: {}", e));
ui.send_callback(callback); ui.send_callback(callback);
return; return;
} }
@ -228,7 +228,7 @@ Server Debug Commands:
Err(e) => { Err(e) => {
ui.display_string_dialog( ui.display_string_dialog(
"Server command 'change_log_level' failed", "Server command 'change_log_level' failed",
e.to_string(), &e,
callback, callback,
); );
} }
@ -247,11 +247,11 @@ Server Debug Commands:
match flag.as_str() { match flag.as_str() {
"app_messages" => { "app_messages" => {
this.inner.lock().enable_app_messages = true; this.inner.lock().enable_app_messages = true;
ui.add_node_event(Level::Info, format!("flag enabled: {}", flag)); ui.add_node_event(Level::Info, &format!("flag enabled: {}", flag));
ui.send_callback(callback); ui.send_callback(callback);
} }
_ => { _ => {
ui.add_node_event(Level::Error, format!("unknown flag: {}", flag)); ui.add_node_event(Level::Error, &format!("unknown flag: {}", flag));
ui.send_callback(callback); ui.send_callback(callback);
} }
} }
@ -269,11 +269,11 @@ Server Debug Commands:
match flag.as_str() { match flag.as_str() {
"app_messages" => { "app_messages" => {
this.inner.lock().enable_app_messages = false; this.inner.lock().enable_app_messages = false;
ui.add_node_event(Level::Info, format!("flag disabled: {}", flag)); ui.add_node_event(Level::Info, &format!("flag disabled: {}", flag));
ui.send_callback(callback); ui.send_callback(callback);
} }
_ => { _ => {
ui.add_node_event(Level::Error, format!("unknown flag: {}", flag)); ui.add_node_event(Level::Error, &format!("unknown flag: {}", flag));
ui.send_callback(callback); ui.send_callback(callback);
} }
} }
@ -413,13 +413,13 @@ Server Debug Commands:
// calls into ui // calls into ui
//////////////////////////////////////////// ////////////////////////////////////////////
pub fn log_message(&self, log_level: Level, message: String) { pub fn log_message(&self, log_level: Level, message: &str) {
self.inner().ui_sender.add_node_event(log_level, message); self.inner().ui_sender.add_node_event(log_level, message);
} }
pub fn update_attachment(&self, attachment: &json::JsonValue) { pub fn update_attachment(&self, attachment: &json::JsonValue) {
self.inner_mut().ui_sender.set_attachment_state( self.inner_mut().ui_sender.set_attachment_state(
attachment["state"].as_str().unwrap_or_default().to_owned(), attachment["state"].as_str().unwrap_or_default(),
attachment["public_internet_ready"] attachment["public_internet_ready"]
.as_bool() .as_bool()
.unwrap_or_default(), .unwrap_or_default(),
@ -458,7 +458,7 @@ Server Debug Commands:
)); ));
} }
if !out.is_empty() { if !out.is_empty() {
self.inner().ui_sender.add_node_event(Level::Info, out); self.inner().ui_sender.add_node_event(Level::Info, &out);
} }
} }
pub fn update_value_change(&self, value_change: &json::JsonValue) { pub fn update_value_change(&self, value_change: &json::JsonValue) {
@ -475,7 +475,7 @@ Server Debug Commands:
datastr, datastr,
if truncated { "..." } else { "" } if truncated { "..." } else { "" }
); );
self.inner().ui_sender.add_node_event(Level::Info, out); self.inner().ui_sender.add_node_event(Level::Info, &out);
} }
pub fn update_log(&self, log: &json::JsonValue) { pub fn update_log(&self, log: &json::JsonValue) {
@ -483,7 +483,7 @@ Server Debug Commands:
Level::from_str(log["log_level"].as_str().unwrap_or("error")).unwrap_or(Level::Error); Level::from_str(log["log_level"].as_str().unwrap_or("error")).unwrap_or(Level::Error);
self.inner().ui_sender.add_node_event( self.inner().ui_sender.add_node_event(
log_level, log_level,
format!( &format!(
"{}: {}{}", "{}: {}{}",
log["log_level"].as_str().unwrap_or("???"), log["log_level"].as_str().unwrap_or("???"),
log["message"].as_str().unwrap_or("???"), log["message"].as_str().unwrap_or("???"),
@ -530,7 +530,7 @@ Server Debug Commands:
self.inner().ui_sender.add_node_event( self.inner().ui_sender.add_node_event(
Level::Info, Level::Info,
format!( &format!(
"AppMessage ({:?}): {}{}", "AppMessage ({:?}): {}{}",
msg["sender"], msg["sender"],
strmsg, strmsg,
@ -570,7 +570,7 @@ Server Debug Commands:
self.inner().ui_sender.add_node_event( self.inner().ui_sender.add_node_event(
Level::Info, Level::Info,
format!( &format!(
"AppCall ({:?}) id = {:016x} : {}{}", "AppCall ({:?}) id = {:016x} : {}{}",
call["sender"], call["sender"],
id, id,

1430
veilid-cli/src/cursive_ui.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,189 @@
use std::io::Write;
use crate::command_processor::*;
use crate::settings::*;
use crate::tools::*;
use crate::ui::*;
use flexi_logger::writers::LogWriter;
use rustyline_async::SharedWriter;
use rustyline_async::{Readline, ReadlineError, ReadlineEvent};
pub type InteractiveUICallback = Box<dyn FnMut() + Send>;
pub struct InteractiveUIInner {
cmdproc: Option<CommandProcessor>,
stdout: Option<SharedWriter>,
error: Option<String>,
done: bool,
}
#[derive(Clone)]
pub struct InteractiveUI {
inner: Arc<Mutex<InteractiveUIInner>>,
}
impl InteractiveUI {
pub fn new(_settings: &Settings) -> (Self, InteractiveUISender) {
// Create the UI object
let this = Self {
inner: Arc::new(Mutex::new(InteractiveUIInner {
cmdproc: None,
stdout: None,
error: None,
done: false,
})),
};
let ui_sender = InteractiveUISender {
inner: this.inner.clone(),
};
(this, ui_sender)
}
pub async fn command_loop(&self) {
let (mut readline, mut stdout) =
match Readline::new("> ".to_owned()).map_err(|e| e.to_string()) {
Ok(v) => v,
Err(e) => {
println!("Error: {:?}", e);
return;
}
};
self.inner.lock().stdout = Some(stdout.clone());
loop {
if self.inner.lock().done {
break;
}
if let Some(e) = self.inner.lock().error.clone() {
println!("Error: {:?}", e);
break;
}
match readline.readline().await {
Ok(ReadlineEvent::Line(line)) => {
let line = line.trim();
if line == "clear" {
if let Err(e) = readline.clear() {
println!("Error: {:?}", e);
}
} else if !line.is_empty() {
readline.add_history_entry(line.to_string());
let cmdproc = self.inner.lock().cmdproc.clone();
if let Some(cmdproc) = &cmdproc {
if let Err(e) = cmdproc.run_command(
line,
UICallback::Interactive(Box::new({
//let mut stdout = stdout.clone();
move || {
// if let Err(e) = writeln!(stdout) {
// println!("Error: {:?}", e);
// }
}
})),
) {
if let Err(e) = writeln!(stdout, "Error: {}", e) {
println!("Error: {:?}", e);
break;
}
}
}
}
}
Ok(ReadlineEvent::Interrupted) => {
break;
}
Ok(ReadlineEvent::Eof) => {
break;
}
Err(ReadlineError::Closed) => {}
Err(ReadlineError::IO(e)) => {
println!("IO Error: {:?}", e);
break;
}
}
}
let _ = readline.flush();
}
}
impl UI for InteractiveUI {
fn set_command_processor(&mut self, cmdproc: CommandProcessor) {
let mut inner = self.inner.lock();
inner.cmdproc = Some(cmdproc);
}
// Note: Cursive is not re-entrant, can't borrow_mut self.siv again after this
fn run_async(&mut self) -> Pin<Box<dyn core::future::Future<Output = ()>>> {
let this = self.clone();
Box::pin(async move {
this.command_loop().await;
})
}
}
//////////////////////////////////////////////////////////////////////////////
#[derive(Clone)]
pub struct InteractiveUISender {
inner: Arc<Mutex<InteractiveUIInner>>,
}
impl UISender for InteractiveUISender {
fn clone_uisender(&self) -> Box<dyn UISender> {
Box::new(self.clone())
}
fn as_logwriter(&self) -> Option<Box<dyn LogWriter>> {
None
}
fn display_string_dialog(&self, title: &str, text: &str, close_cb: UICallback) {
println!("{}: {}", title, text);
if let UICallback::Interactive(mut close_cb) = close_cb {
close_cb()
}
}
fn quit(&self) {
self.inner.lock().done = true;
}
fn send_callback(&self, callback: UICallback) {
if let UICallback::Interactive(mut callback) = callback {
callback();
}
}
fn set_attachment_state(
&mut self,
_state: &str,
_public_internet_ready: bool,
_local_network_ready: bool,
) {
//
}
fn set_network_status(
&mut self,
_started: bool,
_bps_down: u64,
_bps_up: u64,
mut _peers: Vec<json::JsonValue>,
) {
//
}
fn set_config(&mut self, _config: &json::JsonValue) {
//
}
fn set_connection_state(&mut self, _state: ConnectionState) {
//
}
fn add_node_event(&self, _log_color: Level, event: &str) {
let Some(mut stdout) = self.inner.lock().stdout.clone() else {
return;
};
if let Err(e) = writeln!(stdout, "{}", event) {
self.inner.lock().error = Some(e.to_string());
}
}
}

View File

@ -3,7 +3,7 @@
#![deny(unused_must_use)] #![deny(unused_must_use)]
#![recursion_limit = "256"] #![recursion_limit = "256"]
use crate::{settings::NamedSocketAddrs, tools::*}; use crate::{settings::NamedSocketAddrs, tools::*, ui::*};
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use flexi_logger::*; use flexi_logger::*;
@ -11,6 +11,8 @@ use std::path::PathBuf;
mod cached_text_view; mod cached_text_view;
mod client_api_connection; mod client_api_connection;
mod command_processor; mod command_processor;
mod cursive_ui;
mod interactive_ui;
mod peers_table_view; mod peers_table_view;
mod settings; mod settings;
mod tools; mod tools;
@ -31,7 +33,7 @@ struct CmdlineArgs {
#[arg(long, short = 'p')] #[arg(long, short = 'p')]
ipc_path: Option<PathBuf>, ipc_path: Option<PathBuf>,
/// Subnode index to use when connecting /// Subnode index to use when connecting
#[arg(long, short = 'i', default_value = "0")] #[arg(long, default_value = "0")]
subnode_index: usize, subnode_index: usize,
/// Address to connect to /// Address to connect to
#[arg(long, short = 'a')] #[arg(long, short = 'a')]
@ -45,6 +47,23 @@ struct CmdlineArgs {
/// log level /// log level
#[arg(value_enum)] #[arg(value_enum)]
log_level: Option<LogLevel>, log_level: Option<LogLevel>,
/// interactive
#[arg(long, short = 'i', group = "execution_mode")]
interactive: bool,
/// evaluate
#[arg(long, short = 'e', group = "execution_mode")]
evaluate: Option<String>,
/// show log
#[arg(long, short = 'l', group = "execution_mode")]
show_log: bool,
/// read commands from file
#[arg(
long,
short = 'f',
group = "execution_mode",
value_name = "COMMAND_FILE"
)]
command_file: Option<PathBuf>,
} }
fn main() -> Result<(), String> { fn main() -> Result<(), String> {
@ -78,8 +97,29 @@ fn main() -> Result<(), String> {
settings.logging.terminal.enabled = true; settings.logging.terminal.enabled = true;
} }
// If we are running in interactive mode disable some things
let mut enable_cursive = true;
if args.interactive || args.show_log || args.command_file.is_some() || args.evaluate.is_some() {
settings.logging.terminal.enabled = false;
enable_cursive = false;
}
// Create UI object // Create UI object
let (mut sivui, uisender) = ui::UI::new(settings.interface.node_log.scrollback, &settings); let (mut ui, uisender) = if enable_cursive {
let (ui, uisender) = cursive_ui::CursiveUI::new(&settings);
(
Box::new(ui) as Box<dyn UI>,
Box::new(uisender) as Box<dyn UISender>,
)
} else if args.interactive {
let (ui, uisender) = interactive_ui::InteractiveUI::new(&settings);
(
Box::new(ui) as Box<dyn UI>,
Box::new(uisender) as Box<dyn UISender>,
)
} else {
panic!("unknown ui mode");
};
// Set up loggers // Set up loggers
{ {
@ -105,13 +145,13 @@ fn main() -> Result<(), String> {
FileSpec::default() FileSpec::default()
.directory(settings.logging.file.directory.clone()) .directory(settings.logging.file.directory.clone())
.suppress_timestamp(), .suppress_timestamp(),
Box::new(uisender.clone()), uisender.as_logwriter().unwrap(),
) )
.start() .start()
.expect("failed to initialize logger!"); .expect("failed to initialize logger!");
} else { } else {
logger logger
.log_to_writer(Box::new(uisender.clone())) .log_to_writer(uisender.as_logwriter().unwrap())
.start() .start()
.expect("failed to initialize logger!"); .expect("failed to initialize logger!");
} }
@ -195,7 +235,8 @@ fn main() -> Result<(), String> {
// Create command processor // Create command processor
debug!("Creating Command Processor "); debug!("Creating Command Processor ");
let comproc = command_processor::CommandProcessor::new(uisender, &settings); let comproc = command_processor::CommandProcessor::new(uisender, &settings);
sivui.set_command_processor(comproc.clone());
ui.set_command_processor(comproc.clone());
// Create client api client side // Create client api client side
info!("Starting API connection"); info!("Starting API connection");
@ -221,7 +262,7 @@ fn main() -> Result<(), String> {
block_on(async move { block_on(async move {
// Start UI // Start UI
let ui_future = async move { let ui_future = async move {
sivui.run_async().await; ui.run_async().await;
// When UI quits, close connection and command processor cleanly // When UI quits, close connection and command processor cleanly
comproc2.quit(); comproc2.quit();

File diff suppressed because it is too large Load Diff