initial import of main veilid core

This commit is contained in:
John Smith
2021-11-22 11:28:30 -05:00
parent c4cd54e020
commit 9e94a6a96f
218 changed files with 34880 additions and 1 deletions

View File

@@ -0,0 +1,214 @@
use crate::command_processor::*;
use crate::veilid_client_capnp::*;
use anyhow::*;
use async_std::prelude::*;
use capnp::capability::Promise;
use capnp_rpc::{pry, rpc_twoparty_capnp, twoparty, Disconnector, RpcSystem};
use futures::AsyncReadExt;
use log::*;
use std::cell::RefCell;
use std::net::SocketAddr;
use std::rc::Rc;
struct VeilidClientImpl {
comproc: CommandProcessor,
}
impl VeilidClientImpl {
pub fn new(comproc: CommandProcessor) -> Self {
Self { comproc: comproc }
}
}
impl veilid_client::Server for VeilidClientImpl {
fn state_changed(
&mut self,
params: veilid_client::StateChangedParams,
_results: veilid_client::StateChangedResults,
) -> Promise<(), ::capnp::Error> {
let changed = pry!(pry!(params.get()).get_changed());
if changed.has_attachment() {
let attachment = pry!(changed.get_attachment());
let old_state = pry!(attachment.get_old_state());
let new_state = pry!(attachment.get_new_state());
trace!(
"AttachmentStateChange: old_state={} new_state={}",
old_state as u16,
new_state as u16
);
self.comproc.set_attachment_state(new_state);
}
Promise::ok(())
}
}
struct ClientApiConnectionInner {
comproc: CommandProcessor,
connect_addr: Option<SocketAddr>,
disconnector: Option<Disconnector<rpc_twoparty_capnp::Side>>,
server: Option<Rc<RefCell<veilid_server::Client>>>,
disconnect_requested: bool,
}
type Handle<T> = Rc<RefCell<T>>;
#[derive(Clone)]
pub struct ClientApiConnection {
inner: Handle<ClientApiConnectionInner>,
}
impl ClientApiConnection {
pub fn new(comproc: CommandProcessor) -> Self {
Self {
inner: Rc::new(RefCell::new(ClientApiConnectionInner {
comproc: comproc,
connect_addr: None,
disconnector: None,
server: None,
disconnect_requested: false,
})),
}
}
async fn handle_connection(&mut self) -> Result<()> {
trace!("ClientApiConnection::handle_connection");
let connect_addr = self.inner.borrow().connect_addr.unwrap().clone();
// Connect the TCP socket
let stream = async_std::net::TcpStream::connect(connect_addr.clone()).await?;
// If it succeed, disable nagle algorithm
stream.set_nodelay(true)?;
// Create the VAT network
let (reader, writer) = stream.split();
let rpc_network = Box::new(twoparty::VatNetwork::new(
reader,
writer,
rpc_twoparty_capnp::Side::Client,
Default::default(),
));
// Create the rpc system
let mut rpc_system = RpcSystem::new(rpc_network, None);
let mut request;
{
let mut inner = self.inner.borrow_mut();
// Get the bootstrap server connection object
inner.server = Some(Rc::new(RefCell::new(
rpc_system.bootstrap(rpc_twoparty_capnp::Side::Server),
)));
// Store our disconnector future for later (must happen after bootstrap, contrary to documentation)
inner.disconnector = Some(rpc_system.get_disconnector());
// Get a client object to pass to the server for status update callbacks
let client = capnp_rpc::new_client(VeilidClientImpl::new(inner.comproc.clone()));
// Register our client and get a registration object back
request = inner
.server
.as_ref()
.unwrap()
.borrow_mut()
.register_request();
request.get().set_veilid_client(client);
inner
.comproc
.set_connection_state(ConnectionState::Connected(
connect_addr,
std::time::SystemTime::now(),
));
}
// Don't drop the registration
rpc_system.try_join(request.send().promise).await?;
// Drop the server and disconnector too (if we still have it)
let mut inner = self.inner.borrow_mut();
let disconnect_requested = inner.disconnect_requested;
inner.server = None;
inner.disconnector = None;
inner.disconnect_requested = false;
if !disconnect_requested {
// Connection lost
Err(anyhow!("Connection lost"))
} else {
// Connection finished
Ok(())
}
}
pub async fn server_attach(&mut self) -> Result<bool> {
trace!("ClientApiConnection::server_attach");
let server = {
let inner = self.inner.borrow();
inner
.server
.as_ref()
.ok_or(anyhow!("Not connected, ignoring attach request"))?
.clone()
};
let request = server.borrow().attach_request();
let response = request.send().promise.await?;
Ok(response.get()?.get_result())
}
pub async fn server_detach(&mut self) -> Result<bool> {
trace!("ClientApiConnection::server_detach");
let server = {
let inner = self.inner.borrow();
inner
.server
.as_ref()
.ok_or(anyhow!("Not connected, ignoring detach request"))?
.clone()
};
let request = server.borrow().detach_request();
let response = request.send().promise.await?;
Ok(response.get()?.get_result())
}
pub async fn server_shutdown(&mut self) -> Result<bool> {
trace!("ClientApiConnection::server_shutdown");
let server = {
let inner = self.inner.borrow();
inner
.server
.as_ref()
.ok_or(anyhow!("Not connected, ignoring attach request"))?
.clone()
};
let request = server.borrow().shutdown_request();
let response = request.send().promise.await?;
Ok(response.get()?.get_result())
}
// Start Client API connection
pub async fn connect(&mut self, connect_addr: SocketAddr) -> Result<()> {
trace!("ClientApiConnection::connect");
// Save the address to connect to
self.inner.borrow_mut().connect_addr = Some(connect_addr);
self.handle_connection().await
}
// End Client API connection
pub async fn disconnect(&mut self) {
trace!("ClientApiConnection::disconnect");
let disconnector = self.inner.borrow_mut().disconnector.take();
match disconnector {
Some(d) => {
self.inner.borrow_mut().disconnect_requested = true;
d.await.unwrap();
self.inner.borrow_mut().connect_addr = None;
}
None => {
debug!("disconnector doesn't exist");
}
}
}
}

View File

