From ca6c616d66b90ef1ec655760f02e2b7e00df8176 Mon Sep 17 00:00:00 2001 From: John Smith Date: Tue, 6 Sep 2022 16:49:43 -0400 Subject: [PATCH] veilid-cli cleanup --- Cargo.lock | 11 + veilid-cli/Cargo.toml | 4 +- veilid-cli/src/command_processor.rs | 11 +- veilid-cli/src/main.rs | 1 + veilid-cli/src/peers_table_view.rs | 99 ++++++ veilid-cli/src/ui.rs | 96 ++++-- veilid-core/Cargo.toml | 1 + veilid-core/src/network_manager/mod.rs | 18 + veilid-core/src/routing_table/mod.rs | 21 +- veilid-core/src/routing_table/node_ref.rs | 7 +- veilid-core/src/rpc_processor/origin.rs | 53 +++ veilid-core/src/veilid_api/mod.rs | 51 ++- .../src/veilid_api/serialize_helpers.rs | 56 ++++ veilid-flutter/example/lib/main.dart | 3 +- veilid-flutter/example/pubspec.lock | 51 ++- veilid-flutter/example/pubspec.yaml | 2 + veilid-flutter/lib/veilid.dart | 312 ++++++++++++++++-- 17 files changed, 689 insertions(+), 108 deletions(-) create mode 100644 veilid-cli/src/peers_table_view.rs create mode 100644 veilid-core/src/rpc_processor/origin.rs diff --git a/Cargo.lock b/Cargo.lock index 4ab012f4..0c1f5cbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1209,6 +1209,15 @@ dependencies = [ "xi-unicode", ] +[[package]] +name = "cursive_table_view" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8935dd87d19c54b7506b245bc988a7b4e65b1058e1d0d64c0ad9b3188e48060" +dependencies = [ + "cursive_core", +] + [[package]] name = "curve25519-dalek" version = "3.2.1" @@ -5087,6 +5096,7 @@ dependencies = [ "cursive", "cursive-flexi-logger-view", "cursive_buffered_backend", + "cursive_table_view", "directories", "flexi_logger", "futures", @@ -5155,6 +5165,7 @@ dependencies = [ "nix 0.25.0", "no-std-net", "once_cell", + "owning_ref", "owo-colors", "parking_lot 0.12.1", "rand 0.7.3", diff --git a/veilid-cli/Cargo.toml b/veilid-cli/Cargo.toml index 1162b14c..bc2e5e7a 100644 --- a/veilid-cli/Cargo.toml +++ b/veilid-cli/Cargo.toml @@ -23,9 +23,9 @@ tokio-util = { version = "^0", features = ["compat"], optional = true} async-tungstenite = { version = "^0.8" } cursive-flexi-logger-view = { path = "../external/cursive-flexi-logger-view" } cursive_buffered_backend = { path = "../external/cursive_buffered_backend" } -# cursive-multiplex = "0.4.0" +# cursive-multiplex = "0.6.0" # cursive_tree_view = "0.6.0" -# cursive_table_view = "0.12.0" +cursive_table_view = "0.14.0" # cursive-tabs = "0.5.0" clap = "^3" directories = "^4" diff --git a/veilid-cli/src/command_processor.rs b/veilid-cli/src/command_processor.rs index a4872d82..0285630f 100644 --- a/veilid-cli/src/command_processor.rs +++ b/veilid-cli/src/command_processor.rs @@ -8,7 +8,7 @@ use std::net::SocketAddr; use std::rc::Rc; use std::time::{Duration, SystemTime}; use veilid_core::xx::{Eventual, EventualCommon}; -use veilid_core::VeilidConfigLogLevel; +use veilid_core::*; pub fn convert_loglevel(s: &str) -> Result { match s.to_ascii_lowercase().as_str() { @@ -323,9 +323,12 @@ change_log_level - change the log level for a tracing layer } pub fn update_network_status(&mut self, network: veilid_core::VeilidStateNetwork) { - self.inner_mut() - .ui - .set_network_status(network.started, network.bps_down, network.bps_up); + self.inner_mut().ui.set_network_status( + network.started, + network.bps_down, + network.bps_up, + network.peers, + ); } pub fn update_log(&mut self, log: veilid_core::VeilidStateLog) { diff --git a/veilid-cli/src/main.rs b/veilid-cli/src/main.rs index 2531dc1f..3e2b2b2a 100644 --- a/veilid-cli/src/main.rs +++ b/veilid-cli/src/main.rs @@ -12,6 +12,7 @@ use tools::*; mod client_api_connection; mod command_processor; +mod peers_table_view; mod settings; mod tools; mod ui; diff --git a/veilid-cli/src/peers_table_view.rs b/veilid-cli/src/peers_table_view.rs new file mode 100644 index 00000000..1ba95dd3 --- /dev/null +++ b/veilid-cli/src/peers_table_view.rs @@ -0,0 +1,99 @@ +use super::*; +use cursive_table_view::*; +use std::cmp::Ordering; +use veilid_core::PeerTableData; + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub enum PeerTableColumn { + NodeId, + Address, + LatencyAvg, + TransferDownAvg, + TransferUpAvg, +} + +// impl PeerTableColumn { +// fn as_str(&self) -> &str { +// match self { +// PeerTableColumn::NodeId => "Node Id", +// PeerTableColumn::Address => "Address", +// PeerTableColumn::LatencyAvg => "Latency", +// PeerTableColumn::TransferDownAvg => "Down", +// PeerTableColumn::TransferUpAvg => "Up", +// } +// } +// } + +fn format_ts(ts: u64) -> String { + let secs = timestamp_to_secs(ts); + if secs >= 1.0 { + format!("{:.2}s", timestamp_to_secs(ts)) + } else { + format!("{:.2}ms", timestamp_to_secs(ts) * 1000.0) + } +} + +fn format_bps(bps: u64) -> String { + if bps >= 1024u64 * 1024u64 * 1024u64 { + format!("{:.2}GB/s", (bps / (1024u64 * 1024u64)) as f64 / 1024.0) + } else if bps >= 1024u64 * 1024u64 { + format!("{:.2}MB/s", (bps / 1024u64) as f64 / 1024.0) + } else if bps >= 1024u64 { + format!("{:.2}KB/s", bps as f64 / 1024.0) + } else { + format!("{:.2}B/s", bps as f64) + } +} + +impl TableViewItem for PeerTableData { + fn to_column(&self, column: PeerTableColumn) -> String { + match column { + PeerTableColumn::NodeId => self.node_id.encode(), + PeerTableColumn::Address => format!( + "{:?}:{}", + self.peer_address.protocol_type(), + self.peer_address.to_socket_addr() + ), + PeerTableColumn::LatencyAvg => format!( + "{}", + self.peer_stats + .latency + .as_ref() + .map(|l| format_ts(l.average)) + .unwrap_or("---".to_owned()) + ), + PeerTableColumn::TransferDownAvg => format_bps(self.peer_stats.transfer.down.average), + PeerTableColumn::TransferUpAvg => format_bps(self.peer_stats.transfer.up.average), + } + } + + fn cmp(&self, other: &Self, column: PeerTableColumn) -> Ordering + where + Self: Sized, + { + match column { + PeerTableColumn::NodeId => self.node_id.cmp(&other.node_id), + PeerTableColumn::Address => self.to_column(column).cmp(&other.to_column(column)), + PeerTableColumn::LatencyAvg => self + .peer_stats + .latency + .as_ref() + .map(|l| l.average) + .cmp(&other.peer_stats.latency.as_ref().map(|l| l.average)), + PeerTableColumn::TransferDownAvg => self + .peer_stats + .transfer + .down + .average + .cmp(&other.peer_stats.transfer.down.average), + PeerTableColumn::TransferUpAvg => self + .peer_stats + .transfer + .up + .average + .cmp(&other.peer_stats.transfer.up.average), + } + } +} + +pub type PeersTableView = TableView; diff --git a/veilid-cli/src/ui.rs b/veilid-cli/src/ui.rs index ecd780ed..e4ac5da9 100644 --- a/veilid-cli/src/ui.rs +++ b/veilid-cli/src/ui.rs @@ -1,4 +1,5 @@ use crate::command_processor::*; +use crate::peers_table_view::*; use crate::settings::Settings; use crossbeam_channel::Sender; use cursive::align::*; @@ -10,6 +11,7 @@ use cursive::views::*; use cursive::Cursive; use cursive::CursiveRunnable; use cursive_flexi_logger_view::{CursiveLogWriter, FlexiLoggerView}; +//use cursive_multiplex::*; use log::*; use std::cell::RefCell; use std::collections::{HashMap, VecDeque}; @@ -20,7 +22,7 @@ use veilid_core::*; ////////////////////////////////////////////////////////////// /// struct Dirty { - pub value: T, + value: T, dirty: bool, } @@ -52,6 +54,7 @@ struct UIState { network_started: Dirty, network_down_up: Dirty<(f32, f32)>, connection_state: Dirty, + peers_state: Dirty>, } impl UIState { @@ -61,6 +64,7 @@ impl UIState { network_started: Dirty::new(false), network_down_up: Dirty::new((0.0, 0.0)), connection_state: Dirty::new(ConnectionState::Disconnected), + peers_state: Dirty::new(Vec::new()), } } } @@ -219,6 +223,9 @@ impl UI { fn status_bar(s: &mut Cursive) -> ViewRef { s.find_name("status-bar").unwrap() } + fn peers(s: &mut Cursive) -> ViewRef { + s.find_name("peers").unwrap() + } fn render_attachment_state<'a>(inner: &mut UIInner) -> &'a str { match inner.ui_state.attachment_state.get() { AttachmentState::Detached => " Detached [----]", @@ -607,15 +614,23 @@ impl UI { statusbar.set_content(status); } + fn refresh_peers(s: &mut Cursive) { + let mut peers = UI::peers(s); + let inner = Self::inner_mut(s); + peers.set_items_stable(inner.ui_state.peers_state.get().clone()); + } + 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; + let mut refresh_peers = false; if inner.ui_state.attachment_state.take_dirty() { refresh_statusbar = true; refresh_button_attach = true; + refresh_peers = true; } if inner.ui_state.network_started.take_dirty() { refresh_statusbar = true; @@ -627,6 +642,10 @@ impl UI { refresh_statusbar = true; refresh_button_attach = true; refresh_connection_dialog = true; + refresh_peers = true; + } + if inner.ui_state.peers_state.take_dirty() { + refresh_peers = true; } drop(inner); @@ -640,6 +659,9 @@ impl UI { if refresh_connection_dialog { Self::refresh_connection_dialog(s); } + if refresh_peers { + Self::refresh_peers(s); + } } //////////////////////////////////////////////////////////////////////////// @@ -686,30 +708,48 @@ impl UI { 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 node_events_view = Panel::new( + FlexiLoggerView::new_scrollable() + .with_name("node-events") + .full_screen(), + ) + .title_position(HAlign::Left) + .title("Node Events"); + + let peers_table_view = PeersTableView::new() + .column(PeerTableColumn::NodeId, "Node Id", |c| c.width(43)) + .column(PeerTableColumn::Address, "Address", |c| c) + .column(PeerTableColumn::LatencyAvg, "Ping", |c| c.width(8)) + .column(PeerTableColumn::TransferDownAvg, "Down", |c| c.width(8)) + .column(PeerTableColumn::TransferUpAvg, "Up", |c| c.width(8)) + .with_name("peers") + .full_width() + .min_height(8); + + // attempt at using Mux. Mux has bugs, like resizing problems. + // let mut mux = Mux::new(); + // let node_node_events_view = mux + // .add_below(node_events_view, mux.root().build().unwrap()) + // .unwrap(); + // let node_peers_table_view = mux + // .add_below(peers_table_view, node_node_events_view) + // .unwrap(); + // mux.set_container_split_ratio(node_peers_table_view, 0.75) + // .unwrap(); + // let mut mainlayout = LinearLayout::vertical(); + // mainlayout.add_child(mux); + + // Back to fixed layout + let mut mainlayout = LinearLayout::vertical(); + mainlayout.add_child(node_events_view); + mainlayout.add_child(peers_table_view); + // ^^^ fixed layout + let mut command = StyledString::new(); command.append_styled("Command> ", ColorStyle::title_primary()); // - mainlayout.get_mut().add_child( + mainlayout.add_child( LinearLayout::horizontal() .child(TextView::new(command)) .child( @@ -738,7 +778,7 @@ impl UI { ColorStyle::highlight_inactive(), ); - mainlayout.get_mut().add_child( + mainlayout.add_child( LinearLayout::horizontal() .color(Some(ColorStyle::highlight_inactive())) .child( @@ -776,13 +816,20 @@ impl UI { inner.ui_state.attachment_state.set(state); let _ = inner.cb_sink.send(Box::new(UI::update_cb)); } - pub fn set_network_status(&mut self, started: bool, bps_down: u64, bps_up: u64) { + pub fn set_network_status( + &mut self, + started: bool, + bps_down: u64, + bps_up: u64, + peers: Vec, + ) { let mut inner = self.inner.borrow_mut(); inner.ui_state.network_started.set(started); inner.ui_state.network_down_up.set(( ((bps_down as f64) / 1000.0f64) as f32, ((bps_up as f64) / 1000.0f64) as f32, )); + inner.ui_state.peers_state.set(peers); let _ = inner.cb_sink.send(Box::new(UI::update_cb)); } pub fn set_connection_state(&mut self, state: ConnectionState) { @@ -790,6 +837,7 @@ impl UI { inner.ui_state.connection_state.set(state); let _ = inner.cb_sink.send(Box::new(UI::update_cb)); } + pub fn add_node_event(&self, event: String) { let inner = self.inner.borrow(); let color = *inner.log_colors.get(&Level::Info).unwrap(); diff --git a/veilid-core/Cargo.toml b/veilid-core/Cargo.toml index 7ec5379e..4959464b 100644 --- a/veilid-core/Cargo.toml +++ b/veilid-core/Cargo.toml @@ -41,6 +41,7 @@ lazy_static = "^1" directories = "^4" once_cell = "^1" json = "^0" +owning_ref = "^0" flume = { version = "^0", features = ["async"] } enumset = { version= "^1", features = ["serde"] } backtrace = { version = "^0", optional = true } diff --git a/veilid-core/src/network_manager/mod.rs b/veilid-core/src/network_manager/mod.rs index 38bec4d8..ad53e620 100644 --- a/veilid-core/src/network_manager/mod.rs +++ b/veilid-core/src/network_manager/mod.rs @@ -1680,12 +1680,30 @@ impl NetworkManager { started: true, bps_down: inner.stats.self_stats.transfer_stats.down.average, bps_up: inner.stats.self_stats.transfer_stats.up.average, + peers: { + let mut out = Vec::new(); + let routing_table = inner.routing_table.as_ref().unwrap(); + for (k, v) in routing_table.get_recent_peers() { + if let Some(nr) = routing_table.lookup_node_ref(k) { + let peer_stats = nr.peer_stats(); + let peer = PeerTableData { + node_id: k, + peer_address: v.last_connection.remote(), + peer_stats, + }; + out.push(peer); + } + } + + out + }, } } else { VeilidStateNetwork { started: false, bps_down: 0, bps_up: 0, + peers: Vec::new(), } } } diff --git a/veilid-core/src/routing_table/mod.rs b/veilid-core/src/routing_table/mod.rs index 84ed94a6..7aa9e3c4 100644 --- a/veilid-core/src/routing_table/mod.rs +++ b/veilid-core/src/routing_table/mod.rs @@ -29,7 +29,7 @@ const RECENT_PEERS_TABLE_SIZE: usize = 64; #[derive(Debug, Clone, Copy)] pub struct RecentPeersEntry { - last_connection: ConnectionDescriptor, + pub last_connection: ConnectionDescriptor, } /// RoutingTable rwlock-internal data @@ -776,13 +776,15 @@ impl RoutingTable { descriptor: ConnectionDescriptor, timestamp: u64, ) -> Option { - self.create_node_ref(node_id, |e| { - // set the most recent node address for connection finding and udp replies - e.set_last_connection(descriptor, timestamp); - + let out = self.create_node_ref(node_id, |e| { // this node is live because it literally just connected to us e.touch_last_seen(timestamp); - }) + }); + if let Some(nr) = &out { + // set the most recent node address for connection finding and udp replies + nr.set_last_connection(descriptor, timestamp); + } + out } // Ticks about once per second @@ -834,11 +836,8 @@ impl RoutingTable { .collect() } - fn touch_recent_peer( - inner: &mut RoutingTableInner, - node_id: DHTKey, - last_connection: ConnectionDescriptor, - ) { + pub fn touch_recent_peer(&self, node_id: DHTKey, last_connection: ConnectionDescriptor) { + let mut inner = self.inner.write(); inner .recent_peers .insert(node_id, RecentPeersEntry { last_connection }); diff --git a/veilid-core/src/routing_table/node_ref.rs b/veilid-core/src/routing_table/node_ref.rs index d805882f..6d50658b 100644 --- a/veilid-core/src/routing_table/node_ref.rs +++ b/veilid-core/src/routing_table/node_ref.rs @@ -200,6 +200,9 @@ impl NodeRef { pub fn state(&self, cur_ts: u64) -> BucketEntryState { self.operate(|_rti, e| e.state(cur_ts)) } + pub fn peer_stats(&self) -> PeerStats { + self.operate(|_rti, e| e.peer_stats().clone()) + } // Per-RoutingDomain accessors pub fn make_peer_info(&self, routing_domain: RoutingDomain) -> Option { @@ -322,7 +325,9 @@ impl NodeRef { } pub fn set_last_connection(&self, connection_descriptor: ConnectionDescriptor, ts: u64) { - self.operate_mut(|_rti, e| e.set_last_connection(connection_descriptor, ts)) + self.operate_mut(|_rti, e| e.set_last_connection(connection_descriptor, ts)); + self.routing_table + .touch_recent_peer(self.node_id(), connection_descriptor); } pub fn has_any_dial_info(&self) -> bool { diff --git a/veilid-core/src/rpc_processor/origin.rs b/veilid-core/src/rpc_processor/origin.rs new file mode 100644 index 00000000..e1126cab --- /dev/null +++ b/veilid-core/src/rpc_processor/origin.rs @@ -0,0 +1,53 @@ +use super::*; + +#[derive(Debug, Clone)] +pub enum Origin { + Sender, + PrivateRoute(PrivateRoute), +} + +impl Origin { + pub fn sender() -> Self { + Self::Sender + } + + pub fn private_route(private_route: PrivateRoute) -> Self { + Self::PrivateRoute(private_route) + } + + pub fn into_respond_to(self, destination: &Destination) -> Result { + match self { + Self::Sender => { + let peer = match destination { + Destination::Direct { + target, + safety_route_spec, + } => todo!(), + Destination::Relay { + relay, + target, + safety_route_spec, + } => todo!(), + Destination::PrivateRoute { + private_route, + safety_route_spec, + } => todo!(), + }; + let routing_table = peer.routing_table(); + let routing_domain = peer.best_routing_domain(); + // Send some signed node info along with the question if this node needs to be replied to + if routing_table.has_valid_own_node_info() + && !peer.has_seen_our_node_info(routing_domain) + { + let our_sni = self + .routing_table() + .get_own_signed_node_info(routing_domain); + RespondTo::Sender(Some(our_sni)) + } else { + RespondTo::Sender(None) + } + } + Self::PrivateRoute(pr) => RespondTo::PrivateRoute(pr), + } + } +} diff --git a/veilid-core/src/veilid_api/mod.rs b/veilid-core/src/veilid_api/mod.rs index dc7f9078..ed94fea4 100644 --- a/veilid-core/src/veilid_api/mod.rs +++ b/veilid-core/src/veilid_api/mod.rs @@ -215,22 +215,32 @@ impl fmt::Display for VeilidLogLevel { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct VeilidStateLog { pub log_level: VeilidLogLevel, pub message: String, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct VeilidStateAttachment { pub state: AttachmentState, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct PeerTableData { + pub node_id: DHTKey, + pub peer_address: PeerAddress, + pub peer_stats: PeerStats, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct VeilidStateNetwork { pub started: bool, + #[serde(with = "json_as_string")] pub bps_down: u64, + #[serde(with = "json_as_string")] pub bps_up: u64, + pub peers: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -706,6 +716,15 @@ impl Address { } } +impl fmt::Display for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Address::IPV4(v4) => write!(f, "{}", v4), + Address::IPV6(v6) => write!(f, "{}", v6), + } + } +} + impl FromStr for Address { type Err = VeilidAPIError; fn from_str(host: &str) -> Result { @@ -1447,6 +1466,7 @@ impl PeerInfo { #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Serialize, Deserialize)] pub struct PeerAddress { protocol_type: ProtocolType, + #[serde(with = "json_as_string")] socket_address: SocketAddress, } @@ -1565,42 +1585,53 @@ impl FromStr for NodeDialInfo { } } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct LatencyStats { + #[serde(with = "json_as_string")] pub fastest: u64, // fastest latency in the ROLLING_LATENCIES_SIZE last latencies + #[serde(with = "json_as_string")] pub average: u64, // average latency over the ROLLING_LATENCIES_SIZE last latencies + #[serde(with = "json_as_string")] pub slowest: u64, // slowest latency in the ROLLING_LATENCIES_SIZE last latencies } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct TransferStats { - pub total: u64, // total amount transferred ever + #[serde(with = "json_as_string")] + pub total: u64, // total amount transferred ever + #[serde(with = "json_as_string")] pub maximum: u64, // maximum rate over the ROLLING_TRANSFERS_SIZE last amounts + #[serde(with = "json_as_string")] pub average: u64, // average rate over the ROLLING_TRANSFERS_SIZE last amounts + #[serde(with = "json_as_string")] pub minimum: u64, // minimum rate over the ROLLING_TRANSFERS_SIZE last amounts } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct TransferStatsDownUp { pub down: TransferStats, pub up: TransferStats, } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct RPCStats { pub messages_sent: u32, // number of rpcs that have been sent in the total_time range pub messages_rcvd: u32, // number of rpcs that have been received in the total_time range pub questions_in_flight: u32, // number of questions issued that have yet to be answered + #[serde(with = "opt_json_as_string")] pub last_question: Option, // when the peer was last questioned (either successfully or not) and we wanted an answer + #[serde(with = "opt_json_as_string")] pub last_seen_ts: Option, // when the peer was last seen for any reason, including when we first attempted to reach out to it + #[serde(with = "opt_json_as_string")] pub first_consecutive_seen_ts: Option, // the timestamp of the first consecutive proof-of-life for this node (an answer or received question) pub recent_lost_answers: u32, // number of answers that have been lost since we lost reliability pub failed_to_send: u32, // number of messages that have failed to send since we last successfully sent one } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct PeerStats { - pub time_added: u64, // when the peer was added to the routing table + #[serde(with = "json_as_string")] + pub time_added: u64, // when the peer was added to the routing table pub rpc_stats: RPCStats, // information about RPCs pub latency: Option, // latencies for communications with the peer pub transfer: TransferStatsDownUp, // Stats for communications with the peer diff --git a/veilid-core/src/veilid_api/serialize_helpers.rs b/veilid-core/src/veilid_api/serialize_helpers.rs index dde7986a..f2d6d28d 100644 --- a/veilid-core/src/veilid_api/serialize_helpers.rs +++ b/veilid-core/src/veilid_api/serialize_helpers.rs @@ -40,3 +40,59 @@ pub fn serialize_json(val: T) -> String { } } } + +pub mod json_as_string { + use std::fmt::Display; + use std::str::FromStr; + + use serde::{de, Deserialize, Deserializer, Serializer}; + + pub fn serialize(value: &T, serializer: S) -> Result + where + T: Display, + S: Serializer, + { + serializer.collect_str(value) + } + + pub fn deserialize<'de, T, D>(deserializer: D) -> Result + where + T: FromStr, + T::Err: Display, + D: Deserializer<'de>, + { + String::deserialize(deserializer)? + .parse() + .map_err(de::Error::custom) + } +} + +pub mod opt_json_as_string { + use std::fmt::Display; + use std::str::FromStr; + + use serde::{de, Deserialize, Deserializer, Serializer}; + + pub fn serialize(value: &Option, serializer: S) -> Result + where + T: Display, + S: Serializer, + { + match value { + Some(v) => serializer.collect_str(v), + None => serializer.serialize_none(), + } + } + + pub fn deserialize<'de, T, D>(deserializer: D) -> Result, D::Error> + where + T: FromStr, + T::Err: Display, + D: Deserializer<'de>, + { + match Option::::deserialize(deserializer)? { + None => Ok(None), + Some(v) => Ok(Some(v.parse::().map_err(de::Error::custom)?)), + } + } +} diff --git a/veilid-flutter/example/lib/main.dart b/veilid-flutter/example/lib/main.dart index b4686e43..951d7b37 100644 --- a/veilid-flutter/example/lib/main.dart +++ b/veilid-flutter/example/lib/main.dart @@ -3,7 +3,6 @@ import 'dart:typed_data'; import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:veilid/veilid.dart'; import 'package:flutter_loggy/flutter_loggy.dart'; @@ -188,7 +187,7 @@ class _MyAppState extends State with UiLoggy { if (update is VeilidUpdateLog) { await processUpdateLog(update); } else { - loggy.trace("Update: " + update.json.toString()); + loggy.trace("Update: ${update.json}"); } } } diff --git a/veilid-flutter/example/pubspec.lock b/veilid-flutter/example/pubspec.lock index a90f7532..2a4e58ce 100644 --- a/veilid-flutter/example/pubspec.lock +++ b/veilid-flutter/example/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.2" + version: "2.9.0" boolean_selector: dependency: transitive description: @@ -28,21 +28,14 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" + version: "1.2.1" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" collection: dependency: transitive description: @@ -63,21 +56,21 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: transitive description: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" file: dependency: transitive description: name: file url: "https://pub.dartlang.org" source: hosted - version: "6.1.2" + version: "6.1.4" flutter: dependency: "direct main" description: flutter @@ -134,30 +127,30 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" path: - dependency: transitive + dependency: "direct main" description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider url: "https://pub.dartlang.org" @@ -169,14 +162,14 @@ packages: name: path_provider_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.14" + version: "2.0.20" path_provider_ios: dependency: transitive description: name: path_provider_ios url: "https://pub.dartlang.org" source: hosted - version: "2.0.10" + version: "2.0.11" path_provider_linux: dependency: transitive description: @@ -204,7 +197,7 @@ packages: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.3" platform: dependency: transitive description: @@ -232,7 +225,7 @@ packages: name: rxdart url: "https://pub.dartlang.org" source: hosted - version: "0.27.4" + version: "0.27.5" sky_engine: dependency: transitive description: flutter @@ -244,7 +237,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.9.0" stack_trace: dependency: transitive description: @@ -265,21 +258,21 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.12" vector_math: dependency: transitive description: @@ -300,14 +293,14 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.7.0" + version: "3.0.0" xdg_directories: dependency: transitive description: name: xdg_directories url: "https://pub.dartlang.org" source: hosted - version: "0.2.0+1" + version: "0.2.0+2" sdks: dart: ">=2.17.0 <3.0.0" flutter: ">=3.0.0" diff --git a/veilid-flutter/example/pubspec.yaml b/veilid-flutter/example/pubspec.yaml index beff9729..73ee637f 100644 --- a/veilid-flutter/example/pubspec.yaml +++ b/veilid-flutter/example/pubspec.yaml @@ -34,6 +34,8 @@ dependencies: cupertino_icons: ^1.0.2 loggy: ^2.0.1+1 flutter_loggy: ^2.0.1 + path_provider: ^2.0.11 + path: ^1.8.1 dev_dependencies: flutter_test: diff --git a/veilid-flutter/lib/veilid.dart b/veilid-flutter/lib/veilid.dart index dffbb33c..23fb4a4d 100644 --- a/veilid-flutter/lib/veilid.dart +++ b/veilid-flutter/lib/veilid.dart @@ -987,6 +987,243 @@ class VeilidConfig { network = VeilidConfigNetwork.fromJson(json['network']); } +//////////// + +class LatencyStats { + BigInt fastest; + BigInt average; + BigInt slowest; + + LatencyStats({ + required this.fastest, + required this.average, + required this.slowest, + }); + + Map get json { + return { + 'fastest': fastest.toString(), + 'average': average.toString(), + 'slowest': slowest.toString(), + }; + } + + LatencyStats.fromJson(Map json) + : fastest = BigInt.parse(json['fastest']), + average = BigInt.parse(json['average']), + slowest = BigInt.parse(json['slowest']); +} + +//////////// + +class TransferStats { + BigInt total; + BigInt fastest; + BigInt average; + BigInt slowest; + + TransferStats({ + required this.total, + required this.fastest, + required this.average, + required this.slowest, + }); + + Map get json { + return { + 'total': total.toString(), + 'fastest': fastest.toString(), + 'average': average.toString(), + 'slowest': slowest.toString(), + }; + } + + TransferStats.fromJson(Map json) + : total = BigInt.parse(json['fastest']), + fastest = BigInt.parse(json['fastest']), + average = BigInt.parse(json['average']), + slowest = BigInt.parse(json['slowest']); +} + +//////////// + +class TransferStatsDownUp { + TransferStats down; + TransferStats up; + + TransferStatsDownUp({ + required this.down, + required this.up, + }); + + Map get json { + return { + 'down': down.toString(), + 'up': up.toString(), + }; + } + + TransferStatsDownUp.fromJson(Map json) + : down = TransferStats.fromJson(json['down']), + up = TransferStats.fromJson(json['up']); +} + +//////////// + +class RPCStats { + int messagesSent; + int messagesRcvd; + int questionsInFlight; + BigInt? lastQuestion; + BigInt? lastSeenTs; + BigInt? firstConsecutiveSeenTs; + int recentLostAnswers; + int failedToSend; + + RPCStats({ + required this.messagesSent, + required this.messagesRcvd, + required this.questionsInFlight, + required this.lastQuestion, + required this.lastSeenTs, + required this.firstConsecutiveSeenTs, + required this.recentLostAnswers, + required this.failedToSend, + }); + + Map get json { + return { + 'messages_sent': messagesSent, + 'messages_rcvd': messagesRcvd, + 'questions_in_flight': questionsInFlight, + 'last_question': lastQuestion?.toString(), + 'last_seen_ts': lastSeenTs?.toString(), + 'first_consecutive_seen_ts': firstConsecutiveSeenTs?.toString(), + 'recent_lost_answers': recentLostAnswers, + 'failed_to_send': failedToSend, + }; + } + + RPCStats.fromJson(Map json) + : messagesSent = json['messages_sent'], + messagesRcvd = json['messages_rcvd'], + questionsInFlight = json['questions_in_flight'], + lastQuestion = json['last_question'] != null + ? BigInt.parse(json['last_question']) + : null, + lastSeenTs = json['last_seen_ts'] != null + ? BigInt.parse(json['last_seen_ts']) + : null, + firstConsecutiveSeenTs = json['first_consecutive_seen_ts'] != null + ? BigInt.parse(json['first_consecutive_seen_ts']) + : null, + recentLostAnswers = json['recent_lost_answers'], + failedToSend = json['failed_to_send']; +} + +//////////// + +class PeerStats { + BigInt timeAdded; + RPCStats rpcStats; + LatencyStats? latency; + TransferStatsDownUp transfer; + + PeerStats({ + required this.timeAdded, + required this.rpcStats, + required this.latency, + required this.transfer, + }); + + Map get json { + return { + 'time_added': timeAdded.toString(), + 'rpc_stats': rpcStats.json, + 'latency': latency?.json, + 'transfer': transfer.json, + }; + } + + PeerStats.fromJson(Map json) + : timeAdded = BigInt.parse(json['time_added']), + rpcStats = RPCStats.fromJson(json['rpc_stats']), + latency = json['latency'] != null + ? LatencyStats.fromJson(json['latency']) + : null, + transfer = TransferStatsDownUp.fromJson(json['transfer']); +} + +//////////// + +class PeerTableData { + String nodeId; + PeerAddress peerAddress; + PeerStats peerStats; + + PeerTableData({ + required this.nodeId, + required this.peerAddress, + required this.peerStats, + }); + + Map get json { + return { + 'node_id': nodeId, + 'peer_address': peerAddress.json, + 'peer_stats': peerStats.json, + }; + } + + PeerTableData.fromJson(Map json) + : nodeId = json['node_id'], + peerAddress = PeerAddress.fromJson(json['peer_address']), + peerStats = PeerStats.fromJson(json['peer_stats']); +} + +////////////////////////////////////// +/// AttachmentState + +enum ProtocolType { + udp, + tcp, + ws, + wss, +} + +extension ProtocolTypeExt on ProtocolType { + String get json { + return name.toUpperCase(); + } +} + +ProtocolType protocolTypeFromJson(String j) { + return ProtocolType.values.byName(j.toLowerCase()); +} + +//////////// + +class PeerAddress { + ProtocolType protocolType; + String socketAddress; + + PeerAddress({ + required this.protocolType, + required this.socketAddress, + }); + + Map get json { + return { + 'protocol_type': protocolType.json, + 'socket_address': socketAddress, + }; + } + + PeerAddress.fromJson(Map json) + : protocolType = protocolTypeFromJson(json['protocol_type']), + socketAddress = json['socket_address']; +} + ////////////////////////////////////// /// VeilidUpdate @@ -996,16 +1233,17 @@ abstract class VeilidUpdate { case "Log": { return VeilidUpdateLog( - veilidLogLevelFromJson(json["log_level"]), json["message"]); + logLevel: veilidLogLevelFromJson(json["log_level"]), + message: json["message"]); } case "Attachment": { - return VeilidUpdateAttachment(attachmentStateFromJson(json["state"])); + return VeilidUpdateAttachment( + state: VeilidStateAttachment.fromJson(json)); } case "Network": { - return VeilidUpdateNetwork( - json["started"], json["bps_up"], json["bps_down"]); + return VeilidUpdateNetwork(state: VeilidStateNetwork.fromJson(json)); } default: { @@ -1021,7 +1259,10 @@ class VeilidUpdateLog implements VeilidUpdate { final VeilidLogLevel logLevel; final String message; // - VeilidUpdateLog(this.logLevel, this.message); + VeilidUpdateLog({ + required this.logLevel, + required this.message, + }); @override Map get json { @@ -1034,34 +1275,28 @@ class VeilidUpdateLog implements VeilidUpdate { } class VeilidUpdateAttachment implements VeilidUpdate { - final AttachmentState state; + final VeilidStateAttachment state; // - VeilidUpdateAttachment(this.state); + VeilidUpdateAttachment({required this.state}); @override Map get json { - return { - 'kind': "Attachment", - 'state': state.json, - }; + var jsonRep = state.json; + jsonRep['kind'] = "Attachment"; + return jsonRep; } } class VeilidUpdateNetwork implements VeilidUpdate { - final bool started; - final int bpsDown; - final int bpsUp; + final VeilidStateNetwork state; // - VeilidUpdateNetwork(this.started, this.bpsDown, this.bpsUp); + VeilidUpdateNetwork({required this.state}); @override Map get json { - return { - 'kind': "Network", - 'started': started, - 'bps_down': bpsDown, - 'bps_up': bpsUp - }; + var jsonRep = state.json; + jsonRep['kind'] = "Network"; + return jsonRep; } } @@ -1075,6 +1310,12 @@ class VeilidStateAttachment { VeilidStateAttachment.fromJson(Map json) : state = attachmentStateFromJson(json['state']); + + Map get json { + return { + 'state': state.json, + }; + } } ////////////////////////////////////// @@ -1082,11 +1323,30 @@ class VeilidStateAttachment { class VeilidStateNetwork { final bool started; + final int bpsDown; + final int bpsUp; + final List peers; - VeilidStateNetwork(this.started); + VeilidStateNetwork( + {required this.started, + required this.bpsDown, + required this.bpsUp, + required this.peers}); VeilidStateNetwork.fromJson(Map json) - : started = json['started']; + : started = json['started'], + bpsDown = json['bps_down'], + bpsUp = json['bps_up'], + peers = json['peers'].map((j) => PeerTableData.fromJson(j)).toList(); + + Map get json { + return { + 'started': started, + 'bps_down': bpsDown, + 'bps_up': bpsUp, + 'peers': peers.map((p) => p.json).toList(), + }; + } } ////////////////////////////////////// @@ -1096,11 +1356,13 @@ class VeilidState { final VeilidStateAttachment attachment; final VeilidStateNetwork network; - VeilidState(this.attachment, this.network); - VeilidState.fromJson(Map json) : attachment = VeilidStateAttachment.fromJson(json['attachment']), network = VeilidStateNetwork.fromJson(json['network']); + + Map get json { + return {'attachment': attachment.json, 'network': network.json}; + } } //////////////////////////////////////