initial import of main veilid core
This commit is contained in:
214
veilid-cli/src/client_api_connection.rs
Normal file
214
veilid-cli/src/client_api_connection.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
325
veilid-cli/src/command_processor.rs
Normal file
325
veilid-cli/src/command_processor.rs
Normal 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
171
veilid-cli/src/main.rs
Normal 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
269
veilid-cli/src/settings.rs
Normal 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
785
veilid-cli/src/ui.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user