@@ -0,0 +1,325 @@
use crate::client_api_connection::*;
use crate::settings::Settings;
use crate::ui::*;
use crate::veilid_client_capnp::*;
use async_std::prelude::FutureExt;
use log::*;
use std::cell::*;
use std::net::SocketAddr;
use std::rc::Rc;
use std::time::{Duration, SystemTime};
use veilid_core::xx::{Eventual, EventualCommon};
#[derive(PartialEq, Clone)]
pub enum ConnectionState {
Disconnected,
Connected(SocketAddr, SystemTime),
Retrying(SocketAddr, SystemTime),
}
impl ConnectionState {
pub fn is_disconnected(&self) -> bool {
match *self {
Self::Disconnected => true,
_ => false,
}
}
pub fn is_connected(&self) -> bool {
match *self {
Self::Connected(_, _) => true,
_ => false,
}
}
pub fn is_retrying(&self) -> bool {
match *self {
Self::Retrying(_, _) => true,
_ => false,
}
}
}
struct CommandProcessorInner {
ui: UI,
capi: Option<ClientApiConnection>,
reconnect: bool,
finished: bool,
autoconnect: bool,
autoreconnect: bool,
server_addr: Option<SocketAddr>,
connection_waker: Eventual,
}
type Handle<T> = Rc<RefCell<T>>;
#[derive(Clone)]
pub struct CommandProcessor {
inner: Handle<CommandProcessorInner>,
}
impl CommandProcessor {
pub fn new(ui: UI, settings: &Settings) -> Self {
Self {
inner: Rc::new(RefCell::new(CommandProcessorInner {
ui: ui,
capi: None,
reconnect: settings.autoreconnect,
finished: false,
autoconnect: settings.autoconnect,
autoreconnect: settings.autoreconnect,
server_addr: None,
connection_waker: Eventual::new(),
})),
}
}
pub fn set_client_api_connection(&mut self, capi: ClientApiConnection) {
self.inner.borrow_mut().capi = Some(capi);
}
fn inner(&self) -> Ref<CommandProcessorInner> {
self.inner.borrow()
}
fn inner_mut(&self) -> RefMut<CommandProcessorInner> {
self.inner.borrow_mut()
}
fn ui(&self) -> UI {
self.inner.borrow().ui.clone()
}
fn capi(&self) -> ClientApiConnection {
self.inner.borrow().capi.as_ref().unwrap().clone()
}
fn word_split(line: &str) -> (String, Option<String>) {
let trimmed = line.trim();
if let Some(p) = trimmed.find(char::is_whitespace) {
let first = trimmed[0..p].to_owned();
let rest = trimmed[p..].trim_start().to_owned();
(first, Some(rest))
} else {
(trimmed.to_owned(), None)
}
}
pub fn cmd_help(&self, _rest: Option<String>, callback: UICallback) -> Result<(), String> {
trace!("CommandProcessor::cmd_help");
self.ui().add_node_event(
r#"Commands:
exit/quit - exit the client
disconnect - disconnect the client from the Veilid node
shutdown - shut the server down
attach - attach the server to the Veilid network
detach - detach the server from the Veilid network
"#,
);
let ui = self.ui();
callback(ui);
Ok(())
}
pub fn cmd_exit(&self, callback: UICallback) -> Result<(), String> {
trace!("CommandProcessor::cmd_exit");
let ui = self.ui();
callback(ui);
//
self.ui().quit();
Ok(())
}
pub fn cmd_shutdown(&self, callback: UICallback) -> Result<(), String> {
trace!("CommandProcessor::cmd_shutdown");
let mut capi = self.capi();
let ui = self.ui();
async_std::task::spawn_local(async move {
if let Err(e) = capi.server_shutdown().await {
error!("Server command 'shutdown' failed to execute: {}", e);
}
callback(ui);
});
Ok(())
}
pub fn cmd_attach(&self, callback: UICallback) -> Result<(), String> {
trace!("CommandProcessor::cmd_attach");
let mut capi = self.capi();
let ui = self.ui();
async_std::task::spawn_local(async move {
if let Err(e) = capi.server_attach().await {
error!("Server command 'attach' failed to execute: {}", e);
}
callback(ui);
});
Ok(())
}
pub fn cmd_detach(&self, callback: UICallback) -> Result<(), String> {
trace!("CommandProcessor::cmd_detach");
let mut capi = self.capi();
let ui = self.ui();
async_std::task::spawn_local(async move {
if let Err(e) = capi.server_detach().await {
error!("Server command 'detach' failed to execute: {}", e);
}
callback(ui);
});
Ok(())
}
pub fn cmd_disconnect(&self, callback: UICallback) -> Result<(), String> {
trace!("CommandProcessor::cmd_disconnect");
let mut capi = self.capi();
let ui = self.ui();
async_std::task::spawn_local(async move {
capi.disconnect().await;
callback(ui);
});
Ok(())
}
pub fn run_command(&self, command_line: &str, callback: UICallback) -> Result<(), String> {
//
let (cmd, rest) = Self::word_split(command_line);
match cmd.as_str() {
"help" => self.cmd_help(rest, callback),
"exit" => self.cmd_exit(callback),
"quit" => self.cmd_exit(callback),
"disconnect" => self.cmd_disconnect(callback),
"shutdown" => self.cmd_shutdown(callback),
"attach" => self.cmd_attach(callback),
"detach" => self.cmd_detach(callback),
_ => {
callback(self.ui());
Err(format!("Invalid command: {}", cmd))
}
}
}
pub async fn connection_manager(&mut self) {
// Connect until we're done
while !self.inner_mut().finished {
// Wait for connection request
if !self.inner().autoconnect {
let waker = self.inner_mut().connection_waker.instance_clone(());
waker.await;
} else {
self.inner_mut().autoconnect = false;
}
self.inner_mut().connection_waker.reset();
// Loop while we want to keep the connection
let mut first = true;
while self.inner().reconnect {
let server_addr_opt = self.inner_mut().server_addr.clone();
let server_addr = match server_addr_opt {
None => break,
Some(addr) => addr,
};
if first {
info!("Connecting to server at {}", server_addr);
self.set_connection_state(ConnectionState::Retrying(
server_addr.clone(),
SystemTime::now(),
));
} else {
debug!("Retrying connection to {}", server_addr);
}
let mut capi = self.capi();
let res = capi.connect(server_addr.clone()).await;
if let Ok(_) = res {
info!(
"Connection to server at {} terminated normally",
server_addr
);
break;
}
if !self.inner().autoreconnect {
info!("Connection to server lost.");
break;
}
self.set_connection_state(ConnectionState::Retrying(
server_addr.clone(),
SystemTime::now(),
));
debug!("Connection lost, retrying in 2 seconds");
{
let waker = self.inner_mut().connection_waker.instance_clone(());
waker
.race(async_std::task::sleep(Duration::from_millis(2000)))
.await;
}
self.inner_mut().connection_waker.reset();
first = false;
}
info!("Disconnected.");
self.set_connection_state(ConnectionState::Disconnected);
self.inner_mut().reconnect = true;
}
}
// called by ui
////////////////////////////////////////////
pub fn set_server_address(&mut self, server_addr: Option<SocketAddr>) {
self.inner_mut().server_addr = server_addr;
}
pub fn get_server_address(&self) -> Option<SocketAddr> {
self.inner().server_addr.clone()
}
// called by client_api_connection
// calls into ui
////////////////////////////////////////////
pub fn set_attachment_state(&mut self, state: AttachmentState) {
self.inner_mut().ui.set_attachment_state(state);
}
// called by client_api_connection
// calls into ui
////////////////////////////////////////////
pub fn set_connection_state(&mut self, state: ConnectionState) {
self.inner_mut().ui.set_connection_state(state);
}
// called by ui
////////////////////////////////////////////
pub fn start_connection(&mut self) {
self.inner_mut().reconnect = true;
self.inner_mut().connection_waker.resolve();
}
// pub fn stop_connection(&mut self) {
// self.inner_mut().reconnect = false;
// let mut capi = self.capi().clone();
// async_std::task::spawn_local(async move {
// capi.disconnect().await;
// });
// }
pub fn cancel_reconnect(&mut self) {
self.inner_mut().reconnect = false;
self.inner_mut().connection_waker.resolve();
}
pub fn quit(&mut self) {
self.inner_mut().finished = true;
self.inner_mut().reconnect = false;
self.inner_mut().connection_waker.resolve();
}
// called by ui
// calls into client_api_connection
////////////////////////////////////////////
pub fn attach(&mut self) {
trace!("CommandProcessor::attach");
let mut capi = self.capi();
async_std::task::spawn_local(async move {
if let Err(e) = capi.server_attach().await {
error!("Server command 'attach' failed to execute: {}", e);
}
});
}
pub fn detach(&mut self) {
trace!("CommandProcessor::detach");
let mut capi = self.capi();
async_std::task::spawn_local(async move {
if let Err(e) = capi.server_detach().await {
error!("Server command 'detach' failed to execute: {}", e);
}
});
}
}

171
veilid-cli/src/main.rs Normal file
View File

@@ -0,0 +1,171 @@
use anyhow::*;
use async_std::prelude::*;
use clap::{App, Arg};
use flexi_logger::*;
use log::*;
use std::ffi::OsStr;
use std::net::ToSocketAddrs;
mod client_api_connection;
mod command_processor;
mod settings;
mod ui;
pub mod veilid_client_capnp {
include!(concat!(env!("OUT_DIR"), "/proto/veilid_client_capnp.rs"));
}
fn parse_command_line<'a>(
default_config_path: &'a OsStr,
) -> Result<clap::ArgMatches<'a>, clap::Error> {
let matches = App::new("veilid-cli")
.version("0.1")
.about("Veilid Console Client")
.arg(
Arg::with_name("address")
.required(false)
.help("Address to connect to"),
)
.arg(
Arg::with_name("debug")
.long("debug")
.help("Turn on debug logging"),
)
.arg(
Arg::with_name("wait-for-debug")
.long("wait-for-debug")
.help("Wait for debugger to attach"),
)
.arg(
Arg::with_name("trace")
.long("trace")
.conflicts_with("debug")
.help("Turn on trace logging"),
)
.arg(
Arg::with_name("config-file")
.short("c")
.takes_value(true)
.value_name("FILE")
.default_value_os(default_config_path)
.help("Specify a configuration file to use"),
)
.get_matches();
Ok(matches)
}
#[async_std::main]
async fn main() -> Result<()> {
// Get command line options
let default_config_path = settings::Settings::get_default_config_path();
let matches = parse_command_line(default_config_path.as_os_str())?;
if matches.occurrences_of("wait-for-debug") != 0 {
use bugsalot::debugger;
debugger::wait_until_attached(None).expect("state() not implemented on this platform");
}
// Attempt to load configuration
let mut settings = settings::Settings::new(
matches.occurrences_of("config-file") == 0,
matches.value_of_os("config-file").unwrap(),
)
.map_err(|x| Box::new(x))?;
// Set config from command line
if matches.occurrences_of("debug") != 0 {
settings.logging.level = settings::LogLevel::Debug;
settings.logging.terminal.enabled = true;
}
if matches.occurrences_of("trace") != 0 {
settings.logging.level = settings::LogLevel::Trace;
settings.logging.terminal.enabled = true;
}
// Create UI object
let mut sivui = ui::UI::new(settings.interface.node_log.scrollback, &settings);
// Set up loggers
{
let mut specbuilder = LogSpecBuilder::new();
specbuilder.default(settings::convert_loglevel(settings.logging.level));
specbuilder.module("cursive_core", LevelFilter::Off);
specbuilder.module("cursive_buffered_backend", LevelFilter::Off);
specbuilder.module("mio", LevelFilter::Off);
specbuilder.module("async_std", LevelFilter::Off);
specbuilder.module("async_io", LevelFilter::Off);
specbuilder.module("polling", LevelFilter::Off);
let logger = Logger::with(specbuilder.build());
if settings.logging.terminal.enabled {
let flv = sivui.cursive_flexi_logger();
if settings.logging.file.enabled {
std::fs::create_dir_all(settings.logging.file.directory.clone())?;
logger
.log_target(LogTarget::FileAndWriter(flv))
.suppress_timestamp()
// .format(flexi_logger::colored_default_format)
.directory(settings.logging.file.directory.clone())
.start()
.expect("failed to initialize logger!");
} else {
logger
.log_target(LogTarget::Writer(flv))
.suppress_timestamp()
.format(flexi_logger::colored_default_format)
.start()
.expect("failed to initialize logger!");
}
} else if settings.logging.file.enabled {
std::fs::create_dir_all(settings.logging.file.directory.clone())?;
logger
.log_target(LogTarget::File)
.suppress_timestamp()
.directory(settings.logging.file.directory.clone())
.start()
.expect("failed to initialize logger!");
}
}
// Get client address
let server_addrs;
if let Some(address_arg) = matches.value_of("address") {
server_addrs = address_arg
.to_socket_addrs()
.context(format!("Invalid server address '{}'", address_arg))?
.collect()
} else {
server_addrs = settings.address.addrs.clone();
}
let server_addr = server_addrs.first().cloned();
// Create command processor
debug!("Creating Command Processor ");
let mut comproc = command_processor::CommandProcessor::new(sivui.clone(), &settings);
sivui.set_command_processor(comproc.clone());
// Create client api client side
info!("Starting API connection");
let mut capi = client_api_connection::ClientApiConnection::new(comproc.clone());
// Save client api in command processor
comproc.set_client_api_connection(capi.clone());
// Keep a connection to the server
comproc.set_server_address(server_addr);
let mut comproc2 = comproc.clone();
let connection_future = comproc.connection_manager();
// Start UI
let ui_future = async_std::task::spawn_local(async move {
sivui.run_async().await;
// When UI quits, close connection and command processor cleanly
comproc2.quit();
capi.disconnect().await;
});
// Wait for ui and connection to complete
ui_future.join(connection_future).await;
Ok(())
}

269
veilid-cli/src/settings.rs Normal file
View File

@@ -0,0 +1,269 @@
use config;
use directories::*;
use log;
use serde;
use serde_derive::*;
use std::ffi::OsStr;
use std::net::{SocketAddr, ToSocketAddrs};
use std::path::{Path, PathBuf};
pub fn load_default_config(cfg: &mut config::Config) -> Result<(), config::ConfigError> {
let default_config = r###"---
address: "localhost:5959"
autoconnect: true
autoreconnect: true
logging:
level: "info"
terminal:
enabled: false
file:
enabled: true
directory: ""
append: true
interface:
node_log:
scrollback: 2048
command_line:
history_size: 2048
theme:
shadow: false
borders: "simple"
colors:
background : "#333D3D"
shadow : "#000000"
view : "#1c2323"
primary : "#a6d8d3"
secondary : "#8cb4b7"
tertiary : "#eeeeee"
title_primary : "#f93fbd"
title_secondary : "#ff0000"
highlight : "#f93fbd"
highlight_inactive : "#a6d8d3"
highlight_text : "#333333"
log_colors:
trace : "#707070"
debug : "#a0a0a0"
info : "#5cd3c6"
warn : "#fedc50"
error : "#ff4a15"
"###;
cfg.merge(config::File::from_str(
default_config,
config::FileFormat::Yaml,
))
.map(drop)
}
pub fn load_config(
cfg: &mut config::Config,
config_file: &Path,
) -> Result<(), config::ConfigError> {
if let Some(config_file_str) = config_file.to_str() {
cfg.merge(config::File::new(config_file_str, config::FileFormat::Yaml))
.map(drop)
} else {
Err(config::ConfigError::Message(
"config file path is not valid UTF-8".to_owned(),
))
}
}
#[derive(Copy, Clone, Debug)]
pub enum LogLevel {
Error,
Warn,
Info,
Debug,
Trace,
}
impl<'de> serde::Deserialize<'de> for LogLevel {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.to_ascii_lowercase().as_str() {
"error" => Ok(LogLevel::Error),
"warn" => Ok(LogLevel::Warn),
"info" => Ok(LogLevel::Info),
"debug" => Ok(LogLevel::Debug),
"trace" => Ok(LogLevel::Trace),
_ => Err(serde::de::Error::custom(format!(
"Invalid log level: {}",
s
))),
}
}
}
pub fn convert_loglevel(log_level: LogLevel) -> log::LevelFilter {
match log_level {
LogLevel::Error => log::LevelFilter::Error,
LogLevel::Warn => log::LevelFilter::Warn,
LogLevel::Info => log::LevelFilter::Info,
LogLevel::Debug => log::LevelFilter::Debug,
LogLevel::Trace => log::LevelFilter::Trace,
}
}
#[derive(Debug)]
pub struct NamedSocketAddrs {
pub name: String,
pub addrs: Vec<SocketAddr>,
}
impl<'de> serde::Deserialize<'de> for NamedSocketAddrs {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let addr_iter = s
.to_socket_addrs()
.map_err(|x| serde::de::Error::custom(x))?;
Ok(NamedSocketAddrs {
name: s,
addrs: addr_iter.collect(),
})
}
}
#[derive(Debug, Deserialize)]
pub struct Terminal {
pub enabled: bool,
}
#[derive(Debug, Deserialize)]
pub struct File {
pub enabled: bool,
pub directory: String,
pub append: bool,
}
#[derive(Debug, Deserialize)]
pub struct Logging {
pub terminal: Terminal,
pub file: File,
pub level: LogLevel,
}
#[derive(Debug, Deserialize)]
pub struct Colors {
pub background: String,
pub shadow: String,
pub view: String,
pub primary: String,
pub secondary: String,
pub tertiary: String,
pub title_primary: String,
pub title_secondary: String,
pub highlight: String,
pub highlight_inactive: String,
pub highlight_text: String,
}
#[derive(Debug, Deserialize)]
pub struct LogColors {
pub trace: String,
pub debug: String,
pub info: String,
pub warn: String,
pub error: String,
}
#[derive(Debug, Deserialize)]
pub struct Theme {
pub shadow: bool,
pub borders: String,
pub colors: Colors,
pub log_colors: LogColors,
}
#[derive(Debug, Deserialize)]
pub struct NodeLog {
pub scrollback: usize,
}
#[derive(Debug, Deserialize)]
pub struct CommandLine {
pub history_size: usize,
}
#[derive(Debug, Deserialize)]
pub struct Interface {
pub theme: Theme,
pub node_log: NodeLog,
pub command_line: CommandLine,
}
#[derive(Debug, Deserialize)]
pub struct Settings {
pub address: NamedSocketAddrs,
pub autoconnect: bool,
pub autoreconnect: bool,
pub logging: Logging,
pub interface: Interface,
}
impl Settings {
pub fn get_default_config_path() -> PathBuf {
// Get default configuration file location
let mut default_config_path;
if let Some(my_proj_dirs) = ProjectDirs::from("org", "Veilid", "Veilid") {
default_config_path = PathBuf::from(my_proj_dirs.config_dir());
} else {
default_config_path = PathBuf::from("./");
}
default_config_path.push("veilid-client.conf");
default_config_path
}
pub fn get_default_log_directory() -> PathBuf {
// Get default configuration file location
let mut default_log_directory;
if let Some(my_proj_dirs) = ProjectDirs::from("org", "Veilid", "Veilid") {
default_log_directory = PathBuf::from(my_proj_dirs.config_dir());
} else {
default_log_directory = PathBuf::from("./");
}
default_log_directory.push("logs/");
default_log_directory
}
pub fn new(
config_file_is_default: bool,
config_file: &OsStr,
) -> Result<Self, config::ConfigError> {
// Create a config
let mut cfg = config::Config::default();
// Load the default config
load_default_config(&mut cfg)?;
// Use default log directory for logs
cfg.set(
"logging.file.directory",
Settings::get_default_log_directory().to_str(),
)?;
// Merge in the config file if we have one
let config_file_path = Path::new(config_file);
if !config_file_is_default || config_file_path.exists() {
// If the user specifies a config file on the command line then it must exist
load_config(&mut cfg, config_file_path)?;
}
cfg.try_into()
}
}
#[test]
fn test_default_config() {
let mut cfg = config::Config::default();
load_default_config(&mut cfg).unwrap();
let settings = cfg.try_into::<Settings>().unwrap();
println!("default settings: {:?}", settings);
}

785
veilid-cli/src/ui.rs Normal file
View File

@@ -0,0 +1,785 @@
use crate::command_processor::*;
use crate::settings::Settings;
use crate::veilid_client_capnp::*;
use crossbeam_channel::Sender;
use cursive::align::*;
use cursive::event::*;
use cursive::theme::*;
use cursive::traits::*;
use cursive::utils::markup::StyledString;
use cursive::views::*;
use cursive::Cursive;
use cursive::CursiveRunnable;
use cursive_flexi_logger_view::{CursiveLogWriter, FlexiLoggerView};
use log::*;
use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use std::rc::Rc;
use thiserror::Error;
//////////////////////////////////////////////////////////////
///
struct Dirty<T> {
pub value: T,
dirty: bool,
}
impl<T> Dirty<T> {
pub fn new(value: T) -> Self {
Self {
value: value,
dirty: true,
}
}
pub fn set(&mut self, value: T) {
self.value = value;
self.dirty = true;
}
pub fn get(&self) -> &T {
&self.value
}
// pub fn get_mut(&mut self) -> &mut T {
// &mut self.value
// }
pub fn take_dirty(&mut self) -> bool {
let is_dirty = self.dirty;
self.dirty = false;
is_dirty
}
}
pub type UICallback = Box<dyn Fn(UI) + 'static>;
struct UIState {
attachment_state: Dirty<AttachmentState>,
connection_state: Dirty<ConnectionState>,
}
impl UIState {
pub fn new() -> Self {
Self {
attachment_state: Dirty::new(AttachmentState::Detached),
connection_state: Dirty::new(ConnectionState::Disconnected),
}
}
}
#[derive(Error, Debug)]
#[error("???")]
struct UIError;
pub struct UIInner {
ui_state: UIState,
log_colors: HashMap<Level, cursive::theme::Color>,
cmdproc: Option<CommandProcessor>,
cb_sink: Sender<Box<dyn FnOnce(&mut Cursive) + 'static + Send>>,
cmd_history: VecDeque<String>,
cmd_history_position: usize,
cmd_history_max_size: usize,
connection_dialog_state: Option<ConnectionState>,
}
type Handle<T> = Rc<RefCell<T>>;
#[derive(Clone)]
pub struct UI {
siv: Handle<CursiveRunnable>,
inner: Handle<UIInner>,
}
impl UI {
pub fn new(node_log_scrollback: usize, settings: &Settings) -> Self {
cursive_flexi_logger_view::resize(node_log_scrollback);
// Instantiate the cursive runnable
/*
// reduces flicker, but it costs cpu
let mut runnable = CursiveRunnable::new(
|| -> Result<Box<dyn cursive_buffered_backend::Backend>, UIError> {
let crossterm_backend = cursive::backends::crossterm::Backend::init().unwrap();
let buffered_backend =
cursive_buffered_backend::BufferedBackend::new(crossterm_backend);
Ok(Box::new(buffered_backend))
},
);
*/
let runnable = cursive::default();
// Make the callback mechanism easily reachable
let cb_sink = runnable.cb_sink().clone();
// Create the UI object
let this = Self {
siv: Rc::new(RefCell::new(runnable)),
inner: Rc::new(RefCell::new(UIInner {
ui_state: UIState::new(),
log_colors: Default::default(),
cmdproc: None,
cmd_history: {
let mut vd = VecDeque::new();
vd.push_back("".to_string());
vd
},
cmd_history_position: 0,
cmd_history_max_size: settings.interface.command_line.history_size,
connection_dialog_state: None,
cb_sink: cb_sink,
})),
};
let mut siv = this.siv.borrow_mut();
let mut inner = this.inner.borrow_mut();
// Make the inner object accessible in callbacks easily
siv.set_user_data(this.inner.clone());
// Create layouts
let mut mainlayout = LinearLayout::vertical().with_name("main-layout");
mainlayout.get_mut().add_child(
Panel::new(
FlexiLoggerView::new_scrollable()
.with_name("node-events")
.full_screen(),
)
.title_position(HAlign::Left)
.title("Node Events"),
);
mainlayout.get_mut().add_child(
Panel::new(ScrollView::new(
TextView::new("Peer Table")
.with_name("peers")
.fixed_height(8)
.scrollable(),
))
.title_position(HAlign::Left)
.title("Peers"),
);
let mut command = StyledString::new();
command.append_styled("Command> ", ColorStyle::title_primary());
//
mainlayout.get_mut().add_child(
LinearLayout::horizontal()
.child(TextView::new(command))
.child(
EditView::new()
.on_submit(|s, text| {
UI::on_command_line_entered(s, text);
})
.on_edit(|s, text, cursor| UI::on_command_line_edit(s, text, cursor))
.on_up_down(|s, dir| {
UI::on_command_line_history(s, dir);
})
.style(ColorStyle::new(
PaletteColor::Background,
PaletteColor::Secondary,
))
.with_name("command-line")
.full_screen()
.fixed_height(1),
)
.child(
Button::new("Attach", |s| {
UI::on_button_attach_pressed(s);
})
.with_name("button-attach"),
),
);
let mut version = StyledString::new();
version.append_styled(
concat!(" | veilid-cli v", env!("CARGO_PKG_VERSION")),
ColorStyle::highlight_inactive(),
);
mainlayout.get_mut().add_child(
LinearLayout::horizontal()
.color(Some(ColorStyle::highlight_inactive()))
.child(
TextView::new("")
.with_name("status-bar")
.full_screen()
.fixed_height(1),
)
.child(TextView::new(version)),
);
siv.add_fullscreen_layer(mainlayout);
UI::setup_colors(&mut siv, &mut inner, &settings);
UI::setup_quit_handler(&mut siv);
drop(inner);
drop(siv);
this
}
fn command_processor(s: &mut Cursive) -> CommandProcessor {
let inner = Self::inner(s);
inner.cmdproc.as_ref().unwrap().clone()
}
fn inner(s: &mut Cursive) -> std::cell::Ref<'_, UIInner> {
s.user_data::<Handle<UIInner>>().unwrap().borrow()
}
fn inner_mut(s: &mut Cursive) -> std::cell::RefMut<'_, UIInner> {
s.user_data::<Handle<UIInner>>().unwrap().borrow_mut()
}
fn setup_colors(siv: &mut CursiveRunnable, inner: &mut UIInner, settings: &Settings) {
// Make colors
let mut theme = cursive::theme::load_default();
theme.shadow = settings.interface.theme.shadow;
theme.borders = BorderStyle::from(&settings.interface.theme.borders);
theme.palette.set_color(
"background",
Color::parse(settings.interface.theme.colors.background.as_str()).unwrap(),
);
theme.palette.set_color(
"shadow",
Color::parse(settings.interface.theme.colors.shadow.as_str()).unwrap(),
);
theme.palette.set_color(
"view",
Color::parse(settings.interface.theme.colors.view.as_str()).unwrap(),
);
theme.palette.set_color(
"primary",
Color::parse(settings.interface.theme.colors.primary.as_str()).unwrap(),
);
theme.palette.set_color(
"secondary",
Color::parse(settings.interface.theme.colors.secondary.as_str()).unwrap(),
);
theme.palette.set_color(
"tertiary",
Color::parse(settings.interface.theme.colors.tertiary.as_str()).unwrap(),
);
theme.palette.set_color(
"title_primary",
Color::parse(settings.interface.theme.colors.title_primary.as_str()).unwrap(),
);
theme.palette.set_color(
"title_secondary",
Color::parse(settings.interface.theme.colors.title_secondary.as_str()).unwrap(),
);
theme.palette.set_color(
"highlight",
Color::parse(settings.interface.theme.colors.highlight.as_str()).unwrap(),
);
theme.palette.set_color(
"highlight_inactive",
Color::parse(settings.interface.theme.colors.highlight_inactive.as_str()).unwrap(),
);
theme.palette.set_color(
"highlight_text",
Color::parse(settings.interface.theme.colors.highlight_text.as_str()).unwrap(),
);
siv.set_theme(theme);
// Make log colors
let mut colors = HashMap::<Level, cursive::theme::Color>::new();
colors.insert(
Level::Trace,
Color::parse(settings.interface.theme.log_colors.trace.as_str()).unwrap(),
);
colors.insert(
Level::Debug,
Color::parse(settings.interface.theme.log_colors.debug.as_str()).unwrap(),
);
colors.insert(
Level::Info,
Color::parse(settings.interface.theme.log_colors.info.as_str()).unwrap(),
);
colors.insert(
Level::Warn,
Color::parse(settings.interface.theme.log_colors.warn.as_str()).unwrap(),
);
colors.insert(
Level::Error,
Color::parse(settings.interface.theme.log_colors.error.as_str()).unwrap(),
);
inner.log_colors = colors;
}
fn setup_quit_handler(siv: &mut Cursive) {
siv.clear_global_callbacks(cursive::event::Event::CtrlChar('c'));
siv.set_on_pre_event(cursive::event::Event::CtrlChar('c'), UI::quit_handler);
siv.set_global_callback(cursive::event::Event::Key(Key::Esc), UI::quit_handler);
}
fn quit_handler(siv: &mut Cursive) {
siv.add_layer(
Dialog::text("Do you want to exit?")
.button("Yes", |s| s.quit())
.button("No", |mut s| {
s.pop_layer();
UI::setup_quit_handler(&mut s);
}),
);
siv.set_on_pre_event(cursive::event::Event::CtrlChar('c'), |s| {
s.quit();
});
siv.set_global_callback(cursive::event::Event::Key(Key::Esc), |mut s| {
s.pop_layer();
UI::setup_quit_handler(&mut s);
});
}
pub fn cursive_flexi_logger(&mut self) -> Box<CursiveLogWriter> {
let mut flv =
cursive_flexi_logger_view::cursive_flexi_logger(self.siv.borrow().cb_sink().clone());
flv.set_colors(self.inner.borrow().log_colors.clone());
flv
}
pub fn set_command_processor(&mut self, cmdproc: CommandProcessor) {
let mut inner = self.inner.borrow_mut();
inner.cmdproc = Some(cmdproc);
let _ = inner.cb_sink.send(Box::new(UI::update_cb));
}
pub fn set_attachment_state(&mut self, state: AttachmentState) {
let mut inner = self.inner.borrow_mut();
inner.ui_state.attachment_state.set(state);
let _ = inner.cb_sink.send(Box::new(UI::update_cb));
}
pub fn set_connection_state(&mut self, state: ConnectionState) {
let mut inner = self.inner.borrow_mut();
inner.ui_state.connection_state.set(state);
let _ = inner.cb_sink.send(Box::new(UI::update_cb));
}
pub fn add_node_event(&mut self, event: &str) {
let inner = self.inner.borrow_mut();
let color = inner.log_colors.get(&Level::Info).unwrap().clone();
for line in event.lines() {
cursive_flexi_logger_view::push_to_log(StyledString::styled(line, color));
}
let _ = inner.cb_sink.send(Box::new(UI::update_cb));
}
pub fn quit(&mut self) {
let inner = self.inner.borrow_mut();
let _ = inner.cb_sink.send(Box::new(|s| {
s.quit();
}));
}
// Note: Cursive is not re-entrant, can't borrow_mut self.siv again after this
pub async fn run_async(&mut self) {
let mut siv = self.siv.borrow_mut();
siv.run_async().await;
}
// pub fn run(&mut self) {
// let mut siv = self.siv.borrow_mut();
// siv.run();
// }
/////////////////////////////////////////////////////////////////////////////////////
// Private functions
// fn main_layout(s: &mut Cursive) -> ViewRef<LinearLayout> {
// s.find_name("main-layout").unwrap()
// }
// fn column_layout(s: &mut Cursive) -> ViewRef<LinearLayout> {
// s.find_name("column-layout").unwrap()
// }
// fn button_layout(s: &mut Cursive) -> ViewRef<LinearLayout> {
// s.find_name("button-layout").unwrap()
// }
// fn peers(s: &mut Cursive) -> ViewRef<TextView> {
// s.find_name("peers").unwrap()
// }
// fn node_events(s: &mut Cursive) -> ViewRef<FlexiLoggerView> {
// s.find_name("node-events").unwrap()
// }
fn command_line(s: &mut Cursive) -> ViewRef<EditView> {
s.find_name("command-line").unwrap()
}
fn button_attach(s: &mut Cursive) -> ViewRef<Button> {
s.find_name("button-attach").unwrap()
}
fn status_bar(s: &mut Cursive) -> ViewRef<TextView> {
s.find_name("status-bar").unwrap()
}
fn render_attachment_state<'a>(inner: &mut UIInner) -> &'a str {
match inner.ui_state.attachment_state.get() {
AttachmentState::Detached => " Detached [----]",
AttachmentState::Attaching => "Attaching [/ ]",
AttachmentState::AttachedWeak => " Attached [| ]",
AttachmentState::AttachedGood => " Attached [|| ]",
AttachmentState::AttachedStrong => " Attached [||| ]",
AttachmentState::FullyAttached => " Attached [||||]",
AttachmentState::OverAttached => " Attached [++++]",
AttachmentState::Detaching => "Detaching [////]",
}
}
fn render_button_attach<'a>(inner: &mut UIInner) -> (&'a str, bool) {
if let ConnectionState::Connected(_, _) = inner.ui_state.connection_state.get() {
match inner.ui_state.attachment_state.get() {
AttachmentState::Detached => ("Attach", true),
AttachmentState::Attaching => ("Detach", true),
AttachmentState::AttachedWeak => ("Detach", true),
AttachmentState::AttachedGood => ("Detach", true),
AttachmentState::AttachedStrong => ("Detach", true),
AttachmentState::FullyAttached => ("Detach", true),
AttachmentState::OverAttached => ("Detach", true),
AttachmentState::Detaching => ("Detach", false),
}
} else {
(" ---- ", false)
}
}
fn on_command_line_edit(s: &mut Cursive, text: &str, _pos: usize) {
let mut inner = Self::inner_mut(s);
// save edited command to newest history slot
let hlen = inner.cmd_history.len();
inner.cmd_history_position = hlen - 1;
inner.cmd_history[hlen - 1] = text.to_owned();
}
pub fn enable_command_ui(s: &mut Cursive, enabled: bool) {
Self::command_line(s).set_enabled(enabled);
Self::button_attach(s).set_enabled(enabled);
}
fn run_command(s: &mut Cursive, text: &str) -> Result<(), String> {
// disable ui
Self::enable_command_ui(s, false);
// run command
let cmdproc = Self::command_processor(s);
cmdproc.run_command(
text,
Box::new(|ui: UI| {
let _ = ui
.inner
.borrow()
.cb_sink
.send(Box::new(|s| {
Self::enable_command_ui(s, true);
}))
.unwrap();
}),
)
}
fn on_command_line_entered(s: &mut Cursive, text: &str) {
if text.trim().is_empty() {
return;
}
// run command
cursive_flexi_logger_view::push_to_log(StyledString::styled(
format!("> {}", text),
ColorStyle::primary(),
));
match Self::run_command(s, text) {
Ok(_) => {}
Err(e) => {
let color = Self::inner_mut(s)
.log_colors
.get(&Level::Error)
.unwrap()
.clone();
cursive_flexi_logger_view::push_to_log(StyledString::styled(
format!("> {}", text),
color,
));
cursive_flexi_logger_view::push_to_log(StyledString::styled(
format!(" Error: {}", e),
color,
));
return;
}
}
// save to history unless it's a duplicate
{
let mut inner = Self::inner_mut(s);
let hlen = inner.cmd_history.len();
inner.cmd_history[hlen - 1] = text.to_owned();
if hlen >= 2 && inner.cmd_history[hlen - 1] == inner.cmd_history[hlen - 2] {
inner.cmd_history[hlen - 1] = "".to_string();
} else {
if hlen == inner.cmd_history_max_size {
inner.cmd_history.pop_front();
}
inner.cmd_history.push_back("".to_string());
}
let hlen = inner.cmd_history.len();
inner.cmd_history_position = hlen - 1;
}
// Clear the edit field
let mut cmdline = Self::command_line(s);
cmdline.set_content("");
}
fn on_command_line_history(s: &mut Cursive, dir: bool) {
let mut cmdline = Self::command_line(s);
let mut inner = Self::inner_mut(s);
// if at top of buffer or end of buffer, ignore
if (!dir && inner.cmd_history_position == 0)
|| (dir && inner.cmd_history_position == (inner.cmd_history.len() - 1))
{
return;
}
// move the history position
if dir {
inner.cmd_history_position += 1;
} else {
inner.cmd_history_position -= 1;
}
// replace text with current line
let hlen = inner.cmd_history_position;
cmdline.set_content(inner.cmd_history[hlen].as_str());
}
fn on_button_attach_pressed(s: &mut Cursive) {
let action: Option<bool> = match Self::inner_mut(s).ui_state.attachment_state.get() {
AttachmentState::Detached => Some(true),
AttachmentState::Attaching => Some(false),
AttachmentState::AttachedWeak => Some(false),
AttachmentState::AttachedGood => Some(false),
AttachmentState::AttachedStrong => Some(false),
AttachmentState::FullyAttached => Some(false),
AttachmentState::OverAttached => Some(false),
AttachmentState::Detaching => None,
};
let mut cmdproc = Self::command_processor(s);
if let Some(a) = action {
if a {
cmdproc.attach();
} else {
cmdproc.detach();
}
}
}
fn refresh_button_attach(s: &mut Cursive) {
let mut button_attach = UI::button_attach(s);
let mut inner = Self::inner_mut(s);
let (button_text, button_enable) = UI::render_button_attach(&mut inner);
button_attach.set_label(button_text);
button_attach.set_enabled(button_enable);
}
fn submit_connection_address(s: &mut Cursive) {
let edit = s.find_name::<EditView>("connection-address").unwrap();
let addr = (*edit.get_content()).clone();
let sa = match addr.parse::<std::net::SocketAddr>() {
Ok(sa) => Some(sa),
Err(_) => {
s.add_layer(Dialog::text("Invalid address").button("Close", |s| {
s.pop_layer();
}));
return;
}
};
Self::command_processor(s).set_server_address(sa);
Self::command_processor(s).start_connection();
}
fn show_connection_dialog(s: &mut Cursive, state: ConnectionState) -> bool {
let mut inner = Self::inner_mut(s);
let mut show: bool = false;
let mut hide: bool = false;
let mut reset: bool = false;
match state {
ConnectionState::Disconnected => {
if inner.connection_dialog_state == None
|| inner
.connection_dialog_state
.as_ref()
.unwrap()
.is_connected()
{
show = true;
} else if inner
.connection_dialog_state
.as_ref()
.unwrap()
.is_retrying()
{
reset = true;
}
}
ConnectionState::Connected(_, _) => {
if inner.connection_dialog_state != None
&& !inner
.connection_dialog_state
.as_ref()
.unwrap()
.is_connected()
{
hide = true;
}
}
ConnectionState::Retrying(_, _) => {
if inner.connection_dialog_state == None
|| inner
.connection_dialog_state
.as_ref()
.unwrap()
.is_connected()
{
show = true;
} else if inner
.connection_dialog_state
.as_ref()
.unwrap()
.is_disconnected()
{
reset = true;
}
}
}
inner.connection_dialog_state = Some(state);
drop(inner);
if hide {
s.pop_layer();
return true;
}
if show {
s.add_layer(
Dialog::around(
LinearLayout::vertical().child(
LinearLayout::horizontal()
.child(TextView::new("Address:"))
.child(
EditView::new()
.on_submit(|s, _| Self::submit_connection_address(s))
.with_name("connection-address")
.fixed_height(1)
.min_width(40),
),
),
)
.title("Connect to server")
.with_name("connection-dialog"),
);
return true;
}
if reset {
let mut dlg = s.find_name::<Dialog>("connection-dialog").unwrap();
dlg.clear_buttons();
return true;
}
return false;
}
fn refresh_connection_dialog(s: &mut Cursive) {
let new_state = Self::inner(s).ui_state.connection_state.get().clone();
if !Self::show_connection_dialog(s, new_state.clone()) {
return;
}
match new_state {
ConnectionState::Disconnected => {
let addr = match Self::command_processor(s).get_server_address() {
None => "".to_owned(),
Some(addr) => addr.to_string(),
};
debug!("address is {}", addr);
let mut edit = s.find_name::<EditView>("connection-address").unwrap();
edit.set_content(addr.to_string());
edit.set_enabled(true);
let mut dlg = s.find_name::<Dialog>("connection-dialog").unwrap();
dlg.add_button("Connect", Self::submit_connection_address);
}
ConnectionState::Connected(_, _) => {
return;
}
ConnectionState::Retrying(addr, _) => {
//
let mut edit = s.find_name::<EditView>("connection-address").unwrap();
debug!("address is {}", addr);
edit.set_content(addr.to_string());
edit.set_enabled(false);
let mut dlg = s.find_name::<Dialog>("connection-dialog").unwrap();
dlg.add_button("Cancel", |s| {
Self::command_processor(s).cancel_reconnect();
});
}
}
}
fn refresh_statusbar(s: &mut Cursive) {
let mut statusbar = UI::status_bar(s);
let mut inner = Self::inner_mut(s);
let mut status = StyledString::new();
match inner.ui_state.connection_state.get() {
ConnectionState::Disconnected => {
status.append_styled(format!("Disconnected "), ColorStyle::highlight_inactive());
status.append_styled("|", ColorStyle::highlight_inactive());
}
ConnectionState::Retrying(addr, _) => {
status.append_styled(
format!("Reconnecting to {} ", addr.to_string()),
ColorStyle::highlight_inactive(),
);
status.append_styled("|", ColorStyle::highlight_inactive());
}
ConnectionState::Connected(addr, _) => {
status.append_styled(
format!("Connected to {} ", addr.to_string()),
ColorStyle::highlight_inactive(),
);
status.append_styled("|", ColorStyle::highlight_inactive());
// Add attachment state
status.append_styled(
format!(" {} ", UI::render_attachment_state(&mut inner)),
ColorStyle::highlight_inactive(),
);
status.append_styled("|", ColorStyle::highlight_inactive());
// Add bandwidth status
status.append_styled(
" Down: 0.0KB/s Up: 0.0KB/s ",
ColorStyle::highlight_inactive(),
);
status.append_styled("|", ColorStyle::highlight_inactive());
// Add tunnel status
status.append_styled(" No Tunnels ", ColorStyle::highlight_inactive());
status.append_styled("|", ColorStyle::highlight_inactive());
}
};
statusbar.set_content(status);
}
fn update_cb(s: &mut Cursive) {
let mut inner = Self::inner_mut(s);
let mut refresh_statusbar = false;
let mut refresh_button_attach = false;
let mut refresh_connection_dialog = false;
if inner.ui_state.attachment_state.take_dirty() {
refresh_statusbar = true;
refresh_button_attach = true;
}
if inner.ui_state.connection_state.take_dirty() {
refresh_statusbar = true;
refresh_button_attach = true;
refresh_connection_dialog = true;
}
drop(inner);
if refresh_statusbar {
Self::refresh_statusbar(s);
}
if refresh_button_attach {
Self::refresh_button_attach(s);
}
if refresh_connection_dialog {
Self::refresh_connection_dialog(s);
}
}
}