From d7e7f3ba1dfd0f0b17012c76974091a3eb262b7c Mon Sep 17 00:00:00 2001 From: John Smith Date: Tue, 22 Nov 2022 22:48:03 -0500 Subject: [PATCH 01/88] checkpoint --- veilid-core/src/routing_table/privacy.rs | 13 ++++ .../src/routing_table/route_spec_store.rs | 45 ++++++++---- veilid-core/src/rpc_processor/mod.rs | 69 ++++++++++++++++--- veilid-core/src/veilid_api/debug.rs | 21 +++++- 4 files changed, 123 insertions(+), 25 deletions(-) diff --git a/veilid-core/src/routing_table/privacy.rs b/veilid-core/src/routing_table/privacy.rs index 352b716b..c5855a35 100644 --- a/veilid-core/src/routing_table/privacy.rs +++ b/veilid-core/src/routing_table/privacy.rs @@ -83,6 +83,14 @@ impl PrivateRoute { } } + /// Check if this is a stub route + pub fn is_stub(&self) -> bool { + if let PrivateRouteHops::FirstHop(first_hop) = self.hops { + return first_hop.next_hop.is_none(); + } + false + } + /// Remove the first unencrypted hop if possible pub fn pop_first_hop(&mut self) -> Option { match &mut self.hops { @@ -149,6 +157,8 @@ pub struct SafetyRoute { impl SafetyRoute { pub fn new_stub(public_key: DHTKey, private_route: PrivateRoute) -> Self { + // First hop should have already been popped off for stubbed safety routes since + // we are sending directly to the first hop assert!(matches!(private_route.hops, PrivateRouteHops::Data(_))); Self { public_key, @@ -156,6 +166,9 @@ impl SafetyRoute { hops: SafetyRouteHops::Private(private_route), } } + pub fn is_stub(&self) -> bool { + matches!(self.hops, SafetyRouteHops::Private(_)) + } } impl fmt::Display for SafetyRoute { diff --git a/veilid-core/src/routing_table/route_spec_store.rs b/veilid-core/src/routing_table/route_spec_store.rs index 66d80cf6..e5a5afac 100644 --- a/veilid-core/src/routing_table/route_spec_store.rs +++ b/veilid-core/src/routing_table/route_spec_store.rs @@ -757,26 +757,38 @@ impl RouteSpecStore { /// Test an allocated route for continuity pub async fn test_route(&self, key: &DHTKey) -> EyreResult { - let inner = &mut *self.inner.lock(); - let rsd = Self::detail(inner, &key).ok_or_else(|| eyre!("route does not exist"))?; let rpc_processor = self.unlocked_inner.routing_table.rpc_processor(); - // Target is the last hop - let target = rsd.hop_node_refs.last().unwrap().clone(); - let hop_count = rsd.hops.len(); - let stability = rsd.stability; - let sequencing = rsd.sequencing; + let (target, safety_selection) = { + let inner = &mut *self.inner.lock(); + let rsd = Self::detail(inner, &key).ok_or_else(|| eyre!("route does not exist"))?; + + // Routes with just one hop can be pinged directly + // More than one hop can be pinged across the route with the target being the second to last hop + if rsd.hops.len() == 1 { + let target = rsd.hop_node_refs[0].clone(); + let sequencing = rsd.sequencing; + (target, SafetySelection::Unsafe(sequencing)) + } else { + let target = rsd.hop_node_refs[rsd.hops.len() - 2].clone(); + let hop_count = rsd.hops.len(); + let stability = rsd.stability; + let sequencing = rsd.sequencing; + let safety_spec = SafetySpec { + preferred_route: Some(key.clone()), + hop_count, + stability, + sequencing, + }; + (target, SafetySelection::Safe(safety_spec)) + } + }; // Test with ping to end let res = match rpc_processor .rpc_call_status(Destination::Direct { target, - safety_selection: SafetySelection::Safe(SafetySpec { - preferred_route: Some(key.clone()), - hop_count, - stability, - sequencing, - }), + safety_selection, }) .await? { @@ -1100,8 +1112,11 @@ impl RouteSpecStore { // See if the preferred route is here if let Some(preferred_route) = safety_spec.preferred_route { - if inner.content.details.contains_key(&preferred_route) { - return Ok(Some(preferred_route)); + if let Some(preferred_rsd) = inner.content.details.get(&preferred_route) { + // Only use the preferred route if it doesn't end with the avoid nodes + if !avoid_node_ids.contains(preferred_rsd.hops.last().unwrap()) { + return Ok(Some(preferred_route)); + } } } diff --git a/veilid-core/src/rpc_processor/mod.rs b/veilid-core/src/rpc_processor/mod.rs index 1a5c1781..90986223 100644 --- a/veilid-core/src/rpc_processor/mod.rs +++ b/veilid-core/src/rpc_processor/mod.rs @@ -184,11 +184,21 @@ impl Answer { } struct RenderedOperation { - message: Vec, // The rendered operation bytes - node_id: DHTKey, // Destination node id we're sending to + message: Vec, // The rendered operation bytes + node_id: DHTKey, // Destination node id we're sending to node_ref: NodeRef, // Node to send envelope to (may not be destination node id in case of relay) hop_count: usize, // Total safety + private route hop count + 1 hop for the initial send + safety_route: Option, // The safety route used to send the message + private_route: Option, // The private route used to send the message } + +#[derive(Copy, Clone, Debug)] +enum RPCKind { + Question, + Statement, + Answer +} + ///////////////////////////////////////////////////////////////////// pub struct RPCProcessorInner { @@ -484,7 +494,7 @@ impl RPCProcessor { ) -> Result, RPCError> { let routing_table = self.routing_table(); let rss = routing_table.route_spec_store(); - + let pr_is_stub = private_route.is_stub(); let pr_hop_count = private_route.hop_count; let pr_pubkey = private_route.public_key; @@ -542,6 +552,16 @@ impl RPCProcessor { node_id: out_node_id, node_ref: compiled_route.first_hop, hop_count: out_hop_count, + safety_route: if compiled_route.safety_route.is_stub() { + None + } else { + Some(compiled_route.safety_route.public_key) + }, + private_route: if pr_is_stub { + None + } else { + Some(pr_pubkey) + } }; Ok(NetworkResult::value(out)) @@ -610,6 +630,8 @@ impl RPCProcessor { node_id, node_ref, hop_count: 1, + safety_route: None, + private_route: None, }); } SafetySelection::Safe(_) => { @@ -706,7 +728,17 @@ impl RPCProcessor { } } - // Issue a question over the network, possibly using an anonymized route + /// Record failure to send to node or route + fn record_send_failure(&self, rpc_kind: RPCKind, send_ts: u64, node_ref: NodeRef, safety_route: Option, private_route: Option) { + xxx implement me + } + + /// Record success sending to node or route + fn record_send_success(&self, rpc_kind: RPCKind, send_ts: u64, bytes: u64, node_ref: NodeRef, safety_route: Option, private_route: Option) { + xxx implement me + } + + /// Issue a question over the network, possibly using an anonymized route #[instrument(level = "debug", skip(self, question), err)] async fn question( &self, @@ -729,6 +761,8 @@ impl RPCProcessor { node_id, node_ref, hop_count, + safety_route, + private_route, } = network_result_try!(self.render_operation(dest.clone(), &operation)?); // Calculate answer timeout @@ -774,6 +808,11 @@ impl RPCProcessor { } } + // Safety route stats + if let Some(sr_pubkey) = safety_route { + // + } + // Pass back waitable reply completion Ok(NetworkResult::value(WaitableReply { dest, @@ -807,6 +846,8 @@ impl RPCProcessor { node_id, node_ref, hop_count: _, + safety_route, + private_route, } = network_result_try!(self.render_operation(dest, &operation)?); // Send statement @@ -819,17 +860,22 @@ impl RPCProcessor { .map_err(|e| { // If we're returning an error, clean up node_ref - .stats_failed_to_send(send_ts, true); + .stats_failed_to_send(send_ts, false); RPCError::network(e) })? => { // If we couldn't send we're still cleaning up node_ref - .stats_failed_to_send(send_ts, true); + .stats_failed_to_send(send_ts, false); } ); // Successfully sent - node_ref.stats_question_sent(send_ts, bytes, true); + node_ref.stats_question_sent(send_ts, bytes, false); + + // Private route stats + xxx + // Safety route stats + safety_route Ok(NetworkResult::value(())) } @@ -860,6 +906,8 @@ impl RPCProcessor { node_id, node_ref, hop_count: _, + safety_route, + private_route, } = network_result_try!(self.render_operation(dest, &operation)?); // Send the reply @@ -871,7 +919,7 @@ impl RPCProcessor { .map_err(|e| { // If we're returning an error, clean up node_ref - .stats_failed_to_send(send_ts, true); + .stats_failed_to_send(send_ts, false); RPCError::network(e) })? => { // If we couldn't send we're still cleaning up @@ -883,6 +931,11 @@ impl RPCProcessor { // Reply successfully sent node_ref.stats_answer_sent(bytes); + // Private route stats + xxxx + // Safety route stats + xxx + Ok(NetworkResult::value(())) } diff --git a/veilid-core/src/veilid_api/debug.rs b/veilid-core/src/veilid_api/debug.rs index ef0ecef7..fa5ff34b 100644 --- a/veilid-core/src/veilid_api/debug.rs +++ b/veilid-core/src/veilid_api/debug.rs @@ -757,8 +757,25 @@ impl VeilidAPI { return Ok(out); } - async fn debug_route_test(&self, _args: Vec) -> Result { - let out = "xxx".to_string(); + async fn debug_route_test(&self, args: Vec) -> Result { + // + let netman = self.network_manager()?; + let routing_table = netman.routing_table(); + let rss = routing_table.route_spec_store(); + + let route_id = get_debug_argument_at(&args, 1, "debug_route", "route_id", get_dht_key)?; + + let success = rss + .test_route(&route_id) + .await + .map_err(VeilidAPIError::internal)?; + + let out = if success { + "SUCCESS".to_owned() + } else { + "FAILED".to_owned() + }; + return Ok(out); } From 0b2ecd53c778cde1add3a6873546bedf06be7ab6 Mon Sep 17 00:00:00 2001 From: John Smith Date: Wed, 23 Nov 2022 22:12:48 -0500 Subject: [PATCH 02/88] private route stats and tests --- veilid-core/src/network_manager/tasks.rs | 2 +- veilid-core/src/routing_table/privacy.rs | 2 +- .../src/routing_table/route_spec_store.rs | 369 ++++++++------- veilid-core/src/routing_table/tasks.rs | 38 +- veilid-core/src/rpc_processor/mod.rs | 443 +++++++++++++----- veilid-core/src/rpc_processor/rpc_route.rs | 12 +- veilid-core/src/veilid_api/debug.rs | 84 ++-- veilid-core/src/veilid_api/mod.rs | 1 - .../src/veilid_api/serialize_helpers.rs | 2 +- 9 files changed, 624 insertions(+), 329 deletions(-) diff --git a/veilid-core/src/network_manager/tasks.rs b/veilid-core/src/network_manager/tasks.rs index c9834645..3786410a 100644 --- a/veilid-core/src/network_manager/tasks.rs +++ b/veilid-core/src/network_manager/tasks.rs @@ -617,7 +617,7 @@ impl NetworkManager { // Do we know our network class yet? if let Some(network_class) = network_class { - // see if we have any routes that need extending + // see if we have any routes that need testing } // Commit the changes diff --git a/veilid-core/src/routing_table/privacy.rs b/veilid-core/src/routing_table/privacy.rs index c5855a35..a2322baf 100644 --- a/veilid-core/src/routing_table/privacy.rs +++ b/veilid-core/src/routing_table/privacy.rs @@ -85,7 +85,7 @@ impl PrivateRoute { /// Check if this is a stub route pub fn is_stub(&self) -> bool { - if let PrivateRouteHops::FirstHop(first_hop) = self.hops { + if let PrivateRouteHops::FirstHop(first_hop) = &self.hops { return first_hop.next_hop.is_none(); } false diff --git a/veilid-core/src/routing_table/route_spec_store.rs b/veilid-core/src/routing_table/route_spec_store.rs index e5a5afac..3706681b 100644 --- a/veilid-core/src/routing_table/route_spec_store.rs +++ b/veilid-core/src/routing_table/route_spec_store.rs @@ -27,17 +27,26 @@ pub struct KeyPair { secret: DHTKeySecret, } -#[derive(Clone, Debug, RkyvArchive, RkyvSerialize, RkyvDeserialize)] +#[derive(Clone, Debug, Default, RkyvArchive, RkyvSerialize, RkyvDeserialize)] #[archive_attr(repr(C), derive(CheckBytes))] -pub struct RouteSpecDetail { - /// Secret key +pub struct RouteStats { + /// Consecutive failed to send count #[with(Skip)] - pub secret_key: DHTKeySecret, - /// Route hops - pub hops: Vec, - /// Route noderefs + failed_to_send: u32, + /// Questions lost #[with(Skip)] - hop_node_refs: Vec, + questions_lost: u32, + /// Timestamp of when the route was created + created_ts: u64, + /// Timestamp of when the route was last checked for validity + #[with(Skip)] + last_tested_ts: Option, + /// Timestamp of when the route was last sent to + #[with(Skip)] + last_sent_ts: Option, + /// Timestamp of when the route was last received over + #[with(Skip)] + last_received_ts: Option, /// Transfers up and down transfer_stats_down_up: TransferStatsDownUp, /// Latency stats @@ -48,26 +57,104 @@ pub struct RouteSpecDetail { /// Accounting mechanism for the bandwidth across this route #[with(Skip)] transfer_stats_accounting: TransferStatsAccounting, +} + +impl RouteStats { + /// Make new route stats + pub fn new(created_ts: u64) -> Self { + Self { + created_ts, + ..Default::default() + } + } + /// Mark a route as having failed to send + pub fn record_send_failed(&mut self) { + self.failed_to_send += 1; + } + + /// Mark a route as having lost a question + pub fn record_question_lost(&mut self) { + self.questions_lost += 1; + } + + /// Mark a route as having received something + pub fn record_received(&mut self, cur_ts: u64, bytes: u64) { + self.last_received_ts = Some(cur_ts); + self.last_tested_ts = Some(cur_ts); + self.transfer_stats_accounting.add_down(bytes); + } + + /// Mark a route as having been sent to + pub fn record_sent(&mut self, cur_ts: u64, bytes: u64) { + self.last_sent_ts = Some(cur_ts); + self.transfer_stats_accounting.add_up(bytes); + } + + /// Mark a route as having been sent to + pub fn record_latency(&mut self, latency: u64) { + self.latency_stats = self.latency_stats_accounting.record_latency(latency); + } + + /// Mark a route as having been tested + pub fn record_tested(&mut self, cur_ts: u64) { + self.last_tested_ts = Some(cur_ts); + + // Reset question_lost and failed_to_send if we test clean + self.failed_to_send = 0; + self.questions_lost = 0; + } + + /// Roll transfers for these route stats + pub fn roll_transfers(&mut self, last_ts: u64, cur_ts: u64) { + self.transfer_stats_accounting.roll_transfers( + last_ts, + cur_ts, + &mut self.transfer_stats_down_up, + ) + } + + /// Get the latency stats + pub fn latency_stats(&self) -> &LatencyStats { + &self.latency_stats + } + + /// Get the transfer stats + pub fn transfer_stats(&self) -> &TransferStatsDownUp { + &self.transfer_stats_down_up + } + + /// Reset stats when network restarts + pub fn reset(&mut self) { + self.last_tested_ts = None; + self.last_sent_ts = None; + self.last_received_ts = None; + } +} + +#[derive(Clone, Debug, RkyvArchive, RkyvSerialize, RkyvDeserialize)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct RouteSpecDetail { + /// Secret key + #[with(Skip)] + secret_key: DHTKeySecret, + /// Route hops + hops: Vec, + /// Route noderefs + #[with(Skip)] + hop_node_refs: Vec, /// Published private route, do not reuse for ephemeral routes /// Not serialized because all routes should be re-published when restarting #[with(Skip)] published: bool, - // Can optimize the rendering of this route, using node ids only instead of full peer info - #[with(Skip)] - reachable: bool, - /// Timestamp of when the route was created - created_ts: u64, - /// Timestamp of when the route was last checked for validity - last_checked_ts: Option, - /// Timestamp of when the route was last used for anything - last_used_ts: Option, /// Directions this route is guaranteed to work in #[with(RkyvEnumSet)] directions: DirectionSet, /// Stability preference (prefer reliable nodes over faster) - pub stability: Stability, + stability: Stability, /// Sequencing preference (connection oriented protocols vs datagram) - pub sequencing: Sequencing, + sequencing: Sequencing, + /// Stats + stats: RouteStats, } /// The core representation of the RouteSpecStore that can be serialized @@ -80,15 +167,15 @@ pub struct RouteSpecStoreContent { /// What remote private routes have seen #[derive(Debug, Clone, Default)] -struct RemotePrivateRouteInfo { +pub struct RemotePrivateRouteInfo { // The private route itself private_route: Option, - /// Timestamp of when the route was last used for anything - last_used_ts: u64, - /// The time this remote private route last responded - last_replied_ts: Option, /// Did this remote private route see our node info due to no safety route in use seen_our_node_info: bool, + /// Last time this remote private route was requested for any reason (cache expiration) + last_touched_ts: u64, + /// Stats + stats: RouteStats, } /// Ephemeral data used to help the RouteSpecStore operate efficiently @@ -684,18 +771,11 @@ impl RouteSpecStore { secret_key, hops, hop_node_refs, - transfer_stats_down_up: Default::default(), - latency_stats: Default::default(), - latency_stats_accounting: Default::default(), - transfer_stats_accounting: Default::default(), published: false, - reachable: false, - created_ts: cur_ts, - last_checked_ts: None, - last_used_ts: None, directions, stability, sequencing, + stats: RouteStats::new(cur_ts), }; drop(perm_func); @@ -759,46 +839,72 @@ impl RouteSpecStore { pub async fn test_route(&self, key: &DHTKey) -> EyreResult { let rpc_processor = self.unlocked_inner.routing_table.rpc_processor(); - let (target, safety_selection) = { + let dest = { + let private_route = self.assemble_private_route(key, None)?; + let inner = &mut *self.inner.lock(); let rsd = Self::detail(inner, &key).ok_or_else(|| eyre!("route does not exist"))?; + let hop_count = rsd.hops.len(); + let stability = rsd.stability; + let sequencing = rsd.sequencing; // Routes with just one hop can be pinged directly // More than one hop can be pinged across the route with the target being the second to last hop if rsd.hops.len() == 1 { - let target = rsd.hop_node_refs[0].clone(); - let sequencing = rsd.sequencing; - (target, SafetySelection::Unsafe(sequencing)) - } else { - let target = rsd.hop_node_refs[rsd.hops.len() - 2].clone(); - let hop_count = rsd.hops.len(); - let stability = rsd.stability; - let sequencing = rsd.sequencing; let safety_spec = SafetySpec { preferred_route: Some(key.clone()), hop_count, stability, sequencing, }; - (target, SafetySelection::Safe(safety_spec)) + let safety_selection = SafetySelection::Safe(safety_spec); + + Destination::PrivateRoute { + private_route, + safety_selection, + } + } else { + let target = rsd.hop_node_refs[rsd.hops.len() - 2].clone(); + let safety_spec = SafetySpec { + preferred_route: Some(key.clone()), + hop_count, + stability, + sequencing, + }; + let safety_selection = SafetySelection::Safe(safety_spec); + + Destination::Direct { + target, + safety_selection, + } } }; // Test with ping to end - let res = match rpc_processor - .rpc_call_status(Destination::Direct { - target, - safety_selection, - }) - .await? - { + let cur_ts = intf::get_timestamp(); + let res = match rpc_processor.rpc_call_status(dest).await? { NetworkResult::Value(v) => v, _ => { + // // Do route stats for single hop route test because it + // // won't get stats for the route since it's done Direct + // if matches!(safety_selection, SafetySelection::Unsafe(_)) { + // self.with_route_stats(cur_ts, &key, |s| s.record_question_lost()); + // } + // Did not error, but did not come back, just return false return Ok(false); } }; + // // Do route stats for single hop route test because it + // // won't get stats for the route since it's done Direct + // if matches!(safety_selection, SafetySelection::Unsafe(_)) { + // self.with_route_stats(cur_ts, &key, |s| { + // s.record_tested(cur_ts); + // s.record_latency(res.latency); + // }); + // } + Ok(true) } @@ -912,7 +1018,8 @@ impl RouteSpecStore { let pr_hopcount = private_route.hop_count as usize; let max_route_hop_count = self.unlocked_inner.max_route_hop_count; - if pr_hopcount > max_route_hop_count { + // Check private route hop count isn't larger than the max route hop count plus one for the 'first hop' header + if pr_hopcount > (max_route_hop_count + 1) { bail!("private route hop count too long"); } // See if we are using a safety route, if not, short circuit this operation @@ -969,10 +1076,6 @@ impl RouteSpecStore { }; let safety_rsd = Self::detail_mut(inner, &sr_pubkey).unwrap(); - // See if we can optimize this compilation yet - // We don't want to include full nodeinfo if we don't have to - let optimize = safety_rsd.reachable; - // xxx implement caching here! // Create hops @@ -989,6 +1092,12 @@ impl RouteSpecStore { blob_data }; + // We can optimize the peer info in this safety route if it has been successfully + // communicated over either via an outbound test, or used as a private route inbound + // and we are replying over the same route as our safety route outbound + let optimize = safety_rsd.stats.last_tested_ts.is_some() + || safety_rsd.stats.last_received_ts.is_some(); + // Encode each hop from inside to outside // skips the outermost hop since that's entering the // safety route and does not include the dialInfo @@ -1177,7 +1286,7 @@ impl RouteSpecStore { pub fn assemble_private_route( &self, key: &DHTKey, - optimize: Option, + optimized: Option, ) -> EyreResult { let inner = &*self.inner.lock(); let routing_table = self.unlocked_inner.routing_table.clone(); @@ -1187,11 +1296,12 @@ impl RouteSpecStore { // See if we can optimize this compilation yet // We don't want to include full nodeinfo if we don't have to - let optimize = optimize.unwrap_or(rsd.reachable); + let optimized = optimized + .unwrap_or(rsd.stats.last_tested_ts.is_some() || rsd.stats.last_received_ts.is_some()); // Make innermost route hop to our own node let mut route_hop = RouteHop { - node: if optimize { + node: if optimized { RouteNode::NodeId(NodeId::new(routing_table.node_id())) } else { RouteNode::PeerInfo(rti.get_own_peer_info(RoutingDomain::PublicInternet)) @@ -1225,7 +1335,7 @@ impl RouteSpecStore { }; route_hop = RouteHop { - node: if optimize { + node: if optimized { // Optimized, no peer info, just the dht key RouteNode::NodeId(NodeId::new(rsd.hops[h])) } else { @@ -1300,22 +1410,22 @@ impl RouteSpecStore { .remote_private_route_cache .entry(pr_pubkey) .and_modify(|rpr| { - if cur_ts - rpr.last_used_ts >= REMOTE_PRIVATE_ROUTE_CACHE_EXPIRY { + if cur_ts - rpr.last_touched_ts >= REMOTE_PRIVATE_ROUTE_CACHE_EXPIRY { // Start fresh if this had expired - rpr.last_used_ts = cur_ts; - rpr.last_replied_ts = None; rpr.seen_our_node_info = false; + rpr.last_touched_ts = cur_ts; + rpr.stats = RouteStats::new(cur_ts); } else { // If not expired, just mark as being used - rpr.last_used_ts = cur_ts; + rpr.last_touched_ts = cur_ts; } }) .or_insert_with(|| RemotePrivateRouteInfo { // New remote private route cache entry private_route: Some(private_route), - last_used_ts: cur_ts, - last_replied_ts: None, seen_our_node_info: false, + last_touched_ts: cur_ts, + stats: RouteStats::new(cur_ts), }); f(rpr) } @@ -1331,7 +1441,8 @@ impl RouteSpecStore { F: FnOnce(&mut RemotePrivateRouteInfo) -> R, { let rpr = inner.cache.remote_private_route_cache.get_mut(key)?; - if cur_ts - rpr.last_used_ts < REMOTE_PRIVATE_ROUTE_CACHE_EXPIRY { + if cur_ts - rpr.last_touched_ts < REMOTE_PRIVATE_ROUTE_CACHE_EXPIRY { + rpr.last_touched_ts = cur_ts; return Some(f(rpr)); } inner.cache.remote_private_route_cache.remove(key); @@ -1355,6 +1466,12 @@ impl RouteSpecStore { cur_ts: u64, ) -> EyreResult<()> { let inner = &mut *self.inner.lock(); + // Check for local route. If this is not a remote private route + // then we just skip the recording. We may be running a test and using + // our own local route as the destination private route. + if let Some(_) = Self::detail_mut(inner, key) { + return Ok(()); + } if Self::with_get_remote_private_route(inner, cur_ts, key, |rpr| { rpr.seen_our_node_info = true; }) @@ -1365,30 +1482,25 @@ impl RouteSpecStore { Ok(()) } - /// Mark a remote private route as having replied to a question { - pub fn mark_remote_private_route_replied(&self, key: &DHTKey, cur_ts: u64) -> EyreResult<()> { + /// Get the route statistics for any route we know about, local or remote + pub fn with_route_stats(&self, cur_ts: u64, key: &DHTKey, f: F) -> Option + where + F: FnOnce(&mut RouteStats) -> R, + { let inner = &mut *self.inner.lock(); - if Self::with_get_remote_private_route(inner, cur_ts, key, |rpr| { - rpr.last_replied_ts = Some(cur_ts); - }) - .is_none() - { - bail!("private route is missing from store: {}", key); + // Check for local route + if let Some(rsd) = Self::detail_mut(inner, key) { + return Some(f(&mut rsd.stats)); + } + // Check for remote route + if let Some(res) = + Self::with_get_remote_private_route(inner, cur_ts, key, |rpr| f(&mut rpr.stats)) + { + return Some(res); } - Ok(()) - } - /// Mark a remote private route as having beed used { - pub fn mark_remote_private_route_used(&self, key: &DHTKey, cur_ts: u64) -> EyreResult<()> { - let inner = &mut *self.inner.lock(); - if Self::with_get_remote_private_route(inner, cur_ts, key, |rpr| { - rpr.last_used_ts = cur_ts; - }) - .is_none() - { - bail!("private route is missing from store: {}", key); - } - Ok(()) + log_rtab!(debug "route missing for stats: {}", key); + None } /// Clear caches when local our local node info changes @@ -1399,16 +1511,16 @@ impl RouteSpecStore { for (_k, v) in &mut inner.content.details { // Must republish route now v.published = false; - // Route is not known reachable now - v.reachable = false; - // We have yet to check it since local node info changed - v.last_checked_ts = None; + // Restart stats for routes so we test the route again + v.stats.reset(); } // Reset private route cache for (_k, v) in &mut inner.cache.remote_private_route_cache { - v.last_replied_ts = None; + // Our node info has changed v.seen_our_node_info = false; + // Restart stats for routes so we test the route again + v.stats.reset(); } } @@ -1423,78 +1535,17 @@ impl RouteSpecStore { Ok(()) } - /// Mark route as reachable - /// When first deserialized, routes must be re-tested for reachability - /// This can be used to determine if routes need to be sent with full peerinfo or can just use a node id - pub fn mark_route_reachable(&self, key: &DHTKey, reachable: bool) -> EyreResult<()> { - let inner = &mut *self.inner.lock(); - Self::detail_mut(inner, key) - .ok_or_else(|| eyre!("route does not exist"))? - .reachable = reachable; - Ok(()) - } - - /// Mark route as checked - pub fn touch_route_checked(&self, key: &DHTKey, cur_ts: u64) -> EyreResult<()> { - let inner = &mut *self.inner.lock(); - Self::detail_mut(inner, key) - .ok_or_else(|| eyre!("route does not exist"))? - .last_checked_ts = Some(cur_ts); - Ok(()) - } - - /// Mark route as used - pub fn touch_route_used(&self, key: &DHTKey, cur_ts: u64) -> EyreResult<()> { - let inner = &mut *self.inner.lock(); - Self::detail_mut(inner, key) - .ok_or_else(|| eyre!("route does not exist"))? - .last_used_ts = Some(cur_ts); - Ok(()) - } - - /// Record latency on the route - pub fn record_latency(&self, key: &DHTKey, latency: u64) -> EyreResult<()> { - let inner = &mut *self.inner.lock(); - - let rsd = Self::detail_mut(inner, key).ok_or_else(|| eyre!("route does not exist"))?; - rsd.latency_stats = rsd.latency_stats_accounting.record_latency(latency); - Ok(()) - } - - /// Get the calculated latency stats - pub fn latency_stats(&self, key: &DHTKey) -> EyreResult { - let inner = &mut *self.inner.lock(); - Ok(Self::detail_mut(inner, key) - .ok_or_else(|| eyre!("route does not exist"))? - .latency_stats - .clone()) - } - - /// Add download transfers to route - pub fn add_down(&self, key: &DHTKey, bytes: u64) -> EyreResult<()> { - let inner = &mut *self.inner.lock(); - let rsd = Self::detail_mut(inner, key).ok_or_else(|| eyre!("route does not exist"))?; - rsd.transfer_stats_accounting.add_down(bytes); - Ok(()) - } - - /// Add upload transfers to route - pub fn add_up(&self, key: &DHTKey, bytes: u64) -> EyreResult<()> { - let inner = &mut *self.inner.lock(); - let rsd = Self::detail_mut(inner, key).ok_or_else(|| eyre!("route does not exist"))?; - rsd.transfer_stats_accounting.add_up(bytes); - Ok(()) - } - /// Process transfer statistics to get averages pub fn roll_transfers(&self, last_ts: u64, cur_ts: u64) { let inner = &mut *self.inner.lock(); + + // Roll transfers for locally allocated routes for rsd in inner.content.details.values_mut() { - rsd.transfer_stats_accounting.roll_transfers( - last_ts, - cur_ts, - &mut rsd.transfer_stats_down_up, - ); + rsd.stats.roll_transfers(last_ts, cur_ts); + } + // Roll transfers for remote private routes + for (_k, v) in inner.cache.remote_private_route_cache.iter_mut() { + v.stats.roll_transfers(last_ts, cur_ts); } } diff --git a/veilid-core/src/routing_table/tasks.rs b/veilid-core/src/routing_table/tasks.rs index f651d30e..52bf843c 100644 --- a/veilid-core/src/routing_table/tasks.rs +++ b/veilid-core/src/routing_table/tasks.rs @@ -11,25 +11,31 @@ impl RoutingTable { cur_ts: u64, ) -> EyreResult<()> { // log_rtab!("--- rolling_transfers task"); - let mut inner = self.inner.write(); - let inner = &mut *inner; + { + let inner = &mut *self.inner.write(); - // Roll our own node's transfers - inner.self_transfer_stats_accounting.roll_transfers( - last_ts, - cur_ts, - &mut inner.self_transfer_stats, - ); + // Roll our own node's transfers + inner.self_transfer_stats_accounting.roll_transfers( + last_ts, + cur_ts, + &mut inner.self_transfer_stats, + ); - // Roll all bucket entry transfers - let entries: Vec> = inner - .buckets - .iter() - .flat_map(|b| b.entries().map(|(_k, v)| v.clone())) - .collect(); - for v in entries { - v.with_mut(inner, |_rti, e| e.roll_transfers(last_ts, cur_ts)); + // Roll all bucket entry transfers + let entries: Vec> = inner + .buckets + .iter() + .flat_map(|b| b.entries().map(|(_k, v)| v.clone())) + .collect(); + for v in entries { + v.with_mut(inner, |_rti, e| e.roll_transfers(last_ts, cur_ts)); + } } + + // Roll all route transfers + let rss = self.route_spec_store(); + rss.roll_transfers(last_ts, cur_ts); + Ok(()) } diff --git a/veilid-core/src/rpc_processor/mod.rs b/veilid-core/src/rpc_processor/mod.rs index 90986223..866c6ae1 100644 --- a/veilid-core/src/rpc_processor/mod.rs +++ b/veilid-core/src/rpc_processor/mod.rs @@ -55,6 +55,8 @@ struct RPCMessageHeaderDetailDirect { /// Header details for rpc messages received over only a safety route but not a private route #[derive(Debug, Clone)] struct RPCMessageHeaderDetailSafetyRouted { + /// Remote safety route used + remote_safety_route: DHTKey, /// The sequencing used for this route sequencing: Sequencing, } @@ -62,6 +64,8 @@ struct RPCMessageHeaderDetailSafetyRouted { /// Header details for rpc messages received over a private route #[derive(Debug, Clone)] struct RPCMessageHeaderDetailPrivateRouted { + /// Remote safety route used (or possibly node id the case of no safety route) + remote_safety_route: DHTKey, /// The private route we received the rpc over private_route: DHTKey, // The safety spec for replying to this private routed rpc @@ -162,12 +166,14 @@ where #[derive(Debug)] struct WaitableReply { - dest: Destination, handle: OperationWaitHandle, timeout: u64, node_ref: NodeRef, send_ts: u64, send_data_kind: SendDataKind, + safety_route: Option, + remote_private_route: Option, + reply_private_route: Option, } ///////////////////////////////////////////////////////////////////// @@ -184,19 +190,20 @@ impl Answer { } struct RenderedOperation { - message: Vec, // The rendered operation bytes - node_id: DHTKey, // Destination node id we're sending to + message: Vec, // The rendered operation bytes + node_id: DHTKey, // Destination node id we're sending to node_ref: NodeRef, // Node to send envelope to (may not be destination node id in case of relay) hop_count: usize, // Total safety + private route hop count + 1 hop for the initial send safety_route: Option, // The safety route used to send the message - private_route: Option, // The private route used to send the message + remote_private_route: Option, // The private route used to send the message + reply_private_route: Option, // The private route requested to receive the reply } #[derive(Copy, Clone, Debug)] enum RPCKind { Question, Statement, - Answer + Answer, } ///////////////////////////////////////////////////////////////////// @@ -443,42 +450,28 @@ impl RPCProcessor { .await; match &out { Err(_) | Ok(TimeoutOr::Timeout) => { - waitable_reply.node_ref.stats_question_lost(); + self.record_question_lost( + waitable_reply.send_ts, + waitable_reply.node_ref.clone(), + waitable_reply.safety_route, + waitable_reply.remote_private_route, + waitable_reply.reply_private_route, + ); } Ok(TimeoutOr::Value((rpcreader, _))) => { // Reply received let recv_ts = intf::get_timestamp(); - waitable_reply.node_ref.stats_answer_rcvd( + + // Record answer received + self.record_answer_received( waitable_reply.send_ts, recv_ts, rpcreader.header.body_len, - ); - // Process private route replies - if let Destination::PrivateRoute { - private_route, - safety_selection, - } = &waitable_reply.dest - { - let rss = self.routing_table.route_spec_store(); - - // If we received a reply from a private route, mark it as such - if let Err(e) = - rss.mark_remote_private_route_replied(&private_route.public_key, recv_ts) - { - log_rpc!(error "private route missing: {}", e); - } - - // If we sent to a private route without a safety route - // We need to mark our own node info as having been seen so we can optimize sending it - if let SafetySelection::Unsafe(_) = safety_selection { - if let Err(e) = rss.mark_remote_private_route_seen_our_node_info( - &private_route.public_key, - recv_ts, - ) { - log_rpc!(error "private route missing: {}", e); - } - } - } + waitable_reply.node_ref.clone(), + waitable_reply.safety_route, + waitable_reply.remote_private_route, + waitable_reply.reply_private_route, + ) } }; @@ -489,18 +482,19 @@ impl RPCProcessor { fn wrap_with_route( &self, safety_selection: SafetySelection, - private_route: PrivateRoute, + remote_private_route: PrivateRoute, + reply_private_route: Option, message_data: Vec, ) -> Result, RPCError> { let routing_table = self.routing_table(); let rss = routing_table.route_spec_store(); - let pr_is_stub = private_route.is_stub(); - let pr_hop_count = private_route.hop_count; - let pr_pubkey = private_route.public_key; + let pr_is_stub = remote_private_route.is_stub(); + let pr_hop_count = remote_private_route.hop_count; + let pr_pubkey = remote_private_route.public_key; // Compile the safety route with the private route let compiled_route: CompiledRoute = match rss - .compile_safety_route(safety_selection, private_route) + .compile_safety_route(safety_selection, remote_private_route) .map_err(RPCError::internal)? { Some(cr) => cr, @@ -510,6 +504,8 @@ impl RPCProcessor { )) } }; + let sr_is_stub = compiled_route.safety_route.is_stub(); + let sr_pubkey = compiled_route.safety_route.public_key; // Encrypt routed operation // Xmsg + ENC(Xmsg, DH(PKapr, SKbsr)) @@ -552,16 +548,9 @@ impl RPCProcessor { node_id: out_node_id, node_ref: compiled_route.first_hop, hop_count: out_hop_count, - safety_route: if compiled_route.safety_route.is_stub() { - None - } else { - Some(compiled_route.safety_route.public_key) - }, - private_route: if pr_is_stub { - None - } else { - Some(pr_pubkey) - } + safety_route: if sr_is_stub { None } else { Some(sr_pubkey) }, + remote_private_route: if pr_is_stub { None } else { Some(pr_pubkey) }, + reply_private_route, }; Ok(NetworkResult::value(out)) @@ -587,6 +576,15 @@ impl RPCProcessor { builder_to_vec(msg_builder)? }; + // Get reply private route if we are asking for one to be used in our 'respond to' + let reply_private_route = match operation.kind() { + RPCOperationKind::Question(q) => match q.respond_to() { + RespondTo::Sender => None, + RespondTo::PrivateRoute(pr) => Some(pr.public_key), + }, + RPCOperationKind::Statement(_) | RPCOperationKind::Answer(_) => None, + }; + // To where are we sending the request match dest { Destination::Direct { @@ -623,6 +621,9 @@ impl RPCProcessor { node_ref.set_sequencing(sequencing) } + // Reply private route should be None here, even for questions + assert!(reply_private_route.is_none()); + // If no safety route is being used, and we're not sending to a private // route, we can use a direct envelope instead of routing out = NetworkResult::value(RenderedOperation { @@ -631,7 +632,8 @@ impl RPCProcessor { node_ref, hop_count: 1, safety_route: None, - private_route: None, + remote_private_route: None, + reply_private_route: None, }); } SafetySelection::Safe(_) => { @@ -650,7 +652,12 @@ impl RPCProcessor { PrivateRoute::new_stub(node_id, RouteNode::PeerInfo(peer_info)); // Wrap with safety route - out = self.wrap_with_route(safety_selection, private_route, message)?; + out = self.wrap_with_route( + safety_selection, + private_route, + reply_private_route, + message, + )?; } }; } @@ -661,7 +668,12 @@ impl RPCProcessor { // Send to private route // --------------------- // Reply with 'route' operation - out = self.wrap_with_route(safety_selection, private_route, message)?; + out = self.wrap_with_route( + safety_selection, + private_route, + reply_private_route, + message, + )?; } } @@ -729,13 +741,237 @@ impl RPCProcessor { } /// Record failure to send to node or route - fn record_send_failure(&self, rpc_kind: RPCKind, send_ts: u64, node_ref: NodeRef, safety_route: Option, private_route: Option) { - xxx implement me + fn record_send_failure( + &self, + rpc_kind: RPCKind, + send_ts: u64, + node_ref: NodeRef, + safety_route: Option, + remote_private_route: Option, + ) { + let wants_answer = matches!(rpc_kind, RPCKind::Question); + + // Record for node if this was not sent via a route + if safety_route.is_none() && remote_private_route.is_none() { + node_ref.stats_failed_to_send(send_ts, wants_answer); + return; + } + + // If safety route was in use, record failure to send there + if let Some(sr_pubkey) = &safety_route { + let rss = self.routing_table.route_spec_store(); + rss.with_route_stats(send_ts, sr_pubkey, |s| s.record_send_failed()); + } else { + // If no safety route was in use, then it's the private route's fault if we have one + if let Some(pr_pubkey) = &remote_private_route { + let rss = self.routing_table.route_spec_store(); + rss.with_route_stats(send_ts, pr_pubkey, |s| s.record_send_failed()); + } + } + } + + /// Record question lost to node or route + fn record_question_lost( + &self, + send_ts: u64, + node_ref: NodeRef, + safety_route: Option, + remote_private_route: Option, + private_route: Option, + ) { + // Record for node if this was not sent via a route + if safety_route.is_none() && remote_private_route.is_none() { + node_ref.stats_question_lost(); + return; + } + // Get route spec store + let rss = self.routing_table.route_spec_store(); + + // If safety route was used, record question lost there + if let Some(sr_pubkey) = &safety_route { + let rss = self.routing_table.route_spec_store(); + rss.with_route_stats(send_ts, sr_pubkey, |s| { + s.record_question_lost(); + }); + } + // If remote private route was used, record question lost there + if let Some(rpr_pubkey) = &remote_private_route { + rss.with_route_stats(send_ts, rpr_pubkey, |s| { + s.record_question_lost(); + }); + } + // If private route was used, record question lost there + if let Some(pr_pubkey) = &private_route { + rss.with_route_stats(send_ts, pr_pubkey, |s| { + s.record_question_lost(); + }); + } } /// Record success sending to node or route - fn record_send_success(&self, rpc_kind: RPCKind, send_ts: u64, bytes: u64, node_ref: NodeRef, safety_route: Option, private_route: Option) { - xxx implement me + fn record_send_success( + &self, + rpc_kind: RPCKind, + send_ts: u64, + bytes: u64, + node_ref: NodeRef, + safety_route: Option, + remote_private_route: Option, + ) { + let wants_answer = matches!(rpc_kind, RPCKind::Question); + + // Record for node if this was not sent via a route + if safety_route.is_none() && remote_private_route.is_none() { + node_ref.stats_question_sent(send_ts, bytes, wants_answer); + return; + } + + // Get route spec store + let rss = self.routing_table.route_spec_store(); + + // If safety route was used, record send there + if let Some(sr_pubkey) = &safety_route { + rss.with_route_stats(send_ts, sr_pubkey, |s| { + s.record_sent(send_ts, bytes); + }); + } + + // If remote private route was used, record send there + if let Some(pr_pubkey) = &remote_private_route { + let rss = self.routing_table.route_spec_store(); + rss.with_route_stats(send_ts, pr_pubkey, |s| { + s.record_sent(send_ts, bytes); + }); + } + } + + /// Record answer received from node or route + fn record_answer_received( + &self, + send_ts: u64, + recv_ts: u64, + bytes: u64, + node_ref: NodeRef, + safety_route: Option, + remote_private_route: Option, + reply_private_route: Option, + ) { + // Record stats for remote node if this was direct + if safety_route.is_none() && remote_private_route.is_none() && reply_private_route.is_none() + { + node_ref.stats_answer_rcvd(send_ts, recv_ts, bytes); + return; + } + // Get route spec store + let rss = self.routing_table.route_spec_store(); + + // Get latency for all local routes + let mut total_local_latency = 0u64; + let total_latency = recv_ts.saturating_sub(send_ts); + + // If safety route was used, record route there + if let Some(sr_pubkey) = &safety_route { + rss.with_route_stats(send_ts, sr_pubkey, |s| { + // If we received an answer, the safety route we sent over can be considered tested + s.record_tested(recv_ts); + + // If we used a safety route to send, use our last tested latency + total_local_latency += s.latency_stats().average + }); + } + + // If local private route was used, record route there + if let Some(pr_pubkey) = &reply_private_route { + rss.with_route_stats(send_ts, pr_pubkey, |s| { + // Record received bytes + s.record_received(recv_ts, bytes); + + // If we used a private route to receive, use our last tested latency + total_local_latency += s.latency_stats().average + }); + } + + // If remote private route was used, record there + if let Some(rpr_pubkey) = &remote_private_route { + rss.with_route_stats(send_ts, rpr_pubkey, |s| { + // Record received bytes + s.record_received(recv_ts, bytes); + + // The remote route latency is recorded using the total latency minus the total local latency + let remote_latency = total_latency.saturating_sub(total_local_latency); + s.record_latency(remote_latency); + }); + + // If we sent to a private route without a safety route + // We need to mark our own node info as having been seen so we can optimize sending it + if let Err(e) = rss.mark_remote_private_route_seen_our_node_info(&rpr_pubkey, recv_ts) { + log_rpc!(error "private route missing: {}", e); + } + + // We can't record local route latency if a remote private route was used because + // there is no way other than the prior latency estimation to determine how much time was spent + // in the remote private route + // Instead, we rely on local route testing to give us latency numbers for our local routes + } else { + // If no remote private route was used, then record half the total latency on our local routes + // This is fine because if we sent with a local safety route, + // then we must have received with a local private route too, per the design rules + if let Some(sr_pubkey) = &safety_route { + let rss = self.routing_table.route_spec_store(); + rss.with_route_stats(send_ts, sr_pubkey, |s| { + s.record_latency(total_latency / 2); + }); + } + if let Some(pr_pubkey) = &reply_private_route { + rss.with_route_stats(send_ts, pr_pubkey, |s| { + s.record_latency(total_latency / 2); + }); + } + } + } + + /// Record question or statement received from node or route + fn record_question_received(&self, msg: &RPCMessage) { + let recv_ts = msg.header.timestamp; + let bytes = msg.header.body_len; + + // Process messages based on how they were received + match &msg.header.detail { + // Process direct messages + RPCMessageHeaderDetail::Direct(_) => { + if let Some(sender_nr) = msg.opt_sender_nr.clone() { + sender_nr.stats_question_rcvd(recv_ts, bytes); + return; + } + } + // Process messages that arrived with no private route (private route stub) + RPCMessageHeaderDetail::SafetyRouted(d) => { + let rss = self.routing_table.route_spec_store(); + + // This may record nothing if the remote safety route is not also + // a remote private route that been imported, but that's okay + rss.with_route_stats(recv_ts, &d.remote_safety_route, |s| { + s.record_received(recv_ts, bytes); + }); + } + // Process messages that arrived to our private route + RPCMessageHeaderDetail::PrivateRouted(d) => { + let rss = self.routing_table.route_spec_store(); + + // This may record nothing if the remote safety route is not also + // a remote private route that been imported, but that's okay + // it could also be a node id if no remote safety route was used + // in which case this also will do nothing + rss.with_route_stats(recv_ts, &d.remote_safety_route, |s| { + s.record_received(recv_ts, bytes); + }); + + // Record for our local private route we received over + rss.with_route_stats(recv_ts, &d.private_route, |s| { + s.record_received(recv_ts, bytes); + }); + } + } } /// Issue a question over the network, possibly using an anonymized route @@ -762,7 +998,8 @@ impl RPCProcessor { node_ref, hop_count, safety_route, - private_route, + remote_private_route, + reply_private_route, } = network_result_try!(self.render_operation(dest.clone(), &operation)?); // Calculate answer timeout @@ -781,46 +1018,34 @@ impl RPCProcessor { .await .map_err(|e| { // If we're returning an error, clean up - node_ref - .stats_failed_to_send(send_ts, true); + self.record_send_failure(RPCKind::Question, send_ts, node_ref.clone(), safety_route, remote_private_route); RPCError::network(e) })? => { // If we couldn't send we're still cleaning up - node_ref - .stats_failed_to_send(send_ts, true); + self.record_send_failure(RPCKind::Question, send_ts, node_ref.clone(), safety_route, remote_private_route); } ); // Successfully sent - node_ref.stats_question_sent(send_ts, bytes, true); - - // Private route stats - if let Destination::PrivateRoute { - private_route, - safety_selection: _, - } = &dest - { - let rss = self.routing_table.route_spec_store(); - if let Err(e) = - rss.mark_remote_private_route_used(&private_route.public_key, intf::get_timestamp()) - { - log_rpc!(error "private route missing: {}", e); - } - } - - // Safety route stats - if let Some(sr_pubkey) = safety_route { - // - } + self.record_send_success( + RPCKind::Question, + send_ts, + bytes, + node_ref.clone(), + safety_route, + remote_private_route, + ); // Pass back waitable reply completion Ok(NetworkResult::value(WaitableReply { - dest, handle, timeout, node_ref, send_ts, send_data_kind, + safety_route, + remote_private_route, + reply_private_route, })) } @@ -847,7 +1072,8 @@ impl RPCProcessor { node_ref, hop_count: _, safety_route, - private_route, + remote_private_route, + reply_private_route: _, } = network_result_try!(self.render_operation(dest, &operation)?); // Send statement @@ -859,23 +1085,23 @@ impl RPCProcessor { .await .map_err(|e| { // If we're returning an error, clean up - node_ref - .stats_failed_to_send(send_ts, false); + self.record_send_failure(RPCKind::Statement, send_ts, node_ref.clone(), safety_route, remote_private_route); RPCError::network(e) })? => { // If we couldn't send we're still cleaning up - node_ref - .stats_failed_to_send(send_ts, false); + self.record_send_failure(RPCKind::Statement, send_ts, node_ref.clone(), safety_route, remote_private_route); } ); // Successfully sent - node_ref.stats_question_sent(send_ts, bytes, false); - - // Private route stats - xxx - // Safety route stats - safety_route + self.record_send_success( + RPCKind::Statement, + send_ts, + bytes, + node_ref, + safety_route, + remote_private_route, + ); Ok(NetworkResult::value(())) } @@ -907,7 +1133,8 @@ impl RPCProcessor { node_ref, hop_count: _, safety_route, - private_route, + remote_private_route, + reply_private_route: _, } = network_result_try!(self.render_operation(dest, &operation)?); // Send the reply @@ -918,23 +1145,23 @@ impl RPCProcessor { .await .map_err(|e| { // If we're returning an error, clean up - node_ref - .stats_failed_to_send(send_ts, false); + self.record_send_failure(RPCKind::Answer, send_ts, node_ref.clone(), safety_route, remote_private_route); RPCError::network(e) })? => { // If we couldn't send we're still cleaning up - node_ref - .stats_failed_to_send(send_ts, false); + self.record_send_failure(RPCKind::Answer, send_ts, node_ref.clone(), safety_route, remote_private_route); } ); // Reply successfully sent - node_ref.stats_answer_sent(bytes); - - // Private route stats - xxxx - // Safety route stats - xxx + self.record_send_success( + RPCKind::Answer, + send_ts, + bytes, + node_ref, + safety_route, + remote_private_route, + ); Ok(NetworkResult::value(())) } @@ -1019,9 +1246,11 @@ impl RPCProcessor { } }; - // Process stats + // Process stats for questions/statements received let kind = match msg.operation.kind() { RPCOperationKind::Question(_) => { + self.record_question_received(&msg); + if let Some(sender_nr) = msg.opt_sender_nr.clone() { sender_nr.stats_question_rcvd(msg.header.timestamp, msg.header.body_len); } @@ -1147,12 +1376,14 @@ impl RPCProcessor { #[instrument(level = "trace", skip(self, body), err)] pub fn enqueue_safety_routed_message( &self, + remote_safety_route: DHTKey, sequencing: Sequencing, body: Vec, ) -> EyreResult<()> { let msg = RPCMessageEncoded { header: RPCMessageHeader { detail: RPCMessageHeaderDetail::SafetyRouted(RPCMessageHeaderDetailSafetyRouted { + remote_safety_route, sequencing, }), timestamp: intf::get_timestamp(), @@ -1174,6 +1405,7 @@ impl RPCProcessor { #[instrument(level = "trace", skip(self, body), err)] pub fn enqueue_private_routed_message( &self, + remote_safety_route: DHTKey, private_route: DHTKey, safety_spec: SafetySpec, body: Vec, @@ -1182,6 +1414,7 @@ impl RPCProcessor { header: RPCMessageHeader { detail: RPCMessageHeaderDetail::PrivateRouted( RPCMessageHeaderDetailPrivateRouted { + remote_safety_route, private_route, safety_spec, }, diff --git a/veilid-core/src/rpc_processor/rpc_route.rs b/veilid-core/src/rpc_processor/rpc_route.rs index 4abdd681..5b82d2ae 100644 --- a/veilid-core/src/rpc_processor/rpc_route.rs +++ b/veilid-core/src/rpc_processor/rpc_route.rs @@ -135,7 +135,7 @@ impl RPCProcessor { &self, detail: RPCMessageHeaderDetailDirect, routed_operation: RoutedOperation, - safety_route: &SafetyRoute, + remote_safety_route: &SafetyRoute, ) -> Result, RPCError> { // Get sequencing preference let sequencing = if detail @@ -153,7 +153,7 @@ impl RPCProcessor { let node_id_secret = self.routing_table.node_id_secret(); let dh_secret = self .crypto - .cached_dh(&safety_route.public_key, &node_id_secret) + .cached_dh(&remote_safety_route.public_key, &node_id_secret) .map_err(RPCError::protocol)?; let body = match Crypto::decrypt_aead( &routed_operation.data, @@ -168,7 +168,7 @@ impl RPCProcessor { }; // Pass message to RPC system - self.enqueue_safety_routed_message(sequencing, body) + self.enqueue_safety_routed_message(remote_safety_route.public_key, sequencing, body) .map_err(RPCError::internal)?; Ok(NetworkResult::value(())) @@ -180,7 +180,7 @@ impl RPCProcessor { &self, detail: RPCMessageHeaderDetailDirect, routed_operation: RoutedOperation, - safety_route: &SafetyRoute, + remote_safety_route: &SafetyRoute, private_route: &PrivateRoute, ) -> Result, RPCError> { // Get sender id @@ -204,7 +204,7 @@ impl RPCProcessor { // xxx: punish nodes that send messages that fail to decrypt eventually. How to do this for private routes? let dh_secret = self .crypto - .cached_dh(&safety_route.public_key, &secret_key) + .cached_dh(&remote_safety_route.public_key, &secret_key) .map_err(RPCError::protocol)?; let body = Crypto::decrypt_aead( &routed_operation.data, @@ -217,7 +217,7 @@ impl RPCProcessor { ))?; // Pass message to RPC system - self.enqueue_private_routed_message(private_route.public_key, safety_spec, body) + self.enqueue_private_routed_message(remote_safety_route.public_key, private_route.public_key, safety_spec, body) .map_err(RPCError::internal)?; Ok(NetworkResult::value(())) diff --git a/veilid-core/src/veilid_api/debug.rs b/veilid-core/src/veilid_api/debug.rs index fa5ff34b..ebade9d2 100644 --- a/veilid-core/src/veilid_api/debug.rs +++ b/veilid-core/src/veilid_api/debug.rs @@ -845,46 +845,52 @@ impl VeilidAPI { } pub async fn debug(&self, args: String) -> Result { - let args = args.trim_start(); - if args.is_empty() { - // No arguments runs help command - return self.debug_help("".to_owned()).await; - } - let (arg, rest) = args.split_once(' ').unwrap_or((args, "")); - let rest = rest.trim_start().to_owned(); + let res = { + let args = args.trim_start(); + if args.is_empty() { + // No arguments runs help command + return self.debug_help("".to_owned()).await; + } + let (arg, rest) = args.split_once(' ').unwrap_or((args, "")); + let rest = rest.trim_start().to_owned(); - if arg == "help" { - self.debug_help(rest).await - } else if arg == "buckets" { - self.debug_buckets(rest).await - } else if arg == "dialinfo" { - self.debug_dialinfo(rest).await - } else if arg == "txtrecord" { - self.debug_txtrecord(rest).await - } else if arg == "entries" { - self.debug_entries(rest).await - } else if arg == "entry" { - self.debug_entry(rest).await - } else if arg == "ping" { - self.debug_ping(rest).await - } else if arg == "contact" { - self.debug_contact(rest).await - } else if arg == "nodeinfo" { - self.debug_nodeinfo(rest).await - } else if arg == "purge" { - self.debug_purge(rest).await - } else if arg == "attach" { - self.debug_attach(rest).await - } else if arg == "detach" { - self.debug_detach(rest).await - } else if arg == "config" { - self.debug_config(rest).await - } else if arg == "restart" { - self.debug_restart(rest).await - } else if arg == "route" { - self.debug_route(rest).await - } else { - Ok(">>> Unknown command\n".to_owned()) + if arg == "help" { + self.debug_help(rest).await + } else if arg == "buckets" { + self.debug_buckets(rest).await + } else if arg == "dialinfo" { + self.debug_dialinfo(rest).await + } else if arg == "txtrecord" { + self.debug_txtrecord(rest).await + } else if arg == "entries" { + self.debug_entries(rest).await + } else if arg == "entry" { + self.debug_entry(rest).await + } else if arg == "ping" { + self.debug_ping(rest).await + } else if arg == "contact" { + self.debug_contact(rest).await + } else if arg == "nodeinfo" { + self.debug_nodeinfo(rest).await + } else if arg == "purge" { + self.debug_purge(rest).await + } else if arg == "attach" { + self.debug_attach(rest).await + } else if arg == "detach" { + self.debug_detach(rest).await + } else if arg == "config" { + self.debug_config(rest).await + } else if arg == "restart" { + self.debug_restart(rest).await + } else if arg == "route" { + self.debug_route(rest).await + } else { + Ok(">>> Unknown command\n".to_owned()) + } + }; + if let Ok(res) = &res { + debug!("{}", res); } + res } } diff --git a/veilid-core/src/veilid_api/mod.rs b/veilid-core/src/veilid_api/mod.rs index 605fcf90..b56b0277 100644 --- a/veilid-core/src/veilid_api/mod.rs +++ b/veilid-core/src/veilid_api/mod.rs @@ -2492,7 +2492,6 @@ pub enum SignalInfo { peer_info: PeerInfo, // Sender's peer info }, // XXX: WebRTC - // XXX: App-level signalling } ///////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/veilid-core/src/veilid_api/serialize_helpers.rs b/veilid-core/src/veilid_api/serialize_helpers.rs index 680e3a48..91a3b283 100644 --- a/veilid-core/src/veilid_api/serialize_helpers.rs +++ b/veilid-core/src/veilid_api/serialize_helpers.rs @@ -5,7 +5,7 @@ use rkyv::Archive as RkyvArchive; use rkyv::Deserialize as RkyvDeserialize; use rkyv::Serialize as RkyvSerialize; -// XXX: Don't trace these functions as they are used in the transfer of API logs, which will recurse! +// Don't trace these functions as they are used in the transfer of API logs, which will recurse! // #[instrument(level = "trace", ret, err)] pub fn deserialize_json<'a, T: de::Deserialize<'a> + Debug>( From 4d573a966fe3278c40bc732ea2af5c73fab274d3 Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 24 Nov 2022 16:23:33 -0500 Subject: [PATCH 03/88] private route loopbacks --- .../native/network_class_discovery.rs | 9 - .../src/routing_table/route_spec_store.rs | 62 ++--- veilid-core/src/rpc_processor/destination.rs | 29 ++- veilid-core/src/rpc_processor/rpc_route.rs | 213 ++++++++++-------- 4 files changed, 175 insertions(+), 138 deletions(-) diff --git a/veilid-core/src/network_manager/native/network_class_discovery.rs b/veilid-core/src/network_manager/native/network_class_discovery.rs index 8c24687d..89c61755 100644 --- a/veilid-core/src/network_manager/native/network_class_discovery.rs +++ b/veilid-core/src/network_manager/native/network_class_discovery.rs @@ -434,15 +434,6 @@ impl DiscoveryContext { return Ok(true); } - // XXX: is this necessary? - // Redo our external_1 dial info detection because a failed port mapping attempt - // may cause it to become invalid - // Get our external address from some fast node, call it node 1 - // if !self.protocol_get_external_address_1().await { - // // If we couldn't get an external address, then we should just try the whole network class detection again later - // return Ok(false); - // } - // Get the external dial info for our use here let (node_1, external_1_dial_info, external_1_address, protocol_type, address_type) = { let inner = self.inner.lock(); diff --git a/veilid-core/src/routing_table/route_spec_store.rs b/veilid-core/src/routing_table/route_spec_store.rs index 3706681b..b1ebab55 100644 --- a/veilid-core/src/routing_table/route_spec_store.rs +++ b/veilid-core/src/routing_table/route_spec_store.rs @@ -864,7 +864,20 @@ impl RouteSpecStore { safety_selection, } } else { - let target = rsd.hop_node_refs[rsd.hops.len() - 2].clone(); + // let target = rsd.hop_node_refs[rsd.hops.len() - 2].clone(); + // let safety_spec = SafetySpec { + // preferred_route: Some(key.clone()), + // hop_count, + // stability, + // sequencing, + // }; + // let safety_selection = SafetySelection::Safe(safety_spec); + + // Destination::Direct { + // target, + // safety_selection, + // } + let safety_spec = SafetySpec { preferred_route: Some(key.clone()), hop_count, @@ -873,38 +886,22 @@ impl RouteSpecStore { }; let safety_selection = SafetySelection::Safe(safety_spec); - Destination::Direct { - target, + Destination::PrivateRoute { + private_route, safety_selection, } } }; - // Test with ping to end - let cur_ts = intf::get_timestamp(); - let res = match rpc_processor.rpc_call_status(dest).await? { + // Test with double-round trip ping to self + let _res = match rpc_processor.rpc_call_status(dest).await? { NetworkResult::Value(v) => v, _ => { - // // Do route stats for single hop route test because it - // // won't get stats for the route since it's done Direct - // if matches!(safety_selection, SafetySelection::Unsafe(_)) { - // self.with_route_stats(cur_ts, &key, |s| s.record_question_lost()); - // } - // Did not error, but did not come back, just return false return Ok(false); } }; - // // Do route stats for single hop route test because it - // // won't get stats for the route since it's done Direct - // if matches!(safety_selection, SafetySelection::Unsafe(_)) { - // self.with_route_stats(cur_ts, &key, |s| { - // s.record_tested(cur_ts); - // s.record_latency(res.latency); - // }); - // } - Ok(true) } @@ -1065,14 +1062,21 @@ impl RouteSpecStore { bail!("compiled private route should have first hop"); }; - // Get the safety route to use from the spec - let avoid_node_id = match &pr_first_hop.node { - RouteNode::NodeId(n) => n.key, - RouteNode::PeerInfo(p) => p.node_id.key, - }; - let Some(sr_pubkey) = self.get_route_for_safety_spec_inner(inner, rti, &safety_spec, Direction::Outbound.into(), &[avoid_node_id])? else { - // No safety route could be found for this spec - return Ok(None); + // If the safety route requested is also the private route, this is a loopback test, just accept it + let sr_pubkey = if safety_spec.preferred_route == Some(private_route.public_key) { + // Private route is also safety route during loopback test + private_route.public_key + } else { + // Get the safety route to use from the spec + let avoid_node_id = match &pr_first_hop.node { + RouteNode::NodeId(n) => n.key, + RouteNode::PeerInfo(p) => p.node_id.key, + }; + let Some(sr_pubkey) = self.get_route_for_safety_spec_inner(inner, rti, &safety_spec, Direction::Outbound.into(), &[avoid_node_id])? else { + // No safety route could be found for this spec + return Ok(None); + }; + sr_pubkey }; let safety_rsd = Self::detail_mut(inner, &sr_pubkey).unwrap(); diff --git a/veilid-core/src/rpc_processor/destination.rs b/veilid-core/src/rpc_processor/destination.rs index d7a10d0b..dd63776a 100644 --- a/veilid-core/src/rpc_processor/destination.rs +++ b/veilid-core/src/rpc_processor/destination.rs @@ -228,18 +228,29 @@ impl RPCProcessor { ))) } SafetySelection::Safe(safety_spec) => { - // Sent directly but with a safety route, respond to private route - let avoid_node_id = match &pr_first_hop.node { - RouteNode::NodeId(n) => n.key, - RouteNode::PeerInfo(p) => p.node_id.key, - }; + // Sent to a private route via a safety route, respond to private route - let Some(pr_key) = rss - .get_private_route_for_safety_spec(safety_spec, &[avoid_node_id]) - .map_err(RPCError::internal)? else { - return Ok(NetworkResult::no_connection_other("no private route for response at this time")); + // Check for loopback test + let pr_key = if safety_spec.preferred_route + == Some(private_route.public_key) + { + // Private route is also safety route during loopback test + private_route.public_key + } else { + // Get the privat route to respond to that matches the safety route spec we sent the request with + let avoid_node_id = match &pr_first_hop.node { + RouteNode::NodeId(n) => n.key, + RouteNode::PeerInfo(p) => p.node_id.key, }; + let Some(pr_key) = rss + .get_private_route_for_safety_spec(safety_spec, &[avoid_node_id]) + .map_err(RPCError::internal)? else { + return Ok(NetworkResult::no_connection_other("no private route for response at this time")); + }; + pr_key + }; + // Get the assembled route for response let private_route = rss .assemble_private_route(&pr_key, None) diff --git a/veilid-core/src/rpc_processor/rpc_route.rs b/veilid-core/src/rpc_processor/rpc_route.rs index 5b82d2ae..1602858a 100644 --- a/veilid-core/src/rpc_processor/rpc_route.rs +++ b/veilid-core/src/rpc_processor/rpc_route.rs @@ -4,16 +4,17 @@ impl RPCProcessor { #[instrument(level = "trace", skip_all, err)] async fn process_route_safety_route_hop( &self, - route: RPCOperationRoute, + routed_operation: RoutedOperation, route_hop: RouteHop, + safety_route: SafetyRoute, ) -> Result, RPCError> { // Make sure hop count makes sense - if route.safety_route.hop_count as usize > self.unlocked_inner.max_route_hop_count { + if safety_route.hop_count as usize > self.unlocked_inner.max_route_hop_count { return Ok(NetworkResult::invalid_message( "Safety route hop count too high to process", )); } - if route.safety_route.hop_count == 0 { + if safety_route.hop_count == 0 { return Ok(NetworkResult::invalid_message( "Safety route hop count should not be zero if there are more hops", )); @@ -55,11 +56,11 @@ impl RPCProcessor { // Pass along the route let next_hop_route = RPCOperationRoute { safety_route: SafetyRoute { - public_key: route.safety_route.public_key, - hop_count: route.safety_route.hop_count - 1, + public_key: safety_route.public_key, + hop_count: safety_route.hop_count - 1, hops: SafetyRouteHops::Data(route_hop.next_hop.unwrap()), }, - operation: route.operation, + operation: routed_operation, }; let next_hop_route_stmt = RPCStatement::new(RPCStatementDetail::Route(next_hop_route)); @@ -135,7 +136,7 @@ impl RPCProcessor { &self, detail: RPCMessageHeaderDetailDirect, routed_operation: RoutedOperation, - remote_safety_route: &SafetyRoute, + remote_sr_pubkey: DHTKey, ) -> Result, RPCError> { // Get sequencing preference let sequencing = if detail @@ -153,7 +154,7 @@ impl RPCProcessor { let node_id_secret = self.routing_table.node_id_secret(); let dh_secret = self .crypto - .cached_dh(&remote_safety_route.public_key, &node_id_secret) + .cached_dh(&remote_sr_pubkey, &node_id_secret) .map_err(RPCError::protocol)?; let body = match Crypto::decrypt_aead( &routed_operation.data, @@ -168,7 +169,7 @@ impl RPCProcessor { }; // Pass message to RPC system - self.enqueue_safety_routed_message(remote_safety_route.public_key, sequencing, body) + self.enqueue_safety_routed_message(remote_sr_pubkey, sequencing, body) .map_err(RPCError::internal)?; Ok(NetworkResult::value(())) @@ -180,8 +181,8 @@ impl RPCProcessor { &self, detail: RPCMessageHeaderDetailDirect, routed_operation: RoutedOperation, - remote_safety_route: &SafetyRoute, - private_route: &PrivateRoute, + remote_sr_pubkey: DHTKey, + pr_pubkey: DHTKey, ) -> Result, RPCError> { // Get sender id let sender_id = detail.envelope.get_sender_id(); @@ -190,7 +191,7 @@ impl RPCProcessor { let rss = self.routing_table.route_spec_store(); let Some((secret_key, safety_spec)) = rss .validate_signatures( - &private_route.public_key, + &pr_pubkey, &routed_operation.signatures, &routed_operation.data, sender_id, @@ -204,7 +205,7 @@ impl RPCProcessor { // xxx: punish nodes that send messages that fail to decrypt eventually. How to do this for private routes? let dh_secret = self .crypto - .cached_dh(&remote_safety_route.public_key, &secret_key) + .cached_dh(&remote_sr_pubkey, &secret_key) .map_err(RPCError::protocol)?; let body = Crypto::decrypt_aead( &routed_operation.data, @@ -217,7 +218,7 @@ impl RPCProcessor { ))?; // Pass message to RPC system - self.enqueue_private_routed_message(remote_safety_route.public_key, private_route.public_key, safety_spec, body) + self.enqueue_private_routed_message(remote_sr_pubkey, pr_pubkey, safety_spec, body) .map_err(RPCError::internal)?; Ok(NetworkResult::value(())) @@ -228,65 +229,123 @@ impl RPCProcessor { &self, detail: RPCMessageHeaderDetailDirect, routed_operation: RoutedOperation, - safety_route: &SafetyRoute, - private_route: &PrivateRoute, + remote_sr_pubkey: DHTKey, + pr_pubkey: DHTKey, ) -> Result, RPCError> { - // Make sure hop count makes sense - if safety_route.hop_count != 0 { - return Ok(NetworkResult::invalid_message( - "Safety hop count should be zero if switched to private route", - )); - } - if private_route.hop_count != 0 { - return Ok(NetworkResult::invalid_message( - "Private route hop count should be zero if we are at the end", - )); - } - + // If the private route public key is our node id, then this was sent via safety route to our node directly // so there will be no signatures to validate - if private_route.public_key == self.routing_table.node_id() { + if pr_pubkey == self.routing_table.node_id() { // The private route was a stub - self.process_safety_routed_operation(detail, routed_operation, safety_route) + self.process_safety_routed_operation(detail, routed_operation, remote_sr_pubkey) } else { // Both safety and private routes used, should reply with a safety route self.process_private_routed_operation( detail, routed_operation, - safety_route, - private_route, + remote_sr_pubkey, + pr_pubkey, ) } } #[instrument(level = "trace", skip_all, err)] pub(crate) async fn process_private_route_first_hop( &self, - operation: RoutedOperation, + mut routed_operation: RoutedOperation, sr_pubkey: DHTKey, - private_route: &PrivateRoute, + mut private_route: PrivateRoute, ) -> Result, RPCError> { - let PrivateRouteHops::FirstHop(pr_first_hop) = &private_route.hops else { + let Some(pr_first_hop) = private_route.pop_first_hop() else { return Ok(NetworkResult::invalid_message("switching from safety route to private route requires first hop")); }; + // Check for loopback test where private route is the same as safety route + if sr_pubkey == private_route.public_key { + // If so, we're going to turn this thing right around without transiting the network + let PrivateRouteHops::Data(route_hop_data) = private_route.hops else { + return Ok(NetworkResult::invalid_message("Loopback test requires hops")); + }; + + // Decrypt route hop data + let route_hop = network_result_try!(self.decrypt_private_route_hop_data(&route_hop_data, &private_route.public_key, &mut routed_operation)?); + + // Ensure hop count > 0 + if private_route.hop_count == 0 { + return Ok(NetworkResult::invalid_message( + "route should not be at the end", + )); + } + + // Make next PrivateRoute and pass it on + return self.process_route_private_route_hop( + routed_operation, + route_hop.node, + sr_pubkey, + PrivateRoute { + public_key: private_route.public_key, + hop_count: private_route.hop_count - 1, + hops: route_hop + .next_hop + .map(|rhd| PrivateRouteHops::Data(rhd)) + .unwrap_or(PrivateRouteHops::Empty), + }, + ) + .await; + } + // Switching to private route from safety route self.process_route_private_route_hop( - operation, - pr_first_hop.node.clone(), + routed_operation, + pr_first_hop, sr_pubkey, - PrivateRoute { - public_key: private_route.public_key, - hop_count: private_route.hop_count - 1, - hops: pr_first_hop - .next_hop - .clone() - .map(|rhd| PrivateRouteHops::Data(rhd)) - .unwrap_or(PrivateRouteHops::Empty), - }, + private_route, ) .await } + /// Decrypt route hop data and sign routed operation + pub(crate) fn decrypt_private_route_hop_data(&self, route_hop_data: &RouteHopData, pr_pubkey: &DHTKey, route_operation: &mut RoutedOperation) -> Result, RPCError> + { + // Decrypt the blob with DEC(nonce, DH(the PR's public key, this hop's secret) + let node_id_secret = self.routing_table.node_id_secret(); + let dh_secret = self + .crypto + .cached_dh(&pr_pubkey, &node_id_secret) + .map_err(RPCError::protocol)?; + let dec_blob_data = match Crypto::decrypt_aead( + &route_hop_data.blob, + &route_hop_data.nonce, + &dh_secret, + None, + ) { + Ok(v) => v, + Err(e) => { + return Ok(NetworkResult::invalid_message(format!("unable to decrypt private route hop data: {}", e))); + } + }; + let dec_blob_reader = RPCMessageData::new(dec_blob_data).get_reader()?; + + // Decode next RouteHop + let route_hop = { + let rh_reader = dec_blob_reader + .get_root::() + .map_err(RPCError::protocol)?; + decode_route_hop(&rh_reader)? + }; + + // Sign the operation if this is not our last hop + // as the last hop is already signed by the envelope + if route_hop.next_hop.is_some() { + let node_id = self.routing_table.node_id(); + let node_id_secret = self.routing_table.node_id_secret(); + let sig = sign(&node_id, &node_id_secret, &route_operation.data) + .map_err(RPCError::internal)?; + route_operation.signatures.push(sig); + } + + Ok(NetworkResult::value(route_hop)) + } + #[instrument(level = "trace", skip(self, msg), ret, err)] pub(crate) async fn process_route( &self, @@ -322,14 +381,14 @@ impl RPCProcessor { // See what kind of safety route we have going on here match route.safety_route.hops { // There is a safety route hop - SafetyRouteHops::Data(ref d) => { + SafetyRouteHops::Data(ref route_hop_data) => { // Decrypt the blob with DEC(nonce, DH(the SR's public key, this hop's secret) let node_id_secret = self.routing_table.node_id_secret(); let dh_secret = self .crypto .cached_dh(&route.safety_route.public_key, &node_id_secret) .map_err(RPCError::protocol)?; - let mut dec_blob_data = Crypto::decrypt_aead(&d.blob, &d.nonce, &dh_secret, None) + let mut dec_blob_data = Crypto::decrypt_aead(&route_hop_data.blob, &route_hop_data.nonce, &dh_secret, None) .map_err(RPCError::protocol)?; // See if this is last hop in safety route, if so, we're decoding a PrivateRoute not a RouteHop @@ -353,7 +412,7 @@ impl RPCProcessor { network_result_try!(self.process_private_route_first_hop( route.operation, route.safety_route.public_key, - &private_route, + private_route, ) .await?); } else if dec_blob_tag == 0 { @@ -366,16 +425,16 @@ impl RPCProcessor { }; // Continue the full safety route with another hop - network_result_try!(self.process_route_safety_route_hop(route, route_hop) + network_result_try!(self.process_route_safety_route_hop(route.operation, route_hop, route.safety_route) .await?); } else { return Ok(NetworkResult::invalid_message("invalid blob tag")); } } // No safety route left, now doing private route - SafetyRouteHops::Private(ref private_route) => { + SafetyRouteHops::Private(private_route) => { // See if we have a hop, if not, we are at the end of the private route - match &private_route.hops { + match private_route.hops { PrivateRouteHops::FirstHop(_) => { // Safety route was a stub, start with the beginning of the private route network_result_try!(self.process_private_route_first_hop( @@ -386,33 +445,10 @@ impl RPCProcessor { .await?); } PrivateRouteHops::Data(route_hop_data) => { - // Decrypt the blob with DEC(nonce, DH(the PR's public key, this hop's secret) - let node_id_secret = self.routing_table.node_id_secret(); - let dh_secret = self - .crypto - .cached_dh(&private_route.public_key, &node_id_secret) - .map_err(RPCError::protocol)?; - let dec_blob_data = match Crypto::decrypt_aead( - &route_hop_data.blob, - &route_hop_data.nonce, - &dh_secret, - None, - ) { - Ok(v) => v, - Err(e) => { - return Ok(NetworkResult::invalid_message(format!("unable to decrypt private route hop data: {}", e))); - } - }; - let dec_blob_reader = RPCMessageData::new(dec_blob_data).get_reader()?; - - // Decode next RouteHop - let route_hop = { - let rh_reader = dec_blob_reader - .get_root::() - .map_err(RPCError::protocol)?; - decode_route_hop(&rh_reader)? - }; - + + // Decrypt route hop data + let route_hop = network_result_try!(self.decrypt_private_route_hop_data(&route_hop_data, &private_route.public_key, &mut route.operation)?); + // Ensure hop count > 0 if private_route.hop_count == 0 { return Ok(NetworkResult::invalid_message( @@ -420,16 +456,6 @@ impl RPCProcessor { )); } - // Sign the operation if this is not our last hop - // as the last hop is already signed by the envelope - if route_hop.next_hop.is_some() { - let node_id = self.routing_table.node_id(); - let node_id_secret = self.routing_table.node_id_secret(); - let sig = sign(&node_id, &node_id_secret, &route.operation.data) - .map_err(RPCError::internal)?; - route.operation.signatures.push(sig); - } - // Make next PrivateRoute and pass it on network_result_try!(self.process_route_private_route_hop( route.operation, @@ -453,13 +479,18 @@ impl RPCProcessor { "route should be at the end", )); } + if route.safety_route.hop_count != 0 { + return Ok(NetworkResult::invalid_message( + "Safety hop count should be zero if switched to private route", + )); + } // No hops left, time to process the routed operation network_result_try!(self.process_routed_operation( detail, route.operation, - &route.safety_route, - private_route, + route.safety_route.public_key, + private_route.public_key, )?); } } From 05be3c8cc5c9e583da7193bb1c6a1f9286403689 Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 24 Nov 2022 20:17:54 -0500 Subject: [PATCH 04/88] refactor --- veilid-cli/src/client_api_connection.rs | 3 + veilid-cli/src/command_processor.rs | 3 + veilid-core/src/network_manager/mod.rs | 215 +----- veilid-core/src/network_manager/tasks.rs | 699 ------------------ veilid-core/src/network_manager/tasks/mod.rs | 82 ++ .../tasks/public_address_check.rs | 28 + .../tasks/rolling_transfers.rs | 52 ++ veilid-core/src/routing_table/debug.rs | 1 + veilid-core/src/routing_table/mod.rs | 81 +- .../src/routing_table/route_spec_store.rs | 52 +- .../src/routing_table/tasks/bootstrap.rs | 347 +++++++++ .../src/routing_table/tasks/kick_buckets.rs | 23 + veilid-core/src/routing_table/tasks/mod.rs | 202 +++++ .../tasks/peer_minimum_refresh.rs | 47 ++ .../src/routing_table/tasks/ping_validator.rs | 142 ++++ .../tasks/private_route_management.rs | 34 + .../routing_table/tasks/relay_management.rs | 83 +++ .../{tasks.rs => tasks/rolling_transfers.rs} | 23 +- veilid-core/src/veilid_api/mod.rs | 10 + 19 files changed, 1101 insertions(+), 1026 deletions(-) delete mode 100644 veilid-core/src/network_manager/tasks.rs create mode 100644 veilid-core/src/network_manager/tasks/mod.rs create mode 100644 veilid-core/src/network_manager/tasks/public_address_check.rs create mode 100644 veilid-core/src/network_manager/tasks/rolling_transfers.rs create mode 100644 veilid-core/src/routing_table/tasks/bootstrap.rs create mode 100644 veilid-core/src/routing_table/tasks/kick_buckets.rs create mode 100644 veilid-core/src/routing_table/tasks/mod.rs create mode 100644 veilid-core/src/routing_table/tasks/peer_minimum_refresh.rs create mode 100644 veilid-core/src/routing_table/tasks/ping_validator.rs create mode 100644 veilid-core/src/routing_table/tasks/private_route_management.rs create mode 100644 veilid-core/src/routing_table/tasks/relay_management.rs rename veilid-core/src/routing_table/{tasks.rs => tasks/rolling_transfers.rs} (60%) diff --git a/veilid-cli/src/client_api_connection.rs b/veilid-cli/src/client_api_connection.rs index 03b8343c..834612e2 100644 --- a/veilid-cli/src/client_api_connection.rs +++ b/veilid-cli/src/client_api_connection.rs @@ -92,6 +92,9 @@ impl veilid_client::Server for VeilidClientImpl { VeilidUpdate::Config(config) => { self.comproc.update_config(config); } + VeilidUpdate::Route(route) => { + self.comproc.update_route(route); + } VeilidUpdate::Shutdown => self.comproc.update_shutdown(), } diff --git a/veilid-cli/src/command_processor.rs b/veilid-cli/src/command_processor.rs index e2457d77..81e5e65a 100644 --- a/veilid-cli/src/command_processor.rs +++ b/veilid-cli/src/command_processor.rs @@ -404,6 +404,9 @@ reply - reply to an AppCall not handled directly by the server pub fn update_config(&mut self, config: veilid_core::VeilidStateConfig) { self.inner_mut().ui.set_config(config.config) } + pub fn update_route(&mut self, route: veilid_core::VeilidStateRoute) { + //self.inner_mut().ui.set_config(config.config) + } pub fn update_log(&mut self, log: veilid_core::VeilidLog) { self.inner().ui.add_node_event(format!( diff --git a/veilid-core/src/network_manager/mod.rs b/veilid-core/src/network_manager/mod.rs index 6c87aaf1..ba3adccf 100644 --- a/veilid-core/src/network_manager/mod.rs +++ b/veilid-core/src/network_manager/mod.rs @@ -23,7 +23,7 @@ pub use network_connection::*; use connection_handle::*; use connection_limits::*; use crypto::*; -use futures_util::stream::{FuturesOrdered, FuturesUnordered, StreamExt}; +use futures_util::stream::{FuturesUnordered, StreamExt}; use hashlink::LruCache; use intf::*; #[cfg(not(target_arch = "wasm32"))] @@ -37,8 +37,6 @@ use xx::*; //////////////////////////////////////////////////////////////////////////////////////// -pub const RELAY_MANAGEMENT_INTERVAL_SECS: u32 = 1; -pub const PRIVATE_ROUTE_MANAGEMENT_INTERVAL_SECS: u32 = 1; pub const MAX_MESSAGE_SIZE: usize = MAX_ENVELOPE_SIZE; pub const IPADDR_TABLE_SIZE: usize = 1024; pub const IPADDR_MAX_INACTIVE_DURATION_US: u64 = 300_000_000u64; // 5 minutes @@ -48,15 +46,6 @@ pub const PUBLIC_ADDRESS_CHECK_TASK_INTERVAL_SECS: u32 = 60; pub const PUBLIC_ADDRESS_INCONSISTENCY_TIMEOUT_US: u64 = 300_000_000u64; // 5 minutes pub const PUBLIC_ADDRESS_INCONSISTENCY_PUNISHMENT_TIMEOUT_US: u64 = 3600_000_000u64; // 60 minutes pub const BOOT_MAGIC: &[u8; 4] = b"BOOT"; -pub const BOOTSTRAP_TXT_VERSION: u8 = 0; - -#[derive(Clone, Debug)] -pub struct BootstrapRecord { - min_version: u8, - max_version: u8, - dial_info_details: Vec, -} -pub type BootstrapRecordMap = BTreeMap; #[derive(Copy, Clone, Debug, Default)] pub struct ProtocolConfig { @@ -166,11 +155,6 @@ struct NetworkManagerUnlockedInner { update_callback: RwLock>, // Background processes rolling_transfers_task: TickTask, - relay_management_task: TickTask, - private_route_management_task: TickTask, - bootstrap_task: TickTask, - peer_minimum_refresh_task: TickTask, - ping_validator_task: TickTask, public_address_check_task: TickTask, node_info_update_single_future: MustJoinSingleFuture<()>, } @@ -197,7 +181,6 @@ impl NetworkManager { block_store: BlockStore, crypto: Crypto, ) -> NetworkManagerUnlockedInner { - let min_peer_refresh_time_ms = config.get().network.dht.min_peer_refresh_time_ms; NetworkManagerUnlockedInner { config, protected_store, @@ -208,11 +191,6 @@ impl NetworkManager { components: RwLock::new(None), update_callback: RwLock::new(None), rolling_transfers_task: TickTask::new(ROLLING_TRANSFERS_INTERVAL_SECS), - relay_management_task: TickTask::new(RELAY_MANAGEMENT_INTERVAL_SECS), - private_route_management_task: TickTask::new(PRIVATE_ROUTE_MANAGEMENT_INTERVAL_SECS), - bootstrap_task: TickTask::new(1), - peer_minimum_refresh_task: TickTask::new_ms(min_peer_refresh_time_ms), - ping_validator_task: TickTask::new(1), public_address_check_task: TickTask::new(PUBLIC_ADDRESS_CHECK_TASK_INTERVAL_SECS), node_info_update_single_future: MustJoinSingleFuture::new(), } @@ -235,116 +213,9 @@ impl NetworkManager { crypto, )), }; - // Set rolling transfers tick task - { - let this2 = this.clone(); - this.unlocked_inner - .rolling_transfers_task - .set_routine(move |s, l, t| { - Box::pin( - this2 - .clone() - .rolling_transfers_task_routine(s, l, t) - .instrument(trace_span!( - parent: None, - "NetworkManager rolling transfers task routine" - )), - ) - }); - } - // Set relay management tick task - { - let this2 = this.clone(); - this.unlocked_inner - .relay_management_task - .set_routine(move |s, l, t| { - Box::pin( - this2 - .clone() - .relay_management_task_routine(s, l, t) - .instrument(trace_span!(parent: None, "relay management task routine")), - ) - }); - } - // Set private route management tick task - { - let this2 = this.clone(); - this.unlocked_inner - .private_route_management_task - .set_routine(move |s, l, t| { - Box::pin( - this2 - .clone() - .private_route_management_task_routine(s, l, t) - .instrument(trace_span!( - parent: None, - "private route management task routine" - )), - ) - }); - } - // Set bootstrap tick task - { - let this2 = this.clone(); - this.unlocked_inner - .bootstrap_task - .set_routine(move |s, _l, _t| { - Box::pin( - this2 - .clone() - .bootstrap_task_routine(s) - .instrument(trace_span!(parent: None, "bootstrap task routine")), - ) - }); - } - // Set peer minimum refresh tick task - { - let this2 = this.clone(); - this.unlocked_inner - .peer_minimum_refresh_task - .set_routine(move |s, _l, _t| { - Box::pin( - this2 - .clone() - .peer_minimum_refresh_task_routine(s) - .instrument(trace_span!( - parent: None, - "peer minimum refresh task routine" - )), - ) - }); - } - // Set ping validator tick task - { - let this2 = this.clone(); - this.unlocked_inner - .ping_validator_task - .set_routine(move |s, l, t| { - Box::pin( - this2 - .clone() - .ping_validator_task_routine(s, l, t) - .instrument(trace_span!(parent: None, "ping validator task routine")), - ) - }); - } - // Set public address check task - { - let this2 = this.clone(); - this.unlocked_inner - .public_address_check_task - .set_routine(move |s, l, t| { - Box::pin( - this2 - .clone() - .public_address_check_task_routine(s, l, t) - .instrument(trace_span!( - parent: None, - "public address check task routine" - )), - ) - }); - } + + this.start_tasks(); + this } pub fn config(&self) -> VeilidConfig { @@ -492,36 +363,7 @@ impl NetworkManager { debug!("starting network manager shutdown"); // Cancel all tasks - debug!("stopping rolling transfers task"); - if let Err(e) = self.unlocked_inner.rolling_transfers_task.stop().await { - warn!("rolling_transfers_task not stopped: {}", e); - } - debug!("stopping relay management task"); - if let Err(e) = self.unlocked_inner.relay_management_task.stop().await { - warn!("relay_management_task not stopped: {}", e); - } - debug!("stopping bootstrap task"); - if let Err(e) = self.unlocked_inner.bootstrap_task.stop().await { - error!("bootstrap_task not stopped: {}", e); - } - debug!("stopping peer minimum refresh task"); - if let Err(e) = self.unlocked_inner.peer_minimum_refresh_task.stop().await { - error!("peer_minimum_refresh_task not stopped: {}", e); - } - debug!("stopping ping_validator task"); - if let Err(e) = self.unlocked_inner.ping_validator_task.stop().await { - error!("ping_validator_task not stopped: {}", e); - } - debug!("stopping node info update singlefuture"); - if self - .unlocked_inner - .node_info_update_single_future - .join() - .await - .is_err() - { - error!("node_info_update_single_future not stopped"); - } + self.stop_tasks().await; // Shutdown network components if they started up debug!("shutting down network components"); @@ -597,53 +439,6 @@ impl NetworkManager { net.needs_restart() } - pub async fn tick(&self) -> EyreResult<()> { - let routing_table = self.routing_table(); - let net = self.net(); - let receipt_manager = self.receipt_manager(); - - // Run the rolling transfers task - self.unlocked_inner.rolling_transfers_task.tick().await?; - - // Run the relay management task - self.unlocked_inner.relay_management_task.tick().await?; - - // See how many live PublicInternet entries we have - let live_public_internet_entry_count = routing_table.get_entry_count( - RoutingDomain::PublicInternet.into(), - BucketEntryState::Unreliable, - ); - let min_peer_count = self.with_config(|c| c.network.dht.min_peer_count as usize); - - // If none, then add the bootstrap nodes to it - if live_public_internet_entry_count == 0 { - self.unlocked_inner.bootstrap_task.tick().await?; - } - // If we still don't have enough peers, find nodes until we do - else if !self.unlocked_inner.bootstrap_task.is_running() - && live_public_internet_entry_count < min_peer_count - { - self.unlocked_inner.peer_minimum_refresh_task.tick().await?; - } - - // Ping validate some nodes to groom the table - self.unlocked_inner.ping_validator_task.tick().await?; - - // Run the routing table tick - routing_table.tick().await?; - - // Run the low level network tick - net.tick().await?; - - // Run the receipt manager tick - receipt_manager.tick().await?; - - // Purge the client whitelist - self.purge_client_whitelist(); - - Ok(()) - } - /// Get our node's capabilities in the PublicInternet routing domain fn generate_public_internet_node_status(&self) -> PublicInternetNodeStatus { let own_peer_info = self diff --git a/veilid-core/src/network_manager/tasks.rs b/veilid-core/src/network_manager/tasks.rs deleted file mode 100644 index 3786410a..00000000 --- a/veilid-core/src/network_manager/tasks.rs +++ /dev/null @@ -1,699 +0,0 @@ -use super::*; - -use crate::crypto::*; -use crate::xx::*; -use futures_util::FutureExt; -use stop_token::future::FutureExt as StopFutureExt; - -impl NetworkManager { - // Bootstrap lookup process - #[instrument(level = "trace", skip(self), ret, err)] - pub(super) async fn resolve_bootstrap( - &self, - bootstrap: Vec, - ) -> EyreResult { - // Resolve from bootstrap root to bootstrap hostnames - let mut bsnames = Vec::::new(); - for bh in bootstrap { - // Get TXT record for bootstrap (bootstrap.veilid.net, or similar) - let records = intf::txt_lookup(&bh).await?; - for record in records { - // Split the bootstrap name record by commas - for rec in record.split(',') { - let rec = rec.trim(); - // If the name specified is fully qualified, go with it - let bsname = if rec.ends_with('.') { - rec.to_string() - } - // If the name is not fully qualified, prepend it to the bootstrap name - else { - format!("{}.{}", rec, bh) - }; - - // Add to the list of bootstrap name to look up - bsnames.push(bsname); - } - } - } - - // Get bootstrap nodes from hostnames concurrently - let mut unord = FuturesUnordered::new(); - for bsname in bsnames { - unord.push( - async move { - // look up boostrap node txt records - let bsnirecords = match intf::txt_lookup(&bsname).await { - Err(e) => { - warn!("bootstrap node txt lookup failed for {}: {}", bsname, e); - return None; - } - Ok(v) => v, - }; - // for each record resolve into key/bootstraprecord pairs - let mut bootstrap_records: Vec<(DHTKey, BootstrapRecord)> = Vec::new(); - for bsnirecord in bsnirecords { - // Bootstrap TXT Record Format Version 0: - // txt_version,min_version,max_version,nodeid,hostname,dialinfoshort* - // - // Split bootstrap node record by commas. Example: - // 0,0,0,7lxDEabK_qgjbe38RtBa3IZLrud84P6NhGP-pRTZzdQ,bootstrap-1.dev.veilid.net,T5150,U5150,W5150/ws - let records: Vec = bsnirecord - .trim() - .split(',') - .map(|x| x.trim().to_owned()) - .collect(); - if records.len() < 6 { - warn!("invalid number of fields in bootstrap txt record"); - continue; - } - - // Bootstrap TXT record version - let txt_version: u8 = match records[0].parse::() { - Ok(v) => v, - Err(e) => { - warn!( - "invalid txt_version specified in bootstrap node txt record: {}", - e - ); - continue; - } - }; - if txt_version != BOOTSTRAP_TXT_VERSION { - warn!("unsupported bootstrap txt record version"); - continue; - } - - // Min/Max wire protocol version - let min_version: u8 = match records[1].parse::() { - Ok(v) => v, - Err(e) => { - warn!( - "invalid min_version specified in bootstrap node txt record: {}", - e - ); - continue; - } - }; - let max_version: u8 = match records[2].parse::() { - Ok(v) => v, - Err(e) => { - warn!( - "invalid max_version specified in bootstrap node txt record: {}", - e - ); - continue; - } - }; - - // Node Id - let node_id_str = &records[3]; - let node_id_key = match DHTKey::try_decode(node_id_str) { - Ok(v) => v, - Err(e) => { - warn!( - "Invalid node id in bootstrap node record {}: {}", - node_id_str, e - ); - continue; - } - }; - - // Hostname - let hostname_str = &records[4]; - - // If this is our own node id, then we skip it for bootstrap, in case we are a bootstrap node - if self.routing_table().node_id() == node_id_key { - continue; - } - - // Resolve each record and store in node dial infos list - let mut bootstrap_record = BootstrapRecord { - min_version, - max_version, - dial_info_details: Vec::new(), - }; - for rec in &records[5..] { - let rec = rec.trim(); - let dial_infos = match DialInfo::try_vec_from_short(rec, hostname_str) { - Ok(dis) => dis, - Err(e) => { - warn!( - "Couldn't resolve bootstrap node dial info {}: {}", - rec, e - ); - continue; - } - }; - - for di in dial_infos { - bootstrap_record.dial_info_details.push(DialInfoDetail { - dial_info: di, - class: DialInfoClass::Direct, - }); - } - } - bootstrap_records.push((node_id_key, bootstrap_record)); - } - Some(bootstrap_records) - } - .instrument(Span::current()), - ); - } - - let mut bsmap = BootstrapRecordMap::new(); - while let Some(bootstrap_records) = unord.next().await { - if let Some(bootstrap_records) = bootstrap_records { - for (bskey, mut bsrec) in bootstrap_records { - let rec = bsmap.entry(bskey).or_insert_with(|| BootstrapRecord { - min_version: bsrec.min_version, - max_version: bsrec.max_version, - dial_info_details: Vec::new(), - }); - rec.dial_info_details.append(&mut bsrec.dial_info_details); - } - } - } - - Ok(bsmap) - } - - // 'direct' bootstrap task routine for systems incapable of resolving TXT records, such as browser WASM - #[instrument(level = "trace", skip(self), err)] - pub(super) async fn direct_bootstrap_task_routine( - self, - stop_token: StopToken, - bootstrap_dialinfos: Vec, - ) -> EyreResult<()> { - let mut unord = FuturesUnordered::new(); - let routing_table = self.routing_table(); - - for bootstrap_di in bootstrap_dialinfos { - log_net!(debug "direct bootstrap with: {}", bootstrap_di); - - let peer_info = self.boot_request(bootstrap_di).await?; - - log_net!(debug " direct bootstrap peerinfo: {:?}", peer_info); - - // Got peer info, let's add it to the routing table - for pi in peer_info { - let k = pi.node_id.key; - // Register the node - if let Some(nr) = routing_table.register_node_with_signed_node_info( - RoutingDomain::PublicInternet, - k, - pi.signed_node_info, - false, - ) { - // Add this our futures to process in parallel - let routing_table = routing_table.clone(); - unord.push( - // lets ask bootstrap to find ourselves now - async move { routing_table.reverse_find_node(nr, true).await } - .instrument(Span::current()), - ); - } - } - } - - // Wait for all bootstrap operations to complete before we complete the singlefuture - while let Ok(Some(_)) = unord.next().timeout_at(stop_token.clone()).await {} - - Ok(()) - } - - #[instrument(level = "trace", skip(self), err)] - pub(super) async fn bootstrap_task_routine(self, stop_token: StopToken) -> EyreResult<()> { - let (bootstrap, bootstrap_nodes) = { - let c = self.unlocked_inner.config.get(); - ( - c.network.bootstrap.clone(), - c.network.bootstrap_nodes.clone(), - ) - }; - let routing_table = self.routing_table(); - - log_net!(debug "--- bootstrap_task"); - - // See if we are specifying a direct dialinfo for bootstrap, if so use the direct mechanism - if !bootstrap.is_empty() && bootstrap_nodes.is_empty() { - let mut bootstrap_dialinfos = Vec::::new(); - for b in &bootstrap { - if let Ok(bootstrap_di_vec) = DialInfo::try_vec_from_url(&b) { - for bootstrap_di in bootstrap_di_vec { - bootstrap_dialinfos.push(bootstrap_di); - } - } - } - if bootstrap_dialinfos.len() > 0 { - return self - .direct_bootstrap_task_routine(stop_token, bootstrap_dialinfos) - .await; - } - } - - // If we aren't specifying a bootstrap node list explicitly, then pull from the bootstrap server(s) - let bsmap: BootstrapRecordMap = if !bootstrap_nodes.is_empty() { - let mut bsmap = BootstrapRecordMap::new(); - let mut bootstrap_node_dial_infos = Vec::new(); - for b in bootstrap_nodes { - let (id_str, di_str) = b - .split_once('@') - .ok_or_else(|| eyre!("Invalid node dial info in bootstrap entry"))?; - let node_id = - NodeId::from_str(id_str).wrap_err("Invalid node id in bootstrap entry")?; - let dial_info = - DialInfo::from_str(di_str).wrap_err("Invalid dial info in bootstrap entry")?; - bootstrap_node_dial_infos.push((node_id, dial_info)); - } - for (node_id, dial_info) in bootstrap_node_dial_infos { - bsmap - .entry(node_id.key) - .or_insert_with(|| BootstrapRecord { - min_version: MIN_CRYPTO_VERSION, - max_version: MAX_CRYPTO_VERSION, - dial_info_details: Vec::new(), - }) - .dial_info_details - .push(DialInfoDetail { - dial_info, - class: DialInfoClass::Direct, // Bootstraps are always directly reachable - }); - } - bsmap - } else { - // Resolve bootstrap servers and recurse their TXT entries - self.resolve_bootstrap(bootstrap).await? - }; - - // Map all bootstrap entries to a single key with multiple dialinfo - - // Run all bootstrap operations concurrently - let mut unord = FuturesUnordered::new(); - for (k, mut v) in bsmap { - // Sort dial info so we get the preferred order correct - v.dial_info_details.sort(); - - log_net!("--- bootstrapping {} with {:?}", k.encode(), &v); - - // Make invalid signed node info (no signature) - if let Some(nr) = routing_table.register_node_with_signed_node_info( - RoutingDomain::PublicInternet, - k, - SignedNodeInfo::Direct(SignedDirectNodeInfo::with_no_signature(NodeInfo { - network_class: NetworkClass::InboundCapable, // Bootstraps are always inbound capable - outbound_protocols: ProtocolTypeSet::only(ProtocolType::UDP), // Bootstraps do not participate in relaying and will not make outbound requests, but will have UDP enabled - address_types: AddressTypeSet::all(), // Bootstraps are always IPV4 and IPV6 capable - min_version: v.min_version, // Minimum crypto version specified in txt record - max_version: v.max_version, // Maximum crypto version specified in txt record - dial_info_detail_list: v.dial_info_details, // Dial info is as specified in the bootstrap list - })), - true, - ) { - // Add this our futures to process in parallel - let routing_table = routing_table.clone(); - unord.push( - async move { - // Need VALID signed peer info, so ask bootstrap to find_node of itself - // which will ensure it has the bootstrap's signed peer info as part of the response - let _ = routing_table.find_target(nr.clone()).await; - - // Ensure we got the signed peer info - if !nr.signed_node_info_has_valid_signature(RoutingDomain::PublicInternet) { - log_net!(warn - "bootstrap at {:?} did not return valid signed node info", - nr - ); - // If this node info is invalid, it will time out after being unpingable - } else { - // otherwise this bootstrap is valid, lets ask it to find ourselves now - routing_table.reverse_find_node(nr, true).await - } - } - .instrument(Span::current()), - ); - } - } - - // Wait for all bootstrap operations to complete before we complete the singlefuture - while let Ok(Some(_)) = unord.next().timeout_at(stop_token.clone()).await {} - Ok(()) - } - - // Ping each node in the routing table if they need to be pinged - // to determine their reliability - #[instrument(level = "trace", skip(self), err)] - fn ping_validator_public_internet( - &self, - cur_ts: u64, - unord: &mut FuturesUnordered< - SendPinBoxFuture>>, RPCError>>, - >, - ) -> EyreResult<()> { - let rpc = self.rpc_processor(); - let routing_table = self.routing_table(); - - // Get all nodes needing pings in the PublicInternet routing domain - let node_refs = routing_table.get_nodes_needing_ping(RoutingDomain::PublicInternet, cur_ts); - - // Look up any NAT mappings we may need to try to preserve with keepalives - let mut mapped_port_info = routing_table.get_low_level_port_info(); - - // Get the PublicInternet relay if we are using one - let opt_relay_nr = routing_table.relay_node(RoutingDomain::PublicInternet); - let opt_relay_id = opt_relay_nr.map(|nr| nr.node_id()); - - // Get our publicinternet dial info - let dids = routing_table.all_filtered_dial_info_details( - RoutingDomain::PublicInternet.into(), - &DialInfoFilter::all(), - ); - - // For all nodes needing pings, figure out how many and over what protocols - for nr in node_refs { - // If this is a relay, let's check for NAT keepalives - let mut did_pings = false; - if Some(nr.node_id()) == opt_relay_id { - // Relay nodes get pinged over all protocols we have inbound dialinfo for - // This is so we can preserve the inbound NAT mappings at our router - for did in &dids { - // Do we need to do this ping? - // Check if we have already pinged over this low-level-protocol/address-type/port combo - // We want to ensure we do the bare minimum required here - let pt = did.dial_info.protocol_type(); - let at = did.dial_info.address_type(); - let needs_ping = if let Some((llpt, port)) = - mapped_port_info.protocol_to_port.get(&(pt, at)) - { - mapped_port_info - .low_level_protocol_ports - .remove(&(*llpt, at, *port)) - } else { - false - }; - if needs_ping { - let rpc = rpc.clone(); - let dif = did.dial_info.make_filter(); - let nr_filtered = - nr.filtered_clone(NodeRefFilter::new().with_dial_info_filter(dif)); - log_net!("--> Keepalive ping to {:?}", nr_filtered); - unord.push( - async move { rpc.rpc_call_status(Destination::direct(nr_filtered)).await } - .instrument(Span::current()) - .boxed(), - ); - did_pings = true; - } - } - } - // Just do a single ping with the best protocol for all the other nodes, - // ensuring that we at least ping a relay with -something- even if we didnt have - // any mapped ports to preserve - if !did_pings { - let rpc = rpc.clone(); - unord.push( - async move { rpc.rpc_call_status(Destination::direct(nr)).await } - .instrument(Span::current()) - .boxed(), - ); - } - } - - Ok(()) - } - - // Ping each node in the LocalNetwork routing domain if they - // need to be pinged to determine their reliability - #[instrument(level = "trace", skip(self), err)] - fn ping_validator_local_network( - &self, - cur_ts: u64, - unord: &mut FuturesUnordered< - SendPinBoxFuture>>, RPCError>>, - >, - ) -> EyreResult<()> { - let rpc = self.rpc_processor(); - let routing_table = self.routing_table(); - - // Get all nodes needing pings in the LocalNetwork routing domain - let node_refs = routing_table.get_nodes_needing_ping(RoutingDomain::LocalNetwork, cur_ts); - - // For all nodes needing pings, figure out how many and over what protocols - for nr in node_refs { - let rpc = rpc.clone(); - - // Just do a single ping with the best protocol for all the nodes - unord.push( - async move { rpc.rpc_call_status(Destination::direct(nr)).await } - .instrument(Span::current()) - .boxed(), - ); - } - - Ok(()) - } - - // Ping each node in the routing table if they need to be pinged - // to determine their reliability - #[instrument(level = "trace", skip(self), err)] - pub(super) async fn ping_validator_task_routine( - self, - stop_token: StopToken, - _last_ts: u64, - cur_ts: u64, - ) -> EyreResult<()> { - let mut unord = FuturesUnordered::new(); - - // PublicInternet - self.ping_validator_public_internet(cur_ts, &mut unord)?; - - // LocalNetwork - self.ping_validator_local_network(cur_ts, &mut unord)?; - - // Wait for ping futures to complete in parallel - while let Ok(Some(_)) = unord.next().timeout_at(stop_token.clone()).await {} - - Ok(()) - } - - // Ask our remaining peers to give us more peers before we go - // back to the bootstrap servers to keep us from bothering them too much - // This only adds PublicInternet routing domain peers. The discovery - // mechanism for LocalNetwork suffices for locating all the local network - // peers that are available. This, however, may query other LocalNetwork - // nodes for their PublicInternet peers, which is a very fast way to get - // a new node online. - #[instrument(level = "trace", skip(self), err)] - pub(super) async fn peer_minimum_refresh_task_routine( - self, - stop_token: StopToken, - ) -> EyreResult<()> { - let routing_table = self.routing_table(); - let mut ord = FuturesOrdered::new(); - let min_peer_count = { - let c = self.unlocked_inner.config.get(); - c.network.dht.min_peer_count as usize - }; - - // For the PublicInternet routing domain, get list of all peers we know about - // even the unreliable ones, and ask them to find nodes close to our node too - let noderefs = routing_table.find_fastest_nodes( - min_peer_count, - VecDeque::new(), - |_rti, k: DHTKey, v: Option>| { - NodeRef::new(routing_table.clone(), k, v.unwrap().clone(), None) - }, - ); - for nr in noderefs { - let routing_table = routing_table.clone(); - ord.push_back( - async move { routing_table.reverse_find_node(nr, false).await } - .instrument(Span::current()), - ); - } - - // do peer minimum search in order from fastest to slowest - while let Ok(Some(_)) = ord.next().timeout_at(stop_token.clone()).await {} - - Ok(()) - } - - // Keep relays assigned and accessible - #[instrument(level = "trace", skip(self), err)] - pub(super) async fn relay_management_task_routine( - self, - _stop_token: StopToken, - _last_ts: u64, - cur_ts: u64, - ) -> EyreResult<()> { - // Get our node's current node info and network class and do the right thing - let routing_table = self.routing_table(); - let own_peer_info = routing_table.get_own_peer_info(RoutingDomain::PublicInternet); - let own_node_info = own_peer_info.signed_node_info.node_info(); - let network_class = routing_table.get_network_class(RoutingDomain::PublicInternet); - - // Get routing domain editor - let mut editor = routing_table.edit_routing_domain(RoutingDomain::PublicInternet); - - // Do we know our network class yet? - if let Some(network_class) = network_class { - // If we already have a relay, see if it is dead, or if we don't need it any more - let has_relay = { - if let Some(relay_node) = routing_table.relay_node(RoutingDomain::PublicInternet) { - let state = relay_node.state(cur_ts); - // Relay node is dead or no longer needed - if matches!(state, BucketEntryState::Dead) { - info!("Relay node died, dropping relay {}", relay_node); - editor.clear_relay_node(); - false - } else if !own_node_info.requires_relay() { - info!( - "Relay node no longer required, dropping relay {}", - relay_node - ); - editor.clear_relay_node(); - false - } else { - true - } - } else { - false - } - }; - - // Do we need a relay? - if !has_relay && own_node_info.requires_relay() { - // Do we want an outbound relay? - let mut got_outbound_relay = false; - if network_class.outbound_wants_relay() { - // The outbound relay is the host of the PWA - if let Some(outbound_relay_peerinfo) = intf::get_outbound_relay_peer().await { - // Register new outbound relay - if let Some(nr) = routing_table.register_node_with_signed_node_info( - RoutingDomain::PublicInternet, - outbound_relay_peerinfo.node_id.key, - outbound_relay_peerinfo.signed_node_info, - false, - ) { - info!("Outbound relay node selected: {}", nr); - editor.set_relay_node(nr); - got_outbound_relay = true; - } - } - } - if !got_outbound_relay { - // Find a node in our routing table that is an acceptable inbound relay - if let Some(nr) = - routing_table.find_inbound_relay(RoutingDomain::PublicInternet, cur_ts) - { - info!("Inbound relay node selected: {}", nr); - editor.set_relay_node(nr); - } - } - } - } - - // Commit the changes - editor.commit().await; - - Ok(()) - } - - // Keep private routes assigned and accessible - #[instrument(level = "trace", skip(self), err)] - pub(super) async fn private_route_management_task_routine( - self, - _stop_token: StopToken, - _last_ts: u64, - cur_ts: u64, - ) -> EyreResult<()> { - // Get our node's current node info and network class and do the right thing - let routing_table = self.routing_table(); - let own_peer_info = routing_table.get_own_peer_info(RoutingDomain::PublicInternet); - let network_class = routing_table.get_network_class(RoutingDomain::PublicInternet); - - // Get routing domain editor - let mut editor = routing_table.edit_routing_domain(RoutingDomain::PublicInternet); - - // Do we know our network class yet? - if let Some(network_class) = network_class { - - // see if we have any routes that need testing - } - - // Commit the changes - editor.commit().await; - - Ok(()) - } - - // Compute transfer statistics for the low level network - #[instrument(level = "trace", skip(self), err)] - pub(super) async fn rolling_transfers_task_routine( - self, - _stop_token: StopToken, - last_ts: u64, - cur_ts: u64, - ) -> EyreResult<()> { - // log_net!("--- network manager rolling_transfers task"); - { - let inner = &mut *self.inner.lock(); - - // Roll the low level network transfer stats for our address - inner - .stats - .self_stats - .transfer_stats_accounting - .roll_transfers(last_ts, cur_ts, &mut inner.stats.self_stats.transfer_stats); - - // Roll all per-address transfers - let mut dead_addrs: HashSet = HashSet::new(); - for (addr, stats) in &mut inner.stats.per_address_stats { - stats.transfer_stats_accounting.roll_transfers( - last_ts, - cur_ts, - &mut stats.transfer_stats, - ); - - // While we're here, lets see if this address has timed out - if cur_ts - stats.last_seen_ts >= IPADDR_MAX_INACTIVE_DURATION_US { - // it's dead, put it in the dead list - dead_addrs.insert(*addr); - } - } - - // Remove the dead addresses from our tables - for da in &dead_addrs { - inner.stats.per_address_stats.remove(da); - } - } - - // Send update - self.send_network_update(); - - Ok(()) - } - - // Clean up the public address check tables, removing entries that have timed out - #[instrument(level = "trace", skip(self), err)] - pub(super) async fn public_address_check_task_routine( - self, - stop_token: StopToken, - _last_ts: u64, - cur_ts: u64, - ) -> EyreResult<()> { - // go through public_address_inconsistencies_table and time out things that have expired - let mut inner = self.inner.lock(); - for (_, pait_v) in &mut inner.public_address_inconsistencies_table { - let mut expired = Vec::new(); - for (addr, exp_ts) in pait_v.iter() { - if *exp_ts <= cur_ts { - expired.push(*addr); - } - } - for exp in expired { - pait_v.remove(&exp); - } - } - Ok(()) - } -} diff --git a/veilid-core/src/network_manager/tasks/mod.rs b/veilid-core/src/network_manager/tasks/mod.rs new file mode 100644 index 00000000..108afd2d --- /dev/null +++ b/veilid-core/src/network_manager/tasks/mod.rs @@ -0,0 +1,82 @@ +pub mod public_address_check; +pub mod rolling_transfers; + +use super::*; + +impl NetworkManager { + pub(crate) fn start_tasks(&self) { + // Set rolling transfers tick task + { + let this = self.clone(); + self.unlocked_inner + .rolling_transfers_task + .set_routine(move |s, l, t| { + Box::pin( + this.clone() + .rolling_transfers_task_routine(s, l, t) + .instrument(trace_span!( + parent: None, + "NetworkManager rolling transfers task routine" + )), + ) + }); + } + + // Set public address check task + { + let this = self.clone(); + self.unlocked_inner + .public_address_check_task + .set_routine(move |s, l, t| { + Box::pin( + this.clone() + .public_address_check_task_routine(s, l, t) + .instrument(trace_span!( + parent: None, + "public address check task routine" + )), + ) + }); + } + } + + pub async fn tick(&self) -> EyreResult<()> { + let routing_table = self.routing_table(); + let net = self.net(); + let receipt_manager = self.receipt_manager(); + + // Run the rolling transfers task + self.unlocked_inner.rolling_transfers_task.tick().await?; + + // Run the routing table tick + routing_table.tick().await?; + + // Run the low level network tick + net.tick().await?; + + // Run the receipt manager tick + receipt_manager.tick().await?; + + // Purge the client whitelist + self.purge_client_whitelist(); + + Ok(()) + } + + pub(crate) async fn stop_tasks(&self) { + debug!("stopping rolling transfers task"); + if let Err(e) = self.unlocked_inner.rolling_transfers_task.stop().await { + warn!("rolling_transfers_task not stopped: {}", e); + } + debug!("stopping node info update singlefuture"); + if self + .unlocked_inner + .node_info_update_single_future + .join() + .await + .is_err() + { + error!("node_info_update_single_future not stopped"); + } + } +} diff --git a/veilid-core/src/network_manager/tasks/public_address_check.rs b/veilid-core/src/network_manager/tasks/public_address_check.rs new file mode 100644 index 00000000..92303369 --- /dev/null +++ b/veilid-core/src/network_manager/tasks/public_address_check.rs @@ -0,0 +1,28 @@ +use super::super::*; +use crate::xx::*; + +impl NetworkManager { + // Clean up the public address check tables, removing entries that have timed out + #[instrument(level = "trace", skip(self), err)] + pub(crate) async fn public_address_check_task_routine( + self, + stop_token: StopToken, + _last_ts: u64, + cur_ts: u64, + ) -> EyreResult<()> { + // go through public_address_inconsistencies_table and time out things that have expired + let mut inner = self.inner.lock(); + for (_, pait_v) in &mut inner.public_address_inconsistencies_table { + let mut expired = Vec::new(); + for (addr, exp_ts) in pait_v.iter() { + if *exp_ts <= cur_ts { + expired.push(*addr); + } + } + for exp in expired { + pait_v.remove(&exp); + } + } + Ok(()) + } +} diff --git a/veilid-core/src/network_manager/tasks/rolling_transfers.rs b/veilid-core/src/network_manager/tasks/rolling_transfers.rs new file mode 100644 index 00000000..4007a4a9 --- /dev/null +++ b/veilid-core/src/network_manager/tasks/rolling_transfers.rs @@ -0,0 +1,52 @@ +use super::super::*; + +use crate::xx::*; + +impl NetworkManager { + // Compute transfer statistics for the low level network + #[instrument(level = "trace", skip(self), err)] + pub(crate) async fn rolling_transfers_task_routine( + self, + _stop_token: StopToken, + last_ts: u64, + cur_ts: u64, + ) -> EyreResult<()> { + // log_net!("--- network manager rolling_transfers task"); + { + let inner = &mut *self.inner.lock(); + + // Roll the low level network transfer stats for our address + inner + .stats + .self_stats + .transfer_stats_accounting + .roll_transfers(last_ts, cur_ts, &mut inner.stats.self_stats.transfer_stats); + + // Roll all per-address transfers + let mut dead_addrs: HashSet = HashSet::new(); + for (addr, stats) in &mut inner.stats.per_address_stats { + stats.transfer_stats_accounting.roll_transfers( + last_ts, + cur_ts, + &mut stats.transfer_stats, + ); + + // While we're here, lets see if this address has timed out + if cur_ts - stats.last_seen_ts >= IPADDR_MAX_INACTIVE_DURATION_US { + // it's dead, put it in the dead list + dead_addrs.insert(*addr); + } + } + + // Remove the dead addresses from our tables + for da in &dead_addrs { + inner.stats.per_address_stats.remove(da); + } + } + + // Send update + self.send_network_update(); + + Ok(()) + } +} diff --git a/veilid-core/src/routing_table/debug.rs b/veilid-core/src/routing_table/debug.rs index 3170fae6..fcac7958 100644 --- a/veilid-core/src/routing_table/debug.rs +++ b/veilid-core/src/routing_table/debug.rs @@ -1,4 +1,5 @@ use super::*; +use routing_table::tasks::bootstrap::BOOTSTRAP_TXT_VERSION; impl RoutingTable { pub(crate) fn debug_info_nodeinfo(&self) -> String { diff --git a/veilid-core/src/routing_table/mod.rs b/veilid-core/src/routing_table/mod.rs index 89370fc4..1e8e5ea5 100644 --- a/veilid-core/src/routing_table/mod.rs +++ b/veilid-core/src/routing_table/mod.rs @@ -30,6 +30,8 @@ pub use routing_table_inner::*; pub use stats_accounting::*; ////////////////////////////////////////////////////////////////////////// +pub const RELAY_MANAGEMENT_INTERVAL_SECS: u32 = 1; +pub const PRIVATE_ROUTE_MANAGEMENT_INTERVAL_SECS: u32 = 1; pub type LowLevelProtocolPorts = BTreeSet<(LowLevelProtocolType, AddressType, u16)>; pub type ProtocolToPortMapping = BTreeMap<(ProtocolType, AddressType), (LowLevelProtocolType, u16)>; @@ -66,6 +68,16 @@ pub(super) struct RoutingTableUnlockedInner { rolling_transfers_task: TickTask, /// Backgroup process to purge dead routing table entries when necessary kick_buckets_task: TickTask, + /// Background process to get our initial routing table + bootstrap_task: TickTask, + /// Background process to ensure we have enough nodes in our routing table + peer_minimum_refresh_task: TickTask, + /// Background process to check nodes to see if they are still alive and for reliability + ping_validator_task: TickTask, + /// Background process to keep relays up + relay_management_task: TickTask, + /// Background process to keep private routes up + private_route_management_task: TickTask, } #[derive(Clone)] @@ -88,6 +100,11 @@ impl RoutingTable { kick_queue: Mutex::new(BTreeSet::default()), rolling_transfers_task: TickTask::new(ROLLING_TRANSFERS_INTERVAL_SECS), kick_buckets_task: TickTask::new(1), + bootstrap_task: TickTask::new(1), + peer_minimum_refresh_task: TickTask::new_ms(c.network.dht.min_peer_refresh_time_ms), + ping_validator_task: TickTask::new(1), + relay_management_task: TickTask::new(RELAY_MANAGEMENT_INTERVAL_SECS), + private_route_management_task: TickTask::new(PRIVATE_ROUTE_MANAGEMENT_INTERVAL_SECS), } } pub fn new(network_manager: NetworkManager) -> Self { @@ -99,38 +116,8 @@ impl RoutingTable { unlocked_inner, }; - // Set rolling transfers tick task - { - let this2 = this.clone(); - this.unlocked_inner - .rolling_transfers_task - .set_routine(move |s, l, t| { - Box::pin( - this2 - .clone() - .rolling_transfers_task_routine(s, l, t) - .instrument(trace_span!( - parent: None, - "RoutingTable rolling transfers task routine" - )), - ) - }); - } + this.start_tasks(); - // Set kick buckets tick task - { - let this2 = this.clone(); - this.unlocked_inner - .kick_buckets_task - .set_routine(move |s, l, t| { - Box::pin( - this2 - .clone() - .kick_buckets_task_routine(s, l, t) - .instrument(trace_span!(parent: None, "kick buckets task routine")), - ) - }); - } this } @@ -140,6 +127,12 @@ impl RoutingTable { pub fn rpc_processor(&self) -> RPCProcessor { self.network_manager().rpc_processor() } + pub fn with_config(&self, f: F) -> R + where + F: FnOnce(&VeilidConfigInner) -> R, + { + f(&*self.unlocked_inner.config.get()) + } pub fn node_id(&self) -> DHTKey { self.unlocked_inner.node_id @@ -194,15 +187,8 @@ impl RoutingTable { pub async fn terminate(&self) { debug!("starting routing table terminate"); - // Cancel all tasks being ticked - debug!("stopping rolling transfers task"); - if let Err(e) = self.unlocked_inner.rolling_transfers_task.stop().await { - error!("rolling_transfers_task not stopped: {}", e); - } - debug!("stopping kick buckets task"); - if let Err(e) = self.unlocked_inner.kick_buckets_task.stop().await { - error!("kick_buckets_task not stopped: {}", e); - } + // Stop tasks + self.stop_tasks().await; // Load bucket entries from table db if possible debug!("saving routing table entries"); @@ -551,21 +537,6 @@ impl RoutingTable { ) } - /// Ticks about once per second - /// to run tick tasks which may run at slower tick rates as configured - pub async fn tick(&self) -> EyreResult<()> { - // Do rolling transfers every ROLLING_TRANSFERS_INTERVAL_SECS secs - self.unlocked_inner.rolling_transfers_task.tick().await?; - - // Kick buckets task - let kick_bucket_queue_count = self.unlocked_inner.kick_queue.lock().len(); - if kick_bucket_queue_count > 0 { - self.unlocked_inner.kick_buckets_task.tick().await?; - } - - Ok(()) - } - ////////////////////////////////////////////////////////////////////// // Routing Table Health Metrics diff --git a/veilid-core/src/routing_table/route_spec_store.rs b/veilid-core/src/routing_table/route_spec_store.rs index b1ebab55..5723fbcc 100644 --- a/veilid-core/src/routing_table/route_spec_store.rs +++ b/veilid-core/src/routing_table/route_spec_store.rs @@ -839,6 +839,7 @@ impl RouteSpecStore { pub async fn test_route(&self, key: &DHTKey) -> EyreResult { let rpc_processor = self.unlocked_inner.routing_table.rpc_processor(); + // Make loopback route to test with let dest = { let private_route = self.assemble_private_route(key, None)?; @@ -848,48 +849,17 @@ impl RouteSpecStore { let stability = rsd.stability; let sequencing = rsd.sequencing; - // Routes with just one hop can be pinged directly - // More than one hop can be pinged across the route with the target being the second to last hop - if rsd.hops.len() == 1 { - let safety_spec = SafetySpec { - preferred_route: Some(key.clone()), - hop_count, - stability, - sequencing, - }; - let safety_selection = SafetySelection::Safe(safety_spec); + let safety_spec = SafetySpec { + preferred_route: Some(key.clone()), + hop_count, + stability, + sequencing, + }; + let safety_selection = SafetySelection::Safe(safety_spec); - Destination::PrivateRoute { - private_route, - safety_selection, - } - } else { - // let target = rsd.hop_node_refs[rsd.hops.len() - 2].clone(); - // let safety_spec = SafetySpec { - // preferred_route: Some(key.clone()), - // hop_count, - // stability, - // sequencing, - // }; - // let safety_selection = SafetySelection::Safe(safety_spec); - - // Destination::Direct { - // target, - // safety_selection, - // } - - let safety_spec = SafetySpec { - preferred_route: Some(key.clone()), - hop_count, - stability, - sequencing, - }; - let safety_selection = SafetySelection::Safe(safety_spec); - - Destination::PrivateRoute { - private_route, - safety_selection, - } + Destination::PrivateRoute { + private_route, + safety_selection, } }; diff --git a/veilid-core/src/routing_table/tasks/bootstrap.rs b/veilid-core/src/routing_table/tasks/bootstrap.rs new file mode 100644 index 00000000..d7d3da7f --- /dev/null +++ b/veilid-core/src/routing_table/tasks/bootstrap.rs @@ -0,0 +1,347 @@ +use super::super::*; +use crate::xx::*; + +use futures_util::stream::{FuturesUnordered, StreamExt}; +use stop_token::future::FutureExt as StopFutureExt; + +pub const BOOTSTRAP_TXT_VERSION: u8 = 0; + +#[derive(Clone, Debug)] +pub struct BootstrapRecord { + min_version: u8, + max_version: u8, + dial_info_details: Vec, +} +pub type BootstrapRecordMap = BTreeMap; + +impl RoutingTable { + // Bootstrap lookup process + #[instrument(level = "trace", skip(self), ret, err)] + pub(crate) async fn resolve_bootstrap( + &self, + bootstrap: Vec, + ) -> EyreResult { + // Resolve from bootstrap root to bootstrap hostnames + let mut bsnames = Vec::::new(); + for bh in bootstrap { + // Get TXT record for bootstrap (bootstrap.veilid.net, or similar) + let records = intf::txt_lookup(&bh).await?; + for record in records { + // Split the bootstrap name record by commas + for rec in record.split(',') { + let rec = rec.trim(); + // If the name specified is fully qualified, go with it + let bsname = if rec.ends_with('.') { + rec.to_string() + } + // If the name is not fully qualified, prepend it to the bootstrap name + else { + format!("{}.{}", rec, bh) + }; + + // Add to the list of bootstrap name to look up + bsnames.push(bsname); + } + } + } + + // Get bootstrap nodes from hostnames concurrently + let mut unord = FuturesUnordered::new(); + for bsname in bsnames { + unord.push( + async move { + // look up boostrap node txt records + let bsnirecords = match intf::txt_lookup(&bsname).await { + Err(e) => { + warn!("bootstrap node txt lookup failed for {}: {}", bsname, e); + return None; + } + Ok(v) => v, + }; + // for each record resolve into key/bootstraprecord pairs + let mut bootstrap_records: Vec<(DHTKey, BootstrapRecord)> = Vec::new(); + for bsnirecord in bsnirecords { + // Bootstrap TXT Record Format Version 0: + // txt_version,min_version,max_version,nodeid,hostname,dialinfoshort* + // + // Split bootstrap node record by commas. Example: + // 0,0,0,7lxDEabK_qgjbe38RtBa3IZLrud84P6NhGP-pRTZzdQ,bootstrap-1.dev.veilid.net,T5150,U5150,W5150/ws + let records: Vec = bsnirecord + .trim() + .split(',') + .map(|x| x.trim().to_owned()) + .collect(); + if records.len() < 6 { + warn!("invalid number of fields in bootstrap txt record"); + continue; + } + + // Bootstrap TXT record version + let txt_version: u8 = match records[0].parse::() { + Ok(v) => v, + Err(e) => { + warn!( + "invalid txt_version specified in bootstrap node txt record: {}", + e + ); + continue; + } + }; + if txt_version != BOOTSTRAP_TXT_VERSION { + warn!("unsupported bootstrap txt record version"); + continue; + } + + // Min/Max wire protocol version + let min_version: u8 = match records[1].parse::() { + Ok(v) => v, + Err(e) => { + warn!( + "invalid min_version specified in bootstrap node txt record: {}", + e + ); + continue; + } + }; + let max_version: u8 = match records[2].parse::() { + Ok(v) => v, + Err(e) => { + warn!( + "invalid max_version specified in bootstrap node txt record: {}", + e + ); + continue; + } + }; + + // Node Id + let node_id_str = &records[3]; + let node_id_key = match DHTKey::try_decode(node_id_str) { + Ok(v) => v, + Err(e) => { + warn!( + "Invalid node id in bootstrap node record {}: {}", + node_id_str, e + ); + continue; + } + }; + + // Hostname + let hostname_str = &records[4]; + + // If this is our own node id, then we skip it for bootstrap, in case we are a bootstrap node + if self.node_id() == node_id_key { + continue; + } + + // Resolve each record and store in node dial infos list + let mut bootstrap_record = BootstrapRecord { + min_version, + max_version, + dial_info_details: Vec::new(), + }; + for rec in &records[5..] { + let rec = rec.trim(); + let dial_infos = match DialInfo::try_vec_from_short(rec, hostname_str) { + Ok(dis) => dis, + Err(e) => { + warn!( + "Couldn't resolve bootstrap node dial info {}: {}", + rec, e + ); + continue; + } + }; + + for di in dial_infos { + bootstrap_record.dial_info_details.push(DialInfoDetail { + dial_info: di, + class: DialInfoClass::Direct, + }); + } + } + bootstrap_records.push((node_id_key, bootstrap_record)); + } + Some(bootstrap_records) + } + .instrument(Span::current()), + ); + } + + let mut bsmap = BootstrapRecordMap::new(); + while let Some(bootstrap_records) = unord.next().await { + if let Some(bootstrap_records) = bootstrap_records { + for (bskey, mut bsrec) in bootstrap_records { + let rec = bsmap.entry(bskey).or_insert_with(|| BootstrapRecord { + min_version: bsrec.min_version, + max_version: bsrec.max_version, + dial_info_details: Vec::new(), + }); + rec.dial_info_details.append(&mut bsrec.dial_info_details); + } + } + } + + Ok(bsmap) + } + + // 'direct' bootstrap task routine for systems incapable of resolving TXT records, such as browser WASM + #[instrument(level = "trace", skip(self), err)] + pub(crate) async fn direct_bootstrap_task_routine( + self, + stop_token: StopToken, + bootstrap_dialinfos: Vec, + ) -> EyreResult<()> { + let mut unord = FuturesUnordered::new(); + let network_manager = self.network_manager(); + + for bootstrap_di in bootstrap_dialinfos { + log_rtab!(debug "direct bootstrap with: {}", bootstrap_di); + let peer_info = network_manager.boot_request(bootstrap_di).await?; + + log_rtab!(debug " direct bootstrap peerinfo: {:?}", peer_info); + + // Got peer info, let's add it to the routing table + for pi in peer_info { + let k = pi.node_id.key; + // Register the node + if let Some(nr) = self.register_node_with_signed_node_info( + RoutingDomain::PublicInternet, + k, + pi.signed_node_info, + false, + ) { + // Add this our futures to process in parallel + let routing_table = self.clone(); + unord.push( + // lets ask bootstrap to find ourselves now + async move { routing_table.reverse_find_node(nr, true).await } + .instrument(Span::current()), + ); + } + } + } + + // Wait for all bootstrap operations to complete before we complete the singlefuture + while let Ok(Some(_)) = unord.next().timeout_at(stop_token.clone()).await {} + + Ok(()) + } + + #[instrument(level = "trace", skip(self), err)] + pub(crate) async fn bootstrap_task_routine(self, stop_token: StopToken) -> EyreResult<()> { + let (bootstrap, bootstrap_nodes) = self.with_config(|c| { + ( + c.network.bootstrap.clone(), + c.network.bootstrap_nodes.clone(), + ) + }); + + log_rtab!(debug "--- bootstrap_task"); + + // See if we are specifying a direct dialinfo for bootstrap, if so use the direct mechanism + if !bootstrap.is_empty() && bootstrap_nodes.is_empty() { + let mut bootstrap_dialinfos = Vec::::new(); + for b in &bootstrap { + if let Ok(bootstrap_di_vec) = DialInfo::try_vec_from_url(&b) { + for bootstrap_di in bootstrap_di_vec { + bootstrap_dialinfos.push(bootstrap_di); + } + } + } + if bootstrap_dialinfos.len() > 0 { + return self + .direct_bootstrap_task_routine(stop_token, bootstrap_dialinfos) + .await; + } + } + + // If we aren't specifying a bootstrap node list explicitly, then pull from the bootstrap server(s) + let bsmap: BootstrapRecordMap = if !bootstrap_nodes.is_empty() { + let mut bsmap = BootstrapRecordMap::new(); + let mut bootstrap_node_dial_infos = Vec::new(); + for b in bootstrap_nodes { + let (id_str, di_str) = b + .split_once('@') + .ok_or_else(|| eyre!("Invalid node dial info in bootstrap entry"))?; + let node_id = + NodeId::from_str(id_str).wrap_err("Invalid node id in bootstrap entry")?; + let dial_info = + DialInfo::from_str(di_str).wrap_err("Invalid dial info in bootstrap entry")?; + bootstrap_node_dial_infos.push((node_id, dial_info)); + } + for (node_id, dial_info) in bootstrap_node_dial_infos { + bsmap + .entry(node_id.key) + .or_insert_with(|| BootstrapRecord { + min_version: MIN_CRYPTO_VERSION, + max_version: MAX_CRYPTO_VERSION, + dial_info_details: Vec::new(), + }) + .dial_info_details + .push(DialInfoDetail { + dial_info, + class: DialInfoClass::Direct, // Bootstraps are always directly reachable + }); + } + bsmap + } else { + // Resolve bootstrap servers and recurse their TXT entries + self.resolve_bootstrap(bootstrap).await? + }; + + // Map all bootstrap entries to a single key with multiple dialinfo + + // Run all bootstrap operations concurrently + let mut unord = FuturesUnordered::new(); + for (k, mut v) in bsmap { + // Sort dial info so we get the preferred order correct + v.dial_info_details.sort(); + + log_rtab!("--- bootstrapping {} with {:?}", k.encode(), &v); + + // Make invalid signed node info (no signature) + if let Some(nr) = self.register_node_with_signed_node_info( + RoutingDomain::PublicInternet, + k, + SignedNodeInfo::Direct(SignedDirectNodeInfo::with_no_signature(NodeInfo { + network_class: NetworkClass::InboundCapable, // Bootstraps are always inbound capable + outbound_protocols: ProtocolTypeSet::only(ProtocolType::UDP), // Bootstraps do not participate in relaying and will not make outbound requests, but will have UDP enabled + address_types: AddressTypeSet::all(), // Bootstraps are always IPV4 and IPV6 capable + min_version: v.min_version, // Minimum crypto version specified in txt record + max_version: v.max_version, // Maximum crypto version specified in txt record + dial_info_detail_list: v.dial_info_details, // Dial info is as specified in the bootstrap list + })), + true, + ) { + // Add this our futures to process in parallel + let routing_table = self.clone(); + unord.push( + async move { + // Need VALID signed peer info, so ask bootstrap to find_node of itself + // which will ensure it has the bootstrap's signed peer info as part of the response + let _ = routing_table.find_target(nr.clone()).await; + + // Ensure we got the signed peer info + if !nr.signed_node_info_has_valid_signature(RoutingDomain::PublicInternet) { + log_rtab!(warn + "bootstrap at {:?} did not return valid signed node info", + nr + ); + // If this node info is invalid, it will time out after being unpingable + } else { + // otherwise this bootstrap is valid, lets ask it to find ourselves now + routing_table.reverse_find_node(nr, true).await + } + } + .instrument(Span::current()), + ); + } + } + + // Wait for all bootstrap operations to complete before we complete the singlefuture + while let Ok(Some(_)) = unord.next().timeout_at(stop_token.clone()).await {} + Ok(()) + } +} diff --git a/veilid-core/src/routing_table/tasks/kick_buckets.rs b/veilid-core/src/routing_table/tasks/kick_buckets.rs new file mode 100644 index 00000000..730bad1d --- /dev/null +++ b/veilid-core/src/routing_table/tasks/kick_buckets.rs @@ -0,0 +1,23 @@ +use super::super::*; +use crate::xx::*; + +impl RoutingTable { + // Kick the queued buckets in the routing table to free dead nodes if necessary + // Attempts to keep the size of the routing table down to the bucket depth + #[instrument(level = "trace", skip(self), err)] + pub(crate) async fn kick_buckets_task_routine( + self, + _stop_token: StopToken, + _last_ts: u64, + cur_ts: u64, + ) -> EyreResult<()> { + let kick_queue: Vec = core::mem::take(&mut *self.unlocked_inner.kick_queue.lock()) + .into_iter() + .collect(); + let mut inner = self.inner.write(); + for idx in kick_queue { + inner.kick_bucket(idx) + } + Ok(()) + } +} diff --git a/veilid-core/src/routing_table/tasks/mod.rs b/veilid-core/src/routing_table/tasks/mod.rs new file mode 100644 index 00000000..d2992626 --- /dev/null +++ b/veilid-core/src/routing_table/tasks/mod.rs @@ -0,0 +1,202 @@ +pub mod bootstrap; +pub mod kick_buckets; +pub mod peer_minimum_refresh; +pub mod ping_validator; +pub mod private_route_management; +pub mod relay_management; +pub mod rolling_transfers; + +use super::*; + +impl RoutingTable { + pub(crate) fn start_tasks(&self) { + // Set rolling transfers tick task + { + let this = self.clone(); + self.unlocked_inner + .rolling_transfers_task + .set_routine(move |s, l, t| { + Box::pin( + this.clone() + .rolling_transfers_task_routine(s, l, t) + .instrument(trace_span!( + parent: None, + "RoutingTable rolling transfers task routine" + )), + ) + }); + } + + // Set kick buckets tick task + { + let this = self.clone(); + self.unlocked_inner + .kick_buckets_task + .set_routine(move |s, l, t| { + Box::pin( + this.clone() + .kick_buckets_task_routine(s, l, t) + .instrument(trace_span!(parent: None, "kick buckets task routine")), + ) + }); + } + + // Set bootstrap tick task + { + let this = self.clone(); + self.unlocked_inner + .bootstrap_task + .set_routine(move |s, _l, _t| { + Box::pin( + this.clone() + .bootstrap_task_routine(s) + .instrument(trace_span!(parent: None, "bootstrap task routine")), + ) + }); + } + + // Set peer minimum refresh tick task + { + let this = self.clone(); + self.unlocked_inner + .peer_minimum_refresh_task + .set_routine(move |s, _l, _t| { + Box::pin( + this.clone() + .peer_minimum_refresh_task_routine(s) + .instrument(trace_span!( + parent: None, + "peer minimum refresh task routine" + )), + ) + }); + } + + // Set ping validator tick task + { + let this = self.clone(); + self.unlocked_inner + .ping_validator_task + .set_routine(move |s, l, t| { + Box::pin( + this.clone() + .ping_validator_task_routine(s, l, t) + .instrument(trace_span!(parent: None, "ping validator task routine")), + ) + }); + } + + // Set relay management tick task + { + let this = self.clone(); + self.unlocked_inner + .relay_management_task + .set_routine(move |s, l, t| { + Box::pin( + this.clone() + .relay_management_task_routine(s, l, t) + .instrument(trace_span!(parent: None, "relay management task routine")), + ) + }); + } + + // Set private route management tick task + { + let this = self.clone(); + self.unlocked_inner + .private_route_management_task + .set_routine(move |s, l, t| { + Box::pin( + this.clone() + .private_route_management_task_routine(s, l, t) + .instrument(trace_span!( + parent: None, + "private route management task routine" + )), + ) + }); + } + } + + /// Ticks about once per second + /// to run tick tasks which may run at slower tick rates as configured + pub async fn tick(&self) -> EyreResult<()> { + // Do rolling transfers every ROLLING_TRANSFERS_INTERVAL_SECS secs + self.unlocked_inner.rolling_transfers_task.tick().await?; + + // Kick buckets task + let kick_bucket_queue_count = self.unlocked_inner.kick_queue.lock().len(); + if kick_bucket_queue_count > 0 { + self.unlocked_inner.kick_buckets_task.tick().await?; + } + + // See how many live PublicInternet entries we have + let live_public_internet_entry_count = self.get_entry_count( + RoutingDomain::PublicInternet.into(), + BucketEntryState::Unreliable, + ); + let min_peer_count = self.with_config(|c| c.network.dht.min_peer_count as usize); + + // If none, then add the bootstrap nodes to it + if live_public_internet_entry_count == 0 { + self.unlocked_inner.bootstrap_task.tick().await?; + } + // If we still don't have enough peers, find nodes until we do + else if !self.unlocked_inner.bootstrap_task.is_running() + && live_public_internet_entry_count < min_peer_count + { + self.unlocked_inner.peer_minimum_refresh_task.tick().await?; + } + + // Ping validate some nodes to groom the table + self.unlocked_inner.ping_validator_task.tick().await?; + + // Run the relay management task + self.unlocked_inner.relay_management_task.tick().await?; + + // Run the private route management task + self.unlocked_inner + .private_route_management_task + .tick() + .await?; + + Ok(()) + } + + pub(crate) async fn stop_tasks(&self) { + // Cancel all tasks being ticked + debug!("stopping rolling transfers task"); + if let Err(e) = self.unlocked_inner.rolling_transfers_task.stop().await { + error!("rolling_transfers_task not stopped: {}", e); + } + debug!("stopping kick buckets task"); + if let Err(e) = self.unlocked_inner.kick_buckets_task.stop().await { + error!("kick_buckets_task not stopped: {}", e); + } + debug!("stopping bootstrap task"); + if let Err(e) = self.unlocked_inner.bootstrap_task.stop().await { + error!("bootstrap_task not stopped: {}", e); + } + debug!("stopping peer minimum refresh task"); + if let Err(e) = self.unlocked_inner.peer_minimum_refresh_task.stop().await { + error!("peer_minimum_refresh_task not stopped: {}", e); + } + debug!("stopping ping_validator task"); + if let Err(e) = self.unlocked_inner.ping_validator_task.stop().await { + error!("ping_validator_task not stopped: {}", e); + } + debug!("stopping relay management task"); + if let Err(e) = self.unlocked_inner.relay_management_task.stop().await { + warn!("relay_management_task not stopped: {}", e); + } + debug!("stopping private route management task"); + if let Err(e) = self + .unlocked_inner + .private_route_management_task + .stop() + .await + { + warn!("private_route_management_task not stopped: {}", e); + } + } +} diff --git a/veilid-core/src/routing_table/tasks/peer_minimum_refresh.rs b/veilid-core/src/routing_table/tasks/peer_minimum_refresh.rs new file mode 100644 index 00000000..7733755c --- /dev/null +++ b/veilid-core/src/routing_table/tasks/peer_minimum_refresh.rs @@ -0,0 +1,47 @@ +use super::super::*; +use crate::xx::*; + +use futures_util::stream::{FuturesOrdered, StreamExt}; +use stop_token::future::FutureExt as StopFutureExt; + +impl RoutingTable { + // Ask our remaining peers to give us more peers before we go + // back to the bootstrap servers to keep us from bothering them too much + // This only adds PublicInternet routing domain peers. The discovery + // mechanism for LocalNetwork suffices for locating all the local network + // peers that are available. This, however, may query other LocalNetwork + // nodes for their PublicInternet peers, which is a very fast way to get + // a new node online. + #[instrument(level = "trace", skip(self), err)] + pub(crate) async fn peer_minimum_refresh_task_routine( + self, + stop_token: StopToken, + ) -> EyreResult<()> { + let min_peer_count = self.with_config(|c| c.network.dht.min_peer_count as usize); + + // For the PublicInternet routing domain, get list of all peers we know about + // even the unreliable ones, and ask them to find nodes close to our node too + let routing_table = self.clone(); + let noderefs = routing_table.find_fastest_nodes( + min_peer_count, + VecDeque::new(), + |_rti, k: DHTKey, v: Option>| { + NodeRef::new(routing_table.clone(), k, v.unwrap().clone(), None) + }, + ); + + let mut ord = FuturesOrdered::new(); + for nr in noderefs { + let routing_table = self.clone(); + ord.push_back( + async move { routing_table.reverse_find_node(nr, false).await } + .instrument(Span::current()), + ); + } + + // do peer minimum search in order from fastest to slowest + while let Ok(Some(_)) = ord.next().timeout_at(stop_token.clone()).await {} + + Ok(()) + } +} diff --git a/veilid-core/src/routing_table/tasks/ping_validator.rs b/veilid-core/src/routing_table/tasks/ping_validator.rs new file mode 100644 index 00000000..976fb79b --- /dev/null +++ b/veilid-core/src/routing_table/tasks/ping_validator.rs @@ -0,0 +1,142 @@ +use super::super::*; +use crate::xx::*; + +use futures_util::stream::{FuturesUnordered, StreamExt}; +use futures_util::FutureExt; +use stop_token::future::FutureExt as StopFutureExt; + +impl RoutingTable { + // Ping each node in the routing table if they need to be pinged + // to determine their reliability + #[instrument(level = "trace", skip(self), err)] + fn ping_validator_public_internet( + &self, + cur_ts: u64, + unord: &mut FuturesUnordered< + SendPinBoxFuture>>, RPCError>>, + >, + ) -> EyreResult<()> { + let rpc = self.rpc_processor(); + + // Get all nodes needing pings in the PublicInternet routing domain + let node_refs = self.get_nodes_needing_ping(RoutingDomain::PublicInternet, cur_ts); + + // Look up any NAT mappings we may need to try to preserve with keepalives + let mut mapped_port_info = self.get_low_level_port_info(); + + // Get the PublicInternet relay if we are using one + let opt_relay_nr = self.relay_node(RoutingDomain::PublicInternet); + let opt_relay_id = opt_relay_nr.map(|nr| nr.node_id()); + + // Get our publicinternet dial info + let dids = self.all_filtered_dial_info_details( + RoutingDomain::PublicInternet.into(), + &DialInfoFilter::all(), + ); + + // For all nodes needing pings, figure out how many and over what protocols + for nr in node_refs { + // If this is a relay, let's check for NAT keepalives + let mut did_pings = false; + if Some(nr.node_id()) == opt_relay_id { + // Relay nodes get pinged over all protocols we have inbound dialinfo for + // This is so we can preserve the inbound NAT mappings at our router + for did in &dids { + // Do we need to do this ping? + // Check if we have already pinged over this low-level-protocol/address-type/port combo + // We want to ensure we do the bare minimum required here + let pt = did.dial_info.protocol_type(); + let at = did.dial_info.address_type(); + let needs_ping = if let Some((llpt, port)) = + mapped_port_info.protocol_to_port.get(&(pt, at)) + { + mapped_port_info + .low_level_protocol_ports + .remove(&(*llpt, at, *port)) + } else { + false + }; + if needs_ping { + let rpc = rpc.clone(); + let dif = did.dial_info.make_filter(); + let nr_filtered = + nr.filtered_clone(NodeRefFilter::new().with_dial_info_filter(dif)); + log_net!("--> Keepalive ping to {:?}", nr_filtered); + unord.push( + async move { rpc.rpc_call_status(Destination::direct(nr_filtered)).await } + .instrument(Span::current()) + .boxed(), + ); + did_pings = true; + } + } + } + // Just do a single ping with the best protocol for all the other nodes, + // ensuring that we at least ping a relay with -something- even if we didnt have + // any mapped ports to preserve + if !did_pings { + let rpc = rpc.clone(); + unord.push( + async move { rpc.rpc_call_status(Destination::direct(nr)).await } + .instrument(Span::current()) + .boxed(), + ); + } + } + + Ok(()) + } + + // Ping each node in the LocalNetwork routing domain if they + // need to be pinged to determine their reliability + #[instrument(level = "trace", skip(self), err)] + fn ping_validator_local_network( + &self, + cur_ts: u64, + unord: &mut FuturesUnordered< + SendPinBoxFuture>>, RPCError>>, + >, + ) -> EyreResult<()> { + let rpc = self.rpc_processor(); + + // Get all nodes needing pings in the LocalNetwork routing domain + let node_refs = self.get_nodes_needing_ping(RoutingDomain::LocalNetwork, cur_ts); + + // For all nodes needing pings, figure out how many and over what protocols + for nr in node_refs { + let rpc = rpc.clone(); + + // Just do a single ping with the best protocol for all the nodes + unord.push( + async move { rpc.rpc_call_status(Destination::direct(nr)).await } + .instrument(Span::current()) + .boxed(), + ); + } + + Ok(()) + } + + // Ping each node in the routing table if they need to be pinged + // to determine their reliability + #[instrument(level = "trace", skip(self), err)] + pub(crate) async fn ping_validator_task_routine( + self, + stop_token: StopToken, + _last_ts: u64, + cur_ts: u64, + ) -> EyreResult<()> { + let mut unord = FuturesUnordered::new(); + + // PublicInternet + self.ping_validator_public_internet(cur_ts, &mut unord)?; + + // LocalNetwork + self.ping_validator_local_network(cur_ts, &mut unord)?; + + // Wait for ping futures to complete in parallel + while let Ok(Some(_)) = unord.next().timeout_at(stop_token.clone()).await {} + + Ok(()) + } +} diff --git a/veilid-core/src/routing_table/tasks/private_route_management.rs b/veilid-core/src/routing_table/tasks/private_route_management.rs new file mode 100644 index 00000000..e1770a1e --- /dev/null +++ b/veilid-core/src/routing_table/tasks/private_route_management.rs @@ -0,0 +1,34 @@ +use super::super::*; +use crate::xx::*; + +use futures_util::stream::{FuturesOrdered, StreamExt}; +use stop_token::future::FutureExt as StopFutureExt; + +impl RoutingTable { + // Keep private routes assigned and accessible + #[instrument(level = "trace", skip(self), err)] + pub(crate) async fn private_route_management_task_routine( + self, + _stop_token: StopToken, + _last_ts: u64, + cur_ts: u64, + ) -> EyreResult<()> { + // Get our node's current node info and network class and do the right thing + let own_peer_info = self.get_own_peer_info(RoutingDomain::PublicInternet); + let network_class = self.get_network_class(RoutingDomain::PublicInternet); + + // Get routing domain editor + let mut editor = self.edit_routing_domain(RoutingDomain::PublicInternet); + + // Do we know our network class yet? + if let Some(network_class) = network_class { + + // see if we have any routes that need testing + } + + // Commit the changes + editor.commit().await; + + Ok(()) + } +} diff --git a/veilid-core/src/routing_table/tasks/relay_management.rs b/veilid-core/src/routing_table/tasks/relay_management.rs new file mode 100644 index 00000000..85f4fd8a --- /dev/null +++ b/veilid-core/src/routing_table/tasks/relay_management.rs @@ -0,0 +1,83 @@ +use super::super::*; +use crate::xx::*; + +impl RoutingTable { + // Keep relays assigned and accessible + #[instrument(level = "trace", skip(self), err)] + pub(crate) async fn relay_management_task_routine( + self, + _stop_token: StopToken, + _last_ts: u64, + cur_ts: u64, + ) -> EyreResult<()> { + // Get our node's current node info and network class and do the right thing + let own_peer_info = self.get_own_peer_info(RoutingDomain::PublicInternet); + let own_node_info = own_peer_info.signed_node_info.node_info(); + let network_class = self.get_network_class(RoutingDomain::PublicInternet); + + // Get routing domain editor + let mut editor = self.edit_routing_domain(RoutingDomain::PublicInternet); + + // Do we know our network class yet? + if let Some(network_class) = network_class { + // If we already have a relay, see if it is dead, or if we don't need it any more + let has_relay = { + if let Some(relay_node) = self.relay_node(RoutingDomain::PublicInternet) { + let state = relay_node.state(cur_ts); + // Relay node is dead or no longer needed + if matches!(state, BucketEntryState::Dead) { + info!("Relay node died, dropping relay {}", relay_node); + editor.clear_relay_node(); + false + } else if !own_node_info.requires_relay() { + info!( + "Relay node no longer required, dropping relay {}", + relay_node + ); + editor.clear_relay_node(); + false + } else { + true + } + } else { + false + } + }; + + // Do we need a relay? + if !has_relay && own_node_info.requires_relay() { + // Do we want an outbound relay? + let mut got_outbound_relay = false; + if network_class.outbound_wants_relay() { + // The outbound relay is the host of the PWA + if let Some(outbound_relay_peerinfo) = intf::get_outbound_relay_peer().await { + // Register new outbound relay + if let Some(nr) = self.register_node_with_signed_node_info( + RoutingDomain::PublicInternet, + outbound_relay_peerinfo.node_id.key, + outbound_relay_peerinfo.signed_node_info, + false, + ) { + info!("Outbound relay node selected: {}", nr); + editor.set_relay_node(nr); + got_outbound_relay = true; + } + } + } + if !got_outbound_relay { + // Find a node in our routing table that is an acceptable inbound relay + if let Some(nr) = self.find_inbound_relay(RoutingDomain::PublicInternet, cur_ts) + { + info!("Inbound relay node selected: {}", nr); + editor.set_relay_node(nr); + } + } + } + } + + // Commit the changes + editor.commit().await; + + Ok(()) + } +} diff --git a/veilid-core/src/routing_table/tasks.rs b/veilid-core/src/routing_table/tasks/rolling_transfers.rs similarity index 60% rename from veilid-core/src/routing_table/tasks.rs rename to veilid-core/src/routing_table/tasks/rolling_transfers.rs index 52bf843c..b97b8afc 100644 --- a/veilid-core/src/routing_table/tasks.rs +++ b/veilid-core/src/routing_table/tasks/rolling_transfers.rs @@ -1,10 +1,10 @@ -use super::*; +use super::super::*; use crate::xx::*; impl RoutingTable { // Compute transfer statistics to determine how 'fast' a node is #[instrument(level = "trace", skip(self), err)] - pub(super) async fn rolling_transfers_task_routine( + pub(crate) async fn rolling_transfers_task_routine( self, _stop_token: StopToken, last_ts: u64, @@ -38,23 +38,4 @@ impl RoutingTable { Ok(()) } - - // Kick the queued buckets in the routing table to free dead nodes if necessary - // Attempts to keep the size of the routing table down to the bucket depth - #[instrument(level = "trace", skip(self), err)] - pub(super) async fn kick_buckets_task_routine( - self, - _stop_token: StopToken, - _last_ts: u64, - cur_ts: u64, - ) -> EyreResult<()> { - let kick_queue: Vec = core::mem::take(&mut *self.unlocked_inner.kick_queue.lock()) - .into_iter() - .collect(); - let mut inner = self.inner.write(); - for idx in kick_queue { - inner.kick_bucket(idx) - } - Ok(()) - } } diff --git a/veilid-core/src/veilid_api/mod.rs b/veilid-core/src/veilid_api/mod.rs index b56b0277..c8988a4a 100644 --- a/veilid-core/src/veilid_api/mod.rs +++ b/veilid-core/src/veilid_api/mod.rs @@ -320,6 +320,15 @@ pub struct VeilidStateNetwork { pub peers: Vec, } +#[derive( + Debug, Clone, PartialEq, Eq, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct VeilidStateRoute { + pub dead_routes: Vec, + pub dead_remote_routes: Vec, +} + #[derive( Debug, Clone, PartialEq, Eq, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, )] @@ -338,6 +347,7 @@ pub enum VeilidUpdate { Attachment(VeilidStateAttachment), Network(VeilidStateNetwork), Config(VeilidStateConfig), + Route(VeilidStateRoute), Shutdown, } From 79f55f1a0c11f8f13759d6f37ba5bf1816b4c90e Mon Sep 17 00:00:00 2001 From: John Smith Date: Fri, 25 Nov 2022 14:21:55 -0500 Subject: [PATCH 05/88] pr management work --- veilid-cli/src/command_processor.rs | 17 +- veilid-core/src/network_manager/mod.rs | 8 + veilid-core/src/routing_table/mod.rs | 3 + veilid-core/src/routing_table/privacy.rs | 12 + .../src/routing_table/route_spec_store.rs | 331 +++++++++++++++--- .../tasks/private_route_management.rs | 94 ++++- veilid-core/src/rpc_processor/destination.rs | 7 +- veilid-core/src/veilid_api/debug.rs | 27 +- veilid-core/src/veilid_api/mod.rs | 6 +- veilid-core/src/veilid_api/routing_context.rs | 9 +- veilid-flutter/lib/veilid.dart | 40 ++- 11 files changed, 464 insertions(+), 90 deletions(-) diff --git a/veilid-cli/src/command_processor.rs b/veilid-cli/src/command_processor.rs index 81e5e65a..da9d9cb6 100644 --- a/veilid-cli/src/command_processor.rs +++ b/veilid-cli/src/command_processor.rs @@ -405,7 +405,22 @@ reply - reply to an AppCall not handled directly by the server self.inner_mut().ui.set_config(config.config) } pub fn update_route(&mut self, route: veilid_core::VeilidStateRoute) { - //self.inner_mut().ui.set_config(config.config) + let mut out = String::new(); + if !route.dead_routes.is_empty() { + out.push_str(&format!("Dead routes: {:?}", route.dead_routes)); + } + if !route.dead_remote_routes.is_empty() { + if !out.is_empty() { + out.push_str("\n"); + } + out.push_str(&format!( + "Dead remote routes: {:?}", + route.dead_remote_routes + )); + } + if !out.is_empty() { + self.inner().ui.add_node_event(out); + } } pub fn update_log(&mut self, log: veilid_core::VeilidLog) { diff --git a/veilid-core/src/network_manager/mod.rs b/veilid-core/src/network_manager/mod.rs index ba3adccf..f9a22da3 100644 --- a/veilid-core/src/network_manager/mod.rs +++ b/veilid-core/src/network_manager/mod.rs @@ -283,6 +283,14 @@ impl NetworkManager { .connection_manager .clone() } + pub fn update_callback(&self) -> UpdateCallback { + self.unlocked_inner + .update_callback + .read() + .as_ref() + .unwrap() + .clone() + } #[instrument(level = "debug", skip_all, err)] pub async fn init(&self, update_callback: UpdateCallback) -> EyreResult<()> { diff --git a/veilid-core/src/routing_table/mod.rs b/veilid-core/src/routing_table/mod.rs index 1e8e5ea5..c680eb1a 100644 --- a/veilid-core/src/routing_table/mod.rs +++ b/veilid-core/src/routing_table/mod.rs @@ -127,6 +127,9 @@ impl RoutingTable { pub fn rpc_processor(&self) -> RPCProcessor { self.network_manager().rpc_processor() } + pub fn update_callback(&self) -> UpdateCallback { + self.network_manager().update_callback() + } pub fn with_config(&self, f: F) -> R where F: FnOnce(&VeilidConfigInner) -> R, diff --git a/veilid-core/src/routing_table/privacy.rs b/veilid-core/src/routing_table/privacy.rs index a2322baf..e902622f 100644 --- a/veilid-core/src/routing_table/privacy.rs +++ b/veilid-core/src/routing_table/privacy.rs @@ -116,6 +116,18 @@ impl PrivateRoute { PrivateRouteHops::Empty => return None, } } + + pub fn first_hop_node_id(&self) -> Option { + let PrivateRouteHops::FirstHop(pr_first_hop) = &self.hops else { + return None; + }; + + // Get the safety route to use from the spec + Some(match &pr_first_hop.node { + RouteNode::NodeId(n) => n.key, + RouteNode::PeerInfo(p) => p.node_id.key, + }) + } } impl fmt::Display for PrivateRoute { diff --git a/veilid-core/src/routing_table/route_spec_store.rs b/veilid-core/src/routing_table/route_spec_store.rs index 5723fbcc..e0167291 100644 --- a/veilid-core/src/routing_table/route_spec_store.rs +++ b/veilid-core/src/routing_table/route_spec_store.rs @@ -8,6 +8,8 @@ use rkyv::{ const REMOTE_PRIVATE_ROUTE_CACHE_SIZE: usize = 1024; /// Remote private route cache entries expire in 5 minutes if they haven't been used const REMOTE_PRIVATE_ROUTE_CACHE_EXPIRY: u64 = 300_000_000u64; +/// Amount of time a route can remain idle before it gets tested +const ROUTE_MIN_IDLE_TIME_MS: u32 = 30_000; /// Compiled route (safety route + private route) #[derive(Clone, Debug)] @@ -32,25 +34,25 @@ pub struct KeyPair { pub struct RouteStats { /// Consecutive failed to send count #[with(Skip)] - failed_to_send: u32, + pub failed_to_send: u32, /// Questions lost #[with(Skip)] - questions_lost: u32, + pub questions_lost: u32, /// Timestamp of when the route was created - created_ts: u64, + pub created_ts: u64, /// Timestamp of when the route was last checked for validity #[with(Skip)] - last_tested_ts: Option, + pub last_tested_ts: Option, /// Timestamp of when the route was last sent to #[with(Skip)] - last_sent_ts: Option, + pub last_sent_ts: Option, /// Timestamp of when the route was last received over #[with(Skip)] - last_received_ts: Option, + pub last_received_ts: Option, /// Transfers up and down - transfer_stats_down_up: TransferStatsDownUp, + pub transfer_stats_down_up: TransferStatsDownUp, /// Latency stats - latency_stats: LatencyStats, + pub latency_stats: LatencyStats, /// Accounting mechanism for this route's RPC latency #[with(Skip)] latency_stats_accounting: LatencyStatsAccounting, @@ -129,6 +131,28 @@ impl RouteStats { self.last_sent_ts = None; self.last_received_ts = None; } + + /// Check if a route needs testing + pub fn needs_testing(&self, cur_ts: u64) -> bool { + // Has the route had any failures lately? + if self.questions_lost > 0 || self.failed_to_send > 0 { + // If so, always test + return true; + } + + // Has the route been tested within the idle time we'd want to check things? + // (also if we've received successfully over the route, this will get set) + if let Some(last_tested_ts) = self.last_tested_ts { + if cur_ts.saturating_sub(last_tested_ts) > (ROUTE_MIN_IDLE_TIME_MS as u64 * 1000u64) { + return true; + } + } else { + // If this route has never been tested, it needs to be + return true; + } + + false + } } #[derive(Clone, Debug, RkyvArchive, RkyvSerialize, RkyvDeserialize)] @@ -157,6 +181,15 @@ pub struct RouteSpecDetail { stats: RouteStats, } +impl RouteSpecDetail { + pub fn get_stats(&self) -> &RouteStats { + &self.stats + } + pub fn get_stats_mut(&mut self) -> &mut RouteStats { + &mut self.stats + } +} + /// The core representation of the RouteSpecStore that can be serialized #[derive(Debug, Clone, Default, RkyvArchive, RkyvSerialize, RkyvDeserialize)] #[archive_attr(repr(C), derive(CheckBytes))] @@ -178,6 +211,15 @@ pub struct RemotePrivateRouteInfo { stats: RouteStats, } +impl RemotePrivateRouteInfo { + pub fn get_stats(&self) -> &RouteStats { + &self.stats + } + pub fn get_stats_mut(&mut self) -> &mut RouteStats { + &mut self.stats + } +} + /// Ephemeral data used to help the RouteSpecStore operate efficiently #[derive(Debug)] pub struct RouteSpecStoreCache { @@ -189,6 +231,10 @@ pub struct RouteSpecStoreCache { hop_cache: HashSet>, /// Has a remote private route responded to a question and when remote_private_route_cache: LruCache, + /// List of dead allocated routes + dead_routes: Vec, + /// List of dead remote routes + dead_remote_routes: Vec, } impl Default for RouteSpecStoreCache { @@ -198,6 +244,8 @@ impl Default for RouteSpecStoreCache { used_end_nodes: Default::default(), hop_cache: Default::default(), remote_private_route_cache: LruCache::new(REMOTE_PRIVATE_ROUTE_CACHE_SIZE), + dead_routes: Default::default(), + dead_remote_routes: Default::default(), } } } @@ -341,6 +389,7 @@ impl RouteSpecStore { } } + #[instrument(level = "trace", skip(routing_table), err)] pub async fn load(routing_table: RoutingTable) -> EyreResult { let (max_route_hop_count, default_route_hop_count) = { let config = routing_table.network_manager().config(); @@ -413,6 +462,7 @@ impl RouteSpecStore { Ok(rss) } + #[instrument(level = "trace", skip(self), err)] pub async fn save(&self) -> EyreResult<()> { let content = { let inner = self.inner.lock(); @@ -448,6 +498,29 @@ impl RouteSpecStore { Ok(()) } + #[instrument(level = "trace", skip(self))] + pub fn send_route_update(&self) { + let update_callback = self.unlocked_inner.routing_table.update_callback(); + + let (dead_routes, dead_remote_routes) = { + let mut inner = self.inner.lock(); + if inner.cache.dead_routes.is_empty() && inner.cache.dead_remote_routes.is_empty() { + // Nothing to do + return; + } + let dead_routes = core::mem::take(&mut inner.cache.dead_routes); + let dead_remote_routes = core::mem::take(&mut inner.cache.dead_remote_routes); + (dead_routes, dead_remote_routes) + }; + + let update = VeilidUpdate::Route(VeilidStateRoute { + dead_routes, + dead_remote_routes, + }); + + update_callback(update); + } + fn add_to_cache(cache: &mut RouteSpecStoreCache, cache_key: Vec, rsd: &RouteSpecDetail) { if !cache.hop_cache.insert(cache_key) { panic!("route should never be inserted twice"); @@ -500,6 +573,7 @@ impl RouteSpecStore { /// Prefers nodes that are not currently in use by another route /// The route is not yet tested for its reachability /// Returns None if no route could be allocated at this time + #[instrument(level = "trace", skip(self), ret, err)] pub fn allocate_route( &self, stability: Stability, @@ -523,6 +597,7 @@ impl RouteSpecStore { ) } + #[instrument(level = "trace", skip(self, inner, rti), ret, err)] fn allocate_route_inner( &self, inner: &mut RouteSpecStoreInner, @@ -789,6 +864,7 @@ impl RouteSpecStore { Ok(Some(public_key)) } + #[instrument(level = "trace", skip(self, data), ret, err)] pub fn validate_signatures( &self, public_key: &DHTKey, @@ -835,10 +911,8 @@ impl RouteSpecStore { ))) } - /// Test an allocated route for continuity - pub async fn test_route(&self, key: &DHTKey) -> EyreResult { - let rpc_processor = self.unlocked_inner.routing_table.rpc_processor(); - + #[instrument(level = "trace", skip(self), ret, err)] + async fn test_allocated_route(&self, key: &DHTKey) -> EyreResult { // Make loopback route to test with let dest = { let private_route = self.assemble_private_route(key, None)?; @@ -864,6 +938,7 @@ impl RouteSpecStore { }; // Test with double-round trip ping to self + let rpc_processor = self.unlocked_inner.routing_table.rpc_processor(); let _res = match rpc_processor.rpc_call_status(dest).await? { NetworkResult::Value(v) => v, _ => { @@ -875,13 +950,71 @@ impl RouteSpecStore { Ok(true) } - /// Release an allocated route that is no longer in use - pub fn release_route(&self, public_key: DHTKey) -> EyreResult<()> { - let mut inner = self.inner.lock(); - let Some(detail) = inner.content.details.remove(&public_key) else { - bail!("can't release route that was never allocated"); + #[instrument(level = "trace", skip(self), ret, err)] + async fn test_remote_route(&self, key: &DHTKey) -> EyreResult { + // Make private route test + let dest = { + // Get the route to test + let private_route = match self.peek_remote_private_route(key) { + Some(pr) => pr, + None => return Ok(false), + }; + + // Get a safety route that is good enough + let safety_spec = SafetySpec { + preferred_route: None, + hop_count: self.unlocked_inner.default_route_hop_count, + stability: Stability::LowLatency, + sequencing: Sequencing::NoPreference, + }; + + let safety_selection = SafetySelection::Safe(safety_spec); + + Destination::PrivateRoute { + private_route, + safety_selection, + } }; + // Test with double-round trip ping to self + let rpc_processor = self.unlocked_inner.routing_table.rpc_processor(); + let _res = match rpc_processor.rpc_call_status(dest).await? { + NetworkResult::Value(v) => v, + _ => { + // Did not error, but did not come back, just return false + return Ok(false); + } + }; + + Ok(true) + } + + /// Test an allocated route for continuity + #[instrument(level = "trace", skip(self), ret, err)] + pub async fn test_route(&self, key: &DHTKey) -> EyreResult { + let is_remote = { + let inner = &mut *self.inner.lock(); + let cur_ts = intf::get_timestamp(); + Self::with_peek_remote_private_route(inner, cur_ts, key, |_| {}).is_some() + }; + if is_remote { + self.test_remote_route(key).await + } else { + self.test_allocated_route(key).await + } + } + + /// Release an allocated route that is no longer in use + #[instrument(level = "trace", skip(self), ret)] + fn release_allocated_route(&self, public_key: &DHTKey) -> bool { + let mut inner = self.inner.lock(); + let Some(detail) = inner.content.details.remove(public_key) else { + return false; + }; + + // Mark it as dead for the update + inner.cache.dead_routes.push(*public_key); + // Remove from hop cache let cache_key = route_hops_to_hop_cache(&detail.hops); if !inner.cache.hop_cache.remove(&cache_key) { @@ -917,11 +1050,27 @@ impl RouteSpecStore { panic!("used_end_nodes cache should have contained hop"); } } - Ok(()) + true + } + + /// Release an allocated or remote route that is no longer in use + #[instrument(level = "trace", skip(self), ret)] + pub fn release_route(&self, key: &DHTKey) -> bool { + let is_remote = { + let inner = &mut *self.inner.lock(); + let cur_ts = intf::get_timestamp(); + Self::with_peek_remote_private_route(inner, cur_ts, key, |_| {}).is_some() + }; + if is_remote { + self.release_remote_private_route(key) + } else { + self.release_allocated_route(key) + } } /// Find first matching unpublished route that fits into the selection criteria - fn first_unpublished_route_inner<'a>( + /// Don't pick any routes that have failed and haven't been tested yet + fn first_available_route_inner<'a>( inner: &'a RouteSpecStoreInner, min_hop_count: usize, max_hop_count: usize, @@ -930,6 +1079,7 @@ impl RouteSpecStore { directions: DirectionSet, avoid_node_ids: &[DHTKey], ) -> Option { + let cur_ts = intf::get_timestamp(); for detail in &inner.content.details { if detail.1.stability >= stability && detail.1.sequencing >= sequencing @@ -937,6 +1087,7 @@ impl RouteSpecStore { && detail.1.hops.len() <= max_hop_count && detail.1.directions.is_subset(directions) && !detail.1.published + && !detail.1.stats.needs_testing(cur_ts) { let mut avoid = false; for h in &detail.1.hops { @@ -953,19 +1104,47 @@ impl RouteSpecStore { None } - /// List all routes - pub fn list_routes(&self) -> Vec { + /// List all allocated routes + pub fn list_allocated_routes(&self, mut filter: F) -> Vec + where + F: FnMut(&DHTKey, &RouteSpecDetail) -> Option, + { let inner = self.inner.lock(); let mut out = Vec::with_capacity(inner.content.details.len()); for detail in &inner.content.details { - out.push(*detail.0); + if let Some(x) = filter(detail.0, detail.1) { + out.push(x); + } + } + out + } + + /// List all allocated routes + pub fn list_remote_routes(&self, mut filter: F) -> Vec + where + F: FnMut(&DHTKey, &RemotePrivateRouteInfo) -> Option, + { + let inner = self.inner.lock(); + let mut out = Vec::with_capacity(inner.cache.remote_private_route_cache.len()); + for info in &inner.cache.remote_private_route_cache { + if let Some(x) = filter(info.0, info.1) { + out.push(x); + } } out } /// Get the debug description of a route pub fn debug_route(&self, key: &DHTKey) -> Option { - let inner = &*self.inner.lock(); + let inner = &mut *self.inner.lock(); + let cur_ts = intf::get_timestamp(); + // If this is a remote route, print it + if let Some(s) = + Self::with_peek_remote_private_route(inner, cur_ts, key, |rpi| format!("{:#?}", rpi)) + { + return Some(s); + } + // Otherwise check allocated routes Self::detail(inner, key).map(|rsd| format!("{:#?}", rsd)) } @@ -1028,19 +1207,13 @@ impl RouteSpecStore { } }; - let PrivateRouteHops::FirstHop(pr_first_hop) = &private_route.hops else { - bail!("compiled private route should have first hop"); - }; - // If the safety route requested is also the private route, this is a loopback test, just accept it let sr_pubkey = if safety_spec.preferred_route == Some(private_route.public_key) { // Private route is also safety route during loopback test private_route.public_key } else { - // Get the safety route to use from the spec - let avoid_node_id = match &pr_first_hop.node { - RouteNode::NodeId(n) => n.key, - RouteNode::PeerInfo(p) => p.node_id.key, + let Some(avoid_node_id) = private_route.first_hop_node_id() else { + bail!("compiled private route should have first hop"); }; let Some(sr_pubkey) = self.get_route_for_safety_spec_inner(inner, rti, &safety_spec, Direction::Outbound.into(), &[avoid_node_id])? else { // No safety route could be found for this spec @@ -1176,6 +1349,7 @@ impl RouteSpecStore { } /// Get a route that matches a particular safety spec + #[instrument(level = "trace", skip(self, inner, rti), ret, err)] fn get_route_for_safety_spec_inner( &self, inner: &mut RouteSpecStoreInner, @@ -1204,7 +1378,7 @@ impl RouteSpecStore { } // Select a safety route from the pool or make one if we don't have one that matches - let sr_pubkey = if let Some(sr_pubkey) = Self::first_unpublished_route_inner( + let sr_pubkey = if let Some(sr_pubkey) = Self::first_available_route_inner( inner, safety_spec.hop_count, safety_spec.hop_count, @@ -1238,6 +1412,7 @@ impl RouteSpecStore { } /// Get a private sroute to use for the answer to question + #[instrument(level = "trace", skip(self), ret, err)] pub fn get_private_route_for_safety_spec( &self, safety_spec: &SafetySpec, @@ -1257,6 +1432,7 @@ impl RouteSpecStore { } /// Assemble private route for publication + #[instrument(level = "trace", skip(self), err)] pub fn assemble_private_route( &self, key: &DHTKey, @@ -1341,30 +1517,59 @@ impl RouteSpecStore { } /// Import a remote private route for compilation + #[instrument(level = "trace", skip(self, blob), ret, err)] pub fn import_remote_private_route(&self, blob: Vec) -> EyreResult { // decode the pr blob let private_route = RouteSpecStore::blob_to_private_route(blob)?; - // store the private route in our cache - let inner = &mut *self.inner.lock(); - let cur_ts = intf::get_timestamp(); + // ensure private route has first hop + if !matches!(private_route.hops, PrivateRouteHops::FirstHop(_)) { + bail!("private route must have first hop"); + } + // ensure this isn't also an allocated route + let inner = &mut *self.inner.lock(); + if Self::detail(inner, &private_route.public_key).is_some() { + bail!("should not import allocated route"); + } + + // store the private route in our cache + let cur_ts = intf::get_timestamp(); let key = Self::with_create_remote_private_route(inner, cur_ts, private_route, |r| { r.private_route.as_ref().unwrap().public_key.clone() }); Ok(key) } + /// Release a remote private route that is no longer in use + #[instrument(level = "trace", skip(self), ret)] + fn release_remote_private_route(&self, key: &DHTKey) -> bool { + let inner = &mut *self.inner.lock(); + if inner.cache.remote_private_route_cache.remove(key).is_some() { + // Mark it as dead for the update + inner.cache.dead_remote_routes.push(*key); + true + } else { + false + } + } + /// Retrieve an imported remote private route by its public key - pub fn get_remote_private_route(&self, key: &DHTKey) -> EyreResult { + pub fn get_remote_private_route(&self, key: &DHTKey) -> Option { let inner = &mut *self.inner.lock(); let cur_ts = intf::get_timestamp(); - let Some(pr) = Self::with_get_remote_private_route(inner, cur_ts, key, |r| { + Self::with_get_remote_private_route(inner, cur_ts, key, |r| { r.private_route.as_ref().unwrap().clone() - }) else { - bail!("remote private route not found"); - }; - Ok(pr) + }) + } + + /// Retrieve an imported remote private route by its public key but don't 'touch' it + pub fn peek_remote_private_route(&self, key: &DHTKey) -> Option { + let inner = &mut *self.inner.lock(); + let cur_ts = intf::get_timestamp(); + Self::with_peek_remote_private_route(inner, cur_ts, key, |r| { + r.private_route.as_ref().unwrap().clone() + }) } // get or create a remote private route cache entry @@ -1401,7 +1606,19 @@ impl RouteSpecStore { last_touched_ts: cur_ts, stats: RouteStats::new(cur_ts), }); - f(rpr) + + let out = f(rpr); + + // Ensure we LRU out items + if inner.cache.remote_private_route_cache.len() + > inner.cache.remote_private_route_cache.capacity() + { + let (dead_k, _) = inner.cache.remote_private_route_cache.remove_lru().unwrap(); + // Mark it as dead for the update + inner.cache.dead_remote_routes.push(dead_k); + } + + out } // get a remote private route cache entry @@ -1420,16 +1637,41 @@ impl RouteSpecStore { return Some(f(rpr)); } inner.cache.remote_private_route_cache.remove(key); + inner.cache.dead_remote_routes.push(*key); None } + // peek a remote private route cache entry + fn with_peek_remote_private_route( + inner: &mut RouteSpecStoreInner, + cur_ts: u64, + key: &DHTKey, + f: F, + ) -> Option + where + F: FnOnce(&mut RemotePrivateRouteInfo) -> R, + { + match inner.cache.remote_private_route_cache.entry(*key) { + hashlink::lru_cache::Entry::Occupied(mut o) => { + let rpr = o.get_mut(); + if cur_ts - rpr.last_touched_ts < REMOTE_PRIVATE_ROUTE_CACHE_EXPIRY { + return Some(f(rpr)); + } + o.remove(); + inner.cache.dead_remote_routes.push(*key); + None + } + hashlink::lru_cache::Entry::Vacant(_) => None, + } + } + /// Check to see if this remote (not ours) private route has seen our node info yet /// This returns true if we have sent non-safety-route node info to the /// private route and gotten a response before pub fn has_remote_private_route_seen_our_node_info(&self, key: &DHTKey) -> bool { let inner = &mut *self.inner.lock(); let cur_ts = intf::get_timestamp(); - Self::with_get_remote_private_route(inner, cur_ts, key, |rpr| rpr.seen_our_node_info) + Self::with_peek_remote_private_route(inner, cur_ts, key, |rpr| rpr.seen_our_node_info) .unwrap_or_default() } @@ -1468,7 +1710,7 @@ impl RouteSpecStore { } // Check for remote route if let Some(res) = - Self::with_get_remote_private_route(inner, cur_ts, key, |rpr| f(&mut rpr.stats)) + Self::with_peek_remote_private_route(inner, cur_ts, key, |rpr| f(&mut rpr.stats)) { return Some(res); } @@ -1478,6 +1720,7 @@ impl RouteSpecStore { } /// Clear caches when local our local node info changes + #[instrument(level = "trace", skip(self))] pub fn reset(&self) { let inner = &mut *self.inner.lock(); diff --git a/veilid-core/src/routing_table/tasks/private_route_management.rs b/veilid-core/src/routing_table/tasks/private_route_management.rs index e1770a1e..3718366f 100644 --- a/veilid-core/src/routing_table/tasks/private_route_management.rs +++ b/veilid-core/src/routing_table/tasks/private_route_management.rs @@ -1,33 +1,97 @@ use super::super::*; use crate::xx::*; -use futures_util::stream::{FuturesOrdered, StreamExt}; +use futures_util::stream::{FuturesUnordered, StreamExt}; +use futures_util::FutureExt; use stop_token::future::FutureExt as StopFutureExt; impl RoutingTable { - // Keep private routes assigned and accessible - #[instrument(level = "trace", skip(self), err)] + /// Keep private routes assigned and accessible + #[instrument(level = "trace", skip(self, stop_token), err)] pub(crate) async fn private_route_management_task_routine( self, - _stop_token: StopToken, + stop_token: StopToken, _last_ts: u64, cur_ts: u64, ) -> EyreResult<()> { // Get our node's current node info and network class and do the right thing - let own_peer_info = self.get_own_peer_info(RoutingDomain::PublicInternet); - let network_class = self.get_network_class(RoutingDomain::PublicInternet); + let network_class = self + .get_network_class(RoutingDomain::PublicInternet) + .unwrap_or(NetworkClass::Invalid); - // Get routing domain editor - let mut editor = self.edit_routing_domain(RoutingDomain::PublicInternet); - - // Do we know our network class yet? - if let Some(network_class) = network_class { - - // see if we have any routes that need testing + // If we don't know our network class then don't do this yet + if network_class == NetworkClass::Invalid { + return Ok(()); } - // Commit the changes - editor.commit().await; + // Collect any routes that need that need testing + let rss = self.route_spec_store(); + let mut routes_needing_testing = rss.list_allocated_routes(|k, v| { + let stats = v.get_stats(); + if stats.needs_testing(cur_ts) { + return Some(*k); + } else { + return None; + } + }); + let mut remote_routes_needing_testing = rss.list_remote_routes(|k, v| { + let stats = v.get_stats(); + if stats.needs_testing(cur_ts) { + return Some(*k); + } else { + return None; + } + }); + routes_needing_testing.append(&mut remote_routes_needing_testing); + + // Test all the routes that need testing at the same time + #[derive(Default, Debug)] + struct TestRouteContext { + failed: bool, + dead_routes: Vec, + } + + if !routes_needing_testing.is_empty() { + let mut unord = FuturesUnordered::new(); + let ctx = Arc::new(Mutex::new(TestRouteContext::default())); + for r in routes_needing_testing { + let rss = rss.clone(); + let ctx = ctx.clone(); + unord.push( + async move { + let success = match rss.test_route(&r).await { + Ok(v) => v, + Err(e) => { + log_rtab!(error "test route failed: {}", e); + ctx.lock().failed = true; + return; + } + }; + if success { + // Route is okay, leave it alone + return; + } + // Route test failed + ctx.lock().dead_routes.push(r); + } + .instrument(Span::current()) + .boxed(), + ); + } + + // Wait for test_route futures to complete in parallel + while let Ok(Some(_)) = unord.next().timeout_at(stop_token.clone()).await {} + + // Process failed routes + let ctx = &mut *ctx.lock(); + for r in &ctx.dead_routes { + log_rtab!(debug "Dead route: {}", &r); + rss.release_route(r); + } + } + + // Send update (also may send updates for released routes done by other parts of the program) + rss.send_route_update(); Ok(()) } diff --git a/veilid-core/src/rpc_processor/destination.rs b/veilid-core/src/rpc_processor/destination.rs index dd63776a..3c119cff 100644 --- a/veilid-core/src/rpc_processor/destination.rs +++ b/veilid-core/src/rpc_processor/destination.rs @@ -205,7 +205,7 @@ impl RPCProcessor { private_route, safety_selection, } => { - let PrivateRouteHops::FirstHop(pr_first_hop) = &private_route.hops else { + let Some(avoid_node_id) = private_route.first_hop_node_id() else { return Err(RPCError::internal("destination private route must have first hop")); }; @@ -238,11 +238,6 @@ impl RPCProcessor { private_route.public_key } else { // Get the privat route to respond to that matches the safety route spec we sent the request with - let avoid_node_id = match &pr_first_hop.node { - RouteNode::NodeId(n) => n.key, - RouteNode::PeerInfo(p) => p.node_id.key, - }; - let Some(pr_key) = rss .get_private_route_for_safety_spec(safety_spec, &[avoid_node_id]) .map_err(RPCError::internal)? else { diff --git a/veilid-core/src/veilid_api/debug.rs b/veilid-core/src/veilid_api/debug.rs index ebade9d2..4119e3ef 100644 --- a/veilid-core/src/veilid_api/debug.rs +++ b/veilid-core/src/veilid_api/debug.rs @@ -34,13 +34,13 @@ fn get_route_id(rss: RouteSpecStore) -> impl Fn(&str) -> Option { return move |text: &str| { match DHTKey::try_decode(text).ok() { Some(key) => { - let routes = rss.list_routes(); + let routes = rss.list_allocated_routes(|k, _| Some(*k)); if routes.contains(&key) { return Some(key); } } None => { - let routes = rss.list_routes(); + let routes = rss.list_allocated_routes(|k, _| Some(*k)); for r in routes { let rkey = r.encode(); if rkey.starts_with(text) { @@ -126,14 +126,11 @@ fn get_destination(routing_table: RoutingTable) -> impl FnOnce(&str) -> Option { - // Remove imported route - dc.imported_routes.remove(n); - info!("removed dead imported route {}", n); - return None; - } - Ok(v) => v, + let Some(private_route) = rss.get_remote_private_route(&pr_pubkey) else { + // Remove imported route + dc.imported_routes.remove(n); + info!("removed dead imported route {}", n); + return None; }; Some(Destination::private_route( private_route, @@ -636,11 +633,9 @@ impl VeilidAPI { let route_id = get_debug_argument_at(&args, 1, "debug_route", "route_id", get_dht_key)?; // Release route - let out = match rss.release_route(route_id) { - Ok(()) => format!("Released"), - Err(e) => { - format!("Route release failed: {}", e) - } + let out = match rss.release_route(&route_id) { + true => "Released".to_owned(), + false => "Route does not exist".to_owned(), }; Ok(out) @@ -730,7 +725,7 @@ impl VeilidAPI { let routing_table = netman.routing_table(); let rss = routing_table.route_spec_store(); - let routes = rss.list_routes(); + let routes = rss.list_allocated_routes(|k, _| Some(*k)); let mut out = format!("Routes: (count = {}):\n", routes.len()); for r in routes { out.push_str(&format!("{}\n", r.encode())); diff --git a/veilid-core/src/veilid_api/mod.rs b/veilid-core/src/veilid_api/mod.rs index c8988a4a..6a9ce931 100644 --- a/veilid-core/src/veilid_api/mod.rs +++ b/veilid-core/src/veilid_api/mod.rs @@ -2789,8 +2789,7 @@ impl VeilidAPI { .await .map_err(VeilidAPIError::no_connection)? { - rss.release_route(pr_pubkey) - .map_err(VeilidAPIError::generic)?; + rss.release_route(&pr_pubkey); return Err(VeilidAPIError::generic("allocated route failed to test")); } let private_route = rss @@ -2799,8 +2798,7 @@ impl VeilidAPI { let blob = match RouteSpecStore::private_route_to_blob(&private_route) { Ok(v) => v, Err(e) => { - rss.release_route(pr_pubkey) - .map_err(VeilidAPIError::generic)?; + rss.release_route(&pr_pubkey); return Err(VeilidAPIError::internal(e)); } }; diff --git a/veilid-core/src/veilid_api/routing_context.rs b/veilid-core/src/veilid_api/routing_context.rs index 8e17b7de..f33ce133 100644 --- a/veilid-core/src/veilid_api/routing_context.rs +++ b/veilid-core/src/veilid_api/routing_context.rs @@ -129,9 +129,12 @@ impl RoutingContext { Target::PrivateRoute(pr) => { // Get remote private route let rss = self.api.routing_table()?.route_spec_store(); - let private_route = rss - .get_remote_private_route(&pr) - .map_err(|_| VeilidAPIError::KeyNotFound { key: pr })?; + let Some(private_route) = rss + .get_remote_private_route(&pr) + else { + return Err(VeilidAPIError::KeyNotFound { key: pr }); + }; + Ok(rpc_processor::Destination::PrivateRoute { private_route, safety_selection: self.unlocked_inner.safety_selection, diff --git a/veilid-flutter/lib/veilid.dart b/veilid-flutter/lib/veilid.dart index 4c539782..77c26268 100644 --- a/veilid-flutter/lib/veilid.dart +++ b/veilid-flutter/lib/veilid.dart @@ -1266,6 +1266,10 @@ abstract class VeilidUpdate { { return VeilidUpdateConfig(state: VeilidStateConfig.fromJson(json)); } + case "Route": + { + return VeilidUpdateRoute(state: VeilidStateRoute.fromJson(json)); + } default: { throw VeilidAPIExceptionInternal( @@ -1380,6 +1384,19 @@ class VeilidUpdateConfig implements VeilidUpdate { } } +class VeilidUpdateRoute implements VeilidUpdate { + final VeilidStateRoute state; + // + VeilidUpdateRoute({required this.state}); + + @override + Map get json { + var jsonRep = state.json; + jsonRep['kind'] = "Route"; + return jsonRep; + } +} + ////////////////////////////////////// /// VeilidStateAttachment @@ -1444,7 +1461,28 @@ class VeilidStateConfig { : config = jsonDecode(json['config']); Map get json { - return {'config': jsonEncode(config)}; + return {'config': config}; + } +} + +////////////////////////////////////// +/// VeilidStateRoute + +class VeilidStateRoute { + final List deadRoutes; + final List deadRemoteRoutes; + + VeilidStateRoute({ + required this.deadRoutes, + required this.deadRemoteRoutes, + }); + + VeilidStateRoute.fromJson(Map json) + : deadRoutes = jsonDecode(json['dead_routes']), + deadRemoteRoutes = jsonDecode(json['dead_remote_routes']); + + Map get json { + return {'dead_routes': deadRoutes, 'dead_remote_routes': deadRemoteRoutes}; } } From 25ace50d459130a7e50c6b3d991ede17969614a6 Mon Sep 17 00:00:00 2001 From: John Smith Date: Sat, 26 Nov 2022 14:16:02 -0500 Subject: [PATCH 06/88] break everything --- Cargo.lock | 32 +-- veilid-core/Cargo.toml | 13 +- veilid-core/src/crypto/envelope.rs | 34 ++- veilid-core/src/crypto/receipt.rs | 31 +-- veilid-core/src/intf/wasm/protected_store.rs | 50 ++-- .../src/routing_table/route_spec_store.rs | 4 +- veilid-core/src/veilid_api/debug.rs | 12 +- veilid-core/src/veilid_api/mod.rs | 122 +++++----- veilid-core/src/veilid_api/routing_context.rs | 33 +-- veilid-core/src/veilid_config.rs | 6 +- veilid-core/src/xx/ip_extra.rs | 1 - veilid-core/src/xx/mod.rs | 7 +- veilid-flutter/lib/veilid.dart | 80 ++++++- veilid-flutter/lib/veilid_ffi.dart | 207 +++++++++++++++- veilid-flutter/lib/veilid_js.dart | 98 +++++++- veilid-flutter/rust/src/dart_ffi.rs | 224 +++++++++++++++++- veilid-wasm/src/lib.rs | 26 +- 17 files changed, 760 insertions(+), 220 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df0254c1..a9eb7648 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3145,14 +3145,6 @@ dependencies = [ "pin-utils", ] -[[package]] -name = "no-std-net" -version = "0.6.0" -dependencies = [ - "serde", - "serde_test", -] - [[package]] name = "nom" version = "5.1.2" @@ -4140,7 +4132,7 @@ dependencies = [ [[package]] name = "rkyv" version = "0.7.39" -source = "git+https://github.com/crioux/rkyv.git?branch=issue_326#2f19cfac9f31a15e2fe74ad362eec7b011dc35b9" +source = "git+https://github.com/rkyv/rkyv.git?rev=57e2a8d#57e2a8daff3e6381e170e723ed1beea5c113b232" dependencies = [ "bytecheck", "hashbrown", @@ -4153,7 +4145,7 @@ dependencies = [ [[package]] name = "rkyv_derive" version = "0.7.39" -source = "git+https://github.com/crioux/rkyv.git?branch=issue_326#2f19cfac9f31a15e2fe74ad362eec7b011dc35b9" +source = "git+https://github.com/rkyv/rkyv.git?rev=57e2a8d#57e2a8daff3e6381e170e723ed1beea5c113b232" dependencies = [ "proc-macro2", "quote", @@ -4461,15 +4453,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_bytes" -version = "0.11.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc50e8183eeeb6178dcb167ae34a8051d63535023ae38b5d8d12beae193d37b" -dependencies = [ - "serde", -] - [[package]] name = "serde_cbor" version = "0.11.2" @@ -4513,15 +4496,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_test" -version = "1.0.147" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "641666500e4e6fba7b91b73651a375cb53579468ab3c38389289b802797cad94" -dependencies = [ - "serde", -] - [[package]] name = "serde_yaml" version = "0.9.14" @@ -5615,7 +5589,6 @@ dependencies = [ "ndk 0.6.0", "ndk-glue", "nix 0.25.0", - "no-std-net", "once_cell", "owning_ref", "owo-colors", @@ -5631,7 +5604,6 @@ dependencies = [ "send_wrapper 0.6.0", "serde", "serde-big-array", - "serde_bytes", "serde_json", "serial_test", "simplelog 0.12.0", diff --git a/veilid-core/Cargo.toml b/veilid-core/Cargo.toml index e0ec79c8..3e0ebf31 100644 --- a/veilid-core/Cargo.toml +++ b/veilid-core/Cargo.toml @@ -34,6 +34,8 @@ secrecy = "^0" chacha20poly1305 = "^0" chacha20 = "^0" hashlink = { path = "../external/hashlink", features = ["serde_impl"] } +serde = { version = "^1", features = ["derive" ] } +serde_json = { version = "^1" } serde-big-array = "^0" futures-util = { version = "^0", default_features = false, features = ["alloc"] } parking_lot = "^0" @@ -59,9 +61,8 @@ rtnetlink = { version = "^0", default-features = false, optional = true } async-std-resolver = { version = "^0", optional = true } trust-dns-resolver = { version = "^0", optional = true } keyvaluedb = { path = "../external/keyvaluedb/keyvaluedb" } -serde_bytes = { version = "^0" } -#rkyv = { version = "^0", default_features = false, features = ["std", "alloc", "strict", "size_64", "validation"] } -rkyv = { git = "https://github.com/crioux/rkyv.git", branch = "issue_326", default_features = false, features = ["std", "alloc", "strict", "size_64", "validation"] } +#rkyv = { version = "^0", default_features = false, features = ["std", "alloc", "strict", "size_32", "validation"] } +rkyv = { git = "https://github.com/rkyv/rkyv.git", rev = "57e2a8d", default_features = false, features = ["std", "alloc", "strict", "size_32", "validation"] } bytecheck = "^0" # Dependencies for native builds only @@ -85,8 +86,7 @@ rustls-pemfile = "^0.2" futures-util = { version = "^0", default-features = false, features = ["async-await", "sink", "std", "io"] } keyvaluedb-sqlite = { path = "../external/keyvaluedb/keyvaluedb-sqlite" } data-encoding = { version = "^2" } -serde = { version = "^1", features = ["derive" ] } -serde_json = { version = "^1" } + socket2 = "^0" bugsalot = "^0" chrono = "^0" @@ -98,11 +98,8 @@ nix = "^0" wasm-bindgen = "^0" js-sys = "^0" wasm-bindgen-futures = "^0" -no-std-net = { path = "../external/no-std-net", features = ["serde"] } keyvaluedb-web = { path = "../external/keyvaluedb/keyvaluedb-web" } data-encoding = { version = "^2", default_features = false, features = ["alloc"] } -serde = { version = "^1", default-features = false, features = ["derive", "alloc"] } -serde_json = { version = "^1", default-features = false, features = ["alloc"] } getrandom = { version = "^0", features = ["js"] } ws_stream_wasm = "^0" async_executors = { version = "^0", default-features = false, features = [ "bindgen", "timer" ]} diff --git a/veilid-core/src/crypto/envelope.rs b/veilid-core/src/crypto/envelope.rs index 32a02447..ea84e5c6 100644 --- a/veilid-core/src/crypto/envelope.rs +++ b/veilid-core/src/crypto/envelope.rs @@ -76,7 +76,7 @@ impl Envelope { // Ensure we are at least the length of the envelope // Silent drop here, as we use zero length packets as part of the protocol for hole punching if data.len() < MIN_ENVELOPE_SIZE { - return Err(VeilidAPIError::generic("envelope data too small")); + apibail_generic!("envelope data too small"); } // Verify magic number @@ -84,31 +84,28 @@ impl Envelope { .try_into() .map_err(VeilidAPIError::internal)?; if magic != *ENVELOPE_MAGIC { - return Err(VeilidAPIError::generic("bad magic number")); + apibail_generic!("bad magic number"); } // Check version let version = data[0x04]; if version > MAX_CRYPTO_VERSION || version < MIN_CRYPTO_VERSION { - return Err(VeilidAPIError::parse_error( - "unsupported cryptography version", - version, - )); + apibail_parse_error!("unsupported cryptography version", version); } // Get min version let min_version = data[0x05]; if min_version > version { - return Err(VeilidAPIError::parse_error("version too low", version)); + apibail_parse_error!("version too low", version); } // Get max version let max_version = data[0x06]; if version > max_version { - return Err(VeilidAPIError::parse_error("version too high", version)); + apibail_parse_error!("version too high", version); } if min_version > max_version { - return Err(VeilidAPIError::generic("version information invalid")); + apibail_generic!("version information invalid"); } // Get size and ensure it matches the size of the envelope and is less than the maximum message size @@ -118,17 +115,17 @@ impl Envelope { .map_err(VeilidAPIError::internal)?, ); if (size as usize) > MAX_ENVELOPE_SIZE { - return Err(VeilidAPIError::parse_error("envelope too large", size)); + apibail_parse_error!("envelope too large", size); } if (size as usize) != data.len() { - return Err(VeilidAPIError::parse_error( + apibail_parse_error!( "size doesn't match envelope size", format!( "size doesn't match envelope size: size={} data.len()={}", size, data.len() - ), - )); + ) + ); } // Get the timestamp @@ -153,10 +150,10 @@ impl Envelope { // Ensure sender_id and recipient_id are not the same if sender_id == recipient_id { - return Err(VeilidAPIError::parse_error( + apibail_parse_error!( "sender_id should not be same as recipient_id", - recipient_id.encode(), - )); + recipient_id.encode() + ); } // Get signature @@ -206,10 +203,7 @@ impl Envelope { // Ensure body isn't too long let envelope_size: usize = body.len() + MIN_ENVELOPE_SIZE; if envelope_size > MAX_ENVELOPE_SIZE { - return Err(VeilidAPIError::parse_error( - "envelope size is too large", - envelope_size, - )); + apibail_parse_error!("envelope size is too large", envelope_size); } let mut data = vec![0u8; envelope_size]; diff --git a/veilid-core/src/crypto/receipt.rs b/veilid-core/src/crypto/receipt.rs index d67fea51..b622d2f9 100644 --- a/veilid-core/src/crypto/receipt.rs +++ b/veilid-core/src/crypto/receipt.rs @@ -59,10 +59,10 @@ impl Receipt { extra_data: D, ) -> Result { if extra_data.as_ref().len() > MAX_EXTRA_DATA_SIZE { - return Err(VeilidAPIError::parse_error( + apibail_parse_error!( "extra data too large for receipt", - extra_data.as_ref().len(), - )); + extra_data.as_ref().len() + ); } Ok(Self { version, @@ -75,7 +75,7 @@ impl Receipt { pub fn from_signed_data(data: &[u8]) -> Result { // Ensure we are at least the length of the envelope if data.len() < MIN_RECEIPT_SIZE { - return Err(VeilidAPIError::parse_error("receipt too small", data.len())); + apibail_parse_error!("receipt too small", data.len()); } // Verify magic number @@ -83,16 +83,13 @@ impl Receipt { .try_into() .map_err(VeilidAPIError::internal)?; if magic != *RECEIPT_MAGIC { - return Err(VeilidAPIError::generic("bad magic number")); + apibail_generic!("bad magic number"); } // Check version let version = data[0x04]; if version > MAX_CRYPTO_VERSION || version < MIN_CRYPTO_VERSION { - return Err(VeilidAPIError::parse_error( - "unsupported cryptography version", - version, - )); + apibail_parse_error!("unsupported cryptography version", version); } // Get size and ensure it matches the size of the envelope and is less than the maximum message size @@ -102,16 +99,13 @@ impl Receipt { .map_err(VeilidAPIError::internal)?, ); if (size as usize) > MAX_RECEIPT_SIZE { - return Err(VeilidAPIError::parse_error( - "receipt size is too large", - size, - )); + apibail_parse_error!("receipt size is too large", size); } if (size as usize) != data.len() { - return Err(VeilidAPIError::parse_error( + apibail_parse_error!( "size doesn't match receipt size", - format!("size={} data.len()={}", size, data.len()), - )); + format!("size={} data.len()={}", size, data.len()) + ); } // Get sender id @@ -153,10 +147,7 @@ impl Receipt { // Ensure extra data isn't too long let receipt_size: usize = self.extra_data.len() + MIN_RECEIPT_SIZE; if receipt_size > MAX_RECEIPT_SIZE { - return Err(VeilidAPIError::parse_error( - "receipt too large", - receipt_size, - )); + apibail_parse_error!("receipt too large", receipt_size); } let mut data: Vec = vec![0u8; receipt_size]; diff --git a/veilid-core/src/intf/wasm/protected_store.rs b/veilid-core/src/intf/wasm/protected_store.rs index 67a4d1da..e736739d 100644 --- a/veilid-core/src/intf/wasm/protected_store.rs +++ b/veilid-core/src/intf/wasm/protected_store.rs @@ -2,10 +2,7 @@ use super::*; use crate::xx::*; use crate::*; use data_encoding::BASE64URL_NOPAD; -use js_sys::*; -use send_wrapper::*; -use serde::{Deserialize, Serialize}; -use wasm_bindgen_futures::*; +use rkyv::{Archive as RkyvArchive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize}; use web_sys::*; #[derive(Clone)] @@ -44,15 +41,6 @@ impl ProtectedStore { #[instrument(level = "debug", skip(self))] pub async fn terminate(&self) {} - fn keyring_name(&self) -> String { - let c = self.config.get(); - if c.namespace.is_empty() { - "veilid_protected_store".to_owned() - } else { - format!("veilid_protected_store_{}", c.namespace) - } - } - fn browser_key_name(&self, key: &str) -> String { let c = self.config.get(); if c.namespace.is_empty() { @@ -136,22 +124,31 @@ impl ProtectedStore { } #[instrument(level = "trace", skip(self, value))] - pub async fn save_user_secret_frozen(&self, key: &str, value: &T) -> EyreResult + pub async fn save_user_secret_rkyv(&self, key: &str, value: &T) -> EyreResult where T: RkyvSerialize>, { - let v = to_frozen(value)?; + let v = to_rkyv(value)?; + self.save_user_secret(&key, &v).await + } + + #[instrument(level = "trace", skip(self, value))] + pub async fn save_user_secret_json(&self, key: &str, value: &T) -> EyreResult + where + T: serde::Serialize, + { + let v = serde_json::to_vec(value)?; self.save_user_secret(&key, &v).await } #[instrument(level = "trace", skip(self))] - pub async fn load_user_secret_frozen(&self, key: &str) -> EyreResult> + pub async fn load_user_secret_rkyv(&self, key: &str) -> EyreResult> where T: RkyvArchive, ::Archived: for<'t> bytecheck::CheckBytes>, ::Archived: - rkyv::Deserialize, + RkyvDeserialize, { let out = self.load_user_secret(key).await?; let b = match out { @@ -161,7 +158,24 @@ impl ProtectedStore { } }; - let obj = from_frozen(&b)?; + let obj = from_rkyv(b)?; + Ok(Some(obj)) + } + + #[instrument(level = "trace", skip(self))] + pub async fn load_user_secret_json(&self, key: &str) -> EyreResult> + where + T: for<'de> serde::de::Deserialize<'de>, + { + let out = self.load_user_secret(key).await?; + let b = match out { + Some(v) => v, + None => { + return Ok(None); + } + }; + + let obj = serde_json::from_slice(&b)?; Ok(Some(obj)) } diff --git a/veilid-core/src/routing_table/route_spec_store.rs b/veilid-core/src/routing_table/route_spec_store.rs index e0167291..e9c0681f 100644 --- a/veilid-core/src/routing_table/route_spec_store.rs +++ b/veilid-core/src/routing_table/route_spec_store.rs @@ -964,8 +964,8 @@ impl RouteSpecStore { let safety_spec = SafetySpec { preferred_route: None, hop_count: self.unlocked_inner.default_route_hop_count, - stability: Stability::LowLatency, - sequencing: Sequencing::NoPreference, + stability: Stability::default(), + sequencing: Sequencing::default(), }; let safety_selection = SafetySelection::Safe(safety_spec); diff --git a/veilid-core/src/veilid_api/debug.rs b/veilid-core/src/veilid_api/debug.rs index 4119e3ef..93d4beaf 100644 --- a/veilid-core/src/veilid_api/debug.rs +++ b/veilid-core/src/veilid_api/debug.rs @@ -57,14 +57,14 @@ fn get_safety_selection(text: &str, rss: RouteSpecStore) -> Option impl FnOnce(&str) -> Option { return Err(VeilidAPIError::parse_error($x, $y)) }; @@ -563,6 +563,12 @@ pub enum Sequencing { EnsureOrdered, } +impl Default for Sequencing { + fn default() -> Self { + Self::NoPreference + } +} + // Ordering here matters, >= is used to check strength of stability requirement #[derive( Copy, @@ -585,6 +591,12 @@ pub enum Stability { Reliable, } +impl Default for Stability { + fn default() -> Self { + Self::LowLatency + } +} + /// The choice of safety route to include in compiled routes #[derive( Copy, @@ -1543,10 +1555,7 @@ impl FromStr for DialInfo { VeilidAPIError::parse_error(format!("unable to split WS url: {}", e), &url) })?; if split_url.scheme != "ws" || !url.starts_with("ws://") { - return Err(VeilidAPIError::parse_error( - "incorrect scheme for WS dialinfo", - url, - )); + apibail_parse_error!("incorrect scheme for WS dialinfo", url); } let url_port = split_url.port.unwrap_or(80u16); @@ -1574,10 +1583,7 @@ impl FromStr for DialInfo { VeilidAPIError::parse_error(format!("unable to split WSS url: {}", e), &url) })?; if split_url.scheme != "wss" || !url.starts_with("wss://") { - return Err(VeilidAPIError::parse_error( - "incorrect scheme for WSS dialinfo", - url, - )); + apibail_parse_error!("incorrect scheme for WSS dialinfo", url); } let url_port = split_url.port.unwrap_or(443u16); @@ -1628,24 +1634,18 @@ impl DialInfo { VeilidAPIError::parse_error(format!("unable to split WS url: {}", e), &url) })?; if split_url.scheme != "ws" || !url.starts_with("ws://") { - return Err(VeilidAPIError::parse_error( - "incorrect scheme for WS dialinfo", - url, - )); + apibail_parse_error!("incorrect scheme for WS dialinfo", url); } let url_port = split_url.port.unwrap_or(80u16); if url_port != socket_address.port() { - return Err(VeilidAPIError::parse_error( - "socket address port doesn't match url port", - url, - )); + apibail_parse_error!("socket address port doesn't match url port", url); } if let SplitUrlHost::IpAddr(a) = split_url.host { if socket_address.to_ip_addr() != a { - return Err(VeilidAPIError::parse_error( + apibail_parse_error!( format!("request address does not match socket address: {}", a), - socket_address, - )); + socket_address + ); } } Ok(Self::WS(DialInfoWS { @@ -1658,23 +1658,17 @@ impl DialInfo { VeilidAPIError::parse_error(format!("unable to split WSS url: {}", e), &url) })?; if split_url.scheme != "wss" || !url.starts_with("wss://") { - return Err(VeilidAPIError::parse_error( - "incorrect scheme for WSS dialinfo", - url, - )); + apibail_parse_error!("incorrect scheme for WSS dialinfo", url); } let url_port = split_url.port.unwrap_or(443u16); if url_port != socket_address.port() { - return Err(VeilidAPIError::parse_error( - "socket address port doesn't match url port", - url, - )); + apibail_parse_error!("socket address port doesn't match url port", url); } if !matches!(split_url.host, SplitUrlHost::Hostname(_)) { - return Err(VeilidAPIError::parse_error( + apibail_parse_error!( "WSS url can not use address format, only hostname format", - url, - )); + url + ); } Ok(Self::WSS(DialInfoWSS { socket_address: socket_address.to_canonical(), @@ -1778,10 +1772,7 @@ impl DialInfo { let hostname = hostname.as_ref(); if short.len() < 2 { - return Err(VeilidAPIError::parse_error( - "invalid short url length", - short, - )); + apibail_parse_error!("invalid short url length", short); } let url = match &short[0..1] { "U" => { @@ -1797,7 +1788,7 @@ impl DialInfo { format!("wss://{}:{}", hostname, &short[1..]) } _ => { - return Err(VeilidAPIError::parse_error("invalid short url type", short)); + apibail_parse_error!("invalid short url type", short); } }; Self::try_vec_from_url(url) @@ -1815,10 +1806,7 @@ impl DialInfo { "ws" => split_url.port.unwrap_or(80u16), "wss" => split_url.port.unwrap_or(443u16), _ => { - return Err(VeilidAPIError::parse_error( - "Invalid dial info url scheme", - split_url.scheme, - )); + apibail_parse_error!("Invalid dial info url scheme", split_url.scheme); } }; @@ -2753,36 +2741,35 @@ impl VeilidAPI { // Private route allocation #[instrument(level = "debug", skip(self))] - pub async fn new_default_private_route(&self) -> Result<(DHTKey, Vec), VeilidAPIError> { - let config = self.config()?; - let c = config.get(); - self.new_private_route( - Stability::LowLatency, - Sequencing::NoPreference, - c.network.rpc.default_route_hop_count.into(), - ) - .await + pub async fn new_private_route(&self) -> Result<(DHTKey, Vec), VeilidAPIError> { + self.new_custom_private_route(Stability::default(), Sequencing::default()) + .await } #[instrument(level = "debug", skip(self))] - pub async fn new_private_route( + pub async fn new_custom_private_route( &self, stability: Stability, sequencing: Sequencing, - hop_count: usize, ) -> Result<(DHTKey, Vec), VeilidAPIError> { + let default_route_hop_count: usize = { + let config = self.config()?; + let c = config.get(); + c.network.rpc.default_route_hop_count.into() + }; + let rss = self.routing_table()?.route_spec_store(); let r = rss .allocate_route( stability, sequencing, - hop_count, + default_route_hop_count, Direction::Inbound.into(), &[], ) .map_err(VeilidAPIError::internal)?; let Some(pr_pubkey) = r else { - return Err(VeilidAPIError::generic("unable to allocate route")); + apibail_generic!("unable to allocate route"); }; if !rss .test_route(&pr_pubkey) @@ -2790,7 +2777,7 @@ impl VeilidAPI { .map_err(VeilidAPIError::no_connection)? { rss.release_route(&pr_pubkey); - return Err(VeilidAPIError::generic("allocated route failed to test")); + apibail_generic!("allocated route failed to test"); } let private_route = rss .assemble_private_route(&pr_pubkey, Some(true)) @@ -2799,12 +2786,37 @@ impl VeilidAPI { Ok(v) => v, Err(e) => { rss.release_route(&pr_pubkey); - return Err(VeilidAPIError::internal(e)); + apibail_internal!(e); } }; + + rss.mark_route_published(&pr_pubkey, true) + .map_err(VeilidAPIError::internal)?; + Ok((pr_pubkey, blob)) } + #[instrument(level = "debug", skip(self))] + pub fn import_remote_private_route(&self, blob: Vec) -> Result { + let rss = self.routing_table()?.route_spec_store(); + rss.import_remote_private_route(blob) + .map_err(|e| VeilidAPIError::invalid_argument(e, "blob", "private route blob")) + } + + #[instrument(level = "debug", skip(self))] + pub fn release_private_route(&self, key: &DHTKey) -> Result<(), VeilidAPIError> { + let rss = self.routing_table()?.route_spec_store(); + if rss.release_route(key) { + Ok(()) + } else { + Err(VeilidAPIError::invalid_argument( + "release_private_route", + "key", + key, + )) + } + } + //////////////////////////////////////////////////////////////// // App Calls diff --git a/veilid-core/src/veilid_api/routing_context.rs b/veilid-core/src/veilid_api/routing_context.rs index f33ce133..3af6eaae 100644 --- a/veilid-core/src/veilid_api/routing_context.rs +++ b/veilid-core/src/veilid_api/routing_context.rs @@ -39,14 +39,19 @@ impl RoutingContext { api, inner: Arc::new(Mutex::new(RoutingContextInner {})), unlocked_inner: Arc::new(RoutingContextUnlockedInner { - safety_selection: SafetySelection::Unsafe(Sequencing::NoPreference), + safety_selection: SafetySelection::Unsafe(Sequencing::default()), }), } } - pub fn with_default_privacy(self) -> Result { + pub fn with_privacy(self) -> Result { + self.with_custom_privacy(Stability::default()) + } + + pub fn with_custom_privacy(self, stability: Stability) -> Result { let config = self.api.config()?; let c = config.get(); + Ok(Self { api: self.api.clone(), inner: Arc::new(Mutex::new(RoutingContextInner {})), @@ -54,22 +59,13 @@ impl RoutingContext { safety_selection: SafetySelection::Safe(SafetySpec { preferred_route: None, hop_count: c.network.rpc.default_route_hop_count as usize, - stability: Stability::LowLatency, - sequencing: Sequencing::NoPreference, + stability, + sequencing: self.sequencing(), }), }), }) } - pub fn with_privacy(self, safety_spec: SafetySpec) -> Result { - Ok(Self { - api: self.api.clone(), - inner: Arc::new(Mutex::new(RoutingContextInner {})), - unlocked_inner: Arc::new(RoutingContextUnlockedInner { - safety_selection: SafetySelection::Safe(safety_spec), - }), - }) - } - + pub fn with_sequencing(self, sequencing: Sequencing) -> Self { Self { api: self.api.clone(), @@ -87,18 +83,13 @@ impl RoutingContext { }), } } - pub fn sequencing(&self) -> Sequencing { + + fn sequencing(&self) -> Sequencing { match self.unlocked_inner.safety_selection { SafetySelection::Unsafe(sequencing) => sequencing, SafetySelection::Safe(safety_spec) => safety_spec.sequencing, } } - pub fn safety_spec(&self) -> Option { - match self.unlocked_inner.safety_selection { - SafetySelection::Unsafe(_) => None, - SafetySelection::Safe(safety_spec) => Some(safety_spec.clone()), - } - } pub fn api(&self) -> VeilidAPI { self.api.clone() diff --git a/veilid-core/src/veilid_config.rs b/veilid-core/src/veilid_config.rs index 79c37d22..3a33fc0e 100644 --- a/veilid-core/src/veilid_config.rs +++ b/veilid-core/src/veilid_config.rs @@ -756,7 +756,7 @@ impl VeilidConfig { let mut out = &jvc; for k in keypath { if !out.has_key(k) { - apibail_parse!(format!("invalid subkey in key '{}'", key), k); + apibail_parse_error!(format!("invalid subkey in key '{}'", key), k); } out = &out[k]; } @@ -781,12 +781,12 @@ impl VeilidConfig { let mut out = &mut jvc; for k in objkeypath { if !out.has_key(*k) { - apibail_parse!(format!("invalid subkey in key '{}'", key), k); + apibail_parse_error!(format!("invalid subkey in key '{}'", key), k); } out = &mut out[*k]; } if !out.has_key(objkeyname) { - apibail_parse!(format!("invalid subkey in key '{}'", key), objkeyname); + apibail_parse_error!(format!("invalid subkey in key '{}'", key), objkeyname); } out[*objkeyname] = newval; jvc.to_string() diff --git a/veilid-core/src/xx/ip_extra.rs b/veilid-core/src/xx/ip_extra.rs index 8899c719..5328359d 100644 --- a/veilid-core/src/xx/ip_extra.rs +++ b/veilid-core/src/xx/ip_extra.rs @@ -1,6 +1,5 @@ // // This file really shouldn't be necessary, but 'ip' isn't a stable feature -// and things may not agree between the no_std_net crate and the stuff in std. // use crate::xx::*; diff --git a/veilid-core/src/xx/mod.rs b/veilid-core/src/xx/mod.rs index a4c8e008..4169be90 100644 --- a/veilid-core/src/xx/mod.rs +++ b/veilid-core/src/xx/mod.rs @@ -55,6 +55,9 @@ pub use std::convert::{TryFrom, TryInto}; pub use std::fmt; pub use std::future::Future; pub use std::mem; +pub use std::net::{ + IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs, +}; pub use std::ops::{Fn, FnMut, FnOnce}; pub use std::pin::Pin; pub use std::rc::Rc; @@ -73,10 +76,7 @@ cfg_if! { pub use async_lock::MutexGuard as AsyncMutexGuard; pub use async_lock::MutexGuardArc as AsyncMutexGuardArc; pub use async_executors::JoinHandle as LowLevelJoinHandle; - - pub use no_std_net::{ SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs, IpAddr, Ipv4Addr, Ipv6Addr }; } else { - cfg_if! { if #[cfg(feature="rt-async-std")] { pub use async_std::sync::Mutex as AsyncMutex; @@ -92,7 +92,6 @@ cfg_if! { #[compile_error("must use an executor")] } } - pub use std::net::{ SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs, IpAddr, Ipv4Addr, Ipv6Addr }; } } diff --git a/veilid-flutter/lib/veilid.dart b/veilid-flutter/lib/veilid.dart index 77c26268..196df95d 100644 --- a/veilid-flutter/lib/veilid.dart +++ b/veilid-flutter/lib/veilid.dart @@ -1696,6 +1696,70 @@ class VeilidVersion { VeilidVersion(this.major, this.minor, this.patch); } +////////////////////////////////////// +/// Stability + +enum Stability { + lowLatency, + reliable, +} + +extension StabilityExt on Stability { + String get json { + return name.toPascalCase(); + } +} + +Stability stabilityFromJson(String j) { + return Stability.values.byName(j.toCamelCase()); +} + +////////////////////////////////////// +/// Sequencing + +enum Sequencing { + noPreference, + preferOrdered, + ensureOrdered, +} + +extension SequencingExt on Sequencing { + String get json { + return name.toPascalCase(); + } +} + +Sequencing sequencingFromJson(String j) { + return Sequencing.values.byName(j.toCamelCase()); +} + +////////////////////////////////////// +/// KeyBlob +class KeyBlob { + final String key; + final Uint8List blob; + + KeyBlob(this.key, this.blob); + + KeyBlob.fromJson(Map json) + : key = json['key'], + blob = base64Decode(json['blob']); + + Map get json { + return {'key': key, 'blob': base64UrlEncode(blob)}; + } +} + +////////////////////////////////////// +/// VeilidRoutingContext +abstract class VeilidRoutingContext { + VeilidRoutingContext withPrivacy(); + VeilidRoutingContext withCustomPrivacy(Stability stability); + VeilidRoutingContext withSequencing(Sequencing sequencing); + Future appCall(String target, Uint8List request); + Future appMessage(String target, Uint8List message); +} + ////////////////////////////////////// /// Veilid singleton factory @@ -1709,8 +1773,22 @@ abstract class Veilid { Future attach(); Future detach(); Future shutdownVeilidCore(); - Future debug(String command); + + // Routing context + Future routingContext(); + + // Private route allocation + Future newPrivateRoute(); + Future newCustomPrivateRoute( + Stability stability, Sequencing sequencing); + Future importRemotePrivateRoute(Uint8List blob); + Future releasePrivateRoute(String key); + + // App calls Future appCallReply(String id, Uint8List message); + + // Misc String veilidVersionString(); VeilidVersion veilidVersion(); + Future debug(String command); } diff --git a/veilid-flutter/lib/veilid_ffi.dart b/veilid-flutter/lib/veilid_ffi.dart index f4b87f41..63ca4eea 100644 --- a/veilid-flutter/lib/veilid_ffi.dart +++ b/veilid-flutter/lib/veilid_ffi.dart @@ -48,13 +48,56 @@ typedef _AttachDart = void Function(int); // fn detach(port: i64) typedef _DetachC = Void Function(Int64); typedef _DetachDart = void Function(int); -// fn debug(port: i64, log_level: FfiStr) -typedef _DebugC = Void Function(Int64, Pointer); -typedef _DebugDart = void Function(int, Pointer); + +// fn routing_context(port: i64) +typedef _RoutingContextC = Void Function(Int64); +typedef _RoutingContextDart = void Function(int); +// fn release_routing_context(id: u32) +typedef _ReleaseRoutingContextC = Int32 Function(Uint32); +typedef _ReleaseRoutingContextDart = int Function(int); +// fn routing_context_with_privacy(id: u32) -> u32 +typedef _RoutingContextWithPrivacyC = Uint32 Function(Uint32); +typedef _RoutingContextWithPrivacyDart = int Function(int); +// fn routing_context_with_custom_privacy(id: u32, stability: FfiStr) +typedef _RoutingContextWithCustomPrivacyC = Uint32 Function( + Uint32, Pointer); +typedef _RoutingContextWithCustomPrivacyDart = int Function(int, Pointer); +// fn routing_context_with_sequencing(id: u32, sequencing: FfiStr) +typedef _RoutingContextWithSequencingC = Uint32 Function(Uint32, Pointer); +typedef _RoutingContextWithSequencingDart = int Function(int, Pointer); +// fn routing_context_app_call(port: i64, id: u32, target: FfiStr, request: FfiStr) +typedef _RoutingContextAppCallC = Void Function( + Int64, Uint32, Pointer, Pointer); +typedef _RoutingContextAppCallDart = void Function( + int, int, Pointer, Pointer); +// fn routing_context_app_message(port: i64, id: u32, target: FfiStr, request: FfiStr) +typedef _RoutingContextAppMessageC = Void Function( + Int64, Uint32, Pointer, Pointer); +typedef _RoutingContextAppMessageDart = void Function( + int, int, Pointer, Pointer); + +// fn new_private_route(port: i64) +typedef _NewPrivateRouteC = Void Function(Int64); +typedef _NewPrivateRouteDart = void Function(int); +// fn new_custom_private_route(port: i64, stability: FfiStr, sequencing: FfiStr) +typedef _NewCustomPrivateRouteC = Void Function( + Int64, Pointer, Pointer); +typedef _NewCustomPrivateRouteDart = void Function( + int, Pointer, Pointer); +// fn import_remote_private_route(port: i64, blob: FfiStr) +typedef _ImportRemotePrivateRouteC = Void Function(Int64, Pointer); +typedef _ImportRemotePrivateRouteDart = void Function(int, Pointer); +// fn release_private_route(port:i64, key: FfiStr) +typedef _ReleasePrivateRouteC = Void Function(Int64, Pointer); +typedef _ReleasePrivateRouteDart = void Function(int, Pointer); + // fn app_call_reply(port: i64, id: FfiStr, message: FfiStr) typedef _AppCallReplyC = Void Function(Int64, Pointer, Pointer); typedef _AppCallReplyDart = void Function(int, Pointer, Pointer); +// fn debug(port: i64, log_level: FfiStr) +typedef _DebugC = Void Function(Int64, Pointer); +typedef _DebugDart = void Function(int, Pointer); // fn shutdown_veilid_core(port: i64) typedef _ShutdownVeilidCoreC = Void Function(Int64); typedef _ShutdownVeilidCoreDart = void Function(int); @@ -294,6 +337,58 @@ Stream processStreamJson( } } +// FFI implementation of VeilidRoutingContext +class VeilidRoutingContextFFI implements VeilidRoutingContext { + final int _id; + final VeilidFFI _ffi; + + VeilidRoutingContextFFI._(this._id, this._ffi); + @override + VeilidRoutingContextFFI withPrivacy() { + final newId = _ffi._routingContextWithPrivacy(_id); + return VeilidRoutingContextFFI._(newId, _ffi); + } + + @override + VeilidRoutingContextFFI withCustomPrivacy(Stability stability) { + final newId = _ffi._routingContextWithCustomPrivacy( + _id, stability.json.toNativeUtf8()); + return VeilidRoutingContextFFI._(newId, _ffi); + } + + @override + VeilidRoutingContextFFI withSequencing(Sequencing sequencing) { + final newId = + _ffi._routingContextWithSequencing(_id, sequencing.json.toNativeUtf8()); + return VeilidRoutingContextFFI._(newId, _ffi); + } + + @override + Future appCall(String target, Uint8List request) async { + var nativeEncodedTarget = target.toNativeUtf8(); + var nativeEncodedRequest = base64UrlEncode(request).toNativeUtf8(); + + final recvPort = ReceivePort("routing_context_app_call"); + final sendPort = recvPort.sendPort; + _ffi._routingContextAppCall( + sendPort.nativePort, _id, nativeEncodedTarget, nativeEncodedRequest); + final out = await processFuturePlain(recvPort.first); + return base64Decode(out); + } + + @override + Future appMessage(String target, Uint8List message) async { + var nativeEncodedTarget = target.toNativeUtf8(); + var nativeEncodedMessage = base64UrlEncode(message).toNativeUtf8(); + + final recvPort = ReceivePort("routing_context_app_call"); + final sendPort = recvPort.sendPort; + _ffi._routingContextAppCall( + sendPort.nativePort, _id, nativeEncodedTarget, nativeEncodedMessage); + return processFutureVoid(recvPort.first); + } +} + // FFI implementation of high level Veilid API class VeilidFFI implements Veilid { // veilid_core shared library @@ -308,8 +403,23 @@ class VeilidFFI implements Veilid { final _AttachDart _attach; final _DetachDart _detach; final _ShutdownVeilidCoreDart _shutdownVeilidCore; - final _DebugDart _debug; + + final _RoutingContextDart _routingContext; + final _ReleaseRoutingContextDart _releaseRoutingContext; + final _RoutingContextWithPrivacyDart _routingContextWithPrivacy; + final _RoutingContextWithCustomPrivacyDart _routingContextWithCustomPrivacy; + final _RoutingContextWithSequencingDart _routingContextWithSequencing; + final _RoutingContextAppCallDart _routingContextAppCall; + final _RoutingContextAppMessageDart _routingContextAppMessage; + + final _NewPrivateRouteDart _newPrivateRoute; + final _NewCustomPrivateRouteDart _newCustomPrivateRoute; + final _ImportRemotePrivateRouteDart _importRemotePrivateRoute; + final _ReleasePrivateRouteDart _releasePrivateRoute; + final _AppCallReplyDart _appCallReply; + + final _DebugDart _debug; final _VeilidVersionStringDart _veilidVersionString; final _VeilidVersionDart _veilidVersion; @@ -333,9 +443,40 @@ class VeilidFFI implements Veilid { _shutdownVeilidCore = dylib.lookupFunction<_ShutdownVeilidCoreC, _ShutdownVeilidCoreDart>( 'shutdown_veilid_core'), - _debug = dylib.lookupFunction<_DebugC, _DebugDart>('debug'), + _routingContext = + dylib.lookupFunction<_RoutingContextC, _RoutingContextDart>( + 'routing_context'), + _releaseRoutingContext = dylib.lookupFunction<_ReleaseRoutingContextC, + _ReleaseRoutingContextDart>('release_routing_context'), + _routingContextWithPrivacy = dylib.lookupFunction< + _RoutingContextWithPrivacyC, + _RoutingContextWithPrivacyDart>('routing_context_with_privacy'), + _routingContextWithCustomPrivacy = dylib.lookupFunction< + _RoutingContextWithCustomPrivacyC, + _RoutingContextWithCustomPrivacyDart>( + 'routing_context_with_custom_privacy'), + _routingContextWithSequencing = dylib.lookupFunction< + _RoutingContextWithSequencingC, + _RoutingContextWithSequencingDart>( + 'routing_context_with_sequencing'), + _routingContextAppCall = dylib.lookupFunction<_RoutingContextAppCallC, + _RoutingContextAppCallDart>('routing_context_app_call'), + _routingContextAppMessage = dylib.lookupFunction< + _RoutingContextAppMessageC, + _RoutingContextAppMessageDart>('routing_context_app_message'), + _newPrivateRoute = + dylib.lookupFunction<_NewPrivateRouteC, _NewPrivateRouteDart>( + 'new_private_route'), + _newCustomPrivateRoute = dylib.lookupFunction<_NewCustomPrivateRouteC, + _NewCustomPrivateRouteDart>('new_custom_private_route'), + _importRemotePrivateRoute = dylib.lookupFunction< + _ImportRemotePrivateRouteC, + _ImportRemotePrivateRouteDart>('import_remote_private_route'), + _releasePrivateRoute = dylib.lookupFunction<_ReleasePrivateRouteC, + _ReleasePrivateRouteDart>('release_private_route'), _appCallReply = dylib.lookupFunction<_AppCallReplyC, _AppCallReplyDart>( 'app_call_reply'), + _debug = dylib.lookupFunction<_DebugC, _DebugDart>('debug'), _veilidVersionString = dylib.lookupFunction<_VeilidVersionStringC, _VeilidVersionStringDart>('veilid_version_string'), _veilidVersion = @@ -420,14 +561,53 @@ class VeilidFFI implements Veilid { } @override - Future debug(String command) async { - var nativeCommand = command.toNativeUtf8(); - final recvPort = ReceivePort("debug"); + Future routingContext() async { + final recvPort = ReceivePort("routing_context"); final sendPort = recvPort.sendPort; - _debug(sendPort.nativePort, nativeCommand); + _routingContext(sendPort.nativePort); + final id = await processFuturePlain(recvPort.first); + return VeilidRoutingContextFFI._(id, this); + } + + @override + Future newPrivateRoute() async { + final recvPort = ReceivePort("new_private_route"); + final sendPort = recvPort.sendPort; + _newPrivateRoute(sendPort.nativePort); + return processFutureJson(KeyBlob.fromJson, recvPort.first); + } + + @override + Future newCustomPrivateRoute( + Stability stability, Sequencing sequencing) async { + final recvPort = ReceivePort("new_custom_private_route"); + final sendPort = recvPort.sendPort; + _newCustomPrivateRoute(sendPort.nativePort, stability.json.toNativeUtf8(), + sequencing.json.toNativeUtf8()); + final keyblob = await processFutureJson(KeyBlob.fromJson, recvPort.first); + return keyblob; + } + + @override + Future importRemotePrivateRoute(Uint8List blob) async { + var nativeEncodedBlob = base64UrlEncode(blob).toNativeUtf8(); + + final recvPort = ReceivePort("import_remote_private_route"); + final sendPort = recvPort.sendPort; + _importRemotePrivateRoute(sendPort.nativePort, nativeEncodedBlob); return processFuturePlain(recvPort.first); } + @override + Future releasePrivateRoute(String key) async { + var nativeEncodedKey = key.toNativeUtf8(); + + final recvPort = ReceivePort("release_private_route"); + final sendPort = recvPort.sendPort; + _releasePrivateRoute(sendPort.nativePort, nativeEncodedKey); + return processFutureVoid(recvPort.first); + } + @override Future appCallReply(String id, Uint8List message) async { var nativeId = id.toNativeUtf8(); @@ -438,6 +618,15 @@ class VeilidFFI implements Veilid { return processFutureVoid(recvPort.first); } + @override + Future debug(String command) async { + var nativeCommand = command.toNativeUtf8(); + final recvPort = ReceivePort("debug"); + final sendPort = recvPort.sendPort; + _debug(sendPort.nativePort, nativeCommand); + return processFuturePlain(recvPort.first); + } + @override String veilidVersionString() { final versionString = _veilidVersionString(); diff --git a/veilid-flutter/lib/veilid_js.dart b/veilid-flutter/lib/veilid_js.dart index 1d5e907c..7353ae28 100644 --- a/veilid-flutter/lib/veilid_js.dart +++ b/veilid-flutter/lib/veilid_js.dart @@ -19,6 +19,61 @@ Future _wrapApiPromise(Object p) { VeilidAPIException.fromJson(jsonDecode(error as String)))); } +// JS implementation of VeilidRoutingContext +class VeilidRoutingContextJS implements VeilidRoutingContext { + final int _id; + final VeilidFFI _ffi; + + VeilidRoutingContextFFI._(this._id, this._ffi); + @override + VeilidRoutingContextFFI withPrivacy() { + final newId = _ffi._routingContextWithPrivacy(_id); + return VeilidRoutingContextFFI._(newId, _ffi); + } + + @override + VeilidRoutingContextFFI withCustomPrivacy(Stability stability) { + final newId = _ffi._routingContextWithCustomPrivacy( + _id, stability.json.toNativeUtf8()); + return VeilidRoutingContextFFI._(newId, _ffi); + } + + @override + VeilidRoutingContextFFI withSequencing(Sequencing sequencing) { + final newId = + _ffi._routingContextWithSequencing(_id, sequencing.json.toNativeUtf8()); + return VeilidRoutingContextFFI._(newId, _ffi); + } + + @override + Future appCall(String target, Uint8List request) async { + var nativeEncodedTarget = target.toNativeUtf8(); + var nativeEncodedRequest = base64UrlEncode(request).toNativeUtf8(); + + final recvPort = ReceivePort("routing_context_app_call"); + final sendPort = recvPort.sendPort; + _ffi._routingContextAppCall( + sendPort.nativePort, _id, nativeEncodedTarget, nativeEncodedRequest); + final out = await processFuturePlain(recvPort.first); + return base64Decode(out); + } + + @override + Future appMessage(String target, Uint8List message) async { + var nativeEncodedTarget = target.toNativeUtf8(); + var nativeEncodedMessage = base64UrlEncode(message).toNativeUtf8(); + + final recvPort = ReceivePort("routing_context_app_call"); + final sendPort = recvPort.sendPort; + _ffi._routingContextAppCall( + sendPort.nativePort, _id, nativeEncodedTarget, nativeEncodedMessage); + return processFutureVoid(recvPort.first); + } +} + + +// JS implementation of high level Veilid API + class VeilidJS implements Veilid { @override void initializeVeilidCore(Map platformConfigJson) { @@ -78,9 +133,43 @@ class VeilidJS implements Veilid { js_util.callMethod(wasm, "shutdown_veilid_core", [])); } + @override - Future debug(String command) { - return _wrapApiPromise(js_util.callMethod(wasm, "debug", [command])); + Future routingContext() async { + final recvPort = ReceivePort("routing_context"); + final sendPort = recvPort.sendPort; + _routingContext(sendPort.nativePort); + final id = await processFuturePlain(recvPort.first); + return VeilidRoutingContextFFI._(id, this); + } + + @override + Future newPrivateRoute() async { + final recvPort = ReceivePort("new_private_route"); + final sendPort = recvPort.sendPort; + _newPrivateRoute(sendPort.nativePort); + return processFutureJson(KeyBlob.fromJson, recvPort.first); + } + + @override + Future newCustomPrivateRoute( + Stability stability, Sequencing sequencing) async { + return _wrapApiPromise( + js_util.callMethod(wasm, "new_custom_private_route", [stability, sequencing])); + + } + + @override + Future importRemotePrivateRoute(Uint8List blob) async { + var encodedBlob = base64UrlEncode(blob); + return _wrapApiPromise( + js_util.callMethod(wasm, "import_remote_private_route", [encodedBlob])); + } + + @override + Future releasePrivateRoute(String key) async { + return _wrapApiPromise( + js_util.callMethod(wasm, "release_private_route", [key])); } @override @@ -89,6 +178,11 @@ class VeilidJS implements Veilid { return _wrapApiPromise( js_util.callMethod(wasm, "app_call_reply", [id, encodedMessage])); } + + @override + Future debug(String command) { + return _wrapApiPromise(js_util.callMethod(wasm, "debug", [command])); + } @override String veilidVersionString() { diff --git a/veilid-flutter/rust/src/dart_ffi.rs b/veilid-flutter/rust/src/dart_ffi.rs index 00080298..2a45485e 100644 --- a/veilid-flutter/rust/src/dart_ffi.rs +++ b/veilid-flutter/rust/src/dart_ffi.rs @@ -21,6 +21,8 @@ lazy_static! { static ref VEILID_API: AsyncMutex> = AsyncMutex::new(None); static ref FILTERS: Mutex> = Mutex::new(BTreeMap::new()); + static ref ROUTING_CONTEXTS: Mutex> = + Mutex::new(BTreeMap::new()); } async fn get_veilid_api() -> Result { @@ -49,7 +51,7 @@ type APIResult = Result; const APIRESULT_VOID: APIResult<()> = APIResult::Ok(()); ///////////////////////////////////////// -// FFI-specific cofnig +// FFI-specific #[derive(Debug, Deserialize, Serialize)] pub struct VeilidFFIConfigLoggingTerminal { @@ -83,6 +85,13 @@ pub struct VeilidFFIConfig { pub logging: VeilidFFIConfigLogging, } +#[derive(Debug, Deserialize, Serialize)] +pub struct VeilidFFIKeyBlob { + pub key: veilid_core::DHTKey, + #[serde(with = "veilid_core::json_as_base64")] + pub blob: Vec, +} + ///////////////////////////////////////// // Initializer #[no_mangle] @@ -317,13 +326,208 @@ pub extern "C" fn shutdown_veilid_core(port: i64) { }); } +fn add_routing_context(routing_context: veilid_core::RoutingContext) -> u32 { + let mut next_id: u32 = 1; + let mut rc = ROUTING_CONTEXTS.lock(); + while rc.contains_key(&next_id) { + next_id += 1; + } + rc.insert(next_id, routing_context); + next_id +} + #[no_mangle] -pub extern "C" fn debug(port: i64, command: FfiStr) { - let command = command.into_opt_string().unwrap_or_default(); +pub extern "C" fn routing_context(port: i64) { DartIsolateWrapper::new(port).spawn_result(async move { let veilid_api = get_veilid_api().await?; - let out = veilid_api.debug(command).await?; - APIResult::Ok(out) + let routing_context = veilid_api.routing_context(); + let new_id = add_routing_context(routing_context); + APIResult::Ok(new_id) + }); +} + +#[no_mangle] +pub extern "C" fn release_routing_context(id: u32) -> i32 { + let mut rc = ROUTING_CONTEXTS.lock(); + if rc.remove(&id).is_none() { + return 0; + } + return 1; +} + +#[no_mangle] +pub extern "C" fn routing_context_with_privacy(id: u32) -> u32 { + let rc = ROUTING_CONTEXTS.lock(); + let Some(routing_context) = rc.get(&id) else { + return 0; + }; + let Ok(routing_context) = routing_context.clone().with_privacy() else { + return 0; + }; + let new_id = add_routing_context(routing_context); + new_id +} + +#[no_mangle] +pub extern "C" fn routing_context_with_custom_privacy(id: u32, stability: FfiStr) -> u32 { + let stability: veilid_core::Stability = + veilid_core::deserialize_opt_json(stability.into_opt_string()).unwrap(); + + let rc = ROUTING_CONTEXTS.lock(); + let Some(routing_context) = rc.get(&id) else { + return 0; + }; + let Ok(routing_context) = routing_context.clone().with_custom_privacy(stability) else { + return 0; + }; + let new_id = add_routing_context(routing_context); + new_id +} + +#[no_mangle] +pub extern "C" fn routing_context_with_sequencing(id: u32, sequencing: FfiStr) -> u32 { + let sequencing: veilid_core::Sequencing = + veilid_core::deserialize_opt_json(sequencing.into_opt_string()).unwrap(); + + let rc = ROUTING_CONTEXTS.lock(); + let Some(routing_context) = rc.get(&id) else { + return 0; + }; + let routing_context = routing_context.clone().with_sequencing(sequencing); + let new_id = add_routing_context(routing_context); + new_id +} + +#[no_mangle] +pub extern "C" fn routing_context_app_call(port: i64, id: u32, target: FfiStr, request: FfiStr) { + let target: veilid_core::DHTKey = + veilid_core::deserialize_opt_json(target.into_opt_string()).unwrap(); + let request: Vec = data_encoding::BASE64URL_NOPAD + .decode( + veilid_core::deserialize_opt_json::(request.into_opt_string()) + .unwrap() + .as_bytes(), + ) + .unwrap(); + DartIsolateWrapper::new(port).spawn_result_json(async move { + let veilid_api = get_veilid_api().await?; + let routing_table = veilid_api.routing_table()?; + let rss = routing_table.route_spec_store(); + + let routing_context = { + let rc = ROUTING_CONTEXTS.lock(); + let Some(routing_context) = rc.get(&id) else { + return APIResult::Err(veilid_core::VeilidAPIError::invalid_argument("routing_context_app_call", "id", id)); + }; + routing_context.clone() + }; + + let target = if rss.get_remote_private_route(&target).is_some() { + veilid_core::Target::PrivateRoute(target) + } else { + veilid_core::Target::NodeId(veilid_core::NodeId::new(target)) + }; + + let answer = routing_context.app_call(target, request).await?; + let answer = data_encoding::BASE64URL_NOPAD.encode(&answer); + APIResult::Ok(answer) + }); +} + +#[no_mangle] +pub extern "C" fn routing_context_app_message(port: i64, id: u32, target: FfiStr, message: FfiStr) { + let target: veilid_core::DHTKey = + veilid_core::deserialize_opt_json(target.into_opt_string()).unwrap(); + let message: Vec = data_encoding::BASE64URL_NOPAD + .decode( + veilid_core::deserialize_opt_json::(message.into_opt_string()) + .unwrap() + .as_bytes(), + ) + .unwrap(); + DartIsolateWrapper::new(port).spawn_result_json(async move { + let veilid_api = get_veilid_api().await?; + let routing_table = veilid_api.routing_table()?; + let rss = routing_table.route_spec_store(); + + let routing_context = { + let rc = ROUTING_CONTEXTS.lock(); + let Some(routing_context) = rc.get(&id) else { + return APIResult::Err(veilid_core::VeilidAPIError::invalid_argument("routing_context_app_call", "id", id)); + }; + routing_context.clone() + }; + + let target = if rss.get_remote_private_route(&target).is_some() { + veilid_core::Target::PrivateRoute(target) + } else { + veilid_core::Target::NodeId(veilid_core::NodeId::new(target)) + }; + + routing_context.app_message(target, message).await?; + APIRESULT_VOID + }); +} + +#[no_mangle] +pub extern "C" fn new_private_route(port: i64) { + DartIsolateWrapper::new(port).spawn_result_json(async move { + let veilid_api = get_veilid_api().await?; + + let (key, blob) = veilid_api.new_private_route().await?; + + let keyblob = VeilidFFIKeyBlob { key, blob }; + + APIResult::Ok(keyblob) + }); +} + +#[no_mangle] +pub extern "C" fn new_custom_private_route(port: i64, stability: FfiStr, sequencing: FfiStr) { + let stability: veilid_core::Stability = + veilid_core::deserialize_opt_json(stability.into_opt_string()).unwrap(); + let sequencing: veilid_core::Sequencing = + veilid_core::deserialize_opt_json(sequencing.into_opt_string()).unwrap(); + + DartIsolateWrapper::new(port).spawn_result_json(async move { + let veilid_api = get_veilid_api().await?; + + let (key, blob) = veilid_api + .new_custom_private_route(stability, sequencing) + .await?; + + let keyblob = VeilidFFIKeyBlob { key, blob }; + + APIResult::Ok(keyblob) + }); +} + +#[no_mangle] +pub extern "C" fn import_remote_private_route(port: i64, blob: FfiStr) { + let blob: Vec = data_encoding::BASE64URL_NOPAD + .decode( + veilid_core::deserialize_opt_json::(blob.into_opt_string()) + .unwrap() + .as_bytes(), + ) + .unwrap(); + DartIsolateWrapper::new(port).spawn_result(async move { + let veilid_api = get_veilid_api().await?; + + let key = veilid_api.import_remote_private_route(blob)?; + + APIResult::Ok(key.encode()) + }); +} + +#[no_mangle] +pub extern "C" fn release_private_route(port: i64, key: FfiStr) { + let key: veilid_core::DHTKey = + veilid_core::deserialize_opt_json(key.into_opt_string()).unwrap(); + DartIsolateWrapper::new(port).spawn_result_json(async move { + let veilid_api = get_veilid_api().await?; + veilid_api.release_private_route(&key)?; + APIRESULT_VOID }); } @@ -347,6 +551,16 @@ pub extern "C" fn app_call_reply(port: i64, id: FfiStr, message: FfiStr) { }); } +#[no_mangle] +pub extern "C" fn debug(port: i64, command: FfiStr) { + let command = command.into_opt_string().unwrap_or_default(); + DartIsolateWrapper::new(port).spawn_result(async move { + let veilid_api = get_veilid_api().await?; + let out = veilid_api.debug(command).await?; + APIResult::Ok(out) + }); +} + #[no_mangle] pub extern "C" fn veilid_version_string() -> *mut c_char { veilid_core::veilid_version_string().into_ffi_value() diff --git a/veilid-wasm/src/lib.rs b/veilid-wasm/src/lib.rs index e49bb8e6..3fd7e891 100644 --- a/veilid-wasm/src/lib.rs +++ b/veilid-wasm/src/lib.rs @@ -39,6 +39,8 @@ lazy_static! { SendWrapper::new(RefCell::new(None)); static ref FILTERS: SendWrapper>> = SendWrapper::new(RefCell::new(BTreeMap::new())); + static ref ROUTING_CONTEXTS: SendWrapper>> = + SendWrapper::new(RefCell::new(BTreeMap::new())); } fn get_veilid_api() -> Result { @@ -54,20 +56,7 @@ fn take_veilid_api() -> Result(val: T) -> String { - serde_json::to_string(&val).expect("failed to serialize json value") -} - -pub fn deserialize_json( - arg: &str, -) -> Result { - serde_json::from_str(arg).map_err(|e| veilid_core::VeilidAPIError::ParseError { - message: e.to_string(), - value: String::new(), - }) -} - +// JSON Helpers for WASM pub fn to_json(val: T) -> JsValue { JsValue::from_str(&serialize_json(val)) } @@ -104,7 +93,7 @@ where } ///////////////////////////////////////// -// WASM-specific cofnig +// WASM-specific #[derive(Debug, Deserialize, Serialize)] pub struct VeilidWASMConfigLoggingPerformance { @@ -131,6 +120,13 @@ pub struct VeilidWASMConfig { pub logging: VeilidWASMConfigLogging, } +#[derive(Debug, Deserialize, Serialize)] +pub struct VeilidFFIKeyBlob { + pub key: veilid_core::DHTKey, + #[serde(with = "veilid_core::json_as_base64")] + pub blob: Vec, +} + // WASM Bindings #[wasm_bindgen()] From 5df46aecae5558f0877197f866989dc9b22213aa Mon Sep 17 00:00:00 2001 From: John Smith Date: Sat, 26 Nov 2022 16:17:30 -0500 Subject: [PATCH 07/88] cleanup --- Cargo.lock | 263 +- veilid-core/src/core_context.rs | 7 +- veilid-core/src/crypto/tests/test_crypto.rs | 1 - .../src/crypto/tests/test_envelope_receipt.rs | 1 - .../src/network_manager/connection_manager.rs | 1 - veilid-core/src/network_manager/mod.rs | 2 +- .../src/network_manager/native/igd_manager.rs | 1 - .../tasks/public_address_check.rs | 3 +- .../tasks/rolling_transfers.rs | 4 +- .../tests/test_connection_table.rs | 5 +- veilid-core/src/network_manager/wasm/mod.rs | 1 + .../src/network_manager/wasm/protocol/mod.rs | 1 - veilid-core/src/routing_table/mod.rs | 5 +- .../src/routing_table/tasks/bootstrap.rs | 3 +- .../src/routing_table/tasks/kick_buckets.rs | 3 +- .../tasks/peer_minimum_refresh.rs | 3 +- .../src/routing_table/tasks/ping_validator.rs | 3 +- .../tasks/private_route_management.rs | 3 +- .../routing_table/tasks/relay_management.rs | 3 +- .../routing_table/tasks/rolling_transfers.rs | 3 +- .../src/rpc_processor/coders/address.rs | 4 +- .../rpc_processor/coders/address_type_set.rs | 3 +- .../src/rpc_processor/coders/dht_key.rs | 4 +- .../src/rpc_processor/coders/dht_signature.rs | 3 +- .../src/rpc_processor/coders/dial_info.rs | 4 +- .../rpc_processor/coders/dial_info_class.rs | 2 +- .../rpc_processor/coders/dial_info_detail.rs | 3 +- .../src/rpc_processor/coders/network_class.rs | 2 +- .../src/rpc_processor/coders/node_info.rs | 3 +- .../src/rpc_processor/coders/node_status.rs | 3 +- veilid-core/src/rpc_processor/coders/nonce.rs | 3 +- .../rpc_processor/coders/operations/answer.rs | 2 - .../rpc_processor/coders/operations/mod.rs | 2 + .../coders/operations/operation.rs | 3 +- .../coders/operations/operation_app_call.rs | 3 +- .../operations/operation_app_message.rs | 3 +- .../operations/operation_cancel_tunnel.rs | 3 +- .../operations/operation_complete_tunnel.rs | 3 +- .../coders/operations/operation_find_block.rs | 3 +- .../coders/operations/operation_find_node.rs | 3 +- .../coders/operations/operation_get_value.rs | 3 +- .../operations/operation_node_info_update.rs | 3 +- .../operations/operation_return_receipt.rs | 3 +- .../coders/operations/operation_route.rs | 3 +- .../coders/operations/operation_set_value.rs | 3 +- .../coders/operations/operation_signal.rs | 3 +- .../operations/operation_start_tunnel.rs | 3 +- .../coders/operations/operation_status.rs | 3 +- .../operations/operation_supply_block.rs | 3 +- .../operation_validate_dial_info.rs | 3 +- .../operations/operation_value_changed.rs | 3 +- .../operations/operation_watch_value.rs | 3 +- .../coders/operations/question.rs | 2 - .../coders/operations/respond_to.rs | 3 +- .../coders/operations/statement.rs | 2 - .../src/rpc_processor/coders/peer_info.rs | 3 +- .../rpc_processor/coders/protocol_type_set.rs | 3 +- .../src/rpc_processor/coders/sender_info.rs | 3 +- .../src/rpc_processor/coders/signal_info.rs | 3 +- .../coders/signed_direct_node_info.rs | 3 +- .../rpc_processor/coders/signed_node_info.rs | 3 +- .../coders/signed_relayed_node_info.rs | 3 +- .../rpc_processor/coders/socket_address.rs | 3 +- .../src/rpc_processor/coders/tunnel.rs | 3 +- .../src/rpc_processor/coders/value_data.rs | 3 +- .../src/rpc_processor/coders/value_key.rs | 3 +- veilid-core/src/tests/common/mod.rs | 5 - veilid-core/src/tests/mod.rs | 2 - veilid-core/src/tests/native/mod.rs | 3 +- .../tests/native/test_async_peek_stream.rs | 2 +- veilid-core/src/veilid_api/api.rs | 284 ++ veilid-core/src/veilid_api/debug.rs | 51 +- veilid-core/src/veilid_api/error.rs | 186 ++ veilid-core/src/veilid_api/mod.rs | 2827 +---------------- veilid-core/src/veilid_api/routing_context.rs | 21 +- veilid-core/src/veilid_api/types.rs | 2402 ++++++++++++++ 76 files changed, 3107 insertions(+), 3127 deletions(-) create mode 100644 veilid-core/src/veilid_api/api.rs create mode 100644 veilid-core/src/veilid_api/error.rs create mode 100644 veilid-core/src/veilid_api/types.rs diff --git a/Cargo.lock b/Cargo.lock index a9eb7648..76dbdfe4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,9 +63,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.19" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] @@ -218,13 +218,13 @@ dependencies = [ [[package]] name = "async-io" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8121296a9f05be7f34aa4196b1747243b3b62e048bb7906f644f3fbfc490cf7" +checksum = "6fe557ebe0829511ddff4ad3011d159c0e6f144e05e3e8c3ab5095a131900a7b" dependencies = [ "async-lock", "autocfg", - "concurrent-queue 1.2.4", + "concurrent-queue 2.0.0", "futures-lite", "libc", "log", @@ -399,7 +399,7 @@ dependencies = [ "futures-util", "pin-project 1.0.12", "rustc_version", - "tokio 1.21.2", + "tokio 1.22.0", "wasm-bindgen-futures", ] @@ -467,7 +467,7 @@ dependencies = [ "async-trait", "axum-core", "bitflags", - "bytes 1.2.1", + "bytes 1.3.0", "futures-util", "http", "http-body", @@ -480,7 +480,7 @@ dependencies = [ "pin-project-lite 0.2.9", "serde", "sync_wrapper", - "tokio 1.21.2", + "tokio 1.22.0", "tower", "tower-http", "tower-layer", @@ -494,7 +494,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37e5939e02c56fecd5c017c37df4238c0a839fa76b7f97acdd7efb804fd181cc" dependencies = [ "async-trait", - "bytes 1.2.1", + "bytes 1.3.0", "futures-util", "http", "http-body", @@ -513,7 +513,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "libc", - "miniz_oxide", + "miniz_oxide 0.5.4", "object", "rustc-demangle", ] @@ -569,9 +569,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.3.1" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08e53fc5a564bb15bfe6fae56bd71522205f1f91893f9c0116edad6496c183f" +checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef" dependencies = [ "arrayref", "arrayvec", @@ -627,16 +627,16 @@ checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" [[package]] name = "blocking" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" +checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8" dependencies = [ "async-channel", + "async-lock", "async-task", "atomic-waker", "fastrand", "futures-lite", - "once_cell", ] [[package]] @@ -707,9 +707,9 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" [[package]] name = "bytes" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" [[package]] name = "cache-padded" @@ -719,9 +719,9 @@ checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" [[package]] name = "capnp" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e850735c543306805e2ba8ee0a9632b0f62bb05872a8be2e2674e9903a1c048" +checksum = "f4929d71efc55aa42759793d853ecdfa6bb034419d22884e3e9871f0f593ac8d" [[package]] name = "capnp-futures" @@ -761,9 +761,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.76" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a284da2e6fe2092f2353e51713435363112dfd60030e22add80be333fb928f" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" [[package]] name = "cesu8" @@ -830,15 +830,15 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" dependencies = [ "iana-time-zone", "js-sys", "num-integer", "num-traits", - "time 0.1.44", + "time 0.1.45", "wasm-bindgen", "winapi 0.3.9", ] @@ -962,7 +962,7 @@ version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" dependencies = [ - "bytes 1.2.1", + "bytes 1.3.0", "memchr", ] @@ -1031,7 +1031,7 @@ dependencies = [ "serde", "serde_json", "thread_local", - "tokio 1.21.2", + "tokio 1.22.0", "tokio-stream", "tonic", "tracing", @@ -1061,9 +1061,9 @@ dependencies = [ [[package]] name = "constant_time_eq" -version = "0.1.5" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +checksum = "f3ad85c1f65dc7b37604eb0e89748faf0b9653065f2a8ef69f96a687ec1e9279" [[package]] name = "core-foundation" @@ -1174,22 +1174,22 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.11" +version = "0.9.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" dependencies = [ "autocfg", "cfg-if 1.0.0", "crossbeam-utils", - "memoffset", + "memoffset 0.7.1", "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" dependencies = [ "cfg-if 1.0.0", ] @@ -1285,7 +1285,7 @@ dependencies = [ "libc", "log", "signal-hook", - "tokio 1.21.2", + "tokio 1.22.0", "unicode-segmentation", "unicode-width", ] @@ -1330,7 +1330,7 @@ dependencies = [ "num", "owning_ref", "time 0.3.17", - "tokio 1.21.2", + "tokio 1.22.0", "toml", "unicode-segmentation", "unicode-width", @@ -1374,9 +1374,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97abf9f0eca9e52b7f81b945524e76710e6cb2366aead23b7d4fbf72e281f888" +checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453" dependencies = [ "cc", "cxxbridge-flags", @@ -1386,9 +1386,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc32cc5fea1d894b77d269ddb9f192110069a8a9c1f1d441195fba90553dea3" +checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0" dependencies = [ "cc", "codespan-reporting", @@ -1401,15 +1401,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca220e4794c934dc6b1207c3b42856ad4c302f2df1712e9f8d2eec5afaacf1f" +checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71" [[package]] name = "cxxbridge-macro" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b846f081361125bfc8dc9d3940c84e1fd83ba54bbca7b17cd29483c828be0704" +checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470" dependencies = [ "proc-macro2", "quote", @@ -1536,9 +1536,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer 0.10.3", "crypto-common", @@ -1813,12 +1813,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.6.2", ] [[package]] @@ -2120,7 +2120,7 @@ version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" dependencies = [ - "bytes 1.2.1", + "bytes 1.3.0", "fnv", "futures-core", "futures-sink", @@ -2128,7 +2128,7 @@ dependencies = [ "http", "indexmap", "slab", - "tokio 1.21.2", + "tokio 1.22.0", "tokio-util", "tracing", ] @@ -2236,7 +2236,7 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ - "bytes 1.2.1", + "bytes 1.3.0", "fnv", "itoa", ] @@ -2247,7 +2247,7 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ - "bytes 1.2.1", + "bytes 1.3.0", "http", "pin-project-lite 0.2.9", ] @@ -2282,7 +2282,7 @@ version = "0.14.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" dependencies = [ - "bytes 1.2.1", + "bytes 1.3.0", "futures-channel", "futures-core", "futures-util", @@ -2294,7 +2294,7 @@ dependencies = [ "itoa", "pin-project-lite 0.2.9", "socket2", - "tokio 1.21.2", + "tokio 1.22.0", "tower-service", "tracing", "want", @@ -2308,7 +2308,7 @@ checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ "hyper", "pin-project-lite 0.2.9", - "tokio 1.21.2", + "tokio 1.22.0", "tokio-io-timeout", ] @@ -2378,7 +2378,7 @@ name = "igd" version = "0.12.0" dependencies = [ "attohttpc", - "bytes 1.2.1", + "bytes 1.3.0", "futures", "http", "hyper", @@ -2387,7 +2387,7 @@ dependencies = [ "simplelog 0.9.0", "tokio 0.2.25", "tokio 0.3.7", - "tokio 1.21.2", + "tokio 1.22.0", "url", "xmltree", ] @@ -2438,9 +2438,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", "hashbrown", @@ -2487,9 +2487,9 @@ dependencies = [ [[package]] name = "ipconfig" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "723519edce41262b05d4143ceb95050e4c614f483e78e9fd9e39a8275a84ad98" +checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be" dependencies = [ "socket2", "widestring 0.5.1", @@ -2842,6 +2842,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "memory_units" version = "0.4.0" @@ -2869,6 +2878,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.6.23" @@ -2991,7 +3009,7 @@ checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" dependencies = [ "bitflags", "jni-sys", - "ndk-sys 0.4.0", + "ndk-sys 0.4.1+23.1.7779620", "num_enum", "raw-window-handle", "thiserror", @@ -3015,7 +3033,7 @@ dependencies = [ "ndk 0.7.0", "ndk-context", "ndk-macro", - "ndk-sys 0.4.0", + "ndk-sys 0.4.1+23.1.7779620", "once_cell", "parking_lot 0.12.1", ] @@ -3044,9 +3062,9 @@ dependencies = [ [[package]] name = "ndk-sys" -version = "0.4.0" +version = "0.4.1+23.1.7779620" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21d83ec9c63ec5bf950200a8e508bdad6659972187b625469f58ef8c08e29046" +checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" dependencies = [ "jni-sys", ] @@ -3098,12 +3116,12 @@ dependencies = [ name = "netlink-proto" version = "0.9.1" dependencies = [ - "bytes 1.2.1", + "bytes 1.3.0", "futures", "log", "netlink-packet-core", "netlink-sys", - "tokio 1.21.2", + "tokio 1.22.0", ] [[package]] @@ -3111,11 +3129,11 @@ name = "netlink-sys" version = "0.8.1" dependencies = [ "async-io", - "bytes 1.2.1", + "bytes 1.3.0", "futures", "libc", "log", - "tokio 1.21.2", + "tokio 1.22.0", ] [[package]] @@ -3128,7 +3146,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "libc", - "memoffset", + "memoffset 0.6.5", ] [[package]] @@ -3141,7 +3159,7 @@ dependencies = [ "bitflags", "cfg-if 1.0.0", "libc", - "memoffset", + "memoffset 0.6.5", "pin-utils", ] @@ -3362,7 +3380,7 @@ dependencies = [ "prost", "protobuf", "thiserror", - "tokio 1.21.2", + "tokio 1.22.0", "tonic", ] @@ -3426,7 +3444,7 @@ dependencies = [ "percent-encoding", "rand 0.8.5", "thiserror", - "tokio 1.21.2", + "tokio 1.22.0", "tokio-stream", ] @@ -3442,9 +3460,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.4.0" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5bf27447411e9ee3ff51186bf7a08e16c341efdde93f4d823e8844429bed7e" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" [[package]] name = "overload" @@ -3573,9 +3591,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a528564cc62c19a7acac4d81e01f39e53e25e17b934878f4c6d25cc2836e62f8" +checksum = "5f400b0f7905bf702f9f3dc3df5a121b16c54e9e8012c082905fdf09a931861a" dependencies = [ "thiserror", "ucd-trie", @@ -3583,9 +3601,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fd9bc6500181952d34bd0b2b0163a54d794227b498be0b7afa7698d0a7b18f" +checksum = "423c2ba011d6e27b02b482a3707c773d19aec65cc024637aec44e19652e66f63" dependencies = [ "pest", "pest_generator", @@ -3593,9 +3611,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2610d5ac5156217b4ff8e46ddcef7cdf44b273da2ac5bca2ecbfa86a330e7c4" +checksum = "3e64e6c2c85031c02fdbd9e5c72845445ca0a724d419aa0bc068ac620c9935c1" dependencies = [ "pest", "pest_meta", @@ -3606,9 +3624,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824749bf7e21dd66b36fbe26b3f45c713879cccd4a009a917ab8e045ca8246fe" +checksum = "57959b91f0a133f89a68be874a5c88ed689c19cd729ecdb5d762ebf16c64d662" dependencies = [ "once_cell", "pest", @@ -3840,7 +3858,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0841812012b2d4a6145fae9a6af1534873c32aa67fff26bd09f8fa42c83f95a" dependencies = [ - "bytes 1.2.1", + "bytes 1.3.0", "prost-derive", ] @@ -3850,7 +3868,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d8b442418ea0822409d9e7d047cbf1e7e9e1760b172bf9982cf29d517c93511" dependencies = [ - "bytes 1.2.1", + "bytes 1.3.0", "heck", "itertools", "lazy_static", @@ -3885,7 +3903,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "747761bc3dc48f9a34553bf65605cf6cb6288ba219f3450b4275dbd81539551a" dependencies = [ - "bytes 1.2.1", + "bytes 1.3.0", "prost", ] @@ -4018,11 +4036,10 @@ dependencies = [ [[package]] name = "rayon" -version = "1.5.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +checksum = "1e060280438193c554f654141c9ea9417886713b7acd75974c85b18a69a88e0b" dependencies = [ - "autocfg", "crossbeam-deque", "either", "rayon-core", @@ -4030,9 +4047,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.9.3" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -4158,7 +4175,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" dependencies = [ - "bytes 1.2.1", + "bytes 1.3.0", "rustc-hex", ] @@ -4206,7 +4223,7 @@ dependencies = [ "netlink-proto", "nix 0.22.3", "thiserror", - "tokio 1.21.2", + "tokio 1.22.0", ] [[package]] @@ -4476,9 +4493,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.87" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" +checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ "itoa", "ryu", @@ -4556,7 +4573,7 @@ checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest 0.10.5", + "digest 0.10.6", ] [[package]] @@ -4567,7 +4584,7 @@ checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest 0.10.5", + "digest 0.10.6", ] [[package]] @@ -4791,9 +4808,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.26.7" +version = "0.26.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c375d5fd899e32847b8566e10598d6e9f1d9b55ec6de3cdf9e7da4bdc51371bc" +checksum = "29ddf41e393a9133c81d5f0974195366bd57082deac6e0eb02ed39b8341c2bb6" dependencies = [ "cfg-if 1.0.0", "core-foundation-sys 0.8.3", @@ -4870,9 +4887,9 @@ dependencies = [ [[package]] name = "time" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" dependencies = [ "libc", "wasi 0.10.0+wasi-snapshot-preview1", @@ -4972,12 +4989,12 @@ dependencies = [ [[package]] name = "tokio" -version = "1.21.2" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" dependencies = [ "autocfg", - "bytes 1.2.1", + "bytes 1.3.0", "libc", "memchr", "mio 0.8.5", @@ -4998,7 +5015,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ "pin-project-lite 0.2.9", - "tokio 1.21.2", + "tokio 1.22.0", ] [[package]] @@ -5020,7 +5037,7 @@ checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" dependencies = [ "futures-core", "pin-project-lite 0.2.9", - "tokio 1.21.2", + "tokio 1.22.0", ] [[package]] @@ -5029,12 +5046,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" dependencies = [ - "bytes 1.2.1", + "bytes 1.3.0", "futures-core", "futures-io", "futures-sink", "pin-project-lite 0.2.9", - "tokio 1.21.2", + "tokio 1.22.0", "tracing", ] @@ -5057,7 +5074,7 @@ dependencies = [ "async-trait", "axum", "base64 0.13.1", - "bytes 1.2.1", + "bytes 1.3.0", "futures-core", "futures-util", "h2", @@ -5069,7 +5086,7 @@ dependencies = [ "pin-project 1.0.12", "prost", "prost-derive", - "tokio 1.21.2", + "tokio 1.22.0", "tokio-stream", "tokio-util", "tower", @@ -5105,7 +5122,7 @@ dependencies = [ "pin-project-lite 0.2.9", "rand 0.8.5", "slab", - "tokio 1.21.2", + "tokio 1.22.0", "tokio-util", "tower-layer", "tower-service", @@ -5119,7 +5136,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba" dependencies = [ "bitflags", - "bytes 1.2.1", + "bytes 1.3.0", "futures-core", "futures-util", "http", @@ -5304,7 +5321,7 @@ dependencies = [ "smallvec", "thiserror", "tinyvec", - "tokio 1.21.2", + "tokio 1.22.0", "tracing", "url", ] @@ -5324,7 +5341,7 @@ dependencies = [ "resolv-conf", "smallvec", "thiserror", - "tokio 1.21.2", + "tokio 1.22.0", "tracing", "trust-dns-proto", ] @@ -5362,7 +5379,7 @@ checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" dependencies = [ "base64 0.13.1", "byteorder", - "bytes 1.2.1", + "bytes 1.3.0", "http", "httparse", "log", @@ -5532,7 +5549,7 @@ dependencies = [ "serde_derive", "serial_test", "thiserror", - "tokio 1.21.2", + "tokio 1.22.0", "tokio-util", "veilid-core", ] @@ -5611,7 +5628,7 @@ dependencies = [ "static_assertions", "stop-token", "thiserror", - "tokio 1.21.2", + "tokio 1.22.0", "tokio-stream", "tokio-util", "tracing", @@ -5655,7 +5672,7 @@ dependencies = [ "parking_lot 0.12.1", "serde", "serde_json", - "tokio 1.21.2", + "tokio 1.22.0", "tokio-stream", "tokio-util", "tracing", @@ -5702,7 +5719,7 @@ dependencies = [ "signal-hook", "signal-hook-async-std", "stop-token", - "tokio 1.21.2", + "tokio 1.22.0", "tokio-stream", "tokio-util", "tracing", @@ -6201,9 +6218,9 @@ checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" [[package]] name = "winreg" -version = "0.7.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ "winapi 0.3.9", ] @@ -6238,9 +6255,9 @@ dependencies = [ [[package]] name = "wyz" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b31594f29d27036c383b53b59ed3476874d518f0efb151b27a4c275141390e" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" dependencies = [ "tap", ] diff --git a/veilid-core/src/core_context.rs b/veilid-core/src/core_context.rs index 6f39129a..8af47ab5 100644 --- a/veilid-core/src/core_context.rs +++ b/veilid-core/src/core_context.rs @@ -4,6 +4,7 @@ use crate::crypto::Crypto; use crate::veilid_api::*; use crate::veilid_config::*; use crate::xx::*; +use crate::*; pub type UpdateCallback = Arc; @@ -203,7 +204,7 @@ impl VeilidCoreContext { if #[cfg(target_os = "android")] { if crate::intf::utils::android::ANDROID_GLOBALS.lock().is_none() { error!("Android globals are not set up"); - return Err(VeilidAPIError::Internal { message: "Android globals are not set up".to_owned() }); + apibail_internal!("Android globals are not set up"); } } } @@ -251,7 +252,7 @@ pub async fn api_startup( // See if we have an API started up already let mut initialized_lock = INITIALIZED.lock().await; if *initialized_lock { - return Err(VeilidAPIError::AlreadyInitialized); + apibail_already_initialized!(); } // Create core context @@ -274,7 +275,7 @@ pub async fn api_startup_json( // See if we have an API started up already let mut initialized_lock = INITIALIZED.lock().await; if *initialized_lock { - return Err(VeilidAPIError::AlreadyInitialized); + apibail_already_initialized!(); } // Create core context diff --git a/veilid-core/src/crypto/tests/test_crypto.rs b/veilid-core/src/crypto/tests/test_crypto.rs index e1d09294..c1a03800 100644 --- a/veilid-core/src/crypto/tests/test_crypto.rs +++ b/veilid-core/src/crypto/tests/test_crypto.rs @@ -1,7 +1,6 @@ use super::*; use crate::tests::common::test_veilid_config::*; use crate::xx::*; -use crate::*; static LOREM_IPSUM:&[u8] = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. "; diff --git a/veilid-core/src/crypto/tests/test_envelope_receipt.rs b/veilid-core/src/crypto/tests/test_envelope_receipt.rs index 01b7ddf7..1a221dd3 100644 --- a/veilid-core/src/crypto/tests/test_envelope_receipt.rs +++ b/veilid-core/src/crypto/tests/test_envelope_receipt.rs @@ -1,7 +1,6 @@ use super::*; use crate::tests::common::test_veilid_config::*; use crate::xx::*; -use crate::*; pub async fn test_envelope_round_trip() { info!("--- test envelope round trip ---"); diff --git a/veilid-core/src/network_manager/connection_manager.rs b/veilid-core/src/network_manager/connection_manager.rs index e83f069d..30217cce 100644 --- a/veilid-core/src/network_manager/connection_manager.rs +++ b/veilid-core/src/network_manager/connection_manager.rs @@ -1,5 +1,4 @@ use super::*; -use crate::xx::*; use connection_table::*; use network_connection::*; use stop_token::future::FutureExt; diff --git a/veilid-core/src/network_manager/mod.rs b/veilid-core/src/network_manager/mod.rs index f9a22da3..52bc61a5 100644 --- a/veilid-core/src/network_manager/mod.rs +++ b/veilid-core/src/network_manager/mod.rs @@ -1,4 +1,5 @@ use crate::*; +use crate::xx::*; #[cfg(not(target_arch = "wasm32"))] mod native; @@ -33,7 +34,6 @@ use routing_table::*; use rpc_processor::*; #[cfg(target_arch = "wasm32")] use wasm::*; -use xx::*; //////////////////////////////////////////////////////////////////////////////////////// diff --git a/veilid-core/src/network_manager/native/igd_manager.rs b/veilid-core/src/network_manager/native/igd_manager.rs index fe06c70f..fcc46e64 100644 --- a/veilid-core/src/network_manager/native/igd_manager.rs +++ b/veilid-core/src/network_manager/native/igd_manager.rs @@ -1,5 +1,4 @@ use super::*; -use crate::xx::*; use igd::*; use std::net::UdpSocket; diff --git a/veilid-core/src/network_manager/tasks/public_address_check.rs b/veilid-core/src/network_manager/tasks/public_address_check.rs index 92303369..2f7ccc46 100644 --- a/veilid-core/src/network_manager/tasks/public_address_check.rs +++ b/veilid-core/src/network_manager/tasks/public_address_check.rs @@ -1,5 +1,4 @@ -use super::super::*; -use crate::xx::*; +use super::*; impl NetworkManager { // Clean up the public address check tables, removing entries that have timed out diff --git a/veilid-core/src/network_manager/tasks/rolling_transfers.rs b/veilid-core/src/network_manager/tasks/rolling_transfers.rs index 4007a4a9..c3774b6a 100644 --- a/veilid-core/src/network_manager/tasks/rolling_transfers.rs +++ b/veilid-core/src/network_manager/tasks/rolling_transfers.rs @@ -1,6 +1,4 @@ -use super::super::*; - -use crate::xx::*; +use super::*; impl NetworkManager { // Compute transfer statistics for the low level network diff --git a/veilid-core/src/network_manager/tests/test_connection_table.rs b/veilid-core/src/network_manager/tests/test_connection_table.rs index 79306051..6f720107 100644 --- a/veilid-core/src/network_manager/tests/test_connection_table.rs +++ b/veilid-core/src/network_manager/tests/test_connection_table.rs @@ -1,8 +1,7 @@ +use super::*; + use super::connection_table::*; -use super::network_connection::*; use crate::tests::common::test_veilid_config::*; -use crate::xx::*; -use crate::*; pub async fn test_add_get_remove() { let config = get_config(); diff --git a/veilid-core/src/network_manager/wasm/mod.rs b/veilid-core/src/network_manager/wasm/mod.rs index 2489e191..4aca58e4 100644 --- a/veilid-core/src/network_manager/wasm/mod.rs +++ b/veilid-core/src/network_manager/wasm/mod.rs @@ -1,6 +1,7 @@ mod protocol; use super::*; + use crate::routing_table::*; use connection_manager::*; use protocol::ws::WebsocketProtocolHandler; diff --git a/veilid-core/src/network_manager/wasm/protocol/mod.rs b/veilid-core/src/network_manager/wasm/protocol/mod.rs index 16edb5ee..bc4966ca 100644 --- a/veilid-core/src/network_manager/wasm/protocol/mod.rs +++ b/veilid-core/src/network_manager/wasm/protocol/mod.rs @@ -2,7 +2,6 @@ pub mod wrtc; pub mod ws; use super::*; -use crate::xx::*; use std::io; #[derive(Debug)] diff --git a/veilid-core/src/routing_table/mod.rs b/veilid-core/src/routing_table/mod.rs index c680eb1a..bb8a4058 100644 --- a/veilid-core/src/routing_table/mod.rs +++ b/veilid-core/src/routing_table/mod.rs @@ -11,11 +11,12 @@ mod routing_table_inner; mod stats_accounting; mod tasks; +use crate::xx::*; +use crate::*; + use crate::crypto::*; use crate::network_manager::*; use crate::rpc_processor::*; -use crate::xx::*; -use crate::*; use bucket::*; pub use bucket_entry::*; pub use debug::*; diff --git a/veilid-core/src/routing_table/tasks/bootstrap.rs b/veilid-core/src/routing_table/tasks/bootstrap.rs index d7d3da7f..6b16f84a 100644 --- a/veilid-core/src/routing_table/tasks/bootstrap.rs +++ b/veilid-core/src/routing_table/tasks/bootstrap.rs @@ -1,5 +1,4 @@ -use super::super::*; -use crate::xx::*; +use super::*; use futures_util::stream::{FuturesUnordered, StreamExt}; use stop_token::future::FutureExt as StopFutureExt; diff --git a/veilid-core/src/routing_table/tasks/kick_buckets.rs b/veilid-core/src/routing_table/tasks/kick_buckets.rs index 730bad1d..38eef8af 100644 --- a/veilid-core/src/routing_table/tasks/kick_buckets.rs +++ b/veilid-core/src/routing_table/tasks/kick_buckets.rs @@ -1,5 +1,4 @@ -use super::super::*; -use crate::xx::*; +use super::*; impl RoutingTable { // Kick the queued buckets in the routing table to free dead nodes if necessary diff --git a/veilid-core/src/routing_table/tasks/peer_minimum_refresh.rs b/veilid-core/src/routing_table/tasks/peer_minimum_refresh.rs index 7733755c..157e6030 100644 --- a/veilid-core/src/routing_table/tasks/peer_minimum_refresh.rs +++ b/veilid-core/src/routing_table/tasks/peer_minimum_refresh.rs @@ -1,5 +1,4 @@ -use super::super::*; -use crate::xx::*; +use super::*; use futures_util::stream::{FuturesOrdered, StreamExt}; use stop_token::future::FutureExt as StopFutureExt; diff --git a/veilid-core/src/routing_table/tasks/ping_validator.rs b/veilid-core/src/routing_table/tasks/ping_validator.rs index 976fb79b..2460d29f 100644 --- a/veilid-core/src/routing_table/tasks/ping_validator.rs +++ b/veilid-core/src/routing_table/tasks/ping_validator.rs @@ -1,5 +1,4 @@ -use super::super::*; -use crate::xx::*; +use super::*; use futures_util::stream::{FuturesUnordered, StreamExt}; use futures_util::FutureExt; diff --git a/veilid-core/src/routing_table/tasks/private_route_management.rs b/veilid-core/src/routing_table/tasks/private_route_management.rs index 3718366f..f9929e2c 100644 --- a/veilid-core/src/routing_table/tasks/private_route_management.rs +++ b/veilid-core/src/routing_table/tasks/private_route_management.rs @@ -1,5 +1,4 @@ -use super::super::*; -use crate::xx::*; +use super::*; use futures_util::stream::{FuturesUnordered, StreamExt}; use futures_util::FutureExt; diff --git a/veilid-core/src/routing_table/tasks/relay_management.rs b/veilid-core/src/routing_table/tasks/relay_management.rs index 85f4fd8a..056479e2 100644 --- a/veilid-core/src/routing_table/tasks/relay_management.rs +++ b/veilid-core/src/routing_table/tasks/relay_management.rs @@ -1,5 +1,4 @@ -use super::super::*; -use crate::xx::*; +use super::*; impl RoutingTable { // Keep relays assigned and accessible diff --git a/veilid-core/src/routing_table/tasks/rolling_transfers.rs b/veilid-core/src/routing_table/tasks/rolling_transfers.rs index b97b8afc..04177c01 100644 --- a/veilid-core/src/routing_table/tasks/rolling_transfers.rs +++ b/veilid-core/src/routing_table/tasks/rolling_transfers.rs @@ -1,5 +1,4 @@ -use super::super::*; -use crate::xx::*; +use super::*; impl RoutingTable { // Compute transfer statistics to determine how 'fast' a node is diff --git a/veilid-core/src/rpc_processor/coders/address.rs b/veilid-core/src/rpc_processor/coders/address.rs index ff3aabea..7e18ac02 100644 --- a/veilid-core/src/rpc_processor/coders/address.rs +++ b/veilid-core/src/rpc_processor/coders/address.rs @@ -1,7 +1,5 @@ -use crate::xx::*; -use crate::*; +use super::*; use core::convert::TryInto; -use rpc_processor::*; pub fn encode_address( address: &Address, diff --git a/veilid-core/src/rpc_processor/coders/address_type_set.rs b/veilid-core/src/rpc_processor/coders/address_type_set.rs index 67a40159..f341d980 100644 --- a/veilid-core/src/rpc_processor/coders/address_type_set.rs +++ b/veilid-core/src/rpc_processor/coders/address_type_set.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; pub fn encode_address_type_set( address_type_set: &AddressTypeSet, diff --git a/veilid-core/src/rpc_processor/coders/dht_key.rs b/veilid-core/src/rpc_processor/coders/dht_key.rs index dd7a8909..bc1e3f22 100644 --- a/veilid-core/src/rpc_processor/coders/dht_key.rs +++ b/veilid-core/src/rpc_processor/coders/dht_key.rs @@ -1,7 +1,5 @@ -use crate::crypto::*; -use crate::*; +use super::*; use core::convert::TryInto; -use rpc_processor::*; pub fn decode_dht_key(public_key: &veilid_capnp::key256::Reader) -> DHTKey { let u0 = public_key.get_u0().to_be_bytes(); diff --git a/veilid-core/src/rpc_processor/coders/dht_signature.rs b/veilid-core/src/rpc_processor/coders/dht_signature.rs index 9e008faa..5b7427b2 100644 --- a/veilid-core/src/rpc_processor/coders/dht_signature.rs +++ b/veilid-core/src/rpc_processor/coders/dht_signature.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; pub fn encode_signature(sig: &DHTSignature, builder: &mut veilid_capnp::signature512::Builder) { let sig = &sig.bytes; diff --git a/veilid-core/src/rpc_processor/coders/dial_info.rs b/veilid-core/src/rpc_processor/coders/dial_info.rs index c178bb0c..400075b8 100644 --- a/veilid-core/src/rpc_processor/coders/dial_info.rs +++ b/veilid-core/src/rpc_processor/coders/dial_info.rs @@ -1,7 +1,5 @@ -use crate::xx::*; -use crate::*; +use super::*; use core::convert::TryInto; -use rpc_processor::*; pub fn decode_dial_info(reader: &veilid_capnp::dial_info::Reader) -> Result { match reader diff --git a/veilid-core/src/rpc_processor/coders/dial_info_class.rs b/veilid-core/src/rpc_processor/coders/dial_info_class.rs index 71197171..835f060e 100644 --- a/veilid-core/src/rpc_processor/coders/dial_info_class.rs +++ b/veilid-core/src/rpc_processor/coders/dial_info_class.rs @@ -1,4 +1,4 @@ -use crate::*; +use super::*; pub fn encode_dial_info_class(dial_info_class: DialInfoClass) -> veilid_capnp::DialInfoClass { match dial_info_class { diff --git a/veilid-core/src/rpc_processor/coders/dial_info_detail.rs b/veilid-core/src/rpc_processor/coders/dial_info_detail.rs index c3dc1b27..7b012fe3 100644 --- a/veilid-core/src/rpc_processor/coders/dial_info_detail.rs +++ b/veilid-core/src/rpc_processor/coders/dial_info_detail.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; pub fn encode_dial_info_detail( dial_info_detail: &DialInfoDetail, diff --git a/veilid-core/src/rpc_processor/coders/network_class.rs b/veilid-core/src/rpc_processor/coders/network_class.rs index 88d17fce..65bb9e74 100644 --- a/veilid-core/src/rpc_processor/coders/network_class.rs +++ b/veilid-core/src/rpc_processor/coders/network_class.rs @@ -1,4 +1,4 @@ -use crate::*; +use super::*; pub fn encode_network_class(network_class: NetworkClass) -> veilid_capnp::NetworkClass { match network_class { diff --git a/veilid-core/src/rpc_processor/coders/node_info.rs b/veilid-core/src/rpc_processor/coders/node_info.rs index 02f9bd78..14ab95b3 100644 --- a/veilid-core/src/rpc_processor/coders/node_info.rs +++ b/veilid-core/src/rpc_processor/coders/node_info.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; pub fn encode_node_info( node_info: &NodeInfo, diff --git a/veilid-core/src/rpc_processor/coders/node_status.rs b/veilid-core/src/rpc_processor/coders/node_status.rs index a07e977f..ed72123b 100644 --- a/veilid-core/src/rpc_processor/coders/node_status.rs +++ b/veilid-core/src/rpc_processor/coders/node_status.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; pub fn encode_public_internet_node_status( public_internet_node_status: &PublicInternetNodeStatus, diff --git a/veilid-core/src/rpc_processor/coders/nonce.rs b/veilid-core/src/rpc_processor/coders/nonce.rs index 5eb39dce..ac7d48c0 100644 --- a/veilid-core/src/rpc_processor/coders/nonce.rs +++ b/veilid-core/src/rpc_processor/coders/nonce.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; pub fn encode_nonce(nonce: &Nonce, builder: &mut veilid_capnp::nonce24::Builder) { builder.set_u0(u64::from_be_bytes( diff --git a/veilid-core/src/rpc_processor/coders/operations/answer.rs b/veilid-core/src/rpc_processor/coders/operations/answer.rs index e6d801a3..7dd429a2 100644 --- a/veilid-core/src/rpc_processor/coders/operations/answer.rs +++ b/veilid-core/src/rpc_processor/coders/operations/answer.rs @@ -1,6 +1,4 @@ use super::*; -use crate::*; -use rpc_processor::*; #[derive(Debug, Clone)] pub struct RPCAnswer { diff --git a/veilid-core/src/rpc_processor/coders/operations/mod.rs b/veilid-core/src/rpc_processor/coders/operations/mod.rs index 687ca68e..3c91d344 100644 --- a/veilid-core/src/rpc_processor/coders/operations/mod.rs +++ b/veilid-core/src/rpc_processor/coders/operations/mod.rs @@ -45,3 +45,5 @@ pub use operation_watch_value::*; pub use question::*; pub use respond_to::*; pub use statement::*; + +use super::*; diff --git a/veilid-core/src/rpc_processor/coders/operations/operation.rs b/veilid-core/src/rpc_processor/coders/operations/operation.rs index a33ab29c..aeeacf08 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; #[derive(Debug, Clone)] pub enum RPCOperationKind { diff --git a/veilid-core/src/rpc_processor/coders/operations/operation_app_call.rs b/veilid-core/src/rpc_processor/coders/operations/operation_app_call.rs index 609999bb..b1360b9a 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation_app_call.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation_app_call.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; #[derive(Debug, Clone)] pub struct RPCOperationAppCallQ { diff --git a/veilid-core/src/rpc_processor/coders/operations/operation_app_message.rs b/veilid-core/src/rpc_processor/coders/operations/operation_app_message.rs index 5a844f02..5c969be7 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation_app_message.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation_app_message.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; #[derive(Debug, Clone)] pub struct RPCOperationAppMessage { diff --git a/veilid-core/src/rpc_processor/coders/operations/operation_cancel_tunnel.rs b/veilid-core/src/rpc_processor/coders/operations/operation_cancel_tunnel.rs index c9ca4e96..d5ca9ad2 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation_cancel_tunnel.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation_cancel_tunnel.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; #[derive(Debug, Clone)] pub struct RPCOperationCancelTunnelQ { diff --git a/veilid-core/src/rpc_processor/coders/operations/operation_complete_tunnel.rs b/veilid-core/src/rpc_processor/coders/operations/operation_complete_tunnel.rs index 453c38c0..49dc90a8 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation_complete_tunnel.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation_complete_tunnel.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; #[derive(Debug, Clone)] pub struct RPCOperationCompleteTunnelQ { diff --git a/veilid-core/src/rpc_processor/coders/operations/operation_find_block.rs b/veilid-core/src/rpc_processor/coders/operations/operation_find_block.rs index 5503ad31..ce42da3f 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation_find_block.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation_find_block.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; #[derive(Debug, Clone)] pub struct RPCOperationFindBlockQ { diff --git a/veilid-core/src/rpc_processor/coders/operations/operation_find_node.rs b/veilid-core/src/rpc_processor/coders/operations/operation_find_node.rs index 95ca3ea5..cf6bb675 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation_find_node.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation_find_node.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; #[derive(Debug, Clone)] pub struct RPCOperationFindNodeQ { diff --git a/veilid-core/src/rpc_processor/coders/operations/operation_get_value.rs b/veilid-core/src/rpc_processor/coders/operations/operation_get_value.rs index bb800511..f9fc9959 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation_get_value.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation_get_value.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; #[derive(Debug, Clone)] pub struct RPCOperationGetValueQ { diff --git a/veilid-core/src/rpc_processor/coders/operations/operation_node_info_update.rs b/veilid-core/src/rpc_processor/coders/operations/operation_node_info_update.rs index 5f077816..386805a3 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation_node_info_update.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation_node_info_update.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; #[derive(Debug, Clone)] pub struct RPCOperationNodeInfoUpdate { diff --git a/veilid-core/src/rpc_processor/coders/operations/operation_return_receipt.rs b/veilid-core/src/rpc_processor/coders/operations/operation_return_receipt.rs index 31c1d213..bd7517a7 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation_return_receipt.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation_return_receipt.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; #[derive(Debug, Clone)] pub struct RPCOperationReturnReceipt { diff --git a/veilid-core/src/rpc_processor/coders/operations/operation_route.rs b/veilid-core/src/rpc_processor/coders/operations/operation_route.rs index 68c97191..29e56035 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation_route.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation_route.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; #[derive(Debug, Clone)] pub struct RoutedOperation { diff --git a/veilid-core/src/rpc_processor/coders/operations/operation_set_value.rs b/veilid-core/src/rpc_processor/coders/operations/operation_set_value.rs index 18c430d1..23a34421 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation_set_value.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation_set_value.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; #[derive(Debug, Clone)] pub struct RPCOperationSetValueQ { diff --git a/veilid-core/src/rpc_processor/coders/operations/operation_signal.rs b/veilid-core/src/rpc_processor/coders/operations/operation_signal.rs index e31a6f47..4b8a6fd3 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation_signal.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation_signal.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; #[derive(Debug, Clone)] pub struct RPCOperationSignal { diff --git a/veilid-core/src/rpc_processor/coders/operations/operation_start_tunnel.rs b/veilid-core/src/rpc_processor/coders/operations/operation_start_tunnel.rs index 08c1982b..d58e625a 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation_start_tunnel.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation_start_tunnel.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; #[derive(Debug, Clone)] pub struct RPCOperationStartTunnelQ { diff --git a/veilid-core/src/rpc_processor/coders/operations/operation_status.rs b/veilid-core/src/rpc_processor/coders/operations/operation_status.rs index 71ed2117..9ab480a8 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation_status.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation_status.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; #[derive(Debug, Clone)] pub struct RPCOperationStatusQ { diff --git a/veilid-core/src/rpc_processor/coders/operations/operation_supply_block.rs b/veilid-core/src/rpc_processor/coders/operations/operation_supply_block.rs index 67b7ab00..d593650d 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation_supply_block.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation_supply_block.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; #[derive(Debug, Clone)] pub struct RPCOperationSupplyBlockQ { diff --git a/veilid-core/src/rpc_processor/coders/operations/operation_validate_dial_info.rs b/veilid-core/src/rpc_processor/coders/operations/operation_validate_dial_info.rs index a11f2501..63a8bd40 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation_validate_dial_info.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation_validate_dial_info.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; #[derive(Debug, Clone)] pub struct RPCOperationValidateDialInfo { diff --git a/veilid-core/src/rpc_processor/coders/operations/operation_value_changed.rs b/veilid-core/src/rpc_processor/coders/operations/operation_value_changed.rs index c1847118..3d1c08cf 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation_value_changed.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation_value_changed.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; #[derive(Debug, Clone)] pub struct RPCOperationValueChanged { diff --git a/veilid-core/src/rpc_processor/coders/operations/operation_watch_value.rs b/veilid-core/src/rpc_processor/coders/operations/operation_watch_value.rs index cbb08fcb..00c0199c 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation_watch_value.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation_watch_value.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; #[derive(Debug, Clone)] pub struct RPCOperationWatchValueQ { diff --git a/veilid-core/src/rpc_processor/coders/operations/question.rs b/veilid-core/src/rpc_processor/coders/operations/question.rs index 02995b26..4e7e3966 100644 --- a/veilid-core/src/rpc_processor/coders/operations/question.rs +++ b/veilid-core/src/rpc_processor/coders/operations/question.rs @@ -1,6 +1,4 @@ use super::*; -use crate::*; -use rpc_processor::*; #[derive(Debug, Clone)] pub struct RPCQuestion { diff --git a/veilid-core/src/rpc_processor/coders/operations/respond_to.rs b/veilid-core/src/rpc_processor/coders/operations/respond_to.rs index 79c4e358..f05b6d08 100644 --- a/veilid-core/src/rpc_processor/coders/operations/respond_to.rs +++ b/veilid-core/src/rpc_processor/coders/operations/respond_to.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; #[derive(Debug, Clone)] pub enum RespondTo { diff --git a/veilid-core/src/rpc_processor/coders/operations/statement.rs b/veilid-core/src/rpc_processor/coders/operations/statement.rs index 96c71a4c..3a019dce 100644 --- a/veilid-core/src/rpc_processor/coders/operations/statement.rs +++ b/veilid-core/src/rpc_processor/coders/operations/statement.rs @@ -1,6 +1,4 @@ use super::*; -use crate::*; -use rpc_processor::*; #[derive(Debug, Clone)] pub struct RPCStatement { diff --git a/veilid-core/src/rpc_processor/coders/peer_info.rs b/veilid-core/src/rpc_processor/coders/peer_info.rs index 5c7e67ab..5844b8ab 100644 --- a/veilid-core/src/rpc_processor/coders/peer_info.rs +++ b/veilid-core/src/rpc_processor/coders/peer_info.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; pub fn encode_peer_info( peer_info: &PeerInfo, diff --git a/veilid-core/src/rpc_processor/coders/protocol_type_set.rs b/veilid-core/src/rpc_processor/coders/protocol_type_set.rs index 37db7e19..6ebf71e7 100644 --- a/veilid-core/src/rpc_processor/coders/protocol_type_set.rs +++ b/veilid-core/src/rpc_processor/coders/protocol_type_set.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; pub fn encode_protocol_type_set( protocol_type_set: &ProtocolTypeSet, diff --git a/veilid-core/src/rpc_processor/coders/sender_info.rs b/veilid-core/src/rpc_processor/coders/sender_info.rs index 5fbea344..45fd96c8 100644 --- a/veilid-core/src/rpc_processor/coders/sender_info.rs +++ b/veilid-core/src/rpc_processor/coders/sender_info.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; pub fn encode_sender_info( sender_info: &SenderInfo, diff --git a/veilid-core/src/rpc_processor/coders/signal_info.rs b/veilid-core/src/rpc_processor/coders/signal_info.rs index 7a272973..5e9edc84 100644 --- a/veilid-core/src/rpc_processor/coders/signal_info.rs +++ b/veilid-core/src/rpc_processor/coders/signal_info.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; pub fn encode_signal_info( signal_info: &SignalInfo, diff --git a/veilid-core/src/rpc_processor/coders/signed_direct_node_info.rs b/veilid-core/src/rpc_processor/coders/signed_direct_node_info.rs index a04d3445..7b583e21 100644 --- a/veilid-core/src/rpc_processor/coders/signed_direct_node_info.rs +++ b/veilid-core/src/rpc_processor/coders/signed_direct_node_info.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; pub fn encode_signed_direct_node_info( signed_direct_node_info: &SignedDirectNodeInfo, diff --git a/veilid-core/src/rpc_processor/coders/signed_node_info.rs b/veilid-core/src/rpc_processor/coders/signed_node_info.rs index 64ae9c80..2af7cefd 100644 --- a/veilid-core/src/rpc_processor/coders/signed_node_info.rs +++ b/veilid-core/src/rpc_processor/coders/signed_node_info.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; pub fn encode_signed_node_info( signed_node_info: &SignedNodeInfo, diff --git a/veilid-core/src/rpc_processor/coders/signed_relayed_node_info.rs b/veilid-core/src/rpc_processor/coders/signed_relayed_node_info.rs index 646f6597..924a00ad 100644 --- a/veilid-core/src/rpc_processor/coders/signed_relayed_node_info.rs +++ b/veilid-core/src/rpc_processor/coders/signed_relayed_node_info.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; pub fn encode_signed_relayed_node_info( signed_relayed_node_info: &SignedRelayedNodeInfo, diff --git a/veilid-core/src/rpc_processor/coders/socket_address.rs b/veilid-core/src/rpc_processor/coders/socket_address.rs index 3a9c2a42..41542550 100644 --- a/veilid-core/src/rpc_processor/coders/socket_address.rs +++ b/veilid-core/src/rpc_processor/coders/socket_address.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; pub fn encode_socket_address( socket_address: &SocketAddress, diff --git a/veilid-core/src/rpc_processor/coders/tunnel.rs b/veilid-core/src/rpc_processor/coders/tunnel.rs index da12781c..72d2470a 100644 --- a/veilid-core/src/rpc_processor/coders/tunnel.rs +++ b/veilid-core/src/rpc_processor/coders/tunnel.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; pub fn encode_tunnel_mode(tunnel_mode: TunnelMode) -> veilid_capnp::TunnelEndpointMode { match tunnel_mode { diff --git a/veilid-core/src/rpc_processor/coders/value_data.rs b/veilid-core/src/rpc_processor/coders/value_data.rs index cd3ad900..ba859423 100644 --- a/veilid-core/src/rpc_processor/coders/value_data.rs +++ b/veilid-core/src/rpc_processor/coders/value_data.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; pub fn encode_value_data( value_data: &ValueData, diff --git a/veilid-core/src/rpc_processor/coders/value_key.rs b/veilid-core/src/rpc_processor/coders/value_key.rs index 7561b3c6..fcb1c0a2 100644 --- a/veilid-core/src/rpc_processor/coders/value_key.rs +++ b/veilid-core/src/rpc_processor/coders/value_key.rs @@ -1,5 +1,4 @@ -use crate::*; -use rpc_processor::*; +use super::*; pub fn encode_value_key( value_key: &ValueKey, diff --git a/veilid-core/src/tests/common/mod.rs b/veilid-core/src/tests/common/mod.rs index ab47cdd5..a2e63d10 100644 --- a/veilid-core/src/tests/common/mod.rs +++ b/veilid-core/src/tests/common/mod.rs @@ -4,8 +4,3 @@ pub mod test_protected_store; pub mod test_table_store; pub mod test_veilid_config; pub mod test_veilid_core; - -use super::*; - -pub use crypto::tests::*; -pub use network_manager::tests::*; diff --git a/veilid-core/src/tests/mod.rs b/veilid-core/src/tests/mod.rs index a4bdf876..0b6b6216 100644 --- a/veilid-core/src/tests/mod.rs +++ b/veilid-core/src/tests/mod.rs @@ -1,5 +1,3 @@ pub mod common; #[cfg(not(target_arch = "wasm32"))] mod native; - -use super::*; diff --git a/veilid-core/src/tests/native/mod.rs b/veilid-core/src/tests/native/mod.rs index d693eaa5..b41ab132 100644 --- a/veilid-core/src/tests/native/mod.rs +++ b/veilid-core/src/tests/native/mod.rs @@ -3,10 +3,11 @@ mod test_async_peek_stream; +use crate::xx::*; + use crate::crypto::tests::*; use crate::network_manager::tests::*; use crate::tests::common::*; -use crate::xx::*; #[cfg(all(target_os = "android", feature = "android_tests"))] use jni::{objects::JClass, objects::JObject, JNIEnv}; diff --git a/veilid-core/src/tests/native/test_async_peek_stream.rs b/veilid-core/src/tests/native/test_async_peek_stream.rs index f73f95b2..2aaa5d97 100644 --- a/veilid-core/src/tests/native/test_async_peek_stream.rs +++ b/veilid-core/src/tests/native/test_async_peek_stream.rs @@ -1,4 +1,4 @@ -use super::*; +use crate::xx::*; cfg_if! { if #[cfg(feature="rt-async-std")] { diff --git a/veilid-core/src/veilid_api/api.rs b/veilid-core/src/veilid_api/api.rs new file mode 100644 index 00000000..2028023e --- /dev/null +++ b/veilid-core/src/veilid_api/api.rs @@ -0,0 +1,284 @@ +use super::*; + +///////////////////////////////////////////////////////////////////////////////////////////////////// + +struct VeilidAPIInner { + context: Option, +} + +impl fmt::Debug for VeilidAPIInner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "VeilidAPIInner") + } +} + +impl Drop for VeilidAPIInner { + fn drop(&mut self) { + if let Some(context) = self.context.take() { + intf::spawn_detached(api_shutdown(context)); + } + } +} + +#[derive(Clone, Debug)] +pub struct VeilidAPI { + inner: Arc>, +} + +impl VeilidAPI { + #[instrument(skip_all)] + pub(crate) fn new(context: VeilidCoreContext) -> Self { + Self { + inner: Arc::new(Mutex::new(VeilidAPIInner { + context: Some(context), + })), + } + } + + #[instrument(skip_all)] + pub async fn shutdown(self) { + let context = { self.inner.lock().context.take() }; + if let Some(context) = context { + api_shutdown(context).await; + } + } + + pub fn is_shutdown(&self) -> bool { + self.inner.lock().context.is_none() + } + + //////////////////////////////////////////////////////////////// + // Accessors + pub fn config(&self) -> Result { + let inner = self.inner.lock(); + if let Some(context) = &inner.context { + return Ok(context.config.clone()); + } + Err(VeilidAPIError::NotInitialized) + } + pub fn crypto(&self) -> Result { + let inner = self.inner.lock(); + if let Some(context) = &inner.context { + return Ok(context.crypto.clone()); + } + Err(VeilidAPIError::NotInitialized) + } + pub fn table_store(&self) -> Result { + let inner = self.inner.lock(); + if let Some(context) = &inner.context { + return Ok(context.table_store.clone()); + } + Err(VeilidAPIError::not_initialized()) + } + pub fn block_store(&self) -> Result { + let inner = self.inner.lock(); + if let Some(context) = &inner.context { + return Ok(context.block_store.clone()); + } + Err(VeilidAPIError::not_initialized()) + } + pub fn protected_store(&self) -> Result { + let inner = self.inner.lock(); + if let Some(context) = &inner.context { + return Ok(context.protected_store.clone()); + } + Err(VeilidAPIError::not_initialized()) + } + pub fn attachment_manager(&self) -> Result { + let inner = self.inner.lock(); + if let Some(context) = &inner.context { + return Ok(context.attachment_manager.clone()); + } + Err(VeilidAPIError::not_initialized()) + } + pub fn network_manager(&self) -> Result { + let inner = self.inner.lock(); + if let Some(context) = &inner.context { + return Ok(context.attachment_manager.network_manager()); + } + Err(VeilidAPIError::not_initialized()) + } + pub fn rpc_processor(&self) -> Result { + let inner = self.inner.lock(); + if let Some(context) = &inner.context { + return Ok(context.attachment_manager.network_manager().rpc_processor()); + } + Err(VeilidAPIError::NotInitialized) + } + pub fn routing_table(&self) -> Result { + let inner = self.inner.lock(); + if let Some(context) = &inner.context { + return Ok(context.attachment_manager.network_manager().routing_table()); + } + Err(VeilidAPIError::NotInitialized) + } + + //////////////////////////////////////////////////////////////// + // Attach/Detach + + // get a full copy of the current state + pub async fn get_state(&self) -> Result { + let attachment_manager = self.attachment_manager()?; + let network_manager = attachment_manager.network_manager(); + let config = self.config()?; + + let attachment = attachment_manager.get_veilid_state(); + let network = network_manager.get_veilid_state(); + let config = config.get_veilid_state(); + + Ok(VeilidState { + attachment, + network, + config, + }) + } + + // get network connectedness + + // connect to the network + #[instrument(level = "debug", err, skip_all)] + pub async fn attach(&self) -> Result<(), VeilidAPIError> { + let attachment_manager = self.attachment_manager()?; + attachment_manager + .request_attach() + .await + .map_err(|e| VeilidAPIError::internal(e)) + } + + // disconnect from the network + #[instrument(level = "debug", err, skip_all)] + pub async fn detach(&self) -> Result<(), VeilidAPIError> { + let attachment_manager = self.attachment_manager()?; + attachment_manager + .request_detach() + .await + .map_err(|e| VeilidAPIError::internal(e)) + } + + //////////////////////////////////////////////////////////////// + // Routing Context + + #[instrument(level = "debug", skip(self))] + pub fn routing_context(&self) -> RoutingContext { + RoutingContext::new(self.clone()) + } + + //////////////////////////////////////////////////////////////// + // Private route allocation + + #[instrument(level = "debug", skip(self))] + pub async fn new_private_route(&self) -> Result<(DHTKey, Vec), VeilidAPIError> { + self.new_custom_private_route(Stability::default(), Sequencing::default()) + .await + } + + #[instrument(level = "debug", skip(self))] + pub async fn new_custom_private_route( + &self, + stability: Stability, + sequencing: Sequencing, + ) -> Result<(DHTKey, Vec), VeilidAPIError> { + let default_route_hop_count: usize = { + let config = self.config()?; + let c = config.get(); + c.network.rpc.default_route_hop_count.into() + }; + + let rss = self.routing_table()?.route_spec_store(); + let r = rss + .allocate_route( + stability, + sequencing, + default_route_hop_count, + Direction::Inbound.into(), + &[], + ) + .map_err(VeilidAPIError::internal)?; + let Some(pr_pubkey) = r else { + apibail_generic!("unable to allocate route"); + }; + if !rss + .test_route(&pr_pubkey) + .await + .map_err(VeilidAPIError::no_connection)? + { + rss.release_route(&pr_pubkey); + apibail_generic!("allocated route failed to test"); + } + let private_route = rss + .assemble_private_route(&pr_pubkey, Some(true)) + .map_err(VeilidAPIError::generic)?; + let blob = match RouteSpecStore::private_route_to_blob(&private_route) { + Ok(v) => v, + Err(e) => { + rss.release_route(&pr_pubkey); + apibail_internal!(e); + } + }; + + rss.mark_route_published(&pr_pubkey, true) + .map_err(VeilidAPIError::internal)?; + + Ok((pr_pubkey, blob)) + } + + #[instrument(level = "debug", skip(self))] + pub fn import_remote_private_route(&self, blob: Vec) -> Result { + let rss = self.routing_table()?.route_spec_store(); + rss.import_remote_private_route(blob) + .map_err(|e| VeilidAPIError::invalid_argument(e, "blob", "private route blob")) + } + + #[instrument(level = "debug", skip(self))] + pub fn release_private_route(&self, key: &DHTKey) -> Result<(), VeilidAPIError> { + let rss = self.routing_table()?.route_spec_store(); + if rss.release_route(key) { + Ok(()) + } else { + Err(VeilidAPIError::invalid_argument( + "release_private_route", + "key", + key, + )) + } + } + + //////////////////////////////////////////////////////////////// + // App Calls + + #[instrument(level = "debug", skip(self))] + pub async fn app_call_reply(&self, id: u64, message: Vec) -> Result<(), VeilidAPIError> { + let rpc_processor = self.rpc_processor()?; + rpc_processor + .app_call_reply(id, message) + .await + .map_err(|e| e.into()) + } + + //////////////////////////////////////////////////////////////// + // Tunnel Building + + #[instrument(level = "debug", err, skip(self))] + pub async fn start_tunnel( + &self, + _endpoint_mode: TunnelMode, + _depth: u8, + ) -> Result { + panic!("unimplemented"); + } + + #[instrument(level = "debug", err, skip(self))] + pub async fn complete_tunnel( + &self, + _endpoint_mode: TunnelMode, + _depth: u8, + _partial_tunnel: PartialTunnel, + ) -> Result { + panic!("unimplemented"); + } + + #[instrument(level = "debug", err, skip(self))] + pub async fn cancel_tunnel(&self, _tunnel_id: TunnelId) -> Result { + panic!("unimplemented"); + } +} diff --git a/veilid-core/src/veilid_api/debug.rs b/veilid-core/src/veilid_api/debug.rs index 93d4beaf..626f2860 100644 --- a/veilid-core/src/veilid_api/debug.rs +++ b/veilid-core/src/veilid_api/debug.rs @@ -276,15 +276,10 @@ fn get_debug_argument Option>( argument: &str, getter: G, ) -> Result { - if let Some(val) = getter(value) { - Ok(val) - } else { - Err(VeilidAPIError::InvalidArgument { - context: context.to_owned(), - argument: argument.to_owned(), - value: value.to_owned(), - }) - } + let Some(val) = getter(value) else { + apibail_invalid_argument!(context, argument, value); + }; + Ok(val) } fn get_debug_argument_at Option>( debug_args: &[String], @@ -294,21 +289,13 @@ fn get_debug_argument_at Option>( getter: G, ) -> Result { if pos >= debug_args.len() { - return Err(VeilidAPIError::MissingArgument { - context: context.to_owned(), - argument: argument.to_owned(), - }); + apibail_missing_argument!(context, argument); } let value = &debug_args[pos]; - if let Some(val) = getter(value) { - Ok(val) - } else { - Err(VeilidAPIError::InvalidArgument { - context: context.to_owned(), - argument: argument.to_owned(), - value: value.to_owned(), - }) - } + let Some(val) = getter(value) else { + apibail_invalid_argument!(context, argument, value); + }; + Ok(val) } impl VeilidAPI { @@ -351,11 +338,7 @@ impl VeilidAPI { } else if let Some(lim) = get_number(&arg) { limit = lim; } else { - return Err(VeilidAPIError::InvalidArgument { - context: "debug_entries".to_owned(), - argument: "unknown".to_owned(), - value: arg, - }); + apibail_invalid_argument!("debug_entries", "unknown", arg); } } @@ -412,7 +395,7 @@ impl VeilidAPI { async fn debug_restart(&self, args: String) -> Result { let args = args.trim_start(); if args.is_empty() { - return Err(VeilidAPIError::missing_argument("debug_restart", "arg_0")); + apibail_missing_argument!("debug_restart", "arg_0"); } let (arg, _rest) = args.split_once(' ').unwrap_or((args, "")); // let rest = rest.trim_start().to_owned(); @@ -431,11 +414,7 @@ impl VeilidAPI { Ok("Network restarted".to_owned()) } else { - Err(VeilidAPIError::invalid_argument( - "debug_restart", - "arg_1", - arg, - )) + apibail_invalid_argument!("debug_restart", "arg_1", arg); } } @@ -654,11 +633,7 @@ impl VeilidAPI { if full_val == "full" { true } else { - return Err(VeilidAPIError::invalid_argument( - "debug_route", - "full", - full_val, - )); + apibail_invalid_argument!("debug_route", "full", full_val); } } else { false diff --git a/veilid-core/src/veilid_api/error.rs b/veilid-core/src/veilid_api/error.rs new file mode 100644 index 00000000..6d6f1a25 --- /dev/null +++ b/veilid-core/src/veilid_api/error.rs @@ -0,0 +1,186 @@ +use super::*; + +#[allow(unused_macros)] +#[macro_export] +macro_rules! apibail_timeout { + () => { + return Err(VeilidAPIError::timeout()) + }; +} + +#[allow(unused_macros)] +#[macro_export] +macro_rules! apibail_generic { + ($x:expr) => { + return Err(VeilidAPIError::generic($x)) + }; +} + +#[allow(unused_macros)] +#[macro_export] +macro_rules! apibail_internal { + ($x:expr) => { + return Err(VeilidAPIError::internal($x)) + }; +} + +#[allow(unused_macros)] +#[macro_export] +macro_rules! apibail_parse_error { + ($x:expr, $y:expr) => { + return Err(VeilidAPIError::parse_error($x, $y)) + }; +} + +#[allow(unused_macros)] +#[macro_export] +macro_rules! apibail_missing_argument { + ($x:expr, $y:expr) => { + return Err(VeilidAPIError::missing_argument($x, $y)) + }; +} + +#[allow(unused_macros)] +#[macro_export] +macro_rules! apibail_invalid_argument { + ($x:expr, $y:expr, $z:expr) => { + return Err(VeilidAPIError::invalid_argument($x, $y, $z)) + }; +} + +#[allow(unused_macros)] +#[macro_export] +macro_rules! apibail_no_connection { + ($x:expr) => { + return Err(VeilidAPIError::no_connection($x)) + }; +} + +#[allow(unused_macros)] +#[macro_export] +macro_rules! apibail_key_not_found { + ($x:expr) => { + return Err(VeilidAPIError::key_not_found($x)) + }; +} + +#[allow(unused_macros)] +#[macro_export] +macro_rules! apibail_already_initialized { + () => { + return Err(VeilidAPIError::already_initialized()) + }; +} + +#[derive( + ThisError, + Clone, + Debug, + PartialOrd, + PartialEq, + Eq, + Ord, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(u8), derive(CheckBytes))] +#[serde(tag = "kind")] +pub enum VeilidAPIError { + #[error("Not initialized")] + NotInitialized, + #[error("Already initialized")] + AlreadyInitialized, + #[error("Timeout")] + Timeout, + #[error("Shutdown")] + Shutdown, + #[error("Key not found: {key}")] + KeyNotFound { key: DHTKey }, + #[error("No connection: {message}")] + NoConnection { message: String }, + #[error("No peer info: {node_id}")] + NoPeerInfo { node_id: NodeId }, + #[error("Internal: {message}")] + Internal { message: String }, + #[error("Unimplemented: {message}")] + Unimplemented { message: String }, + #[error("Parse error: '{message}' with value '{value}'")] + ParseError { message: String, value: String }, + #[error("Invalid argument: '{argument}' for '{context}' with value '{value}'")] + InvalidArgument { + context: String, + argument: String, + value: String, + }, + #[error("Missing argument: '{argument}' for '{context}'")] + MissingArgument { context: String, argument: String }, + #[error("Generic: {message}")] + Generic { message: String }, +} + +impl VeilidAPIError { + pub fn not_initialized() -> Self { + Self::NotInitialized + } + pub fn already_initialized() -> Self { + Self::AlreadyInitialized + } + pub fn timeout() -> Self { + Self::Timeout + } + pub fn shutdown() -> Self { + Self::Shutdown + } + pub fn key_not_found(key: DHTKey) -> Self { + Self::KeyNotFound { key } + } + pub fn no_connection(msg: T) -> Self { + Self::NoConnection { + message: msg.to_string(), + } + } + pub fn no_peer_info(node_id: NodeId) -> Self { + Self::NoPeerInfo { node_id } + } + pub fn internal(msg: T) -> Self { + Self::Internal { + message: msg.to_string(), + } + } + pub fn unimplemented(msg: T) -> Self { + Self::Unimplemented { + message: msg.to_string(), + } + } + pub fn parse_error(msg: T, value: S) -> Self { + Self::ParseError { + message: msg.to_string(), + value: value.to_string(), + } + } + pub fn invalid_argument( + context: T, + argument: S, + value: R, + ) -> Self { + Self::InvalidArgument { + context: context.to_string(), + argument: argument.to_string(), + value: value.to_string(), + } + } + pub fn missing_argument(context: T, argument: S) -> Self { + Self::MissingArgument { + context: context.to_string(), + argument: argument.to_string(), + } + } + pub fn generic(msg: T) -> Self { + Self::Generic { + message: msg.to_string(), + } + } +} diff --git a/veilid-core/src/veilid_api/mod.rs b/veilid-core/src/veilid_api/mod.rs index 7c83fe34..a293cd46 100644 --- a/veilid-core/src/veilid_api/mod.rs +++ b/veilid-core/src/veilid_api/mod.rs @@ -1,12 +1,18 @@ #![allow(dead_code)] +mod api; mod debug; +mod error; mod routing_context; mod serialize_helpers; +mod types; +pub use api::*; pub use debug::*; +pub use error::*; pub use routing_context::*; pub use serialize_helpers::*; +pub use types::*; use crate::*; @@ -35,2824 +41,3 @@ use serde::*; use xx::*; ///////////////////////////////////////////////////////////////////////////////////////////////////// - -#[allow(unused_macros)] -#[macro_export] -macro_rules! apibail_generic { - ($x:expr) => { - return Err(VeilidAPIError::generic($x)) - }; -} - -#[allow(unused_macros)] -#[macro_export] -macro_rules! apibail_internal { - ($x:expr) => { - return Err(VeilidAPIError::internal($x)) - }; -} - -#[allow(unused_macros)] -#[macro_export] -macro_rules! apibail_parse_error { - ($x:expr, $y:expr) => { - return Err(VeilidAPIError::parse_error($x, $y)) - }; -} - -#[derive( - ThisError, - Clone, - Debug, - PartialOrd, - PartialEq, - Eq, - Ord, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(u8), derive(CheckBytes))] -#[serde(tag = "kind")] -pub enum VeilidAPIError { - #[error("Not initialized")] - NotInitialized, - #[error("Already initialized")] - AlreadyInitialized, - #[error("Timeout")] - Timeout, - #[error("Shutdown")] - Shutdown, - #[error("Key not found: {key}")] - KeyNotFound { key: DHTKey }, - #[error("No connection: {message}")] - NoConnection { message: String }, - #[error("No peer info: {node_id}")] - NoPeerInfo { node_id: NodeId }, - #[error("Internal: {message}")] - Internal { message: String }, - #[error("Unimplemented: {message}")] - Unimplemented { message: String }, - #[error("Parse error: '{message}' with value '{value}'")] - ParseError { message: String, value: String }, - #[error("Invalid argument: '{argument}' for '{context}' with value '{value}'")] - InvalidArgument { - context: String, - argument: String, - value: String, - }, - #[error("Missing argument: '{argument}' for '{context}'")] - MissingArgument { context: String, argument: String }, - #[error("Generic: {message}")] - Generic { message: String }, -} - -impl VeilidAPIError { - pub fn not_initialized() -> Self { - Self::NotInitialized - } - pub fn already_initialized() -> Self { - Self::AlreadyInitialized - } - pub fn timeout() -> Self { - Self::Timeout - } - pub fn shutdown() -> Self { - Self::Shutdown - } - pub fn key_not_found(key: DHTKey) -> Self { - Self::KeyNotFound { key } - } - pub fn no_connection(msg: T) -> Self { - Self::NoConnection { - message: msg.to_string(), - } - } - pub fn no_peer_info(node_id: NodeId) -> Self { - Self::NoPeerInfo { node_id } - } - pub fn internal(msg: T) -> Self { - Self::Internal { - message: msg.to_string(), - } - } - pub fn unimplemented(msg: T) -> Self { - Self::Unimplemented { - message: msg.to_string(), - } - } - pub fn parse_error(msg: T, value: S) -> Self { - Self::ParseError { - message: msg.to_string(), - value: value.to_string(), - } - } - pub fn invalid_argument( - context: T, - argument: S, - value: R, - ) -> Self { - Self::InvalidArgument { - context: context.to_string(), - argument: argument.to_string(), - value: value.to_string(), - } - } - pub fn missing_argument(context: T, argument: S) -> Self { - Self::MissingArgument { - context: context.to_string(), - argument: argument.to_string(), - } - } - pub fn generic(msg: T) -> Self { - Self::Generic { - message: msg.to_string(), - } - } -} - -///////////////////////////////////////////////////////////////////////////////////////////////////// - -#[derive( - Debug, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Copy, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(u8), derive(CheckBytes))] -pub enum VeilidLogLevel { - Error = 1, - Warn, - Info, - Debug, - Trace, -} - -impl VeilidLogLevel { - pub fn from_tracing_level(level: tracing::Level) -> VeilidLogLevel { - match level { - tracing::Level::ERROR => VeilidLogLevel::Error, - tracing::Level::WARN => VeilidLogLevel::Warn, - tracing::Level::INFO => VeilidLogLevel::Info, - tracing::Level::DEBUG => VeilidLogLevel::Debug, - tracing::Level::TRACE => VeilidLogLevel::Trace, - } - } - pub fn from_log_level(level: log::Level) -> VeilidLogLevel { - match level { - log::Level::Error => VeilidLogLevel::Error, - log::Level::Warn => VeilidLogLevel::Warn, - log::Level::Info => VeilidLogLevel::Info, - log::Level::Debug => VeilidLogLevel::Debug, - log::Level::Trace => VeilidLogLevel::Trace, - } - } - pub fn to_tracing_level(&self) -> tracing::Level { - match self { - Self::Error => tracing::Level::ERROR, - Self::Warn => tracing::Level::WARN, - Self::Info => tracing::Level::INFO, - Self::Debug => tracing::Level::DEBUG, - Self::Trace => tracing::Level::TRACE, - } - } - pub fn to_log_level(&self) -> log::Level { - match self { - Self::Error => log::Level::Error, - Self::Warn => log::Level::Warn, - Self::Info => log::Level::Info, - Self::Debug => log::Level::Debug, - Self::Trace => log::Level::Trace, - } - } -} - -impl fmt::Display for VeilidLogLevel { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - let text = match self { - Self::Error => "ERROR", - Self::Warn => "WARN", - Self::Info => "INFO", - Self::Debug => "DEBUG", - Self::Trace => "TRACE", - }; - write!(f, "{}", text) - } -} - -#[derive( - Debug, Clone, PartialEq, Eq, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct VeilidLog { - pub log_level: VeilidLogLevel, - pub message: String, - pub backtrace: Option, -} - -#[derive( - Debug, Clone, PartialEq, Eq, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct VeilidAppMessage { - /// Some(sender) if the message was sent directly, None if received via a private/safety route - #[serde(with = "opt_json_as_string")] - pub sender: Option, - /// The content of the message to deliver to the application - #[serde(with = "json_as_base64")] - pub message: Vec, -} - -#[derive( - Debug, Clone, PartialEq, Eq, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct VeilidAppCall { - /// Some(sender) if the request was sent directly, None if received via a private/safety route - #[serde(with = "opt_json_as_string")] - pub sender: Option, - /// The content of the request to deliver to the application - #[serde(with = "json_as_base64")] - pub message: Vec, - /// The id to reply to - #[serde(with = "json_as_string")] - pub id: u64, -} - -#[derive( - Debug, Clone, PartialEq, Eq, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct VeilidStateAttachment { - pub state: AttachmentState, -} - -#[derive( - Debug, Clone, PartialEq, Eq, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct PeerTableData { - pub node_id: DHTKey, - pub peer_address: PeerAddress, - pub peer_stats: PeerStats, -} - -#[derive( - Debug, Clone, PartialEq, Eq, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -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, PartialEq, Eq, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct VeilidStateRoute { - pub dead_routes: Vec, - pub dead_remote_routes: Vec, -} - -#[derive( - Debug, Clone, PartialEq, Eq, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct VeilidStateConfig { - pub config: VeilidConfigInner, -} - -#[derive(Debug, Clone, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] -#[archive_attr(repr(u8), derive(CheckBytes))] -#[serde(tag = "kind")] -pub enum VeilidUpdate { - Log(VeilidLog), - AppMessage(VeilidAppMessage), - AppCall(VeilidAppCall), - Attachment(VeilidStateAttachment), - Network(VeilidStateNetwork), - Config(VeilidStateConfig), - Route(VeilidStateRoute), - Shutdown, -} - -#[derive(Debug, Clone, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct VeilidState { - pub attachment: VeilidStateAttachment, - pub network: VeilidStateNetwork, - pub config: VeilidStateConfig, -} - -///////////////////////////////////////////////////////////////////////////////////////////////////// -/// -#[derive( - Clone, - Debug, - Default, - PartialOrd, - PartialEq, - Eq, - Ord, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct NodeId { - pub key: DHTKey, -} -impl NodeId { - pub fn new(key: DHTKey) -> Self { - Self { key } - } -} -impl fmt::Display for NodeId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - write!(f, "{}", self.key.encode()) - } -} -impl FromStr for NodeId { - type Err = VeilidAPIError; - fn from_str(s: &str) -> Result { - Ok(Self { - key: DHTKey::try_decode(s)?, - }) - } -} - -#[derive( - Clone, - Debug, - Default, - PartialOrd, - PartialEq, - Eq, - Ord, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct ValueKey { - pub key: DHTKey, - pub subkey: Option, -} -impl ValueKey { - pub fn new(key: DHTKey) -> Self { - Self { key, subkey: None } - } - pub fn new_subkey(key: DHTKey, subkey: String) -> Self { - Self { - key, - subkey: if subkey.is_empty() { - None - } else { - Some(subkey) - }, - } - } -} - -#[derive( - Clone, - Debug, - Default, - PartialOrd, - PartialEq, - Eq, - Ord, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct ValueData { - pub data: Vec, - pub seq: u32, -} -impl ValueData { - pub fn new(data: Vec) -> Self { - Self { data, seq: 0 } - } - pub fn new_with_seq(data: Vec, seq: u32) -> Self { - Self { data, seq } - } - pub fn change(&mut self, data: Vec) { - self.data = data; - self.seq += 1; - } -} - -#[derive( - Clone, - Debug, - Default, - PartialOrd, - PartialEq, - Eq, - Ord, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct BlockId { - pub key: DHTKey, -} -impl BlockId { - pub fn new(key: DHTKey) -> Self { - Self { key } - } -} - -///////////////////////////////////////////////////////////////////////////////////////////////////// - -// Keep member order appropriate for sorting < preference -#[derive( - Copy, - Clone, - Debug, - Eq, - PartialEq, - Ord, - PartialOrd, - Hash, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(u8), derive(CheckBytes))] -pub enum DialInfoClass { - Direct = 0, // D = Directly reachable with public IP and no firewall, with statically configured port - Mapped = 1, // M = Directly reachable with via portmap behind any NAT or firewalled with dynamically negotiated port - FullConeNAT = 2, // F = Directly reachable device without portmap behind full-cone NAT - Blocked = 3, // B = Inbound blocked at firewall but may hole punch with public address - AddressRestrictedNAT = 4, // A = Device without portmap behind address-only restricted NAT - PortRestrictedNAT = 5, // P = Device without portmap behind address-and-port restricted NAT -} - -impl DialInfoClass { - // Is a signal required to do an inbound hole-punch? - pub fn requires_signal(&self) -> bool { - matches!( - self, - Self::Blocked | Self::AddressRestrictedNAT | Self::PortRestrictedNAT - ) - } - - // Does a relay node need to be allocated for this dial info? - // For full cone NAT, the relay itself may not be used but the keepalive sent to it - // is required to keep the NAT mapping valid in the router state table - pub fn requires_relay(&self) -> bool { - matches!( - self, - Self::FullConeNAT - | Self::Blocked - | Self::AddressRestrictedNAT - | Self::PortRestrictedNAT - ) - } -} - -// Ordering here matters, >= is used to check strength of sequencing requirement -#[derive( - Copy, - Clone, - Debug, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(u8), derive(CheckBytes))] -pub enum Sequencing { - NoPreference, - PreferOrdered, - EnsureOrdered, -} - -impl Default for Sequencing { - fn default() -> Self { - Self::NoPreference - } -} - -// Ordering here matters, >= is used to check strength of stability requirement -#[derive( - Copy, - Clone, - Debug, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(u8), derive(CheckBytes))] -pub enum Stability { - LowLatency, - Reliable, -} - -impl Default for Stability { - fn default() -> Self { - Self::LowLatency - } -} - -/// The choice of safety route to include in compiled routes -#[derive( - Copy, - Clone, - Debug, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(u8), derive(CheckBytes))] -pub enum SafetySelection { - /// Don't use a safety route, only specify the sequencing preference - Unsafe(Sequencing), - /// Use a safety route and parameters specified by a SafetySpec - Safe(SafetySpec), -} - -/// Options for safety routes (sender privacy) -#[derive( - Copy, - Clone, - Debug, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct SafetySpec { - /// preferred safety route if it still exists - pub preferred_route: Option, - /// must be greater than 0 - pub hop_count: usize, - /// prefer reliability over speed - pub stability: Stability, - /// prefer connection-oriented sequenced protocols - pub sequencing: Sequencing, -} - -// Keep member order appropriate for sorting < preference -#[derive( - Debug, - Clone, - PartialEq, - PartialOrd, - Ord, - Eq, - Hash, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct DialInfoDetail { - pub class: DialInfoClass, - pub dial_info: DialInfo, -} - -impl MatchesDialInfoFilter for DialInfoDetail { - fn matches_filter(&self, filter: &DialInfoFilter) -> bool { - self.dial_info.matches_filter(filter) - } -} - -impl DialInfoDetail { - pub fn ordered_sequencing_sort(a: &DialInfoDetail, b: &DialInfoDetail) -> core::cmp::Ordering { - if a.class < b.class { - return core::cmp::Ordering::Less; - } - if a.class > b.class { - return core::cmp::Ordering::Greater; - } - DialInfo::ordered_sequencing_sort(&a.dial_info, &b.dial_info) - } - pub const NO_SORT: std::option::Option< - for<'r, 's> fn( - &'r veilid_api::DialInfoDetail, - &'s veilid_api::DialInfoDetail, - ) -> std::cmp::Ordering, - > = None:: core::cmp::Ordering>; -} - -#[derive( - Copy, - Clone, - Debug, - Eq, - PartialEq, - Ord, - PartialOrd, - Hash, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(u8), derive(CheckBytes))] -pub enum NetworkClass { - InboundCapable = 0, // I = Inbound capable without relay, may require signal - OutboundOnly = 1, // O = Outbound only, inbound relay required except with reverse connect signal - WebApp = 2, // W = PWA, outbound relay is required in most cases - Invalid = 3, // X = Invalid network class, we don't know how to reach this node -} - -impl Default for NetworkClass { - fn default() -> Self { - Self::Invalid - } -} - -impl NetworkClass { - // Should an outbound relay be kept available? - pub fn outbound_wants_relay(&self) -> bool { - matches!(self, Self::WebApp) - } -} - -/// RoutingDomain-specific status for each node -/// is returned by the StatusA call - -/// PublicInternet RoutingDomain Status -#[derive( - Clone, Debug, Default, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct PublicInternetNodeStatus { - pub will_route: bool, - pub will_tunnel: bool, - pub will_signal: bool, - pub will_relay: bool, - pub will_validate_dial_info: bool, -} - -#[derive( - Clone, Debug, Default, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct LocalNetworkNodeStatus { - pub will_relay: bool, - pub will_validate_dial_info: bool, -} - -#[derive(Clone, Debug, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] -#[archive_attr(repr(u8), derive(CheckBytes))] -pub enum NodeStatus { - PublicInternet(PublicInternetNodeStatus), - LocalNetwork(LocalNetworkNodeStatus), -} - -impl NodeStatus { - pub fn will_route(&self) -> bool { - match self { - NodeStatus::PublicInternet(pi) => pi.will_route, - NodeStatus::LocalNetwork(_) => false, - } - } - pub fn will_tunnel(&self) -> bool { - match self { - NodeStatus::PublicInternet(pi) => pi.will_tunnel, - NodeStatus::LocalNetwork(_) => false, - } - } - pub fn will_signal(&self) -> bool { - match self { - NodeStatus::PublicInternet(pi) => pi.will_signal, - NodeStatus::LocalNetwork(_) => false, - } - } - pub fn will_relay(&self) -> bool { - match self { - NodeStatus::PublicInternet(pi) => pi.will_relay, - NodeStatus::LocalNetwork(ln) => ln.will_relay, - } - } - pub fn will_validate_dial_info(&self) -> bool { - match self { - NodeStatus::PublicInternet(pi) => pi.will_validate_dial_info, - NodeStatus::LocalNetwork(ln) => ln.will_validate_dial_info, - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct NodeInfo { - pub network_class: NetworkClass, - #[with(RkyvEnumSet)] - pub outbound_protocols: ProtocolTypeSet, - #[with(RkyvEnumSet)] - pub address_types: AddressTypeSet, - pub min_version: u8, - pub max_version: u8, - pub dial_info_detail_list: Vec, -} - -impl NodeInfo { - pub fn first_filtered_dial_info_detail( - &self, - sort: Option, - filter: F, - ) -> Option - where - S: Fn(&DialInfoDetail, &DialInfoDetail) -> std::cmp::Ordering, - F: Fn(&DialInfoDetail) -> bool, - { - if let Some(sort) = sort { - let mut dids = self.dial_info_detail_list.clone(); - dids.sort_by(sort); - for did in dids { - if filter(&did) { - return Some(did); - } - } - } else { - for did in &self.dial_info_detail_list { - if filter(did) { - return Some(did.clone()); - } - } - }; - None - } - - pub fn all_filtered_dial_info_details( - &self, - sort: Option, - filter: F, - ) -> Vec - where - S: Fn(&DialInfoDetail, &DialInfoDetail) -> std::cmp::Ordering, - F: Fn(&DialInfoDetail) -> bool, - { - let mut dial_info_detail_list = Vec::new(); - - if let Some(sort) = sort { - let mut dids = self.dial_info_detail_list.clone(); - dids.sort_by(sort); - for did in dids { - if filter(&did) { - dial_info_detail_list.push(did); - } - } - } else { - for did in &self.dial_info_detail_list { - if filter(did) { - dial_info_detail_list.push(did.clone()); - } - } - }; - dial_info_detail_list - } - - /// Does this node has some dial info - pub fn has_dial_info(&self) -> bool { - !self.dial_info_detail_list.is_empty() - } - - /// Is some relay required either for signal or inbound relay or outbound relay? - pub fn requires_relay(&self) -> bool { - match self.network_class { - NetworkClass::InboundCapable => { - for did in &self.dial_info_detail_list { - if did.class.requires_relay() { - return true; - } - } - } - NetworkClass::OutboundOnly => { - return true; - } - NetworkClass::WebApp => { - return true; - } - NetworkClass::Invalid => {} - } - false - } - - /// Can this node assist with signalling? Yes but only if it doesn't require signalling, itself. - pub fn can_signal(&self) -> bool { - // Must be inbound capable - if !matches!(self.network_class, NetworkClass::InboundCapable) { - return false; - } - // Do any of our dial info require signalling? if so, we can't offer signalling - for did in &self.dial_info_detail_list { - if did.class.requires_signal() { - return false; - } - } - true - } - - /// Can this node relay be an inbound relay? - pub fn can_inbound_relay(&self) -> bool { - // For now this is the same - self.can_signal() - } - - /// Is this node capable of validating dial info - pub fn can_validate_dial_info(&self) -> bool { - // For now this is the same - self.can_signal() - } -} - -#[allow(clippy::derive_hash_xor_eq)] -#[derive( - Debug, - PartialOrd, - Ord, - Hash, - EnumSetType, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[enumset(repr = "u8")] -#[archive_attr(repr(u8), derive(CheckBytes))] -pub enum Direction { - Inbound, - Outbound, -} -pub type DirectionSet = EnumSet; - -// Keep member order appropriate for sorting < preference -// Must match DialInfo order -#[allow(clippy::derive_hash_xor_eq)] -#[derive( - Debug, - PartialOrd, - Ord, - Hash, - EnumSetType, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[enumset(repr = "u8")] -#[archive_attr(repr(u8), derive(CheckBytes))] -pub enum LowLevelProtocolType { - UDP, - TCP, -} - -impl LowLevelProtocolType { - pub fn is_connection_oriented(&self) -> bool { - matches!(self, LowLevelProtocolType::TCP) - } -} -pub type LowLevelProtocolTypeSet = EnumSet; - -// Keep member order appropriate for sorting < preference -// Must match DialInfo order -#[allow(clippy::derive_hash_xor_eq)] -#[derive( - Debug, - PartialOrd, - Ord, - Hash, - EnumSetType, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[enumset(repr = "u8")] -#[archive_attr(repr(u8), derive(CheckBytes))] -pub enum ProtocolType { - UDP, - TCP, - WS, - WSS, -} - -impl ProtocolType { - pub fn is_connection_oriented(&self) -> bool { - matches!( - self, - ProtocolType::TCP | ProtocolType::WS | ProtocolType::WSS - ) - } - pub fn low_level_protocol_type(&self) -> LowLevelProtocolType { - match self { - ProtocolType::UDP => LowLevelProtocolType::UDP, - ProtocolType::TCP | ProtocolType::WS | ProtocolType::WSS => LowLevelProtocolType::TCP, - } - } - pub fn sort_order(&self, sequencing: Sequencing) -> usize { - match self { - ProtocolType::UDP => { - if sequencing != Sequencing::NoPreference { - 3 - } else { - 0 - } - } - ProtocolType::TCP => { - if sequencing != Sequencing::NoPreference { - 0 - } else { - 1 - } - } - ProtocolType::WS => { - if sequencing != Sequencing::NoPreference { - 1 - } else { - 2 - } - } - ProtocolType::WSS => { - if sequencing != Sequencing::NoPreference { - 2 - } else { - 3 - } - } - } - } - pub fn all_ordered_set() -> ProtocolTypeSet { - ProtocolType::TCP | ProtocolType::WS | ProtocolType::WSS - } -} - -pub type ProtocolTypeSet = EnumSet; - -#[allow(clippy::derive_hash_xor_eq)] -#[derive( - Debug, - PartialOrd, - Ord, - Hash, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, - EnumSetType, -)] -#[enumset(repr = "u8")] -#[archive_attr(repr(u8), derive(CheckBytes))] -pub enum AddressType { - IPV4, - IPV6, -} -pub type AddressTypeSet = EnumSet; - -// Routing domain here is listed in order of preference, keep in order -#[allow(clippy::derive_hash_xor_eq)] -#[derive( - Debug, - Ord, - PartialOrd, - Hash, - EnumSetType, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[enumset(repr = "u8")] -#[archive_attr(repr(u8), derive(CheckBytes))] -pub enum RoutingDomain { - LocalNetwork = 0, - PublicInternet = 1, -} -impl RoutingDomain { - pub const fn count() -> usize { - 2 - } - pub const fn all() -> [RoutingDomain; RoutingDomain::count()] { - // Routing domain here is listed in order of preference, keep in order - [RoutingDomain::LocalNetwork, RoutingDomain::PublicInternet] - } -} -pub type RoutingDomainSet = EnumSet; - -#[derive( - Copy, - Clone, - Debug, - PartialEq, - PartialOrd, - Ord, - Eq, - Hash, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(u8), derive(CheckBytes))] -pub enum Address { - IPV4(Ipv4Addr), - IPV6(Ipv6Addr), -} - -impl Default for Address { - fn default() -> Self { - Address::IPV4(Ipv4Addr::new(0, 0, 0, 0)) - } -} - -impl Address { - pub fn from_socket_addr(sa: SocketAddr) -> Address { - match sa { - SocketAddr::V4(v4) => Address::IPV4(*v4.ip()), - SocketAddr::V6(v6) => Address::IPV6(*v6.ip()), - } - } - pub fn from_ip_addr(addr: IpAddr) -> Address { - match addr { - IpAddr::V4(v4) => Address::IPV4(v4), - IpAddr::V6(v6) => Address::IPV6(v6), - } - } - pub fn address_type(&self) -> AddressType { - match self { - Address::IPV4(_) => AddressType::IPV4, - Address::IPV6(_) => AddressType::IPV6, - } - } - pub fn address_string(&self) -> String { - match self { - Address::IPV4(v4) => v4.to_string(), - Address::IPV6(v6) => v6.to_string(), - } - } - pub fn address_string_with_port(&self, port: u16) -> String { - match self { - Address::IPV4(v4) => format!("{}:{}", v4, port), - Address::IPV6(v6) => format!("[{}]:{}", v6, port), - } - } - pub fn is_unspecified(&self) -> bool { - match self { - Address::IPV4(v4) => ipv4addr_is_unspecified(v4), - Address::IPV6(v6) => ipv6addr_is_unspecified(v6), - } - } - pub fn is_global(&self) -> bool { - match self { - Address::IPV4(v4) => ipv4addr_is_global(v4) && !ipv4addr_is_multicast(v4), - Address::IPV6(v6) => ipv6addr_is_unicast_global(v6), - } - } - pub fn is_local(&self) -> bool { - match self { - Address::IPV4(v4) => { - ipv4addr_is_private(v4) - || ipv4addr_is_link_local(v4) - || ipv4addr_is_ietf_protocol_assignment(v4) - } - Address::IPV6(v6) => { - ipv6addr_is_unicast_site_local(v6) - || ipv6addr_is_unicast_link_local(v6) - || ipv6addr_is_unique_local(v6) - } - } - } - pub fn to_ip_addr(&self) -> IpAddr { - match self { - Self::IPV4(a) => IpAddr::V4(*a), - Self::IPV6(a) => IpAddr::V6(*a), - } - } - pub fn to_socket_addr(&self, port: u16) -> SocketAddr { - SocketAddr::new(self.to_ip_addr(), port) - } - pub fn to_canonical(&self) -> Address { - match self { - Address::IPV4(v4) => Address::IPV4(*v4), - Address::IPV6(v6) => match v6.to_ipv4() { - Some(v4) => Address::IPV4(v4), - None => Address::IPV6(*v6), - }, - } - } -} - -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 { - if let Ok(addr) = Ipv4Addr::from_str(host) { - Ok(Address::IPV4(addr)) - } else if let Ok(addr) = Ipv6Addr::from_str(host) { - Ok(Address::IPV6(addr)) - } else { - Err(VeilidAPIError::parse_error( - "Address::from_str failed", - host, - )) - } - } -} - -#[derive( - Copy, - Default, - Clone, - Debug, - PartialEq, - PartialOrd, - Ord, - Eq, - Hash, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct SocketAddress { - address: Address, - port: u16, -} - -impl SocketAddress { - pub fn new(address: Address, port: u16) -> Self { - Self { address, port } - } - pub fn from_socket_addr(sa: SocketAddr) -> SocketAddress { - Self { - address: Address::from_socket_addr(sa), - port: sa.port(), - } - } - pub fn address(&self) -> Address { - self.address - } - pub fn address_type(&self) -> AddressType { - self.address.address_type() - } - pub fn port(&self) -> u16 { - self.port - } - pub fn to_canonical(&self) -> SocketAddress { - SocketAddress { - address: self.address.to_canonical(), - port: self.port, - } - } - pub fn to_ip_addr(&self) -> IpAddr { - self.address.to_ip_addr() - } - pub fn to_socket_addr(&self) -> SocketAddr { - self.address.to_socket_addr(self.port) - } -} - -impl fmt::Display for SocketAddress { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - write!(f, "{}", self.to_socket_addr()) - } -} - -impl FromStr for SocketAddress { - type Err = VeilidAPIError; - fn from_str(s: &str) -> Result { - let sa = SocketAddr::from_str(s) - .map_err(|e| VeilidAPIError::parse_error("Failed to parse SocketAddress", e))?; - Ok(SocketAddress::from_socket_addr(sa)) - } -} - -////////////////////////////////////////////////////////////////// - -#[derive( - Copy, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct DialInfoFilter { - #[with(RkyvEnumSet)] - pub protocol_type_set: ProtocolTypeSet, - #[with(RkyvEnumSet)] - pub address_type_set: AddressTypeSet, -} - -impl Default for DialInfoFilter { - fn default() -> Self { - Self { - protocol_type_set: ProtocolTypeSet::all(), - address_type_set: AddressTypeSet::all(), - } - } -} - -impl DialInfoFilter { - pub fn all() -> Self { - Self { - protocol_type_set: ProtocolTypeSet::all(), - address_type_set: AddressTypeSet::all(), - } - } - pub fn with_protocol_type(mut self, protocol_type: ProtocolType) -> Self { - self.protocol_type_set = ProtocolTypeSet::only(protocol_type); - self - } - pub fn with_protocol_type_set(mut self, protocol_set: ProtocolTypeSet) -> Self { - self.protocol_type_set = protocol_set; - self - } - pub fn with_address_type(mut self, address_type: AddressType) -> Self { - self.address_type_set = AddressTypeSet::only(address_type); - self - } - pub fn with_address_type_set(mut self, address_set: AddressTypeSet) -> Self { - self.address_type_set = address_set; - self - } - pub fn filtered(mut self, other_dif: &DialInfoFilter) -> Self { - self.protocol_type_set &= other_dif.protocol_type_set; - self.address_type_set &= other_dif.address_type_set; - self - } - pub fn is_dead(&self) -> bool { - self.protocol_type_set.is_empty() || self.address_type_set.is_empty() - } -} - -impl fmt::Debug for DialInfoFilter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - let mut out = String::new(); - if self.protocol_type_set != ProtocolTypeSet::all() { - out += &format!("+{:?}", self.protocol_type_set); - } else { - out += "*"; - } - if self.address_type_set != AddressTypeSet::all() { - out += &format!("+{:?}", self.address_type_set); - } else { - out += "*"; - } - write!(f, "[{}]", out) - } -} - -pub trait MatchesDialInfoFilter { - fn matches_filter(&self, filter: &DialInfoFilter) -> bool; -} - -#[derive( - Clone, - Default, - Debug, - PartialEq, - PartialOrd, - Ord, - Eq, - Hash, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct DialInfoUDP { - pub socket_address: SocketAddress, -} - -#[derive( - Clone, - Default, - Debug, - PartialEq, - PartialOrd, - Ord, - Eq, - Hash, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct DialInfoTCP { - pub socket_address: SocketAddress, -} - -#[derive( - Clone, - Default, - Debug, - PartialEq, - PartialOrd, - Ord, - Eq, - Hash, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct DialInfoWS { - pub socket_address: SocketAddress, - pub request: String, -} - -#[derive( - Clone, - Default, - Debug, - PartialEq, - PartialOrd, - Ord, - Eq, - Hash, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct DialInfoWSS { - pub socket_address: SocketAddress, - pub request: String, -} - -// Keep member order appropriate for sorting < preference -// Must match ProtocolType order -#[derive( - Clone, - Debug, - PartialEq, - PartialOrd, - Ord, - Eq, - Hash, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(u8), derive(CheckBytes))] -#[serde(tag = "kind")] -pub enum DialInfo { - UDP(DialInfoUDP), - TCP(DialInfoTCP), - WS(DialInfoWS), - WSS(DialInfoWSS), -} -impl Default for DialInfo { - fn default() -> Self { - DialInfo::UDP(DialInfoUDP::default()) - } -} - -impl fmt::Display for DialInfo { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - match self { - DialInfo::UDP(di) => write!(f, "udp|{}", di.socket_address), - DialInfo::TCP(di) => write!(f, "tcp|{}", di.socket_address), - DialInfo::WS(di) => { - let url = format!("ws://{}", di.request); - let split_url = SplitUrl::from_str(&url).unwrap(); - match split_url.host { - SplitUrlHost::Hostname(_) => { - write!(f, "ws|{}|{}", di.socket_address.to_ip_addr(), di.request) - } - SplitUrlHost::IpAddr(a) => { - if di.socket_address.to_ip_addr() == a { - write!(f, "ws|{}", di.request) - } else { - panic!("resolved address does not match url: {}", di.request); - } - } - } - } - DialInfo::WSS(di) => { - let url = format!("wss://{}", di.request); - let split_url = SplitUrl::from_str(&url).unwrap(); - match split_url.host { - SplitUrlHost::Hostname(_) => { - write!(f, "wss|{}|{}", di.socket_address.to_ip_addr(), di.request) - } - SplitUrlHost::IpAddr(_) => { - panic!( - "secure websockets can not use ip address in request: {}", - di.request - ); - } - } - } - } - } -} - -impl FromStr for DialInfo { - type Err = VeilidAPIError; - fn from_str(s: &str) -> Result { - let (proto, rest) = s.split_once('|').ok_or_else(|| { - VeilidAPIError::parse_error("DialInfo::from_str missing protocol '|' separator", s) - })?; - match proto { - "udp" => { - let socket_address = SocketAddress::from_str(rest)?; - Ok(DialInfo::udp(socket_address)) - } - "tcp" => { - let socket_address = SocketAddress::from_str(rest)?; - Ok(DialInfo::tcp(socket_address)) - } - "ws" => { - let url = format!("ws://{}", rest); - let split_url = SplitUrl::from_str(&url).map_err(|e| { - VeilidAPIError::parse_error(format!("unable to split WS url: {}", e), &url) - })?; - if split_url.scheme != "ws" || !url.starts_with("ws://") { - apibail_parse_error!("incorrect scheme for WS dialinfo", url); - } - let url_port = split_url.port.unwrap_or(80u16); - - match rest.split_once('|') { - Some((sa, rest)) => { - let address = Address::from_str(sa)?; - - DialInfo::try_ws( - SocketAddress::new(address, url_port), - format!("ws://{}", rest), - ) - } - None => { - let address = Address::from_str(&split_url.host.to_string())?; - DialInfo::try_ws( - SocketAddress::new(address, url_port), - format!("ws://{}", rest), - ) - } - } - } - "wss" => { - let url = format!("wss://{}", rest); - let split_url = SplitUrl::from_str(&url).map_err(|e| { - VeilidAPIError::parse_error(format!("unable to split WSS url: {}", e), &url) - })?; - if split_url.scheme != "wss" || !url.starts_with("wss://") { - apibail_parse_error!("incorrect scheme for WSS dialinfo", url); - } - let url_port = split_url.port.unwrap_or(443u16); - - let (a, rest) = rest.split_once('|').ok_or_else(|| { - VeilidAPIError::parse_error( - "DialInfo::from_str missing socket address '|' separator", - s, - ) - })?; - - let address = Address::from_str(a)?; - DialInfo::try_wss( - SocketAddress::new(address, url_port), - format!("wss://{}", rest), - ) - } - _ => Err(VeilidAPIError::parse_error( - "DialInfo::from_str has invalid scheme", - s, - )), - } - } -} - -impl DialInfo { - pub fn udp_from_socketaddr(socket_addr: SocketAddr) -> Self { - Self::UDP(DialInfoUDP { - socket_address: SocketAddress::from_socket_addr(socket_addr).to_canonical(), - }) - } - pub fn tcp_from_socketaddr(socket_addr: SocketAddr) -> Self { - Self::TCP(DialInfoTCP { - socket_address: SocketAddress::from_socket_addr(socket_addr).to_canonical(), - }) - } - pub fn udp(socket_address: SocketAddress) -> Self { - Self::UDP(DialInfoUDP { - socket_address: socket_address.to_canonical(), - }) - } - pub fn tcp(socket_address: SocketAddress) -> Self { - Self::TCP(DialInfoTCP { - socket_address: socket_address.to_canonical(), - }) - } - pub fn try_ws(socket_address: SocketAddress, url: String) -> Result { - let split_url = SplitUrl::from_str(&url).map_err(|e| { - VeilidAPIError::parse_error(format!("unable to split WS url: {}", e), &url) - })?; - if split_url.scheme != "ws" || !url.starts_with("ws://") { - apibail_parse_error!("incorrect scheme for WS dialinfo", url); - } - let url_port = split_url.port.unwrap_or(80u16); - if url_port != socket_address.port() { - apibail_parse_error!("socket address port doesn't match url port", url); - } - if let SplitUrlHost::IpAddr(a) = split_url.host { - if socket_address.to_ip_addr() != a { - apibail_parse_error!( - format!("request address does not match socket address: {}", a), - socket_address - ); - } - } - Ok(Self::WS(DialInfoWS { - socket_address: socket_address.to_canonical(), - request: url[5..].to_string(), - })) - } - pub fn try_wss(socket_address: SocketAddress, url: String) -> Result { - let split_url = SplitUrl::from_str(&url).map_err(|e| { - VeilidAPIError::parse_error(format!("unable to split WSS url: {}", e), &url) - })?; - if split_url.scheme != "wss" || !url.starts_with("wss://") { - apibail_parse_error!("incorrect scheme for WSS dialinfo", url); - } - let url_port = split_url.port.unwrap_or(443u16); - if url_port != socket_address.port() { - apibail_parse_error!("socket address port doesn't match url port", url); - } - if !matches!(split_url.host, SplitUrlHost::Hostname(_)) { - apibail_parse_error!( - "WSS url can not use address format, only hostname format", - url - ); - } - Ok(Self::WSS(DialInfoWSS { - socket_address: socket_address.to_canonical(), - request: url[6..].to_string(), - })) - } - pub fn protocol_type(&self) -> ProtocolType { - match self { - Self::UDP(_) => ProtocolType::UDP, - Self::TCP(_) => ProtocolType::TCP, - Self::WS(_) => ProtocolType::WS, - Self::WSS(_) => ProtocolType::WSS, - } - } - pub fn address_type(&self) -> AddressType { - self.socket_address().address_type() - } - pub fn address(&self) -> Address { - match self { - Self::UDP(di) => di.socket_address.address, - Self::TCP(di) => di.socket_address.address, - Self::WS(di) => di.socket_address.address, - Self::WSS(di) => di.socket_address.address, - } - } - pub fn socket_address(&self) -> SocketAddress { - match self { - Self::UDP(di) => di.socket_address, - Self::TCP(di) => di.socket_address, - Self::WS(di) => di.socket_address, - Self::WSS(di) => di.socket_address, - } - } - pub fn to_ip_addr(&self) -> IpAddr { - match self { - Self::UDP(di) => di.socket_address.to_ip_addr(), - Self::TCP(di) => di.socket_address.to_ip_addr(), - Self::WS(di) => di.socket_address.to_ip_addr(), - Self::WSS(di) => di.socket_address.to_ip_addr(), - } - } - pub fn port(&self) -> u16 { - match self { - Self::UDP(di) => di.socket_address.port, - Self::TCP(di) => di.socket_address.port, - Self::WS(di) => di.socket_address.port, - Self::WSS(di) => di.socket_address.port, - } - } - pub fn set_port(&mut self, port: u16) { - match self { - Self::UDP(di) => di.socket_address.port = port, - Self::TCP(di) => di.socket_address.port = port, - Self::WS(di) => di.socket_address.port = port, - Self::WSS(di) => di.socket_address.port = port, - } - } - pub fn to_socket_addr(&self) -> SocketAddr { - match self { - Self::UDP(di) => di.socket_address.to_socket_addr(), - Self::TCP(di) => di.socket_address.to_socket_addr(), - Self::WS(di) => di.socket_address.to_socket_addr(), - Self::WSS(di) => di.socket_address.to_socket_addr(), - } - } - pub fn to_peer_address(&self) -> PeerAddress { - match self { - Self::UDP(di) => PeerAddress::new(di.socket_address, ProtocolType::UDP), - Self::TCP(di) => PeerAddress::new(di.socket_address, ProtocolType::TCP), - Self::WS(di) => PeerAddress::new(di.socket_address, ProtocolType::WS), - Self::WSS(di) => PeerAddress::new(di.socket_address, ProtocolType::WSS), - } - } - pub fn request(&self) -> Option { - match self { - Self::UDP(_) => None, - Self::TCP(_) => None, - Self::WS(di) => Some(format!("ws://{}", di.request)), - Self::WSS(di) => Some(format!("wss://{}", di.request)), - } - } - pub fn is_valid(&self) -> bool { - let socket_address = self.socket_address(); - let address = socket_address.address(); - let port = socket_address.port(); - (address.is_global() || address.is_local()) && port > 0 - } - - pub fn make_filter(&self) -> DialInfoFilter { - DialInfoFilter { - protocol_type_set: ProtocolTypeSet::only(self.protocol_type()), - address_type_set: AddressTypeSet::only(self.address_type()), - } - } - - pub fn try_vec_from_short, H: AsRef>( - short: S, - hostname: H, - ) -> Result, VeilidAPIError> { - let short = short.as_ref(); - let hostname = hostname.as_ref(); - - if short.len() < 2 { - apibail_parse_error!("invalid short url length", short); - } - let url = match &short[0..1] { - "U" => { - format!("udp://{}:{}", hostname, &short[1..]) - } - "T" => { - format!("tcp://{}:{}", hostname, &short[1..]) - } - "W" => { - format!("ws://{}:{}", hostname, &short[1..]) - } - "S" => { - format!("wss://{}:{}", hostname, &short[1..]) - } - _ => { - apibail_parse_error!("invalid short url type", short); - } - }; - Self::try_vec_from_url(url) - } - - pub fn try_vec_from_url>(url: S) -> Result, VeilidAPIError> { - let url = url.as_ref(); - let split_url = SplitUrl::from_str(url) - .map_err(|e| VeilidAPIError::parse_error(format!("unable to split url: {}", e), url))?; - - let port = match split_url.scheme.as_str() { - "udp" | "tcp" => split_url - .port - .ok_or_else(|| VeilidAPIError::parse_error("Missing port in udp url", url))?, - "ws" => split_url.port.unwrap_or(80u16), - "wss" => split_url.port.unwrap_or(443u16), - _ => { - apibail_parse_error!("Invalid dial info url scheme", split_url.scheme); - } - }; - - let socket_addrs = { - // Resolve if possible, WASM doesn't support resolution and doesn't need it to connect to the dialinfo - // This will not be used on signed dialinfo, only for bootstrapping, so we don't need to worry about - // the '0.0.0.0' address being propagated across the routing table - cfg_if::cfg_if! { - if #[cfg(target_arch = "wasm32")] { - vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0,0,0,0)), port)] - } else { - match split_url.host { - SplitUrlHost::Hostname(_) => split_url - .host_port(port) - .to_socket_addrs() - .map_err(|_| VeilidAPIError::parse_error("couldn't resolve hostname in url", url))? - .collect(), - SplitUrlHost::IpAddr(a) => vec![SocketAddr::new(a, port)], - } - } - } - }; - - let mut out = Vec::new(); - for sa in socket_addrs { - out.push(match split_url.scheme.as_str() { - "udp" => Self::udp_from_socketaddr(sa), - "tcp" => Self::tcp_from_socketaddr(sa), - "ws" => Self::try_ws( - SocketAddress::from_socket_addr(sa).to_canonical(), - url.to_string(), - )?, - "wss" => Self::try_wss( - SocketAddress::from_socket_addr(sa).to_canonical(), - url.to_string(), - )?, - _ => { - unreachable!("Invalid dial info url scheme") - } - }); - } - Ok(out) - } - - pub async fn to_short(&self) -> (String, String) { - match self { - DialInfo::UDP(di) => ( - format!("U{}", di.socket_address.port()), - intf::ptr_lookup(di.socket_address.to_ip_addr()) - .await - .unwrap_or_else(|_| di.socket_address.to_string()), - ), - DialInfo::TCP(di) => ( - format!("T{}", di.socket_address.port()), - intf::ptr_lookup(di.socket_address.to_ip_addr()) - .await - .unwrap_or_else(|_| di.socket_address.to_string()), - ), - DialInfo::WS(di) => { - let mut split_url = SplitUrl::from_str(&format!("ws://{}", di.request)).unwrap(); - if let SplitUrlHost::IpAddr(a) = split_url.host { - if let Ok(host) = intf::ptr_lookup(a).await { - split_url.host = SplitUrlHost::Hostname(host); - } - } - ( - format!( - "W{}{}", - split_url.port.unwrap_or(80), - split_url - .path - .map(|p| format!("/{}", p)) - .unwrap_or_default() - ), - split_url.host.to_string(), - ) - } - DialInfo::WSS(di) => { - let mut split_url = SplitUrl::from_str(&format!("wss://{}", di.request)).unwrap(); - if let SplitUrlHost::IpAddr(a) = split_url.host { - if let Ok(host) = intf::ptr_lookup(a).await { - split_url.host = SplitUrlHost::Hostname(host); - } - } - ( - format!( - "S{}{}", - split_url.port.unwrap_or(443), - split_url - .path - .map(|p| format!("/{}", p)) - .unwrap_or_default() - ), - split_url.host.to_string(), - ) - } - } - } - pub async fn to_url(&self) -> String { - match self { - DialInfo::UDP(di) => intf::ptr_lookup(di.socket_address.to_ip_addr()) - .await - .map(|h| format!("udp://{}:{}", h, di.socket_address.port())) - .unwrap_or_else(|_| format!("udp://{}", di.socket_address)), - DialInfo::TCP(di) => intf::ptr_lookup(di.socket_address.to_ip_addr()) - .await - .map(|h| format!("tcp://{}:{}", h, di.socket_address.port())) - .unwrap_or_else(|_| format!("tcp://{}", di.socket_address)), - DialInfo::WS(di) => { - let mut split_url = SplitUrl::from_str(&format!("ws://{}", di.request)).unwrap(); - if let SplitUrlHost::IpAddr(a) = split_url.host { - if let Ok(host) = intf::ptr_lookup(a).await { - split_url.host = SplitUrlHost::Hostname(host); - } - } - split_url.to_string() - } - DialInfo::WSS(di) => { - let mut split_url = SplitUrl::from_str(&format!("wss://{}", di.request)).unwrap(); - if let SplitUrlHost::IpAddr(a) = split_url.host { - if let Ok(host) = intf::ptr_lookup(a).await { - split_url.host = SplitUrlHost::Hostname(host); - } - } - split_url.to_string() - } - } - } - - pub fn ordered_sequencing_sort(a: &DialInfo, b: &DialInfo) -> core::cmp::Ordering { - let ca = a.protocol_type().sort_order(Sequencing::EnsureOrdered); - let cb = b.protocol_type().sort_order(Sequencing::EnsureOrdered); - if ca < cb { - return core::cmp::Ordering::Less; - } - if ca > cb { - return core::cmp::Ordering::Greater; - } - match (a, b) { - (DialInfo::UDP(a), DialInfo::UDP(b)) => a.cmp(b), - (DialInfo::TCP(a), DialInfo::TCP(b)) => a.cmp(b), - (DialInfo::WS(a), DialInfo::WS(b)) => a.cmp(b), - (DialInfo::WSS(a), DialInfo::WSS(b)) => a.cmp(b), - _ => unreachable!(), - } - } -} - -impl MatchesDialInfoFilter for DialInfo { - fn matches_filter(&self, filter: &DialInfoFilter) -> bool { - if !filter.protocol_type_set.contains(self.protocol_type()) { - return false; - } - if !filter.address_type_set.contains(self.address_type()) { - return false; - } - true - } -} - -////////////////////////////////////////////////////////////////////////// - -// Signed NodeInfo that can be passed around amongst peers and verifiable -#[derive(Clone, Debug, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct SignedDirectNodeInfo { - pub node_info: NodeInfo, - pub timestamp: u64, - pub signature: Option, -} - -impl SignedDirectNodeInfo { - pub fn new( - node_id: NodeId, - node_info: NodeInfo, - timestamp: u64, - signature: DHTSignature, - ) -> Result { - let node_info_bytes = Self::make_signature_bytes(&node_info, timestamp)?; - verify(&node_id.key, &node_info_bytes, &signature)?; - Ok(Self { - node_info, - timestamp, - signature: Some(signature), - }) - } - - pub fn with_secret( - node_id: NodeId, - node_info: NodeInfo, - secret: &DHTKeySecret, - ) -> Result { - let timestamp = intf::get_timestamp(); - let node_info_bytes = Self::make_signature_bytes(&node_info, timestamp)?; - let signature = sign(&node_id.key, secret, &node_info_bytes)?; - Ok(Self { - node_info, - timestamp, - signature: Some(signature), - }) - } - - fn make_signature_bytes( - node_info: &NodeInfo, - timestamp: u64, - ) -> Result, VeilidAPIError> { - let mut node_info_bytes = Vec::new(); - - // Add nodeinfo to signature - let mut ni_msg = ::capnp::message::Builder::new_default(); - let mut ni_builder = ni_msg.init_root::(); - encode_node_info(node_info, &mut ni_builder).map_err(VeilidAPIError::internal)?; - node_info_bytes.append(&mut builder_to_vec(ni_msg).map_err(VeilidAPIError::internal)?); - - // Add timestamp to signature - node_info_bytes.append(&mut timestamp.to_le_bytes().to_vec()); - - Ok(node_info_bytes) - } - - pub fn with_no_signature(node_info: NodeInfo) -> Self { - Self { - node_info, - signature: None, - timestamp: intf::get_timestamp(), - } - } - - pub fn has_valid_signature(&self) -> bool { - self.signature.is_some() - } -} - -/// Signed NodeInfo with a relay that can be passed around amongst peers and verifiable -#[derive(Clone, Debug, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct SignedRelayedNodeInfo { - pub node_info: NodeInfo, - pub relay_id: NodeId, - pub relay_info: SignedDirectNodeInfo, - pub timestamp: u64, - pub signature: DHTSignature, -} - -impl SignedRelayedNodeInfo { - pub fn new( - node_id: NodeId, - node_info: NodeInfo, - relay_id: NodeId, - relay_info: SignedDirectNodeInfo, - timestamp: u64, - signature: DHTSignature, - ) -> Result { - let node_info_bytes = - Self::make_signature_bytes(&node_info, &relay_id, &relay_info, timestamp)?; - verify(&node_id.key, &node_info_bytes, &signature)?; - Ok(Self { - node_info, - relay_id, - relay_info, - signature, - timestamp, - }) - } - - pub fn with_secret( - node_id: NodeId, - node_info: NodeInfo, - relay_id: NodeId, - relay_info: SignedDirectNodeInfo, - secret: &DHTKeySecret, - ) -> Result { - let timestamp = intf::get_timestamp(); - let node_info_bytes = - Self::make_signature_bytes(&node_info, &relay_id, &relay_info, timestamp)?; - let signature = sign(&node_id.key, secret, &node_info_bytes)?; - Ok(Self { - node_info, - relay_id, - relay_info, - signature, - timestamp, - }) - } - - fn make_signature_bytes( - node_info: &NodeInfo, - relay_id: &NodeId, - relay_info: &SignedDirectNodeInfo, - timestamp: u64, - ) -> Result, VeilidAPIError> { - let mut sig_bytes = Vec::new(); - - // Add nodeinfo to signature - let mut ni_msg = ::capnp::message::Builder::new_default(); - let mut ni_builder = ni_msg.init_root::(); - encode_node_info(node_info, &mut ni_builder).map_err(VeilidAPIError::internal)?; - sig_bytes.append(&mut builder_to_vec(ni_msg).map_err(VeilidAPIError::internal)?); - - // Add relay id to signature - let mut rid_msg = ::capnp::message::Builder::new_default(); - let mut rid_builder = rid_msg.init_root::(); - encode_dht_key(&relay_id.key, &mut rid_builder).map_err(VeilidAPIError::internal)?; - sig_bytes.append(&mut builder_to_vec(rid_msg).map_err(VeilidAPIError::internal)?); - - // Add relay info to signature - let mut ri_msg = ::capnp::message::Builder::new_default(); - let mut ri_builder = ri_msg.init_root::(); - encode_signed_direct_node_info(relay_info, &mut ri_builder) - .map_err(VeilidAPIError::internal)?; - sig_bytes.append(&mut builder_to_vec(ri_msg).map_err(VeilidAPIError::internal)?); - - // Add timestamp to signature - sig_bytes.append(&mut timestamp.to_le_bytes().to_vec()); - - Ok(sig_bytes) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] -#[archive_attr(repr(u8), derive(CheckBytes))] -pub enum SignedNodeInfo { - Direct(SignedDirectNodeInfo), - Relayed(SignedRelayedNodeInfo), -} - -impl SignedNodeInfo { - pub fn has_valid_signature(&self) -> bool { - match self { - SignedNodeInfo::Direct(d) => d.has_valid_signature(), - SignedNodeInfo::Relayed(_) => true, - } - } - - pub fn timestamp(&self) -> u64 { - match self { - SignedNodeInfo::Direct(d) => d.timestamp, - SignedNodeInfo::Relayed(r) => r.timestamp, - } - } - - pub fn node_info(&self) -> &NodeInfo { - match self { - SignedNodeInfo::Direct(d) => &d.node_info, - SignedNodeInfo::Relayed(r) => &r.node_info, - } - } - pub fn relay_id(&self) -> Option { - match self { - SignedNodeInfo::Direct(_) => None, - SignedNodeInfo::Relayed(r) => Some(r.relay_id.clone()), - } - } - pub fn relay_info(&self) -> Option<&NodeInfo> { - match self { - SignedNodeInfo::Direct(_) => None, - SignedNodeInfo::Relayed(r) => Some(&r.relay_info.node_info), - } - } - pub fn relay_peer_info(&self) -> Option { - match self { - SignedNodeInfo::Direct(_) => None, - SignedNodeInfo::Relayed(r) => Some(PeerInfo::new( - r.relay_id.clone(), - SignedNodeInfo::Direct(r.relay_info.clone()), - )), - } - } - pub fn has_any_dial_info(&self) -> bool { - self.node_info().has_dial_info() - || self - .relay_info() - .map(|relay_ni| relay_ni.has_dial_info()) - .unwrap_or_default() - } - - pub fn has_sequencing_matched_dial_info(&self, sequencing: Sequencing) -> bool { - // Check our dial info - for did in &self.node_info().dial_info_detail_list { - match sequencing { - Sequencing::NoPreference | Sequencing::PreferOrdered => return true, - Sequencing::EnsureOrdered => { - if did.dial_info.protocol_type().is_connection_oriented() { - return true; - } - } - } - } - // Check our relay if we have one - return self - .relay_info() - .map(|relay_ni| { - for did in &relay_ni.dial_info_detail_list { - match sequencing { - Sequencing::NoPreference | Sequencing::PreferOrdered => return true, - Sequencing::EnsureOrdered => { - if did.dial_info.protocol_type().is_connection_oriented() { - return true; - } - } - } - } - false - }) - .unwrap_or_default(); - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct PeerInfo { - pub node_id: NodeId, - pub signed_node_info: SignedNodeInfo, -} - -impl PeerInfo { - pub fn new(node_id: NodeId, signed_node_info: SignedNodeInfo) -> Self { - Self { - node_id, - signed_node_info, - } - } -} - -#[derive( - Copy, - Clone, - Debug, - PartialEq, - PartialOrd, - Eq, - Ord, - Hash, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct PeerAddress { - protocol_type: ProtocolType, - #[serde(with = "json_as_string")] - socket_address: SocketAddress, -} - -impl PeerAddress { - pub fn new(socket_address: SocketAddress, protocol_type: ProtocolType) -> Self { - Self { - socket_address: socket_address.to_canonical(), - protocol_type, - } - } - - pub fn socket_address(&self) -> &SocketAddress { - &self.socket_address - } - - pub fn protocol_type(&self) -> ProtocolType { - self.protocol_type - } - - pub fn to_socket_addr(&self) -> SocketAddr { - self.socket_address.to_socket_addr() - } - - pub fn address_type(&self) -> AddressType { - self.socket_address.address_type() - } -} - -/// Represents the 5-tuple of an established connection -/// Not used to specify connections to create, that is reserved for DialInfo -/// -/// ConnectionDescriptors should never be from unspecified local addresses for connection oriented protocols -/// If the medium does not allow local addresses, None should have been used or 'new_no_local' -/// If we are specifying only a port, then the socket's 'local_address()' should have been used, since an -/// established connection is always from a real address to another real address. -#[derive( - Copy, - Clone, - Debug, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct ConnectionDescriptor { - remote: PeerAddress, - local: Option, -} - -impl ConnectionDescriptor { - pub fn new(remote: PeerAddress, local: SocketAddress) -> Self { - assert!( - !remote.protocol_type().is_connection_oriented() || !local.address().is_unspecified() - ); - - Self { - remote, - local: Some(local), - } - } - pub fn new_no_local(remote: PeerAddress) -> Self { - Self { - remote, - local: None, - } - } - pub fn remote(&self) -> PeerAddress { - self.remote - } - pub fn remote_address(&self) -> &SocketAddress { - self.remote.socket_address() - } - pub fn local(&self) -> Option { - self.local - } - pub fn protocol_type(&self) -> ProtocolType { - self.remote.protocol_type - } - pub fn address_type(&self) -> AddressType { - self.remote.address_type() - } - pub fn make_dial_info_filter(&self) -> DialInfoFilter { - DialInfoFilter::all() - .with_protocol_type(self.protocol_type()) - .with_address_type(self.address_type()) - } -} - -impl MatchesDialInfoFilter for ConnectionDescriptor { - fn matches_filter(&self, filter: &DialInfoFilter) -> bool { - if !filter.protocol_type_set.contains(self.protocol_type()) { - return false; - } - if !filter.address_type_set.contains(self.address_type()) { - return false; - } - true - } -} - -////////////////////////////////////////////////////////////////////////// - -#[derive( - Clone, - Debug, - Default, - PartialEq, - Eq, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -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, - PartialEq, - Eq, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct TransferStats { - #[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, - PartialEq, - Eq, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct TransferStatsDownUp { - pub down: TransferStats, - pub up: TransferStats, -} - -#[derive( - Clone, - Debug, - Default, - PartialEq, - Eq, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -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, - PartialEq, - Eq, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct PeerStats { - #[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 -} - -pub type ValueChangeCallback = - Arc) -> SendPinBoxFuture<()> + Send + Sync + 'static>; - -///////////////////////////////////////////////////////////////////////////////////////////////////// - -#[derive(Clone, Debug, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] -#[archive_attr(repr(u8), derive(CheckBytes))] -pub enum SignalInfo { - HolePunch { - // UDP Hole Punch Request - receipt: Vec, // Receipt to be returned after the hole punch - peer_info: PeerInfo, // Sender's peer info - }, - ReverseConnect { - // Reverse Connection Request - receipt: Vec, // Receipt to be returned by the reverse connection - peer_info: PeerInfo, // Sender's peer info - }, - // XXX: WebRTC -} - -///////////////////////////////////////////////////////////////////////////////////////////////////// -#[derive( - Copy, - Clone, - Debug, - PartialOrd, - PartialEq, - Eq, - Ord, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(u8), derive(CheckBytes))] -pub enum TunnelMode { - Raw, - Turn, -} - -#[derive( - Copy, - Clone, - Debug, - PartialOrd, - PartialEq, - Eq, - Ord, - Serialize, - Deserialize, - RkyvArchive, - RkyvSerialize, - RkyvDeserialize, -)] -#[archive_attr(repr(u8), derive(CheckBytes))] -pub enum TunnelError { - BadId, // Tunnel ID was rejected - NoEndpoint, // Endpoint was unreachable - RejectedMode, // Endpoint couldn't provide mode - NoCapacity, // Endpoint is full -} - -pub type TunnelId = u64; - -#[derive(Clone, Debug, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct TunnelEndpoint { - pub mode: TunnelMode, - pub description: String, // XXX: TODO -} - -impl Default for TunnelEndpoint { - fn default() -> Self { - Self { - mode: TunnelMode::Raw, - description: "".to_string(), - } - } -} - -#[derive( - Clone, Debug, Default, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct FullTunnel { - pub id: TunnelId, - pub timeout: u64, - pub local: TunnelEndpoint, - pub remote: TunnelEndpoint, -} - -#[derive( - Clone, Debug, Default, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, -)] -#[archive_attr(repr(C), derive(CheckBytes))] -pub struct PartialTunnel { - pub id: TunnelId, - pub timeout: u64, - pub local: TunnelEndpoint, -} - -///////////////////////////////////////////////////////////////////////////////////////////////////// - -struct VeilidAPIInner { - context: Option, -} - -impl fmt::Debug for VeilidAPIInner { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "VeilidAPIInner") - } -} - -impl Drop for VeilidAPIInner { - fn drop(&mut self) { - if let Some(context) = self.context.take() { - intf::spawn_detached(api_shutdown(context)); - } - } -} - -#[derive(Clone, Debug)] -pub struct VeilidAPI { - inner: Arc>, -} - -impl VeilidAPI { - #[instrument(skip_all)] - pub(crate) fn new(context: VeilidCoreContext) -> Self { - Self { - inner: Arc::new(Mutex::new(VeilidAPIInner { - context: Some(context), - })), - } - } - - #[instrument(skip_all)] - pub async fn shutdown(self) { - let context = { self.inner.lock().context.take() }; - if let Some(context) = context { - api_shutdown(context).await; - } - } - - pub fn is_shutdown(&self) -> bool { - self.inner.lock().context.is_none() - } - - //////////////////////////////////////////////////////////////// - // Accessors - pub fn config(&self) -> Result { - let inner = self.inner.lock(); - if let Some(context) = &inner.context { - return Ok(context.config.clone()); - } - Err(VeilidAPIError::NotInitialized) - } - pub fn crypto(&self) -> Result { - let inner = self.inner.lock(); - if let Some(context) = &inner.context { - return Ok(context.crypto.clone()); - } - Err(VeilidAPIError::NotInitialized) - } - pub fn table_store(&self) -> Result { - let inner = self.inner.lock(); - if let Some(context) = &inner.context { - return Ok(context.table_store.clone()); - } - Err(VeilidAPIError::not_initialized()) - } - pub fn block_store(&self) -> Result { - let inner = self.inner.lock(); - if let Some(context) = &inner.context { - return Ok(context.block_store.clone()); - } - Err(VeilidAPIError::not_initialized()) - } - pub fn protected_store(&self) -> Result { - let inner = self.inner.lock(); - if let Some(context) = &inner.context { - return Ok(context.protected_store.clone()); - } - Err(VeilidAPIError::not_initialized()) - } - pub fn attachment_manager(&self) -> Result { - let inner = self.inner.lock(); - if let Some(context) = &inner.context { - return Ok(context.attachment_manager.clone()); - } - Err(VeilidAPIError::not_initialized()) - } - pub fn network_manager(&self) -> Result { - let inner = self.inner.lock(); - if let Some(context) = &inner.context { - return Ok(context.attachment_manager.network_manager()); - } - Err(VeilidAPIError::not_initialized()) - } - pub fn rpc_processor(&self) -> Result { - let inner = self.inner.lock(); - if let Some(context) = &inner.context { - return Ok(context.attachment_manager.network_manager().rpc_processor()); - } - Err(VeilidAPIError::NotInitialized) - } - pub fn routing_table(&self) -> Result { - let inner = self.inner.lock(); - if let Some(context) = &inner.context { - return Ok(context.attachment_manager.network_manager().routing_table()); - } - Err(VeilidAPIError::NotInitialized) - } - - //////////////////////////////////////////////////////////////// - // Attach/Detach - - // get a full copy of the current state - pub async fn get_state(&self) -> Result { - let attachment_manager = self.attachment_manager()?; - let network_manager = attachment_manager.network_manager(); - let config = self.config()?; - - let attachment = attachment_manager.get_veilid_state(); - let network = network_manager.get_veilid_state(); - let config = config.get_veilid_state(); - - Ok(VeilidState { - attachment, - network, - config, - }) - } - - // get network connectedness - - // connect to the network - #[instrument(level = "debug", err, skip_all)] - pub async fn attach(&self) -> Result<(), VeilidAPIError> { - let attachment_manager = self.attachment_manager()?; - attachment_manager - .request_attach() - .await - .map_err(|e| VeilidAPIError::internal(e)) - } - - // disconnect from the network - #[instrument(level = "debug", err, skip_all)] - pub async fn detach(&self) -> Result<(), VeilidAPIError> { - let attachment_manager = self.attachment_manager()?; - attachment_manager - .request_detach() - .await - .map_err(|e| VeilidAPIError::internal(e)) - } - - //////////////////////////////////////////////////////////////// - // Routing Context - - #[instrument(level = "debug", skip(self))] - pub fn routing_context(&self) -> RoutingContext { - RoutingContext::new(self.clone()) - } - - //////////////////////////////////////////////////////////////// - // Private route allocation - - #[instrument(level = "debug", skip(self))] - pub async fn new_private_route(&self) -> Result<(DHTKey, Vec), VeilidAPIError> { - self.new_custom_private_route(Stability::default(), Sequencing::default()) - .await - } - - #[instrument(level = "debug", skip(self))] - pub async fn new_custom_private_route( - &self, - stability: Stability, - sequencing: Sequencing, - ) -> Result<(DHTKey, Vec), VeilidAPIError> { - let default_route_hop_count: usize = { - let config = self.config()?; - let c = config.get(); - c.network.rpc.default_route_hop_count.into() - }; - - let rss = self.routing_table()?.route_spec_store(); - let r = rss - .allocate_route( - stability, - sequencing, - default_route_hop_count, - Direction::Inbound.into(), - &[], - ) - .map_err(VeilidAPIError::internal)?; - let Some(pr_pubkey) = r else { - apibail_generic!("unable to allocate route"); - }; - if !rss - .test_route(&pr_pubkey) - .await - .map_err(VeilidAPIError::no_connection)? - { - rss.release_route(&pr_pubkey); - apibail_generic!("allocated route failed to test"); - } - let private_route = rss - .assemble_private_route(&pr_pubkey, Some(true)) - .map_err(VeilidAPIError::generic)?; - let blob = match RouteSpecStore::private_route_to_blob(&private_route) { - Ok(v) => v, - Err(e) => { - rss.release_route(&pr_pubkey); - apibail_internal!(e); - } - }; - - rss.mark_route_published(&pr_pubkey, true) - .map_err(VeilidAPIError::internal)?; - - Ok((pr_pubkey, blob)) - } - - #[instrument(level = "debug", skip(self))] - pub fn import_remote_private_route(&self, blob: Vec) -> Result { - let rss = self.routing_table()?.route_spec_store(); - rss.import_remote_private_route(blob) - .map_err(|e| VeilidAPIError::invalid_argument(e, "blob", "private route blob")) - } - - #[instrument(level = "debug", skip(self))] - pub fn release_private_route(&self, key: &DHTKey) -> Result<(), VeilidAPIError> { - let rss = self.routing_table()?.route_spec_store(); - if rss.release_route(key) { - Ok(()) - } else { - Err(VeilidAPIError::invalid_argument( - "release_private_route", - "key", - key, - )) - } - } - - //////////////////////////////////////////////////////////////// - // App Calls - - #[instrument(level = "debug", skip(self))] - pub async fn app_call_reply(&self, id: u64, message: Vec) -> Result<(), VeilidAPIError> { - let rpc_processor = self.rpc_processor()?; - rpc_processor - .app_call_reply(id, message) - .await - .map_err(|e| e.into()) - } - - //////////////////////////////////////////////////////////////// - // Tunnel Building - - #[instrument(level = "debug", err, skip(self))] - pub async fn start_tunnel( - &self, - _endpoint_mode: TunnelMode, - _depth: u8, - ) -> Result { - panic!("unimplemented"); - } - - #[instrument(level = "debug", err, skip(self))] - pub async fn complete_tunnel( - &self, - _endpoint_mode: TunnelMode, - _depth: u8, - _partial_tunnel: PartialTunnel, - ) -> Result { - panic!("unimplemented"); - } - - #[instrument(level = "debug", err, skip(self))] - pub async fn cancel_tunnel(&self, _tunnel_id: TunnelId) -> Result { - panic!("unimplemented"); - } -} diff --git a/veilid-core/src/veilid_api/routing_context.rs b/veilid-core/src/veilid_api/routing_context.rs index 3af6eaae..4e712330 100644 --- a/veilid-core/src/veilid_api/routing_context.rs +++ b/veilid-core/src/veilid_api/routing_context.rs @@ -1,4 +1,5 @@ use super::*; + /////////////////////////////////////////////////////////////////////////////////////// #[derive(Clone, Debug)] @@ -106,7 +107,7 @@ impl RoutingContext { // Resolve node let mut nr = match rpc_processor.resolve_node(node_id.key).await { Ok(Some(nr)) => nr, - Ok(None) => return Err(VeilidAPIError::KeyNotFound { key: node_id.key }), + Ok(None) => apibail_key_not_found!(node_id.key), Err(e) => return Err(e.into()), }; // Apply sequencing to match safety selection @@ -123,7 +124,7 @@ impl RoutingContext { let Some(private_route) = rss .get_remote_private_route(&pr) else { - return Err(VeilidAPIError::KeyNotFound { key: pr }); + apibail_key_not_found!(pr); }; Ok(rpc_processor::Destination::PrivateRoute { @@ -151,15 +152,13 @@ impl RoutingContext { // Send app message let answer = match rpc_processor.rpc_call_app_call(dest, request).await { Ok(NetworkResult::Value(v)) => v, - Ok(NetworkResult::Timeout) => return Err(VeilidAPIError::Timeout), + Ok(NetworkResult::Timeout) => apibail_timeout!(), Ok(NetworkResult::NoConnection(e)) | Ok(NetworkResult::AlreadyExists(e)) => { - return Err(VeilidAPIError::NoConnection { - message: e.to_string(), - }) + apibail_no_connection!(e); } Ok(NetworkResult::InvalidMessage(message)) => { - return Err(VeilidAPIError::Generic { message }) + apibail_generic!(message); } Err(e) => return Err(e.into()), }; @@ -181,14 +180,12 @@ impl RoutingContext { // Send app message match rpc_processor.rpc_call_app_message(dest, message).await { Ok(NetworkResult::Value(())) => {} - Ok(NetworkResult::Timeout) => return Err(VeilidAPIError::Timeout), + Ok(NetworkResult::Timeout) => apibail_timeout!(), Ok(NetworkResult::NoConnection(e)) | Ok(NetworkResult::AlreadyExists(e)) => { - return Err(VeilidAPIError::NoConnection { - message: e.to_string(), - }) + apibail_no_connection!(e); } Ok(NetworkResult::InvalidMessage(message)) => { - return Err(VeilidAPIError::Generic { message }) + apibail_generic!(message); } Err(e) => return Err(e.into()), }; diff --git a/veilid-core/src/veilid_api/types.rs b/veilid-core/src/veilid_api/types.rs new file mode 100644 index 00000000..e8bb0e96 --- /dev/null +++ b/veilid-core/src/veilid_api/types.rs @@ -0,0 +1,2402 @@ +use super::*; + +///////////////////////////////////////////////////////////////////////////////////////////////////// + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Copy, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(u8), derive(CheckBytes))] +pub enum VeilidLogLevel { + Error = 1, + Warn, + Info, + Debug, + Trace, +} + +impl VeilidLogLevel { + pub fn from_tracing_level(level: tracing::Level) -> VeilidLogLevel { + match level { + tracing::Level::ERROR => VeilidLogLevel::Error, + tracing::Level::WARN => VeilidLogLevel::Warn, + tracing::Level::INFO => VeilidLogLevel::Info, + tracing::Level::DEBUG => VeilidLogLevel::Debug, + tracing::Level::TRACE => VeilidLogLevel::Trace, + } + } + pub fn from_log_level(level: log::Level) -> VeilidLogLevel { + match level { + log::Level::Error => VeilidLogLevel::Error, + log::Level::Warn => VeilidLogLevel::Warn, + log::Level::Info => VeilidLogLevel::Info, + log::Level::Debug => VeilidLogLevel::Debug, + log::Level::Trace => VeilidLogLevel::Trace, + } + } + pub fn to_tracing_level(&self) -> tracing::Level { + match self { + Self::Error => tracing::Level::ERROR, + Self::Warn => tracing::Level::WARN, + Self::Info => tracing::Level::INFO, + Self::Debug => tracing::Level::DEBUG, + Self::Trace => tracing::Level::TRACE, + } + } + pub fn to_log_level(&self) -> log::Level { + match self { + Self::Error => log::Level::Error, + Self::Warn => log::Level::Warn, + Self::Info => log::Level::Info, + Self::Debug => log::Level::Debug, + Self::Trace => log::Level::Trace, + } + } +} + +impl fmt::Display for VeilidLogLevel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + let text = match self { + Self::Error => "ERROR", + Self::Warn => "WARN", + Self::Info => "INFO", + Self::Debug => "DEBUG", + Self::Trace => "TRACE", + }; + write!(f, "{}", text) + } +} + +#[derive( + Debug, Clone, PartialEq, Eq, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct VeilidLog { + pub log_level: VeilidLogLevel, + pub message: String, + pub backtrace: Option, +} + +#[derive( + Debug, Clone, PartialEq, Eq, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct VeilidAppMessage { + /// Some(sender) if the message was sent directly, None if received via a private/safety route + #[serde(with = "opt_json_as_string")] + pub sender: Option, + /// The content of the message to deliver to the application + #[serde(with = "json_as_base64")] + pub message: Vec, +} + +#[derive( + Debug, Clone, PartialEq, Eq, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct VeilidAppCall { + /// Some(sender) if the request was sent directly, None if received via a private/safety route + #[serde(with = "opt_json_as_string")] + pub sender: Option, + /// The content of the request to deliver to the application + #[serde(with = "json_as_base64")] + pub message: Vec, + /// The id to reply to + #[serde(with = "json_as_string")] + pub id: u64, +} + +#[derive( + Debug, Clone, PartialEq, Eq, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct VeilidStateAttachment { + pub state: AttachmentState, +} + +#[derive( + Debug, Clone, PartialEq, Eq, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct PeerTableData { + pub node_id: DHTKey, + pub peer_address: PeerAddress, + pub peer_stats: PeerStats, +} + +#[derive( + Debug, Clone, PartialEq, Eq, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +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, PartialEq, Eq, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct VeilidStateRoute { + pub dead_routes: Vec, + pub dead_remote_routes: Vec, +} + +#[derive( + Debug, Clone, PartialEq, Eq, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct VeilidStateConfig { + pub config: VeilidConfigInner, +} + +#[derive(Debug, Clone, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] +#[archive_attr(repr(u8), derive(CheckBytes))] +#[serde(tag = "kind")] +pub enum VeilidUpdate { + Log(VeilidLog), + AppMessage(VeilidAppMessage), + AppCall(VeilidAppCall), + Attachment(VeilidStateAttachment), + Network(VeilidStateNetwork), + Config(VeilidStateConfig), + Route(VeilidStateRoute), + Shutdown, +} + +#[derive(Debug, Clone, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct VeilidState { + pub attachment: VeilidStateAttachment, + pub network: VeilidStateNetwork, + pub config: VeilidStateConfig, +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// +/// +#[derive( + Clone, + Debug, + Default, + PartialOrd, + PartialEq, + Eq, + Ord, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct NodeId { + pub key: DHTKey, +} +impl NodeId { + pub fn new(key: DHTKey) -> Self { + Self { key } + } +} +impl fmt::Display for NodeId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!(f, "{}", self.key.encode()) + } +} +impl FromStr for NodeId { + type Err = VeilidAPIError; + fn from_str(s: &str) -> Result { + Ok(Self { + key: DHTKey::try_decode(s)?, + }) + } +} + +#[derive( + Clone, + Debug, + Default, + PartialOrd, + PartialEq, + Eq, + Ord, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct ValueKey { + pub key: DHTKey, + pub subkey: Option, +} +impl ValueKey { + pub fn new(key: DHTKey) -> Self { + Self { key, subkey: None } + } + pub fn new_subkey(key: DHTKey, subkey: String) -> Self { + Self { + key, + subkey: if subkey.is_empty() { + None + } else { + Some(subkey) + }, + } + } +} + +#[derive( + Clone, + Debug, + Default, + PartialOrd, + PartialEq, + Eq, + Ord, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct ValueData { + pub data: Vec, + pub seq: u32, +} +impl ValueData { + pub fn new(data: Vec) -> Self { + Self { data, seq: 0 } + } + pub fn new_with_seq(data: Vec, seq: u32) -> Self { + Self { data, seq } + } + pub fn change(&mut self, data: Vec) { + self.data = data; + self.seq += 1; + } +} + +#[derive( + Clone, + Debug, + Default, + PartialOrd, + PartialEq, + Eq, + Ord, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct BlockId { + pub key: DHTKey, +} +impl BlockId { + pub fn new(key: DHTKey) -> Self { + Self { key } + } +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// + +// Keep member order appropriate for sorting < preference +#[derive( + Copy, + Clone, + Debug, + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(u8), derive(CheckBytes))] +pub enum DialInfoClass { + Direct = 0, // D = Directly reachable with public IP and no firewall, with statically configured port + Mapped = 1, // M = Directly reachable with via portmap behind any NAT or firewalled with dynamically negotiated port + FullConeNAT = 2, // F = Directly reachable device without portmap behind full-cone NAT + Blocked = 3, // B = Inbound blocked at firewall but may hole punch with public address + AddressRestrictedNAT = 4, // A = Device without portmap behind address-only restricted NAT + PortRestrictedNAT = 5, // P = Device without portmap behind address-and-port restricted NAT +} + +impl DialInfoClass { + // Is a signal required to do an inbound hole-punch? + pub fn requires_signal(&self) -> bool { + matches!( + self, + Self::Blocked | Self::AddressRestrictedNAT | Self::PortRestrictedNAT + ) + } + + // Does a relay node need to be allocated for this dial info? + // For full cone NAT, the relay itself may not be used but the keepalive sent to it + // is required to keep the NAT mapping valid in the router state table + pub fn requires_relay(&self) -> bool { + matches!( + self, + Self::FullConeNAT + | Self::Blocked + | Self::AddressRestrictedNAT + | Self::PortRestrictedNAT + ) + } +} + +// Ordering here matters, >= is used to check strength of sequencing requirement +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(u8), derive(CheckBytes))] +pub enum Sequencing { + NoPreference, + PreferOrdered, + EnsureOrdered, +} + +impl Default for Sequencing { + fn default() -> Self { + Self::NoPreference + } +} + +// Ordering here matters, >= is used to check strength of stability requirement +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(u8), derive(CheckBytes))] +pub enum Stability { + LowLatency, + Reliable, +} + +impl Default for Stability { + fn default() -> Self { + Self::LowLatency + } +} + +/// The choice of safety route to include in compiled routes +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(u8), derive(CheckBytes))] +pub enum SafetySelection { + /// Don't use a safety route, only specify the sequencing preference + Unsafe(Sequencing), + /// Use a safety route and parameters specified by a SafetySpec + Safe(SafetySpec), +} + +/// Options for safety routes (sender privacy) +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct SafetySpec { + /// preferred safety route if it still exists + pub preferred_route: Option, + /// must be greater than 0 + pub hop_count: usize, + /// prefer reliability over speed + pub stability: Stability, + /// prefer connection-oriented sequenced protocols + pub sequencing: Sequencing, +} + +// Keep member order appropriate for sorting < preference +#[derive( + Debug, + Clone, + PartialEq, + PartialOrd, + Ord, + Eq, + Hash, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct DialInfoDetail { + pub class: DialInfoClass, + pub dial_info: DialInfo, +} + +impl MatchesDialInfoFilter for DialInfoDetail { + fn matches_filter(&self, filter: &DialInfoFilter) -> bool { + self.dial_info.matches_filter(filter) + } +} + +impl DialInfoDetail { + pub fn ordered_sequencing_sort(a: &DialInfoDetail, b: &DialInfoDetail) -> core::cmp::Ordering { + if a.class < b.class { + return core::cmp::Ordering::Less; + } + if a.class > b.class { + return core::cmp::Ordering::Greater; + } + DialInfo::ordered_sequencing_sort(&a.dial_info, &b.dial_info) + } + pub const NO_SORT: std::option::Option< + for<'r, 's> fn( + &'r veilid_api::DialInfoDetail, + &'s veilid_api::DialInfoDetail, + ) -> std::cmp::Ordering, + > = None:: core::cmp::Ordering>; +} + +#[derive( + Copy, + Clone, + Debug, + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(u8), derive(CheckBytes))] +pub enum NetworkClass { + InboundCapable = 0, // I = Inbound capable without relay, may require signal + OutboundOnly = 1, // O = Outbound only, inbound relay required except with reverse connect signal + WebApp = 2, // W = PWA, outbound relay is required in most cases + Invalid = 3, // X = Invalid network class, we don't know how to reach this node +} + +impl Default for NetworkClass { + fn default() -> Self { + Self::Invalid + } +} + +impl NetworkClass { + // Should an outbound relay be kept available? + pub fn outbound_wants_relay(&self) -> bool { + matches!(self, Self::WebApp) + } +} + +/// RoutingDomain-specific status for each node +/// is returned by the StatusA call + +/// PublicInternet RoutingDomain Status +#[derive( + Clone, Debug, Default, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct PublicInternetNodeStatus { + pub will_route: bool, + pub will_tunnel: bool, + pub will_signal: bool, + pub will_relay: bool, + pub will_validate_dial_info: bool, +} + +#[derive( + Clone, Debug, Default, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct LocalNetworkNodeStatus { + pub will_relay: bool, + pub will_validate_dial_info: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] +#[archive_attr(repr(u8), derive(CheckBytes))] +pub enum NodeStatus { + PublicInternet(PublicInternetNodeStatus), + LocalNetwork(LocalNetworkNodeStatus), +} + +impl NodeStatus { + pub fn will_route(&self) -> bool { + match self { + NodeStatus::PublicInternet(pi) => pi.will_route, + NodeStatus::LocalNetwork(_) => false, + } + } + pub fn will_tunnel(&self) -> bool { + match self { + NodeStatus::PublicInternet(pi) => pi.will_tunnel, + NodeStatus::LocalNetwork(_) => false, + } + } + pub fn will_signal(&self) -> bool { + match self { + NodeStatus::PublicInternet(pi) => pi.will_signal, + NodeStatus::LocalNetwork(_) => false, + } + } + pub fn will_relay(&self) -> bool { + match self { + NodeStatus::PublicInternet(pi) => pi.will_relay, + NodeStatus::LocalNetwork(ln) => ln.will_relay, + } + } + pub fn will_validate_dial_info(&self) -> bool { + match self { + NodeStatus::PublicInternet(pi) => pi.will_validate_dial_info, + NodeStatus::LocalNetwork(ln) => ln.will_validate_dial_info, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct NodeInfo { + pub network_class: NetworkClass, + #[with(RkyvEnumSet)] + pub outbound_protocols: ProtocolTypeSet, + #[with(RkyvEnumSet)] + pub address_types: AddressTypeSet, + pub min_version: u8, + pub max_version: u8, + pub dial_info_detail_list: Vec, +} + +impl NodeInfo { + pub fn first_filtered_dial_info_detail( + &self, + sort: Option, + filter: F, + ) -> Option + where + S: Fn(&DialInfoDetail, &DialInfoDetail) -> std::cmp::Ordering, + F: Fn(&DialInfoDetail) -> bool, + { + if let Some(sort) = sort { + let mut dids = self.dial_info_detail_list.clone(); + dids.sort_by(sort); + for did in dids { + if filter(&did) { + return Some(did); + } + } + } else { + for did in &self.dial_info_detail_list { + if filter(did) { + return Some(did.clone()); + } + } + }; + None + } + + pub fn all_filtered_dial_info_details( + &self, + sort: Option, + filter: F, + ) -> Vec + where + S: Fn(&DialInfoDetail, &DialInfoDetail) -> std::cmp::Ordering, + F: Fn(&DialInfoDetail) -> bool, + { + let mut dial_info_detail_list = Vec::new(); + + if let Some(sort) = sort { + let mut dids = self.dial_info_detail_list.clone(); + dids.sort_by(sort); + for did in dids { + if filter(&did) { + dial_info_detail_list.push(did); + } + } + } else { + for did in &self.dial_info_detail_list { + if filter(did) { + dial_info_detail_list.push(did.clone()); + } + } + }; + dial_info_detail_list + } + + /// Does this node has some dial info + pub fn has_dial_info(&self) -> bool { + !self.dial_info_detail_list.is_empty() + } + + /// Is some relay required either for signal or inbound relay or outbound relay? + pub fn requires_relay(&self) -> bool { + match self.network_class { + NetworkClass::InboundCapable => { + for did in &self.dial_info_detail_list { + if did.class.requires_relay() { + return true; + } + } + } + NetworkClass::OutboundOnly => { + return true; + } + NetworkClass::WebApp => { + return true; + } + NetworkClass::Invalid => {} + } + false + } + + /// Can this node assist with signalling? Yes but only if it doesn't require signalling, itself. + pub fn can_signal(&self) -> bool { + // Must be inbound capable + if !matches!(self.network_class, NetworkClass::InboundCapable) { + return false; + } + // Do any of our dial info require signalling? if so, we can't offer signalling + for did in &self.dial_info_detail_list { + if did.class.requires_signal() { + return false; + } + } + true + } + + /// Can this node relay be an inbound relay? + pub fn can_inbound_relay(&self) -> bool { + // For now this is the same + self.can_signal() + } + + /// Is this node capable of validating dial info + pub fn can_validate_dial_info(&self) -> bool { + // For now this is the same + self.can_signal() + } +} + +#[allow(clippy::derive_hash_xor_eq)] +#[derive( + Debug, + PartialOrd, + Ord, + Hash, + EnumSetType, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[enumset(repr = "u8")] +#[archive_attr(repr(u8), derive(CheckBytes))] +pub enum Direction { + Inbound, + Outbound, +} +pub type DirectionSet = EnumSet; + +// Keep member order appropriate for sorting < preference +// Must match DialInfo order +#[allow(clippy::derive_hash_xor_eq)] +#[derive( + Debug, + PartialOrd, + Ord, + Hash, + EnumSetType, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[enumset(repr = "u8")] +#[archive_attr(repr(u8), derive(CheckBytes))] +pub enum LowLevelProtocolType { + UDP, + TCP, +} + +impl LowLevelProtocolType { + pub fn is_connection_oriented(&self) -> bool { + matches!(self, LowLevelProtocolType::TCP) + } +} +pub type LowLevelProtocolTypeSet = EnumSet; + +// Keep member order appropriate for sorting < preference +// Must match DialInfo order +#[allow(clippy::derive_hash_xor_eq)] +#[derive( + Debug, + PartialOrd, + Ord, + Hash, + EnumSetType, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[enumset(repr = "u8")] +#[archive_attr(repr(u8), derive(CheckBytes))] +pub enum ProtocolType { + UDP, + TCP, + WS, + WSS, +} + +impl ProtocolType { + pub fn is_connection_oriented(&self) -> bool { + matches!( + self, + ProtocolType::TCP | ProtocolType::WS | ProtocolType::WSS + ) + } + pub fn low_level_protocol_type(&self) -> LowLevelProtocolType { + match self { + ProtocolType::UDP => LowLevelProtocolType::UDP, + ProtocolType::TCP | ProtocolType::WS | ProtocolType::WSS => LowLevelProtocolType::TCP, + } + } + pub fn sort_order(&self, sequencing: Sequencing) -> usize { + match self { + ProtocolType::UDP => { + if sequencing != Sequencing::NoPreference { + 3 + } else { + 0 + } + } + ProtocolType::TCP => { + if sequencing != Sequencing::NoPreference { + 0 + } else { + 1 + } + } + ProtocolType::WS => { + if sequencing != Sequencing::NoPreference { + 1 + } else { + 2 + } + } + ProtocolType::WSS => { + if sequencing != Sequencing::NoPreference { + 2 + } else { + 3 + } + } + } + } + pub fn all_ordered_set() -> ProtocolTypeSet { + ProtocolType::TCP | ProtocolType::WS | ProtocolType::WSS + } +} + +pub type ProtocolTypeSet = EnumSet; + +#[allow(clippy::derive_hash_xor_eq)] +#[derive( + Debug, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, + EnumSetType, +)] +#[enumset(repr = "u8")] +#[archive_attr(repr(u8), derive(CheckBytes))] +pub enum AddressType { + IPV4, + IPV6, +} +pub type AddressTypeSet = EnumSet; + +// Routing domain here is listed in order of preference, keep in order +#[allow(clippy::derive_hash_xor_eq)] +#[derive( + Debug, + Ord, + PartialOrd, + Hash, + EnumSetType, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[enumset(repr = "u8")] +#[archive_attr(repr(u8), derive(CheckBytes))] +pub enum RoutingDomain { + LocalNetwork = 0, + PublicInternet = 1, +} +impl RoutingDomain { + pub const fn count() -> usize { + 2 + } + pub const fn all() -> [RoutingDomain; RoutingDomain::count()] { + // Routing domain here is listed in order of preference, keep in order + [RoutingDomain::LocalNetwork, RoutingDomain::PublicInternet] + } +} +pub type RoutingDomainSet = EnumSet; + +#[derive( + Copy, + Clone, + Debug, + PartialEq, + PartialOrd, + Ord, + Eq, + Hash, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(u8), derive(CheckBytes))] +pub enum Address { + IPV4(Ipv4Addr), + IPV6(Ipv6Addr), +} + +impl Default for Address { + fn default() -> Self { + Address::IPV4(Ipv4Addr::new(0, 0, 0, 0)) + } +} + +impl Address { + pub fn from_socket_addr(sa: SocketAddr) -> Address { + match sa { + SocketAddr::V4(v4) => Address::IPV4(*v4.ip()), + SocketAddr::V6(v6) => Address::IPV6(*v6.ip()), + } + } + pub fn from_ip_addr(addr: IpAddr) -> Address { + match addr { + IpAddr::V4(v4) => Address::IPV4(v4), + IpAddr::V6(v6) => Address::IPV6(v6), + } + } + pub fn address_type(&self) -> AddressType { + match self { + Address::IPV4(_) => AddressType::IPV4, + Address::IPV6(_) => AddressType::IPV6, + } + } + pub fn address_string(&self) -> String { + match self { + Address::IPV4(v4) => v4.to_string(), + Address::IPV6(v6) => v6.to_string(), + } + } + pub fn address_string_with_port(&self, port: u16) -> String { + match self { + Address::IPV4(v4) => format!("{}:{}", v4, port), + Address::IPV6(v6) => format!("[{}]:{}", v6, port), + } + } + pub fn is_unspecified(&self) -> bool { + match self { + Address::IPV4(v4) => ipv4addr_is_unspecified(v4), + Address::IPV6(v6) => ipv6addr_is_unspecified(v6), + } + } + pub fn is_global(&self) -> bool { + match self { + Address::IPV4(v4) => ipv4addr_is_global(v4) && !ipv4addr_is_multicast(v4), + Address::IPV6(v6) => ipv6addr_is_unicast_global(v6), + } + } + pub fn is_local(&self) -> bool { + match self { + Address::IPV4(v4) => { + ipv4addr_is_private(v4) + || ipv4addr_is_link_local(v4) + || ipv4addr_is_ietf_protocol_assignment(v4) + } + Address::IPV6(v6) => { + ipv6addr_is_unicast_site_local(v6) + || ipv6addr_is_unicast_link_local(v6) + || ipv6addr_is_unique_local(v6) + } + } + } + pub fn to_ip_addr(&self) -> IpAddr { + match self { + Self::IPV4(a) => IpAddr::V4(*a), + Self::IPV6(a) => IpAddr::V6(*a), + } + } + pub fn to_socket_addr(&self, port: u16) -> SocketAddr { + SocketAddr::new(self.to_ip_addr(), port) + } + pub fn to_canonical(&self) -> Address { + match self { + Address::IPV4(v4) => Address::IPV4(*v4), + Address::IPV6(v6) => match v6.to_ipv4() { + Some(v4) => Address::IPV4(v4), + None => Address::IPV6(*v6), + }, + } + } +} + +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 { + if let Ok(addr) = Ipv4Addr::from_str(host) { + Ok(Address::IPV4(addr)) + } else if let Ok(addr) = Ipv6Addr::from_str(host) { + Ok(Address::IPV6(addr)) + } else { + Err(VeilidAPIError::parse_error( + "Address::from_str failed", + host, + )) + } + } +} + +#[derive( + Copy, + Default, + Clone, + Debug, + PartialEq, + PartialOrd, + Ord, + Eq, + Hash, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct SocketAddress { + address: Address, + port: u16, +} + +impl SocketAddress { + pub fn new(address: Address, port: u16) -> Self { + Self { address, port } + } + pub fn from_socket_addr(sa: SocketAddr) -> SocketAddress { + Self { + address: Address::from_socket_addr(sa), + port: sa.port(), + } + } + pub fn address(&self) -> Address { + self.address + } + pub fn address_type(&self) -> AddressType { + self.address.address_type() + } + pub fn port(&self) -> u16 { + self.port + } + pub fn to_canonical(&self) -> SocketAddress { + SocketAddress { + address: self.address.to_canonical(), + port: self.port, + } + } + pub fn to_ip_addr(&self) -> IpAddr { + self.address.to_ip_addr() + } + pub fn to_socket_addr(&self) -> SocketAddr { + self.address.to_socket_addr(self.port) + } +} + +impl fmt::Display for SocketAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!(f, "{}", self.to_socket_addr()) + } +} + +impl FromStr for SocketAddress { + type Err = VeilidAPIError; + fn from_str(s: &str) -> Result { + let sa = SocketAddr::from_str(s) + .map_err(|e| VeilidAPIError::parse_error("Failed to parse SocketAddress", e))?; + Ok(SocketAddress::from_socket_addr(sa)) + } +} + +////////////////////////////////////////////////////////////////// + +#[derive( + Copy, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct DialInfoFilter { + #[with(RkyvEnumSet)] + pub protocol_type_set: ProtocolTypeSet, + #[with(RkyvEnumSet)] + pub address_type_set: AddressTypeSet, +} + +impl Default for DialInfoFilter { + fn default() -> Self { + Self { + protocol_type_set: ProtocolTypeSet::all(), + address_type_set: AddressTypeSet::all(), + } + } +} + +impl DialInfoFilter { + pub fn all() -> Self { + Self { + protocol_type_set: ProtocolTypeSet::all(), + address_type_set: AddressTypeSet::all(), + } + } + pub fn with_protocol_type(mut self, protocol_type: ProtocolType) -> Self { + self.protocol_type_set = ProtocolTypeSet::only(protocol_type); + self + } + pub fn with_protocol_type_set(mut self, protocol_set: ProtocolTypeSet) -> Self { + self.protocol_type_set = protocol_set; + self + } + pub fn with_address_type(mut self, address_type: AddressType) -> Self { + self.address_type_set = AddressTypeSet::only(address_type); + self + } + pub fn with_address_type_set(mut self, address_set: AddressTypeSet) -> Self { + self.address_type_set = address_set; + self + } + pub fn filtered(mut self, other_dif: &DialInfoFilter) -> Self { + self.protocol_type_set &= other_dif.protocol_type_set; + self.address_type_set &= other_dif.address_type_set; + self + } + pub fn is_dead(&self) -> bool { + self.protocol_type_set.is_empty() || self.address_type_set.is_empty() + } +} + +impl fmt::Debug for DialInfoFilter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + let mut out = String::new(); + if self.protocol_type_set != ProtocolTypeSet::all() { + out += &format!("+{:?}", self.protocol_type_set); + } else { + out += "*"; + } + if self.address_type_set != AddressTypeSet::all() { + out += &format!("+{:?}", self.address_type_set); + } else { + out += "*"; + } + write!(f, "[{}]", out) + } +} + +pub trait MatchesDialInfoFilter { + fn matches_filter(&self, filter: &DialInfoFilter) -> bool; +} + +#[derive( + Clone, + Default, + Debug, + PartialEq, + PartialOrd, + Ord, + Eq, + Hash, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct DialInfoUDP { + pub socket_address: SocketAddress, +} + +#[derive( + Clone, + Default, + Debug, + PartialEq, + PartialOrd, + Ord, + Eq, + Hash, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct DialInfoTCP { + pub socket_address: SocketAddress, +} + +#[derive( + Clone, + Default, + Debug, + PartialEq, + PartialOrd, + Ord, + Eq, + Hash, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct DialInfoWS { + pub socket_address: SocketAddress, + pub request: String, +} + +#[derive( + Clone, + Default, + Debug, + PartialEq, + PartialOrd, + Ord, + Eq, + Hash, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct DialInfoWSS { + pub socket_address: SocketAddress, + pub request: String, +} + +// Keep member order appropriate for sorting < preference +// Must match ProtocolType order +#[derive( + Clone, + Debug, + PartialEq, + PartialOrd, + Ord, + Eq, + Hash, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(u8), derive(CheckBytes))] +#[serde(tag = "kind")] +pub enum DialInfo { + UDP(DialInfoUDP), + TCP(DialInfoTCP), + WS(DialInfoWS), + WSS(DialInfoWSS), +} +impl Default for DialInfo { + fn default() -> Self { + DialInfo::UDP(DialInfoUDP::default()) + } +} + +impl fmt::Display for DialInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + DialInfo::UDP(di) => write!(f, "udp|{}", di.socket_address), + DialInfo::TCP(di) => write!(f, "tcp|{}", di.socket_address), + DialInfo::WS(di) => { + let url = format!("ws://{}", di.request); + let split_url = SplitUrl::from_str(&url).unwrap(); + match split_url.host { + SplitUrlHost::Hostname(_) => { + write!(f, "ws|{}|{}", di.socket_address.to_ip_addr(), di.request) + } + SplitUrlHost::IpAddr(a) => { + if di.socket_address.to_ip_addr() == a { + write!(f, "ws|{}", di.request) + } else { + panic!("resolved address does not match url: {}", di.request); + } + } + } + } + DialInfo::WSS(di) => { + let url = format!("wss://{}", di.request); + let split_url = SplitUrl::from_str(&url).unwrap(); + match split_url.host { + SplitUrlHost::Hostname(_) => { + write!(f, "wss|{}|{}", di.socket_address.to_ip_addr(), di.request) + } + SplitUrlHost::IpAddr(_) => { + panic!( + "secure websockets can not use ip address in request: {}", + di.request + ); + } + } + } + } + } +} + +impl FromStr for DialInfo { + type Err = VeilidAPIError; + fn from_str(s: &str) -> Result { + let (proto, rest) = s.split_once('|').ok_or_else(|| { + VeilidAPIError::parse_error("DialInfo::from_str missing protocol '|' separator", s) + })?; + match proto { + "udp" => { + let socket_address = SocketAddress::from_str(rest)?; + Ok(DialInfo::udp(socket_address)) + } + "tcp" => { + let socket_address = SocketAddress::from_str(rest)?; + Ok(DialInfo::tcp(socket_address)) + } + "ws" => { + let url = format!("ws://{}", rest); + let split_url = SplitUrl::from_str(&url).map_err(|e| { + VeilidAPIError::parse_error(format!("unable to split WS url: {}", e), &url) + })?; + if split_url.scheme != "ws" || !url.starts_with("ws://") { + apibail_parse_error!("incorrect scheme for WS dialinfo", url); + } + let url_port = split_url.port.unwrap_or(80u16); + + match rest.split_once('|') { + Some((sa, rest)) => { + let address = Address::from_str(sa)?; + + DialInfo::try_ws( + SocketAddress::new(address, url_port), + format!("ws://{}", rest), + ) + } + None => { + let address = Address::from_str(&split_url.host.to_string())?; + DialInfo::try_ws( + SocketAddress::new(address, url_port), + format!("ws://{}", rest), + ) + } + } + } + "wss" => { + let url = format!("wss://{}", rest); + let split_url = SplitUrl::from_str(&url).map_err(|e| { + VeilidAPIError::parse_error(format!("unable to split WSS url: {}", e), &url) + })?; + if split_url.scheme != "wss" || !url.starts_with("wss://") { + apibail_parse_error!("incorrect scheme for WSS dialinfo", url); + } + let url_port = split_url.port.unwrap_or(443u16); + + let (a, rest) = rest.split_once('|').ok_or_else(|| { + VeilidAPIError::parse_error( + "DialInfo::from_str missing socket address '|' separator", + s, + ) + })?; + + let address = Address::from_str(a)?; + DialInfo::try_wss( + SocketAddress::new(address, url_port), + format!("wss://{}", rest), + ) + } + _ => Err(VeilidAPIError::parse_error( + "DialInfo::from_str has invalid scheme", + s, + )), + } + } +} + +impl DialInfo { + pub fn udp_from_socketaddr(socket_addr: SocketAddr) -> Self { + Self::UDP(DialInfoUDP { + socket_address: SocketAddress::from_socket_addr(socket_addr).to_canonical(), + }) + } + pub fn tcp_from_socketaddr(socket_addr: SocketAddr) -> Self { + Self::TCP(DialInfoTCP { + socket_address: SocketAddress::from_socket_addr(socket_addr).to_canonical(), + }) + } + pub fn udp(socket_address: SocketAddress) -> Self { + Self::UDP(DialInfoUDP { + socket_address: socket_address.to_canonical(), + }) + } + pub fn tcp(socket_address: SocketAddress) -> Self { + Self::TCP(DialInfoTCP { + socket_address: socket_address.to_canonical(), + }) + } + pub fn try_ws(socket_address: SocketAddress, url: String) -> Result { + let split_url = SplitUrl::from_str(&url).map_err(|e| { + VeilidAPIError::parse_error(format!("unable to split WS url: {}", e), &url) + })?; + if split_url.scheme != "ws" || !url.starts_with("ws://") { + apibail_parse_error!("incorrect scheme for WS dialinfo", url); + } + let url_port = split_url.port.unwrap_or(80u16); + if url_port != socket_address.port() { + apibail_parse_error!("socket address port doesn't match url port", url); + } + if let SplitUrlHost::IpAddr(a) = split_url.host { + if socket_address.to_ip_addr() != a { + apibail_parse_error!( + format!("request address does not match socket address: {}", a), + socket_address + ); + } + } + Ok(Self::WS(DialInfoWS { + socket_address: socket_address.to_canonical(), + request: url[5..].to_string(), + })) + } + pub fn try_wss(socket_address: SocketAddress, url: String) -> Result { + let split_url = SplitUrl::from_str(&url).map_err(|e| { + VeilidAPIError::parse_error(format!("unable to split WSS url: {}", e), &url) + })?; + if split_url.scheme != "wss" || !url.starts_with("wss://") { + apibail_parse_error!("incorrect scheme for WSS dialinfo", url); + } + let url_port = split_url.port.unwrap_or(443u16); + if url_port != socket_address.port() { + apibail_parse_error!("socket address port doesn't match url port", url); + } + if !matches!(split_url.host, SplitUrlHost::Hostname(_)) { + apibail_parse_error!( + "WSS url can not use address format, only hostname format", + url + ); + } + Ok(Self::WSS(DialInfoWSS { + socket_address: socket_address.to_canonical(), + request: url[6..].to_string(), + })) + } + pub fn protocol_type(&self) -> ProtocolType { + match self { + Self::UDP(_) => ProtocolType::UDP, + Self::TCP(_) => ProtocolType::TCP, + Self::WS(_) => ProtocolType::WS, + Self::WSS(_) => ProtocolType::WSS, + } + } + pub fn address_type(&self) -> AddressType { + self.socket_address().address_type() + } + pub fn address(&self) -> Address { + match self { + Self::UDP(di) => di.socket_address.address, + Self::TCP(di) => di.socket_address.address, + Self::WS(di) => di.socket_address.address, + Self::WSS(di) => di.socket_address.address, + } + } + pub fn socket_address(&self) -> SocketAddress { + match self { + Self::UDP(di) => di.socket_address, + Self::TCP(di) => di.socket_address, + Self::WS(di) => di.socket_address, + Self::WSS(di) => di.socket_address, + } + } + pub fn to_ip_addr(&self) -> IpAddr { + match self { + Self::UDP(di) => di.socket_address.to_ip_addr(), + Self::TCP(di) => di.socket_address.to_ip_addr(), + Self::WS(di) => di.socket_address.to_ip_addr(), + Self::WSS(di) => di.socket_address.to_ip_addr(), + } + } + pub fn port(&self) -> u16 { + match self { + Self::UDP(di) => di.socket_address.port, + Self::TCP(di) => di.socket_address.port, + Self::WS(di) => di.socket_address.port, + Self::WSS(di) => di.socket_address.port, + } + } + pub fn set_port(&mut self, port: u16) { + match self { + Self::UDP(di) => di.socket_address.port = port, + Self::TCP(di) => di.socket_address.port = port, + Self::WS(di) => di.socket_address.port = port, + Self::WSS(di) => di.socket_address.port = port, + } + } + pub fn to_socket_addr(&self) -> SocketAddr { + match self { + Self::UDP(di) => di.socket_address.to_socket_addr(), + Self::TCP(di) => di.socket_address.to_socket_addr(), + Self::WS(di) => di.socket_address.to_socket_addr(), + Self::WSS(di) => di.socket_address.to_socket_addr(), + } + } + pub fn to_peer_address(&self) -> PeerAddress { + match self { + Self::UDP(di) => PeerAddress::new(di.socket_address, ProtocolType::UDP), + Self::TCP(di) => PeerAddress::new(di.socket_address, ProtocolType::TCP), + Self::WS(di) => PeerAddress::new(di.socket_address, ProtocolType::WS), + Self::WSS(di) => PeerAddress::new(di.socket_address, ProtocolType::WSS), + } + } + pub fn request(&self) -> Option { + match self { + Self::UDP(_) => None, + Self::TCP(_) => None, + Self::WS(di) => Some(format!("ws://{}", di.request)), + Self::WSS(di) => Some(format!("wss://{}", di.request)), + } + } + pub fn is_valid(&self) -> bool { + let socket_address = self.socket_address(); + let address = socket_address.address(); + let port = socket_address.port(); + (address.is_global() || address.is_local()) && port > 0 + } + + pub fn make_filter(&self) -> DialInfoFilter { + DialInfoFilter { + protocol_type_set: ProtocolTypeSet::only(self.protocol_type()), + address_type_set: AddressTypeSet::only(self.address_type()), + } + } + + pub fn try_vec_from_short, H: AsRef>( + short: S, + hostname: H, + ) -> Result, VeilidAPIError> { + let short = short.as_ref(); + let hostname = hostname.as_ref(); + + if short.len() < 2 { + apibail_parse_error!("invalid short url length", short); + } + let url = match &short[0..1] { + "U" => { + format!("udp://{}:{}", hostname, &short[1..]) + } + "T" => { + format!("tcp://{}:{}", hostname, &short[1..]) + } + "W" => { + format!("ws://{}:{}", hostname, &short[1..]) + } + "S" => { + format!("wss://{}:{}", hostname, &short[1..]) + } + _ => { + apibail_parse_error!("invalid short url type", short); + } + }; + Self::try_vec_from_url(url) + } + + pub fn try_vec_from_url>(url: S) -> Result, VeilidAPIError> { + let url = url.as_ref(); + let split_url = SplitUrl::from_str(url) + .map_err(|e| VeilidAPIError::parse_error(format!("unable to split url: {}", e), url))?; + + let port = match split_url.scheme.as_str() { + "udp" | "tcp" => split_url + .port + .ok_or_else(|| VeilidAPIError::parse_error("Missing port in udp url", url))?, + "ws" => split_url.port.unwrap_or(80u16), + "wss" => split_url.port.unwrap_or(443u16), + _ => { + apibail_parse_error!("Invalid dial info url scheme", split_url.scheme); + } + }; + + let socket_addrs = { + // Resolve if possible, WASM doesn't support resolution and doesn't need it to connect to the dialinfo + // This will not be used on signed dialinfo, only for bootstrapping, so we don't need to worry about + // the '0.0.0.0' address being propagated across the routing table + cfg_if::cfg_if! { + if #[cfg(target_arch = "wasm32")] { + vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0,0,0,0)), port)] + } else { + match split_url.host { + SplitUrlHost::Hostname(_) => split_url + .host_port(port) + .to_socket_addrs() + .map_err(|_| VeilidAPIError::parse_error("couldn't resolve hostname in url", url))? + .collect(), + SplitUrlHost::IpAddr(a) => vec![SocketAddr::new(a, port)], + } + } + } + }; + + let mut out = Vec::new(); + for sa in socket_addrs { + out.push(match split_url.scheme.as_str() { + "udp" => Self::udp_from_socketaddr(sa), + "tcp" => Self::tcp_from_socketaddr(sa), + "ws" => Self::try_ws( + SocketAddress::from_socket_addr(sa).to_canonical(), + url.to_string(), + )?, + "wss" => Self::try_wss( + SocketAddress::from_socket_addr(sa).to_canonical(), + url.to_string(), + )?, + _ => { + unreachable!("Invalid dial info url scheme") + } + }); + } + Ok(out) + } + + pub async fn to_short(&self) -> (String, String) { + match self { + DialInfo::UDP(di) => ( + format!("U{}", di.socket_address.port()), + intf::ptr_lookup(di.socket_address.to_ip_addr()) + .await + .unwrap_or_else(|_| di.socket_address.to_string()), + ), + DialInfo::TCP(di) => ( + format!("T{}", di.socket_address.port()), + intf::ptr_lookup(di.socket_address.to_ip_addr()) + .await + .unwrap_or_else(|_| di.socket_address.to_string()), + ), + DialInfo::WS(di) => { + let mut split_url = SplitUrl::from_str(&format!("ws://{}", di.request)).unwrap(); + if let SplitUrlHost::IpAddr(a) = split_url.host { + if let Ok(host) = intf::ptr_lookup(a).await { + split_url.host = SplitUrlHost::Hostname(host); + } + } + ( + format!( + "W{}{}", + split_url.port.unwrap_or(80), + split_url + .path + .map(|p| format!("/{}", p)) + .unwrap_or_default() + ), + split_url.host.to_string(), + ) + } + DialInfo::WSS(di) => { + let mut split_url = SplitUrl::from_str(&format!("wss://{}", di.request)).unwrap(); + if let SplitUrlHost::IpAddr(a) = split_url.host { + if let Ok(host) = intf::ptr_lookup(a).await { + split_url.host = SplitUrlHost::Hostname(host); + } + } + ( + format!( + "S{}{}", + split_url.port.unwrap_or(443), + split_url + .path + .map(|p| format!("/{}", p)) + .unwrap_or_default() + ), + split_url.host.to_string(), + ) + } + } + } + pub async fn to_url(&self) -> String { + match self { + DialInfo::UDP(di) => intf::ptr_lookup(di.socket_address.to_ip_addr()) + .await + .map(|h| format!("udp://{}:{}", h, di.socket_address.port())) + .unwrap_or_else(|_| format!("udp://{}", di.socket_address)), + DialInfo::TCP(di) => intf::ptr_lookup(di.socket_address.to_ip_addr()) + .await + .map(|h| format!("tcp://{}:{}", h, di.socket_address.port())) + .unwrap_or_else(|_| format!("tcp://{}", di.socket_address)), + DialInfo::WS(di) => { + let mut split_url = SplitUrl::from_str(&format!("ws://{}", di.request)).unwrap(); + if let SplitUrlHost::IpAddr(a) = split_url.host { + if let Ok(host) = intf::ptr_lookup(a).await { + split_url.host = SplitUrlHost::Hostname(host); + } + } + split_url.to_string() + } + DialInfo::WSS(di) => { + let mut split_url = SplitUrl::from_str(&format!("wss://{}", di.request)).unwrap(); + if let SplitUrlHost::IpAddr(a) = split_url.host { + if let Ok(host) = intf::ptr_lookup(a).await { + split_url.host = SplitUrlHost::Hostname(host); + } + } + split_url.to_string() + } + } + } + + pub fn ordered_sequencing_sort(a: &DialInfo, b: &DialInfo) -> core::cmp::Ordering { + let ca = a.protocol_type().sort_order(Sequencing::EnsureOrdered); + let cb = b.protocol_type().sort_order(Sequencing::EnsureOrdered); + if ca < cb { + return core::cmp::Ordering::Less; + } + if ca > cb { + return core::cmp::Ordering::Greater; + } + match (a, b) { + (DialInfo::UDP(a), DialInfo::UDP(b)) => a.cmp(b), + (DialInfo::TCP(a), DialInfo::TCP(b)) => a.cmp(b), + (DialInfo::WS(a), DialInfo::WS(b)) => a.cmp(b), + (DialInfo::WSS(a), DialInfo::WSS(b)) => a.cmp(b), + _ => unreachable!(), + } + } +} + +impl MatchesDialInfoFilter for DialInfo { + fn matches_filter(&self, filter: &DialInfoFilter) -> bool { + if !filter.protocol_type_set.contains(self.protocol_type()) { + return false; + } + if !filter.address_type_set.contains(self.address_type()) { + return false; + } + true + } +} + +////////////////////////////////////////////////////////////////////////// + +// Signed NodeInfo that can be passed around amongst peers and verifiable +#[derive(Clone, Debug, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct SignedDirectNodeInfo { + pub node_info: NodeInfo, + pub timestamp: u64, + pub signature: Option, +} + +impl SignedDirectNodeInfo { + pub fn new( + node_id: NodeId, + node_info: NodeInfo, + timestamp: u64, + signature: DHTSignature, + ) -> Result { + let node_info_bytes = Self::make_signature_bytes(&node_info, timestamp)?; + verify(&node_id.key, &node_info_bytes, &signature)?; + Ok(Self { + node_info, + timestamp, + signature: Some(signature), + }) + } + + pub fn with_secret( + node_id: NodeId, + node_info: NodeInfo, + secret: &DHTKeySecret, + ) -> Result { + let timestamp = intf::get_timestamp(); + let node_info_bytes = Self::make_signature_bytes(&node_info, timestamp)?; + let signature = sign(&node_id.key, secret, &node_info_bytes)?; + Ok(Self { + node_info, + timestamp, + signature: Some(signature), + }) + } + + fn make_signature_bytes( + node_info: &NodeInfo, + timestamp: u64, + ) -> Result, VeilidAPIError> { + let mut node_info_bytes = Vec::new(); + + // Add nodeinfo to signature + let mut ni_msg = ::capnp::message::Builder::new_default(); + let mut ni_builder = ni_msg.init_root::(); + encode_node_info(node_info, &mut ni_builder).map_err(VeilidAPIError::internal)?; + node_info_bytes.append(&mut builder_to_vec(ni_msg).map_err(VeilidAPIError::internal)?); + + // Add timestamp to signature + node_info_bytes.append(&mut timestamp.to_le_bytes().to_vec()); + + Ok(node_info_bytes) + } + + pub fn with_no_signature(node_info: NodeInfo) -> Self { + Self { + node_info, + signature: None, + timestamp: intf::get_timestamp(), + } + } + + pub fn has_valid_signature(&self) -> bool { + self.signature.is_some() + } +} + +/// Signed NodeInfo with a relay that can be passed around amongst peers and verifiable +#[derive(Clone, Debug, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct SignedRelayedNodeInfo { + pub node_info: NodeInfo, + pub relay_id: NodeId, + pub relay_info: SignedDirectNodeInfo, + pub timestamp: u64, + pub signature: DHTSignature, +} + +impl SignedRelayedNodeInfo { + pub fn new( + node_id: NodeId, + node_info: NodeInfo, + relay_id: NodeId, + relay_info: SignedDirectNodeInfo, + timestamp: u64, + signature: DHTSignature, + ) -> Result { + let node_info_bytes = + Self::make_signature_bytes(&node_info, &relay_id, &relay_info, timestamp)?; + verify(&node_id.key, &node_info_bytes, &signature)?; + Ok(Self { + node_info, + relay_id, + relay_info, + signature, + timestamp, + }) + } + + pub fn with_secret( + node_id: NodeId, + node_info: NodeInfo, + relay_id: NodeId, + relay_info: SignedDirectNodeInfo, + secret: &DHTKeySecret, + ) -> Result { + let timestamp = intf::get_timestamp(); + let node_info_bytes = + Self::make_signature_bytes(&node_info, &relay_id, &relay_info, timestamp)?; + let signature = sign(&node_id.key, secret, &node_info_bytes)?; + Ok(Self { + node_info, + relay_id, + relay_info, + signature, + timestamp, + }) + } + + fn make_signature_bytes( + node_info: &NodeInfo, + relay_id: &NodeId, + relay_info: &SignedDirectNodeInfo, + timestamp: u64, + ) -> Result, VeilidAPIError> { + let mut sig_bytes = Vec::new(); + + // Add nodeinfo to signature + let mut ni_msg = ::capnp::message::Builder::new_default(); + let mut ni_builder = ni_msg.init_root::(); + encode_node_info(node_info, &mut ni_builder).map_err(VeilidAPIError::internal)?; + sig_bytes.append(&mut builder_to_vec(ni_msg).map_err(VeilidAPIError::internal)?); + + // Add relay id to signature + let mut rid_msg = ::capnp::message::Builder::new_default(); + let mut rid_builder = rid_msg.init_root::(); + encode_dht_key(&relay_id.key, &mut rid_builder).map_err(VeilidAPIError::internal)?; + sig_bytes.append(&mut builder_to_vec(rid_msg).map_err(VeilidAPIError::internal)?); + + // Add relay info to signature + let mut ri_msg = ::capnp::message::Builder::new_default(); + let mut ri_builder = ri_msg.init_root::(); + encode_signed_direct_node_info(relay_info, &mut ri_builder) + .map_err(VeilidAPIError::internal)?; + sig_bytes.append(&mut builder_to_vec(ri_msg).map_err(VeilidAPIError::internal)?); + + // Add timestamp to signature + sig_bytes.append(&mut timestamp.to_le_bytes().to_vec()); + + Ok(sig_bytes) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] +#[archive_attr(repr(u8), derive(CheckBytes))] +pub enum SignedNodeInfo { + Direct(SignedDirectNodeInfo), + Relayed(SignedRelayedNodeInfo), +} + +impl SignedNodeInfo { + pub fn has_valid_signature(&self) -> bool { + match self { + SignedNodeInfo::Direct(d) => d.has_valid_signature(), + SignedNodeInfo::Relayed(_) => true, + } + } + + pub fn timestamp(&self) -> u64 { + match self { + SignedNodeInfo::Direct(d) => d.timestamp, + SignedNodeInfo::Relayed(r) => r.timestamp, + } + } + + pub fn node_info(&self) -> &NodeInfo { + match self { + SignedNodeInfo::Direct(d) => &d.node_info, + SignedNodeInfo::Relayed(r) => &r.node_info, + } + } + pub fn relay_id(&self) -> Option { + match self { + SignedNodeInfo::Direct(_) => None, + SignedNodeInfo::Relayed(r) => Some(r.relay_id.clone()), + } + } + pub fn relay_info(&self) -> Option<&NodeInfo> { + match self { + SignedNodeInfo::Direct(_) => None, + SignedNodeInfo::Relayed(r) => Some(&r.relay_info.node_info), + } + } + pub fn relay_peer_info(&self) -> Option { + match self { + SignedNodeInfo::Direct(_) => None, + SignedNodeInfo::Relayed(r) => Some(PeerInfo::new( + r.relay_id.clone(), + SignedNodeInfo::Direct(r.relay_info.clone()), + )), + } + } + pub fn has_any_dial_info(&self) -> bool { + self.node_info().has_dial_info() + || self + .relay_info() + .map(|relay_ni| relay_ni.has_dial_info()) + .unwrap_or_default() + } + + pub fn has_sequencing_matched_dial_info(&self, sequencing: Sequencing) -> bool { + // Check our dial info + for did in &self.node_info().dial_info_detail_list { + match sequencing { + Sequencing::NoPreference | Sequencing::PreferOrdered => return true, + Sequencing::EnsureOrdered => { + if did.dial_info.protocol_type().is_connection_oriented() { + return true; + } + } + } + } + // Check our relay if we have one + return self + .relay_info() + .map(|relay_ni| { + for did in &relay_ni.dial_info_detail_list { + match sequencing { + Sequencing::NoPreference | Sequencing::PreferOrdered => return true, + Sequencing::EnsureOrdered => { + if did.dial_info.protocol_type().is_connection_oriented() { + return true; + } + } + } + } + false + }) + .unwrap_or_default(); + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct PeerInfo { + pub node_id: NodeId, + pub signed_node_info: SignedNodeInfo, +} + +impl PeerInfo { + pub fn new(node_id: NodeId, signed_node_info: SignedNodeInfo) -> Self { + Self { + node_id, + signed_node_info, + } + } +} + +#[derive( + Copy, + Clone, + Debug, + PartialEq, + PartialOrd, + Eq, + Ord, + Hash, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct PeerAddress { + protocol_type: ProtocolType, + #[serde(with = "json_as_string")] + socket_address: SocketAddress, +} + +impl PeerAddress { + pub fn new(socket_address: SocketAddress, protocol_type: ProtocolType) -> Self { + Self { + socket_address: socket_address.to_canonical(), + protocol_type, + } + } + + pub fn socket_address(&self) -> &SocketAddress { + &self.socket_address + } + + pub fn protocol_type(&self) -> ProtocolType { + self.protocol_type + } + + pub fn to_socket_addr(&self) -> SocketAddr { + self.socket_address.to_socket_addr() + } + + pub fn address_type(&self) -> AddressType { + self.socket_address.address_type() + } +} + +/// Represents the 5-tuple of an established connection +/// Not used to specify connections to create, that is reserved for DialInfo +/// +/// ConnectionDescriptors should never be from unspecified local addresses for connection oriented protocols +/// If the medium does not allow local addresses, None should have been used or 'new_no_local' +/// If we are specifying only a port, then the socket's 'local_address()' should have been used, since an +/// established connection is always from a real address to another real address. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct ConnectionDescriptor { + remote: PeerAddress, + local: Option, +} + +impl ConnectionDescriptor { + pub fn new(remote: PeerAddress, local: SocketAddress) -> Self { + assert!( + !remote.protocol_type().is_connection_oriented() || !local.address().is_unspecified() + ); + + Self { + remote, + local: Some(local), + } + } + pub fn new_no_local(remote: PeerAddress) -> Self { + Self { + remote, + local: None, + } + } + pub fn remote(&self) -> PeerAddress { + self.remote + } + pub fn remote_address(&self) -> &SocketAddress { + self.remote.socket_address() + } + pub fn local(&self) -> Option { + self.local + } + pub fn protocol_type(&self) -> ProtocolType { + self.remote.protocol_type + } + pub fn address_type(&self) -> AddressType { + self.remote.address_type() + } + pub fn make_dial_info_filter(&self) -> DialInfoFilter { + DialInfoFilter::all() + .with_protocol_type(self.protocol_type()) + .with_address_type(self.address_type()) + } +} + +impl MatchesDialInfoFilter for ConnectionDescriptor { + fn matches_filter(&self, filter: &DialInfoFilter) -> bool { + if !filter.protocol_type_set.contains(self.protocol_type()) { + return false; + } + if !filter.address_type_set.contains(self.address_type()) { + return false; + } + true + } +} + +////////////////////////////////////////////////////////////////////////// + +#[derive( + Clone, + Debug, + Default, + PartialEq, + Eq, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +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, + PartialEq, + Eq, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct TransferStats { + #[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, + PartialEq, + Eq, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct TransferStatsDownUp { + pub down: TransferStats, + pub up: TransferStats, +} + +#[derive( + Clone, + Debug, + Default, + PartialEq, + Eq, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +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, + PartialEq, + Eq, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct PeerStats { + #[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 +} + +pub type ValueChangeCallback = + Arc) -> SendPinBoxFuture<()> + Send + Sync + 'static>; + +///////////////////////////////////////////////////////////////////////////////////////////////////// + +#[derive(Clone, Debug, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] +#[archive_attr(repr(u8), derive(CheckBytes))] +pub enum SignalInfo { + HolePunch { + // UDP Hole Punch Request + receipt: Vec, // Receipt to be returned after the hole punch + peer_info: PeerInfo, // Sender's peer info + }, + ReverseConnect { + // Reverse Connection Request + receipt: Vec, // Receipt to be returned by the reverse connection + peer_info: PeerInfo, // Sender's peer info + }, + // XXX: WebRTC +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// +#[derive( + Copy, + Clone, + Debug, + PartialOrd, + PartialEq, + Eq, + Ord, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(u8), derive(CheckBytes))] +pub enum TunnelMode { + Raw, + Turn, +} + +#[derive( + Copy, + Clone, + Debug, + PartialOrd, + PartialEq, + Eq, + Ord, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[archive_attr(repr(u8), derive(CheckBytes))] +pub enum TunnelError { + BadId, // Tunnel ID was rejected + NoEndpoint, // Endpoint was unreachable + RejectedMode, // Endpoint couldn't provide mode + NoCapacity, // Endpoint is full +} + +pub type TunnelId = u64; + +#[derive(Clone, Debug, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct TunnelEndpoint { + pub mode: TunnelMode, + pub description: String, // XXX: TODO +} + +impl Default for TunnelEndpoint { + fn default() -> Self { + Self { + mode: TunnelMode::Raw, + description: "".to_string(), + } + } +} + +#[derive( + Clone, Debug, Default, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct FullTunnel { + pub id: TunnelId, + pub timeout: u64, + pub local: TunnelEndpoint, + pub remote: TunnelEndpoint, +} + +#[derive( + Clone, Debug, Default, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize, +)] +#[archive_attr(repr(C), derive(CheckBytes))] +pub struct PartialTunnel { + pub id: TunnelId, + pub timeout: u64, + pub local: TunnelEndpoint, +} From b1bdf76ae800d43d8aede917284e07c4e843b3ec Mon Sep 17 00:00:00 2001 From: John Smith Date: Sat, 26 Nov 2022 21:37:23 -0500 Subject: [PATCH 08/88] refactor --- .gitmodules | 3 - Cargo.toml | 1 + doc/config/sample.config | 1 - doc/config/veilid-server-config.md | 1 - veilid-cli/src/client_api_connection.rs | 4 +- veilid-cli/src/command_processor.rs | 2 +- veilid-cli/src/main.rs | 1 - veilid-cli/src/tools.rs | 14 +- veilid-core/Cargo.toml | 5 +- veilid-core/src/attachment_manager.rs | 8 +- veilid-core/src/crypto/key.rs | 1 - veilid-core/src/crypto/mod.rs | 6 +- veilid-core/src/intf/native/system.rs | 191 ------------------ .../utils/network_interfaces/netlink.rs | 2 +- veilid-core/src/intf/wasm/system.rs | 153 +------------- veilid-core/src/intf/wasm/utils/mod.rs | 53 ----- veilid-core/src/lib.rs | 5 +- .../src/network_manager/connection_limits.rs | 4 +- .../src/network_manager/connection_manager.rs | 2 +- veilid-core/src/network_manager/mod.rs | 33 ++- .../src/network_manager/native/igd_manager.rs | 14 +- veilid-core/src/network_manager/native/mod.rs | 41 +--- .../network_manager/native/natpmp_manager.rs | 18 -- .../native/network_class_discovery.rs | 6 +- .../src/network_manager/native/network_tcp.rs | 2 +- .../src/network_manager/native/network_udp.rs | 2 +- .../native/protocol/sockets.rs | 2 +- .../src/network_manager/network_connection.rs | 16 +- veilid-core/src/receipt_manager.rs | 2 +- veilid-core/src/routing_table/bucket.rs | 2 +- veilid-core/src/routing_table/bucket_entry.rs | 6 +- veilid-core/src/routing_table/debug.rs | 4 +- veilid-core/src/routing_table/node_ref.rs | 2 +- .../src/routing_table/route_spec_store.rs | 18 +- .../src/routing_table/routing_table_inner.rs | 14 +- .../coders/operations/operation.rs | 4 +- veilid-core/src/rpc_processor/mod.rs | 19 +- .../src/rpc_processor/operation_waiter.rs | 10 +- .../src/tests/common/test_async_tag_lock.rs | 25 ++- .../src/tests/common/test_host_interface.rs | 94 ++++----- .../src/tests/common/test_veilid_config.rs | 2 - .../src/tests/common/test_veilid_core.rs | 6 +- veilid-core/src/veilid_api/api.rs | 2 +- veilid-core/src/veilid_api/types.rs | 6 +- veilid-core/src/veilid_config.rs | 2 - veilid-core/src/veilid_rng.rs | 28 --- veilid-flutter/example/lib/config.dart | 1 - veilid-flutter/lib/veilid.dart | 4 - veilid-server/src/settings.rs | 5 - veilid-tools/Cargo.toml | 179 ++++++++++++++++ veilid-tools/ios_build.sh | 44 ++++ .../src}/async_peek_stream.rs | 1 + .../xx => veilid-tools/src}/async_tag_lock.rs | 1 + .../src/xx => veilid-tools/src}/bump_port.rs | 1 + .../xx => veilid-tools/src}/clone_stream.rs | 3 +- .../src/xx => veilid-tools/src}/eventual.rs | 1 + .../xx => veilid-tools/src}/eventual_base.rs | 0 .../xx => veilid-tools/src}/eventual_value.rs | 1 + .../src}/eventual_value_clone.rs | 1 + veilid-tools/src/interval.rs | 49 +++++ .../xx => veilid-tools/src}/ip_addr_port.rs | 1 + .../src/xx => veilid-tools/src}/ip_extra.rs | 3 +- .../src/xx => veilid-tools/src}/log_thru.rs | 2 +- .../src/xx => veilid-tools/src}/mod.rs | 27 ++- .../src}/must_join_handle.rs | 3 +- .../src}/must_join_single_future.rs | 6 +- .../xx => veilid-tools/src}/mutable_future.rs | 0 .../xx => veilid-tools/src}/network_result.rs | 1 + veilid-tools/src/random.rs | 81 ++++++++ .../src}/single_shot_eventual.rs | 0 veilid-tools/src/sleep.rs | 34 ++++ veilid-tools/src/spawn.rs | 119 +++++++++++ .../src/xx => veilid-tools/src}/split_url.rs | 5 +- .../src/xx => veilid-tools/src}/tick_task.rs | 4 +- veilid-tools/src/timeout.rs | 32 +++ .../src/xx => veilid-tools/src}/timeout_or.rs | 2 +- veilid-tools/src/timestamp.rs | 25 +++ .../src/xx => veilid-tools/src}/tools.rs | 39 +++- veilid-tools/src/wasm.rs | 52 +++++ veilid-wasm/tests/web.rs | 1 - 80 files changed, 865 insertions(+), 700 deletions(-) delete mode 100644 veilid-core/src/network_manager/native/natpmp_manager.rs delete mode 100644 veilid-core/src/veilid_rng.rs create mode 100644 veilid-tools/Cargo.toml create mode 100755 veilid-tools/ios_build.sh rename {veilid-core/src/xx => veilid-tools/src}/async_peek_stream.rs (99%) rename {veilid-core/src/xx => veilid-tools/src}/async_tag_lock.rs (99%) rename {veilid-core/src/xx => veilid-tools/src}/bump_port.rs (99%) rename {veilid-core/src/xx => veilid-tools/src}/clone_stream.rs (99%) rename {veilid-core/src/xx => veilid-tools/src}/eventual.rs (99%) rename {veilid-core/src/xx => veilid-tools/src}/eventual_base.rs (100%) rename {veilid-core/src/xx => veilid-tools/src}/eventual_value.rs (99%) rename {veilid-core/src/xx => veilid-tools/src}/eventual_value_clone.rs (99%) create mode 100644 veilid-tools/src/interval.rs rename {veilid-core/src/xx => veilid-tools/src}/ip_addr_port.rs (99%) rename {veilid-core/src/xx => veilid-tools/src}/ip_extra.rs (99%) rename {veilid-core/src/xx => veilid-tools/src}/log_thru.rs (99%) rename {veilid-core/src/xx => veilid-tools/src}/mod.rs (90%) rename {veilid-core/src/xx => veilid-tools/src}/must_join_handle.rs (98%) rename {veilid-core/src/xx => veilid-tools/src}/must_join_single_future.rs (97%) rename {veilid-core/src/xx => veilid-tools/src}/mutable_future.rs (100%) rename {veilid-core/src/xx => veilid-tools/src}/network_result.rs (99%) create mode 100644 veilid-tools/src/random.rs rename {veilid-core/src/xx => veilid-tools/src}/single_shot_eventual.rs (100%) create mode 100644 veilid-tools/src/sleep.rs create mode 100644 veilid-tools/src/spawn.rs rename {veilid-core/src/xx => veilid-tools/src}/split_url.rs (99%) rename {veilid-core/src/xx => veilid-tools/src}/tick_task.rs (99%) create mode 100644 veilid-tools/src/timeout.rs rename {veilid-core/src/xx => veilid-tools/src}/timeout_or.rs (99%) create mode 100644 veilid-tools/src/timestamp.rs rename {veilid-core/src/xx => veilid-tools/src}/tools.rs (86%) create mode 100644 veilid-tools/src/wasm.rs diff --git a/.gitmodules b/.gitmodules index a5c2ae1b..3cdd9159 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,9 +16,6 @@ [submodule "external/netlink"] path = external/netlink url = ../netlink.git -[submodule "external/no-std-net"] - path = external/no-std-net - url = ../no-std-net.git [submodule "external/libmdns"] path = external/libmdns url = ../libmdns.git diff --git a/Cargo.toml b/Cargo.toml index 3ca2a512..6ea5de42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ + "veilid-tools", "veilid-core", "veilid-server", "veilid-cli", diff --git a/doc/config/sample.config b/doc/config/sample.config index c7e65f0f..8c939d13 100644 --- a/doc/config/sample.config +++ b/doc/config/sample.config @@ -81,7 +81,6 @@ core: min_peer_refresh_time_ms: 2000 validate_dial_info_receipt_time_ms: 2000 upnp: true - natpmp: false detect_address_changes: true enable_local_peer_scope: false restricted_nat_retries: 0 diff --git a/doc/config/veilid-server-config.md b/doc/config/veilid-server-config.md index 83203e3b..8fbe74fe 100644 --- a/doc/config/veilid-server-config.md +++ b/doc/config/veilid-server-config.md @@ -193,7 +193,6 @@ network: bootstrap: ['bootstrap.dev.veilid.net'] bootstrap_nodes: [] upnp: true - natpmp: false detect_address_changes: true enable_local_peer_scope: false restricted_nat_retries: 0 diff --git a/veilid-cli/src/client_api_connection.rs b/veilid-cli/src/client_api_connection.rs index 834612e2..9715a450 100644 --- a/veilid-cli/src/client_api_connection.rs +++ b/veilid-cli/src/client_api_connection.rs @@ -229,8 +229,8 @@ impl ClientApiConnection { // Wait until rpc system completion or disconnect was requested let res = rpc_jh.await; - #[cfg(feature = "rt-tokio")] - let res = res.map_err(|e| format!("join error: {}", e))?; + // #[cfg(feature = "rt-tokio")] + // let res = res.map_err(|e| format!("join error: {}", e))?; res.map_err(|e| format!("client RPC system error: {}", e)) } diff --git a/veilid-cli/src/command_processor.rs b/veilid-cli/src/command_processor.rs index da9d9cb6..dcf4d719 100644 --- a/veilid-cli/src/command_processor.rs +++ b/veilid-cli/src/command_processor.rs @@ -7,7 +7,7 @@ use std::cell::*; use std::net::SocketAddr; use std::rc::Rc; use std::time::{Duration, SystemTime}; -use veilid_core::xx::{Eventual, EventualCommon}; +use veilid_core::xx::*; use veilid_core::*; pub fn convert_loglevel(s: &str) -> Result { diff --git a/veilid-cli/src/main.rs b/veilid-cli/src/main.rs index 3e2b2b2a..0ecd7b37 100644 --- a/veilid-cli/src/main.rs +++ b/veilid-cli/src/main.rs @@ -8,7 +8,6 @@ use flexi_logger::*; use std::ffi::OsStr; use std::net::ToSocketAddrs; use std::path::Path; -use tools::*; mod client_api_connection; mod command_processor; diff --git a/veilid-cli/src/tools.rs b/veilid-cli/src/tools.rs index 3ec094d2..6202d490 100644 --- a/veilid-cli/src/tools.rs +++ b/veilid-cli/src/tools.rs @@ -6,12 +6,7 @@ cfg_if! { pub use async_std::task::JoinHandle; pub use async_std::net::TcpStream; pub use async_std::future::TimeoutError; - pub fn spawn_local + 'static, T: 'static>(f: F) -> JoinHandle { - async_std::task::spawn_local(f) - } - pub fn spawn_detached_local + 'static, T: 'static>(f: F) { - let _ = async_std::task::spawn_local(f); - } + pub use async_std::task::sleep; pub use async_std::future::timeout; pub fn block_on, T>(f: F) -> T { @@ -21,12 +16,7 @@ cfg_if! { pub use tokio::task::JoinHandle; pub use tokio::net::TcpStream; pub use tokio::time::error::Elapsed as TimeoutError; - pub fn spawn_local + 'static, T: 'static>(f: F) -> JoinHandle { - tokio::task::spawn_local(f) - } - pub fn spawn_detached_local + 'static, T: 'static>(f: F) { - let _ = tokio::task::spawn_local(f); - } + pub use tokio::time::sleep; pub use tokio::time::timeout; pub fn block_on, T>(f: F) -> T { diff --git a/veilid-core/Cargo.toml b/veilid-core/Cargo.toml index 3e0ebf31..ef4ea26b 100644 --- a/veilid-core/Cargo.toml +++ b/veilid-core/Cargo.toml @@ -11,14 +11,15 @@ crate-type = ["cdylib", "staticlib", "rlib"] [features] default = [] -rt-async-std = [ "async-std", "async-std-resolver", "async_executors/async_std", "rtnetlink?/smol_socket" ] -rt-tokio = [ "tokio", "tokio-util", "tokio-stream", "trust-dns-resolver/tokio-runtime", "async_executors/tokio_tp", "async_executors/tokio_io", "async_executors/tokio_timer", "rtnetlink?/tokio_socket" ] +rt-async-std = [ "async-std", "async-std-resolver", "async_executors/async_std", "rtnetlink?/smol_socket", "veilid-tools/rt-async-std" ] +rt-tokio = [ "tokio", "tokio-util", "tokio-stream", "trust-dns-resolver/tokio-runtime", "async_executors/tokio_tp", "async_executors/tokio_io", "async_executors/tokio_timer", "rtnetlink?/tokio_socket", "veilid-tools/rt-tokio" ] android_tests = [] ios_tests = [ "simplelog" ] tracking = [] [dependencies] +veilid_tools = { path = "../veilid-tools", features = "tracing" } tracing = { version = "^0", features = ["log", "attributes"] } tracing-subscriber = "^0" tracing-error = "^0" diff --git a/veilid-core/src/attachment_manager.rs b/veilid-core/src/attachment_manager.rs index 48447e3f..c05cf628 100644 --- a/veilid-core/src/attachment_manager.rs +++ b/veilid-core/src/attachment_manager.rs @@ -254,7 +254,7 @@ impl AttachmentManager { #[instrument(level = "debug", skip(self))] async fn attachment_maintainer(self) { debug!("attachment starting"); - self.inner.lock().attach_timestamp = Some(intf::get_timestamp()); + self.inner.lock().attach_timestamp = Some(get_timestamp()); let netman = self.network_manager(); let mut restart; @@ -286,7 +286,7 @@ impl AttachmentManager { self.update_attachment().await; // sleep should be at the end in case maintain_peers changes state - intf::sleep(1000).await; + sleep(1000).await; } debug!("stopped maintaining peers"); @@ -299,7 +299,7 @@ impl AttachmentManager { debug!("completely restarting attachment"); // chill out for a second first, give network stack time to settle out - intf::sleep(1000).await; + sleep(1000).await; } trace!("stopping attachment"); @@ -348,7 +348,7 @@ impl AttachmentManager { return; } inner.maintain_peers = true; - inner.attachment_maintainer_jh = Some(intf::spawn(self.clone().attachment_maintainer())); + inner.attachment_maintainer_jh = Some(spawn(self.clone().attachment_maintainer())); } #[instrument(level = "trace", skip(self))] diff --git a/veilid-core/src/crypto/key.rs b/veilid-core/src/crypto/key.rs index c5468084..02a4ef72 100644 --- a/veilid-core/src/crypto/key.rs +++ b/veilid-core/src/crypto/key.rs @@ -1,4 +1,3 @@ -use crate::veilid_rng::*; use crate::xx::*; use crate::*; diff --git a/veilid-core/src/crypto/mod.rs b/veilid-core/src/crypto/mod.rs index ebede803..30b658e5 100644 --- a/veilid-core/src/crypto/mod.rs +++ b/veilid-core/src/crypto/mod.rs @@ -137,7 +137,7 @@ impl Crypto { // Schedule flushing let this = self.clone(); - let flush_future = intf::interval(60000, move || { + let flush_future = interval(60000, move || { let this = this.clone(); async move { if let Err(e) = this.flush().await { @@ -229,13 +229,13 @@ impl Crypto { pub fn get_random_nonce() -> Nonce { let mut nonce = [0u8; 24]; - intf::random_bytes(&mut nonce).unwrap(); + random_bytes(&mut nonce).unwrap(); nonce } pub fn get_random_secret() -> SharedSecret { let mut s = [0u8; 32]; - intf::random_bytes(&mut s).unwrap(); + random_bytes(&mut s).unwrap(); s } diff --git a/veilid-core/src/intf/native/system.rs b/veilid-core/src/intf/native/system.rs index 56154e47..1c2fa281 100644 --- a/veilid-core/src/intf/native/system.rs +++ b/veilid-core/src/intf/native/system.rs @@ -1,201 +1,10 @@ #![allow(dead_code)] - use crate::xx::*; -use rand::prelude::*; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -pub fn get_timestamp() -> u64 { - match SystemTime::now().duration_since(UNIX_EPOCH) { - Ok(n) => n.as_micros() as u64, - Err(_) => panic!("SystemTime before UNIX_EPOCH!"), - } -} - -// pub fn get_timestamp_string() -> String { -// let dt = chrono::Utc::now(); -// dt.time().format("%H:%M:%S.3f").to_string() -// } - -pub fn random_bytes(dest: &mut [u8]) -> EyreResult<()> { - let mut rng = rand::thread_rng(); - rng.try_fill_bytes(dest).wrap_err("failed to fill bytes") -} - -pub fn get_random_u32() -> u32 { - let mut rng = rand::thread_rng(); - rng.next_u32() -} - -pub fn get_random_u64() -> u64 { - let mut rng = rand::thread_rng(); - rng.next_u64() -} - -pub async fn sleep(millis: u32) { - if millis == 0 { - cfg_if! { - if #[cfg(feature="rt-async-std")] { - async_std::task::yield_now().await; - } else if #[cfg(feature="rt-tokio")] { - tokio::task::yield_now().await; - } - } - } else { - cfg_if! { - if #[cfg(feature="rt-async-std")] { - async_std::task::sleep(Duration::from_millis(u64::from(millis))).await; - } else if #[cfg(feature="rt-tokio")] { - tokio::time::sleep(Duration::from_millis(u64::from(millis))).await; - } - } - } -} - -pub fn system_boxed<'a, Out>( - future: impl Future + Send + 'a, -) -> SendPinBoxFutureLifetime<'a, Out> { - Box::pin(future) -} - -pub fn spawn(future: impl Future + Send + 'static) -> MustJoinHandle -where - Out: Send + 'static, -{ - cfg_if! { - if #[cfg(feature="rt-async-std")] { - MustJoinHandle::new(async_std::task::spawn(future)) - } else if #[cfg(feature="rt-tokio")] { - MustJoinHandle::new(tokio::task::spawn(future)) - } - } -} - -pub fn spawn_local(future: impl Future + 'static) -> MustJoinHandle -where - Out: 'static, -{ - cfg_if! { - if #[cfg(feature="rt-async-std")] { - MustJoinHandle::new(async_std::task::spawn_local(future)) - } else if #[cfg(feature="rt-tokio")] { - MustJoinHandle::new(tokio::task::spawn_local(future)) - } - } -} - -// pub fn spawn_with_local_set( -// future: impl Future + Send + 'static, -// ) -> MustJoinHandle -// where -// Out: Send + 'static, -// { -// cfg_if! { -// if #[cfg(feature="rt-async-std")] { -// spawn(future) -// } else if #[cfg(feature="rt-tokio")] { -// MustJoinHandle::new(tokio::task::spawn_blocking(move || { -// let rt = tokio::runtime::Handle::current(); -// rt.block_on(async { -// let local = tokio::task::LocalSet::new(); -// local.run_until(future).await -// }) -// })) -// } -// } -// } - -pub fn spawn_detached(future: impl Future + Send + 'static) -where - Out: Send + 'static, -{ - cfg_if! { - if #[cfg(feature="rt-async-std")] { - drop(async_std::task::spawn(future)); - } else if #[cfg(feature="rt-tokio")] { - drop(tokio::task::spawn(future)); - } - } -} - -pub fn interval(freq_ms: u32, callback: F) -> SendPinBoxFuture<()> -where - F: Fn() -> FUT + Send + Sync + 'static, - FUT: Future + Send, -{ - let e = Eventual::new(); - - let ie = e.clone(); - let jh = spawn(async move { - while timeout(freq_ms, ie.instance_clone(())).await.is_err() { - callback().await; - } - }); - - Box::pin(async move { - e.resolve().await; - jh.await; - }) -} - -pub async fn timeout(dur_ms: u32, f: F) -> Result -where - F: Future, -{ - cfg_if! { - if #[cfg(feature="rt-async-std")] { - async_std::future::timeout(Duration::from_millis(dur_ms as u64), f).await.map_err(|e| e.into()) - } else if #[cfg(feature="rt-tokio")] { - tokio::time::timeout(Duration::from_millis(dur_ms as u64), f).await.map_err(|e| e.into()) - } - } -} - -pub async fn blocking_wrapper(blocking_task: F, err_result: R) -> R -where - F: FnOnce() -> R + Send + 'static, - R: Send + 'static, -{ - // run blocking stuff in blocking thread - cfg_if! { - if #[cfg(feature="rt-async-std")] { - async_std::task::spawn_blocking(blocking_task).await - } else if #[cfg(feature="rt-tokio")] { - tokio::task::spawn_blocking(blocking_task).await.unwrap_or(err_result) - } else { - #[compile_error("must use an executor")] - } - } -} - -pub fn get_concurrency() -> u32 { - std::thread::available_parallelism() - .map(|x| x.get()) - .unwrap_or_else(|e| { - warn!("unable to get concurrency defaulting to single core: {}", e); - 1 - }) as u32 -} pub async fn get_outbound_relay_peer() -> Option { panic!("Native Veilid should never require an outbound relay"); } -/* -pub fn async_callback(fut: F, ok_fn: OF, err_fn: EF) -where - F: Future> + Send + 'static, - OF: FnOnce(T) + Send + 'static, - EF: FnOnce(E) + Send + 'static, -{ - spawn(Box::pin(async move { - match fut.await { - Ok(v) => ok_fn(v), - Err(e) => err_fn(e), - }; - })); -} -*/ - ///////////////////////////////////////////////////////////////////////////////// // Resolver // diff --git a/veilid-core/src/intf/native/utils/network_interfaces/netlink.rs b/veilid-core/src/intf/native/utils/network_interfaces/netlink.rs index fc53362e..f41988a6 100644 --- a/veilid-core/src/intf/native/utils/network_interfaces/netlink.rs +++ b/veilid-core/src/intf/native/utils/network_interfaces/netlink.rs @@ -322,7 +322,7 @@ impl PlatformSupportNetlink { .wrap_err("failed to create rtnetlink socket")?; // Spawn a connection handler - let connection_jh = intf::spawn(connection); + let connection_jh = spawn(connection); // Save the connection self.connection_jh = Some(connection_jh); diff --git a/veilid-core/src/intf/wasm/system.rs b/veilid-core/src/intf/wasm/system.rs index 00158ad8..36b8bd14 100644 --- a/veilid-core/src/intf/wasm/system.rs +++ b/veilid-core/src/intf/wasm/system.rs @@ -1,159 +1,8 @@ -use super::utils; use crate::xx::*; -use crate::*; + use async_executors::{Bindgen, LocalSpawnHandleExt, SpawnHandleExt, Timer}; use futures_util::future::{select, Either}; use js_sys::*; -//use wasm_bindgen_futures::*; -//use web_sys::*; - -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(catch, structural, js_namespace = global, js_name = setTimeout)] - fn nodejs_global_set_timeout_with_callback_and_timeout_and_arguments_0( - handler: &::js_sys::Function, - timeout: u32, - ) -> Result; -} - -pub fn get_timestamp() -> u64 { - if utils::is_browser() { - return (Date::now() * 1000.0f64) as u64; - } else { - panic!("WASM requires browser environment"); - } -} - -// pub fn get_timestamp_string() -> String { -// let date = Date::new_0(); -// let hours = Date::get_utc_hours(&date); -// let minutes = Date::get_utc_minutes(&date); -// let seconds = Date::get_utc_seconds(&date); -// let milliseconds = Date::get_utc_milliseconds(&date); -// format!( -// "{:02}:{:02}:{:02}.{}", -// hours, minutes, seconds, milliseconds -// ) -// } - -pub fn random_bytes(dest: &mut [u8]) -> EyreResult<()> { - let len = dest.len(); - let u32len = len / 4; - let remlen = len % 4; - - for n in 0..u32len { - let r = (Math::random() * (u32::max_value() as f64)) as u32; - - dest[n * 4 + 0] = (r & 0xFF) as u8; - dest[n * 4 + 1] = ((r >> 8) & 0xFF) as u8; - dest[n * 4 + 2] = ((r >> 16) & 0xFF) as u8; - dest[n * 4 + 3] = ((r >> 24) & 0xFF) as u8; - } - if remlen > 0 { - let r = (Math::random() * (u32::max_value() as f64)) as u32; - for n in 0..remlen { - dest[u32len * 4 + n] = ((r >> (n * 8)) & 0xFF) as u8; - } - } - - Ok(()) -} - -pub fn get_random_u32() -> u32 { - (Math::random() * (u32::max_value() as f64)) as u32 -} - -pub fn get_random_u64() -> u64 { - let v1: u32 = get_random_u32(); - let v2: u32 = get_random_u32(); - ((v1 as u64) << 32) | ((v2 as u32) as u64) -} - -pub async fn sleep(millis: u32) { - Bindgen.sleep(Duration::from_millis(millis.into())).await -} - -pub fn system_boxed<'a, Out>( - future: impl Future + Send + 'a, -) -> SendPinBoxFutureLifetime<'a, Out> { - Box::pin(future) -} - -pub fn spawn(future: impl Future + Send + 'static) -> MustJoinHandle -where - Out: Send + 'static, -{ - MustJoinHandle::new( - Bindgen - .spawn_handle(future) - .expect("wasm-bindgen-futures spawn should never error out"), - ) -} - -pub fn spawn_local(future: impl Future + 'static) -> MustJoinHandle -where - Out: 'static, -{ - MustJoinHandle::new( - Bindgen - .spawn_handle_local(future) - .expect("wasm-bindgen-futures spawn_local should never error out"), - ) -} - -// pub fn spawn_with_local_set( -// future: impl Future + Send + 'static, -// ) -> MustJoinHandle -// where -// Out: Send + 'static, -// { -// spawn(future) -// } - -pub fn spawn_detached(future: impl Future + Send + 'static) -where - Out: Send + 'static, -{ - Bindgen - .spawn_handle_local(future) - .expect("wasm-bindgen-futures spawn_local should never error out") - .detach() -} - -pub fn interval(freq_ms: u32, callback: F) -> SendPinBoxFuture<()> -where - F: Fn() -> FUT + Send + Sync + 'static, - FUT: Future + Send, -{ - let e = Eventual::new(); - - let ie = e.clone(); - let jh = spawn(Box::pin(async move { - while timeout(freq_ms, ie.instance_clone(())).await.is_err() { - callback().await; - } - })); - - Box::pin(async move { - e.resolve().await; - jh.await; - }) -} - -pub async fn timeout(dur_ms: u32, f: F) -> Result -where - F: Future, -{ - match select(Box::pin(intf::sleep(dur_ms)), Box::pin(f)).await { - Either::Left((_x, _b)) => Err(TimeoutError()), - Either::Right((y, _a)) => Ok(y), - } -} - -// xxx: for now until wasm threads are more stable, and/or we bother with web workers -pub fn get_concurrency() -> u32 { - 1 -} pub async fn get_outbound_relay_peer() -> Option { // unimplemented! diff --git a/veilid-core/src/intf/wasm/utils/mod.rs b/veilid-core/src/intf/wasm/utils/mod.rs index fcec19f7..8b137891 100644 --- a/veilid-core/src/intf/wasm/utils/mod.rs +++ b/veilid-core/src/intf/wasm/utils/mod.rs @@ -1,54 +1 @@ -#![cfg(target_arch = "wasm32")] -use crate::xx::*; -use core::sync::atomic::{AtomicI8, Ordering}; -use js_sys::{global, Reflect}; - -#[wasm_bindgen] -extern "C" { - // Use `js_namespace` here to bind `console.log(..)` instead of just - // `log(..)` - #[wasm_bindgen(js_namespace = console, js_name = log)] - pub fn console_log(s: &str); - - #[wasm_bindgen] - pub fn alert(s: &str); -} - -pub fn is_browser() -> bool { - static CACHE: AtomicI8 = AtomicI8::new(-1); - let cache = CACHE.load(Ordering::Relaxed); - if cache != -1 { - return cache != 0; - } - - let res = Reflect::has(&global().as_ref(), &"window".into()).unwrap_or_default(); - - CACHE.store(res as i8, Ordering::Relaxed); - - res -} - -// pub fn is_browser_https() -> bool { -// static CACHE: AtomicI8 = AtomicI8::new(-1); -// let cache = CACHE.load(Ordering::Relaxed); -// if cache != -1 { -// return cache != 0; -// } - -// let res = js_sys::eval("window.location.protocol === 'https'") -// .map(|res| res.is_truthy()) -// .unwrap_or_default(); - -// CACHE.store(res as i8, Ordering::Relaxed); - -// res -// } - -#[derive(ThisError, Debug, Clone, Eq, PartialEq)] -#[error("JsValue error")] -pub struct JsValueError(String); - -pub fn map_jsvalue_error(x: JsValue) -> JsValueError { - JsValueError(x.as_string().unwrap_or_default()) -} diff --git a/veilid-core/src/lib.rs b/veilid-core/src/lib.rs index adc8d656..89a6456c 100644 --- a/veilid-core/src/lib.rs +++ b/veilid-core/src/lib.rs @@ -32,10 +32,6 @@ mod veilid_api; #[macro_use] mod veilid_config; mod veilid_layer_filter; -mod veilid_rng; - -#[macro_use] -pub mod xx; pub use self::api_tracing_layer::ApiTracingLayer; pub use self::attachment_manager::AttachmentState; @@ -43,6 +39,7 @@ pub use self::core_context::{api_startup, api_startup_json, UpdateCallback}; pub use self::veilid_api::*; pub use self::veilid_config::*; pub use self::veilid_layer_filter::*; +pub use veilid_tools as tools; pub mod veilid_capnp { include!(concat!(env!("OUT_DIR"), "/proto/veilid_capnp.rs")); diff --git a/veilid-core/src/network_manager/connection_limits.rs b/veilid-core/src/network_manager/connection_limits.rs index 9c2121c0..f25f1654 100644 --- a/veilid-core/src/network_manager/connection_limits.rs +++ b/veilid-core/src/network_manager/connection_limits.rs @@ -78,7 +78,7 @@ impl ConnectionLimits { pub fn add(&mut self, addr: IpAddr) -> Result<(), AddressFilterError> { let ipblock = ip_to_ipblock(self.max_connections_per_ip6_prefix_size, addr); - let ts = intf::get_timestamp(); + let ts = get_timestamp(); self.purge_old_timestamps(ts); @@ -134,7 +134,7 @@ impl ConnectionLimits { pub fn remove(&mut self, addr: IpAddr) -> Result<(), AddressNotInTableError> { let ipblock = ip_to_ipblock(self.max_connections_per_ip6_prefix_size, addr); - let ts = intf::get_timestamp(); + let ts = get_timestamp(); self.purge_old_timestamps(ts); match ipblock { diff --git a/veilid-core/src/network_manager/connection_manager.rs b/veilid-core/src/network_manager/connection_manager.rs index 30217cce..b8c62e84 100644 --- a/veilid-core/src/network_manager/connection_manager.rs +++ b/veilid-core/src/network_manager/connection_manager.rs @@ -319,7 +319,7 @@ impl ConnectionManager { }; log_net!(debug "get_or_create_connection retries left: {}", retry_count); retry_count -= 1; - intf::sleep(500).await; + sleep(500).await; }); // Add to the connection table diff --git a/veilid-core/src/network_manager/mod.rs b/veilid-core/src/network_manager/mod.rs index 52bc61a5..f5fec43e 100644 --- a/veilid-core/src/network_manager/mod.rs +++ b/veilid-core/src/network_manager/mod.rs @@ -1,5 +1,5 @@ -use crate::*; use crate::xx::*; +use crate::*; #[cfg(not(target_arch = "wasm32"))] mod native; @@ -403,11 +403,11 @@ impl NetworkManager { let mut inner = self.inner.lock(); match inner.client_whitelist.entry(client) { hashlink::lru_cache::Entry::Occupied(mut entry) => { - entry.get_mut().last_seen_ts = intf::get_timestamp() + entry.get_mut().last_seen_ts = get_timestamp() } hashlink::lru_cache::Entry::Vacant(entry) => { entry.insert(ClientWhitelistEntry { - last_seen_ts: intf::get_timestamp(), + last_seen_ts: get_timestamp(), }); } } @@ -419,7 +419,7 @@ impl NetworkManager { match inner.client_whitelist.entry(client) { hashlink::lru_cache::Entry::Occupied(mut entry) => { - entry.get_mut().last_seen_ts = intf::get_timestamp(); + entry.get_mut().last_seen_ts = get_timestamp(); true } hashlink::lru_cache::Entry::Vacant(_) => false, @@ -429,7 +429,7 @@ impl NetworkManager { pub fn purge_client_whitelist(&self) { let timeout_ms = self.with_config(|c| c.network.client_whitelist_timeout_ms); let mut inner = self.inner.lock(); - let cutoff_timestamp = intf::get_timestamp() - ((timeout_ms as u64) * 1000u64); + let cutoff_timestamp = get_timestamp() - ((timeout_ms as u64) * 1000u64); // Remove clients from the whitelist that haven't been since since our whitelist timeout while inner .client_whitelist @@ -516,7 +516,7 @@ impl NetworkManager { .wrap_err("failed to generate signed receipt")?; // Record the receipt for later - let exp_ts = intf::get_timestamp() + expiration_us; + let exp_ts = get_timestamp() + expiration_us; receipt_manager.record_receipt(receipt, exp_ts, expected_returns, callback); Ok(out) @@ -540,7 +540,7 @@ impl NetworkManager { .wrap_err("failed to generate signed receipt")?; // Record the receipt for later - let exp_ts = intf::get_timestamp() + expiration_us; + let exp_ts = get_timestamp() + expiration_us; let eventual = SingleShotEventual::new(Some(ReceiptEvent::Cancelled)); let instance = eventual.instance(); receipt_manager.record_single_shot_receipt(receipt, exp_ts, eventual); @@ -707,7 +707,7 @@ impl NetworkManager { // XXX: do we need a delay here? or another hole punch packet? // Set the hole punch as our 'last connection' to ensure we return the receipt over the direct hole punch - peer_nr.set_last_connection(connection_descriptor, intf::get_timestamp()); + peer_nr.set_last_connection(connection_descriptor, get_timestamp()); // Return the receipt using the same dial info send the receipt to it rpc.rpc_call_return_receipt(Destination::direct(peer_nr), receipt) @@ -731,7 +731,7 @@ impl NetworkManager { let node_id_secret = routing_table.node_id_secret(); // Get timestamp, nonce - let ts = intf::get_timestamp(); + let ts = get_timestamp(); let nonce = Crypto::get_random_nonce(); // Encode envelope @@ -1116,8 +1116,7 @@ impl NetworkManager { // ); // Update timestamp for this last connection since we just sent to it - node_ref - .set_last_connection(connection_descriptor, intf::get_timestamp()); + node_ref.set_last_connection(connection_descriptor, get_timestamp()); return Ok(NetworkResult::value(SendDataKind::Existing( connection_descriptor, @@ -1149,7 +1148,7 @@ impl NetworkManager { this.net().send_data_to_dial_info(dial_info, data).await? ); // If we connected to this node directly, save off the last connection so we can use it again - node_ref.set_last_connection(connection_descriptor, intf::get_timestamp()); + node_ref.set_last_connection(connection_descriptor, get_timestamp()); Ok(NetworkResult::value(SendDataKind::Direct( connection_descriptor, @@ -1324,7 +1323,7 @@ impl NetworkManager { }); // Validate timestamp isn't too old - let ts = intf::get_timestamp(); + let ts = get_timestamp(); let ets = envelope.get_timestamp(); if let Some(tsbehind) = tsbehind { if tsbehind > 0 && (ts > ets && ts - ets > tsbehind) { @@ -1631,7 +1630,7 @@ impl NetworkManager { // public dialinfo let inconsistent = if inconsistencies.len() >= PUBLIC_ADDRESS_CHANGE_DETECTION_COUNT { - let exp_ts = intf::get_timestamp() + PUBLIC_ADDRESS_INCONSISTENCY_TIMEOUT_US; + let exp_ts = get_timestamp() + PUBLIC_ADDRESS_INCONSISTENCY_TIMEOUT_US; for i in &inconsistencies { pait.insert(*i, exp_ts); } @@ -1644,8 +1643,8 @@ impl NetworkManager { .public_address_inconsistencies_table .entry(key) .or_insert_with(|| HashMap::new()); - let exp_ts = intf::get_timestamp() - + PUBLIC_ADDRESS_INCONSISTENCY_PUNISHMENT_TIMEOUT_US; + let exp_ts = + get_timestamp() + PUBLIC_ADDRESS_INCONSISTENCY_PUNISHMENT_TIMEOUT_US; for i in inconsistencies { pait.insert(i, exp_ts); } @@ -1733,7 +1732,7 @@ impl NetworkManager { } // Get the list of refs to all nodes to update - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); let node_refs = this.routing_table() .get_nodes_needing_updates(routing_domain, cur_ts, all); diff --git a/veilid-core/src/network_manager/native/igd_manager.rs b/veilid-core/src/network_manager/native/igd_manager.rs index fcc46e64..f7580a89 100644 --- a/veilid-core/src/network_manager/native/igd_manager.rs +++ b/veilid-core/src/network_manager/native/igd_manager.rs @@ -176,7 +176,7 @@ impl IGDManager { mapped_port: u16, ) -> Option<()> { let this = self.clone(); - intf::blocking_wrapper(move || { + blocking_wrapper(move || { let mut inner = this.inner.lock(); // If we already have this port mapped, just return the existing portmap @@ -215,7 +215,7 @@ impl IGDManager { expected_external_address: Option, ) -> Option { let this = self.clone(); - intf::blocking_wrapper(move || { + blocking_wrapper(move || { let mut inner = this.inner.lock(); // If we already have this port mapped, just return the existing portmap @@ -275,7 +275,7 @@ impl IGDManager { }; // Add to mapping list to keep alive - let timestamp = intf::get_timestamp(); + let timestamp = get_timestamp(); inner.port_maps.insert(PortMapKey { llpt, at, @@ -301,7 +301,7 @@ impl IGDManager { let mut renews: Vec<(PortMapKey, PortMapValue)> = Vec::new(); { let inner = self.inner.lock(); - let now = intf::get_timestamp(); + let now = get_timestamp(); for (k, v) in &inner.port_maps { let mapping_lifetime = now.saturating_sub(v.timestamp); @@ -323,7 +323,7 @@ impl IGDManager { } let this = self.clone(); - intf::blocking_wrapper(move || { + blocking_wrapper(move || { let mut inner = this.inner.lock(); // Process full renewals @@ -356,7 +356,7 @@ impl IGDManager { inner.port_maps.insert(k, PortMapValue { ext_ip: v.ext_ip, mapped_port, - timestamp: intf::get_timestamp(), + timestamp: get_timestamp(), renewal_lifetime: (UPNP_MAPPING_LIFETIME_MS / 2) as u64 * 1000u64, renewal_attempts: 0, }); @@ -397,7 +397,7 @@ impl IGDManager { inner.port_maps.insert(k, PortMapValue { ext_ip: v.ext_ip, mapped_port: v.mapped_port, - timestamp: intf::get_timestamp(), + timestamp: get_timestamp(), renewal_lifetime: (UPNP_MAPPING_LIFETIME_MS / 2) as u64 * 1000u64, renewal_attempts: 0, }); diff --git a/veilid-core/src/network_manager/native/mod.rs b/veilid-core/src/network_manager/native/mod.rs index f13e3fb7..8978918f 100644 --- a/veilid-core/src/network_manager/native/mod.rs +++ b/veilid-core/src/network_manager/native/mod.rs @@ -1,5 +1,4 @@ mod igd_manager; -mod natpmp_manager; mod network_class_discovery; mod network_tcp; mod network_udp; @@ -94,11 +93,9 @@ struct NetworkUnlockedInner { update_network_class_task: TickTask, network_interfaces_task: TickTask, upnp_task: TickTask, - natpmp_task: TickTask, // Managers igd_manager: igd_manager::IGDManager, - natpmp_manager: natpmp_manager::NATPMPManager, } #[derive(Clone)] @@ -150,9 +147,7 @@ impl Network { update_network_class_task: TickTask::new(1), network_interfaces_task: TickTask::new(5), upnp_task: TickTask::new(1), - natpmp_task: TickTask::new(1), igd_manager: igd_manager::IGDManager::new(config.clone()), - natpmp_manager: natpmp_manager::NATPMPManager::new(config), } } @@ -196,13 +191,6 @@ impl Network { .upnp_task .set_routine(move |s, l, t| Box::pin(this2.clone().upnp_task_routine(s, l, t))); } - // Set natpmp tick task - { - let this2 = this.clone(); - this.unlocked_inner - .natpmp_task - .set_routine(move |s, l, t| Box::pin(this2.clone().natpmp_task_routine(s, l, t))); - } this } @@ -904,31 +892,11 @@ impl Network { Ok(()) } - #[instrument(level = "trace", skip(self), err)] - pub async fn natpmp_task_routine( - self, - stop_token: StopToken, - _l: u64, - _t: u64, - ) -> EyreResult<()> { - if !self.unlocked_inner.natpmp_manager.tick().await? { - info!("natpmp failed, restarting local network"); - let mut inner = self.inner.lock(); - inner.network_needs_restart = true; - } - - Ok(()) - } - pub async fn tick(&self) -> EyreResult<()> { - let (detect_address_changes, upnp, natpmp) = { + let (detect_address_changes, upnp) = { let config = self.network_manager().config(); let c = config.get(); - ( - c.network.detect_address_changes, - c.network.upnp, - c.network.natpmp, - ) + (c.network.detect_address_changes, c.network.upnp) }; // If we need to figure out our network class, tick the task for it @@ -962,11 +930,6 @@ impl Network { self.unlocked_inner.upnp_task.tick().await?; } - // If we need to tick natpmp, do it - if natpmp && !self.needs_restart() { - self.unlocked_inner.natpmp_task.tick().await?; - } - Ok(()) } } diff --git a/veilid-core/src/network_manager/native/natpmp_manager.rs b/veilid-core/src/network_manager/native/natpmp_manager.rs deleted file mode 100644 index 4342abfc..00000000 --- a/veilid-core/src/network_manager/native/natpmp_manager.rs +++ /dev/null @@ -1,18 +0,0 @@ -use super::*; - -pub struct NATPMPManager { - config: VeilidConfig, -} - -impl NATPMPManager { - // - - pub fn new(config: VeilidConfig) -> Self { - Self { config } - } - - pub async fn tick(&self) -> EyreResult { - // xxx - Ok(true) - } -} diff --git a/veilid-core/src/network_manager/native/network_class_discovery.rs b/veilid-core/src/network_manager/native/network_class_discovery.rs index 89c61755..3f7e6122 100644 --- a/veilid-core/src/network_manager/native/network_class_discovery.rs +++ b/veilid-core/src/network_manager/native/network_class_discovery.rs @@ -275,7 +275,7 @@ impl DiscoveryContext { LowLevelProtocolType::UDP => "udp", LowLevelProtocolType::TCP => "tcp", }); - intf::sleep(PORT_MAP_VALIDATE_DELAY_MS).await + sleep(PORT_MAP_VALIDATE_DELAY_MS).await } else { break; } @@ -304,9 +304,9 @@ impl DiscoveryContext { #[instrument(level = "trace", skip(self), ret)] async fn try_port_mapping(&self) -> Option { - let (enable_upnp, _enable_natpmp) = { + let enable_upnp = { let c = self.net.config.get(); - (c.network.upnp, c.network.natpmp) + c.network.upnp }; if enable_upnp { diff --git a/veilid-core/src/network_manager/native/network_tcp.rs b/veilid-core/src/network_manager/native/network_tcp.rs index ebca4378..b537160a 100644 --- a/veilid-core/src/network_manager/native/network_tcp.rs +++ b/veilid-core/src/network_manager/native/network_tcp.rs @@ -58,7 +58,7 @@ impl Network { // Don't waste more than N seconds getting it though, in case someone // is trying to DoS us with a bunch of connections or something // read a chunk of the stream - intf::timeout( + timeout( tls_connection_initial_timeout_ms, ps.peek_exact(&mut first_packet), ) diff --git a/veilid-core/src/network_manager/native/network_udp.rs b/veilid-core/src/network_manager/native/network_udp.rs index b00bf643..1174238b 100644 --- a/veilid-core/src/network_manager/native/network_udp.rs +++ b/veilid-core/src/network_manager/native/network_udp.rs @@ -10,7 +10,7 @@ impl Network { c.network.protocol.udp.socket_pool_size }; if task_count == 0 { - task_count = intf::get_concurrency() / 2; + task_count = get_concurrency() / 2; if task_count == 0 { task_count = 1; } diff --git a/veilid-core/src/network_manager/native/protocol/sockets.rs b/veilid-core/src/network_manager/native/protocol/sockets.rs index c8918e33..0cf7454d 100644 --- a/veilid-core/src/network_manager/native/protocol/sockets.rs +++ b/veilid-core/src/network_manager/native/protocol/sockets.rs @@ -196,7 +196,7 @@ pub async fn nonblocking_connect( let async_stream = Async::new(std::net::TcpStream::from(socket))?; // The stream becomes writable when connected - timeout_or_try!(intf::timeout(timeout_ms, async_stream.writable()) + timeout_or_try!(timeout(timeout_ms, async_stream.writable()) .await .into_timeout_or() .into_result()?); diff --git a/veilid-core/src/network_manager/network_connection.rs b/veilid-core/src/network_manager/network_connection.rs index cffc91ae..3d410615 100644 --- a/veilid-core/src/network_manager/network_connection.rs +++ b/veilid-core/src/network_manager/network_connection.rs @@ -99,13 +99,13 @@ pub struct NetworkConnection { impl NetworkConnection { pub(super) fn dummy(id: NetworkConnectionId, descriptor: ConnectionDescriptor) -> Self { // Create handle for sending (dummy is immediately disconnected) - let (sender, _receiver) = flume::bounded(intf::get_concurrency() as usize); + let (sender, _receiver) = flume::bounded(get_concurrency() as usize); Self { connection_id: id, descriptor, processor: None, - established_time: intf::get_timestamp(), + established_time: get_timestamp(), stats: Arc::new(Mutex::new(NetworkConnectionStats { last_message_sent_time: None, last_message_recv_time: None, @@ -125,7 +125,7 @@ impl NetworkConnection { let descriptor = protocol_connection.descriptor(); // Create handle for sending - let (sender, receiver) = flume::bounded(intf::get_concurrency() as usize); + let (sender, receiver) = flume::bounded(get_concurrency() as usize); // Create stats let stats = Arc::new(Mutex::new(NetworkConnectionStats { @@ -137,7 +137,7 @@ impl NetworkConnection { let local_stop_token = stop_source.token(); // Spawn connection processor and pass in protocol connection - let processor = intf::spawn(Self::process_connection( + let processor = spawn(Self::process_connection( connection_manager, local_stop_token, manager_stop_token, @@ -153,7 +153,7 @@ impl NetworkConnection { connection_id, descriptor, processor: Some(processor), - established_time: intf::get_timestamp(), + established_time: get_timestamp(), stats, sender, stop_source: Some(stop_source), @@ -185,7 +185,7 @@ impl NetworkConnection { stats: Arc>, message: Vec, ) -> io::Result> { - let ts = intf::get_timestamp(); + let ts = get_timestamp(); let out = network_result_try!(protocol_connection.send(message).await?); let mut stats = stats.lock(); @@ -199,7 +199,7 @@ impl NetworkConnection { protocol_connection: &ProtocolNetworkConnection, stats: Arc>, ) -> io::Result>> { - let ts = intf::get_timestamp(); + let ts = get_timestamp(); let out = network_result_try!(protocol_connection.recv().await?); let mut stats = stats.lock(); @@ -246,7 +246,7 @@ impl NetworkConnection { // Push mutable timer so we can reset it // Normally we would use an io::timeout here, but WASM won't support that, so we use a mutable sleep future let new_timer = || { - intf::sleep(connection_manager.connection_inactivity_timeout_ms()).then(|_| async { + sleep(connection_manager.connection_inactivity_timeout_ms()).then(|_| async { // timeout log_net!("== Connection timeout on {:?}", descriptor.green()); RecvLoopAction::Timeout diff --git a/veilid-core/src/receipt_manager.rs b/veilid-core/src/receipt_manager.rs index 3e9971a3..54f37765 100644 --- a/veilid-core/src/receipt_manager.rs +++ b/veilid-core/src/receipt_manager.rs @@ -281,7 +281,7 @@ impl ReceiptManager { }; (inner.next_oldest_ts, inner.timeout_task.clone(), stop_token) }; - let now = intf::get_timestamp(); + let now = get_timestamp(); // If we have at least one timestamp to expire, lets do it if let Some(next_oldest_ts) = next_oldest_ts { if now >= next_oldest_ts { diff --git a/veilid-core/src/routing_table/bucket.rs b/veilid-core/src/routing_table/bucket.rs index b1e3199c..d1a93d2e 100644 --- a/veilid-core/src/routing_table/bucket.rs +++ b/veilid-core/src/routing_table/bucket.rs @@ -120,7 +120,7 @@ impl Bucket { .iter() .map(|(k, v)| (k.clone(), v.clone())) .collect(); - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); sorted_entries.sort_by(|a, b| -> core::cmp::Ordering { if a.0 == b.0 { return core::cmp::Ordering::Equal; diff --git a/veilid-core/src/routing_table/bucket_entry.rs b/veilid-core/src/routing_table/bucket_entry.rs index c4cf88c6..b63d59e9 100644 --- a/veilid-core/src/routing_table/bucket_entry.rs +++ b/veilid-core/src/routing_table/bucket_entry.rs @@ -231,7 +231,7 @@ impl BucketEntryInner { // No need to update the signednodeinfo though since the timestamp is the same // Touch the node and let it try to live again self.updated_since_last_network_change = true; - self.touch_last_seen(intf::get_timestamp()); + self.touch_last_seen(get_timestamp()); } return; } @@ -258,7 +258,7 @@ impl BucketEntryInner { // Update the signed node info *opt_current_sni = Some(Box::new(signed_node_info)); self.updated_since_last_network_change = true; - self.touch_last_seen(intf::get_timestamp()); + self.touch_last_seen(get_timestamp()); } pub fn has_node_info(&self, routing_domain_set: RoutingDomainSet) -> bool { @@ -672,7 +672,7 @@ pub struct BucketEntry { impl BucketEntry { pub(super) fn new() -> Self { - let now = intf::get_timestamp(); + let now = get_timestamp(); Self { ref_count: AtomicU32::new(0), inner: RwLock::new(BucketEntryInner { diff --git a/veilid-core/src/routing_table/debug.rs b/veilid-core/src/routing_table/debug.rs index fcac7958..45779587 100644 --- a/veilid-core/src/routing_table/debug.rs +++ b/veilid-core/src/routing_table/debug.rs @@ -104,7 +104,7 @@ impl RoutingTable { pub(crate) fn debug_info_entries(&self, limit: usize, min_state: BucketEntryState) -> String { let inner = self.inner.read(); let inner = &*inner; - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); let mut out = String::new(); @@ -164,7 +164,7 @@ impl RoutingTable { pub(crate) fn debug_info_buckets(&self, min_state: BucketEntryState) -> String { let inner = self.inner.read(); let inner = &*inner; - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); let mut out = String::new(); const COLS: usize = 16; diff --git a/veilid-core/src/routing_table/node_ref.rs b/veilid-core/src/routing_table/node_ref.rs index ae77c4df..de456feb 100644 --- a/veilid-core/src/routing_table/node_ref.rs +++ b/veilid-core/src/routing_table/node_ref.rs @@ -275,7 +275,7 @@ pub trait NodeRefBase: Sized { } else { // If this is not connection oriented, then we check our last seen time // to see if this mapping has expired (beyond our timeout) - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); if (last_seen + (CONNECTIONLESS_TIMEOUT_SECS as u64 * 1_000_000u64)) >= cur_ts { return Some(last_connection); } diff --git a/veilid-core/src/routing_table/route_spec_store.rs b/veilid-core/src/routing_table/route_spec_store.rs index e9c0681f..cf56df49 100644 --- a/veilid-core/src/routing_table/route_spec_store.rs +++ b/veilid-core/src/routing_table/route_spec_store.rs @@ -624,7 +624,7 @@ impl RouteSpecStore { .map(|nr| nr.node_id()); // Get list of all nodes, and sort them for selection - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); let filter = Box::new( move |rti: &RoutingTableInner, k: DHTKey, v: Option>| -> bool { // Exclude our own node from routes @@ -994,7 +994,7 @@ impl RouteSpecStore { pub async fn test_route(&self, key: &DHTKey) -> EyreResult { let is_remote = { let inner = &mut *self.inner.lock(); - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); Self::with_peek_remote_private_route(inner, cur_ts, key, |_| {}).is_some() }; if is_remote { @@ -1058,7 +1058,7 @@ impl RouteSpecStore { pub fn release_route(&self, key: &DHTKey) -> bool { let is_remote = { let inner = &mut *self.inner.lock(); - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); Self::with_peek_remote_private_route(inner, cur_ts, key, |_| {}).is_some() }; if is_remote { @@ -1079,7 +1079,7 @@ impl RouteSpecStore { directions: DirectionSet, avoid_node_ids: &[DHTKey], ) -> Option { - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); for detail in &inner.content.details { if detail.1.stability >= stability && detail.1.sequencing >= sequencing @@ -1137,7 +1137,7 @@ impl RouteSpecStore { /// Get the debug description of a route pub fn debug_route(&self, key: &DHTKey) -> Option { let inner = &mut *self.inner.lock(); - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); // If this is a remote route, print it if let Some(s) = Self::with_peek_remote_private_route(inner, cur_ts, key, |rpi| format!("{:#?}", rpi)) @@ -1534,7 +1534,7 @@ impl RouteSpecStore { } // store the private route in our cache - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); let key = Self::with_create_remote_private_route(inner, cur_ts, private_route, |r| { r.private_route.as_ref().unwrap().public_key.clone() }); @@ -1557,7 +1557,7 @@ impl RouteSpecStore { /// Retrieve an imported remote private route by its public key pub fn get_remote_private_route(&self, key: &DHTKey) -> Option { let inner = &mut *self.inner.lock(); - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); Self::with_get_remote_private_route(inner, cur_ts, key, |r| { r.private_route.as_ref().unwrap().clone() }) @@ -1566,7 +1566,7 @@ impl RouteSpecStore { /// Retrieve an imported remote private route by its public key but don't 'touch' it pub fn peek_remote_private_route(&self, key: &DHTKey) -> Option { let inner = &mut *self.inner.lock(); - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); Self::with_peek_remote_private_route(inner, cur_ts, key, |r| { r.private_route.as_ref().unwrap().clone() }) @@ -1670,7 +1670,7 @@ impl RouteSpecStore { /// private route and gotten a response before pub fn has_remote_private_route_seen_our_node_info(&self, key: &DHTKey) -> bool { let inner = &mut *self.inner.lock(); - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); Self::with_peek_remote_private_route(inner, cur_ts, key, |rpr| rpr.seen_our_node_info) .unwrap_or_default() } diff --git a/veilid-core/src/routing_table/routing_table_inner.rs b/veilid-core/src/routing_table/routing_table_inner.rs index f37672fe..42a4b09b 100644 --- a/veilid-core/src/routing_table/routing_table_inner.rs +++ b/veilid-core/src/routing_table/routing_table_inner.rs @@ -227,7 +227,7 @@ impl RoutingTableInner { } pub fn reset_all_seen_our_node_info(&mut self, routing_domain: RoutingDomain) { - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); self.with_entries_mut(cur_ts, BucketEntryState::Dead, |rti, _, v| { v.with_mut(rti, |_rti, e| { e.set_seen_our_node_info(routing_domain, false); @@ -237,7 +237,7 @@ impl RoutingTableInner { } pub fn reset_all_updated_since_last_network_change(&mut self) { - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); self.with_entries_mut(cur_ts, BucketEntryState::Dead, |rti, _, v| { v.with_mut(rti, |_rti, e| { e.set_updated_since_last_network_change(false) @@ -330,7 +330,7 @@ impl RoutingTableInner { // If the local network topology has changed, nuke the existing local node info and let new local discovery happen if changed { - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); self.with_entries_mut(cur_ts, BucketEntryState::Dead, |rti, _, e| { e.with_mut(rti, |_rti, e| { e.clear_signed_node_info(RoutingDomain::LocalNetwork); @@ -410,7 +410,7 @@ impl RoutingTableInner { min_state: BucketEntryState, ) -> usize { let mut count = 0usize; - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); self.with_entries(cur_ts, min_state, |rti, _, e| { if e.with(rti, |_rti, e| e.best_routing_domain(routing_domain_set)) .is_some() @@ -712,7 +712,7 @@ impl RoutingTableInner { pub fn get_routing_table_health(&self) -> RoutingTableHealth { let mut health = RoutingTableHealth::default(); - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); for bucket in &self.buckets { for (_, v) in bucket.entries() { match v.with(self, |_rti, e| e.state(cur_ts)) { @@ -869,7 +869,7 @@ impl RoutingTableInner { where T: for<'r> FnMut(&'r RoutingTableInner, DHTKey, Option>) -> O, { - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); // Add filter to remove dead nodes always let filter_dead = Box::new( @@ -954,7 +954,7 @@ impl RoutingTableInner { where T: for<'r> FnMut(&'r RoutingTableInner, DHTKey, Option>) -> O, { - let cur_ts = intf::get_timestamp(); + let cur_ts = get_timestamp(); let node_count = { let config = self.config(); let c = config.get(); diff --git a/veilid-core/src/rpc_processor/coders/operations/operation.rs b/veilid-core/src/rpc_processor/coders/operations/operation.rs index aeeacf08..a594e7ec 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation.rs @@ -64,7 +64,7 @@ pub struct RPCOperation { impl RPCOperation { pub fn new_question(question: RPCQuestion, sender_node_info: Option) -> Self { Self { - op_id: intf::get_random_u64(), + op_id: get_random_u64(), sender_node_info, kind: RPCOperationKind::Question(question), } @@ -74,7 +74,7 @@ impl RPCOperation { sender_node_info: Option, ) -> Self { Self { - op_id: intf::get_random_u64(), + op_id: get_random_u64(), sender_node_info, kind: RPCOperationKind::Statement(statement), } diff --git a/veilid-core/src/rpc_processor/mod.rs b/veilid-core/src/rpc_processor/mod.rs index 866c6ae1..646c665f 100644 --- a/veilid-core/src/rpc_processor/mod.rs +++ b/veilid-core/src/rpc_processor/mod.rs @@ -28,6 +28,7 @@ pub use rpc_error::*; pub use rpc_status::*; use super::*; + use crate::crypto::*; use crate::xx::*; use futures_util::StreamExt; @@ -256,7 +257,7 @@ impl RPCProcessor { let timeout = ms_to_us(c.network.rpc.timeout_ms); let max_route_hop_count = c.network.rpc.max_route_hop_count as usize; if concurrency == 0 { - concurrency = intf::get_concurrency() / 2; + concurrency = get_concurrency() / 2; if concurrency == 0 { concurrency = 1; } @@ -313,7 +314,7 @@ impl RPCProcessor { for _ in 0..self.unlocked_inner.concurrency { let this = self.clone(); let receiver = channel.1.clone(); - let jh = intf::spawn(Self::rpc_worker( + let jh = spawn(Self::rpc_worker( this, inner.stop_source.as_ref().unwrap().token(), receiver, @@ -460,7 +461,7 @@ impl RPCProcessor { } Ok(TimeoutOr::Value((rpcreader, _))) => { // Reply received - let recv_ts = intf::get_timestamp(); + let recv_ts = get_timestamp(); // Record answer received self.record_answer_received( @@ -1011,7 +1012,7 @@ impl RPCProcessor { // Send question let bytes = message.len() as u64; - let send_ts = intf::get_timestamp(); + let send_ts = get_timestamp(); let send_data_kind = network_result_try!(self .network_manager() .send_envelope(node_ref.clone(), Some(node_id), message) @@ -1078,7 +1079,7 @@ impl RPCProcessor { // Send statement let bytes = message.len() as u64; - let send_ts = intf::get_timestamp(); + let send_ts = get_timestamp(); let _send_data_kind = network_result_try!(self .network_manager() .send_envelope(node_ref.clone(), Some(node_id), message) @@ -1139,7 +1140,7 @@ impl RPCProcessor { // Send the reply let bytes = message.len() as u64; - let send_ts = intf::get_timestamp(); + let send_ts = get_timestamp(); network_result_try!(self.network_manager() .send_envelope(node_ref.clone(), Some(node_id), message) .await @@ -1357,7 +1358,7 @@ impl RPCProcessor { connection_descriptor, routing_domain, }), - timestamp: intf::get_timestamp(), + timestamp: get_timestamp(), body_len: body.len() as u64, }, data: RPCMessageData { contents: body }, @@ -1386,7 +1387,7 @@ impl RPCProcessor { remote_safety_route, sequencing, }), - timestamp: intf::get_timestamp(), + timestamp: get_timestamp(), body_len: body.len() as u64, }, data: RPCMessageData { contents: body }, @@ -1419,7 +1420,7 @@ impl RPCProcessor { safety_spec, }, ), - timestamp: intf::get_timestamp(), + timestamp: get_timestamp(), body_len: body.len() as u64, }, data: RPCMessageData { contents: body }, diff --git a/veilid-core/src/rpc_processor/operation_waiter.rs b/veilid-core/src/rpc_processor/operation_waiter.rs index 4dc71332..3bf14b94 100644 --- a/veilid-core/src/rpc_processor/operation_waiter.rs +++ b/veilid-core/src/rpc_processor/operation_waiter.rs @@ -104,9 +104,9 @@ where pub async fn wait_for_op( &self, mut handle: OperationWaitHandle, - timeout: u64, + timeout_us: u64, ) -> Result, RPCError> { - let timeout_ms = u32::try_from(timeout / 1000u64) + let timeout_ms = u32::try_from(timeout_us / 1000u64) .map_err(|e| RPCError::map_internal("invalid timeout")(e))?; // Take the instance @@ -114,8 +114,8 @@ where let eventual_instance = handle.eventual_instance.take().unwrap(); // wait for eventualvalue - let start_ts = intf::get_timestamp(); - let res = intf::timeout(timeout_ms, eventual_instance) + let start_ts = get_timestamp(); + let res = timeout(timeout_ms, eventual_instance) .await .into_timeout_or(); Ok(res @@ -125,7 +125,7 @@ where }) .map(|res| { let (_span_id, ret) = res.take_value().unwrap(); - let end_ts = intf::get_timestamp(); + let end_ts = get_timestamp(); //xxx: causes crash (Missing otel data span extensions) // Span::current().follows_from(span_id); diff --git a/veilid-core/src/tests/common/test_async_tag_lock.rs b/veilid-core/src/tests/common/test_async_tag_lock.rs index e590d4df..350cd9a4 100644 --- a/veilid-core/src/tests/common/test_async_tag_lock.rs +++ b/veilid-core/src/tests/common/test_async_tag_lock.rs @@ -1,5 +1,4 @@ use crate::xx::*; -use crate::*; pub async fn test_simple_no_contention() { info!("test_simple_no_contention"); @@ -36,12 +35,12 @@ pub async fn test_simple_single_contention() { let g1 = table.lock_tag(a1).await; info!("locked"); - let t1 = intf::spawn(async move { + let t1 = spawn(async move { // move the guard into the task let _g1_take = g1; // hold the guard for a bit info!("waiting"); - intf::sleep(1000).await; + sleep(1000).await; // release the guard info!("released"); }); @@ -68,21 +67,21 @@ pub async fn test_simple_double_contention() { let g2 = table.lock_tag(a2).await; info!("locked"); - let t1 = intf::spawn(async move { + let t1 = spawn(async move { // move the guard into the tas let _g1_take = g1; // hold the guard for a bit info!("waiting"); - intf::sleep(1000).await; + sleep(1000).await; // release the guard info!("released"); }); - let t2 = intf::spawn(async move { + let t2 = spawn(async move { // move the guard into the task let _g2_take = g2; // hold the guard for a bit info!("waiting"); - intf::sleep(500).await; + sleep(500).await; // release the guard info!("released"); }); @@ -109,37 +108,37 @@ pub async fn test_parallel_single_contention() { let a1 = SocketAddr::new("1.2.3.4".parse().unwrap(), 1234); let table1 = table.clone(); - let t1 = intf::spawn(async move { + let t1 = spawn(async move { // lock the tag let _g = table1.lock_tag(a1).await; info!("locked t1"); // hold the guard for a bit info!("waiting t1"); - intf::sleep(500).await; + sleep(500).await; // release the guard info!("released t1"); }); let table2 = table.clone(); - let t2 = intf::spawn(async move { + let t2 = spawn(async move { // lock the tag let _g = table2.lock_tag(a1).await; info!("locked t2"); // hold the guard for a bit info!("waiting t2"); - intf::sleep(500).await; + sleep(500).await; // release the guard info!("released t2"); }); let table3 = table.clone(); - let t3 = intf::spawn(async move { + let t3 = spawn(async move { // lock the tag let _g = table3.lock_tag(a1).await; info!("locked t3"); // hold the guard for a bit info!("waiting t3"); - intf::sleep(500).await; + sleep(500).await; // release the guard info!("released t3"); }); diff --git a/veilid-core/src/tests/common/test_host_interface.rs b/veilid-core/src/tests/common/test_host_interface.rs index 6bad2e12..cc0f750e 100644 --- a/veilid-core/src/tests/common/test_host_interface.rs +++ b/veilid-core/src/tests/common/test_host_interface.rs @@ -15,8 +15,8 @@ pub async fn test_log() { pub async fn test_get_timestamp() { info!("testing get_timestamp"); - let t1 = intf::get_timestamp(); - let t2 = intf::get_timestamp(); + let t1 = get_timestamp(); + let t2 = get_timestamp(); assert!(t2 >= t1); } @@ -31,8 +31,8 @@ pub async fn test_eventual() { let i4 = e1.instance_clone(4u32); drop(i2); - let jh = intf::spawn(async move { - intf::sleep(1000).await; + let jh = spawn(async move { + sleep(1000).await; e1.resolve(); }); @@ -48,14 +48,14 @@ pub async fn test_eventual() { let i3 = e1.instance_clone(3u32); let i4 = e1.instance_clone(4u32); let e1_c1 = e1.clone(); - let jh = intf::spawn(async move { + let jh = spawn(async move { let i5 = e1.instance_clone(5u32); let i6 = e1.instance_clone(6u32); assert_eq!(i1.await, 1u32); assert_eq!(i5.await, 5u32); assert_eq!(i6.await, 6u32); }); - intf::sleep(1000).await; + sleep(1000).await; let resolved = e1_c1.resolve(); drop(i2); drop(i3); @@ -68,11 +68,11 @@ pub async fn test_eventual() { let i1 = e1.instance_clone(1u32); let i2 = e1.instance_clone(2u32); let e1_c1 = e1.clone(); - let jh = intf::spawn(async move { + let jh = spawn(async move { assert_eq!(i1.await, 1u32); assert_eq!(i2.await, 2u32); }); - intf::sleep(1000).await; + sleep(1000).await; e1_c1.resolve().await; jh.await; @@ -81,11 +81,11 @@ pub async fn test_eventual() { // let j1 = e1.instance_clone(1u32); let j2 = e1.instance_clone(2u32); - let jh = intf::spawn(async move { + let jh = spawn(async move { assert_eq!(j1.await, 1u32); assert_eq!(j2.await, 2u32); }); - intf::sleep(1000).await; + sleep(1000).await; e1_c1.resolve().await; jh.await; @@ -106,8 +106,8 @@ pub async fn test_eventual_value() { drop(i2); let e1_c1 = e1.clone(); - let jh = intf::spawn(async move { - intf::sleep(1000).await; + let jh = spawn(async move { + sleep(1000).await; e1_c1.resolve(3u32); }); @@ -123,14 +123,14 @@ pub async fn test_eventual_value() { let i3 = e1.instance(); let i4 = e1.instance(); let e1_c1 = e1.clone(); - let jh = intf::spawn(async move { + let jh = spawn(async move { let i5 = e1.instance(); let i6 = e1.instance(); i1.await; i5.await; i6.await; }); - intf::sleep(1000).await; + sleep(1000).await; let resolved = e1_c1.resolve(4u16); drop(i2); drop(i3); @@ -145,11 +145,11 @@ pub async fn test_eventual_value() { let i1 = e1.instance(); let i2 = e1.instance(); let e1_c1 = e1.clone(); - let jh = intf::spawn(async move { + let jh = spawn(async move { i1.await; i2.await; }); - intf::sleep(1000).await; + sleep(1000).await; e1_c1.resolve(5u32).await; jh.await; assert_eq!(e1_c1.take_value(), Some(5u32)); @@ -158,11 +158,11 @@ pub async fn test_eventual_value() { // let j1 = e1.instance(); let j2 = e1.instance(); - let jh = intf::spawn(async move { + let jh = spawn(async move { j1.await; j2.await; }); - intf::sleep(1000).await; + sleep(1000).await; e1_c1.resolve(6u32).await; jh.await; assert_eq!(e1_c1.take_value(), Some(6u32)); @@ -182,8 +182,8 @@ pub async fn test_eventual_value_clone() { let i4 = e1.instance(); drop(i2); - let jh = intf::spawn(async move { - intf::sleep(1000).await; + let jh = spawn(async move { + sleep(1000).await; e1.resolve(3u32); }); @@ -200,14 +200,14 @@ pub async fn test_eventual_value_clone() { let i3 = e1.instance(); let i4 = e1.instance(); let e1_c1 = e1.clone(); - let jh = intf::spawn(async move { + let jh = spawn(async move { let i5 = e1.instance(); let i6 = e1.instance(); assert_eq!(i1.await, 4); assert_eq!(i5.await, 4); assert_eq!(i6.await, 4); }); - intf::sleep(1000).await; + sleep(1000).await; let resolved = e1_c1.resolve(4u16); drop(i2); drop(i3); @@ -221,22 +221,22 @@ pub async fn test_eventual_value_clone() { let i1 = e1.instance(); let i2 = e1.instance(); let e1_c1 = e1.clone(); - let jh = intf::spawn(async move { + let jh = spawn(async move { assert_eq!(i1.await, 5); assert_eq!(i2.await, 5); }); - intf::sleep(1000).await; + sleep(1000).await; e1_c1.resolve(5u32).await; jh.await; e1_c1.reset(); // let j1 = e1.instance(); let j2 = e1.instance(); - let jh = intf::spawn(async move { + let jh = spawn(async move { assert_eq!(j1.await, 6); assert_eq!(j2.await, 6); }); - intf::sleep(1000).await; + sleep(1000).await; e1_c1.resolve(6u32).await; jh.await; e1_c1.reset(); @@ -246,7 +246,7 @@ pub async fn test_interval() { info!("testing interval"); let tick: Arc> = Arc::new(Mutex::new(0u32)); - let stopper = intf::interval(1000, move || { + let stopper = interval(1000, move || { let tick = tick.clone(); async move { let mut tick = tick.lock(); @@ -255,7 +255,7 @@ pub async fn test_interval() { } }); - intf::sleep(5500).await; + sleep(5500).await; stopper.await; } @@ -266,19 +266,19 @@ pub async fn test_timeout() { let tick: Arc> = Arc::new(Mutex::new(0u32)); let tick_1 = tick.clone(); assert!( - intf::timeout(2500, async move { + timeout(2500, async move { let mut tick = tick_1.lock(); trace!("tick {}", tick); - intf::sleep(1000).await; + sleep(1000).await; *tick += 1; trace!("tick {}", tick); - intf::sleep(1000).await; + sleep(1000).await; *tick += 1; trace!("tick {}", tick); - intf::sleep(1000).await; + sleep(1000).await; *tick += 1; trace!("tick {}", tick); - intf::sleep(1000).await; + sleep(1000).await; *tick += 1; }) .await @@ -305,7 +305,7 @@ pub async fn test_sleep() { let sys_time = SystemTime::now(); let one_sec = Duration::from_secs(1); - intf::sleep(1000).await; + sleep(1000).await; assert!(sys_time.elapsed().unwrap() >= one_sec); } } @@ -462,7 +462,7 @@ cfg_if! { if #[cfg(not(target_arch = "wasm32"))] { pub async fn test_network_interfaces() { info!("testing network interfaces"); - let t1 = intf::get_timestamp(); + let t1 = get_timestamp(); let interfaces = intf::utils::network_interfaces::NetworkInterfaces::new(); let count = 100; for x in 0..count { @@ -471,7 +471,7 @@ cfg_if! { error!("error refreshing interfaces: {}", e); } } - let t2 = intf::get_timestamp(); + let t2 = get_timestamp(); let tdiff = ((t2 - t1) as f64)/1000000.0f64; info!("running network interface test with {} iterations took {} seconds", count, tdiff); info!("interfaces: {:#?}", interfaces) @@ -481,12 +481,12 @@ cfg_if! { pub async fn test_get_random_u64() { info!("testing random number generator for u64"); - let t1 = intf::get_timestamp(); + let t1 = get_timestamp(); let count = 10000; for _ in 0..count { - let _ = intf::get_random_u64(); + let _ = get_random_u64(); } - let t2 = intf::get_timestamp(); + let t2 = get_timestamp(); let tdiff = ((t2 - t1) as f64) / 1000000.0f64; info!( "running network interface test with {} iterations took {} seconds", @@ -496,12 +496,12 @@ pub async fn test_get_random_u64() { pub async fn test_get_random_u32() { info!("testing random number generator for u32"); - let t1 = intf::get_timestamp(); + let t1 = get_timestamp(); let count = 10000; for _ in 0..count { - let _ = intf::get_random_u32(); + let _ = get_random_u32(); } - let t2 = intf::get_timestamp(); + let t2 = get_timestamp(); let tdiff = ((t2 - t1) as f64) / 1000000.0f64; info!( "running network interface test with {} iterations took {} seconds", @@ -515,7 +515,7 @@ pub async fn test_must_join_single_future() { assert_eq!(sf.check().await, Ok(None)); assert_eq!( sf.single_spawn(async { - intf::sleep(2000).await; + sleep(2000).await; 69 }) .await, @@ -526,22 +526,22 @@ pub async fn test_must_join_single_future() { assert_eq!(sf.join().await, Ok(Some(69))); assert_eq!( sf.single_spawn(async { - intf::sleep(1000).await; + sleep(1000).await; 37 }) .await, Ok((None, true)) ); - intf::sleep(2000).await; + sleep(2000).await; assert_eq!( sf.single_spawn(async { - intf::sleep(1000).await; + sleep(1000).await; 27 }) .await, Ok((Some(37), true)) ); - intf::sleep(2000).await; + sleep(2000).await; assert_eq!(sf.join().await, Ok(Some(27))); assert_eq!(sf.check().await, Ok(None)); } diff --git a/veilid-core/src/tests/common/test_veilid_config.rs b/veilid-core/src/tests/common/test_veilid_config.rs index e73e79a1..c5c6f56f 100644 --- a/veilid-core/src/tests/common/test_veilid_config.rs +++ b/veilid-core/src/tests/common/test_veilid_config.rs @@ -222,7 +222,6 @@ fn config_callback(key: String) -> ConfigCallbackReturn { "network.dht.min_peer_refresh_time_ms" => Ok(Box::new(2_000u32)), "network.dht.validate_dial_info_receipt_time_ms" => Ok(Box::new(5_000u32)), "network.upnp" => Ok(Box::new(false)), - "network.natpmp" => Ok(Box::new(false)), "network.detect_address_changes" => Ok(Box::new(true)), "network.restricted_nat_retries" => Ok(Box::new(3u32)), "network.tls.certificate_path" => Ok(Box::new(get_certfile_path())), @@ -352,7 +351,6 @@ pub async fn test_config() { ); assert_eq!(inner.network.upnp, false); - assert_eq!(inner.network.natpmp, false); assert_eq!(inner.network.detect_address_changes, true); assert_eq!(inner.network.restricted_nat_retries, 3u32); assert_eq!(inner.network.tls.certificate_path, get_certfile_path()); diff --git a/veilid-core/src/tests/common/test_veilid_core.rs b/veilid-core/src/tests/common/test_veilid_core.rs index ed31f2a6..e3ba0670 100644 --- a/veilid-core/src/tests/common/test_veilid_core.rs +++ b/veilid-core/src/tests/common/test_veilid_core.rs @@ -20,9 +20,9 @@ pub async fn test_attach_detach() { .await .expect("startup failed"); api.attach().await.unwrap(); - intf::sleep(5000).await; + sleep(5000).await; api.detach().await.unwrap(); - intf::sleep(2000).await; + sleep(2000).await; api.shutdown().await; info!("--- test auto detach ---"); @@ -31,7 +31,7 @@ pub async fn test_attach_detach() { .await .expect("startup failed"); api.attach().await.unwrap(); - intf::sleep(5000).await; + sleep(5000).await; api.shutdown().await; info!("--- test detach without attach ---"); diff --git a/veilid-core/src/veilid_api/api.rs b/veilid-core/src/veilid_api/api.rs index 2028023e..4ef5e5e6 100644 --- a/veilid-core/src/veilid_api/api.rs +++ b/veilid-core/src/veilid_api/api.rs @@ -15,7 +15,7 @@ impl fmt::Debug for VeilidAPIInner { impl Drop for VeilidAPIInner { fn drop(&mut self) { if let Some(context) = self.context.take() { - intf::spawn_detached(api_shutdown(context)); + spawn_detached(api_shutdown(context)); } } } diff --git a/veilid-core/src/veilid_api/types.rs b/veilid-core/src/veilid_api/types.rs index e8bb0e96..baa3c9cb 100644 --- a/veilid-core/src/veilid_api/types.rs +++ b/veilid-core/src/veilid_api/types.rs @@ -1826,7 +1826,7 @@ impl SignedDirectNodeInfo { node_info: NodeInfo, secret: &DHTKeySecret, ) -> Result { - let timestamp = intf::get_timestamp(); + let timestamp = get_timestamp(); let node_info_bytes = Self::make_signature_bytes(&node_info, timestamp)?; let signature = sign(&node_id.key, secret, &node_info_bytes)?; Ok(Self { @@ -1858,7 +1858,7 @@ impl SignedDirectNodeInfo { Self { node_info, signature: None, - timestamp: intf::get_timestamp(), + timestamp: get_timestamp(), } } @@ -1906,7 +1906,7 @@ impl SignedRelayedNodeInfo { relay_info: SignedDirectNodeInfo, secret: &DHTKeySecret, ) -> Result { - let timestamp = intf::get_timestamp(); + let timestamp = get_timestamp(); let node_info_bytes = Self::make_signature_bytes(&node_info, &relay_id, &relay_info, timestamp)?; let signature = sign(&node_id.key, secret, &node_info_bytes)?; diff --git a/veilid-core/src/veilid_config.rs b/veilid-core/src/veilid_config.rs index 3a33fc0e..c1469dfa 100644 --- a/veilid-core/src/veilid_config.rs +++ b/veilid-core/src/veilid_config.rs @@ -369,7 +369,6 @@ pub struct VeilidConfigNetwork { pub rpc: VeilidConfigRPC, pub dht: VeilidConfigDHT, pub upnp: bool, - pub natpmp: bool, pub detect_address_changes: bool, pub restricted_nat_retries: u32, pub tls: VeilidConfigTLS, @@ -665,7 +664,6 @@ impl VeilidConfig { get_config!(inner.network.rpc.max_route_hop_count); get_config!(inner.network.rpc.default_route_hop_count); get_config!(inner.network.upnp); - get_config!(inner.network.natpmp); get_config!(inner.network.detect_address_changes); get_config!(inner.network.restricted_nat_retries); get_config!(inner.network.tls.certificate_path); diff --git a/veilid-core/src/veilid_rng.rs b/veilid-core/src/veilid_rng.rs deleted file mode 100644 index 529d5550..00000000 --- a/veilid-core/src/veilid_rng.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::*; - -use rand::{CryptoRng, Error, RngCore}; - -#[derive(Clone, Copy, Debug, Default)] -pub struct VeilidRng; - -impl CryptoRng for VeilidRng {} - -impl RngCore for VeilidRng { - fn next_u32(&mut self) -> u32 { - intf::get_random_u32() - } - - fn next_u64(&mut self) -> u64 { - intf::get_random_u64() - } - - fn fill_bytes(&mut self, dest: &mut [u8]) { - if let Err(e) = self.try_fill_bytes(dest) { - panic!("Error: {}", e); - } - } - - fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> { - intf::random_bytes(dest).map_err(Error::new) - } -} diff --git a/veilid-flutter/example/lib/config.dart b/veilid-flutter/example/lib/config.dart index aaeea802..6f155825 100644 --- a/veilid-flutter/example/lib/config.dart +++ b/veilid-flutter/example/lib/config.dart @@ -84,7 +84,6 @@ Future getDefaultVeilidConfig() async { validateDialInfoReceiptTimeMs: 2000, ), upnp: true, - natpmp: true, detectAddressChanges: true, restrictedNatRetries: 0, tls: VeilidConfigTLS( diff --git a/veilid-flutter/lib/veilid.dart b/veilid-flutter/lib/veilid.dart index 196df95d..219dad64 100644 --- a/veilid-flutter/lib/veilid.dart +++ b/veilid-flutter/lib/veilid.dart @@ -745,7 +745,6 @@ class VeilidConfigNetwork { VeilidConfigRPC rpc; VeilidConfigDHT dht; bool upnp; - bool natpmp; bool detectAddressChanges; int restrictedNatRetries; VeilidConfigTLS tls; @@ -770,7 +769,6 @@ class VeilidConfigNetwork { required this.rpc, required this.dht, required this.upnp, - required this.natpmp, required this.detectAddressChanges, required this.restrictedNatRetries, required this.tls, @@ -797,7 +795,6 @@ class VeilidConfigNetwork { 'rpc': rpc.json, 'dht': dht.json, 'upnp': upnp, - 'natpmp': natpmp, 'detect_address_changes': detectAddressChanges, 'restricted_nat_retries': restrictedNatRetries, 'tls': tls.json, @@ -827,7 +824,6 @@ class VeilidConfigNetwork { rpc = VeilidConfigRPC.fromJson(json['rpc']), dht = VeilidConfigDHT.fromJson(json['dht']), upnp = json['upnp'], - natpmp = json['natpmp'], detectAddressChanges = json['detect_address_changes'], restrictedNatRetries = json['restricted_nat_retries'], tls = VeilidConfigTLS.fromJson(json['tls']), diff --git a/veilid-server/src/settings.rs b/veilid-server/src/settings.rs index 203076cd..1636acc0 100644 --- a/veilid-server/src/settings.rs +++ b/veilid-server/src/settings.rs @@ -100,7 +100,6 @@ core: min_peer_refresh_time_ms: 2000 validate_dial_info_receipt_time_ms: 2000 upnp: true - natpmp: false detect_address_changes: true restricted_nat_retries: 0 tls: @@ -607,7 +606,6 @@ pub struct Network { pub rpc: Rpc, pub dht: Dht, pub upnp: bool, - pub natpmp: bool, pub detect_address_changes: bool, pub restricted_nat_retries: u32, pub tls: Tls, @@ -1005,7 +1003,6 @@ impl Settings { value ); set_config_value!(inner.core.network.upnp, value); - set_config_value!(inner.core.network.natpmp, value); set_config_value!(inner.core.network.detect_address_changes, value); set_config_value!(inner.core.network.restricted_nat_retries, value); set_config_value!(inner.core.network.tls.certificate_path, value); @@ -1206,7 +1203,6 @@ impl Settings { inner.core.network.dht.validate_dial_info_receipt_time_ms, )), "network.upnp" => Ok(Box::new(inner.core.network.upnp)), - "network.natpmp" => Ok(Box::new(inner.core.network.natpmp)), "network.detect_address_changes" => { Ok(Box::new(inner.core.network.detect_address_changes)) } @@ -1530,7 +1526,6 @@ mod tests { ); // assert_eq!(s.core.network.upnp, true); - assert_eq!(s.core.network.natpmp, false); assert_eq!(s.core.network.detect_address_changes, true); assert_eq!(s.core.network.restricted_nat_retries, 0u32); // diff --git a/veilid-tools/Cargo.toml b/veilid-tools/Cargo.toml new file mode 100644 index 00000000..3e0ebf31 --- /dev/null +++ b/veilid-tools/Cargo.toml @@ -0,0 +1,179 @@ +[package] +name = "veilid-core" +version = "0.1.0" +authors = ["John Smith "] +edition = "2021" +build = "build.rs" +license = "LGPL-2.0-or-later OR MPL-2.0 OR (MIT AND BSD-3-Clause)" + +[lib] +crate-type = ["cdylib", "staticlib", "rlib"] + +[features] +default = [] +rt-async-std = [ "async-std", "async-std-resolver", "async_executors/async_std", "rtnetlink?/smol_socket" ] +rt-tokio = [ "tokio", "tokio-util", "tokio-stream", "trust-dns-resolver/tokio-runtime", "async_executors/tokio_tp", "async_executors/tokio_io", "async_executors/tokio_timer", "rtnetlink?/tokio_socket" ] + +android_tests = [] +ios_tests = [ "simplelog" ] +tracking = [] + +[dependencies] +tracing = { version = "^0", features = ["log", "attributes"] } +tracing-subscriber = "^0" +tracing-error = "^0" +eyre = "^0" +capnp = { version = "^0", default_features = false } +rust-fsm = "^0" +static_assertions = "^1" +cfg-if = "^1" +thiserror = "^1" +hex = "^0" +generic-array = "^0" +secrecy = "^0" +chacha20poly1305 = "^0" +chacha20 = "^0" +hashlink = { path = "../external/hashlink", features = ["serde_impl"] } +serde = { version = "^1", features = ["derive" ] } +serde_json = { version = "^1" } +serde-big-array = "^0" +futures-util = { version = "^0", default_features = false, features = ["alloc"] } +parking_lot = "^0" +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" } +owo-colors = "^3" +stop-token = { version = "^0", default-features = false } +ed25519-dalek = { version = "^1", default_features = false, features = ["alloc", "u64_backend"] } +x25519-dalek = { package = "x25519-dalek-ng", version = "^1", default_features = false, features = ["u64_backend"] } +curve25519-dalek = { package = "curve25519-dalek-ng", version = "^4", default_features = false, features = ["alloc", "u64_backend"] } +# ed25519-dalek needs rand 0.7 until it updates itself +rand = "0.7" +# curve25519-dalek-ng is stuck on digest 0.9.0 +blake3 = { version = "1.1.0", default_features = false } +digest = "0.9.0" +rtnetlink = { version = "^0", default-features = false, optional = true } +async-std-resolver = { version = "^0", optional = true } +trust-dns-resolver = { version = "^0", optional = true } +keyvaluedb = { path = "../external/keyvaluedb/keyvaluedb" } +#rkyv = { version = "^0", default_features = false, features = ["std", "alloc", "strict", "size_32", "validation"] } +rkyv = { git = "https://github.com/rkyv/rkyv.git", rev = "57e2a8d", default_features = false, features = ["std", "alloc", "strict", "size_32", "validation"] } +bytecheck = "^0" + +# Dependencies for native builds only +# Linux, Windows, Mac, iOS, Android +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +async-std = { version = "^1", features = ["unstable"], optional = true} +tokio = { version = "^1", features = ["full"], optional = true} +tokio-util = { version = "^0", features = ["compat"], optional = true} +tokio-stream = { version = "^0", features = ["net"], optional = true} +async-io = { version = "^1" } +async-tungstenite = { version = "^0", features = ["async-tls"] } +maplit = "^1" +config = { version = "^0", features = ["yaml"] } +keyring-manager = { path = "../external/keyring-manager" } +async-tls = "^0.11" +igd = { path = "../external/rust-igd" } +webpki = "^0" +webpki-roots = "^0" +rustls = "^0.19" +rustls-pemfile = "^0.2" +futures-util = { version = "^0", default-features = false, features = ["async-await", "sink", "std", "io"] } +keyvaluedb-sqlite = { path = "../external/keyvaluedb/keyvaluedb-sqlite" } +data-encoding = { version = "^2" } + +socket2 = "^0" +bugsalot = "^0" +chrono = "^0" +libc = "^0" +nix = "^0" + +# Dependencies for WASM builds only +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "^0" +js-sys = "^0" +wasm-bindgen-futures = "^0" +keyvaluedb-web = { path = "../external/keyvaluedb/keyvaluedb-web" } +data-encoding = { version = "^2", default_features = false, features = ["alloc"] } +getrandom = { version = "^0", features = ["js"] } +ws_stream_wasm = "^0" +async_executors = { version = "^0", default-features = false, features = [ "bindgen", "timer" ]} +async-lock = "^2" +send_wrapper = { version = "^0", features = ["futures"] } +wasm-logger = "^0" +tracing-wasm = "^0" + +# Configuration for WASM32 'web-sys' crate +[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] +version = "^0" +features = [ + 'Document', + 'HtmlDocument', + # 'Element', + # 'HtmlElement', + # 'Node', + 'IdbFactory', + 'IdbOpenDbRequest', + 'Storage', + 'Location', + 'Window', +] + +# Dependencies for Android +[target.'cfg(target_os = "android")'.dependencies] +jni = "^0" +jni-sys = "^0" +ndk = { version = "^0", features = ["trace"] } +ndk-glue = { version = "^0", features = ["logger"] } +tracing-android = { version = "^0" } + +# Dependenices for all Unix (Linux, Android, MacOS, iOS) +[target.'cfg(unix)'.dependencies] +ifstructs = "^0" + +# Dependencies for Linux or Android +[target.'cfg(any(target_os = "android",target_os = "linux"))'.dependencies] +rtnetlink = { version = "^0", default-features = false } + +# Dependencies for Windows +[target.'cfg(target_os = "windows")'.dependencies] +winapi = { version = "^0", features = [ "iptypes", "iphlpapi" ] } +windows = { version = "^0", features = [ "Win32_NetworkManagement_Dns", "Win32_Foundation", "alloc" ]} +windows-permissions = "^0" + +# Dependencies for iOS +[target.'cfg(target_os = "ios")'.dependencies] +simplelog = { version = "^0", optional = true } + +# Rusqlite configuration to ensure platforms that don't come with sqlite get it bundled +# Except WASM which doesn't use sqlite +[target.'cfg(all(not(target_os = "ios"),not(target_os = "android"),not(target_arch = "wasm32")))'.dependencies.rusqlite] +version = "^0" +features = ["bundled"] + +### DEV DEPENDENCIES + +[dev-dependencies] +serial_test = "^0" + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +simplelog = { version = "^0", features=["test"] } + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wasm-bindgen-test = "^0" +console_error_panic_hook = "^0" +wee_alloc = "^0" +wasm-logger = "^0" + +### BUILD OPTIONS + +[build-dependencies] +capnpc = "^0" + +[package.metadata.wasm-pack.profile.release] +wasm-opt = ["-O", "--enable-mutable-globals"] diff --git a/veilid-tools/ios_build.sh b/veilid-tools/ios_build.sh new file mode 100755 index 00000000..4eb08eca --- /dev/null +++ b/veilid-tools/ios_build.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +CARGO_MANIFEST_PATH=$(python -c "import os; print(os.path.realpath(\"$SCRIPTDIR/Cargo.toml\"))") +# echo CARGO_MANIFEST_PATH: $CARGO_MANIFEST_PATH + +if [ "$CONFIGURATION" == "Debug" ]; then + EXTRA_CARGO_OPTIONS="$@" +else + EXTRA_CARGO_OPTIONS="$@ --release" +fi +ARCHS=${ARCHS:=arm64} +for arch in $ARCHS +do + if [ "$arch" == "arm64" ]; then + echo arm64 + CARGO_TARGET=aarch64-apple-ios + #CARGO_TOOLCHAIN=+ios-arm64-1.57.0 + CARGO_TOOLCHAIN= + elif [ "$arch" == "x86_64" ]; then + echo x86_64 + CARGO_TARGET=x86_64-apple-ios + CARGO_TOOLCHAIN= + else + echo Unsupported ARCH: $arch + continue + fi + + CARGO=`which cargo` + CARGO=${CARGO:=~/.cargo/bin/cargo} + CARGO_DIR=$(dirname $CARGO) + + # Choose arm64 brew for unit tests by default if we are on M1 + if [ -f /opt/homebrew/bin/brew ]; then + HOMEBREW_DIR=/opt/homebrew/bin + elif [ -f /usr/local/bin/brew ]; then + HOMEBREW_DIR=/usr/local/bin + else + HOMEBREW_DIR=$(dirname `which brew`) + fi + + env -i PATH=/usr/bin:/bin:$HOMEBREW_DIR:$CARGO_DIR HOME="$HOME" USER="$USER" cargo $CARGO_TOOLCHAIN build $EXTRA_CARGO_OPTIONS --target $CARGO_TARGET --manifest-path $CARGO_MANIFEST_PATH +done + diff --git a/veilid-core/src/xx/async_peek_stream.rs b/veilid-tools/src/async_peek_stream.rs similarity index 99% rename from veilid-core/src/xx/async_peek_stream.rs rename to veilid-tools/src/async_peek_stream.rs index dd64ea26..26b15962 100644 --- a/veilid-core/src/xx/async_peek_stream.rs +++ b/veilid-tools/src/async_peek_stream.rs @@ -1,4 +1,5 @@ use super::*; + use std::io; use task::{Context, Poll}; diff --git a/veilid-core/src/xx/async_tag_lock.rs b/veilid-tools/src/async_tag_lock.rs similarity index 99% rename from veilid-core/src/xx/async_tag_lock.rs rename to veilid-tools/src/async_tag_lock.rs index 5f0623c1..7dcaec02 100644 --- a/veilid-core/src/xx/async_tag_lock.rs +++ b/veilid-tools/src/async_tag_lock.rs @@ -1,4 +1,5 @@ use super::*; + use core::fmt::Debug; use core::hash::Hash; diff --git a/veilid-core/src/xx/bump_port.rs b/veilid-tools/src/bump_port.rs similarity index 99% rename from veilid-core/src/xx/bump_port.rs rename to veilid-tools/src/bump_port.rs index 487b2d20..ebdd2628 100644 --- a/veilid-core/src/xx/bump_port.rs +++ b/veilid-tools/src/bump_port.rs @@ -1,4 +1,5 @@ use super::*; + cfg_if! { if #[cfg(target_arch = "wasm32")] { diff --git a/veilid-core/src/xx/clone_stream.rs b/veilid-tools/src/clone_stream.rs similarity index 99% rename from veilid-core/src/xx/clone_stream.rs rename to veilid-tools/src/clone_stream.rs index 3790966c..18508071 100644 --- a/veilid-core/src/xx/clone_stream.rs +++ b/veilid-tools/src/clone_stream.rs @@ -1,4 +1,5 @@ -use crate::xx::*; +use super::*; + use core::pin::Pin; use core::task::{Context, Poll}; use futures_util::AsyncRead as Read; diff --git a/veilid-core/src/xx/eventual.rs b/veilid-tools/src/eventual.rs similarity index 99% rename from veilid-core/src/xx/eventual.rs rename to veilid-tools/src/eventual.rs index 7883ad70..9ad0f6c0 100644 --- a/veilid-core/src/xx/eventual.rs +++ b/veilid-tools/src/eventual.rs @@ -1,4 +1,5 @@ use super::*; + use eventual_base::*; pub struct Eventual { diff --git a/veilid-core/src/xx/eventual_base.rs b/veilid-tools/src/eventual_base.rs similarity index 100% rename from veilid-core/src/xx/eventual_base.rs rename to veilid-tools/src/eventual_base.rs diff --git a/veilid-core/src/xx/eventual_value.rs b/veilid-tools/src/eventual_value.rs similarity index 99% rename from veilid-core/src/xx/eventual_value.rs rename to veilid-tools/src/eventual_value.rs index 2bdf2a43..16650f31 100644 --- a/veilid-core/src/xx/eventual_value.rs +++ b/veilid-tools/src/eventual_value.rs @@ -1,4 +1,5 @@ use super::*; + use eventual_base::*; pub struct EventualValue { diff --git a/veilid-core/src/xx/eventual_value_clone.rs b/veilid-tools/src/eventual_value_clone.rs similarity index 99% rename from veilid-core/src/xx/eventual_value_clone.rs rename to veilid-tools/src/eventual_value_clone.rs index b18c375c..fdaa9cf8 100644 --- a/veilid-core/src/xx/eventual_value_clone.rs +++ b/veilid-tools/src/eventual_value_clone.rs @@ -1,4 +1,5 @@ use super::*; + use eventual_base::*; pub struct EventualValueClone { diff --git a/veilid-tools/src/interval.rs b/veilid-tools/src/interval.rs new file mode 100644 index 00000000..1d9a0bee --- /dev/null +++ b/veilid-tools/src/interval.rs @@ -0,0 +1,49 @@ +use super::*; + +cfg_if! { + if #[cfg(target_arch = "wasm32")] { + + pub fn interval(freq_ms: u32, callback: F) -> SendPinBoxFuture<()> + where + F: Fn() -> FUT + Send + Sync + 'static, + FUT: Future + Send, + { + let e = Eventual::new(); + + let ie = e.clone(); + let jh = spawn(Box::pin(async move { + while timeout(freq_ms, ie.instance_clone(())).await.is_err() { + callback().await; + } + })); + + Box::pin(async move { + e.resolve().await; + jh.await; + }) + } + + } else { + + pub fn interval(freq_ms: u32, callback: F) -> SendPinBoxFuture<()> + where + F: Fn() -> FUT + Send + Sync + 'static, + FUT: Future + Send, + { + let e = Eventual::new(); + + let ie = e.clone(); + let jh = spawn(async move { + while timeout(freq_ms, ie.instance_clone(())).await.is_err() { + callback().await; + } + }); + + Box::pin(async move { + e.resolve().await; + jh.await; + }) + } + + } +} diff --git a/veilid-core/src/xx/ip_addr_port.rs b/veilid-tools/src/ip_addr_port.rs similarity index 99% rename from veilid-core/src/xx/ip_addr_port.rs rename to veilid-tools/src/ip_addr_port.rs index e87d1a2e..118c588a 100644 --- a/veilid-core/src/xx/ip_addr_port.rs +++ b/veilid-tools/src/ip_addr_port.rs @@ -1,4 +1,5 @@ use super::*; + use core::fmt; #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)] pub struct IpAddrPort { diff --git a/veilid-core/src/xx/ip_extra.rs b/veilid-tools/src/ip_extra.rs similarity index 99% rename from veilid-core/src/xx/ip_extra.rs rename to veilid-tools/src/ip_extra.rs index 5328359d..edbce74b 100644 --- a/veilid-core/src/xx/ip_extra.rs +++ b/veilid-tools/src/ip_extra.rs @@ -2,7 +2,8 @@ // This file really shouldn't be necessary, but 'ip' isn't a stable feature // -use crate::xx::*; +use super::*; + use core::hash::*; #[derive(Copy, PartialEq, Eq, Clone, Hash, Debug)] diff --git a/veilid-core/src/xx/log_thru.rs b/veilid-tools/src/log_thru.rs similarity index 99% rename from veilid-core/src/xx/log_thru.rs rename to veilid-tools/src/log_thru.rs index feedfcbf..b6fecc01 100644 --- a/veilid-core/src/xx/log_thru.rs +++ b/veilid-tools/src/log_thru.rs @@ -2,7 +2,7 @@ // Pass errors through and log them simultaneously via map_err() // Also contains common log facilities (net, rpc, rtab, pstore, crypto, etc ) -pub use alloc::string::{String, ToString}; +use alloc::string::{String, ToString}; pub fn map_to_string(arg: X) -> String { arg.to_string() diff --git a/veilid-core/src/xx/mod.rs b/veilid-tools/src/mod.rs similarity index 90% rename from veilid-core/src/xx/mod.rs rename to veilid-tools/src/mod.rs index 4169be90..15529449 100644 --- a/veilid-core/src/xx/mod.rs +++ b/veilid-tools/src/mod.rs @@ -6,6 +6,7 @@ mod eventual; mod eventual_base; mod eventual_value; mod eventual_value_clone; +mod interval; mod ip_addr_port; mod ip_extra; mod log_thru; @@ -13,11 +14,18 @@ mod must_join_handle; mod must_join_single_future; mod mutable_future; mod network_result; +mod random; mod single_shot_eventual; +mod sleep; +mod spawn; mod split_url; mod tick_task; +mod timeout; mod timeout_or; +mod timestamp; mod tools; +#[cfg(target_arch = "wasm32")] +mod wasm; pub use cfg_if::*; #[allow(unused_imports)] @@ -33,8 +41,13 @@ pub use split_url::*; pub use static_assertions::*; pub use stop_token::*; pub use thiserror::Error as ThisError; -pub use tracing::*; - +cfg_if! { + if #[cfg(feature = "tracing")] { + pub use tracing::*; + } else { + pub use log::*; + } +} pub type PinBox = Pin>; pub type PinBoxFuture = PinBox + 'static>; pub type PinBoxFutureLifetime<'a, T> = PinBox + 'a>; @@ -70,8 +83,6 @@ pub use std::vec::Vec; cfg_if! { if #[cfg(target_arch = "wasm32")] { - pub use wasm_bindgen::prelude::*; - pub use async_lock::Mutex as AsyncMutex; pub use async_lock::MutexGuard as AsyncMutexGuard; pub use async_lock::MutexGuardArc as AsyncMutexGuardArc; @@ -103,13 +114,21 @@ pub use eventual::*; pub use eventual_base::{EventualCommon, EventualResolvedFuture}; pub use eventual_value::*; pub use eventual_value_clone::*; +pub use interval::*; pub use ip_addr_port::*; pub use ip_extra::*; pub use must_join_handle::*; pub use must_join_single_future::*; pub use mutable_future::*; pub use network_result::*; +pub use random::*; pub use single_shot_eventual::*; +pub use sleep::*; +pub use spawn::*; pub use tick_task::*; +pub use timeout::*; pub use timeout_or::*; +pub use timestamp::*; pub use tools::*; +#[cfg(target_arch = "wasm32")] +pub use wasm::*; diff --git a/veilid-core/src/xx/must_join_handle.rs b/veilid-tools/src/must_join_handle.rs similarity index 98% rename from veilid-core/src/xx/must_join_handle.rs rename to veilid-tools/src/must_join_handle.rs index 0f90de3a..a9de40b7 100644 --- a/veilid-core/src/xx/must_join_handle.rs +++ b/veilid-tools/src/must_join_handle.rs @@ -1,6 +1,5 @@ use super::*; -use core::future::Future; -use core::pin::Pin; + use core::task::{Context, Poll}; #[derive(Debug)] diff --git a/veilid-core/src/xx/must_join_single_future.rs b/veilid-tools/src/must_join_single_future.rs similarity index 97% rename from veilid-core/src/xx/must_join_single_future.rs rename to veilid-tools/src/must_join_single_future.rs index ffebeaae..42663ad4 100644 --- a/veilid-core/src/xx/must_join_single_future.rs +++ b/veilid-tools/src/must_join_single_future.rs @@ -1,5 +1,5 @@ use super::*; -use crate::*; + use core::task::Poll; use futures_util::poll; @@ -157,7 +157,7 @@ where // Run if we should do that if run { - self.unlock(Some(intf::spawn_local(future))); + self.unlock(Some(spawn_local(future))); } // Return the prior result if we have one @@ -197,7 +197,7 @@ where } // Run if we should do that if run { - self.unlock(Some(intf::spawn(future))); + self.unlock(Some(spawn(future))); } // Return the prior result if we have one Ok((out, run)) diff --git a/veilid-core/src/xx/mutable_future.rs b/veilid-tools/src/mutable_future.rs similarity index 100% rename from veilid-core/src/xx/mutable_future.rs rename to veilid-tools/src/mutable_future.rs diff --git a/veilid-core/src/xx/network_result.rs b/veilid-tools/src/network_result.rs similarity index 99% rename from veilid-core/src/xx/network_result.rs rename to veilid-tools/src/network_result.rs index dd50d33c..306f9ffd 100644 --- a/veilid-core/src/xx/network_result.rs +++ b/veilid-tools/src/network_result.rs @@ -1,4 +1,5 @@ use super::*; + use core::fmt::{Debug, Display}; use core::result::Result; use std::error::Error; diff --git a/veilid-tools/src/random.rs b/veilid-tools/src/random.rs new file mode 100644 index 00000000..2f395b55 --- /dev/null +++ b/veilid-tools/src/random.rs @@ -0,0 +1,81 @@ +use super::*; +use rand::prelude::*; + +#[derive(Clone, Copy, Debug, Default)] +pub struct VeilidRng; + +impl CryptoRng for VeilidRng {} + +impl RngCore for VeilidRng { + fn next_u32(&mut self) -> u32 { + get_random_u32() + } + + fn next_u64(&mut self) -> u64 { + get_random_u64() + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + if let Err(e) = self.try_fill_bytes(dest) { + panic!("Error: {}", e); + } + } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand::Error> { + random_bytes(dest).map_err(rand::Error::new) + } +} + +cfg_if! { + if #[cfg(target_arch = "wasm32")] { + pub fn random_bytes(dest: &mut [u8]) -> EyreResult<()> { + let len = dest.len(); + let u32len = len / 4; + let remlen = len % 4; + + for n in 0..u32len { + let r = (Math::random() * (u32::max_value() as f64)) as u32; + + dest[n * 4 + 0] = (r & 0xFF) as u8; + dest[n * 4 + 1] = ((r >> 8) & 0xFF) as u8; + dest[n * 4 + 2] = ((r >> 16) & 0xFF) as u8; + dest[n * 4 + 3] = ((r >> 24) & 0xFF) as u8; + } + if remlen > 0 { + let r = (Math::random() * (u32::max_value() as f64)) as u32; + for n in 0..remlen { + dest[u32len * 4 + n] = ((r >> (n * 8)) & 0xFF) as u8; + } + } + + Ok(()) + } + + pub fn get_random_u32() -> u32 { + (Math::random() * (u32::max_value() as f64)) as u32 + } + + pub fn get_random_u64() -> u64 { + let v1: u32 = get_random_u32(); + let v2: u32 = get_random_u32(); + ((v1 as u64) << 32) | ((v2 as u32) as u64) + } + + } else { + + pub fn random_bytes(dest: &mut [u8]) -> EyreResult<()> { + let mut rng = rand::thread_rng(); + rng.try_fill_bytes(dest).wrap_err("failed to fill bytes") + } + + pub fn get_random_u32() -> u32 { + let mut rng = rand::thread_rng(); + rng.next_u32() + } + + pub fn get_random_u64() -> u64 { + let mut rng = rand::thread_rng(); + rng.next_u64() + } + } +} diff --git a/veilid-core/src/xx/single_shot_eventual.rs b/veilid-tools/src/single_shot_eventual.rs similarity index 100% rename from veilid-core/src/xx/single_shot_eventual.rs rename to veilid-tools/src/single_shot_eventual.rs diff --git a/veilid-tools/src/sleep.rs b/veilid-tools/src/sleep.rs new file mode 100644 index 00000000..c0d4a899 --- /dev/null +++ b/veilid-tools/src/sleep.rs @@ -0,0 +1,34 @@ +use super::*; +use std::time::Duration; + +cfg_if! { + if #[cfg(target_arch = "wasm32")] { + use async_executors::Bindgen; + + pub async fn sleep(millis: u32) { + Bindgen.sleep(Duration::from_millis(millis.into())).await + } + + } else { + + pub async fn sleep(millis: u32) { + if millis == 0 { + cfg_if! { + if #[cfg(feature="rt-async-std")] { + async_std::task::yield_now().await; + } else if #[cfg(feature="rt-tokio")] { + tokio::task::yield_now().await; + } + } + } else { + cfg_if! { + if #[cfg(feature="rt-async-std")] { + async_std::task::sleep(Duration::from_millis(u64::from(millis))).await; + } else if #[cfg(feature="rt-tokio")] { + tokio::time::sleep(Duration::from_millis(u64::from(millis))).await; + } + } + } + } + } +} diff --git a/veilid-tools/src/spawn.rs b/veilid-tools/src/spawn.rs new file mode 100644 index 00000000..0ee5497a --- /dev/null +++ b/veilid-tools/src/spawn.rs @@ -0,0 +1,119 @@ +use super::*; + +cfg_if! { + if #[cfg(target_arch = "wasm32")] { + use async_executors::{Bindgen, LocalSpawnHandleExt, SpawnHandleExt}; + + pub fn spawn(future: impl Future + Send + 'static) -> MustJoinHandle + where + Out: Send + 'static, + { + MustJoinHandle::new( + Bindgen + .spawn_handle(future) + .expect("wasm-bindgen-futures spawn_handle_local should never error out"), + ) + } + + pub fn spawn_local(future: impl Future + 'static) -> MustJoinHandle + where + Out: 'static, + { + MustJoinHandle::new( + Bindgen + .spawn_handle_local(future) + .expect("wasm-bindgen-futures spawn_handle_local should never error out"), + ) + } + + pub fn spawn_detached(future: impl Future + Send + 'static) + where + Out: Send + 'static, + { + Bindgen + .spawn_handle_local(future) + .expect("wasm-bindgen-futures spawn_handle_local should never error out") + .detach() + } + pub fn spawn_detached_local(future: impl Future + 'static) + where + Out: 'static, + { + Bindgen + .spawn_handle_local(future) + .expect("wasm-bindgen-futures spawn_handle_local should never error out") + .detach() + } + + } else { + + pub fn spawn(future: impl Future + Send + 'static) -> MustJoinHandle + where + Out: Send + 'static, + { + cfg_if! { + if #[cfg(feature="rt-async-std")] { + MustJoinHandle::new(async_std::task::spawn(future)) + } else if #[cfg(feature="rt-tokio")] { + MustJoinHandle::new(tokio::task::spawn(future)) + } + } + } + + pub fn spawn_local(future: impl Future + 'static) -> MustJoinHandle + where + Out: 'static, + { + cfg_if! { + if #[cfg(feature="rt-async-std")] { + MustJoinHandle::new(async_std::task::spawn_local(future)) + } else if #[cfg(feature="rt-tokio")] { + MustJoinHandle::new(tokio::task::spawn_local(future)) + } + } + } + + pub fn spawn_detached(future: impl Future + Send + 'static) + where + Out: Send + 'static, + { + cfg_if! { + if #[cfg(feature="rt-async-std")] { + drop(async_std::task::spawn(future)); + } else if #[cfg(feature="rt-tokio")] { + drop(tokio::task::spawn(future)); + } + } + } + + pub fn spawn_detached_local(future: impl Future + 'static) + where + Out: 'static, + { + cfg_if! { + if #[cfg(feature="rt-async-std")] { + drop(async_std::task::spawn_local(future)); + } else if #[cfg(feature="rt-tokio")] { + drop(tokio::task::spawn_local(future)); + } + } + } + + pub async fn blocking_wrapper(blocking_task: F, err_result: R) -> R + where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, + { + // run blocking stuff in blocking thread + cfg_if! { + if #[cfg(feature="rt-async-std")] { + async_std::task::spawn_blocking(blocking_task).await + } else if #[cfg(feature="rt-tokio")] { + tokio::task::spawn_blocking(blocking_task).await.unwrap_or(err_result) + } else { + #[compile_error("must use an executor")] + } + } + } + } +} diff --git a/veilid-core/src/xx/split_url.rs b/veilid-tools/src/split_url.rs similarity index 99% rename from veilid-core/src/xx/split_url.rs rename to veilid-tools/src/split_url.rs index 076f7212..718d4946 100644 --- a/veilid-core/src/xx/split_url.rs +++ b/veilid-tools/src/split_url.rs @@ -8,10 +8,7 @@ // Only IP address and DNS hostname host fields are supported use super::*; -use alloc::borrow::ToOwned; -use alloc::string::String; -use alloc::vec::Vec; -use core::fmt; + use core::str::FromStr; fn is_alphanum(c: u8) -> bool { diff --git a/veilid-core/src/xx/tick_task.rs b/veilid-tools/src/tick_task.rs similarity index 99% rename from veilid-core/src/xx/tick_task.rs rename to veilid-tools/src/tick_task.rs index 2d5d1e70..68ce4ddc 100644 --- a/veilid-core/src/xx/tick_task.rs +++ b/veilid-tools/src/tick_task.rs @@ -1,5 +1,5 @@ use super::*; -use crate::*; + use core::sync::atomic::{AtomicU64, Ordering}; use once_cell::sync::OnceCell; @@ -80,7 +80,7 @@ impl TickTask { } pub async fn tick(&self) -> Result<(), E> { - let now = intf::get_timestamp(); + let now = get_timestamp(); let last_timestamp_us = self.last_timestamp_us.load(Ordering::Acquire); if last_timestamp_us != 0u64 && now.saturating_sub(last_timestamp_us) < self.tick_period_us diff --git a/veilid-tools/src/timeout.rs b/veilid-tools/src/timeout.rs new file mode 100644 index 00000000..07858381 --- /dev/null +++ b/veilid-tools/src/timeout.rs @@ -0,0 +1,32 @@ +use super::*; + +cfg_if! { + if #[cfg(target_arch = "wasm32")] { + + pub async fn timeout(dur_ms: u32, f: F) -> Result + where + F: Future, + { + match select(Box::pin(intf::sleep(dur_ms)), Box::pin(f)).await { + Either::Left((_x, _b)) => Err(TimeoutError()), + Either::Right((y, _a)) => Ok(y), + } + } + + } else { + + pub async fn timeout(dur_ms: u32, f: F) -> Result + where + F: Future, + { + cfg_if! { + if #[cfg(feature="rt-async-std")] { + async_std::future::timeout(Duration::from_millis(dur_ms as u64), f).await.map_err(|e| e.into()) + } else if #[cfg(feature="rt-tokio")] { + tokio::time::timeout(Duration::from_millis(dur_ms as u64), f).await.map_err(|e| e.into()) + } + } + } + + } +} diff --git a/veilid-core/src/xx/timeout_or.rs b/veilid-tools/src/timeout_or.rs similarity index 99% rename from veilid-core/src/xx/timeout_or.rs rename to veilid-tools/src/timeout_or.rs index 62786c36..43463e32 100644 --- a/veilid-core/src/xx/timeout_or.rs +++ b/veilid-tools/src/timeout_or.rs @@ -1,5 +1,5 @@ use super::*; -use cfg_if::*; + use core::fmt::{Debug, Display}; use core::result::Result; use std::error::Error; diff --git a/veilid-tools/src/timestamp.rs b/veilid-tools/src/timestamp.rs new file mode 100644 index 00000000..af042327 --- /dev/null +++ b/veilid-tools/src/timestamp.rs @@ -0,0 +1,25 @@ +use super::*; + +cfg_if! { + if #[cfg(target_arch = "wasm32")] { + use js_sys::Date; + + pub fn get_timestamp() -> u64 { + if utils::is_browser() { + return (Date::now() * 1000.0f64) as u64; + } else { + panic!("WASM requires browser environment"); + } + } + } else { + use std::time::{SystemTime, UNIX_EPOCH}; + + pub fn get_timestamp() -> u64 { + match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(n) => n.as_micros() as u64, + Err(_) => panic!("SystemTime before UNIX_EPOCH!"), + } + } + + } +} diff --git a/veilid-core/src/xx/tools.rs b/veilid-tools/src/tools.rs similarity index 86% rename from veilid-core/src/xx/tools.rs rename to veilid-tools/src/tools.rs index 347de4f1..1758fdf2 100644 --- a/veilid-core/src/xx/tools.rs +++ b/veilid-tools/src/tools.rs @@ -1,8 +1,11 @@ -use crate::xx::*; +use super::*; + use alloc::string::ToString; use std::io; use std::path::Path; +////////////////////////////////////////////////////////////////////////////////////////////////////////////// + #[macro_export] macro_rules! assert_err { ($ex:expr) => { @@ -30,6 +33,40 @@ macro_rules! bail_io_error_other { }; } +////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +pub fn system_boxed<'a, Out>( + future: impl Future + Send + 'a, +) -> SendPinBoxFutureLifetime<'a, Out> { + Box::pin(future) +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +cfg_if! { + if #[cfg(target_arch = "wasm32")] { + + // xxx: for now until wasm threads are more stable, and/or we bother with web workers + pub fn get_concurrency() -> u32 { + 1 + } + + } else { + + pub fn get_concurrency() -> u32 { + std::thread::available_parallelism() + .map(|x| x.get()) + .unwrap_or_else(|e| { + warn!("unable to get concurrency defaulting to single core: {}", e); + 1 + }) as u32 + } + + } +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// + pub fn split_port(name: &str) -> EyreResult<(String, Option)> { if let Some(split) = name.rfind(':') { let hoststr = &name[0..split]; diff --git a/veilid-tools/src/wasm.rs b/veilid-tools/src/wasm.rs new file mode 100644 index 00000000..34408dbf --- /dev/null +++ b/veilid-tools/src/wasm.rs @@ -0,0 +1,52 @@ +use super::*; +use core::sync::atomic::{AtomicI8, Ordering}; +use js_sys::{global, Reflect}; + +#[wasm_bindgen] +extern "C" { + // Use `js_namespace` here to bind `console.log(..)` instead of just + // `log(..)` + #[wasm_bindgen(js_namespace = console, js_name = log)] + pub fn console_log(s: &str); + + #[wasm_bindgen] + pub fn alert(s: &str); +} + +pub fn is_browser() -> bool { + static CACHE: AtomicI8 = AtomicI8::new(-1); + let cache = CACHE.load(Ordering::Relaxed); + if cache != -1 { + return cache != 0; + } + + let res = Reflect::has(&global().as_ref(), &"window".into()).unwrap_or_default(); + + CACHE.store(res as i8, Ordering::Relaxed); + + res +} + +// pub fn is_browser_https() -> bool { +// static CACHE: AtomicI8 = AtomicI8::new(-1); +// let cache = CACHE.load(Ordering::Relaxed); +// if cache != -1 { +// return cache != 0; +// } + +// let res = js_sys::eval("window.location.protocol === 'https'") +// .map(|res| res.is_truthy()) +// .unwrap_or_default(); + +// CACHE.store(res as i8, Ordering::Relaxed); + +// res +// } + +#[derive(ThisError, Debug, Clone, Eq, PartialEq)] +#[error("JsValue error")] +pub struct JsValueError(String); + +pub fn map_jsvalue_error(x: JsValue) -> JsValueError { + JsValueError(x.as_string().unwrap_or_default()) +} diff --git a/veilid-wasm/tests/web.rs b/veilid-wasm/tests/web.rs index 8856b033..a986d126 100644 --- a/veilid-wasm/tests/web.rs +++ b/veilid-wasm/tests/web.rs @@ -61,7 +61,6 @@ fn init_callbacks() { case "network.dht.min_peer_refresh_time": return 2000000; case "network.dht.validate_dial_info_receipt_time": return 5000000; case "network.upnp": return false; - case "network.natpmp": return false; case "network.detect_address_changes": return true; case "network.address_filter": return true; case "network.restricted_nat_retries": return 3; From 503dbcf00470d1fdb9e28212e362e152cdf0741b Mon Sep 17 00:00:00 2001 From: John Smith Date: Sat, 26 Nov 2022 21:39:23 -0500 Subject: [PATCH 09/88] remove submodule --- external/no-std-net | 1 - 1 file changed, 1 deletion(-) delete mode 160000 external/no-std-net diff --git a/external/no-std-net b/external/no-std-net deleted file mode 160000 index db4af788..00000000 --- a/external/no-std-net +++ /dev/null @@ -1 +0,0 @@ -Subproject commit db4af788049b5073567a36cb2e7b0445af66ab1c From 07e3201e064ad88e7677575a3d7365dc76e1d45b Mon Sep 17 00:00:00 2001 From: John Smith Date: Sat, 26 Nov 2022 22:18:55 -0500 Subject: [PATCH 10/88] checkpoint --- Cargo.lock | 137 +++++++++++++++++- veilid-cli/src/command_processor.rs | 1 - veilid-core/Cargo.toml | 2 +- .../src/network_manager/native/igd_manager.rs | 1 + veilid-tools/Cargo.toml | 25 ++-- veilid-tools/src/{mod.rs => lib.rs} | 2 +- veilid-tools/src/log_thru.rs | 2 +- veilid-tools/src/tools.rs | 3 +- 8 files changed, 147 insertions(+), 26 deletions(-) rename veilid-tools/src/{mod.rs => lib.rs} (98%) diff --git a/Cargo.lock b/Cargo.lock index 76dbdfe4..8690c6fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -334,6 +334,18 @@ version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" +[[package]] +name = "async-tls" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7e7fbc0843fc5ad3d5ca889c5b2bea9130984d34cd0e62db57ab70c2529a8e3" +dependencies = [ + "futures", + "rustls 0.18.1", + "webpki 0.21.4", + "webpki-roots 0.20.0", +] + [[package]] name = "async-tls" version = "0.11.0" @@ -342,7 +354,7 @@ checksum = "2f23d769dbf1838d5df5156e7b1ad404f4c463d1ac2c6aeb6cd943630f8a8400" dependencies = [ "futures-core", "futures-io", - "rustls", + "rustls 0.19.1", "webpki 0.21.4", "webpki-roots 0.21.1", ] @@ -364,6 +376,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5c45a0dd44b7e6533ac4e7acc38ead1a3b39885f5bbb738140d30ea528abc7c" dependencies = [ + "async-tls 0.9.0", "futures-io", "futures-util", "log", @@ -377,7 +390,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b750efd83b7e716a015eed5ebb583cda83c52d9b24a8f0125e5c48c3313c9f8" dependencies = [ - "async-tls", + "async-tls 0.11.0", "futures-io", "futures-util", "log", @@ -4296,6 +4309,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustls" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d1126dcf58e93cee7d098dbda643b5f92ed724f1f6a63007c1116eed6700c81" +dependencies = [ + "base64 0.12.3", + "log", + "ring", + "sct", + "webpki 0.21.4", +] + [[package]] name = "rustls" version = "0.19.1" @@ -5562,7 +5588,7 @@ dependencies = [ "async-lock", "async-std", "async-std-resolver", - "async-tls", + "async-tls 0.11.0", "async-tungstenite 0.18.0", "async_executors", "backtrace", @@ -5615,7 +5641,7 @@ dependencies = [ "rtnetlink", "rusqlite", "rust-fsm", - "rustls", + "rustls 0.19.1", "rustls-pemfile", "secrecy", "send_wrapper 0.6.0", @@ -5637,6 +5663,7 @@ dependencies = [ "tracing-subscriber", "tracing-wasm", "trust-dns-resolver", + "veilid-tools", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", @@ -5732,6 +5759,99 @@ dependencies = [ "windows-service", ] +[[package]] +name = "veilid-tools" +version = "0.1.0" +dependencies = [ + "async-io", + "async-lock", + "async-std", + "async-std-resolver", + "async-tls 0.11.0", + "async-tungstenite 0.8.0", + "async_executors", + "backtrace", + "blake3", + "bugsalot", + "bytecheck", + "cfg-if 1.0.0", + "chacha20 0.8.2", + "chacha20poly1305", + "chrono", + "config", + "console_error_panic_hook", + "curve25519-dalek-ng", + "data-encoding", + "digest 0.9.0", + "directories", + "ed25519-dalek", + "enumset", + "eyre", + "flume", + "futures-util", + "generic-array", + "getrandom 0.2.8", + "hashlink 0.8.1", + "hex", + "ifstructs", + "igd", + "jni", + "jni-sys", + "js-sys", + "json", + "keyring-manager", + "keyvaluedb", + "keyvaluedb-sqlite", + "keyvaluedb-web", + "lazy_static", + "libc", + "log", + "maplit", + "ndk 0.6.0", + "ndk-glue", + "nix 0.22.3", + "once_cell", + "owning_ref", + "owo-colors", + "parking_lot 0.11.2", + "rand 0.7.3", + "rkyv", + "rtnetlink", + "rusqlite", + "rustls 0.19.1", + "rustls-pemfile", + "secrecy", + "send_wrapper 0.6.0", + "serde", + "serde-big-array", + "serde_json", + "serial_test", + "simplelog 0.9.0", + "socket2", + "static_assertions", + "stop-token", + "thiserror", + "tokio 1.22.0", + "tokio-stream", + "tokio-util", + "tracing", + "tracing-android", + "trust-dns-resolver", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "wasm-logger", + "web-sys", + "webpki 0.21.4", + "webpki-roots 0.21.1", + "wee_alloc", + "winapi 0.3.9", + "windows", + "windows-permissions", + "ws_stream_wasm", + "x25519-dalek-ng", +] + [[package]] name = "veilid-wasm" version = "0.1.0" @@ -5940,6 +6060,15 @@ dependencies = [ "untrusted", ] +[[package]] +name = "webpki-roots" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f20dea7535251981a9670857150d571846545088359b28e4951d350bdaf179f" +dependencies = [ + "webpki 0.21.4", +] + [[package]] name = "webpki-roots" version = "0.21.1" diff --git a/veilid-cli/src/command_processor.rs b/veilid-cli/src/command_processor.rs index dcf4d719..88674eeb 100644 --- a/veilid-cli/src/command_processor.rs +++ b/veilid-cli/src/command_processor.rs @@ -1,6 +1,5 @@ use crate::client_api_connection::*; use crate::settings::Settings; -use crate::tools::*; use crate::ui::*; use log::*; use std::cell::*; diff --git a/veilid-core/Cargo.toml b/veilid-core/Cargo.toml index ef4ea26b..626ec92f 100644 --- a/veilid-core/Cargo.toml +++ b/veilid-core/Cargo.toml @@ -19,7 +19,7 @@ ios_tests = [ "simplelog" ] tracking = [] [dependencies] -veilid_tools = { path = "../veilid-tools", features = "tracing" } +veilid-tools = { path = "../veilid-tools", features = [ "tracing" ] } tracing = { version = "^0", features = ["log", "attributes"] } tracing-subscriber = "^0" tracing-error = "^0" diff --git a/veilid-core/src/network_manager/native/igd_manager.rs b/veilid-core/src/network_manager/native/igd_manager.rs index f7580a89..3765a4ae 100644 --- a/veilid-core/src/network_manager/native/igd_manager.rs +++ b/veilid-core/src/network_manager/native/igd_manager.rs @@ -2,6 +2,7 @@ use super::*; use igd::*; use std::net::UdpSocket; + const UPNP_GATEWAY_DETECT_TIMEOUT_MS: u32 = 5_000; const UPNP_MAPPING_LIFETIME_MS: u32 = 120_000; const UPNP_MAPPING_ATTEMPTS: u32 = 3; diff --git a/veilid-tools/Cargo.toml b/veilid-tools/Cargo.toml index 3e0ebf31..2e9aefc2 100644 --- a/veilid-tools/Cargo.toml +++ b/veilid-tools/Cargo.toml @@ -1,30 +1,27 @@ [package] -name = "veilid-core" +name = "veilid-tools" version = "0.1.0" authors = ["John Smith "] edition = "2021" -build = "build.rs" license = "LGPL-2.0-or-later OR MPL-2.0 OR (MIT AND BSD-3-Clause)" [lib] -crate-type = ["cdylib", "staticlib", "rlib"] +crate-type = ["rlib"] [features] -default = [] +default = [ "rt-tokio" ] rt-async-std = [ "async-std", "async-std-resolver", "async_executors/async_std", "rtnetlink?/smol_socket" ] rt-tokio = [ "tokio", "tokio-util", "tokio-stream", "trust-dns-resolver/tokio-runtime", "async_executors/tokio_tp", "async_executors/tokio_io", "async_executors/tokio_timer", "rtnetlink?/tokio_socket" ] android_tests = [] ios_tests = [ "simplelog" ] tracking = [] +tracing = [ "dep:tracing" ] [dependencies] -tracing = { version = "^0", features = ["log", "attributes"] } -tracing-subscriber = "^0" -tracing-error = "^0" +tracing = { version = "^0", features = ["log", "attributes"], optional = true } +log = { version = "^0", optional = true } eyre = "^0" -capnp = { version = "^0", default_features = false } -rust-fsm = "^0" static_assertions = "^1" cfg-if = "^1" thiserror = "^1" @@ -100,13 +97,12 @@ js-sys = "^0" wasm-bindgen-futures = "^0" keyvaluedb-web = { path = "../external/keyvaluedb/keyvaluedb-web" } data-encoding = { version = "^2", default_features = false, features = ["alloc"] } -getrandom = { version = "^0", features = ["js"] } +getrandom = { version = "^0.2", features = ["js"] } ws_stream_wasm = "^0" async_executors = { version = "^0", default-features = false, features = [ "bindgen", "timer" ]} async-lock = "^2" -send_wrapper = { version = "^0", features = ["futures"] } +send_wrapper = { version = "^0.6", features = ["futures"] } wasm-logger = "^0" -tracing-wasm = "^0" # Configuration for WASM32 'web-sys' crate [target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] @@ -142,7 +138,7 @@ rtnetlink = { version = "^0", default-features = false } # Dependencies for Windows [target.'cfg(target_os = "windows")'.dependencies] -winapi = { version = "^0", features = [ "iptypes", "iphlpapi" ] } +winapi = { version = "^0.3.9", features = [ "iptypes", "iphlpapi" ] } windows = { version = "^0", features = [ "Win32_NetworkManagement_Dns", "Win32_Foundation", "alloc" ]} windows-permissions = "^0" @@ -172,8 +168,5 @@ wasm-logger = "^0" ### BUILD OPTIONS -[build-dependencies] -capnpc = "^0" - [package.metadata.wasm-pack.profile.release] wasm-opt = ["-O", "--enable-mutable-globals"] diff --git a/veilid-tools/src/mod.rs b/veilid-tools/src/lib.rs similarity index 98% rename from veilid-tools/src/mod.rs rename to veilid-tools/src/lib.rs index 15529449..d5f6ac70 100644 --- a/veilid-tools/src/mod.rs +++ b/veilid-tools/src/lib.rs @@ -74,7 +74,7 @@ pub use std::net::{ pub use std::ops::{Fn, FnMut, FnOnce}; pub use std::pin::Pin; pub use std::rc::Rc; -pub use std::string::String; +pub use std::string::{String, ToString}; pub use std::sync::atomic::{AtomicBool, Ordering}; pub use std::sync::{Arc, Weak}; pub use std::task; diff --git a/veilid-tools/src/log_thru.rs b/veilid-tools/src/log_thru.rs index b6fecc01..587a8393 100644 --- a/veilid-tools/src/log_thru.rs +++ b/veilid-tools/src/log_thru.rs @@ -2,7 +2,7 @@ // Pass errors through and log them simultaneously via map_err() // Also contains common log facilities (net, rpc, rtab, pstore, crypto, etc ) -use alloc::string::{String, ToString}; +use super::*; pub fn map_to_string(arg: X) -> String { arg.to_string() diff --git a/veilid-tools/src/tools.rs b/veilid-tools/src/tools.rs index 1758fdf2..1d388121 100644 --- a/veilid-tools/src/tools.rs +++ b/veilid-tools/src/tools.rs @@ -1,6 +1,5 @@ use super::*; -use alloc::string::ToString; use std::io; use std::path::Path; @@ -276,7 +275,7 @@ cfg_if::cfg_if! { Ok(()) } } else { - pub fn ensure_file_private_owner>(_path: P) -> Result<(),String> + pub fn ensure_file_private_owner>(_path: P) -> Result<(), String> { Ok(()) } From 3a7d9b57b5a0aa1d54935936ea6d8212aa8e22c6 Mon Sep 17 00:00:00 2001 From: John Smith Date: Sun, 27 Nov 2022 09:00:20 -0500 Subject: [PATCH 11/88] veilid-tools work --- Cargo.lock | 96 +-- .../src/tests/common/test_host_interface.rs | 2 +- veilid-core/tests/web.rs | 49 -- veilid-tools/Cargo.toml | 95 +-- veilid-tools/src/lib.rs | 4 + veilid-tools/src/must_join_handle.rs | 2 +- veilid-tools/src/split_url.rs | 2 +- veilid-tools/src/tests/.gitignore | 4 + veilid-tools/src/tests/android/.gitignore | 16 + .../src/tests/android/.idea/.gitignore | 3 + veilid-tools/src/tests/android/.idea/.name | 1 + .../src/tests/android/.idea/compiler.xml | 6 + .../src/tests/android/.idea/gradle.xml | 21 + .../tests/android/.idea/jarRepositories.xml | 25 + veilid-tools/src/tests/android/.idea/misc.xml | 9 + veilid-tools/src/tests/android/.idea/vcs.xml | 6 + veilid-tools/src/tests/android/.project | 28 + .../org.eclipse.buildship.core.prefs | 13 + veilid-tools/src/tests/android/adb+.sh | 20 + veilid-tools/src/tests/android/app/.classpath | 6 + veilid-tools/src/tests/android/app/.gitignore | 4 + veilid-tools/src/tests/android/app/.project | 34 ++ .../org.eclipse.buildship.core.prefs | 2 + .../src/tests/android/app/CMakeLists.txt | 3 + .../src/tests/android/app/build.gradle | 87 +++ .../src/tests/android/app/cpplink.cpp | 0 .../src/tests/android/app/proguard-rules.pro | 21 + .../android/app/src/main/AndroidManifest.xml | 25 + .../MainActivity.java | 37 ++ .../drawable-v24/ic_launcher_foreground.xml | 30 + .../res/drawable/ic_launcher_background.xml | 170 ++++++ .../app/src/main/res/layout/activity_main.xml | 18 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3593 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5339 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2636 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3388 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4926 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7472 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7909 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 11873 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10652 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16570 bytes .../app/src/main/res/values-night/themes.xml | 16 + .../app/src/main/res/values/colors.xml | 10 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/themes.xml | 16 + veilid-tools/src/tests/android/build.gradle | 28 + .../src/tests/android/gradle.properties | 19 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + veilid-tools/src/tests/android/gradlew | 172 ++++++ veilid-tools/src/tests/android/gradlew.bat | 84 +++ .../tests/android/install_on_all_devices.sh | 2 + .../tests/android/remove_from_all_devices.sh | 3 + .../src/tests/android/settings.gradle | 2 + veilid-tools/src/tests/common/mod.rs | 2 + .../src/tests/common/test_async_tag_lock.rs | 158 +++++ .../src/tests/common/test_host_interface.rs | 554 ++++++++++++++++++ veilid-tools/src/tests/files/cert.pem | 88 +++ veilid-tools/src/tests/files/key.pem | 27 + veilid-tools/src/tests/ios/.gitignore | 91 +++ .../ios/veilidtools-tests/veilid-tools.c | 8 + .../ios/veilidtools-tests/veilid-tools.h | 13 + .../veilidtools-tests-Bridging-Header.h | 5 + .../project.pbxproj | 413 +++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcschemes/veilidcore-tests.xcscheme | 78 +++ .../veilidtools-tests/AppDelegate.swift | 37 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 ++++ .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 56 ++ .../Base.lproj/Main.storyboard | 70 +++ .../veilidtools-tests/Info.plist | 66 +++ .../veilidtools-tests/SceneDelegate.swift | 53 ++ .../veilidtools-tests/ViewController.swift | 19 + veilid-tools/src/tests/mod.rs | 3 + veilid-tools/src/tests/native/mod.rs | 152 +++++ .../tests/native/test_async_peek_stream.rs | 352 +++++++++++ veilid-tools/tests/web.rs | 87 +++ 83 files changed, 3440 insertions(+), 232 deletions(-) create mode 100644 veilid-tools/src/tests/.gitignore create mode 100644 veilid-tools/src/tests/android/.gitignore create mode 100644 veilid-tools/src/tests/android/.idea/.gitignore create mode 100644 veilid-tools/src/tests/android/.idea/.name create mode 100644 veilid-tools/src/tests/android/.idea/compiler.xml create mode 100644 veilid-tools/src/tests/android/.idea/gradle.xml create mode 100644 veilid-tools/src/tests/android/.idea/jarRepositories.xml create mode 100644 veilid-tools/src/tests/android/.idea/misc.xml create mode 100644 veilid-tools/src/tests/android/.idea/vcs.xml create mode 100644 veilid-tools/src/tests/android/.project create mode 100644 veilid-tools/src/tests/android/.settings/org.eclipse.buildship.core.prefs create mode 100755 veilid-tools/src/tests/android/adb+.sh create mode 100644 veilid-tools/src/tests/android/app/.classpath create mode 100644 veilid-tools/src/tests/android/app/.gitignore create mode 100644 veilid-tools/src/tests/android/app/.project create mode 100644 veilid-tools/src/tests/android/app/.settings/org.eclipse.buildship.core.prefs create mode 100644 veilid-tools/src/tests/android/app/CMakeLists.txt create mode 100644 veilid-tools/src/tests/android/app/build.gradle create mode 100644 veilid-tools/src/tests/android/app/cpplink.cpp create mode 100644 veilid-tools/src/tests/android/app/proguard-rules.pro create mode 100644 veilid-tools/src/tests/android/app/src/main/AndroidManifest.xml create mode 100644 veilid-tools/src/tests/android/app/src/main/java/com/veilid/veilid-core/veilid-core_android_tests/MainActivity.java create mode 100644 veilid-tools/src/tests/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 veilid-tools/src/tests/android/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 veilid-tools/src/tests/android/app/src/main/res/layout/activity_main.xml create mode 100644 veilid-tools/src/tests/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 veilid-tools/src/tests/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 veilid-tools/src/tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 veilid-tools/src/tests/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 veilid-tools/src/tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 veilid-tools/src/tests/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 veilid-tools/src/tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 veilid-tools/src/tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 veilid-tools/src/tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 veilid-tools/src/tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 veilid-tools/src/tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 veilid-tools/src/tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 veilid-tools/src/tests/android/app/src/main/res/values-night/themes.xml create mode 100644 veilid-tools/src/tests/android/app/src/main/res/values/colors.xml create mode 100644 veilid-tools/src/tests/android/app/src/main/res/values/strings.xml create mode 100644 veilid-tools/src/tests/android/app/src/main/res/values/themes.xml create mode 100644 veilid-tools/src/tests/android/build.gradle create mode 100644 veilid-tools/src/tests/android/gradle.properties create mode 100644 veilid-tools/src/tests/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 veilid-tools/src/tests/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 veilid-tools/src/tests/android/gradlew create mode 100644 veilid-tools/src/tests/android/gradlew.bat create mode 100755 veilid-tools/src/tests/android/install_on_all_devices.sh create mode 100755 veilid-tools/src/tests/android/remove_from_all_devices.sh create mode 100644 veilid-tools/src/tests/android/settings.gradle create mode 100644 veilid-tools/src/tests/common/mod.rs create mode 100644 veilid-tools/src/tests/common/test_async_tag_lock.rs create mode 100644 veilid-tools/src/tests/common/test_host_interface.rs create mode 100644 veilid-tools/src/tests/files/cert.pem create mode 100644 veilid-tools/src/tests/files/key.pem create mode 100644 veilid-tools/src/tests/ios/.gitignore create mode 100644 veilid-tools/src/tests/ios/veilidtools-tests/veilid-tools.c create mode 100644 veilid-tools/src/tests/ios/veilidtools-tests/veilid-tools.h create mode 100644 veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests-Bridging-Header.h create mode 100644 veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj create mode 100644 veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/xcshareddata/xcschemes/veilidcore-tests.xcscheme create mode 100644 veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/AppDelegate.swift create mode 100644 veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Assets.xcassets/Contents.json create mode 100644 veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Base.lproj/LaunchScreen.storyboard create mode 100644 veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Base.lproj/Main.storyboard create mode 100644 veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Info.plist create mode 100644 veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/SceneDelegate.swift create mode 100644 veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/ViewController.swift create mode 100644 veilid-tools/src/tests/mod.rs create mode 100644 veilid-tools/src/tests/native/mod.rs create mode 100644 veilid-tools/src/tests/native/test_async_peek_stream.rs create mode 100644 veilid-tools/tests/web.rs diff --git a/Cargo.lock b/Cargo.lock index 8690c6fa..39b78d46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -334,18 +334,6 @@ version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" -[[package]] -name = "async-tls" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7e7fbc0843fc5ad3d5ca889c5b2bea9130984d34cd0e62db57ab70c2529a8e3" -dependencies = [ - "futures", - "rustls 0.18.1", - "webpki 0.21.4", - "webpki-roots 0.20.0", -] - [[package]] name = "async-tls" version = "0.11.0" @@ -354,7 +342,7 @@ checksum = "2f23d769dbf1838d5df5156e7b1ad404f4c463d1ac2c6aeb6cd943630f8a8400" dependencies = [ "futures-core", "futures-io", - "rustls 0.19.1", + "rustls", "webpki 0.21.4", "webpki-roots 0.21.1", ] @@ -376,7 +364,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5c45a0dd44b7e6533ac4e7acc38ead1a3b39885f5bbb738140d30ea528abc7c" dependencies = [ - "async-tls 0.9.0", "futures-io", "futures-util", "log", @@ -390,7 +377,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b750efd83b7e716a015eed5ebb583cda83c52d9b24a8f0125e5c48c3313c9f8" dependencies = [ - "async-tls 0.11.0", + "async-tls", "futures-io", "futures-util", "log", @@ -4309,19 +4296,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustls" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d1126dcf58e93cee7d098dbda643b5f92ed724f1f6a63007c1116eed6700c81" -dependencies = [ - "base64 0.12.3", - "log", - "ring", - "sct", - "webpki 0.21.4", -] - [[package]] name = "rustls" version = "0.19.1" @@ -5588,7 +5562,7 @@ dependencies = [ "async-lock", "async-std", "async-std-resolver", - "async-tls 0.11.0", + "async-tls", "async-tungstenite 0.18.0", "async_executors", "backtrace", @@ -5641,7 +5615,7 @@ dependencies = [ "rtnetlink", "rusqlite", "rust-fsm", - "rustls 0.19.1", + "rustls", "rustls-pemfile", "secrecy", "send_wrapper 0.6.0", @@ -5763,47 +5737,15 @@ dependencies = [ name = "veilid-tools" version = "0.1.0" dependencies = [ - "async-io", "async-lock", "async-std", - "async-std-resolver", - "async-tls 0.11.0", - "async-tungstenite 0.8.0", "async_executors", - "backtrace", - "blake3", - "bugsalot", - "bytecheck", "cfg-if 1.0.0", - "chacha20 0.8.2", - "chacha20poly1305", - "chrono", - "config", - "console_error_panic_hook", - "curve25519-dalek-ng", - "data-encoding", - "digest 0.9.0", - "directories", - "ed25519-dalek", - "enumset", "eyre", - "flume", "futures-util", - "generic-array", - "getrandom 0.2.8", - "hashlink 0.8.1", - "hex", - "ifstructs", - "igd", "jni", "jni-sys", "js-sys", - "json", - "keyring-manager", - "keyvaluedb", - "keyvaluedb-sqlite", - "keyvaluedb-web", - "lazy_static", "libc", "log", "maplit", @@ -5811,45 +5753,24 @@ dependencies = [ "ndk-glue", "nix 0.22.3", "once_cell", - "owning_ref", "owo-colors", "parking_lot 0.11.2", "rand 0.7.3", - "rkyv", - "rtnetlink", - "rusqlite", - "rustls 0.19.1", - "rustls-pemfile", - "secrecy", "send_wrapper 0.6.0", - "serde", - "serde-big-array", - "serde_json", "serial_test", "simplelog 0.9.0", - "socket2", "static_assertions", "stop-token", "thiserror", "tokio 1.22.0", - "tokio-stream", "tokio-util", "tracing", "tracing-android", - "trust-dns-resolver", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", "wasm-logger", - "web-sys", - "webpki 0.21.4", - "webpki-roots 0.21.1", "wee_alloc", - "winapi 0.3.9", - "windows", - "windows-permissions", - "ws_stream_wasm", - "x25519-dalek-ng", ] [[package]] @@ -6060,15 +5981,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "webpki-roots" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f20dea7535251981a9670857150d571846545088359b28e4951d350bdaf179f" -dependencies = [ - "webpki 0.21.4", -] - [[package]] name = "webpki-roots" version = "0.21.1" diff --git a/veilid-core/src/tests/common/test_host_interface.rs b/veilid-core/src/tests/common/test_host_interface.rs index cc0f750e..322f5d5e 100644 --- a/veilid-core/src/tests/common/test_host_interface.rs +++ b/veilid-core/src/tests/common/test_host_interface.rs @@ -567,7 +567,7 @@ pub async fn test_all() { test_get_random_u32().await; test_sleep().await; #[cfg(not(target_arch = "wasm32"))] - test_network_interfaces().await; + test_network_interfaces().await; XXX KEEP THIS IN NATIVE TESTS test_must_join_single_future().await; test_eventual().await; test_eventual_value().await; diff --git a/veilid-core/tests/web.rs b/veilid-core/tests/web.rs index a5ac14b7..c4df57b1 100644 --- a/veilid-core/tests/web.rs +++ b/veilid-core/tests/web.rs @@ -23,13 +23,6 @@ pub fn setup() -> () { }); } -#[wasm_bindgen_test] -async fn run_test_dht_key() { - setup(); - - test_dht_key::test_all().await; -} - #[wasm_bindgen_test] async fn run_test_host_interface() { setup(); @@ -37,48 +30,6 @@ async fn run_test_host_interface() { test_host_interface::test_all().await; } -#[wasm_bindgen_test] -async fn run_test_veilid_core() { - setup(); - - test_veilid_core::test_all().await; -} - -#[wasm_bindgen_test] -async fn run_test_config() { - setup(); - - test_veilid_config::test_all().await; -} - -#[wasm_bindgen_test] -async fn run_test_connection_table() { - setup(); - - test_connection_table::test_all().await; -} - -#[wasm_bindgen_test] -async fn run_test_table_store() { - setup(); - - test_table_store::test_all().await; -} - -#[wasm_bindgen_test] -async fn run_test_crypto() { - setup(); - - test_crypto::test_all().await; -} - -#[wasm_bindgen_test] -async fn run_test_envelope_receipt() { - setup(); - - test_envelope_receipt::test_all().await; -} - #[wasm_bindgen_test] async fn run_test_async_tag_lock() { setup(); diff --git a/veilid-tools/Cargo.toml b/veilid-tools/Cargo.toml index 2e9aefc2..9d306626 100644 --- a/veilid-tools/Cargo.toml +++ b/veilid-tools/Cargo.toml @@ -9,14 +9,15 @@ license = "LGPL-2.0-or-later OR MPL-2.0 OR (MIT AND BSD-3-Clause)" crate-type = ["rlib"] [features] -default = [ "rt-tokio" ] -rt-async-std = [ "async-std", "async-std-resolver", "async_executors/async_std", "rtnetlink?/smol_socket" ] -rt-tokio = [ "tokio", "tokio-util", "tokio-stream", "trust-dns-resolver/tokio-runtime", "async_executors/tokio_tp", "async_executors/tokio_io", "async_executors/tokio_timer", "rtnetlink?/tokio_socket" ] +default = [ "rt-tokio", "tracing" ] +rt-async-std = [ "async-std", "async_executors/async_std", ] +rt-tokio = [ "tokio", "tokio-util", "async_executors/tokio_tp", "async_executors/tokio_io", "async_executors/tokio_timer", ] android_tests = [] ios_tests = [ "simplelog" ] tracking = [] tracing = [ "dep:tracing" ] +log = [ "dep:log" ] [dependencies] tracing = { version = "^0", features = ["log", "attributes"], optional = true } @@ -25,42 +26,12 @@ eyre = "^0" static_assertions = "^1" cfg-if = "^1" thiserror = "^1" -hex = "^0" -generic-array = "^0" -secrecy = "^0" -chacha20poly1305 = "^0" -chacha20 = "^0" -hashlink = { path = "../external/hashlink", features = ["serde_impl"] } -serde = { version = "^1", features = ["derive" ] } -serde_json = { version = "^1" } -serde-big-array = "^0" futures-util = { version = "^0", default_features = false, features = ["alloc"] } parking_lot = "^0" -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" } owo-colors = "^3" stop-token = { version = "^0", default-features = false } -ed25519-dalek = { version = "^1", default_features = false, features = ["alloc", "u64_backend"] } -x25519-dalek = { package = "x25519-dalek-ng", version = "^1", default_features = false, features = ["u64_backend"] } -curve25519-dalek = { package = "curve25519-dalek-ng", version = "^4", default_features = false, features = ["alloc", "u64_backend"] } -# ed25519-dalek needs rand 0.7 until it updates itself rand = "0.7" -# curve25519-dalek-ng is stuck on digest 0.9.0 -blake3 = { version = "1.1.0", default_features = false } -digest = "0.9.0" -rtnetlink = { version = "^0", default-features = false, optional = true } -async-std-resolver = { version = "^0", optional = true } -trust-dns-resolver = { version = "^0", optional = true } -keyvaluedb = { path = "../external/keyvaluedb/keyvaluedb" } -#rkyv = { version = "^0", default_features = false, features = ["std", "alloc", "strict", "size_32", "validation"] } -rkyv = { git = "https://github.com/rkyv/rkyv.git", rev = "57e2a8d", default_features = false, features = ["std", "alloc", "strict", "size_32", "validation"] } -bytecheck = "^0" # Dependencies for native builds only # Linux, Windows, Mac, iOS, Android @@ -68,25 +39,9 @@ bytecheck = "^0" async-std = { version = "^1", features = ["unstable"], optional = true} tokio = { version = "^1", features = ["full"], optional = true} tokio-util = { version = "^0", features = ["compat"], optional = true} -tokio-stream = { version = "^0", features = ["net"], optional = true} -async-io = { version = "^1" } -async-tungstenite = { version = "^0", features = ["async-tls"] } maplit = "^1" -config = { version = "^0", features = ["yaml"] } -keyring-manager = { path = "../external/keyring-manager" } -async-tls = "^0.11" -igd = { path = "../external/rust-igd" } -webpki = "^0" -webpki-roots = "^0" -rustls = "^0.19" -rustls-pemfile = "^0.2" futures-util = { version = "^0", default-features = false, features = ["async-await", "sink", "std", "io"] } -keyvaluedb-sqlite = { path = "../external/keyvaluedb/keyvaluedb-sqlite" } -data-encoding = { version = "^2" } -socket2 = "^0" -bugsalot = "^0" -chrono = "^0" libc = "^0" nix = "^0" @@ -95,30 +50,10 @@ nix = "^0" wasm-bindgen = "^0" js-sys = "^0" wasm-bindgen-futures = "^0" -keyvaluedb-web = { path = "../external/keyvaluedb/keyvaluedb-web" } -data-encoding = { version = "^2", default_features = false, features = ["alloc"] } -getrandom = { version = "^0.2", features = ["js"] } -ws_stream_wasm = "^0" async_executors = { version = "^0", default-features = false, features = [ "bindgen", "timer" ]} async-lock = "^2" send_wrapper = { version = "^0.6", features = ["futures"] } -wasm-logger = "^0" -# Configuration for WASM32 'web-sys' crate -[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] -version = "^0" -features = [ - 'Document', - 'HtmlDocument', - # 'Element', - # 'HtmlElement', - # 'Node', - 'IdbFactory', - 'IdbOpenDbRequest', - 'Storage', - 'Location', - 'Window', -] # Dependencies for Android [target.'cfg(target_os = "android")'.dependencies] @@ -128,30 +63,15 @@ ndk = { version = "^0", features = ["trace"] } ndk-glue = { version = "^0", features = ["logger"] } tracing-android = { version = "^0" } -# Dependenices for all Unix (Linux, Android, MacOS, iOS) -[target.'cfg(unix)'.dependencies] -ifstructs = "^0" - -# Dependencies for Linux or Android -[target.'cfg(any(target_os = "android",target_os = "linux"))'.dependencies] -rtnetlink = { version = "^0", default-features = false } - # Dependencies for Windows -[target.'cfg(target_os = "windows")'.dependencies] -winapi = { version = "^0.3.9", features = [ "iptypes", "iphlpapi" ] } -windows = { version = "^0", features = [ "Win32_NetworkManagement_Dns", "Win32_Foundation", "alloc" ]} -windows-permissions = "^0" +# [target.'cfg(target_os = "windows")'.dependencies] +# windows = { version = "^0", features = [ "Win32_NetworkManagement_Dns", "Win32_Foundation", "alloc" ]} +# windows-permissions = "^0" # Dependencies for iOS [target.'cfg(target_os = "ios")'.dependencies] simplelog = { version = "^0", optional = true } -# Rusqlite configuration to ensure platforms that don't come with sqlite get it bundled -# Except WASM which doesn't use sqlite -[target.'cfg(all(not(target_os = "ios"),not(target_os = "android"),not(target_arch = "wasm32")))'.dependencies.rusqlite] -version = "^0" -features = ["bundled"] - ### DEV DEPENDENCIES [dev-dependencies] @@ -162,7 +82,6 @@ simplelog = { version = "^0", features=["test"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = "^0" -console_error_panic_hook = "^0" wee_alloc = "^0" wasm-logger = "^0" diff --git a/veilid-tools/src/lib.rs b/veilid-tools/src/lib.rs index d5f6ac70..0838c00f 100644 --- a/veilid-tools/src/lib.rs +++ b/veilid-tools/src/lib.rs @@ -74,6 +74,7 @@ pub use std::net::{ pub use std::ops::{Fn, FnMut, FnOnce}; pub use std::pin::Pin; pub use std::rc::Rc; +pub use std::str::FromStr; pub use std::string::{String, ToString}; pub use std::sync::atomic::{AtomicBool, Ordering}; pub use std::sync::{Arc, Weak}; @@ -132,3 +133,6 @@ pub use timestamp::*; pub use tools::*; #[cfg(target_arch = "wasm32")] pub use wasm::*; + +// Tests must be public for wasm-pack tests +pub mod tests; diff --git a/veilid-tools/src/must_join_handle.rs b/veilid-tools/src/must_join_handle.rs index a9de40b7..ff7231f1 100644 --- a/veilid-tools/src/must_join_handle.rs +++ b/veilid-tools/src/must_join_handle.rs @@ -77,7 +77,7 @@ impl Future for MustJoinHandle { } } } - }else if #[cfg(target_arch = "wasm32")] { + } else if #[cfg(target_arch = "wasm32")] { Poll::Ready(t) } else { compile_error!("needs executor implementation") diff --git a/veilid-tools/src/split_url.rs b/veilid-tools/src/split_url.rs index 718d4946..9cfd8c94 100644 --- a/veilid-tools/src/split_url.rs +++ b/veilid-tools/src/split_url.rs @@ -9,7 +9,7 @@ use super::*; -use core::str::FromStr; +use std::str::FromStr; fn is_alphanum(c: u8) -> bool { matches!(c, diff --git a/veilid-tools/src/tests/.gitignore b/veilid-tools/src/tests/.gitignore new file mode 100644 index 00000000..e8c47856 --- /dev/null +++ b/veilid-tools/src/tests/.gitignore @@ -0,0 +1,4 @@ +# exclude everything +tmp/* +# exception to the rule +!tmp/.gitkeep diff --git a/veilid-tools/src/tests/android/.gitignore b/veilid-tools/src/tests/android/.gitignore new file mode 100644 index 00000000..82e4fd4b --- /dev/null +++ b/veilid-tools/src/tests/android/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +/.idea/deploymentTargetDropDown.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/veilid-tools/src/tests/android/.idea/.gitignore b/veilid-tools/src/tests/android/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/veilid-tools/src/tests/android/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/veilid-tools/src/tests/android/.idea/.name b/veilid-tools/src/tests/android/.idea/.name new file mode 100644 index 00000000..cde590be --- /dev/null +++ b/veilid-tools/src/tests/android/.idea/.name @@ -0,0 +1 @@ +Veilid Tools Tests \ No newline at end of file diff --git a/veilid-tools/src/tests/android/.idea/compiler.xml b/veilid-tools/src/tests/android/.idea/compiler.xml new file mode 100644 index 00000000..fb7f4a8a --- /dev/null +++ b/veilid-tools/src/tests/android/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/veilid-tools/src/tests/android/.idea/gradle.xml b/veilid-tools/src/tests/android/.idea/gradle.xml new file mode 100644 index 00000000..4989a70a --- /dev/null +++ b/veilid-tools/src/tests/android/.idea/gradle.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/veilid-tools/src/tests/android/.idea/jarRepositories.xml b/veilid-tools/src/tests/android/.idea/jarRepositories.xml new file mode 100644 index 00000000..a5f05cd8 --- /dev/null +++ b/veilid-tools/src/tests/android/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/veilid-tools/src/tests/android/.idea/misc.xml b/veilid-tools/src/tests/android/.idea/misc.xml new file mode 100644 index 00000000..ef61796f --- /dev/null +++ b/veilid-tools/src/tests/android/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/veilid-tools/src/tests/android/.idea/vcs.xml b/veilid-tools/src/tests/android/.idea/vcs.xml new file mode 100644 index 00000000..4fce1d86 --- /dev/null +++ b/veilid-tools/src/tests/android/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/veilid-tools/src/tests/android/.project b/veilid-tools/src/tests/android/.project new file mode 100644 index 00000000..cf0850ea --- /dev/null +++ b/veilid-tools/src/tests/android/.project @@ -0,0 +1,28 @@ + + + Veilid Tools Tests + Project android created by Buildship. + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.buildship.core.gradleprojectnature + + + + 0 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/veilid-tools/src/tests/android/.settings/org.eclipse.buildship.core.prefs b/veilid-tools/src/tests/android/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 00000000..094110d3 --- /dev/null +++ b/veilid-tools/src/tests/android/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,13 @@ +arguments= +auto.sync=false +build.scans.enabled=false +connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) +connection.project.dir= +eclipse.preferences.version=1 +gradle.user.home= +java.home=/opt/android-studio/jre +jvm.arguments= +offline.mode=false +override.workspace.settings=true +show.console.view=true +show.executions.view=true diff --git a/veilid-tools/src/tests/android/adb+.sh b/veilid-tools/src/tests/android/adb+.sh new file mode 100755 index 00000000..5aa7fbbf --- /dev/null +++ b/veilid-tools/src/tests/android/adb+.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Script adb+ +# Usage +# You can run any command adb provide on all your current devices +# ./adb+ is the equivalent of ./adb -s +# +# Examples +# ./adb+ version +# ./adb+ install apidemo.apk +# ./adb+ uninstall com.example.android.apis + +adb devices | while read line +do + if [ ! "$line" = "" ] && [ `echo $line | awk '{print $2}'` = "device" ] + then + device=`echo $line | awk '{print $1}'` + echo "$device $@ ..." + adb -s $device $@ + fi +done diff --git a/veilid-tools/src/tests/android/app/.classpath b/veilid-tools/src/tests/android/app/.classpath new file mode 100644 index 00000000..4a04201c --- /dev/null +++ b/veilid-tools/src/tests/android/app/.classpath @@ -0,0 +1,6 @@ + + + + + + diff --git a/veilid-tools/src/tests/android/app/.gitignore b/veilid-tools/src/tests/android/app/.gitignore new file mode 100644 index 00000000..23c35ec2 --- /dev/null +++ b/veilid-tools/src/tests/android/app/.gitignore @@ -0,0 +1,4 @@ +/build +/.cxx + + diff --git a/veilid-tools/src/tests/android/app/.project b/veilid-tools/src/tests/android/app/.project new file mode 100644 index 00000000..c2e60fbb --- /dev/null +++ b/veilid-tools/src/tests/android/app/.project @@ -0,0 +1,34 @@ + + + VeilidTools Tests-app + Project VeilidTools Tests-app created by Buildship. + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.buildship.core.gradleprojectnature + + + + 1635633714053 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/veilid-tools/src/tests/android/app/.settings/org.eclipse.buildship.core.prefs b/veilid-tools/src/tests/android/app/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 00000000..b1886adb --- /dev/null +++ b/veilid-tools/src/tests/android/app/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,2 @@ +connection.project.dir=.. +eclipse.preferences.version=1 diff --git a/veilid-tools/src/tests/android/app/CMakeLists.txt b/veilid-tools/src/tests/android/app/CMakeLists.txt new file mode 100644 index 00000000..57518a5d --- /dev/null +++ b/veilid-tools/src/tests/android/app/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.1) +project(cpplink CXX) +add_library(cpplink cpplink.cpp) \ No newline at end of file diff --git a/veilid-tools/src/tests/android/app/build.gradle b/veilid-tools/src/tests/android/app/build.gradle new file mode 100644 index 00000000..21c6c24a --- /dev/null +++ b/veilid-tools/src/tests/android/app/build.gradle @@ -0,0 +1,87 @@ +plugins { + id 'com.android.application' +} + +android { + compileSdkVersion 30 + buildToolsVersion "30.0.3" + + defaultConfig { + applicationId "com.veilid.veilidtools.veilidtools_android_tests" + minSdkVersion 24 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + ndk { + abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' + } + + // Required to copy libc++_shared.so + externalNativeBuild { + cmake { + arguments "-DANDROID_STL=c++_shared" + targets "cpplink" + } + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + ndkVersion '22.0.7026061' + + // Required to copy libc++_shared.so + externalNativeBuild { + cmake { + path file('CMakeLists.txt') + } + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.3.1' + implementation 'com.google.android.material:material:1.4.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.2' + implementation 'androidx.security:security-crypto:1.1.0-alpha03' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' +} + +apply plugin: 'org.mozilla.rust-android-gradle.rust-android' + +cargo { + module = "../../../../../veilid-tools" + libname = "veilid_tools" + targets = ["arm", "arm64", "x86", "x86_64"] + targetDirectory = "../../../../../target" + prebuiltToolchains = true + profile = gradle.startParameter.taskNames.any{it.toLowerCase().contains("debug")} ? "debug" : "release" + pythonCommand = "python3" + features { + defaultAnd("android_tests", "rt-tokio") + } +} + +afterEvaluate { + // The `cargoBuild` task isn't available until after evaluation. + android.applicationVariants.all { variant -> + def productFlavor = "" + variant.productFlavors.each { + productFlavor += "${it.name.capitalize()}" + } + def buildType = "${variant.buildType.name.capitalize()}" + tasks["generate${productFlavor}${buildType}Assets"].dependsOn(tasks["cargoBuild"]) + } +} + diff --git a/veilid-tools/src/tests/android/app/cpplink.cpp b/veilid-tools/src/tests/android/app/cpplink.cpp new file mode 100644 index 00000000..e69de29b diff --git a/veilid-tools/src/tests/android/app/proguard-rules.pro b/veilid-tools/src/tests/android/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/veilid-tools/src/tests/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/veilid-tools/src/tests/android/app/src/main/AndroidManifest.xml b/veilid-tools/src/tests/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..9c9b4431 --- /dev/null +++ b/veilid-tools/src/tests/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/veilid-tools/src/tests/android/app/src/main/java/com/veilid/veilid-core/veilid-core_android_tests/MainActivity.java b/veilid-tools/src/tests/android/app/src/main/java/com/veilid/veilid-core/veilid-core_android_tests/MainActivity.java new file mode 100644 index 00000000..2b73f488 --- /dev/null +++ b/veilid-tools/src/tests/android/app/src/main/java/com/veilid/veilid-core/veilid-core_android_tests/MainActivity.java @@ -0,0 +1,37 @@ +package com.veilid.veilidtools.veilidtools_android_tests; + +import androidx.appcompat.app.AppCompatActivity; +import android.content.Context; +import android.os.Bundle; + +public class MainActivity extends AppCompatActivity { + + static { + System.loadLibrary("veilid_tools"); + } + + private static native void run_tests(Context context); + + private Thread testThread; + + class TestThread extends Thread { + private Context context; + + TestThread(Context context) { + this.context = context; + } + + public void run() { + run_tests(this.context); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + this.testThread = new TestThread(this); + this.testThread.start(); + } +} diff --git a/veilid-tools/src/tests/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/veilid-tools/src/tests/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/veilid-tools/src/tests/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/veilid-tools/src/tests/android/app/src/main/res/drawable/ic_launcher_background.xml b/veilid-tools/src/tests/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/veilid-tools/src/tests/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/veilid-tools/src/tests/android/app/src/main/res/layout/activity_main.xml b/veilid-tools/src/tests/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..4fc24441 --- /dev/null +++ b/veilid-tools/src/tests/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/veilid-tools/src/tests/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/veilid-tools/src/tests/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..eca70cfe --- /dev/null +++ b/veilid-tools/src/tests/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/veilid-tools/src/tests/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/veilid-tools/src/tests/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..eca70cfe --- /dev/null +++ b/veilid-tools/src/tests/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/veilid-tools/src/tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/veilid-tools/src/tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a571e60098c92c2baca8a5df62f2929cbff01b52 GIT binary patch literal 3593 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 P00000NkvXXu0mjfP=x?Y literal 0 HcmV?d00001 diff --git a/veilid-tools/src/tests/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/veilid-tools/src/tests/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..61da551c5594a1f9d26193983d2cd69189014603 GIT binary patch literal 5339 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 literal 0 HcmV?d00001 diff --git a/veilid-tools/src/tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/veilid-tools/src/tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..b216f2d313cc673d8b8c4da591c174ebed52795c GIT binary patch literal 11873 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 literal 0 HcmV?d00001 diff --git a/veilid-tools/src/tests/android/app/src/main/res/values-night/themes.xml b/veilid-tools/src/tests/android/app/src/main/res/values-night/themes.xml new file mode 100644 index 00000000..5ee570a8 --- /dev/null +++ b/veilid-tools/src/tests/android/app/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/veilid-tools/src/tests/android/app/src/main/res/values/colors.xml b/veilid-tools/src/tests/android/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..f8c6127d --- /dev/null +++ b/veilid-tools/src/tests/android/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/veilid-tools/src/tests/android/app/src/main/res/values/strings.xml b/veilid-tools/src/tests/android/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..ca58c272 --- /dev/null +++ b/veilid-tools/src/tests/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + VeilidTools Tests + \ No newline at end of file diff --git a/veilid-tools/src/tests/android/app/src/main/res/values/themes.xml b/veilid-tools/src/tests/android/app/src/main/res/values/themes.xml new file mode 100644 index 00000000..914f69d4 --- /dev/null +++ b/veilid-tools/src/tests/android/app/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/veilid-tools/src/tests/android/build.gradle b/veilid-tools/src/tests/android/build.gradle new file mode 100644 index 00000000..96496236 --- /dev/null +++ b/veilid-tools/src/tests/android/build.gradle @@ -0,0 +1,28 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + google() + jcenter() + } + dependencies { + classpath "com.android.tools.build:gradle:4.1.2" + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +plugins { + id "org.mozilla.rust-android-gradle.rust-android" version "0.9.0" +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/veilid-tools/src/tests/android/gradle.properties b/veilid-tools/src/tests/android/gradle.properties new file mode 100644 index 00000000..52f5917c --- /dev/null +++ b/veilid-tools/src/tests/android/gradle.properties @@ -0,0 +1,19 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true \ No newline at end of file diff --git a/veilid-tools/src/tests/android/gradle/wrapper/gradle-wrapper.jar b/veilid-tools/src/tests/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f6b961fd5a86aa5fbfe90f707c3138408be7c718 GIT binary patch literal 54329 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

<5G2M1WZP5 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 F{|7XC?P&l2 literal 0 HcmV?d00001 diff --git a/veilid-tools/src/tests/android/gradle/wrapper/gradle-wrapper.properties b/veilid-tools/src/tests/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..3a56e3d2 --- /dev/null +++ b/veilid-tools/src/tests/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Jun 21 14:26:26 PDT 2021 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip diff --git a/veilid-tools/src/tests/android/gradlew b/veilid-tools/src/tests/android/gradlew new file mode 100755 index 00000000..cccdd3d5 --- /dev/null +++ b/veilid-tools/src/tests/android/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/veilid-tools/src/tests/android/gradlew.bat b/veilid-tools/src/tests/android/gradlew.bat new file mode 100644 index 00000000..e95643d6 --- /dev/null +++ b/veilid-tools/src/tests/android/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/veilid-tools/src/tests/android/install_on_all_devices.sh b/veilid-tools/src/tests/android/install_on_all_devices.sh new file mode 100755 index 00000000..f96d53b1 --- /dev/null +++ b/veilid-tools/src/tests/android/install_on_all_devices.sh @@ -0,0 +1,2 @@ +#!/bin/bash +./gradlew installDebug diff --git a/veilid-tools/src/tests/android/remove_from_all_devices.sh b/veilid-tools/src/tests/android/remove_from_all_devices.sh new file mode 100755 index 00000000..eea0ef7e --- /dev/null +++ b/veilid-tools/src/tests/android/remove_from_all_devices.sh @@ -0,0 +1,3 @@ +#!/bin/bash +./adb+.sh uninstall com.veilid.veilidtools.veilidtools_android_tests + diff --git a/veilid-tools/src/tests/android/settings.gradle b/veilid-tools/src/tests/android/settings.gradle new file mode 100644 index 00000000..ff5d71fe --- /dev/null +++ b/veilid-tools/src/tests/android/settings.gradle @@ -0,0 +1,2 @@ +include ':app' +rootProject.name = "Veilid Tools Tests" \ No newline at end of file diff --git a/veilid-tools/src/tests/common/mod.rs b/veilid-tools/src/tests/common/mod.rs new file mode 100644 index 00000000..06433eae --- /dev/null +++ b/veilid-tools/src/tests/common/mod.rs @@ -0,0 +1,2 @@ +pub mod test_async_tag_lock; +pub mod test_host_interface; diff --git a/veilid-tools/src/tests/common/test_async_tag_lock.rs b/veilid-tools/src/tests/common/test_async_tag_lock.rs new file mode 100644 index 00000000..c1b2ecc4 --- /dev/null +++ b/veilid-tools/src/tests/common/test_async_tag_lock.rs @@ -0,0 +1,158 @@ +use crate::*; + +pub async fn test_simple_no_contention() { + info!("test_simple_no_contention"); + + let table = AsyncTagLockTable::new(); + + let a1 = SocketAddr::new("1.2.3.4".parse().unwrap(), 1234); + let a2 = SocketAddr::new("6.9.6.9".parse().unwrap(), 6969); + + { + let g1 = table.lock_tag(a1).await; + let g2 = table.lock_tag(a2).await; + drop(g2); + drop(g1); + } + + { + let g1 = table.lock_tag(a1).await; + let g2 = table.lock_tag(a2).await; + drop(g1); + drop(g2); + } + + assert_eq!(table.len(), 0); +} + +pub async fn test_simple_single_contention() { + info!("test_simple_single_contention"); + + let table = AsyncTagLockTable::new(); + + let a1 = SocketAddr::new("1.2.3.4".parse().unwrap(), 1234); + + let g1 = table.lock_tag(a1).await; + + info!("locked"); + let t1 = spawn(async move { + // move the guard into the task + let _g1_take = g1; + // hold the guard for a bit + info!("waiting"); + sleep(1000).await; + // release the guard + info!("released"); + }); + + // wait to lock again, will contend until spawned task exits + let _g1_b = table.lock_tag(a1).await; + info!("locked"); + + // Ensure task is joined + t1.await; + + assert_eq!(table.len(), 1); +} + +pub async fn test_simple_double_contention() { + info!("test_simple_double_contention"); + + let table = AsyncTagLockTable::new(); + + let a1 = SocketAddr::new("1.2.3.4".parse().unwrap(), 1234); + let a2 = SocketAddr::new("6.9.6.9".parse().unwrap(), 6969); + + let g1 = table.lock_tag(a1).await; + let g2 = table.lock_tag(a2).await; + + info!("locked"); + let t1 = spawn(async move { + // move the guard into the tas + let _g1_take = g1; + // hold the guard for a bit + info!("waiting"); + sleep(1000).await; + // release the guard + info!("released"); + }); + let t2 = spawn(async move { + // move the guard into the task + let _g2_take = g2; + // hold the guard for a bit + info!("waiting"); + sleep(500).await; + // release the guard + info!("released"); + }); + + // wait to lock again, will contend until spawned task exits + let _g1_b = table.lock_tag(a1).await; + // wait to lock again, should complete immediately + let _g2_b = table.lock_tag(a2).await; + + info!("locked"); + + // Ensure tasks are joined + t1.await; + t2.await; + + assert_eq!(table.len(), 2); +} + +pub async fn test_parallel_single_contention() { + info!("test_parallel_single_contention"); + + let table = AsyncTagLockTable::new(); + + let a1 = SocketAddr::new("1.2.3.4".parse().unwrap(), 1234); + + let table1 = table.clone(); + let t1 = spawn(async move { + // lock the tag + let _g = table1.lock_tag(a1).await; + info!("locked t1"); + // hold the guard for a bit + info!("waiting t1"); + sleep(500).await; + // release the guard + info!("released t1"); + }); + + let table2 = table.clone(); + let t2 = spawn(async move { + // lock the tag + let _g = table2.lock_tag(a1).await; + info!("locked t2"); + // hold the guard for a bit + info!("waiting t2"); + sleep(500).await; + // release the guard + info!("released t2"); + }); + + let table3 = table.clone(); + let t3 = spawn(async move { + // lock the tag + let _g = table3.lock_tag(a1).await; + info!("locked t3"); + // hold the guard for a bit + info!("waiting t3"); + sleep(500).await; + // release the guard + info!("released t3"); + }); + + // Ensure tasks are joined + t1.await; + t2.await; + t3.await; + + assert_eq!(table.len(), 0); +} + +pub async fn test_all() { + test_simple_no_contention().await; + test_simple_single_contention().await; + test_parallel_single_contention().await; +} diff --git a/veilid-tools/src/tests/common/test_host_interface.rs b/veilid-tools/src/tests/common/test_host_interface.rs new file mode 100644 index 00000000..2d73c399 --- /dev/null +++ b/veilid-tools/src/tests/common/test_host_interface.rs @@ -0,0 +1,554 @@ +use crate::*; + +cfg_if! { + if #[cfg(target_arch = "wasm32")] { + use js_sys::*; + } else { + use std::time::{Duration, SystemTime}; + } +} + +pub async fn test_log() { + info!("testing log"); +} + +pub async fn test_get_timestamp() { + info!("testing get_timestamp"); + let t1 = get_timestamp(); + let t2 = get_timestamp(); + assert!(t2 >= t1); +} + +pub async fn test_eventual() { + info!("testing Eventual"); + { + let e1 = Eventual::new(); + let i1 = e1.instance_clone(1u32); + let i2 = e1.instance_clone(2u32); + let i3 = e1.instance_clone(3u32); + drop(i3); + let i4 = e1.instance_clone(4u32); + drop(i2); + + let jh = spawn(async move { + sleep(1000).await; + e1.resolve(); + }); + + assert_eq!(i1.await, 1u32); + assert_eq!(i4.await, 4u32); + + jh.await; + } + { + let e1 = Eventual::new(); + let i1 = e1.instance_clone(1u32); + let i2 = e1.instance_clone(2u32); + let i3 = e1.instance_clone(3u32); + let i4 = e1.instance_clone(4u32); + let e1_c1 = e1.clone(); + let jh = spawn(async move { + let i5 = e1.instance_clone(5u32); + let i6 = e1.instance_clone(6u32); + assert_eq!(i1.await, 1u32); + assert_eq!(i5.await, 5u32); + assert_eq!(i6.await, 6u32); + }); + sleep(1000).await; + let resolved = e1_c1.resolve(); + drop(i2); + drop(i3); + assert_eq!(i4.await, 4u32); + resolved.await; + jh.await; + } + { + let e1 = Eventual::new(); + let i1 = e1.instance_clone(1u32); + let i2 = e1.instance_clone(2u32); + let e1_c1 = e1.clone(); + let jh = spawn(async move { + assert_eq!(i1.await, 1u32); + assert_eq!(i2.await, 2u32); + }); + sleep(1000).await; + e1_c1.resolve().await; + + jh.await; + + e1_c1.reset(); + // + let j1 = e1.instance_clone(1u32); + let j2 = e1.instance_clone(2u32); + let jh = spawn(async move { + assert_eq!(j1.await, 1u32); + assert_eq!(j2.await, 2u32); + }); + sleep(1000).await; + e1_c1.resolve().await; + + jh.await; + + e1_c1.reset(); + } +} + +pub async fn test_eventual_value() { + info!("testing Eventual Value"); + { + let e1 = EventualValue::::new(); + let i1 = e1.instance(); + let i2 = e1.instance(); + let i3 = e1.instance(); + drop(i3); + let i4 = e1.instance(); + drop(i2); + + let e1_c1 = e1.clone(); + let jh = spawn(async move { + sleep(1000).await; + e1_c1.resolve(3u32); + }); + + i1.await; + i4.await; + jh.await; + assert_eq!(e1.take_value(), Some(3u32)); + } + { + let e1 = EventualValue::new(); + let i1 = e1.instance(); + let i2 = e1.instance(); + let i3 = e1.instance(); + let i4 = e1.instance(); + let e1_c1 = e1.clone(); + let jh = spawn(async move { + let i5 = e1.instance(); + let i6 = e1.instance(); + i1.await; + i5.await; + i6.await; + }); + sleep(1000).await; + let resolved = e1_c1.resolve(4u16); + drop(i2); + drop(i3); + i4.await; + resolved.await; + jh.await; + assert_eq!(e1_c1.take_value(), Some(4u16)); + } + { + let e1 = EventualValue::new(); + assert_eq!(e1.take_value(), None); + let i1 = e1.instance(); + let i2 = e1.instance(); + let e1_c1 = e1.clone(); + let jh = spawn(async move { + i1.await; + i2.await; + }); + sleep(1000).await; + e1_c1.resolve(5u32).await; + jh.await; + assert_eq!(e1_c1.take_value(), Some(5u32)); + e1_c1.reset(); + assert_eq!(e1_c1.take_value(), None); + // + let j1 = e1.instance(); + let j2 = e1.instance(); + let jh = spawn(async move { + j1.await; + j2.await; + }); + sleep(1000).await; + e1_c1.resolve(6u32).await; + jh.await; + assert_eq!(e1_c1.take_value(), Some(6u32)); + e1_c1.reset(); + assert_eq!(e1_c1.take_value(), None); + } +} + +pub async fn test_eventual_value_clone() { + info!("testing Eventual Value Clone"); + { + let e1 = EventualValueClone::::new(); + let i1 = e1.instance(); + let i2 = e1.instance(); + let i3 = e1.instance(); + drop(i3); + let i4 = e1.instance(); + drop(i2); + + let jh = spawn(async move { + sleep(1000).await; + e1.resolve(3u32); + }); + + assert_eq!(i1.await, 3); + assert_eq!(i4.await, 3); + + jh.await; + } + + { + let e1 = EventualValueClone::new(); + let i1 = e1.instance(); + let i2 = e1.instance(); + let i3 = e1.instance(); + let i4 = e1.instance(); + let e1_c1 = e1.clone(); + let jh = spawn(async move { + let i5 = e1.instance(); + let i6 = e1.instance(); + assert_eq!(i1.await, 4); + assert_eq!(i5.await, 4); + assert_eq!(i6.await, 4); + }); + sleep(1000).await; + let resolved = e1_c1.resolve(4u16); + drop(i2); + drop(i3); + assert_eq!(i4.await, 4); + resolved.await; + jh.await; + } + + { + let e1 = EventualValueClone::new(); + let i1 = e1.instance(); + let i2 = e1.instance(); + let e1_c1 = e1.clone(); + let jh = spawn(async move { + assert_eq!(i1.await, 5); + assert_eq!(i2.await, 5); + }); + sleep(1000).await; + e1_c1.resolve(5u32).await; + jh.await; + e1_c1.reset(); + // + let j1 = e1.instance(); + let j2 = e1.instance(); + let jh = spawn(async move { + assert_eq!(j1.await, 6); + assert_eq!(j2.await, 6); + }); + sleep(1000).await; + e1_c1.resolve(6u32).await; + jh.await; + e1_c1.reset(); + } +} +pub async fn test_interval() { + info!("testing interval"); + + let tick: Arc> = Arc::new(Mutex::new(0u32)); + let stopper = interval(1000, move || { + let tick = tick.clone(); + async move { + let mut tick = tick.lock(); + trace!("tick {}", tick); + *tick += 1; + } + }); + + sleep(5500).await; + + stopper.await; +} + +pub async fn test_timeout() { + info!("testing timeout"); + + let tick: Arc> = Arc::new(Mutex::new(0u32)); + let tick_1 = tick.clone(); + assert!( + timeout(2500, async move { + let mut tick = tick_1.lock(); + trace!("tick {}", tick); + sleep(1000).await; + *tick += 1; + trace!("tick {}", tick); + sleep(1000).await; + *tick += 1; + trace!("tick {}", tick); + sleep(1000).await; + *tick += 1; + trace!("tick {}", tick); + sleep(1000).await; + *tick += 1; + }) + .await + .is_err(), + "should have timed out" + ); + + let ticks = *tick.lock(); + assert!(ticks <= 2); +} + +pub async fn test_sleep() { + info!("testing sleep"); + cfg_if! { + if #[cfg(target_arch = "wasm32")] { + + let t1 = Date::now(); + intf::sleep(1000).await; + let t2 = Date::now(); + assert!((t2-t1) >= 1000.0); + + } else { + + let sys_time = SystemTime::now(); + let one_sec = Duration::from_secs(1); + + sleep(1000).await; + assert!(sys_time.elapsed().unwrap() >= one_sec); + } + } +} + +macro_rules! assert_split_url { + ($url:expr, $scheme:expr, $host:expr) => { + assert_eq!( + SplitUrl::from_str($url), + Ok(SplitUrl::new($scheme, None, $host, None, None)) + ); + }; + ($url:expr, $scheme:expr, $host:expr, $port:expr) => { + assert_eq!( + SplitUrl::from_str($url), + Ok(SplitUrl::new($scheme, None, $host, $port, None)) + ); + }; + ($url:expr, $scheme:expr, $host:expr, $port:expr, $path:expr) => { + assert_eq!( + SplitUrl::from_str($url), + Ok(SplitUrl::new( + $scheme, + None, + $host, + $port, + Some(SplitUrlPath::new( + $path, + Option::::None, + Option::::None + )) + )) + ); + }; + ($url:expr, $scheme:expr, $host:expr, $port:expr, $path:expr, $frag:expr, $query:expr) => { + assert_eq!( + SplitUrl::from_str($url), + Ok(SplitUrl::new( + $scheme, + None, + $host, + $port, + Some(SplitUrlPath::new($path, $frag, $query)) + )) + ); + }; +} + +macro_rules! assert_split_url_parse { + ($url:expr) => { + let url = $url; + let su1 = SplitUrl::from_str(url).expect("should parse"); + assert_eq!(su1.to_string(), url); + }; +} + +fn host>(s: S) -> SplitUrlHost { + SplitUrlHost::Hostname(s.as_ref().to_owned()) +} + +fn ip>(s: S) -> SplitUrlHost { + SplitUrlHost::IpAddr(IpAddr::from_str(s.as_ref()).unwrap()) +} + +pub async fn test_split_url() { + info!("testing split_url"); + + assert_split_url!("http://foo", "http", host("foo")); + assert_split_url!("http://foo:1234", "http", host("foo"), Some(1234)); + assert_split_url!("http://foo:1234/", "http", host("foo"), Some(1234), ""); + assert_split_url!( + "http://foo:1234/asdf/qwer", + "http", + host("foo"), + Some(1234), + "asdf/qwer" + ); + assert_split_url!("http://foo/", "http", host("foo"), None, ""); + assert_split_url!("http://11.2.3.144/", "http", ip("11.2.3.144"), None, ""); + assert_split_url!("http://[1111::2222]/", "http", ip("1111::2222"), None, ""); + assert_split_url!( + "http://[1111::2222]:123/", + "http", + ip("1111::2222"), + Some(123), + "" + ); + + assert_split_url!( + "http://foo/asdf/qwer", + "http", + host("foo"), + None, + "asdf/qwer" + ); + assert_split_url!( + "http://foo/asdf/qwer#3", + "http", + host("foo"), + None, + "asdf/qwer", + Some("3"), + Option::::None + ); + assert_split_url!( + "http://foo/asdf/qwer?xxx", + "http", + host("foo"), + None, + "asdf/qwer", + Option::::None, + Some("xxx") + ); + assert_split_url!( + "http://foo/asdf/qwer#yyy?xxx", + "http", + host("foo"), + None, + "asdf/qwer", + Some("yyy"), + Some("xxx") + ); + assert_err!(SplitUrl::from_str("://asdf")); + assert_err!(SplitUrl::from_str("")); + assert_err!(SplitUrl::from_str("::")); + assert_err!(SplitUrl::from_str("://:")); + assert_err!(SplitUrl::from_str("a://:")); + assert_err!(SplitUrl::from_str("a://:1243")); + assert_err!(SplitUrl::from_str("a://:65536")); + assert_err!(SplitUrl::from_str("a://:-16")); + assert_err!(SplitUrl::from_str("a:///")); + assert_err!(SplitUrl::from_str("a:///qwer:")); + assert_err!(SplitUrl::from_str("a:///qwer://")); + assert_err!(SplitUrl::from_str("a://qwer://")); + assert_err!(SplitUrl::from_str("a://[1111::2222]:/")); + assert_err!(SplitUrl::from_str("a://[1111::2222]:")); + + assert_split_url_parse!("sch://foo:bar@baz.com:1234/fnord#qux?zuz"); + assert_split_url_parse!("sch://foo:bar@baz.com:1234/fnord#qux"); + assert_split_url_parse!("sch://foo:bar@baz.com:1234/fnord?zuz"); + assert_split_url_parse!("sch://foo:bar@baz.com:1234/fnord/"); + assert_split_url_parse!("sch://foo:bar@baz.com:1234//"); + assert_split_url_parse!("sch://foo:bar@baz.com:1234"); + assert_split_url_parse!("sch://foo:bar@[1111::2222]:1234"); + assert_split_url_parse!("sch://foo:bar@[::]:1234"); + assert_split_url_parse!("sch://foo:bar@1.2.3.4:1234"); + assert_split_url_parse!("sch://@baz.com:1234"); + assert_split_url_parse!("sch://baz.com/asdf/asdf"); + assert_split_url_parse!("sch://baz.com/"); + assert_split_url_parse!("s://s"); +} + +pub async fn test_get_random_u64() { + info!("testing random number generator for u64"); + let t1 = get_timestamp(); + let count = 10000; + for _ in 0..count { + let _ = get_random_u64(); + } + let t2 = get_timestamp(); + let tdiff = ((t2 - t1) as f64) / 1000000.0f64; + info!( + "running network interface test with {} iterations took {} seconds", + count, tdiff + ); +} + +pub async fn test_get_random_u32() { + info!("testing random number generator for u32"); + let t1 = get_timestamp(); + let count = 10000; + for _ in 0..count { + let _ = get_random_u32(); + } + let t2 = get_timestamp(); + let tdiff = ((t2 - t1) as f64) / 1000000.0f64; + info!( + "running network interface test with {} iterations took {} seconds", + count, tdiff + ); +} + +pub async fn test_must_join_single_future() { + info!("testing must join single future"); + let sf = MustJoinSingleFuture::::new(); + assert_eq!(sf.check().await, Ok(None)); + assert_eq!( + sf.single_spawn(async { + sleep(2000).await; + 69 + }) + .await, + Ok((None, true)) + ); + assert_eq!(sf.check().await, Ok(None)); + assert_eq!(sf.single_spawn(async { panic!() }).await, Ok((None, false))); + assert_eq!(sf.join().await, Ok(Some(69))); + assert_eq!( + sf.single_spawn(async { + sleep(1000).await; + 37 + }) + .await, + Ok((None, true)) + ); + sleep(2000).await; + assert_eq!( + sf.single_spawn(async { + sleep(1000).await; + 27 + }) + .await, + Ok((Some(37), true)) + ); + sleep(2000).await; + assert_eq!(sf.join().await, Ok(Some(27))); + assert_eq!(sf.check().await, Ok(None)); +} + +pub async fn test_tools() { + info!("testing retry_falloff_log"); + let mut last_us = 0u64; + for x in 0..1024 { + let cur_us = x as u64 * 1000000u64; + if retry_falloff_log(last_us, cur_us, 10_000_000u64, 6_000_000_000u64, 2.0f64) { + info!(" retry at {} secs", timestamp_to_secs(cur_us)); + last_us = cur_us; + } + } +} + +pub async fn test_all() { + test_log().await; + test_get_timestamp().await; + test_tools().await; + test_split_url().await; + test_get_random_u64().await; + test_get_random_u32().await; + test_sleep().await; + #[cfg(not(target_arch = "wasm32"))] + test_must_join_single_future().await; + test_eventual().await; + test_eventual_value().await; + test_eventual_value_clone().await; + test_interval().await; + test_timeout().await; +} diff --git a/veilid-tools/src/tests/files/cert.pem b/veilid-tools/src/tests/files/cert.pem new file mode 100644 index 00000000..b86846ed --- /dev/null +++ b/veilid-tools/src/tests/files/cert.pem @@ -0,0 +1,88 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 12:ce:63:bd:90:f5:ab:de:6d:7f:d7:3e:f3:e6:bb + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN=Veilid Test CA + Validity + Not Before: Nov 22 13:52:16 2021 GMT + Not After : Feb 25 13:52:16 2024 GMT + Subject: CN=Veilid Test Certificate + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (2048 bit) + Modulus: + 00:cb:2e:7a:47:81:be:6f:6b:53:37:51:c1:50:68: + 5a:44:3d:ba:b9:9b:78:40:84:35:d4:0e:e8:41:a6: + 0e:0a:b9:34:ae:97:a3:37:3e:81:ed:6c:0f:f8:8a: + 8b:0b:1a:ed:06:97:57:6d:49:a5:ec:b4:c4:d8:6d: + d2:57:c3:87:89:99:ee:b0:d7:c5:82:a1:dc:d5:98: + b3:ef:10:da:c0:5c:38:a2:bb:15:3e:0e:5e:bc:a0: + cd:a1:f0:07:67:bb:57:3f:89:cc:72:4f:bb:c0:a7: + ed:ad:15:07:61:c2:b4:21:73:39:00:9b:8f:aa:04: + 1b:c4:9d:d4:00:44:87:b1:79:b4:e1:4e:01:3c:ee: + a4:bb:f9:ad:5d:88:41:03:b4:bf:df:bf:71:24:ee: + 0b:69:59:55:dd:43:d1:91:04:de:98:9c:54:f2:ee: + 63:78:fe:76:19:bf:e6:5d:d6:58:81:3c:1b:02:3d: + 5d:cc:70:4a:c1:84:06:f6:1a:db:16:b0:e0:30:b0: + 3a:85:41:48:a1:88:c5:38:04:7b:03:c4:86:f0:da: + 1a:ff:bc:d1:ac:7f:cd:0c:e8:5a:42:5e:43:7f:0d: + 61:5d:41:67:0f:b8:07:47:21:93:44:b2:ab:fa:d8: + 69:bb:b9:6d:a1:56:6d:23:54:aa:49:67:e7:57:c6: + e9:c7 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + X509v3 Subject Key Identifier: + 70:ED:B0:96:71:33:43:16:EF:32:FF:69:11:C9:F0:02:3F:6C:81:88 + X509v3 Authority Key Identifier: + keyid:5D:7F:8D:AF:1A:56:D3:F4:CA:3D:D3:6D:EF:50:11:F7:64:99:6F:02 + DirName:/CN=Veilid Test CA + serial:22:7A:2E:68:7C:DF:7B:81:85:1A:50:98:16:62:22:D0:0B:D6:1C:4A + + X509v3 Extended Key Usage: + TLS Web Server Authentication + X509v3 Key Usage: + Digital Signature, Key Encipherment + X509v3 Subject Alternative Name: + DNS:Veilid Test Certificate + Signature Algorithm: sha256WithRSAEncryption + b8:fc:ac:62:d6:95:af:09:db:24:7d:82:2c:02:e1:d0:7b:f5: + 69:03:a4:42:55:c6:0d:2a:f1:9d:0e:c4:9b:78:40:7d:0d:7d: + ec:66:f6:c4:6d:06:d0:5b:58:de:ba:e6:67:ea:af:41:a3:87: + b4:37:8b:a8:1f:51:ae:70:e0:0d:f5:51:0a:7a:b3:b3:1d:d1: + 77:92:63:35:ae:50:9e:04:3d:04:6e:f1:60:c8:e3:8f:1f:75: + 47:05:27:a0:ff:c5:1b:30:68:b2:f9:5b:e6:f2:81:0f:9b:f2: + e8:8c:9d:b6:57:b2:c1:29:e7:d0:d0:88:b3:ba:8e:78:2e:ef: + ce:03:a3:12:fa:b4:e9:4e:1f:de:1a:cb:77:72:6b:71:98:02: + 37:d2:b4:02:f0:2c:08:67:ca:75:0d:af:81:bf:f8:57:f8:d9: + 4a:93:4f:db:3c:e1:af:3e:ab:9c:fe:87:f0:3a:01:21:6a:5c: + 99:83:e3:03:47:98:15:23:24:b3:ee:29:27:f4:f1:34:c1:e4: + f8:39:5a:92:da:c7:08:dc:71:87:1c:ff:67:e7:ef:24:bc:34: + e3:4e:e0:16:12:84:60:d4:7f:a2:c0:5b:85:a9:c5:ef:78:0b: + c3:64:cb:b4:05:eb:51:e5:c1:0f:60:da:5c:98:08:bf:5d:b9: + 1d:33:a7:26 +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIPEs5jvZD1q95tf9c+8+a7MA0GCSqGSIb3DQEBCwUAMBkx +FzAVBgNVBAMMDlZlaWxpZCBUZXN0IENBMB4XDTIxMTEyMjEzNTIxNloXDTI0MDIy +NTEzNTIxNlowIjEgMB4GA1UEAwwXVmVpbGlkIFRlc3QgQ2VydGlmaWNhdGUwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLLnpHgb5va1M3UcFQaFpEPbq5 +m3hAhDXUDuhBpg4KuTSul6M3PoHtbA/4iosLGu0Gl1dtSaXstMTYbdJXw4eJme6w +18WCodzVmLPvENrAXDiiuxU+Dl68oM2h8Adnu1c/icxyT7vAp+2tFQdhwrQhczkA +m4+qBBvEndQARIexebThTgE87qS7+a1diEEDtL/fv3Ek7gtpWVXdQ9GRBN6YnFTy +7mN4/nYZv+Zd1liBPBsCPV3McErBhAb2GtsWsOAwsDqFQUihiMU4BHsDxIbw2hr/ +vNGsf80M6FpCXkN/DWFdQWcPuAdHIZNEsqv62Gm7uW2hVm0jVKpJZ+dXxunHAgMB +AAGjgckwgcYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUcO2wlnEzQxbvMv9pEcnwAj9s +gYgwVAYDVR0jBE0wS4AUXX+NrxpW0/TKPdNt71AR92SZbwKhHaQbMBkxFzAVBgNV +BAMMDlZlaWxpZCBUZXN0IENBghQiei5ofN97gYUaUJgWYiLQC9YcSjATBgNVHSUE +DDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwIgYDVR0RBBswGYIXVmVpbGlkIFRl +c3QgQ2VydGlmaWNhdGUwDQYJKoZIhvcNAQELBQADggEBALj8rGLWla8J2yR9giwC +4dB79WkDpEJVxg0q8Z0OxJt4QH0Nfexm9sRtBtBbWN665mfqr0Gjh7Q3i6gfUa5w +4A31UQp6s7Md0XeSYzWuUJ4EPQRu8WDI448fdUcFJ6D/xRswaLL5W+bygQ+b8uiM +nbZXssEp59DQiLO6jngu784DoxL6tOlOH94ay3dya3GYAjfStALwLAhnynUNr4G/ ++Ff42UqTT9s84a8+q5z+h/A6ASFqXJmD4wNHmBUjJLPuKSf08TTB5Pg5WpLaxwjc +cYcc/2fn7yS8NONO4BYShGDUf6LAW4Wpxe94C8Nky7QF61HlwQ9g2lyYCL9duR0z +pyY= +-----END CERTIFICATE----- diff --git a/veilid-tools/src/tests/files/key.pem b/veilid-tools/src/tests/files/key.pem new file mode 100644 index 00000000..38968f47 --- /dev/null +++ b/veilid-tools/src/tests/files/key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAyy56R4G+b2tTN1HBUGhaRD26uZt4QIQ11A7oQaYOCrk0rpej +Nz6B7WwP+IqLCxrtBpdXbUml7LTE2G3SV8OHiZnusNfFgqHc1Ziz7xDawFw4orsV +Pg5evKDNofAHZ7tXP4nMck+7wKftrRUHYcK0IXM5AJuPqgQbxJ3UAESHsXm04U4B +PO6ku/mtXYhBA7S/379xJO4LaVlV3UPRkQTemJxU8u5jeP52Gb/mXdZYgTwbAj1d +zHBKwYQG9hrbFrDgMLA6hUFIoYjFOAR7A8SG8Noa/7zRrH/NDOhaQl5Dfw1hXUFn +D7gHRyGTRLKr+thpu7ltoVZtI1SqSWfnV8bpxwIDAQABAoIBAQCae5MjbUWC56JU +7EdEQKNpQVoIp2mt/BgFTPRQfdYtVxX0LX0+krss7r3R5lzDq8xN96HUiWur5uHI +APAuJI+YEr8GHHii0zjZ+onMmg8ItNWm/QGwtjJXzxeqKZsnxqwWtkoJHBCP8d5n +fBapwOU+jaHokV6RESCfxLSdI33cdGcOgDAn4/lvcXZ4Pq0qbitFuZwBPpobHbp4 +Mo94K7oh4KCt3FDMfZshkSF0wlquRIeUsI2uZUofybDa/j1RgEsqBZIrHqM6xXV1 +/r13+mMZC4otE0qhBV9jTYffaxooOnae8/Ve0FgaPWpNm7AD6p7l4a3csIkcggMS +xx7cntR5AoGBAOvPgDDLJ7h+AgY7jAd13/eZ75NtuEWbSq4c4Kwwy1K17L+3isX/ +RRkQ5qGTNsT6j1dddfwzX6Rhsi+JywczEdJdWgGNpFCIa8Ly2+47YLDpv0ZIISIZ +V0Ngg6dyuuQo7gFlLz9Dhe/If32/93gEW6HZOjn+GmQu53ZSDdHvukpjAoGBANyT +0GZzaP2ulV3Pk+9nJhC6eK2BZWF4ODXHsAgNUEyWZ4qDM71oDxyFbaWS2IDg7jz7 +T2wVffRFDcx8oNXWbhWbejSBGHWI8HRk0Ki83K0r7Cj8Uhy53rQjGOsdLf3K9T9h +GGVcwMHlBGIvswqTnJKysvgcoh1Ir+6RqbfCmG5NAoGAaVa8UQ+vor7HcLlRCFQj +xJvDZfxxgMaqSbUkuEbjzQLvy4TWPTSXTWc7X5o/sSasub5KYmsgonHyA0Juq7yo +jWyeNGttp3wJh4CttnJX8y+3/lFiW7UuQi7vIPIjgqC2EXF99ajYQBE0wpvqlHZ9 +6IL9e8KDT5WUWEq3WbzZXzkCgYB/0Md6FnZISdoTui0nFMZh+yvinpB4ookv4L6I +a+6T8rOc99oLbzkSdd7LiwQZ6j0i6R1krC+IVFtimvU39EFmE+oEcqoRsYBkcebX +YFkfn8wBE/Ug4DPEfnH6C7aS0gC68TCJy+2GbYbUvn8pKdAY0aQTUcQ+49fOjmmi +KgjaIQKBgQDoT0af/7a7LnY9dkbkz624HmNVyMPOa4/STrdxyy3NRhq/dysRW+At +x30nvCWpv0Z5BAyaUCrRWPGFxhv3/Z7qb4fx5uUbC3Jc04I5D6fwYqrQofGS8TMK +Lrg83o5Ag++pllu1IeWiGQPRbn7VZ+O6pISgpRpYBexXGyLJ6wtcAw== +-----END RSA PRIVATE KEY----- diff --git a/veilid-tools/src/tests/ios/.gitignore b/veilid-tools/src/tests/ios/.gitignore new file mode 100644 index 00000000..438326e0 --- /dev/null +++ b/veilid-tools/src/tests/ios/.gitignore @@ -0,0 +1,91 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilid-tools.c b/veilid-tools/src/tests/ios/veilidtools-tests/veilid-tools.c new file mode 100644 index 00000000..5d59813e --- /dev/null +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilid-tools.c @@ -0,0 +1,8 @@ +// +// veilidtools.c +// veilidtools-tests +// +// Created by JSmith on 7/6/21. +// + +#include "veilid-tools.h" diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilid-tools.h b/veilid-tools/src/tests/ios/veilidtools-tests/veilid-tools.h new file mode 100644 index 00000000..8f11707a --- /dev/null +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilid-tools.h @@ -0,0 +1,13 @@ +// +// veilid-tools.h +// veilid-tools-tests +// +// Created by JSmith on 7/6/21. +// + +#ifndef veilid_tools_h +#define veilid_tools_h + +void run_veilid_tools_tests(void); + +#endif /* veilid-tools_h */ diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests-Bridging-Header.h b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests-Bridging-Header.h new file mode 100644 index 00000000..3344ec39 --- /dev/null +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests-Bridging-Header.h @@ -0,0 +1,5 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#include "veilid-tools.h" diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj new file mode 100644 index 00000000..b47f1821 --- /dev/null +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj @@ -0,0 +1,413 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 4317C6BD2694A676009C717F /* veilid-tools.c in Sources */ = {isa = PBXBuildFile; fileRef = 4317C6BC2694A676009C717F /* veilid-tools.c */; }; + 43C436B0268904AC002D11C5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C436AF268904AC002D11C5 /* AppDelegate.swift */; }; + 43C436B2268904AC002D11C5 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C436B1268904AC002D11C5 /* SceneDelegate.swift */; }; + 43C436B4268904AC002D11C5 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C436B3268904AC002D11C5 /* ViewController.swift */; }; + 43C436B7268904AC002D11C5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 43C436B5268904AC002D11C5 /* Main.storyboard */; }; + 43C436B9268904AD002D11C5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43C436B8268904AD002D11C5 /* Assets.xcassets */; }; + 43C436BC268904AD002D11C5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 43C436BA268904AD002D11C5 /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 4317C6BA2694A675009C717F /* veilidtools-tests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "veilidtools-tests-Bridging-Header.h"; sourceTree = ""; }; + 4317C6BB2694A676009C717F /* veilid-tools.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "veilid-tools.h"; sourceTree = ""; }; + 4317C6BC2694A676009C717F /* veilid-tools.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = "veilid-tools.c"; sourceTree = ""; }; + 43C436AC268904AC002D11C5 /* veilidtools-tests.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "veilidtools-tests.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 43C436AF268904AC002D11C5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 43C436B1268904AC002D11C5 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 43C436B3268904AC002D11C5 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 43C436B6268904AC002D11C5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 43C436B8268904AD002D11C5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 43C436BB268904AD002D11C5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 43C436BD268904AD002D11C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 43C436A9268904AC002D11C5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 4317C6B7269490DA009C717F /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 43C436A3268904AC002D11C5 = { + isa = PBXGroup; + children = ( + 4317C6BB2694A676009C717F /* veilid-tools.h */, + 4317C6BC2694A676009C717F /* veilid-tools.c */, + 43C436AE268904AC002D11C5 /* veilidtools-tests */, + 43C436AD268904AC002D11C5 /* Products */, + 4317C6B7269490DA009C717F /* Frameworks */, + 4317C6BA2694A675009C717F /* veilidtools-tests-Bridging-Header.h */, + ); + sourceTree = ""; + }; + 43C436AD268904AC002D11C5 /* Products */ = { + isa = PBXGroup; + children = ( + 43C436AC268904AC002D11C5 /* veilidtools-tests.app */, + ); + name = Products; + sourceTree = ""; + }; + 43C436AE268904AC002D11C5 /* veilidtools-tests */ = { + isa = PBXGroup; + children = ( + 43C436AF268904AC002D11C5 /* AppDelegate.swift */, + 43C436B1268904AC002D11C5 /* SceneDelegate.swift */, + 43C436B3268904AC002D11C5 /* ViewController.swift */, + 43C436B5268904AC002D11C5 /* Main.storyboard */, + 43C436B8268904AD002D11C5 /* Assets.xcassets */, + 43C436BA268904AD002D11C5 /* LaunchScreen.storyboard */, + 43C436BD268904AD002D11C5 /* Info.plist */, + ); + path = "veilidtools-tests"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 43C436AB268904AC002D11C5 /* veilidtools-tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 43C436C0268904AD002D11C5 /* Build configuration list for PBXNativeTarget "veilidtools-tests" */; + buildPhases = ( + 43C436C326893020002D11C5 /* Cargo Build */, + 43C436A8268904AC002D11C5 /* Sources */, + 43C436A9268904AC002D11C5 /* Frameworks */, + 43C436AA268904AC002D11C5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "veilidtools-tests"; + productName = "veilidtools-tests"; + productReference = 43C436AC268904AC002D11C5 /* veilidtools-tests.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 43C436A4268904AC002D11C5 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1250; + LastUpgradeCheck = 1250; + TargetAttributes = { + 43C436AB268904AC002D11C5 = { + CreatedOnToolsVersion = 12.5.1; + LastSwiftMigration = 1250; + }; + }; + }; + buildConfigurationList = 43C436A7268904AC002D11C5 /* Build configuration list for PBXProject "veilidtools-tests" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 43C436A3268904AC002D11C5; + productRefGroup = 43C436AD268904AC002D11C5 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 43C436AB268904AC002D11C5 /* veilidtools-tests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 43C436AA268904AC002D11C5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 43C436BC268904AD002D11C5 /* LaunchScreen.storyboard in Resources */, + 43C436B9268904AD002D11C5 /* Assets.xcassets in Resources */, + 43C436B7268904AC002D11C5 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 43C436C326893020002D11C5 /* Cargo Build */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Cargo Build"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "../../../../ios_build.sh --features ios_tests\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 43C436A8268904AC002D11C5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 43C436B4268904AC002D11C5 /* ViewController.swift in Sources */, + 43C436B0268904AC002D11C5 /* AppDelegate.swift in Sources */, + 43C436B2268904AC002D11C5 /* SceneDelegate.swift in Sources */, + 4317C6BD2694A676009C717F /* veilid-tools.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 43C436B5268904AC002D11C5 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 43C436B6268904AC002D11C5 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 43C436BA268904AD002D11C5 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 43C436BB268904AD002D11C5 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 43C436BE268904AD002D11C5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = arm64; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.3; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 43C436BF268904AD002D11C5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = arm64; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.3; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 43C436C1268904AD002D11C5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ZJPQSFX5MW; + INFOPLIST_FILE = "veilidtools-tests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 12.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + OTHER_LDFLAGS = ""; + "OTHER_LDFLAGS[sdk=iphoneos*]" = ( + "-L../../../../../target/aarch64-apple-ios/debug", + "-lveilid_tools", + ); + "OTHER_LDFLAGS[sdk=iphonesimulator*]" = ( + "-L../../../../../target/x86_64-apple-ios/debug", + "-lveilid_tools", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.veilid.veilidtools-tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "veilidtools-tests-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 43C436C2268904AD002D11C5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ZJPQSFX5MW; + INFOPLIST_FILE = "veilidtools-tests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 12.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + OTHER_LDFLAGS = ""; + "OTHER_LDFLAGS[sdk=iphoneos*]" = ( + "-L../../../../../target/aarch64-apple-ios/release", + "-lveilid_tools", + ); + "OTHER_LDFLAGS[sdk=iphonesimulator*]" = ( + "-L../../../../../target/x86_64-apple-ios/release", + "-lveilid_tools", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.veilid.veilidtools-tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "veilidtools-tests-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 43C436A7268904AC002D11C5 /* Build configuration list for PBXProject "veilidtools-tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 43C436BE268904AD002D11C5 /* Debug */, + 43C436BF268904AD002D11C5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 43C436C0268904AD002D11C5 /* Build configuration list for PBXNativeTarget "veilidtools-tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 43C436C1268904AD002D11C5 /* Debug */, + 43C436C2268904AD002D11C5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 43C436A4268904AC002D11C5 /* Project object */; +} diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/xcshareddata/xcschemes/veilidcore-tests.xcscheme b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/xcshareddata/xcschemes/veilidcore-tests.xcscheme new file mode 100644 index 00000000..f1525db5 --- /dev/null +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/xcshareddata/xcschemes/veilidcore-tests.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/AppDelegate.swift b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/AppDelegate.swift new file mode 100644 index 00000000..7b8b8e2b --- /dev/null +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/AppDelegate.swift @@ -0,0 +1,37 @@ +// +// AppDelegate.swift +// veilidtools-tests +// +// Created by JSmith on 6/27/21. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window : UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + @available(iOS 13.0, *) + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + @available(iOS 13.0, *) + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Assets.xcassets/AccentColor.colorset/Contents.json b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Assets.xcassets/AppIcon.appiconset/Contents.json b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..9221b9bb --- /dev/null +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Assets.xcassets/Contents.json b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Base.lproj/LaunchScreen.storyboard b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..3fd36928 --- /dev/null +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Base.lproj/Main.storyboard b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Base.lproj/Main.storyboard new file mode 100644 index 00000000..678f8974 --- /dev/null +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Base.lproj/Main.storyboard @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Info.plist b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Info.plist new file mode 100644 index 00000000..5b531f7b --- /dev/null +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/Info.plist @@ -0,0 +1,66 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/SceneDelegate.swift b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/SceneDelegate.swift new file mode 100644 index 00000000..5ea6baef --- /dev/null +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/SceneDelegate.swift @@ -0,0 +1,53 @@ +// +// SceneDelegate.swift +// veilidtools-tests +// +// Created by JSmith on 6/27/21. +// + +import UIKit + +@available(iOS 13.0, *) +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/ViewController.swift b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/ViewController.swift new file mode 100644 index 00000000..922d5462 --- /dev/null +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/ViewController.swift @@ -0,0 +1,19 @@ +// +// ViewController.swift +// veilidtools-tests +// +// Created by JSmith on 6/27/21. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + run_veilid_tools_tests() + } + + +} + diff --git a/veilid-tools/src/tests/mod.rs b/veilid-tools/src/tests/mod.rs new file mode 100644 index 00000000..0b6b6216 --- /dev/null +++ b/veilid-tools/src/tests/mod.rs @@ -0,0 +1,3 @@ +pub mod common; +#[cfg(not(target_arch = "wasm32"))] +mod native; diff --git a/veilid-tools/src/tests/native/mod.rs b/veilid-tools/src/tests/native/mod.rs new file mode 100644 index 00000000..9ab6f14d --- /dev/null +++ b/veilid-tools/src/tests/native/mod.rs @@ -0,0 +1,152 @@ +//! Test suite for Native +#![cfg(not(target_arch = "wasm32"))] + +mod test_async_peek_stream; + +use crate::tests::common::*; +use crate::*; + +#[cfg(all(target_os = "android", feature = "android_tests"))] +use jni::{objects::JClass, objects::JObject, JNIEnv}; + +#[cfg(all(target_os = "android", feature = "android_tests"))] +#[no_mangle] +#[allow(non_snake_case)] +pub extern "system" fn Java_com_veilid_veilidtools_veilidtools_1android_1tests_MainActivity_run_1tests( + env: JNIEnv, + _class: JClass, + ctx: JObject, +) { + crate::intf::utils::android::veilid_tools_setup_android( + env, + ctx, + "veilid_tools", + crate::veilid_config::VeilidConfigLogLevel::Trace, + ); + run_all_tests(); +} + +#[cfg(all(target_os = "ios", feature = "ios_tests"))] +#[no_mangle] +pub extern "C" fn run_veilid_tools_tests() { + let log_path: std::path::PathBuf = [ + std::env::var("HOME").unwrap().as_str(), + "Documents", + "veilid-tools.log", + ] + .iter() + .collect(); + crate::intf::utils::ios_test_setup::veilid_tools_setup( + "veilid-tools", + Some(Level::Trace), + Some((Level::Trace, log_path.as_path())), + ); + run_all_tests(); +} + +/////////////////////////////////////////////////////////////////////////// + +#[allow(dead_code)] +pub fn run_all_tests() { + info!("TEST: exec_test_host_interface"); + exec_test_host_interface(); + info!("TEST: exec_test_async_peek_stream"); + exec_test_async_peek_stream(); + info!("TEST: exec_test_async_tag_lock"); + exec_test_async_tag_lock(); + + info!("Finished unit tests"); +} + +#[cfg(feature = "rt-tokio")] +fn block_on, T>(f: F) -> T { + let rt = tokio::runtime::Runtime::new().unwrap(); + let local = tokio::task::LocalSet::new(); + local.block_on(&rt, f) +} +#[cfg(feature = "rt-async-std")] +fn block_on, T>(f: F) -> T { + async_std::task::block_on(f) +} + +fn exec_test_host_interface() { + block_on(async { + test_host_interface::test_all().await; + }); +} +fn exec_test_async_peek_stream() { + block_on(async { + test_async_peek_stream::test_all().await; + }) +} +fn exec_test_async_tag_lock() { + block_on(async { + test_async_tag_lock::test_all().await; + }) +} +/////////////////////////////////////////////////////////////////////////// +cfg_if! { + if #[cfg(test)] { + + static DEFAULT_LOG_IGNORE_LIST: [&str; 21] = [ + "mio", + "h2", + "hyper", + "tower", + "tonic", + "tokio", + "runtime", + "tokio_util", + "want", + "serial_test", + "async_std", + "async_io", + "polling", + "rustls", + "async_tungstenite", + "tungstenite", + "netlink_proto", + "netlink_sys", + "trust_dns_resolver", + "trust_dns_proto", + "attohttpc", + ]; + + use serial_test::serial; + use simplelog::*; + use std::sync::Once; + + static SETUP_ONCE: Once = Once::new(); + + pub fn setup() { + SETUP_ONCE.call_once(|| { + let mut cb = ConfigBuilder::new(); + for ig in DEFAULT_LOG_IGNORE_LIST { + cb.add_filter_ignore_str(ig); + } + TestLogger::init(LevelFilter::Trace, cb.build()).unwrap(); + }); + } + + #[test] + #[serial] + fn run_test_host_interface() { + setup(); + exec_test_host_interface(); + } + + #[test] + #[serial] + fn run_test_async_peek_stream() { + setup(); + exec_test_async_peek_stream(); + } + + #[test] + #[serial] + fn run_test_async_tag_lock() { + setup(); + exec_test_async_tag_lock(); + } + } +} diff --git a/veilid-tools/src/tests/native/test_async_peek_stream.rs b/veilid-tools/src/tests/native/test_async_peek_stream.rs new file mode 100644 index 00000000..0797a4f8 --- /dev/null +++ b/veilid-tools/src/tests/native/test_async_peek_stream.rs @@ -0,0 +1,352 @@ +use crate::*; + +cfg_if! { + if #[cfg(feature="rt-async-std")] { + use async_std::net::{TcpListener, TcpStream}; + use async_std::prelude::FutureExt; + use async_std::task::sleep; + } else if #[cfg(feature="rt-tokio")] { + use tokio::net::{TcpListener, TcpStream}; + use tokio::time::sleep; + use tokio_util::compat::*; + } +} + +use futures_util::{AsyncReadExt, AsyncWriteExt}; +use std::io; + +static MESSAGE: &[u8; 62] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + +async fn make_tcp_loopback() -> Result<(TcpStream, TcpStream), io::Error> { + let listener = TcpListener::bind("127.0.0.1:0").await?; + let local_addr = listener.local_addr()?; + + let accept_future = async { + let (accepted_stream, peer_address) = listener.accept().await?; + trace!("connection from {}", peer_address); + accepted_stream.set_nodelay(true)?; + Result::::Ok(accepted_stream) + }; + let connect_future = async { + sleep(Duration::from_secs(1)).await; + let connected_stream = TcpStream::connect(local_addr).await?; + connected_stream.set_nodelay(true)?; + Result::::Ok(connected_stream) + }; + + cfg_if! { + if #[cfg(feature="rt-async-std")] { + accept_future.try_join(connect_future).await + } else if #[cfg(feature="rt-tokio")] { + tokio::try_join!(accept_future, connect_future) + } + } +} + +async fn make_async_peek_stream_loopback() -> (AsyncPeekStream, AsyncPeekStream) { + let (acc, conn) = make_tcp_loopback().await.unwrap(); + #[cfg(feature = "rt-tokio")] + let acc = acc.compat(); + #[cfg(feature = "rt-tokio")] + let conn = conn.compat(); + + let aps_a = AsyncPeekStream::new(acc); + let aps_c = AsyncPeekStream::new(conn); + + (aps_a, aps_c) +} + +#[cfg(feature = "rt-tokio")] +async fn make_stream_loopback() -> (Compat, Compat) { + let (a, c) = make_tcp_loopback().await.unwrap(); + (a.compat(), c.compat()) +} +#[cfg(feature = "rt-async-std")] +async fn make_stream_loopback() -> (TcpStream, TcpStream) { + make_tcp_loopback().await.unwrap() +} + +pub async fn test_nothing() { + info!("test_nothing"); + let (mut a, mut c) = make_stream_loopback().await; + let outbuf = MESSAGE.to_vec(); + + a.write_all(&outbuf).await.unwrap(); + + let mut inbuf: Vec = Vec::new(); + inbuf.resize(outbuf.len(), 0u8); + c.read_exact(&mut inbuf).await.unwrap(); + + assert_eq!(inbuf, outbuf); +} + +pub async fn test_no_peek() { + info!("test_no_peek"); + let (mut a, mut c) = make_async_peek_stream_loopback().await; + + let outbuf = MESSAGE.to_vec(); + + a.write_all(&outbuf).await.unwrap(); + + let mut inbuf: Vec = Vec::new(); + inbuf.resize(outbuf.len(), 0u8); + c.read_exact(&mut inbuf).await.unwrap(); + + assert_eq!(inbuf, outbuf); +} + +pub async fn test_peek_all_read() { + info!("test_peek_all_read"); + + let (mut a, mut c) = make_async_peek_stream_loopback().await; + // write everything + let outbuf = MESSAGE.to_vec(); + a.write_all(&outbuf).await.unwrap(); + + // peek everything + let mut peekbuf1: Vec = Vec::new(); + peekbuf1.resize(outbuf.len(), 0u8); + let peeksize1 = c.peek(&mut peekbuf1).await.unwrap(); + + assert_eq!(peeksize1, peekbuf1.len()); + // read everything + let mut inbuf: Vec = Vec::new(); + inbuf.resize(outbuf.len(), 0u8); + c.read_exact(&mut inbuf).await.unwrap(); + + assert_eq!(inbuf, outbuf); + assert_eq!(peekbuf1, outbuf); +} + +pub async fn test_peek_some_read() { + info!("test_peek_some_read"); + + let (mut a, mut c) = make_async_peek_stream_loopback().await; + + // write everything + let outbuf = MESSAGE.to_vec(); + a.write_all(&outbuf).await.unwrap(); + + // peek partially + let mut peekbuf1: Vec = Vec::new(); + peekbuf1.resize(outbuf.len() / 2, 0u8); + let peeksize1 = c.peek(&mut peekbuf1).await.unwrap(); + assert_eq!(peeksize1, peekbuf1.len()); + // read everything + let mut inbuf: Vec = Vec::new(); + inbuf.resize(outbuf.len(), 0u8); + c.read_exact(&mut inbuf).await.unwrap(); + + assert_eq!(inbuf, outbuf); + assert_eq!(peekbuf1, outbuf[0..peeksize1].to_vec()); +} + +pub async fn test_peek_some_peek_some_read() { + info!("test_peek_some_peek_some_read"); + + let (mut a, mut c) = make_async_peek_stream_loopback().await; + + // write everything + let outbuf = MESSAGE.to_vec(); + a.write_all(&outbuf).await.unwrap(); + + // peek partially + let mut peekbuf1: Vec = Vec::new(); + peekbuf1.resize(outbuf.len() / 4, 0u8); + let peeksize1 = c.peek(&mut peekbuf1).await.unwrap(); + assert_eq!(peeksize1, peekbuf1.len()); + + // peek partially + let mut peekbuf2: Vec = Vec::new(); + peekbuf2.resize(peeksize1 + 1, 0u8); + let peeksize2 = c.peek(&mut peekbuf2).await.unwrap(); + assert_eq!(peeksize2, peekbuf2.len()); + + // read everything + let mut inbuf: Vec = Vec::new(); + inbuf.resize(outbuf.len(), 0u8); + c.read_exact(&mut inbuf).await.unwrap(); + + assert_eq!(inbuf, outbuf); + assert_eq!(peekbuf1, outbuf[0..peeksize1].to_vec()); + assert_eq!(peekbuf2, outbuf[0..peeksize2].to_vec()); +} + +pub async fn test_peek_some_read_peek_some_read() { + info!("test_peek_some_read_peek_some_read"); + + let (mut a, mut c) = make_async_peek_stream_loopback().await; + + // write everything + let outbuf = MESSAGE.to_vec(); + a.write_all(&outbuf).await.unwrap(); + + // peek partially + let mut peekbuf1: Vec = Vec::new(); + peekbuf1.resize(outbuf.len() / 4, 0u8); + let peeksize1 = c.peek(&mut peekbuf1).await.unwrap(); + assert_eq!(peeksize1, peekbuf1.len()); + + // read partially + let mut inbuf1: Vec = Vec::new(); + inbuf1.resize(peeksize1 - 1, 0u8); + c.read_exact(&mut inbuf1).await.unwrap(); + + // peek partially + let mut peekbuf2: Vec = Vec::new(); + peekbuf2.resize(2, 0u8); + let peeksize2 = c.peek(&mut peekbuf2).await.unwrap(); + assert_eq!(peeksize2, peekbuf2.len()); + + // read partially + let mut inbuf2: Vec = Vec::new(); + inbuf2.resize(2, 0u8); + c.read_exact(&mut inbuf2).await.unwrap(); + + assert_eq!(peekbuf1, outbuf[0..peeksize1].to_vec()); + assert_eq!(inbuf1, outbuf[0..peeksize1 - 1].to_vec()); + assert_eq!(peekbuf2, outbuf[peeksize1 - 1..peeksize1 + 1].to_vec()); + assert_eq!(inbuf2, peekbuf2); +} + +pub async fn test_peek_some_read_peek_all_read() { + info!("test_peek_some_read_peek_all_read"); + + let (mut a, mut c) = make_async_peek_stream_loopback().await; + + // write everything + let outbuf = MESSAGE.to_vec(); + a.write_all(&outbuf).await.unwrap(); + + // peek partially + let mut peekbuf1: Vec = Vec::new(); + peekbuf1.resize(outbuf.len() / 4, 0u8); + let peeksize1 = c.peek(&mut peekbuf1).await.unwrap(); + assert_eq!(peeksize1, peekbuf1.len()); + + // read partially + let mut inbuf1: Vec = Vec::new(); + inbuf1.resize(peeksize1 + 1, 0u8); + c.read_exact(&mut inbuf1).await.unwrap(); + + // peek past end + let mut peekbuf2: Vec = Vec::new(); + peekbuf2.resize(outbuf.len(), 0u8); + let peeksize2 = c.peek(&mut peekbuf2).await.unwrap(); + assert_eq!(peeksize2, outbuf.len() - (peeksize1 + 1)); + + // read remaining + let mut inbuf2: Vec = Vec::new(); + inbuf2.resize(peeksize2, 0u8); + c.read_exact(&mut inbuf2).await.unwrap(); + + assert_eq!(peekbuf1, outbuf[0..peeksize1].to_vec()); + assert_eq!(inbuf1, outbuf[0..peeksize1 + 1].to_vec()); + assert_eq!( + peekbuf2[0..peeksize2].to_vec(), + outbuf[peeksize1 + 1..outbuf.len()].to_vec() + ); + assert_eq!(inbuf2, peekbuf2[0..peeksize2].to_vec()); +} + +pub async fn test_peek_some_read_peek_some_read_all_read() { + info!("test_peek_some_read_peek_some_read_peek_all_read"); + + let (mut a, mut c) = make_async_peek_stream_loopback().await; + + // write everything + let outbuf = MESSAGE.to_vec(); + a.write_all(&outbuf).await.unwrap(); + + // peek partially + let mut peekbuf1: Vec = Vec::new(); + peekbuf1.resize(outbuf.len() / 4, 0u8); + let peeksize1 = c.peek(&mut peekbuf1).await.unwrap(); + assert_eq!(peeksize1, peekbuf1.len()); + + // read partially + let mut inbuf1: Vec = Vec::new(); + inbuf1.resize(peeksize1 - 1, 0u8); + c.read_exact(&mut inbuf1).await.unwrap(); + + // peek partially + let mut peekbuf2: Vec = Vec::new(); + peekbuf2.resize(2, 0u8); + let peeksize2 = c.peek(&mut peekbuf2).await.unwrap(); + assert_eq!(peeksize2, peekbuf2.len()); + // read partially + let mut inbuf2: Vec = Vec::new(); + inbuf2.resize(1, 0u8); + c.read_exact(&mut inbuf2).await.unwrap(); + + // read remaining + let mut inbuf3: Vec = Vec::new(); + inbuf3.resize(outbuf.len() - peeksize1, 0u8); + c.read_exact(&mut inbuf3).await.unwrap(); + + assert_eq!(peekbuf1, outbuf[0..peeksize1].to_vec()); + assert_eq!(inbuf1, outbuf[0..peeksize1 - 1].to_vec()); + assert_eq!( + peekbuf2[0..peeksize2].to_vec(), + outbuf[peeksize1 - 1..peeksize1 + 1].to_vec() + ); + assert_eq!(inbuf2, peekbuf2[0..1].to_vec()); + assert_eq!(inbuf3, outbuf[peeksize1..outbuf.len()].to_vec()); +} + +pub async fn test_peek_exact_read_peek_exact_read_all_read() { + info!("test_peek_exact_read_peek_exact_read_all_read"); + + let (mut a, mut c) = make_async_peek_stream_loopback().await; + + // write everything + let outbuf = MESSAGE.to_vec(); + a.write_all(&outbuf).await.unwrap(); + + // peek partially + let mut peekbuf1: Vec = Vec::new(); + peekbuf1.resize(outbuf.len() / 4, 0u8); + let peeksize1 = c.peek_exact(&mut peekbuf1).await.unwrap(); + assert_eq!(peeksize1, peekbuf1.len()); + + // read partially + let mut inbuf1: Vec = Vec::new(); + inbuf1.resize(peeksize1 - 1, 0u8); + c.read_exact(&mut inbuf1).await.unwrap(); + + // peek partially + let mut peekbuf2: Vec = Vec::new(); + peekbuf2.resize(2, 0u8); + let peeksize2 = c.peek_exact(&mut peekbuf2).await.unwrap(); + assert_eq!(peeksize2, peekbuf2.len()); + // read partially + let mut inbuf2: Vec = Vec::new(); + inbuf2.resize(1, 0u8); + c.read_exact(&mut inbuf2).await.unwrap(); + + // read remaining + let mut inbuf3: Vec = Vec::new(); + inbuf3.resize(outbuf.len() - peeksize1, 0u8); + c.read_exact(&mut inbuf3).await.unwrap(); + + assert_eq!(peekbuf1, outbuf[0..peeksize1].to_vec()); + assert_eq!(inbuf1, outbuf[0..peeksize1 - 1].to_vec()); + assert_eq!( + peekbuf2[0..peeksize2].to_vec(), + outbuf[peeksize1 - 1..peeksize1 + 1].to_vec() + ); + assert_eq!(inbuf2, peekbuf2[0..1].to_vec()); + assert_eq!(inbuf3, outbuf[peeksize1..outbuf.len()].to_vec()); +} + +pub async fn test_all() { + test_nothing().await; + test_no_peek().await; + test_peek_all_read().await; + test_peek_some_read().await; + test_peek_some_peek_some_read().await; + test_peek_some_read_peek_some_read().await; + test_peek_some_read_peek_all_read().await; + test_peek_some_read_peek_some_read_all_read().await; + test_peek_exact_read_peek_exact_read_all_read().await; +} diff --git a/veilid-tools/tests/web.rs b/veilid-tools/tests/web.rs new file mode 100644 index 00000000..7e7c3c5f --- /dev/null +++ b/veilid-tools/tests/web.rs @@ -0,0 +1,87 @@ +//! Test suite for the Web and headless browsers. +#![cfg(target_arch = "wasm32")] + +use veilid_tools::tests::common::*; +use veilid_tools::xx::*; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +extern crate wee_alloc; +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +static SETUP_ONCE: Once = Once::new(); +pub fn setup() -> () { + SETUP_ONCE.call_once(|| { + console_error_panic_hook::set_once(); + let mut builder = tracing_wasm::WASMLayerConfigBuilder::new(); + builder.set_report_logs_in_timings(false); + builder.set_max_level(Level::TRACE); + builder.set_console_config(tracing_wasm::ConsoleConfig::ReportWithConsoleColor); + tracing_wasm::set_as_global_default_with_config(builder.build()); + }); +} + +#[wasm_bindgen_test] +async fn run_test_dht_key() { + setup(); + + test_dht_key::test_all().await; +} + +#[wasm_bindgen_test] +async fn run_test_host_interface() { + setup(); + + test_host_interface::test_all().await; +} + +#[wasm_bindgen_test] +async fn run_test_veilid_tools() { + setup(); + + test_veilid_tools::test_all().await; +} + +#[wasm_bindgen_test] +async fn run_test_config() { + setup(); + + test_veilid_config::test_all().await; +} + +#[wasm_bindgen_test] +async fn run_test_connection_table() { + setup(); + + test_connection_table::test_all().await; +} + +#[wasm_bindgen_test] +async fn run_test_table_store() { + setup(); + + test_table_store::test_all().await; +} + +#[wasm_bindgen_test] +async fn run_test_crypto() { + setup(); + + test_crypto::test_all().await; +} + +#[wasm_bindgen_test] +async fn run_test_envelope_receipt() { + setup(); + + test_envelope_receipt::test_all().await; +} + +#[wasm_bindgen_test] +async fn run_test_async_tag_lock() { + setup(); + + test_async_tag_lock::test_all().await; +} From 87366c7fb2c67f4a64370ee8430d0c9fe461676e Mon Sep 17 00:00:00 2001 From: John Smith Date: Sun, 27 Nov 2022 09:03:10 -0500 Subject: [PATCH 12/88] xfer --- veilid-tools/src/split_url.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/veilid-tools/src/split_url.rs b/veilid-tools/src/split_url.rs index 9cfd8c94..033ffc96 100644 --- a/veilid-tools/src/split_url.rs +++ b/veilid-tools/src/split_url.rs @@ -9,8 +9,6 @@ use super::*; -use std::str::FromStr; - fn is_alphanum(c: u8) -> bool { matches!(c, b'A'..=b'Z' From 80c8a62ea1fea91b9078887da706655e08895297 Mon Sep 17 00:00:00 2001 From: John Smith Date: Sun, 27 Nov 2022 10:52:07 -0500 Subject: [PATCH 13/88] wasm fixes --- Cargo.lock | 2 + veilid-tools/Cargo.toml | 8 ++- veilid-tools/src/random.rs | 2 + veilid-tools/src/sleep.rs | 2 +- .../src/tests/common/test_host_interface.rs | 2 +- veilid-tools/src/timeout.rs | 2 +- veilid-tools/src/timestamp.rs | 2 +- veilid-tools/src/wasm.rs | 25 +++---- veilid-tools/tests/web.rs | 65 +++---------------- 9 files changed, 36 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 39b78d46..a66192cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5741,6 +5741,7 @@ dependencies = [ "async-std", "async_executors", "cfg-if 1.0.0", + "console_error_panic_hook", "eyre", "futures-util", "jni", @@ -5766,6 +5767,7 @@ dependencies = [ "tokio-util", "tracing", "tracing-android", + "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", diff --git a/veilid-tools/Cargo.toml b/veilid-tools/Cargo.toml index 9d306626..2095866f 100644 --- a/veilid-tools/Cargo.toml +++ b/veilid-tools/Cargo.toml @@ -9,7 +9,7 @@ license = "LGPL-2.0-or-later OR MPL-2.0 OR (MIT AND BSD-3-Clause)" crate-type = ["rlib"] [features] -default = [ "rt-tokio", "tracing" ] +default = [ "tracing" ] rt-async-std = [ "async-std", "async_executors/async_std", ] rt-tokio = [ "tokio", "tokio-util", "async_executors/tokio_tp", "async_executors/tokio_io", "async_executors/tokio_timer", ] @@ -54,14 +54,13 @@ async_executors = { version = "^0", default-features = false, features = [ "bind async-lock = "^2" send_wrapper = { version = "^0.6", features = ["futures"] } - # Dependencies for Android [target.'cfg(target_os = "android")'.dependencies] jni = "^0" jni-sys = "^0" ndk = { version = "^0", features = ["trace"] } ndk-glue = { version = "^0", features = ["logger"] } -tracing-android = { version = "^0" } +tracing-android = { version = "^0", optional = true } # Dependencies for Windows # [target.'cfg(target_os = "windows")'.dependencies] @@ -81,9 +80,12 @@ serial_test = "^0" simplelog = { version = "^0", features=["test"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] +console_error_panic_hook = "^0" wasm-bindgen-test = "^0" wee_alloc = "^0" wasm-logger = "^0" +tracing-wasm = { version = "^0" } +parking_lot = { version = "^0", features = ["wasm-bindgen"]} ### BUILD OPTIONS diff --git a/veilid-tools/src/random.rs b/veilid-tools/src/random.rs index 2f395b55..9c3d9fa7 100644 --- a/veilid-tools/src/random.rs +++ b/veilid-tools/src/random.rs @@ -28,6 +28,8 @@ impl RngCore for VeilidRng { cfg_if! { if #[cfg(target_arch = "wasm32")] { + use js_sys::Math; + pub fn random_bytes(dest: &mut [u8]) -> EyreResult<()> { let len = dest.len(); let u32len = len / 4; diff --git a/veilid-tools/src/sleep.rs b/veilid-tools/src/sleep.rs index c0d4a899..31c458c5 100644 --- a/veilid-tools/src/sleep.rs +++ b/veilid-tools/src/sleep.rs @@ -3,7 +3,7 @@ use std::time::Duration; cfg_if! { if #[cfg(target_arch = "wasm32")] { - use async_executors::Bindgen; + use async_executors::{Bindgen, Timer}; pub async fn sleep(millis: u32) { Bindgen.sleep(Duration::from_millis(millis.into())).await diff --git a/veilid-tools/src/tests/common/test_host_interface.rs b/veilid-tools/src/tests/common/test_host_interface.rs index 2d73c399..8f31a953 100644 --- a/veilid-tools/src/tests/common/test_host_interface.rs +++ b/veilid-tools/src/tests/common/test_host_interface.rs @@ -295,7 +295,7 @@ pub async fn test_sleep() { if #[cfg(target_arch = "wasm32")] { let t1 = Date::now(); - intf::sleep(1000).await; + sleep(1000).await; let t2 = Date::now(); assert!((t2-t1) >= 1000.0); diff --git a/veilid-tools/src/timeout.rs b/veilid-tools/src/timeout.rs index 07858381..1ca2df26 100644 --- a/veilid-tools/src/timeout.rs +++ b/veilid-tools/src/timeout.rs @@ -7,7 +7,7 @@ cfg_if! { where F: Future, { - match select(Box::pin(intf::sleep(dur_ms)), Box::pin(f)).await { + match select(Box::pin(sleep(dur_ms)), Box::pin(f)).await { Either::Left((_x, _b)) => Err(TimeoutError()), Either::Right((y, _a)) => Ok(y), } diff --git a/veilid-tools/src/timestamp.rs b/veilid-tools/src/timestamp.rs index af042327..4b5c187e 100644 --- a/veilid-tools/src/timestamp.rs +++ b/veilid-tools/src/timestamp.rs @@ -5,7 +5,7 @@ cfg_if! { use js_sys::Date; pub fn get_timestamp() -> u64 { - if utils::is_browser() { + if is_browser() { return (Date::now() * 1000.0f64) as u64; } else { panic!("WASM requires browser environment"); diff --git a/veilid-tools/src/wasm.rs b/veilid-tools/src/wasm.rs index 34408dbf..bfecbf9b 100644 --- a/veilid-tools/src/wasm.rs +++ b/veilid-tools/src/wasm.rs @@ -1,6 +1,7 @@ use super::*; use core::sync::atomic::{AtomicI8, Ordering}; use js_sys::{global, Reflect}; +use wasm_bindgen::prelude::*; #[wasm_bindgen] extern "C" { @@ -27,21 +28,21 @@ pub fn is_browser() -> bool { res } -// pub fn is_browser_https() -> bool { -// static CACHE: AtomicI8 = AtomicI8::new(-1); -// let cache = CACHE.load(Ordering::Relaxed); -// if cache != -1 { -// return cache != 0; -// } +pub fn is_browser_https() -> bool { + static CACHE: AtomicI8 = AtomicI8::new(-1); + let cache = CACHE.load(Ordering::Relaxed); + if cache != -1 { + return cache != 0; + } -// let res = js_sys::eval("window.location.protocol === 'https'") -// .map(|res| res.is_truthy()) -// .unwrap_or_default(); + let res = js_sys::eval("window.location.protocol === 'https'") + .map(|res| res.is_truthy()) + .unwrap_or_default(); -// CACHE.store(res as i8, Ordering::Relaxed); + CACHE.store(res as i8, Ordering::Relaxed); -// res -// } + res +} #[derive(ThisError, Debug, Clone, Eq, PartialEq)] #[error("JsValue error")] diff --git a/veilid-tools/tests/web.rs b/veilid-tools/tests/web.rs index 7e7c3c5f..6e4ba0f8 100644 --- a/veilid-tools/tests/web.rs +++ b/veilid-tools/tests/web.rs @@ -2,7 +2,7 @@ #![cfg(target_arch = "wasm32")] use veilid_tools::tests::common::*; -use veilid_tools::xx::*; +use veilid_tools::*; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); @@ -15,21 +15,18 @@ static SETUP_ONCE: Once = Once::new(); pub fn setup() -> () { SETUP_ONCE.call_once(|| { console_error_panic_hook::set_once(); - let mut builder = tracing_wasm::WASMLayerConfigBuilder::new(); - builder.set_report_logs_in_timings(false); - builder.set_max_level(Level::TRACE); - builder.set_console_config(tracing_wasm::ConsoleConfig::ReportWithConsoleColor); - tracing_wasm::set_as_global_default_with_config(builder.build()); + cfg_if! { + if #[cfg(feature = "tracing")] { + let mut builder = tracing_wasm::WASMLayerConfigBuilder::new(); + builder.set_report_logs_in_timings(false); + builder.set_max_level(Level::TRACE); + builder.set_console_config(tracing_wasm::ConsoleConfig::ReportWithConsoleColor); + tracing_wasm::set_as_global_default_with_config(builder.build()); + } + } }); } -#[wasm_bindgen_test] -async fn run_test_dht_key() { - setup(); - - test_dht_key::test_all().await; -} - #[wasm_bindgen_test] async fn run_test_host_interface() { setup(); @@ -37,48 +34,6 @@ async fn run_test_host_interface() { test_host_interface::test_all().await; } -#[wasm_bindgen_test] -async fn run_test_veilid_tools() { - setup(); - - test_veilid_tools::test_all().await; -} - -#[wasm_bindgen_test] -async fn run_test_config() { - setup(); - - test_veilid_config::test_all().await; -} - -#[wasm_bindgen_test] -async fn run_test_connection_table() { - setup(); - - test_connection_table::test_all().await; -} - -#[wasm_bindgen_test] -async fn run_test_table_store() { - setup(); - - test_table_store::test_all().await; -} - -#[wasm_bindgen_test] -async fn run_test_crypto() { - setup(); - - test_crypto::test_all().await; -} - -#[wasm_bindgen_test] -async fn run_test_envelope_receipt() { - setup(); - - test_envelope_receipt::test_all().await; -} - #[wasm_bindgen_test] async fn run_test_async_tag_lock() { setup(); From a34da6ff75d81ac92c25e0240f975f387a579b7b Mon Sep 17 00:00:00 2001 From: John Smith Date: Sun, 27 Nov 2022 11:27:06 -0500 Subject: [PATCH 14/88] clean up tests --- veilid-tools/run_tests.sh | 9 +++++++++ veilid-tools/src/spawn.rs | 1 + veilid-tools/webdriver.json | 15 +++++++++++++++ 3 files changed, 25 insertions(+) create mode 100755 veilid-tools/run_tests.sh create mode 100644 veilid-tools/webdriver.json diff --git a/veilid-tools/run_tests.sh b/veilid-tools/run_tests.sh new file mode 100755 index 00000000..065ec2ed --- /dev/null +++ b/veilid-tools/run_tests.sh @@ -0,0 +1,9 @@ +#!/bin/bash +if [[ "$1" == "wasm" ]]; then + WASM_BINDGEN_TEST_TIMEOUT=120 wasm-pack test --chrome --headless +else + cargo test --features=rt-tokio + cargo test --features=rt-async-std + cargo test --features=rt-tokio,log --no-default-features + cargo test --features=rt-async-std,log --no-default-features +fi diff --git a/veilid-tools/src/spawn.rs b/veilid-tools/src/spawn.rs index 0ee5497a..8d85b01c 100644 --- a/veilid-tools/src/spawn.rs +++ b/veilid-tools/src/spawn.rs @@ -99,6 +99,7 @@ cfg_if! { } } + #[allow(unused_variables)] pub async fn blocking_wrapper(blocking_task: F, err_result: R) -> R where F: FnOnce() -> R + Send + 'static, diff --git a/veilid-tools/webdriver.json b/veilid-tools/webdriver.json new file mode 100644 index 00000000..c2d6865e --- /dev/null +++ b/veilid-tools/webdriver.json @@ -0,0 +1,15 @@ +{ + "moz:firefoxOptions": { + "prefs": { + "media.navigator.streams.fake": true, + "media.navigator.permission.disabled": true + }, + "args": [] + }, + "goog:chromeOptions": { + "args": [ + "--use-fake-device-for-media-stream", + "--use-fake-ui-for-media-stream" + ] + } +} From d99273334d94338bbf81c724c43d1b3b383aec42 Mon Sep 17 00:00:00 2001 From: John Smith Date: Sun, 27 Nov 2022 22:33:41 -0500 Subject: [PATCH 15/88] ios work --- Cargo.lock | 3 +- .../src/intf/native/utils/android/mod.rs | 31 ++-- .../utils/{ios_test_setup.rs => ios/mod.rs} | 0 veilid-core/tests/node.rs | 83 --------- veilid-tools/Cargo.toml | 26 +-- veilid-tools/ios_build.sh | 25 ++- veilid-tools/run_tests.sh | 17 ++ veilid-tools/src/lib.rs | 7 + veilid-tools/src/tests/android/mod.rs | 80 ++++++++ .../{ => veilidtools-tests}/.gitignore | 0 .../{ => veilidtools-tests}/.idea/.gitignore | 0 .../{ => veilidtools-tests}/.idea/.name | 0 .../.idea/compiler.xml | 0 .../{ => veilidtools-tests}/.idea/gradle.xml | 0 .../.idea/jarRepositories.xml | 0 .../{ => veilidtools-tests}/.idea/misc.xml | 0 .../{ => veilidtools-tests}/.idea/vcs.xml | 0 .../android/{ => veilidtools-tests}/.project | 0 .../org.eclipse.buildship.core.prefs | 0 .../android/{ => veilidtools-tests}/adb+.sh | 0 .../{ => veilidtools-tests}/app/.classpath | 0 .../{ => veilidtools-tests}/app/.gitignore | 0 .../{ => veilidtools-tests}/app/.project | 0 .../org.eclipse.buildship.core.prefs | 0 .../app/CMakeLists.txt | 0 .../{ => veilidtools-tests}/app/build.gradle | 0 .../{ => veilidtools-tests}/app/cpplink.cpp | 0 .../app/proguard-rules.pro | 0 .../app/src/main/AndroidManifest.xml | 0 .../MainActivity.java | 0 .../drawable-v24/ic_launcher_foreground.xml | 0 .../res/drawable/ic_launcher_background.xml | 0 .../app/src/main/res/layout/activity_main.xml | 0 .../res/mipmap-anydpi-v26/ic_launcher.xml | 0 .../mipmap-anydpi-v26/ic_launcher_round.xml | 0 .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin .../res/mipmap-hdpi/ic_launcher_round.png | Bin .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin .../res/mipmap-mdpi/ic_launcher_round.png | Bin .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin .../res/mipmap-xhdpi/ic_launcher_round.png | Bin .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin .../app/src/main/res/values-night/themes.xml | 0 .../app/src/main/res/values/colors.xml | 0 .../app/src/main/res/values/strings.xml | 0 .../app/src/main/res/values/themes.xml | 0 .../{ => veilidtools-tests}/build.gradle | 0 .../{ => veilidtools-tests}/gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.jar | Bin .../gradle/wrapper/gradle-wrapper.properties | 0 .../android/{ => veilidtools-tests}/gradlew | 0 .../{ => veilidtools-tests}/gradlew.bat | 0 .../install_on_all_devices.sh | 0 .../remove_from_all_devices.sh | 0 .../{ => veilidtools-tests}/settings.gradle | 0 veilid-tools/src/tests/common/mod.rs | 25 +++ veilid-tools/src/tests/ios/mod.rs | 61 ++++++ .../project.pbxproj | 174 +++++++++--------- ...ts.xcscheme => veilidtools-tests.xcscheme} | 8 +- .../veilidtools-tests/ViewController.swift | 2 + veilid-tools/src/tests/mod.rs | 8 + veilid-tools/src/tests/native/mod.rs | 76 +++----- veilid-tools/tests/web.rs | 2 +- 66 files changed, 374 insertions(+), 254 deletions(-) rename veilid-core/src/intf/native/utils/{ios_test_setup.rs => ios/mod.rs} (100%) delete mode 100644 veilid-core/tests/node.rs create mode 100644 veilid-tools/src/tests/android/mod.rs rename veilid-tools/src/tests/android/{ => veilidtools-tests}/.gitignore (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/.idea/.gitignore (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/.idea/.name (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/.idea/compiler.xml (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/.idea/gradle.xml (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/.idea/jarRepositories.xml (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/.idea/misc.xml (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/.idea/vcs.xml (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/.project (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/.settings/org.eclipse.buildship.core.prefs (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/adb+.sh (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/.classpath (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/.gitignore (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/.project (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/.settings/org.eclipse.buildship.core.prefs (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/CMakeLists.txt (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/build.gradle (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/cpplink.cpp (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/proguard-rules.pro (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/AndroidManifest.xml (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/java/com/veilid/veilid-core/veilid-core_android_tests/MainActivity.java (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/res/drawable-v24/ic_launcher_foreground.xml (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/res/drawable/ic_launcher_background.xml (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/res/layout/activity_main.xml (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/res/mipmap-hdpi/ic_launcher.png (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/res/mipmap-hdpi/ic_launcher_round.png (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/res/mipmap-mdpi/ic_launcher.png (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/res/mipmap-mdpi/ic_launcher_round.png (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/res/mipmap-xhdpi/ic_launcher.png (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/res/values-night/themes.xml (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/res/values/colors.xml (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/res/values/strings.xml (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/app/src/main/res/values/themes.xml (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/build.gradle (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/gradle.properties (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/gradle/wrapper/gradle-wrapper.jar (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/gradle/wrapper/gradle-wrapper.properties (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/gradlew (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/gradlew.bat (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/install_on_all_devices.sh (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/remove_from_all_devices.sh (100%) rename veilid-tools/src/tests/android/{ => veilidtools-tests}/settings.gradle (100%) create mode 100644 veilid-tools/src/tests/ios/mod.rs rename veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/xcshareddata/xcschemes/{veilidcore-tests.xcscheme => veilidtools-tests.xcscheme} (92%) diff --git a/Cargo.lock b/Cargo.lock index a66192cd..b754585c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5759,7 +5759,7 @@ dependencies = [ "rand 0.7.3", "send_wrapper 0.6.0", "serial_test", - "simplelog 0.9.0", + "simplelog 0.12.0", "static_assertions", "stop-token", "thiserror", @@ -5767,6 +5767,7 @@ dependencies = [ "tokio-util", "tracing", "tracing-android", + "tracing-subscriber", "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", diff --git a/veilid-core/src/intf/native/utils/android/mod.rs b/veilid-core/src/intf/native/utils/android/mod.rs index d7bdd15b..a6e3097a 100644 --- a/veilid-core/src/intf/native/utils/android/mod.rs +++ b/veilid-core/src/intf/native/utils/android/mod.rs @@ -46,22 +46,23 @@ pub fn veilid_core_setup_android<'a>( log_tag: &'a str, log_level: VeilidConfigLogLevel, ) { - // Set up subscriber and layers - let subscriber = Registry::default(); - let mut layers = Vec::new(); - let mut filters = BTreeMap::new(); - let filter = VeilidLayerFilter::new(log_level, None); - let layer = tracing_android::layer(log_tag) - .expect("failed to set up android logging") - .with_filter(filter.clone()); - filters.insert("system", filter); - layers.push(layer.boxed()); - - let subscriber = subscriber.with(layers); - subscriber - .try_init() - .expect("failed to init android tracing"); + cfg_if! { + if #[cfg(feature = "tracing")] { + // Set up subscriber and layers + let subscriber = Registry::default(); + let mut layers = Vec::new(); + let filter = VeilidLayerFilter::new(log_level, None); + let layer = tracing_android::layer(log_tag) + .expect("failed to set up android logging") + .with_filter(filter.clone()); + layers.push(layer.boxed()); + let subscriber = subscriber.with(layers); + subscriber + .try_init() + .expect("failed to init android tracing"); + } + } // Set up panic hook for backtraces panic::set_hook(Box::new(|panic_info| { let bt = Backtrace::new(); diff --git a/veilid-core/src/intf/native/utils/ios_test_setup.rs b/veilid-core/src/intf/native/utils/ios/mod.rs similarity index 100% rename from veilid-core/src/intf/native/utils/ios_test_setup.rs rename to veilid-core/src/intf/native/utils/ios/mod.rs diff --git a/veilid-core/tests/node.rs b/veilid-core/tests/node.rs deleted file mode 100644 index da301eeb..00000000 --- a/veilid-core/tests/node.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! Test suite for NodeJS -#![cfg(target_arch = "wasm32")] - -use veilid_core::tests::common::*; -use veilid_core::xx::*; -use wasm_bindgen_test::*; - -wasm_bindgen_test_configure!(); - -extern crate wee_alloc; -#[global_allocator] -static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; - -static SETUP_ONCE: Once = Once::new(); -pub fn setup() -> () { - SETUP_ONCE.call_once(|| { - console_error_panic_hook::set_once(); - wasm_logger::init(wasm_logger::Config::new(log::Level::Trace)); - }); -} - -#[wasm_bindgen_test] -async fn run_test_dht_key() { - setup(); - - test_dht_key::test_all().await; -} - -#[wasm_bindgen_test] -async fn run_test_host_interface() { - setup(); - - test_host_interface::test_all().await; -} - -#[wasm_bindgen_test] -async fn run_test_veilid_core() { - setup(); - - test_veilid_core::test_all().await; -} - -#[wasm_bindgen_test] -async fn run_test_config() { - setup(); - - test_veilid_config::test_all().await; -} - -#[wasm_bindgen_test] -async fn run_test_connection_table() { - setup(); - - test_connection_table::test_all().await; -} - -#[wasm_bindgen_test] -async fn run_test_table_store() { - setup(); - - test_table_store::test_all().await; -} - -#[wasm_bindgen_test] -async fn run_test_crypto() { - setup(); - - test_crypto::test_all().await; -} - -#[wasm_bindgen_test] -async fn run_test_envelope_receipt() { - setup(); - - test_envelope_receipt::test_all().await; -} - -#[wasm_bindgen_test] -async fn run_test_async_tag_lock() { - setup(); - - test_async_tag_lock::test_all().await; -} diff --git a/veilid-tools/Cargo.toml b/veilid-tools/Cargo.toml index 2095866f..05732f9b 100644 --- a/veilid-tools/Cargo.toml +++ b/veilid-tools/Cargo.toml @@ -6,22 +6,23 @@ edition = "2021" license = "LGPL-2.0-or-later OR MPL-2.0 OR (MIT AND BSD-3-Clause)" [lib] -crate-type = ["rlib"] +# Staticlib for iOS tests, rlib for everything else +crate-type = [ "staticlib", "rlib" ] [features] -default = [ "tracing" ] +default = [] rt-async-std = [ "async-std", "async_executors/async_std", ] rt-tokio = [ "tokio", "tokio-util", "async_executors/tokio_tp", "async_executors/tokio_io", "async_executors/tokio_timer", ] android_tests = [] -ios_tests = [ "simplelog" ] +ios_tests = [] tracking = [] -tracing = [ "dep:tracing" ] -log = [ "dep:log" ] +tracing = [ "dep:tracing", "dep:tracing-subscriber" ] [dependencies] tracing = { version = "^0", features = ["log", "attributes"], optional = true } -log = { version = "^0", optional = true } +tracing-subscriber = { version = "^0", optional = true } +log = { version = "^0" } eyre = "^0" static_assertions = "^1" cfg-if = "^1" @@ -31,7 +32,7 @@ parking_lot = "^0" once_cell = "^1" owo-colors = "^3" stop-token = { version = "^0", default-features = false } -rand = "0.7" +rand = "^0.7" # Dependencies for native builds only # Linux, Windows, Mac, iOS, Android @@ -69,15 +70,13 @@ tracing-android = { version = "^0", optional = true } # Dependencies for iOS [target.'cfg(target_os = "ios")'.dependencies] -simplelog = { version = "^0", optional = true } +simplelog = { version = "^0.12", features = [ "test" ] } ### DEV DEPENDENCIES [dev-dependencies] serial_test = "^0" - -[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -simplelog = { version = "^0", features=["test"] } +simplelog = { version = "^0.12", features = [ "test" ] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] console_error_panic_hook = "^0" @@ -91,3 +90,8 @@ parking_lot = { version = "^0", features = ["wasm-bindgen"]} [package.metadata.wasm-pack.profile.release] wasm-opt = ["-O", "--enable-mutable-globals"] + +[package.metadata.ios] +build_targets = ["aarch64-apple-ios", "aarch64-apple-ios-sim", "x86_64-apple-ios"] +deployment_target = "12.0" +build_id_prefix = "com.veilid.veilidtools" diff --git a/veilid-tools/ios_build.sh b/veilid-tools/ios_build.sh index 4eb08eca..c4aea7a4 100755 --- a/veilid-tools/ios_build.sh +++ b/veilid-tools/ios_build.sh @@ -1,20 +1,36 @@ #!/bin/bash SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -CARGO_MANIFEST_PATH=$(python -c "import os; print(os.path.realpath(\"$SCRIPTDIR/Cargo.toml\"))") +CARGO_MANIFEST_PATH=$(python3 -c "import os; print(os.path.realpath(\"$SCRIPTDIR/Cargo.toml\"))") +TARGET_PATH=$(python3 -c "import os; print(os.path.realpath(\"$SCRIPTDIR/../target\"))") +PACKAGE_NAME=$1 +shift # echo CARGO_MANIFEST_PATH: $CARGO_MANIFEST_PATH if [ "$CONFIGURATION" == "Debug" ]; then EXTRA_CARGO_OPTIONS="$@" + BUILD_MODE="debug" else EXTRA_CARGO_OPTIONS="$@ --release" + BUILD_MODE="release" fi ARCHS=${ARCHS:=arm64} + +if [ "$PLATFORM_NAME" == "iphonesimulator" ]; then + LIPO_OUT_NAME="lipo-ios-sim" +else + LIPO_OUT_NAME="lipo-ios" +fi + for arch in $ARCHS do if [ "$arch" == "arm64" ]; then echo arm64 - CARGO_TARGET=aarch64-apple-ios + if [ "$PLATFORM_NAME" == "iphonesimulator" ]; then + CARGO_TARGET=aarch64-apple-ios-sim + else + CARGO_TARGET=aarch64-apple-ios + fi #CARGO_TOOLCHAIN=+ios-arm64-1.57.0 CARGO_TOOLCHAIN= elif [ "$arch" == "x86_64" ]; then @@ -40,5 +56,10 @@ do fi env -i PATH=/usr/bin:/bin:$HOMEBREW_DIR:$CARGO_DIR HOME="$HOME" USER="$USER" cargo $CARGO_TOOLCHAIN build $EXTRA_CARGO_OPTIONS --target $CARGO_TARGET --manifest-path $CARGO_MANIFEST_PATH + + LIPOS="$LIPOS $TARGET_PATH/$CARGO_TARGET/$BUILD_MODE/lib$PACKAGE_NAME.a" + done +mkdir -p "$TARGET_PATH/$LIPO_OUT_NAME/$BUILD_MODE/" +lipo $LIPOS -create -output "$TARGET_PATH/$LIPO_OUT_NAME/$BUILD_MODE/lib$PACKAGE_NAME.a" diff --git a/veilid-tools/run_tests.sh b/veilid-tools/run_tests.sh index 065ec2ed..f53337b2 100755 --- a/veilid-tools/run_tests.sh +++ b/veilid-tools/run_tests.sh @@ -1,9 +1,26 @@ #!/bin/bash +SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +pushd $SCRIPTDIR 2>/dev/null if [[ "$1" == "wasm" ]]; then WASM_BINDGEN_TEST_TIMEOUT=120 wasm-pack test --chrome --headless +elif [[ "$1" == "ios" ]]; then + SYMROOT=/tmp/testout + APPNAME=veilidtools-tests + BUNDLENAME=com.veilid.veilidtools-tests + + xcrun xcodebuild -project src/tests/ios/$APPNAME/$APPNAME.xcodeproj/ -scheme $APPNAME -destination "generic/platform=iOS Simulator" SYMROOT=$SYMROOT + ID=$(xcrun simctl create test-iphone com.apple.CoreSimulator.SimDeviceType.iPhone-14-Pro com.apple.CoreSimulator.SimRuntime.iOS-16-1 2>/dev/null) + xcrun simctl boot $ID + xcrun simctl bootstatus $ID + xcrun simctl install $ID $SYMROOT/Debug-iphonesimulator/$APPNAME.app + xcrun simctl launch --console $ID $BUNDLENAME + xcrun simctl delete all + rm -rf /tmp/testout else cargo test --features=rt-tokio cargo test --features=rt-async-std cargo test --features=rt-tokio,log --no-default-features cargo test --features=rt-async-std,log --no-default-features fi +popd 2>/dev/null \ No newline at end of file diff --git a/veilid-tools/src/lib.rs b/veilid-tools/src/lib.rs index 0838c00f..0f16316b 100644 --- a/veilid-tools/src/lib.rs +++ b/veilid-tools/src/lib.rs @@ -136,3 +136,10 @@ pub use wasm::*; // Tests must be public for wasm-pack tests pub mod tests; + +// For iOS tests + +#[no_mangle] +pub extern "C" fn main_rs() { + // start game code here +} diff --git a/veilid-tools/src/tests/android/mod.rs b/veilid-tools/src/tests/android/mod.rs new file mode 100644 index 00000000..6fb0ad83 --- /dev/null +++ b/veilid-tools/src/tests/android/mod.rs @@ -0,0 +1,80 @@ +use super::*; + +use jni::errors::Result as JniResult; +use jni::{objects::GlobalRef, objects::JObject, objects::JString, JNIEnv, JavaVM}; +use lazy_static::*; +use std::backtrace::Backtrace; +use std::panic; +use tracing::*; +use tracing_subscriber::prelude::*; +use tracing_subscriber::*; + +pub struct AndroidGlobals { + pub vm: JavaVM, + pub ctx: GlobalRef, +} + +impl Drop for AndroidGlobals { + fn drop(&mut self) { + // Ensure we're attached before dropping GlobalRef + self.vm.attach_current_thread_as_daemon().unwrap(); + } +} + +lazy_static! { + pub static ref ANDROID_GLOBALS: Arc>> = Arc::new(Mutex::new(None)); +} + +pub fn veilid_tools_setup_android_no_log<'a>(env: JNIEnv<'a>, ctx: JObject<'a>) { + *ANDROID_GLOBALS.lock() = Some(AndroidGlobals { + vm: env.get_java_vm().unwrap(), + ctx: env.new_global_ref(ctx).unwrap(), + }); +} + +pub fn veilid_tools_setup<'a>(env: JNIEnv<'a>, ctx: JObject<'a>, log_tag: &'a str) { + cfg_if! { + if #[cfg(feature = "tracing")] { + // Set up subscriber and layers + + let subscriber = Registry::default(); + let mut layers = Vec::new(); + let layer = tracing_android::layer(log_tag) + .expect("failed to set up android logging") + .with_filter(LevelFilter::TRACE); + layers.push(layer.boxed()); + + let subscriber = subscriber.with(layers); + subscriber + .try_init() + .expect("failed to init android tracing"); + } + } + + // Set up panic hook for backtraces + panic::set_hook(Box::new(|panic_info| { + let bt = Backtrace::capture(); + error!("Backtrace:\n{:?}", bt); + })); + + veilid_core_setup_android_no_log(env, ctx); +} + +pub fn get_android_globals() -> (JavaVM, GlobalRef) { + let globals_locked = ANDROID_GLOBALS.lock(); + let globals = globals_locked.as_ref().unwrap(); + let env = globals.vm.attach_current_thread_as_daemon().unwrap(); + let vm = env.get_java_vm().unwrap(); + let ctx = globals.ctx.clone(); + (vm, ctx) +} + +pub fn with_null_local_frame<'b, T, F>(env: JNIEnv<'b>, s: i32, f: F) -> JniResult +where + F: FnOnce() -> JniResult, +{ + env.push_local_frame(s)?; + let out = f(); + env.pop_local_frame(JObject::null())?; + out +} diff --git a/veilid-tools/src/tests/android/.gitignore b/veilid-tools/src/tests/android/veilidtools-tests/.gitignore similarity index 100% rename from veilid-tools/src/tests/android/.gitignore rename to veilid-tools/src/tests/android/veilidtools-tests/.gitignore diff --git a/veilid-tools/src/tests/android/.idea/.gitignore b/veilid-tools/src/tests/android/veilidtools-tests/.idea/.gitignore similarity index 100% rename from veilid-tools/src/tests/android/.idea/.gitignore rename to veilid-tools/src/tests/android/veilidtools-tests/.idea/.gitignore diff --git a/veilid-tools/src/tests/android/.idea/.name b/veilid-tools/src/tests/android/veilidtools-tests/.idea/.name similarity index 100% rename from veilid-tools/src/tests/android/.idea/.name rename to veilid-tools/src/tests/android/veilidtools-tests/.idea/.name diff --git a/veilid-tools/src/tests/android/.idea/compiler.xml b/veilid-tools/src/tests/android/veilidtools-tests/.idea/compiler.xml similarity index 100% rename from veilid-tools/src/tests/android/.idea/compiler.xml rename to veilid-tools/src/tests/android/veilidtools-tests/.idea/compiler.xml diff --git a/veilid-tools/src/tests/android/.idea/gradle.xml b/veilid-tools/src/tests/android/veilidtools-tests/.idea/gradle.xml similarity index 100% rename from veilid-tools/src/tests/android/.idea/gradle.xml rename to veilid-tools/src/tests/android/veilidtools-tests/.idea/gradle.xml diff --git a/veilid-tools/src/tests/android/.idea/jarRepositories.xml b/veilid-tools/src/tests/android/veilidtools-tests/.idea/jarRepositories.xml similarity index 100% rename from veilid-tools/src/tests/android/.idea/jarRepositories.xml rename to veilid-tools/src/tests/android/veilidtools-tests/.idea/jarRepositories.xml diff --git a/veilid-tools/src/tests/android/.idea/misc.xml b/veilid-tools/src/tests/android/veilidtools-tests/.idea/misc.xml similarity index 100% rename from veilid-tools/src/tests/android/.idea/misc.xml rename to veilid-tools/src/tests/android/veilidtools-tests/.idea/misc.xml diff --git a/veilid-tools/src/tests/android/.idea/vcs.xml b/veilid-tools/src/tests/android/veilidtools-tests/.idea/vcs.xml similarity index 100% rename from veilid-tools/src/tests/android/.idea/vcs.xml rename to veilid-tools/src/tests/android/veilidtools-tests/.idea/vcs.xml diff --git a/veilid-tools/src/tests/android/.project b/veilid-tools/src/tests/android/veilidtools-tests/.project similarity index 100% rename from veilid-tools/src/tests/android/.project rename to veilid-tools/src/tests/android/veilidtools-tests/.project diff --git a/veilid-tools/src/tests/android/.settings/org.eclipse.buildship.core.prefs b/veilid-tools/src/tests/android/veilidtools-tests/.settings/org.eclipse.buildship.core.prefs similarity index 100% rename from veilid-tools/src/tests/android/.settings/org.eclipse.buildship.core.prefs rename to veilid-tools/src/tests/android/veilidtools-tests/.settings/org.eclipse.buildship.core.prefs diff --git a/veilid-tools/src/tests/android/adb+.sh b/veilid-tools/src/tests/android/veilidtools-tests/adb+.sh similarity index 100% rename from veilid-tools/src/tests/android/adb+.sh rename to veilid-tools/src/tests/android/veilidtools-tests/adb+.sh diff --git a/veilid-tools/src/tests/android/app/.classpath b/veilid-tools/src/tests/android/veilidtools-tests/app/.classpath similarity index 100% rename from veilid-tools/src/tests/android/app/.classpath rename to veilid-tools/src/tests/android/veilidtools-tests/app/.classpath diff --git a/veilid-tools/src/tests/android/app/.gitignore b/veilid-tools/src/tests/android/veilidtools-tests/app/.gitignore similarity index 100% rename from veilid-tools/src/tests/android/app/.gitignore rename to veilid-tools/src/tests/android/veilidtools-tests/app/.gitignore diff --git a/veilid-tools/src/tests/android/app/.project b/veilid-tools/src/tests/android/veilidtools-tests/app/.project similarity index 100% rename from veilid-tools/src/tests/android/app/.project rename to veilid-tools/src/tests/android/veilidtools-tests/app/.project diff --git a/veilid-tools/src/tests/android/app/.settings/org.eclipse.buildship.core.prefs b/veilid-tools/src/tests/android/veilidtools-tests/app/.settings/org.eclipse.buildship.core.prefs similarity index 100% rename from veilid-tools/src/tests/android/app/.settings/org.eclipse.buildship.core.prefs rename to veilid-tools/src/tests/android/veilidtools-tests/app/.settings/org.eclipse.buildship.core.prefs diff --git a/veilid-tools/src/tests/android/app/CMakeLists.txt b/veilid-tools/src/tests/android/veilidtools-tests/app/CMakeLists.txt similarity index 100% rename from veilid-tools/src/tests/android/app/CMakeLists.txt rename to veilid-tools/src/tests/android/veilidtools-tests/app/CMakeLists.txt diff --git a/veilid-tools/src/tests/android/app/build.gradle b/veilid-tools/src/tests/android/veilidtools-tests/app/build.gradle similarity index 100% rename from veilid-tools/src/tests/android/app/build.gradle rename to veilid-tools/src/tests/android/veilidtools-tests/app/build.gradle diff --git a/veilid-tools/src/tests/android/app/cpplink.cpp b/veilid-tools/src/tests/android/veilidtools-tests/app/cpplink.cpp similarity index 100% rename from veilid-tools/src/tests/android/app/cpplink.cpp rename to veilid-tools/src/tests/android/veilidtools-tests/app/cpplink.cpp diff --git a/veilid-tools/src/tests/android/app/proguard-rules.pro b/veilid-tools/src/tests/android/veilidtools-tests/app/proguard-rules.pro similarity index 100% rename from veilid-tools/src/tests/android/app/proguard-rules.pro rename to veilid-tools/src/tests/android/veilidtools-tests/app/proguard-rules.pro diff --git a/veilid-tools/src/tests/android/app/src/main/AndroidManifest.xml b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/AndroidManifest.xml similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/AndroidManifest.xml rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/AndroidManifest.xml diff --git a/veilid-tools/src/tests/android/app/src/main/java/com/veilid/veilid-core/veilid-core_android_tests/MainActivity.java b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/java/com/veilid/veilid-core/veilid-core_android_tests/MainActivity.java similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/java/com/veilid/veilid-core/veilid-core_android_tests/MainActivity.java rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/java/com/veilid/veilid-core/veilid-core_android_tests/MainActivity.java diff --git a/veilid-tools/src/tests/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/drawable-v24/ic_launcher_foreground.xml similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/drawable-v24/ic_launcher_foreground.xml diff --git a/veilid-tools/src/tests/android/app/src/main/res/drawable/ic_launcher_background.xml b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/res/drawable/ic_launcher_background.xml rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/drawable/ic_launcher_background.xml diff --git a/veilid-tools/src/tests/android/app/src/main/res/layout/activity_main.xml b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/layout/activity_main.xml similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/res/layout/activity_main.xml rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/layout/activity_main.xml diff --git a/veilid-tools/src/tests/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/veilid-tools/src/tests/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/veilid-tools/src/tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/veilid-tools/src/tests/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-hdpi/ic_launcher_round.png diff --git a/veilid-tools/src/tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/veilid-tools/src/tests/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-mdpi/ic_launcher_round.png diff --git a/veilid-tools/src/tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/veilid-tools/src/tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/veilid-tools/src/tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/veilid-tools/src/tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/veilid-tools/src/tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/veilid-tools/src/tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/veilid-tools/src/tests/android/app/src/main/res/values-night/themes.xml b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/values-night/themes.xml similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/res/values-night/themes.xml rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/values-night/themes.xml diff --git a/veilid-tools/src/tests/android/app/src/main/res/values/colors.xml b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/values/colors.xml similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/res/values/colors.xml rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/values/colors.xml diff --git a/veilid-tools/src/tests/android/app/src/main/res/values/strings.xml b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/values/strings.xml similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/res/values/strings.xml rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/values/strings.xml diff --git a/veilid-tools/src/tests/android/app/src/main/res/values/themes.xml b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/values/themes.xml similarity index 100% rename from veilid-tools/src/tests/android/app/src/main/res/values/themes.xml rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/values/themes.xml diff --git a/veilid-tools/src/tests/android/build.gradle b/veilid-tools/src/tests/android/veilidtools-tests/build.gradle similarity index 100% rename from veilid-tools/src/tests/android/build.gradle rename to veilid-tools/src/tests/android/veilidtools-tests/build.gradle diff --git a/veilid-tools/src/tests/android/gradle.properties b/veilid-tools/src/tests/android/veilidtools-tests/gradle.properties similarity index 100% rename from veilid-tools/src/tests/android/gradle.properties rename to veilid-tools/src/tests/android/veilidtools-tests/gradle.properties diff --git a/veilid-tools/src/tests/android/gradle/wrapper/gradle-wrapper.jar b/veilid-tools/src/tests/android/veilidtools-tests/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from veilid-tools/src/tests/android/gradle/wrapper/gradle-wrapper.jar rename to veilid-tools/src/tests/android/veilidtools-tests/gradle/wrapper/gradle-wrapper.jar diff --git a/veilid-tools/src/tests/android/gradle/wrapper/gradle-wrapper.properties b/veilid-tools/src/tests/android/veilidtools-tests/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from veilid-tools/src/tests/android/gradle/wrapper/gradle-wrapper.properties rename to veilid-tools/src/tests/android/veilidtools-tests/gradle/wrapper/gradle-wrapper.properties diff --git a/veilid-tools/src/tests/android/gradlew b/veilid-tools/src/tests/android/veilidtools-tests/gradlew similarity index 100% rename from veilid-tools/src/tests/android/gradlew rename to veilid-tools/src/tests/android/veilidtools-tests/gradlew diff --git a/veilid-tools/src/tests/android/gradlew.bat b/veilid-tools/src/tests/android/veilidtools-tests/gradlew.bat similarity index 100% rename from veilid-tools/src/tests/android/gradlew.bat rename to veilid-tools/src/tests/android/veilidtools-tests/gradlew.bat diff --git a/veilid-tools/src/tests/android/install_on_all_devices.sh b/veilid-tools/src/tests/android/veilidtools-tests/install_on_all_devices.sh similarity index 100% rename from veilid-tools/src/tests/android/install_on_all_devices.sh rename to veilid-tools/src/tests/android/veilidtools-tests/install_on_all_devices.sh diff --git a/veilid-tools/src/tests/android/remove_from_all_devices.sh b/veilid-tools/src/tests/android/veilidtools-tests/remove_from_all_devices.sh similarity index 100% rename from veilid-tools/src/tests/android/remove_from_all_devices.sh rename to veilid-tools/src/tests/android/veilidtools-tests/remove_from_all_devices.sh diff --git a/veilid-tools/src/tests/android/settings.gradle b/veilid-tools/src/tests/android/veilidtools-tests/settings.gradle similarity index 100% rename from veilid-tools/src/tests/android/settings.gradle rename to veilid-tools/src/tests/android/veilidtools-tests/settings.gradle diff --git a/veilid-tools/src/tests/common/mod.rs b/veilid-tools/src/tests/common/mod.rs index 06433eae..393bcfa9 100644 --- a/veilid-tools/src/tests/common/mod.rs +++ b/veilid-tools/src/tests/common/mod.rs @@ -1,2 +1,27 @@ pub mod test_async_tag_lock; pub mod test_host_interface; + +#[allow(dead_code)] +pub static DEFAULT_LOG_IGNORE_LIST: [&str; 21] = [ + "mio", + "h2", + "hyper", + "tower", + "tonic", + "tokio", + "runtime", + "tokio_util", + "want", + "serial_test", + "async_std", + "async_io", + "polling", + "rustls", + "async_tungstenite", + "tungstenite", + "netlink_proto", + "netlink_sys", + "trust_dns_resolver", + "trust_dns_proto", + "attohttpc", +]; diff --git a/veilid-tools/src/tests/ios/mod.rs b/veilid-tools/src/tests/ios/mod.rs new file mode 100644 index 00000000..358955a8 --- /dev/null +++ b/veilid-tools/src/tests/ios/mod.rs @@ -0,0 +1,61 @@ +use super::*; + +use std::backtrace::Backtrace; +use std::panic; + +pub fn veilid_tools_setup<'a>() -> Result<(), String> { + cfg_if! { + if #[cfg(feature = "tracing")] { + use tracing_subscriber::{filter, fmt, prelude::*}; + let mut filters = filter::Targets::new(); + for ig in DEFAULT_LOG_IGNORE_LIST { + filters = filters.with_target(ig, filter::LevelFilter::OFF); + } + let fmt_layer = fmt::layer(); + tracing_subscriber::registry() + .with(filters) + .with(filter::LevelFilter::TRACE) + .with(fmt_layer) + .init(); + } else { + use simplelog::*; + let mut logs: Vec> = Vec::new(); + let mut cb = ConfigBuilder::new(); + for ig in DEFAULT_LOG_IGNORE_LIST { + cb.add_filter_ignore_str(ig); + } + logs.push(TermLogger::new( + LevelFilter::Trace, + cb.build(), + TerminalMode::Mixed, + ColorChoice::Auto, + )); + CombinedLogger::init(logs).map_err(|e| format!("logger init error: {}", e))?; + } + } + + panic::set_hook(Box::new(|panic_info| { + let bt = Backtrace::capture(); + if let Some(location) = panic_info.location() { + error!( + "panic occurred in file '{}' at line {}", + location.file(), + location.line(), + ); + } else { + error!("panic occurred but can't get location information..."); + } + if let Some(s) = panic_info.payload().downcast_ref::<&str>() { + error!("panic payload: {:?}", s); + } else if let Some(s) = panic_info.payload().downcast_ref::() { + error!("panic payload: {:?}", s); + } else if let Some(a) = panic_info.payload().downcast_ref::() { + error!("panic payload: {:?}", a); + } else { + error!("no panic payload"); + } + error!("Backtrace:\n{:?}", bt); + })); + + Ok(()) +} diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj index b47f1821..51f168d3 100644 --- a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj @@ -7,31 +7,31 @@ objects = { /* Begin PBXBuildFile section */ - 4317C6BD2694A676009C717F /* veilid-tools.c in Sources */ = {isa = PBXBuildFile; fileRef = 4317C6BC2694A676009C717F /* veilid-tools.c */; }; - 43C436B0268904AC002D11C5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C436AF268904AC002D11C5 /* AppDelegate.swift */; }; - 43C436B2268904AC002D11C5 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C436B1268904AC002D11C5 /* SceneDelegate.swift */; }; - 43C436B4268904AC002D11C5 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C436B3268904AC002D11C5 /* ViewController.swift */; }; - 43C436B7268904AC002D11C5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 43C436B5268904AC002D11C5 /* Main.storyboard */; }; - 43C436B9268904AD002D11C5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43C436B8268904AD002D11C5 /* Assets.xcassets */; }; - 43C436BC268904AD002D11C5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 43C436BA268904AD002D11C5 /* LaunchScreen.storyboard */; }; + 4317C6BD3694A676009C717F /* (null) in Sources */ = {isa = PBXBuildFile; }; + 43C436B0368904AC002D11C5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C436AF368904AC002D11C5 /* AppDelegate.swift */; }; + 43C436B2368904AC002D11C5 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C436B1368904AC002D11C5 /* SceneDelegate.swift */; }; + 43C436B4368904AC002D11C5 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C436B3368904AC002D11C5 /* ViewController.swift */; }; + 43C436B7368904AC002D11C5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 43C436B5368904AC002D11C5 /* Main.storyboard */; }; + 43C436B9368904AD002D11C5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43C436B8368904AD002D11C5 /* Assets.xcassets */; }; + 43C436BC368904AD002D11C5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 43C436BA368904AD002D11C5 /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 4317C6BA2694A675009C717F /* veilidtools-tests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "veilidtools-tests-Bridging-Header.h"; sourceTree = ""; }; - 4317C6BB2694A676009C717F /* veilid-tools.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "veilid-tools.h"; sourceTree = ""; }; - 4317C6BC2694A676009C717F /* veilid-tools.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = "veilid-tools.c"; sourceTree = ""; }; - 43C436AC268904AC002D11C5 /* veilidtools-tests.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "veilidtools-tests.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 43C436AF268904AC002D11C5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 43C436B1268904AC002D11C5 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 43C436B3268904AC002D11C5 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - 43C436B6268904AC002D11C5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 43C436B8268904AD002D11C5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 43C436BB268904AD002D11C5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 43C436BD268904AD002D11C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 4317C6BA3694A675009C717F /* veilidtools-tests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "veilidtools-tests-Bridging-Header.h"; sourceTree = ""; }; + 4317C6BB3694A676009C717F /* veilid-tools.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "veilid-tools.h"; sourceTree = ""; }; + 4317C6BC3694A676009C717F /* veilid-tools.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = "veilid-tools.c"; sourceTree = ""; }; + 43C436AC368904AC002D11C5 /* veilidtools-tests.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "veilidtools-tests.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 43C436AF368904AC002D11C5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 43C436B1368904AC002D11C5 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 43C436B3368904AC002D11C5 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 43C436B6368904AC002D11C5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 43C436B8368904AD002D11C5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 43C436BB368904AD002D11C5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 43C436BD368904AD002D11C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 43C436A9268904AC002D11C5 /* Frameworks */ = { + 43C436A9368904AC002D11C5 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( @@ -41,43 +41,43 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 4317C6B7269490DA009C717F /* Frameworks */ = { + 4317C6B7369490DA009C717F /* Frameworks */ = { isa = PBXGroup; children = ( ); name = Frameworks; sourceTree = ""; }; - 43C436A3268904AC002D11C5 = { + 43C436A3368904AC002D11C5 = { isa = PBXGroup; children = ( - 4317C6BB2694A676009C717F /* veilid-tools.h */, - 4317C6BC2694A676009C717F /* veilid-tools.c */, - 43C436AE268904AC002D11C5 /* veilidtools-tests */, - 43C436AD268904AC002D11C5 /* Products */, - 4317C6B7269490DA009C717F /* Frameworks */, - 4317C6BA2694A675009C717F /* veilidtools-tests-Bridging-Header.h */, + 4317C6BB3694A676009C717F /* veilid-tools.h */, + 4317C6BC3694A676009C717F /* veilid-tools.c */, + 43C436AE368904AC002D11C5 /* veilidtools-tests */, + 43C436AD368904AC002D11C5 /* Products */, + 4317C6B7369490DA009C717F /* Frameworks */, + 4317C6BA3694A675009C717F /* veilidtools-tests-Bridging-Header.h */, ); sourceTree = ""; }; - 43C436AD268904AC002D11C5 /* Products */ = { + 43C436AD368904AC002D11C5 /* Products */ = { isa = PBXGroup; children = ( - 43C436AC268904AC002D11C5 /* veilidtools-tests.app */, + 43C436AC368904AC002D11C5 /* veilidtools-tests.app */, ); name = Products; sourceTree = ""; }; - 43C436AE268904AC002D11C5 /* veilidtools-tests */ = { + 43C436AE368904AC002D11C5 /* veilidtools-tests */ = { isa = PBXGroup; children = ( - 43C436AF268904AC002D11C5 /* AppDelegate.swift */, - 43C436B1268904AC002D11C5 /* SceneDelegate.swift */, - 43C436B3268904AC002D11C5 /* ViewController.swift */, - 43C436B5268904AC002D11C5 /* Main.storyboard */, - 43C436B8268904AD002D11C5 /* Assets.xcassets */, - 43C436BA268904AD002D11C5 /* LaunchScreen.storyboard */, - 43C436BD268904AD002D11C5 /* Info.plist */, + 43C436AF368904AC002D11C5 /* AppDelegate.swift */, + 43C436B1368904AC002D11C5 /* SceneDelegate.swift */, + 43C436B3368904AC002D11C5 /* ViewController.swift */, + 43C436B5368904AC002D11C5 /* Main.storyboard */, + 43C436B8368904AD002D11C5 /* Assets.xcassets */, + 43C436BA368904AD002D11C5 /* LaunchScreen.storyboard */, + 43C436BD368904AD002D11C5 /* Info.plist */, ); path = "veilidtools-tests"; sourceTree = ""; @@ -85,14 +85,14 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 43C436AB268904AC002D11C5 /* veilidtools-tests */ = { + 43C436AB368904AC002D11C5 /* veilidtools-tests */ = { isa = PBXNativeTarget; - buildConfigurationList = 43C436C0268904AD002D11C5 /* Build configuration list for PBXNativeTarget "veilidtools-tests" */; + buildConfigurationList = 43C436C0368904AD002D11C5 /* Build configuration list for PBXNativeTarget "veilidtools-tests" */; buildPhases = ( - 43C436C326893020002D11C5 /* Cargo Build */, - 43C436A8268904AC002D11C5 /* Sources */, - 43C436A9268904AC002D11C5 /* Frameworks */, - 43C436AA268904AC002D11C5 /* Resources */, + 43C436C336893020002D11C5 /* Cargo Build */, + 43C436A8368904AC002D11C5 /* Sources */, + 43C436A9368904AC002D11C5 /* Frameworks */, + 43C436AA368904AC002D11C5 /* Resources */, ); buildRules = ( ); @@ -100,25 +100,25 @@ ); name = "veilidtools-tests"; productName = "veilidtools-tests"; - productReference = 43C436AC268904AC002D11C5 /* veilidtools-tests.app */; + productReference = 43C436AC368904AC002D11C5 /* veilidtools-tests.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ - 43C436A4268904AC002D11C5 /* Project object */ = { + 43C436A4368904AC002D11C5 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1250; LastUpgradeCheck = 1250; TargetAttributes = { - 43C436AB268904AC002D11C5 = { + 43C436AB368904AC002D11C5 = { CreatedOnToolsVersion = 12.5.1; LastSwiftMigration = 1250; }; }; }; - buildConfigurationList = 43C436A7268904AC002D11C5 /* Build configuration list for PBXProject "veilidtools-tests" */; + buildConfigurationList = 43C436A7368904AC002D11C5 /* Build configuration list for PBXProject "veilidtools-tests" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; @@ -126,31 +126,31 @@ en, Base, ); - mainGroup = 43C436A3268904AC002D11C5; - productRefGroup = 43C436AD268904AC002D11C5 /* Products */; + mainGroup = 43C436A3368904AC002D11C5; + productRefGroup = 43C436AD368904AC002D11C5 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( - 43C436AB268904AC002D11C5 /* veilidtools-tests */, + 43C436AB368904AC002D11C5 /* veilidtools-tests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 43C436AA268904AC002D11C5 /* Resources */ = { + 43C436AA368904AC002D11C5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 43C436BC268904AD002D11C5 /* LaunchScreen.storyboard in Resources */, - 43C436B9268904AD002D11C5 /* Assets.xcassets in Resources */, - 43C436B7268904AC002D11C5 /* Main.storyboard in Resources */, + 43C436BC368904AD002D11C5 /* LaunchScreen.storyboard in Resources */, + 43C436B9368904AD002D11C5 /* Assets.xcassets in Resources */, + 43C436B7368904AC002D11C5 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 43C436C326893020002D11C5 /* Cargo Build */ = { + 43C436C336893020002D11C5 /* Cargo Build */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -167,37 +167,37 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "../../../../ios_build.sh --features ios_tests\n"; + shellScript = "../../../../ios_build.sh veilid_tools --features ios_tests,rt-tokio\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 43C436A8268904AC002D11C5 /* Sources */ = { + 43C436A8368904AC002D11C5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 43C436B4268904AC002D11C5 /* ViewController.swift in Sources */, - 43C436B0268904AC002D11C5 /* AppDelegate.swift in Sources */, - 43C436B2268904AC002D11C5 /* SceneDelegate.swift in Sources */, - 4317C6BD2694A676009C717F /* veilid-tools.c in Sources */, + 43C436B4368904AC002D11C5 /* ViewController.swift in Sources */, + 43C436B0368904AC002D11C5 /* AppDelegate.swift in Sources */, + 43C436B2368904AC002D11C5 /* SceneDelegate.swift in Sources */, + 4317C6BD3694A676009C717F /* (null) in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ - 43C436B5268904AC002D11C5 /* Main.storyboard */ = { + 43C436B5368904AC002D11C5 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( - 43C436B6268904AC002D11C5 /* Base */, + 43C436B6368904AC002D11C5 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; - 43C436BA268904AD002D11C5 /* LaunchScreen.storyboard */ = { + 43C436BA368904AD002D11C5 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( - 43C436BB268904AD002D11C5 /* Base */, + 43C436BB368904AD002D11C5 /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; @@ -205,11 +205,11 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 43C436BE268904AD002D11C5 /* Debug */ = { + 43C436BE368904AD002D11C5 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = arm64; + ARCHS = "$(ARCHS_STANDARD)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -264,14 +264,15 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; - 43C436BF268904AD002D11C5 /* Release */ = { + 43C436BF368904AD002D11C5 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = arm64; + ARCHS = "$(ARCHS_STANDARD)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -319,16 +320,18 @@ SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; - 43C436C1268904AD002D11C5 /* Debug */ = { + 43C436C1368904AD002D11C5 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ZJPQSFX5MW; INFOPLIST_FILE = "veilidtools-tests/Info.plist"; @@ -338,16 +341,13 @@ "@executable_path/Frameworks", ); OTHER_LDFLAGS = ""; - "OTHER_LDFLAGS[sdk=iphoneos*]" = ( - "-L../../../../../target/aarch64-apple-ios/debug", - "-lveilid_tools", - ); "OTHER_LDFLAGS[sdk=iphonesimulator*]" = ( - "-L../../../../../target/x86_64-apple-ios/debug", + "-L../../../../../target/lipo-ios-sim/debug", "-lveilid_tools", ); PRODUCT_BUNDLE_IDENTIFIER = "com.veilid.veilidtools-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "veilidtools-tests-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -355,12 +355,13 @@ }; name = Debug; }; - 43C436C2268904AD002D11C5 /* Release */ = { + 43C436C2368904AD002D11C5 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ZJPQSFX5MW; INFOPLIST_FILE = "veilidtools-tests/Info.plist"; @@ -370,16 +371,13 @@ "@executable_path/Frameworks", ); OTHER_LDFLAGS = ""; - "OTHER_LDFLAGS[sdk=iphoneos*]" = ( - "-L../../../../../target/aarch64-apple-ios/release", - "-lveilid_tools", - ); "OTHER_LDFLAGS[sdk=iphonesimulator*]" = ( - "-L../../../../../target/x86_64-apple-ios/release", + "-L../../../../../target/lipo-ios-sim/release", "-lveilid_tools", ); PRODUCT_BUNDLE_IDENTIFIER = "com.veilid.veilidtools-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "veilidtools-tests-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -389,25 +387,25 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 43C436A7268904AC002D11C5 /* Build configuration list for PBXProject "veilidtools-tests" */ = { + 43C436A7368904AC002D11C5 /* Build configuration list for PBXProject "veilidtools-tests" */ = { isa = XCConfigurationList; buildConfigurations = ( - 43C436BE268904AD002D11C5 /* Debug */, - 43C436BF268904AD002D11C5 /* Release */, + 43C436BE368904AD002D11C5 /* Debug */, + 43C436BF368904AD002D11C5 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 43C436C0268904AD002D11C5 /* Build configuration list for PBXNativeTarget "veilidtools-tests" */ = { + 43C436C0368904AD002D11C5 /* Build configuration list for PBXNativeTarget "veilidtools-tests" */ = { isa = XCConfigurationList; buildConfigurations = ( - 43C436C1268904AD002D11C5 /* Debug */, - 43C436C2268904AD002D11C5 /* Release */, + 43C436C1368904AD002D11C5 /* Debug */, + 43C436C2368904AD002D11C5 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; - rootObject = 43C436A4268904AC002D11C5 /* Project object */; + rootObject = 43C436A4368904AC002D11C5 /* Project object */; } diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/xcshareddata/xcschemes/veilidcore-tests.xcscheme b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/xcshareddata/xcschemes/veilidtools-tests.xcscheme similarity index 92% rename from veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/xcshareddata/xcschemes/veilidcore-tests.xcscheme rename to veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/xcshareddata/xcschemes/veilidtools-tests.xcscheme index f1525db5..ec28aa85 100644 --- a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/xcshareddata/xcschemes/veilidcore-tests.xcscheme +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/xcshareddata/xcschemes/veilidtools-tests.xcscheme @@ -1,6 +1,6 @@ @@ -44,7 +44,7 @@ runnableDebuggingMode = "0"> @@ -61,7 +61,7 @@ runnableDebuggingMode = "0"> diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/ViewController.swift b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/ViewController.swift index 922d5462..0f0cf31f 100644 --- a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/ViewController.swift +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests/ViewController.swift @@ -6,12 +6,14 @@ // import UIKit +import Darwin class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() run_veilid_tools_tests() + exit(0) } diff --git a/veilid-tools/src/tests/mod.rs b/veilid-tools/src/tests/mod.rs index 0b6b6216..7208386a 100644 --- a/veilid-tools/src/tests/mod.rs +++ b/veilid-tools/src/tests/mod.rs @@ -1,3 +1,11 @@ +#[cfg(target_os = "android")] +mod android; pub mod common; +#[cfg(target_os = "ios")] +mod ios; #[cfg(not(target_arch = "wasm32"))] mod native; + +use super::*; + +pub use common::*; diff --git a/veilid-tools/src/tests/native/mod.rs b/veilid-tools/src/tests/native/mod.rs index 9ab6f14d..892e95b7 100644 --- a/veilid-tools/src/tests/native/mod.rs +++ b/veilid-tools/src/tests/native/mod.rs @@ -3,8 +3,7 @@ mod test_async_peek_stream; -use crate::tests::common::*; -use crate::*; +use super::*; #[cfg(all(target_os = "android", feature = "android_tests"))] use jni::{objects::JClass, objects::JObject, JNIEnv}; @@ -17,30 +16,15 @@ pub extern "system" fn Java_com_veilid_veilidtools_veilidtools_1android_1tests_M _class: JClass, ctx: JObject, ) { - crate::intf::utils::android::veilid_tools_setup_android( - env, - ctx, - "veilid_tools", - crate::veilid_config::VeilidConfigLogLevel::Trace, - ); + crate::tests::android::veilid_tools_setup(env, ctx, "veilid-tools"); run_all_tests(); } #[cfg(all(target_os = "ios", feature = "ios_tests"))] #[no_mangle] +#[allow(dead_code)] pub extern "C" fn run_veilid_tools_tests() { - let log_path: std::path::PathBuf = [ - std::env::var("HOME").unwrap().as_str(), - "Documents", - "veilid-tools.log", - ] - .iter() - .collect(); - crate::intf::utils::ios_test_setup::veilid_tools_setup( - "veilid-tools", - Some(Level::Trace), - Some((Level::Trace, log_path.as_path())), - ); + crate::tests::ios::veilid_tools_setup().expect("setup failed"); run_all_tests(); } @@ -88,43 +72,37 @@ fn exec_test_async_tag_lock() { cfg_if! { if #[cfg(test)] { - static DEFAULT_LOG_IGNORE_LIST: [&str; 21] = [ - "mio", - "h2", - "hyper", - "tower", - "tonic", - "tokio", - "runtime", - "tokio_util", - "want", - "serial_test", - "async_std", - "async_io", - "polling", - "rustls", - "async_tungstenite", - "tungstenite", - "netlink_proto", - "netlink_sys", - "trust_dns_resolver", - "trust_dns_proto", - "attohttpc", - ]; - use serial_test::serial; - use simplelog::*; use std::sync::Once; static SETUP_ONCE: Once = Once::new(); pub fn setup() { SETUP_ONCE.call_once(|| { - let mut cb = ConfigBuilder::new(); - for ig in DEFAULT_LOG_IGNORE_LIST { - cb.add_filter_ignore_str(ig); + + cfg_if! { + if #[cfg(feature = "tracing")] { + use tracing_subscriber::{filter, fmt, prelude::*}; + let mut filters = filter::Targets::new(); + for ig in DEFAULT_LOG_IGNORE_LIST { + filters = filters.with_target(ig, filter::LevelFilter::OFF); + } + let fmt_layer = fmt::layer(); + tracing_subscriber::registry() + .with(filters) + .with(filter::LevelFilter::TRACE) + .with(fmt_layer) + .init(); + } else { + use simplelog::*; + let mut cb = ConfigBuilder::new(); + for ig in DEFAULT_LOG_IGNORE_LIST { + cb.add_filter_ignore_str(ig); + } + TestLogger::init(LevelFilter::Trace, cb.build()).unwrap(); + } } - TestLogger::init(LevelFilter::Trace, cb.build()).unwrap(); + }); } diff --git a/veilid-tools/tests/web.rs b/veilid-tools/tests/web.rs index 6e4ba0f8..89905c70 100644 --- a/veilid-tools/tests/web.rs +++ b/veilid-tools/tests/web.rs @@ -1,7 +1,7 @@ //! Test suite for the Web and headless browsers. #![cfg(target_arch = "wasm32")] -use veilid_tools::tests::common::*; +use veilid_tools::tests::*; use veilid_tools::*; use wasm_bindgen_test::*; From c62d21e5cc20f6140656e7bfac029aa6dc06e0c0 Mon Sep 17 00:00:00 2001 From: John Smith Date: Sun, 27 Nov 2022 22:42:34 -0500 Subject: [PATCH 16/88] ios work --- .../veilidtools-tests.xcodeproj/project.pbxproj | 8 ++++++++ veilid-tools/src/tests/mod.rs | 1 + 2 files changed, 9 insertions(+) diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj index 51f168d3..d840dc66 100644 --- a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj @@ -341,6 +341,10 @@ "@executable_path/Frameworks", ); OTHER_LDFLAGS = ""; + "OTHER_LDFLAGS[sdk=iphoneos*]" = ( + "-L../../../../../target/lipo-ios/debug", + "-lveilid_tools", + ); "OTHER_LDFLAGS[sdk=iphonesimulator*]" = ( "-L../../../../../target/lipo-ios-sim/debug", "-lveilid_tools", @@ -371,6 +375,10 @@ "@executable_path/Frameworks", ); OTHER_LDFLAGS = ""; + "OTHER_LDFLAGS[sdk=iphoneos*]" = ( + "-L../../../../../target/lipo-ios/release", + "-lveilid_tools", + ); "OTHER_LDFLAGS[sdk=iphonesimulator*]" = ( "-L../../../../../target/lipo-ios-sim/release", "-lveilid_tools", diff --git a/veilid-tools/src/tests/mod.rs b/veilid-tools/src/tests/mod.rs index 7208386a..e10c08c2 100644 --- a/veilid-tools/src/tests/mod.rs +++ b/veilid-tools/src/tests/mod.rs @@ -6,6 +6,7 @@ mod ios; #[cfg(not(target_arch = "wasm32"))] mod native; +#[allow(unused_imports)] use super::*; pub use common::*; From 273a10f9666d4d39c5d5d1ac1f6d4f291ab95555 Mon Sep 17 00:00:00 2001 From: John Smith Date: Tue, 29 Nov 2022 12:16:28 -0500 Subject: [PATCH 17/88] veilid-tools work --- Cargo.lock | 206 +++++++++++++++++- setup_macos.sh | 2 +- veilid-tools/Cargo.toml | 8 +- veilid-tools/ios_build.sh | 6 +- veilid-tools/new_android_sim.sh | 24 ++ veilid-tools/new_ios_sim.sh | 9 + veilid-tools/run_tests.sh | 46 +++- veilid-tools/src/tests/android/mod.rs | 83 ++++--- .../android/veilidtools-tests/.idea/.name | 2 +- .../android/veilidtools-tests/.idea/vcs.xml | 1 + .../veilidtools-tests/app/build.gradle | 49 +++-- .../app/src/main/AndroidManifest.xml | 5 +- .../MainActivity.java | 4 +- .../android/veilidtools-tests/build.gradle | 6 +- .../gradle/wrapper/gradle-wrapper.properties | 6 +- .../android/veilidtools-tests/settings.gradle | 2 +- veilid-tools/src/tests/native/mod.rs | 2 +- veilid-tools/tests/web.rs | 2 + 18 files changed, 390 insertions(+), 73 deletions(-) create mode 100755 veilid-tools/new_android_sim.sh create mode 100755 veilid-tools/new_ios_sim.sh rename veilid-tools/src/tests/android/veilidtools-tests/app/src/main/java/com/veilid/{veilid-core/veilid-core_android_tests => veilidtools_tests}/MainActivity.java (87%) diff --git a/Cargo.lock b/Cargo.lock index b754585c..209bfa55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -100,6 +100,23 @@ dependencies = [ "syn", ] +[[package]] +name = "android-logd-logger" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53eff4527d2f64c8374a3bbe1d280ce660203e8c83e4a893231037a488639a7b" +dependencies = [ + "bytes 1.3.0", + "env_logger 0.8.4", + "lazy_static", + "libc", + "log", + "redox_syscall", + "thiserror", + "time 0.2.27", + "winapi 0.3.9", +] + [[package]] name = "android_log-sys" version = "0.2.0" @@ -113,7 +130,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e9dd62f37dea550caf48c77591dc50bd1a378ce08855be1a0c42a97b7550fb" dependencies = [ "android_log-sys", - "env_logger", + "env_logger 0.9.3", "log", "once_cell", ] @@ -398,7 +415,7 @@ dependencies = [ "futures-timer", "futures-util", "pin-project 1.0.12", - "rustc_version", + "rustc_version 0.4.0", "tokio 1.22.0", "wasm-bindgen-futures", ] @@ -411,7 +428,7 @@ checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" dependencies = [ "futures", "pharos", - "rustc_version", + "rustc_version 0.4.0", ] [[package]] @@ -518,6 +535,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + [[package]] name = "base64" version = "0.12.3" @@ -1059,6 +1082,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "const_fn" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" + [[package]] name = "constant_time_eq" version = "0.2.4" @@ -1564,6 +1593,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + [[package]] name = "dlv-list" version = "0.3.0" @@ -1673,6 +1708,16 @@ dependencies = [ "syn", ] +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" version = "0.9.3" @@ -3630,7 +3675,7 @@ checksum = "57959b91f0a133f89a68be874a5c88ed689c19cd729ecdb5d762ebf16c64d662" dependencies = [ "once_cell", "pest", - "sha1", + "sha1 0.10.5", ] [[package]] @@ -3650,7 +3695,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" dependencies = [ "futures", - "rustc_version", + "rustc_version 0.4.0", ] [[package]] @@ -3843,6 +3888,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + [[package]] name = "proc-macro2" version = "1.0.47" @@ -4287,13 +4338,22 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver", + "semver 1.0.14", ] [[package]] @@ -4425,12 +4485,27 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + [[package]] name = "semver" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "send_wrapper" version = "0.4.0" @@ -4576,6 +4651,15 @@ dependencies = [ "digest 0.10.6", ] +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + [[package]] name = "sha1" version = "0.10.5" @@ -4587,6 +4671,12 @@ dependencies = [ "digest 0.10.6", ] +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + [[package]] name = "sha2" version = "0.9.9" @@ -4741,12 +4831,70 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "standback" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" +dependencies = [ + "version_check", +] + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version 0.2.3", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1 0.6.1", + "syn", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + [[package]] name = "stop-token" version = "0.7.0" @@ -4896,6 +5044,21 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "time" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb", + "time-macros 0.1.1", + "version_check", + "winapi 0.3.9", +] + [[package]] name = "time" version = "0.3.17" @@ -4907,7 +5070,7 @@ dependencies = [ "num_threads", "serde", "time-core", - "time-macros", + "time-macros 0.2.6", ] [[package]] @@ -4916,6 +5079,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + [[package]] name = "time-macros" version = "0.2.6" @@ -4925,6 +5098,19 @@ dependencies = [ "time-core", ] +[[package]] +name = "time-macros-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -5737,6 +5923,7 @@ dependencies = [ name = "veilid-tools" version = "0.1.0" dependencies = [ + "android-logd-logger", "async-lock", "async-std", "async_executors", @@ -5747,12 +5934,13 @@ dependencies = [ "jni", "jni-sys", "js-sys", + "lazy_static", "libc", "log", "maplit", "ndk 0.6.0", "ndk-glue", - "nix 0.22.3", + "nix 0.25.0", "once_cell", "owo-colors", "parking_lot 0.11.2", @@ -6289,7 +6477,7 @@ dependencies = [ "futures", "js-sys", "pharos", - "rustc_version", + "rustc_version 0.4.0", "send_wrapper 0.5.0", "thiserror", "wasm-bindgen", diff --git a/setup_macos.sh b/setup_macos.sh index 91d3c4e9..5530c238 100755 --- a/setup_macos.sh +++ b/setup_macos.sh @@ -108,5 +108,5 @@ if [ "$BREW_USER" == "" ]; then BREW_USER=`whoami` fi fi -sudo -H -u $BREW_USER brew install capnp cmake wabt llvm protobuf +sudo -H -u $BREW_USER brew install capnp cmake wabt llvm protobuf openjdk@11 diff --git a/veilid-tools/Cargo.toml b/veilid-tools/Cargo.toml index 05732f9b..0a223274 100644 --- a/veilid-tools/Cargo.toml +++ b/veilid-tools/Cargo.toml @@ -6,15 +6,15 @@ edition = "2021" license = "LGPL-2.0-or-later OR MPL-2.0 OR (MIT AND BSD-3-Clause)" [lib] -# Staticlib for iOS tests, rlib for everything else -crate-type = [ "staticlib", "rlib" ] +# staticlib for iOS tests, cydlib for android tests, rlib for everything else +crate-type = [ "cdylib", "staticlib", "rlib" ] [features] default = [] rt-async-std = [ "async-std", "async_executors/async_std", ] rt-tokio = [ "tokio", "tokio-util", "async_executors/tokio_tp", "async_executors/tokio_io", "async_executors/tokio_timer", ] -android_tests = [] +android_tests = [ "dep:tracing-android" ] ios_tests = [] tracking = [] tracing = [ "dep:tracing", "dep:tracing-subscriber" ] @@ -61,7 +61,9 @@ jni = "^0" jni-sys = "^0" ndk = { version = "^0", features = ["trace"] } ndk-glue = { version = "^0", features = ["logger"] } +lazy_static = "^1.4.0" tracing-android = { version = "^0", optional = true } +android-logd-logger = "0.2.1" # Dependencies for Windows # [target.'cfg(target_os = "windows")'.dependencies] diff --git a/veilid-tools/ios_build.sh b/veilid-tools/ios_build.sh index c4aea7a4..2b1014dc 100755 --- a/veilid-tools/ios_build.sh +++ b/veilid-tools/ios_build.sh @@ -1,11 +1,11 @@ #!/bin/bash SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -CARGO_MANIFEST_PATH=$(python3 -c "import os; print(os.path.realpath(\"$SCRIPTDIR/Cargo.toml\"))") -TARGET_PATH=$(python3 -c "import os; print(os.path.realpath(\"$SCRIPTDIR/../target\"))") +CARGO_MANIFEST_PATH=$(python3 -c "import os; import json; print(json.loads(os.popen('cargo locate-project').read())['root'])") +CARGO_WORKSPACE_PATH=$(python3 -c "import os; import json; print(json.loads(os.popen('cargo locate-project --workspace').read())['root'])") +TARGET_PATH=$(python3 -c "import os; print(os.path.realpath(\"$CARGO_WORKSPACE_PATH/../target\"))") PACKAGE_NAME=$1 shift -# echo CARGO_MANIFEST_PATH: $CARGO_MANIFEST_PATH if [ "$CONFIGURATION" == "Debug" ]; then EXTRA_CARGO_OPTIONS="$@" diff --git a/veilid-tools/new_android_sim.sh b/veilid-tools/new_android_sim.sh new file mode 100755 index 00000000..eac100d0 --- /dev/null +++ b/veilid-tools/new_android_sim.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +UNAME_M=`uname -m` +if [[ "$UNAME_M" == "arm64" ]]; then + ANDROID_ABI=arm64-v8a +elif [[ "$UNAME_M" == "x86_64" ]]; then + ANDROID_ABI=x86 +else + echo "Unknown platform" + exit 1 +fi +AVD_NAME="testavd" +AVD_TAG="google_atd" +AVD_IMAGE="system-images;android-30;$AVD_TAG;$ANDROID_ABI" +AVD_DEVICE="Nexus 10" +# Install AVD image +$ANDROID_SDK_ROOT/tools/bin/sdkmanager --install "$AVD_IMAGE" +# Make AVD +echo "no" | $ANDROID_SDK_ROOT/tools/bin/avdmanager --verbose create avd --force --name "$AVD_NAME" --package "$AVD_IMAGE" --tag "$AVD_TAG" --abi "$ANDROID_ABI" --device "$AVD_DEVICE" +# Run emulator +$ANDROID_SDK_ROOT/emulator/emulator -avd testavd -no-snapshot -no-boot-anim -no-window & +( trap exit SIGINT ; read -r -d '' _ /dev/null) +xcrun simctl boot $ID +xcrun simctl bootstatus $ID +echo Simulator ID is $ID +( trap exit SIGINT ; read -r -d '' _ /dev/null) - xcrun simctl boot $ID - xcrun simctl bootstatus $ID + + # Run in temporary simulator xcrun simctl install $ID $SYMROOT/Debug-iphonesimulator/$APPNAME.app xcrun simctl launch --console $ID $BUNDLENAME - xcrun simctl delete all + + # Clean up build output rm -rf /tmp/testout + +elif [[ "$1" == "android" ]]; then + ID="$2" + if [[ "$ID" == "" ]]; then + echo "No emulator ID specified" + exit 1 + fi + APPNAME=veilidtools-tests + APPID=com.veilid.veilidtools_tests + ACTIVITYNAME=MainActivity + pushd src/tests/android/$APPNAME >/dev/null + # Build apk + ./gradlew assembleDebug + # Wait for boot + adb -s $ID wait-for-device + # Install app + adb -s $ID install -r ./app/build/outputs/apk/debug/app-debug.apk + # Start activity + adb -s $ID shell am start-activity -W $APPID/.$ACTIVITYNAME + # Get the pid of the program + APP_PID=`adb -s $ID shell pidof -s $APPID` + # Print the logcat + adb -s $ID shell logcat -d veilid-tools:V *:S & + # Wait for the pid to be done + while [ "$(adb -s $ID shell pidof -s $APPID)" != "" ]; do + sleep 1 + done + # Terminate logcat + kill %1 + # Finished + popd >/dev/null + else cargo test --features=rt-tokio cargo test --features=rt-async-std diff --git a/veilid-tools/src/tests/android/mod.rs b/veilid-tools/src/tests/android/mod.rs index 6fb0ad83..c238052e 100644 --- a/veilid-tools/src/tests/android/mod.rs +++ b/veilid-tools/src/tests/android/mod.rs @@ -1,13 +1,10 @@ use super::*; -use jni::errors::Result as JniResult; -use jni::{objects::GlobalRef, objects::JObject, objects::JString, JNIEnv, JavaVM}; +//use jni::errors::Result as JniResult; +use jni::{objects::GlobalRef, objects::JObject, JNIEnv, JavaVM}; use lazy_static::*; use std::backtrace::Backtrace; use std::panic; -use tracing::*; -use tracing_subscriber::prelude::*; -use tracing_subscriber::*; pub struct AndroidGlobals { pub vm: JavaVM, @@ -35,46 +32,82 @@ pub fn veilid_tools_setup_android_no_log<'a>(env: JNIEnv<'a>, ctx: JObject<'a>) pub fn veilid_tools_setup<'a>(env: JNIEnv<'a>, ctx: JObject<'a>, log_tag: &'a str) { cfg_if! { if #[cfg(feature = "tracing")] { - // Set up subscriber and layers + use tracing::*; + use tracing_subscriber::prelude::*; + use tracing_subscriber::*; + let mut filters = filter::Targets::new(); + for ig in DEFAULT_LOG_IGNORE_LIST { + filters = filters.with_target(ig, filter::LevelFilter::OFF); + } + + // Set up subscriber and layers let subscriber = Registry::default(); let mut layers = Vec::new(); let layer = tracing_android::layer(log_tag) .expect("failed to set up android logging") - .with_filter(LevelFilter::TRACE); + .with_filter(filter::LevelFilter::TRACE) + .with_filter(filters); layers.push(layer.boxed()); let subscriber = subscriber.with(layers); subscriber .try_init() .expect("failed to init android tracing"); + } else { + let mut builder = android_logd_logger::builder(); + builder.tag(log_tag); + builder.prepend_module(true); + builder.filter_level(LevelFilter::Trace); + for ig in DEFAULT_LOG_IGNORE_LIST { + builder.filter_module(ig, LevelFilter::Off); + } + builder.init(); } } // Set up panic hook for backtraces panic::set_hook(Box::new(|panic_info| { let bt = Backtrace::capture(); + if let Some(location) = panic_info.location() { + error!( + "panic occurred in file '{}' at line {}", + location.file(), + location.line(), + ); + } else { + error!("panic occurred but can't get location information..."); + } + if let Some(s) = panic_info.payload().downcast_ref::<&str>() { + error!("panic payload: {:?}", s); + } else if let Some(s) = panic_info.payload().downcast_ref::() { + error!("panic payload: {:?}", s); + } else if let Some(a) = panic_info.payload().downcast_ref::() { + error!("panic payload: {:?}", a); + } else { + error!("no panic payload"); + } error!("Backtrace:\n{:?}", bt); })); - veilid_core_setup_android_no_log(env, ctx); + veilid_tools_setup_android_no_log(env, ctx); } -pub fn get_android_globals() -> (JavaVM, GlobalRef) { - let globals_locked = ANDROID_GLOBALS.lock(); - let globals = globals_locked.as_ref().unwrap(); - let env = globals.vm.attach_current_thread_as_daemon().unwrap(); - let vm = env.get_java_vm().unwrap(); - let ctx = globals.ctx.clone(); - (vm, ctx) -} +// pub fn get_android_globals() -> (JavaVM, GlobalRef) { +// let globals_locked = ANDROID_GLOBALS.lock(); +// let globals = globals_locked.as_ref().unwrap(); +// let env = globals.vm.attach_current_thread_as_daemon().unwrap(); +// let vm = env.get_java_vm().unwrap(); +// let ctx = globals.ctx.clone(); +// (vm, ctx) +// } -pub fn with_null_local_frame<'b, T, F>(env: JNIEnv<'b>, s: i32, f: F) -> JniResult -where - F: FnOnce() -> JniResult, -{ - env.push_local_frame(s)?; - let out = f(); - env.pop_local_frame(JObject::null())?; - out -} +// pub fn with_null_local_frame<'b, T, F>(env: JNIEnv<'b>, s: i32, f: F) -> JniResult +// where +// F: FnOnce() -> JniResult, +// { +// env.push_local_frame(s)?; +// let out = f(); +// env.pop_local_frame(JObject::null())?; +// out +// } diff --git a/veilid-tools/src/tests/android/veilidtools-tests/.idea/.name b/veilid-tools/src/tests/android/veilidtools-tests/.idea/.name index cde590be..2efbb980 100644 --- a/veilid-tools/src/tests/android/veilidtools-tests/.idea/.name +++ b/veilid-tools/src/tests/android/veilidtools-tests/.idea/.name @@ -1 +1 @@ -Veilid Tools Tests \ No newline at end of file +Veilid-Tools Tests \ No newline at end of file diff --git a/veilid-tools/src/tests/android/veilidtools-tests/.idea/vcs.xml b/veilid-tools/src/tests/android/veilidtools-tests/.idea/vcs.xml index 4fce1d86..c68f248e 100644 --- a/veilid-tools/src/tests/android/veilidtools-tests/.idea/vcs.xml +++ b/veilid-tools/src/tests/android/veilidtools-tests/.idea/vcs.xml @@ -1,6 +1,7 @@ + \ No newline at end of file diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/build.gradle b/veilid-tools/src/tests/android/veilidtools-tests/app/build.gradle index 21c6c24a..5e30101c 100644 --- a/veilid-tools/src/tests/android/veilidtools-tests/app/build.gradle +++ b/veilid-tools/src/tests/android/veilidtools-tests/app/build.gradle @@ -3,13 +3,13 @@ plugins { } android { - compileSdkVersion 30 - buildToolsVersion "30.0.3" + compileSdkVersion 33 + buildToolsVersion "33.0.1" defaultConfig { - applicationId "com.veilid.veilidtools.veilidtools_android_tests" + applicationId "com.veilid.veilidtools_tests" minSdkVersion 24 - targetSdkVersion 30 + targetSdkVersion 33 versionCode 1 versionName "1.0" @@ -38,33 +38,53 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - ndkVersion '22.0.7026061' + ndkVersion '25.1.8937393' // Required to copy libc++_shared.so externalNativeBuild { cmake { + version '3.22.1' path file('CMakeLists.txt') } } + namespace 'com.veilid.veilidtools_tests' + + testOptions { + managedDevices { + devices { + pixel2api30 (com.android.build.api.dsl.ManagedVirtualDevice) { + // Use device profiles you typically see in Android Studio. + device = "Pixel 2" + // ATD currently support only API level 30. + apiLevel = 30 + // You can also specify "google-atd" if you require Google + // Play Services. + systemImageSource = "aosp-atd" + // Whether the image must be a 64 bit image. + require64Bit = false + } + } + } + } } dependencies { - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation 'com.google.android.material:material:1.4.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.2' - implementation 'androidx.security:security-crypto:1.1.0-alpha03' - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + implementation 'androidx.appcompat:appcompat:1.5.1' + implementation 'com.google.android.material:material:1.7.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.security:security-crypto:1.1.0-alpha04' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' + androidTestImplementation 'androidx.test:runner:1.5.1' + androidTestImplementation 'androidx.test:rules:1.5.0' } apply plugin: 'org.mozilla.rust-android-gradle.rust-android' cargo { - module = "../../../../../veilid-tools" + module = "../../../../../" libname = "veilid_tools" targets = ["arm", "arm64", "x86", "x86_64"] - targetDirectory = "../../../../../target" + targetDirectory = "../../../../../../target" prebuiltToolchains = true profile = gradle.startParameter.taskNames.any{it.toLowerCase().contains("debug")} ? "debug" : "release" pythonCommand = "python3" @@ -84,4 +104,3 @@ afterEvaluate { tasks["generate${productFlavor}${buildType}Assets"].dependsOn(tasks["cargoBuild"]) } } - diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/AndroidManifest.xml b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/AndroidManifest.xml index 9c9b4431..e21e0583 100644 --- a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/AndroidManifest.xml +++ b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + @@ -13,7 +12,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.VeilidToolsTests"> - + diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/java/com/veilid/veilid-core/veilid-core_android_tests/MainActivity.java b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/java/com/veilid/veilidtools_tests/MainActivity.java similarity index 87% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/java/com/veilid/veilid-core/veilid-core_android_tests/MainActivity.java rename to veilid-tools/src/tests/android/veilidtools-tests/app/src/main/java/com/veilid/veilidtools_tests/MainActivity.java index 2b73f488..8e24334f 100644 --- a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/java/com/veilid/veilid-core/veilid-core_android_tests/MainActivity.java +++ b/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/java/com/veilid/veilidtools_tests/MainActivity.java @@ -1,4 +1,4 @@ -package com.veilid.veilidtools.veilidtools_android_tests; +package com.veilid.veilidtools_tests; import androidx.appcompat.app.AppCompatActivity; import android.content.Context; @@ -23,6 +23,8 @@ public class MainActivity extends AppCompatActivity { public void run() { run_tests(this.context); + ((MainActivity)this.context).finish(); + System.exit(0); } } diff --git a/veilid-tools/src/tests/android/veilidtools-tests/build.gradle b/veilid-tools/src/tests/android/veilidtools-tests/build.gradle index 96496236..e49af7c9 100644 --- a/veilid-tools/src/tests/android/veilidtools-tests/build.gradle +++ b/veilid-tools/src/tests/android/veilidtools-tests/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath "com.android.tools.build:gradle:4.1.2" + classpath 'com.android.tools.build:gradle:7.3.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -13,7 +13,7 @@ buildscript { } plugins { - id "org.mozilla.rust-android-gradle.rust-android" version "0.9.0" + id "org.mozilla.rust-android-gradle.rust-android" version "0.9.3" } allprojects { @@ -25,4 +25,4 @@ allprojects { task clean(type: Delete) { delete rootProject.buildDir -} \ No newline at end of file +} diff --git a/veilid-tools/src/tests/android/veilidtools-tests/gradle/wrapper/gradle-wrapper.properties b/veilid-tools/src/tests/android/veilidtools-tests/gradle/wrapper/gradle-wrapper.properties index 3a56e3d2..0f75328c 100644 --- a/veilid-tools/src/tests/android/veilidtools-tests/gradle/wrapper/gradle-wrapper.properties +++ b/veilid-tools/src/tests/android/veilidtools-tests/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Jun 21 14:26:26 PDT 2021 +#Mon Nov 28 22:38:53 EST 2022 distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip +zipStoreBase=GRADLE_USER_HOME diff --git a/veilid-tools/src/tests/android/veilidtools-tests/settings.gradle b/veilid-tools/src/tests/android/veilidtools-tests/settings.gradle index ff5d71fe..a391cf6f 100644 --- a/veilid-tools/src/tests/android/veilidtools-tests/settings.gradle +++ b/veilid-tools/src/tests/android/veilidtools-tests/settings.gradle @@ -1,2 +1,2 @@ include ':app' -rootProject.name = "Veilid Tools Tests" \ No newline at end of file +rootProject.name = "Veilid-Tools Tests" \ No newline at end of file diff --git a/veilid-tools/src/tests/native/mod.rs b/veilid-tools/src/tests/native/mod.rs index 892e95b7..b4d2728e 100644 --- a/veilid-tools/src/tests/native/mod.rs +++ b/veilid-tools/src/tests/native/mod.rs @@ -11,7 +11,7 @@ use jni::{objects::JClass, objects::JObject, JNIEnv}; #[cfg(all(target_os = "android", feature = "android_tests"))] #[no_mangle] #[allow(non_snake_case)] -pub extern "system" fn Java_com_veilid_veilidtools_veilidtools_1android_1tests_MainActivity_run_1tests( +pub extern "system" fn Java_com_veilid_veilidtools_1tests_MainActivity_run_1tests( env: JNIEnv, _class: JClass, ctx: JObject, diff --git a/veilid-tools/tests/web.rs b/veilid-tools/tests/web.rs index 89905c70..ac81ab39 100644 --- a/veilid-tools/tests/web.rs +++ b/veilid-tools/tests/web.rs @@ -22,6 +22,8 @@ pub fn setup() -> () { builder.set_max_level(Level::TRACE); builder.set_console_config(tracing_wasm::ConsoleConfig::ReportWithConsoleColor); tracing_wasm::set_as_global_default_with_config(builder.build()); + } else { + wasm_logger::init(wasm_logger::Config::default()); } } }); From f7582fabb28873bad9d923420b8d5701c935b832 Mon Sep 17 00:00:00 2001 From: John Smith Date: Tue, 29 Nov 2022 12:32:05 -0500 Subject: [PATCH 18/88] refactor --- scripts/{ => deprecated}/debug_main_node.sh | 0 scripts/{ => deprecated}/debug_subnode_1.sh | 0 scripts/{ => deprecated}/local-test.yml | 0 scripts/{ => deprecated}/run_2.sh | 0 scripts/{ => deprecated}/run_20.sh | 0 scripts/{ => deprecated}/run_3.sh | 0 scripts/{ => deprecated}/run_4.sh | 0 scripts/{ => deprecated}/run_8.sh | 0 scripts/{ => deprecated}/run_local_test.py | 0 {veilid-tools => scripts}/ios_build.sh | 19 +++++--- {veilid-tools => scripts}/new_android_sim.sh | 0 {veilid-tools => scripts}/new_ios_sim.sh | 0 veilid-core/ios_build.sh | 44 ------------------- .../project.pbxproj | 2 +- 14 files changed, 14 insertions(+), 51 deletions(-) rename scripts/{ => deprecated}/debug_main_node.sh (100%) rename scripts/{ => deprecated}/debug_subnode_1.sh (100%) rename scripts/{ => deprecated}/local-test.yml (100%) rename scripts/{ => deprecated}/run_2.sh (100%) rename scripts/{ => deprecated}/run_20.sh (100%) rename scripts/{ => deprecated}/run_3.sh (100%) rename scripts/{ => deprecated}/run_4.sh (100%) rename scripts/{ => deprecated}/run_8.sh (100%) rename scripts/{ => deprecated}/run_local_test.py (100%) rename {veilid-tools => scripts}/ios_build.sh (85%) rename {veilid-tools => scripts}/new_android_sim.sh (100%) rename {veilid-tools => scripts}/new_ios_sim.sh (100%) delete mode 100755 veilid-core/ios_build.sh diff --git a/scripts/debug_main_node.sh b/scripts/deprecated/debug_main_node.sh similarity index 100% rename from scripts/debug_main_node.sh rename to scripts/deprecated/debug_main_node.sh diff --git a/scripts/debug_subnode_1.sh b/scripts/deprecated/debug_subnode_1.sh similarity index 100% rename from scripts/debug_subnode_1.sh rename to scripts/deprecated/debug_subnode_1.sh diff --git a/scripts/local-test.yml b/scripts/deprecated/local-test.yml similarity index 100% rename from scripts/local-test.yml rename to scripts/deprecated/local-test.yml diff --git a/scripts/run_2.sh b/scripts/deprecated/run_2.sh similarity index 100% rename from scripts/run_2.sh rename to scripts/deprecated/run_2.sh diff --git a/scripts/run_20.sh b/scripts/deprecated/run_20.sh similarity index 100% rename from scripts/run_20.sh rename to scripts/deprecated/run_20.sh diff --git a/scripts/run_3.sh b/scripts/deprecated/run_3.sh similarity index 100% rename from scripts/run_3.sh rename to scripts/deprecated/run_3.sh diff --git a/scripts/run_4.sh b/scripts/deprecated/run_4.sh similarity index 100% rename from scripts/run_4.sh rename to scripts/deprecated/run_4.sh diff --git a/scripts/run_8.sh b/scripts/deprecated/run_8.sh similarity index 100% rename from scripts/run_8.sh rename to scripts/deprecated/run_8.sh diff --git a/scripts/run_local_test.py b/scripts/deprecated/run_local_test.py similarity index 100% rename from scripts/run_local_test.py rename to scripts/deprecated/run_local_test.py diff --git a/veilid-tools/ios_build.sh b/scripts/ios_build.sh similarity index 85% rename from veilid-tools/ios_build.sh rename to scripts/ios_build.sh index 2b1014dc..4062014b 100755 --- a/veilid-tools/ios_build.sh +++ b/scripts/ios_build.sh @@ -1,8 +1,17 @@ #!/bin/bash -SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -CARGO_MANIFEST_PATH=$(python3 -c "import os; import json; print(json.loads(os.popen('cargo locate-project').read())['root'])") -CARGO_WORKSPACE_PATH=$(python3 -c "import os; import json; print(json.loads(os.popen('cargo locate-project --workspace').read())['root'])") +CARGO=`which cargo` +CARGO=${CARGO:=~/.cargo/bin/cargo} +CARGO_DIR=$(dirname $CARGO) + +WORKING_DIR=$1 +shift +echo $WORKING_DIR +pushd $WORKING_DIR >/dev/null +echo PWD: `pwd` + +CARGO_MANIFEST_PATH=$(python3 -c "import os; import json; print(json.loads(os.popen('$CARGO locate-project').read())['root'])") +CARGO_WORKSPACE_PATH=$(python3 -c "import os; import json; print(json.loads(os.popen('$CARGO locate-project --workspace').read())['root'])") TARGET_PATH=$(python3 -c "import os; print(os.path.realpath(\"$CARGO_WORKSPACE_PATH/../target\"))") PACKAGE_NAME=$1 shift @@ -42,9 +51,7 @@ do continue fi - CARGO=`which cargo` - CARGO=${CARGO:=~/.cargo/bin/cargo} - CARGO_DIR=$(dirname $CARGO) + # Choose arm64 brew for unit tests by default if we are on M1 if [ -f /opt/homebrew/bin/brew ]; then diff --git a/veilid-tools/new_android_sim.sh b/scripts/new_android_sim.sh similarity index 100% rename from veilid-tools/new_android_sim.sh rename to scripts/new_android_sim.sh diff --git a/veilid-tools/new_ios_sim.sh b/scripts/new_ios_sim.sh similarity index 100% rename from veilid-tools/new_ios_sim.sh rename to scripts/new_ios_sim.sh diff --git a/veilid-core/ios_build.sh b/veilid-core/ios_build.sh deleted file mode 100755 index 4eb08eca..00000000 --- a/veilid-core/ios_build.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -CARGO_MANIFEST_PATH=$(python -c "import os; print(os.path.realpath(\"$SCRIPTDIR/Cargo.toml\"))") -# echo CARGO_MANIFEST_PATH: $CARGO_MANIFEST_PATH - -if [ "$CONFIGURATION" == "Debug" ]; then - EXTRA_CARGO_OPTIONS="$@" -else - EXTRA_CARGO_OPTIONS="$@ --release" -fi -ARCHS=${ARCHS:=arm64} -for arch in $ARCHS -do - if [ "$arch" == "arm64" ]; then - echo arm64 - CARGO_TARGET=aarch64-apple-ios - #CARGO_TOOLCHAIN=+ios-arm64-1.57.0 - CARGO_TOOLCHAIN= - elif [ "$arch" == "x86_64" ]; then - echo x86_64 - CARGO_TARGET=x86_64-apple-ios - CARGO_TOOLCHAIN= - else - echo Unsupported ARCH: $arch - continue - fi - - CARGO=`which cargo` - CARGO=${CARGO:=~/.cargo/bin/cargo} - CARGO_DIR=$(dirname $CARGO) - - # Choose arm64 brew for unit tests by default if we are on M1 - if [ -f /opt/homebrew/bin/brew ]; then - HOMEBREW_DIR=/opt/homebrew/bin - elif [ -f /usr/local/bin/brew ]; then - HOMEBREW_DIR=/usr/local/bin - else - HOMEBREW_DIR=$(dirname `which brew`) - fi - - env -i PATH=/usr/bin:/bin:$HOMEBREW_DIR:$CARGO_DIR HOME="$HOME" USER="$USER" cargo $CARGO_TOOLCHAIN build $EXTRA_CARGO_OPTIONS --target $CARGO_TARGET --manifest-path $CARGO_MANIFEST_PATH -done - diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj index d840dc66..17193b37 100644 --- a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj @@ -167,7 +167,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "../../../../ios_build.sh veilid_tools --features ios_tests,rt-tokio\n"; + shellScript = "../../../../../scripts/ios_build.sh ../../../../ veilid_tools --features ios_tests,rt-tokio\n"; }; /* End PBXShellScriptBuildPhase section */ From 5c0a5009717c15a705be6582858400bb577886df Mon Sep 17 00:00:00 2001 From: John Smith Date: Tue, 29 Nov 2022 19:22:33 -0500 Subject: [PATCH 19/88] core fixes --- Cargo.lock | 2 +- veilid-core/Cargo.toml | 1 - veilid-core/run_tests.sh | 62 ++ veilid-core/src/api_tracing_layer.rs | 2 +- veilid-core/src/attachment_manager.rs | 2 - veilid-core/src/core_context.rs | 1 - veilid-core/src/crypto/envelope.rs | 1 - veilid-core/src/crypto/key.rs | 1 - veilid-core/src/crypto/mod.rs | 2 +- veilid-core/src/crypto/receipt.rs | 1 - veilid-core/src/crypto/tests/test_crypto.rs | 1 - veilid-core/src/crypto/tests/test_dht_key.rs | 1 - .../src/crypto/tests/test_envelope_receipt.rs | 1 - .../{utils => }/android/get_directories.rs | 1 - .../intf/native/{utils => }/android/mod.rs | 1 - veilid-core/src/intf/native/block_store.rs | 1 - .../src/intf/native/{utils => }/ios/mod.rs | 1 - veilid-core/src/intf/native/mod.rs | 7 +- .../{utils => }/network_interfaces/apple.rs | 2 +- .../{utils => }/network_interfaces/mod.rs | 4 +- .../{utils => }/network_interfaces/netlink.rs | 1 - .../network_interfaces/sockaddr_tools.rs | 0 .../{utils => }/network_interfaces/tools.rs | 0 .../{utils => }/network_interfaces/windows.rs | 3 +- .../src/intf/native/protected_store.rs | 1 - veilid-core/src/intf/native/system.rs | 3 +- veilid-core/src/intf/native/table_store.rs | 1 - veilid-core/src/intf/native/utils/mod.rs | 5 - veilid-core/src/intf/table_db.rs | 1 - veilid-core/src/intf/wasm/block_store.rs | 2 - veilid-core/src/intf/wasm/protected_store.rs | 2 +- veilid-core/src/intf/wasm/system.rs | 2 - veilid-core/src/intf/wasm/table_store.rs | 1 - veilid-core/src/intf/wasm/utils/mod.rs | 1 - veilid-core/src/lib.rs | 3 +- veilid-core/src/network_manager/mod.rs | 1 - veilid-core/src/network_manager/native/mod.rs | 2 +- .../network_manager/native/protocol/mod.rs | 1 - .../native/protocol/sockets.rs | 1 - veilid-core/src/receipt_manager.rs | 1 - veilid-core/src/routing_table/mod.rs | 1 - .../src/routing_table/stats_accounting.rs | 1 - veilid-core/src/rpc_processor/mod.rs | 1 - veilid-core/src/tests/common/mod.rs | 1 - .../src/tests/common/test_async_tag_lock.rs | 158 ----- .../src/tests/common/test_host_interface.rs | 556 +----------------- .../src/tests/common/test_protected_store.rs | 1 - .../src/tests/common/test_table_store.rs | 1 - .../src/tests/common/test_veilid_config.rs | 2 +- .../src/tests/common/test_veilid_core.rs | 1 - veilid-core/src/tests/native/mod.rs | 34 +- .../tests/native/test_async_peek_stream.rs | 352 ----------- veilid-core/src/veilid_api/mod.rs | 8 +- veilid-core/src/veilid_config.rs | 1 - veilid-core/src/veilid_layer_filter.rs | 1 - veilid-tools/Cargo.toml | 1 + .../src/callback_state_machine.rs | 3 +- veilid-tools/src/lib.rs | 2 + .../src/tests/common/test_host_interface.rs | 4 +- veilid-tools/tests/web.rs | 2 +- 60 files changed, 98 insertions(+), 1160 deletions(-) create mode 100755 veilid-core/run_tests.sh rename veilid-core/src/intf/native/{utils => }/android/get_directories.rs (98%) rename veilid-core/src/intf/native/{utils => }/android/mod.rs (99%) rename veilid-core/src/intf/native/{utils => }/ios/mod.rs (99%) rename veilid-core/src/intf/native/{utils => }/network_interfaces/apple.rs (99%) rename veilid-core/src/intf/native/{utils => }/network_interfaces/mod.rs (99%) rename veilid-core/src/intf/native/{utils => }/network_interfaces/netlink.rs (99%) rename veilid-core/src/intf/native/{utils => }/network_interfaces/sockaddr_tools.rs (100%) rename veilid-core/src/intf/native/{utils => }/network_interfaces/tools.rs (100%) rename veilid-core/src/intf/native/{utils => }/network_interfaces/windows.rs (99%) delete mode 100644 veilid-core/src/intf/native/utils/mod.rs delete mode 100644 veilid-core/src/intf/wasm/utils/mod.rs delete mode 100644 veilid-core/src/tests/common/test_async_tag_lock.rs delete mode 100644 veilid-core/src/tests/native/test_async_peek_stream.rs rename {veilid-core => veilid-tools}/src/callback_state_machine.rs (98%) diff --git a/Cargo.lock b/Cargo.lock index 209bfa55..31a72bd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5800,7 +5800,6 @@ dependencies = [ "rkyv", "rtnetlink", "rusqlite", - "rust-fsm", "rustls", "rustls-pemfile", "secrecy", @@ -5945,6 +5944,7 @@ dependencies = [ "owo-colors", "parking_lot 0.11.2", "rand 0.7.3", + "rust-fsm", "send_wrapper 0.6.0", "serial_test", "simplelog 0.12.0", diff --git a/veilid-core/Cargo.toml b/veilid-core/Cargo.toml index 626ec92f..fa0656da 100644 --- a/veilid-core/Cargo.toml +++ b/veilid-core/Cargo.toml @@ -25,7 +25,6 @@ tracing-subscriber = "^0" tracing-error = "^0" eyre = "^0" capnp = { version = "^0", default_features = false } -rust-fsm = "^0" static_assertions = "^1" cfg-if = "^1" thiserror = "^1" diff --git a/veilid-core/run_tests.sh b/veilid-core/run_tests.sh new file mode 100755 index 00000000..a4bbb16f --- /dev/null +++ b/veilid-core/run_tests.sh @@ -0,0 +1,62 @@ +#!/bin/bash +SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +pushd $SCRIPTDIR 2>/dev/null +if [[ "$1" == "wasm" ]]; then + WASM_BINDGEN_TEST_TIMEOUT=120 wasm-pack test --chrome --headless +elif [[ "$1" == "ios" ]]; then + SYMROOT=/tmp/testout + APPNAME=veilidcore-tests + BUNDLENAME=com.veilid.veilidcore-tests + ID="$2" + if [[ "$ID" == "" ]]; then + echo "No emulator ID specified" + exit 1 + fi + + # Build for simulator + xcrun xcodebuild -project src/tests/ios/$APPNAME/$APPNAME.xcodeproj/ -scheme $APPNAME -destination "generic/platform=iOS Simulator" SYMROOT=$SYMROOT + + # Run in temporary simulator + xcrun simctl install $ID $SYMROOT/Debug-iphonesimulator/$APPNAME.app + xcrun simctl launch --console $ID $BUNDLENAME + + # Clean up build output + rm -rf /tmp/testout + +elif [[ "$1" == "android" ]]; then + ID="$2" + if [[ "$ID" == "" ]]; then + echo "No emulator ID specified" + exit 1 + fi + APPNAME=veilidcore-tests + APPID=com.veilid.veilidcore_tests + ACTIVITYNAME=MainActivity + pushd src/tests/android/$APPNAME >/dev/null + # Build apk + ./gradlew assembleDebug + # Wait for boot + adb -s $ID wait-for-device + # Install app + adb -s $ID install -r ./app/build/outputs/apk/debug/app-debug.apk + # Start activity + adb -s $ID shell am start-activity -W $APPID/.$ACTIVITYNAME + # Get the pid of the program + APP_PID=`adb -s $ID shell pidof -s $APPID` + # Print the logcat + adb -s $ID shell logcat -d veilid-core:V *:S & + # Wait for the pid to be done + while [ "$(adb -s $ID shell pidof -s $APPID)" != "" ]; do + sleep 1 + done + # Terminate logcat + kill %1 + # Finished + popd >/dev/null + +else + cargo test --features=rt-tokio + cargo test --features=rt-async-std +fi +popd 2>/dev/null \ No newline at end of file diff --git a/veilid-core/src/api_tracing_layer.rs b/veilid-core/src/api_tracing_layer.rs index dac00241..8de371ec 100644 --- a/veilid-core/src/api_tracing_layer.rs +++ b/veilid-core/src/api_tracing_layer.rs @@ -1,6 +1,6 @@ use crate::core_context::*; use crate::veilid_api::*; -use crate::xx::*; +use crate::*; use core::fmt::Write; use once_cell::sync::OnceCell; use tracing_subscriber::*; diff --git a/veilid-core/src/attachment_manager.rs b/veilid-core/src/attachment_manager.rs index c05cf628..c47b9f52 100644 --- a/veilid-core/src/attachment_manager.rs +++ b/veilid-core/src/attachment_manager.rs @@ -1,8 +1,6 @@ -use crate::callback_state_machine::*; use crate::crypto::Crypto; use crate::network_manager::*; use crate::routing_table::*; -use crate::xx::*; use crate::*; use core::convert::TryFrom; use core::fmt; diff --git a/veilid-core/src/core_context.rs b/veilid-core/src/core_context.rs index 8af47ab5..aa4c2593 100644 --- a/veilid-core/src/core_context.rs +++ b/veilid-core/src/core_context.rs @@ -3,7 +3,6 @@ use crate::attachment_manager::*; use crate::crypto::Crypto; use crate::veilid_api::*; use crate::veilid_config::*; -use crate::xx::*; use crate::*; pub type UpdateCallback = Arc; diff --git a/veilid-core/src/crypto/envelope.rs b/veilid-core/src/crypto/envelope.rs index ea84e5c6..efdec697 100644 --- a/veilid-core/src/crypto/envelope.rs +++ b/veilid-core/src/crypto/envelope.rs @@ -2,7 +2,6 @@ #![allow(clippy::absurd_extreme_comparisons)] use super::*; use crate::routing_table::VersionRange; -use crate::xx::*; use crate::*; use core::convert::TryInto; diff --git a/veilid-core/src/crypto/key.rs b/veilid-core/src/crypto/key.rs index 02a4ef72..218ffcb8 100644 --- a/veilid-core/src/crypto/key.rs +++ b/veilid-core/src/crypto/key.rs @@ -1,4 +1,3 @@ -use crate::xx::*; use crate::*; use core::cmp::{Eq, Ord, PartialEq, PartialOrd}; diff --git a/veilid-core/src/crypto/mod.rs b/veilid-core/src/crypto/mod.rs index 30b658e5..60c96897 100644 --- a/veilid-core/src/crypto/mod.rs +++ b/veilid-core/src/crypto/mod.rs @@ -13,7 +13,6 @@ pub use value::*; pub const MIN_CRYPTO_VERSION: u8 = 0u8; pub const MAX_CRYPTO_VERSION: u8 = 0u8; -use crate::xx::*; use crate::*; use chacha20::cipher::{KeyIvInit, StreamCipher}; use chacha20::XChaCha20; @@ -25,6 +24,7 @@ use ed25519_dalek as ed; use hashlink::linked_hash_map::Entry; use hashlink::LruCache; use serde::{Deserialize, Serialize}; + use x25519_dalek as xd; pub type SharedSecret = [u8; 32]; diff --git a/veilid-core/src/crypto/receipt.rs b/veilid-core/src/crypto/receipt.rs index b622d2f9..d59e5f36 100644 --- a/veilid-core/src/crypto/receipt.rs +++ b/veilid-core/src/crypto/receipt.rs @@ -1,7 +1,6 @@ #![allow(dead_code)] #![allow(clippy::absurd_extreme_comparisons)] use super::*; -use crate::xx::*; use crate::*; use core::convert::TryInto; use data_encoding::BASE64URL_NOPAD; diff --git a/veilid-core/src/crypto/tests/test_crypto.rs b/veilid-core/src/crypto/tests/test_crypto.rs index c1a03800..5710c8d9 100644 --- a/veilid-core/src/crypto/tests/test_crypto.rs +++ b/veilid-core/src/crypto/tests/test_crypto.rs @@ -1,6 +1,5 @@ use super::*; use crate::tests::common::test_veilid_config::*; -use crate::xx::*; static LOREM_IPSUM:&[u8] = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. "; diff --git a/veilid-core/src/crypto/tests/test_dht_key.rs b/veilid-core/src/crypto/tests/test_dht_key.rs index d6876ac3..26526d7b 100644 --- a/veilid-core/src/crypto/tests/test_dht_key.rs +++ b/veilid-core/src/crypto/tests/test_dht_key.rs @@ -1,7 +1,6 @@ #![allow(clippy::bool_assert_comparison)] use super::*; -use crate::xx::*; use core::convert::TryFrom; static LOREM_IPSUM:&str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. "; diff --git a/veilid-core/src/crypto/tests/test_envelope_receipt.rs b/veilid-core/src/crypto/tests/test_envelope_receipt.rs index 1a221dd3..723eeaff 100644 --- a/veilid-core/src/crypto/tests/test_envelope_receipt.rs +++ b/veilid-core/src/crypto/tests/test_envelope_receipt.rs @@ -1,6 +1,5 @@ use super::*; use crate::tests::common::test_veilid_config::*; -use crate::xx::*; pub async fn test_envelope_round_trip() { info!("--- test envelope round trip ---"); diff --git a/veilid-core/src/intf/native/utils/android/get_directories.rs b/veilid-core/src/intf/native/android/get_directories.rs similarity index 98% rename from veilid-core/src/intf/native/utils/android/get_directories.rs rename to veilid-core/src/intf/native/android/get_directories.rs index 5bd07e5c..0dfc2ace 100644 --- a/veilid-core/src/intf/native/utils/android/get_directories.rs +++ b/veilid-core/src/intf/native/android/get_directories.rs @@ -1,5 +1,4 @@ use super::*; -use crate::xx::*; pub fn get_files_dir() -> String { let aglock = ANDROID_GLOBALS.lock(); diff --git a/veilid-core/src/intf/native/utils/android/mod.rs b/veilid-core/src/intf/native/android/mod.rs similarity index 99% rename from veilid-core/src/intf/native/utils/android/mod.rs rename to veilid-core/src/intf/native/android/mod.rs index a6e3097a..424efe7f 100644 --- a/veilid-core/src/intf/native/utils/android/mod.rs +++ b/veilid-core/src/intf/native/android/mod.rs @@ -6,7 +6,6 @@ mod get_directories; pub use get_directories::*; use crate::veilid_config::VeilidConfigLogLevel; -use crate::xx::*; use crate::*; use backtrace::Backtrace; use jni::errors::Result as JniResult; diff --git a/veilid-core/src/intf/native/block_store.rs b/veilid-core/src/intf/native/block_store.rs index 6df36e6b..a5ced7e5 100644 --- a/veilid-core/src/intf/native/block_store.rs +++ b/veilid-core/src/intf/native/block_store.rs @@ -1,4 +1,3 @@ -use crate::xx::*; use crate::*; struct BlockStoreInner { diff --git a/veilid-core/src/intf/native/utils/ios/mod.rs b/veilid-core/src/intf/native/ios/mod.rs similarity index 99% rename from veilid-core/src/intf/native/utils/ios/mod.rs rename to veilid-core/src/intf/native/ios/mod.rs index 1a6b4f02..cf6a6465 100644 --- a/veilid-core/src/intf/native/utils/ios/mod.rs +++ b/veilid-core/src/intf/native/ios/mod.rs @@ -1,4 +1,3 @@ -use crate::xx::*; use backtrace::Backtrace; use log::*; use simplelog::*; diff --git a/veilid-core/src/intf/native/mod.rs b/veilid-core/src/intf/native/mod.rs index 663d9082..369a0183 100644 --- a/veilid-core/src/intf/native/mod.rs +++ b/veilid-core/src/intf/native/mod.rs @@ -2,9 +2,14 @@ mod block_store; mod protected_store; mod system; mod table_store; -pub mod utils; pub use block_store::*; pub use protected_store::*; pub use system::*; pub use table_store::*; + +#[cfg(target_os = "android")] +pub mod android; +#[cfg(all(target_os = "ios", feature = "ios_tests"))] +pub mod ios_test_setup; +pub mod network_interfaces; diff --git a/veilid-core/src/intf/native/utils/network_interfaces/apple.rs b/veilid-core/src/intf/native/network_interfaces/apple.rs similarity index 99% rename from veilid-core/src/intf/native/utils/network_interfaces/apple.rs rename to veilid-core/src/intf/native/network_interfaces/apple.rs index 97b6d8fd..c643b2ad 100644 --- a/veilid-core/src/intf/native/utils/network_interfaces/apple.rs +++ b/veilid-core/src/intf/native/network_interfaces/apple.rs @@ -1,5 +1,5 @@ use super::*; -use crate::*; + use libc::{ close, freeifaddrs, getifaddrs, if_nametoindex, ifaddrs, ioctl, pid_t, sockaddr, sockaddr_in6, socket, sysctl, time_t, AF_INET6, CTL_NET, IFF_BROADCAST, IFF_LOOPBACK, IFF_RUNNING, IFNAMSIZ, diff --git a/veilid-core/src/intf/native/utils/network_interfaces/mod.rs b/veilid-core/src/intf/native/network_interfaces/mod.rs similarity index 99% rename from veilid-core/src/intf/native/utils/network_interfaces/mod.rs rename to veilid-core/src/intf/native/network_interfaces/mod.rs index 1b14053f..3a05cb98 100644 --- a/veilid-core/src/intf/native/utils/network_interfaces/mod.rs +++ b/veilid-core/src/intf/native/network_interfaces/mod.rs @@ -1,7 +1,7 @@ -use crate::xx::*; -use core::fmt; mod tools; +use crate::*; + cfg_if::cfg_if! { if #[cfg(any(target_os = "linux", target_os = "android"))] { mod netlink; diff --git a/veilid-core/src/intf/native/utils/network_interfaces/netlink.rs b/veilid-core/src/intf/native/network_interfaces/netlink.rs similarity index 99% rename from veilid-core/src/intf/native/utils/network_interfaces/netlink.rs rename to veilid-core/src/intf/native/network_interfaces/netlink.rs index f41988a6..94f53a1f 100644 --- a/veilid-core/src/intf/native/utils/network_interfaces/netlink.rs +++ b/veilid-core/src/intf/native/network_interfaces/netlink.rs @@ -1,5 +1,4 @@ use super::*; -use crate::*; use alloc::collections::btree_map::Entry; use futures_util::stream::TryStreamExt; diff --git a/veilid-core/src/intf/native/utils/network_interfaces/sockaddr_tools.rs b/veilid-core/src/intf/native/network_interfaces/sockaddr_tools.rs similarity index 100% rename from veilid-core/src/intf/native/utils/network_interfaces/sockaddr_tools.rs rename to veilid-core/src/intf/native/network_interfaces/sockaddr_tools.rs diff --git a/veilid-core/src/intf/native/utils/network_interfaces/tools.rs b/veilid-core/src/intf/native/network_interfaces/tools.rs similarity index 100% rename from veilid-core/src/intf/native/utils/network_interfaces/tools.rs rename to veilid-core/src/intf/native/network_interfaces/tools.rs diff --git a/veilid-core/src/intf/native/utils/network_interfaces/windows.rs b/veilid-core/src/intf/native/network_interfaces/windows.rs similarity index 99% rename from veilid-core/src/intf/native/utils/network_interfaces/windows.rs rename to veilid-core/src/intf/native/network_interfaces/windows.rs index d4ca6ec1..53b74ba2 100644 --- a/veilid-core/src/intf/native/utils/network_interfaces/windows.rs +++ b/veilid-core/src/intf/native/network_interfaces/windows.rs @@ -63,7 +63,8 @@ impl PlatformSupportWindows { // } // Iterate all the interfaces - let windows_interfaces = WindowsInterfaces::new().wrap_err("failed to get windows interfaces")?; + let windows_interfaces = + WindowsInterfaces::new().wrap_err("failed to get windows interfaces")?; for windows_interface in windows_interfaces.iter() { // Get name let intf_name = windows_interface.name(); diff --git a/veilid-core/src/intf/native/protected_store.rs b/veilid-core/src/intf/native/protected_store.rs index a3c35f68..e3a3be94 100644 --- a/veilid-core/src/intf/native/protected_store.rs +++ b/veilid-core/src/intf/native/protected_store.rs @@ -1,4 +1,3 @@ -use crate::xx::*; use crate::*; use data_encoding::BASE64URL_NOPAD; use keyring_manager::*; diff --git a/veilid-core/src/intf/native/system.rs b/veilid-core/src/intf/native/system.rs index 1c2fa281..5491bff4 100644 --- a/veilid-core/src/intf/native/system.rs +++ b/veilid-core/src/intf/native/system.rs @@ -1,5 +1,6 @@ #![allow(dead_code)] -use crate::xx::*; + +use crate::*; pub async fn get_outbound_relay_peer() -> Option { panic!("Native Veilid should never require an outbound relay"); diff --git a/veilid-core/src/intf/native/table_store.rs b/veilid-core/src/intf/native/table_store.rs index 5d5ceb14..fca78232 100644 --- a/veilid-core/src/intf/native/table_store.rs +++ b/veilid-core/src/intf/native/table_store.rs @@ -1,5 +1,4 @@ use crate::intf::table_db::*; -use crate::xx::*; use crate::*; use keyvaluedb_sqlite::*; use std::path::PathBuf; diff --git a/veilid-core/src/intf/native/utils/mod.rs b/veilid-core/src/intf/native/utils/mod.rs deleted file mode 100644 index d49d97ff..00000000 --- a/veilid-core/src/intf/native/utils/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[cfg(target_os = "android")] -pub mod android; -#[cfg(all(target_os = "ios", feature = "ios_tests"))] -pub mod ios_test_setup; -pub mod network_interfaces; diff --git a/veilid-core/src/intf/table_db.rs b/veilid-core/src/intf/table_db.rs index 6e080247..23f789c7 100644 --- a/veilid-core/src/intf/table_db.rs +++ b/veilid-core/src/intf/table_db.rs @@ -1,4 +1,3 @@ -use crate::xx::*; use crate::*; use rkyv::{Archive as RkyvArchive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize}; diff --git a/veilid-core/src/intf/wasm/block_store.rs b/veilid-core/src/intf/wasm/block_store.rs index 3b207947..2bd7d280 100644 --- a/veilid-core/src/intf/wasm/block_store.rs +++ b/veilid-core/src/intf/wasm/block_store.rs @@ -1,5 +1,3 @@ - -use crate::xx::*; use crate::*; struct BlockStoreInner { diff --git a/veilid-core/src/intf/wasm/protected_store.rs b/veilid-core/src/intf/wasm/protected_store.rs index e736739d..20f6ae89 100644 --- a/veilid-core/src/intf/wasm/protected_store.rs +++ b/veilid-core/src/intf/wasm/protected_store.rs @@ -1,8 +1,8 @@ use super::*; -use crate::xx::*; use crate::*; use data_encoding::BASE64URL_NOPAD; use rkyv::{Archive as RkyvArchive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize}; + use web_sys::*; #[derive(Clone)] diff --git a/veilid-core/src/intf/wasm/system.rs b/veilid-core/src/intf/wasm/system.rs index 36b8bd14..6de02714 100644 --- a/veilid-core/src/intf/wasm/system.rs +++ b/veilid-core/src/intf/wasm/system.rs @@ -1,5 +1,3 @@ -use crate::xx::*; - use async_executors::{Bindgen, LocalSpawnHandleExt, SpawnHandleExt, Timer}; use futures_util::future::{select, Either}; use js_sys::*; diff --git a/veilid-core/src/intf/wasm/table_store.rs b/veilid-core/src/intf/wasm/table_store.rs index b49481ff..2123e8ea 100644 --- a/veilid-core/src/intf/wasm/table_store.rs +++ b/veilid-core/src/intf/wasm/table_store.rs @@ -1,7 +1,6 @@ use super::*; use crate::intf::table_db::*; -use crate::xx::*; use crate::*; use keyvaluedb_web::*; diff --git a/veilid-core/src/intf/wasm/utils/mod.rs b/veilid-core/src/intf/wasm/utils/mod.rs deleted file mode 100644 index 8b137891..00000000 --- a/veilid-core/src/intf/wasm/utils/mod.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/veilid-core/src/lib.rs b/veilid-core/src/lib.rs index 89a6456c..e66eb522 100644 --- a/veilid-core/src/lib.rs +++ b/veilid-core/src/lib.rs @@ -20,7 +20,6 @@ extern crate alloc; mod api_tracing_layer; mod attachment_manager; -mod callback_state_machine; mod core_context; mod crypto; mod intf; @@ -84,3 +83,5 @@ pub static DEFAULT_LOG_IGNORE_LIST: [&str; 21] = [ "trust_dns_proto", "attohttpc", ]; + +use veilid_tools::*; diff --git a/veilid-core/src/network_manager/mod.rs b/veilid-core/src/network_manager/mod.rs index f5fec43e..a91e55ed 100644 --- a/veilid-core/src/network_manager/mod.rs +++ b/veilid-core/src/network_manager/mod.rs @@ -1,4 +1,3 @@ -use crate::xx::*; use crate::*; #[cfg(not(target_arch = "wasm32"))] diff --git a/veilid-core/src/network_manager/native/mod.rs b/veilid-core/src/network_manager/native/mod.rs index 8978918f..b588273b 100644 --- a/veilid-core/src/network_manager/native/mod.rs +++ b/veilid-core/src/network_manager/native/mod.rs @@ -8,12 +8,12 @@ mod start_protocols; use super::*; use crate::routing_table::*; use connection_manager::*; +use network_interfaces::*; use network_tcp::*; use protocol::tcp::RawTcpProtocolHandler; use protocol::udp::RawUdpProtocolHandler; use protocol::ws::WebsocketProtocolHandler; pub use protocol::*; -use utils::network_interfaces::*; use async_tls::TlsAcceptor; use futures_util::StreamExt; diff --git a/veilid-core/src/network_manager/native/protocol/mod.rs b/veilid-core/src/network_manager/native/protocol/mod.rs index 4c2dab23..0a41a77b 100644 --- a/veilid-core/src/network_manager/native/protocol/mod.rs +++ b/veilid-core/src/network_manager/native/protocol/mod.rs @@ -5,7 +5,6 @@ pub mod wrtc; pub mod ws; use super::*; -use crate::xx::*; use std::io; #[derive(Debug)] diff --git a/veilid-core/src/network_manager/native/protocol/sockets.rs b/veilid-core/src/network_manager/native/protocol/sockets.rs index 0cf7454d..608e19b5 100644 --- a/veilid-core/src/network_manager/native/protocol/sockets.rs +++ b/veilid-core/src/network_manager/native/protocol/sockets.rs @@ -1,4 +1,3 @@ -use crate::xx::*; use crate::*; use async_io::Async; use std::io; diff --git a/veilid-core/src/receipt_manager.rs b/veilid-core/src/receipt_manager.rs index 54f37765..be548ff1 100644 --- a/veilid-core/src/receipt_manager.rs +++ b/veilid-core/src/receipt_manager.rs @@ -5,7 +5,6 @@ use futures_util::stream::{FuturesUnordered, StreamExt}; use network_manager::*; use routing_table::*; use stop_token::future::FutureExt; -use xx::*; #[derive(Clone, Debug)] pub enum ReceiptEvent { diff --git a/veilid-core/src/routing_table/mod.rs b/veilid-core/src/routing_table/mod.rs index bb8a4058..9b885d2f 100644 --- a/veilid-core/src/routing_table/mod.rs +++ b/veilid-core/src/routing_table/mod.rs @@ -11,7 +11,6 @@ mod routing_table_inner; mod stats_accounting; mod tasks; -use crate::xx::*; use crate::*; use crate::crypto::*; diff --git a/veilid-core/src/routing_table/stats_accounting.rs b/veilid-core/src/routing_table/stats_accounting.rs index 7ef69bd6..7167636e 100644 --- a/veilid-core/src/routing_table/stats_accounting.rs +++ b/veilid-core/src/routing_table/stats_accounting.rs @@ -1,4 +1,3 @@ -use crate::xx::*; use crate::*; use alloc::collections::VecDeque; diff --git a/veilid-core/src/rpc_processor/mod.rs b/veilid-core/src/rpc_processor/mod.rs index 646c665f..5a159899 100644 --- a/veilid-core/src/rpc_processor/mod.rs +++ b/veilid-core/src/rpc_processor/mod.rs @@ -30,7 +30,6 @@ pub use rpc_status::*; use super::*; use crate::crypto::*; -use crate::xx::*; use futures_util::StreamExt; use network_manager::*; use receipt_manager::*; diff --git a/veilid-core/src/tests/common/mod.rs b/veilid-core/src/tests/common/mod.rs index a2e63d10..f0fbc066 100644 --- a/veilid-core/src/tests/common/mod.rs +++ b/veilid-core/src/tests/common/mod.rs @@ -1,4 +1,3 @@ -pub mod test_async_tag_lock; pub mod test_host_interface; pub mod test_protected_store; pub mod test_table_store; diff --git a/veilid-core/src/tests/common/test_async_tag_lock.rs b/veilid-core/src/tests/common/test_async_tag_lock.rs deleted file mode 100644 index 350cd9a4..00000000 --- a/veilid-core/src/tests/common/test_async_tag_lock.rs +++ /dev/null @@ -1,158 +0,0 @@ -use crate::xx::*; - -pub async fn test_simple_no_contention() { - info!("test_simple_no_contention"); - - let table = AsyncTagLockTable::new(); - - let a1 = SocketAddr::new("1.2.3.4".parse().unwrap(), 1234); - let a2 = SocketAddr::new("6.9.6.9".parse().unwrap(), 6969); - - { - let g1 = table.lock_tag(a1).await; - let g2 = table.lock_tag(a2).await; - drop(g2); - drop(g1); - } - - { - let g1 = table.lock_tag(a1).await; - let g2 = table.lock_tag(a2).await; - drop(g1); - drop(g2); - } - - assert_eq!(table.len(), 0); -} - -pub async fn test_simple_single_contention() { - info!("test_simple_single_contention"); - - let table = AsyncTagLockTable::new(); - - let a1 = SocketAddr::new("1.2.3.4".parse().unwrap(), 1234); - - let g1 = table.lock_tag(a1).await; - - info!("locked"); - let t1 = spawn(async move { - // move the guard into the task - let _g1_take = g1; - // hold the guard for a bit - info!("waiting"); - sleep(1000).await; - // release the guard - info!("released"); - }); - - // wait to lock again, will contend until spawned task exits - let _g1_b = table.lock_tag(a1).await; - info!("locked"); - - // Ensure task is joined - t1.await; - - assert_eq!(table.len(), 1); -} - -pub async fn test_simple_double_contention() { - info!("test_simple_double_contention"); - - let table = AsyncTagLockTable::new(); - - let a1 = SocketAddr::new("1.2.3.4".parse().unwrap(), 1234); - let a2 = SocketAddr::new("6.9.6.9".parse().unwrap(), 6969); - - let g1 = table.lock_tag(a1).await; - let g2 = table.lock_tag(a2).await; - - info!("locked"); - let t1 = spawn(async move { - // move the guard into the tas - let _g1_take = g1; - // hold the guard for a bit - info!("waiting"); - sleep(1000).await; - // release the guard - info!("released"); - }); - let t2 = spawn(async move { - // move the guard into the task - let _g2_take = g2; - // hold the guard for a bit - info!("waiting"); - sleep(500).await; - // release the guard - info!("released"); - }); - - // wait to lock again, will contend until spawned task exits - let _g1_b = table.lock_tag(a1).await; - // wait to lock again, should complete immediately - let _g2_b = table.lock_tag(a2).await; - - info!("locked"); - - // Ensure tasks are joined - t1.await; - t2.await; - - assert_eq!(table.len(), 2); -} - -pub async fn test_parallel_single_contention() { - info!("test_parallel_single_contention"); - - let table = AsyncTagLockTable::new(); - - let a1 = SocketAddr::new("1.2.3.4".parse().unwrap(), 1234); - - let table1 = table.clone(); - let t1 = spawn(async move { - // lock the tag - let _g = table1.lock_tag(a1).await; - info!("locked t1"); - // hold the guard for a bit - info!("waiting t1"); - sleep(500).await; - // release the guard - info!("released t1"); - }); - - let table2 = table.clone(); - let t2 = spawn(async move { - // lock the tag - let _g = table2.lock_tag(a1).await; - info!("locked t2"); - // hold the guard for a bit - info!("waiting t2"); - sleep(500).await; - // release the guard - info!("released t2"); - }); - - let table3 = table.clone(); - let t3 = spawn(async move { - // lock the tag - let _g = table3.lock_tag(a1).await; - info!("locked t3"); - // hold the guard for a bit - info!("waiting t3"); - sleep(500).await; - // release the guard - info!("released t3"); - }); - - // Ensure tasks are joined - t1.await; - t2.await; - t3.await; - - assert_eq!(table.len(), 0); -} - -pub async fn test_all() { - test_simple_no_contention().await; - test_simple_single_contention().await; - test_parallel_single_contention().await; -} diff --git a/veilid-core/src/tests/common/test_host_interface.rs b/veilid-core/src/tests/common/test_host_interface.rs index 322f5d5e..83618b7f 100644 --- a/veilid-core/src/tests/common/test_host_interface.rs +++ b/veilid-core/src/tests/common/test_host_interface.rs @@ -1,469 +1,13 @@ -use crate::xx::*; use crate::*; -cfg_if! { - if #[cfg(target_arch = "wasm32")] { - use js_sys::*; - } else { - use std::time::{Duration, SystemTime}; - } -} - -pub async fn test_log() { - info!("testing log"); -} - -pub async fn test_get_timestamp() { - info!("testing get_timestamp"); - let t1 = get_timestamp(); - let t2 = get_timestamp(); - assert!(t2 >= t1); -} - -pub async fn test_eventual() { - info!("testing Eventual"); - { - let e1 = Eventual::new(); - let i1 = e1.instance_clone(1u32); - let i2 = e1.instance_clone(2u32); - let i3 = e1.instance_clone(3u32); - drop(i3); - let i4 = e1.instance_clone(4u32); - drop(i2); - - let jh = spawn(async move { - sleep(1000).await; - e1.resolve(); - }); - - assert_eq!(i1.await, 1u32); - assert_eq!(i4.await, 4u32); - - jh.await; - } - { - let e1 = Eventual::new(); - let i1 = e1.instance_clone(1u32); - let i2 = e1.instance_clone(2u32); - let i3 = e1.instance_clone(3u32); - let i4 = e1.instance_clone(4u32); - let e1_c1 = e1.clone(); - let jh = spawn(async move { - let i5 = e1.instance_clone(5u32); - let i6 = e1.instance_clone(6u32); - assert_eq!(i1.await, 1u32); - assert_eq!(i5.await, 5u32); - assert_eq!(i6.await, 6u32); - }); - sleep(1000).await; - let resolved = e1_c1.resolve(); - drop(i2); - drop(i3); - assert_eq!(i4.await, 4u32); - resolved.await; - jh.await; - } - { - let e1 = Eventual::new(); - let i1 = e1.instance_clone(1u32); - let i2 = e1.instance_clone(2u32); - let e1_c1 = e1.clone(); - let jh = spawn(async move { - assert_eq!(i1.await, 1u32); - assert_eq!(i2.await, 2u32); - }); - sleep(1000).await; - e1_c1.resolve().await; - - jh.await; - - e1_c1.reset(); - // - let j1 = e1.instance_clone(1u32); - let j2 = e1.instance_clone(2u32); - let jh = spawn(async move { - assert_eq!(j1.await, 1u32); - assert_eq!(j2.await, 2u32); - }); - sleep(1000).await; - e1_c1.resolve().await; - - jh.await; - - e1_c1.reset(); - } -} - -pub async fn test_eventual_value() { - info!("testing Eventual Value"); - { - let e1 = EventualValue::::new(); - let i1 = e1.instance(); - let i2 = e1.instance(); - let i3 = e1.instance(); - drop(i3); - let i4 = e1.instance(); - drop(i2); - - let e1_c1 = e1.clone(); - let jh = spawn(async move { - sleep(1000).await; - e1_c1.resolve(3u32); - }); - - i1.await; - i4.await; - jh.await; - assert_eq!(e1.take_value(), Some(3u32)); - } - { - let e1 = EventualValue::new(); - let i1 = e1.instance(); - let i2 = e1.instance(); - let i3 = e1.instance(); - let i4 = e1.instance(); - let e1_c1 = e1.clone(); - let jh = spawn(async move { - let i5 = e1.instance(); - let i6 = e1.instance(); - i1.await; - i5.await; - i6.await; - }); - sleep(1000).await; - let resolved = e1_c1.resolve(4u16); - drop(i2); - drop(i3); - i4.await; - resolved.await; - jh.await; - assert_eq!(e1_c1.take_value(), Some(4u16)); - } - { - let e1 = EventualValue::new(); - assert_eq!(e1.take_value(), None); - let i1 = e1.instance(); - let i2 = e1.instance(); - let e1_c1 = e1.clone(); - let jh = spawn(async move { - i1.await; - i2.await; - }); - sleep(1000).await; - e1_c1.resolve(5u32).await; - jh.await; - assert_eq!(e1_c1.take_value(), Some(5u32)); - e1_c1.reset(); - assert_eq!(e1_c1.take_value(), None); - // - let j1 = e1.instance(); - let j2 = e1.instance(); - let jh = spawn(async move { - j1.await; - j2.await; - }); - sleep(1000).await; - e1_c1.resolve(6u32).await; - jh.await; - assert_eq!(e1_c1.take_value(), Some(6u32)); - e1_c1.reset(); - assert_eq!(e1_c1.take_value(), None); - } -} - -pub async fn test_eventual_value_clone() { - info!("testing Eventual Value Clone"); - { - let e1 = EventualValueClone::::new(); - let i1 = e1.instance(); - let i2 = e1.instance(); - let i3 = e1.instance(); - drop(i3); - let i4 = e1.instance(); - drop(i2); - - let jh = spawn(async move { - sleep(1000).await; - e1.resolve(3u32); - }); - - assert_eq!(i1.await, 3); - assert_eq!(i4.await, 3); - - jh.await; - } - - { - let e1 = EventualValueClone::new(); - let i1 = e1.instance(); - let i2 = e1.instance(); - let i3 = e1.instance(); - let i4 = e1.instance(); - let e1_c1 = e1.clone(); - let jh = spawn(async move { - let i5 = e1.instance(); - let i6 = e1.instance(); - assert_eq!(i1.await, 4); - assert_eq!(i5.await, 4); - assert_eq!(i6.await, 4); - }); - sleep(1000).await; - let resolved = e1_c1.resolve(4u16); - drop(i2); - drop(i3); - assert_eq!(i4.await, 4); - resolved.await; - jh.await; - } - - { - let e1 = EventualValueClone::new(); - let i1 = e1.instance(); - let i2 = e1.instance(); - let e1_c1 = e1.clone(); - let jh = spawn(async move { - assert_eq!(i1.await, 5); - assert_eq!(i2.await, 5); - }); - sleep(1000).await; - e1_c1.resolve(5u32).await; - jh.await; - e1_c1.reset(); - // - let j1 = e1.instance(); - let j2 = e1.instance(); - let jh = spawn(async move { - assert_eq!(j1.await, 6); - assert_eq!(j2.await, 6); - }); - sleep(1000).await; - e1_c1.resolve(6u32).await; - jh.await; - e1_c1.reset(); - } -} -pub async fn test_interval() { - info!("testing interval"); - - let tick: Arc> = Arc::new(Mutex::new(0u32)); - let stopper = interval(1000, move || { - let tick = tick.clone(); - async move { - let mut tick = tick.lock(); - trace!("tick {}", tick); - *tick += 1; - } - }); - - sleep(5500).await; - - stopper.await; -} - -pub async fn test_timeout() { - info!("testing timeout"); - - let tick: Arc> = Arc::new(Mutex::new(0u32)); - let tick_1 = tick.clone(); - assert!( - timeout(2500, async move { - let mut tick = tick_1.lock(); - trace!("tick {}", tick); - sleep(1000).await; - *tick += 1; - trace!("tick {}", tick); - sleep(1000).await; - *tick += 1; - trace!("tick {}", tick); - sleep(1000).await; - *tick += 1; - trace!("tick {}", tick); - sleep(1000).await; - *tick += 1; - }) - .await - .is_err(), - "should have timed out" - ); - - let ticks = *tick.lock(); - assert!(ticks <= 2); -} - -pub async fn test_sleep() { - info!("testing sleep"); - cfg_if! { - if #[cfg(target_arch = "wasm32")] { - - let t1 = Date::now(); - intf::sleep(1000).await; - let t2 = Date::now(); - assert!((t2-t1) >= 1000.0); - - } else { - - let sys_time = SystemTime::now(); - let one_sec = Duration::from_secs(1); - - sleep(1000).await; - assert!(sys_time.elapsed().unwrap() >= one_sec); - } - } -} - -macro_rules! assert_split_url { - ($url:expr, $scheme:expr, $host:expr) => { - assert_eq!( - SplitUrl::from_str($url), - Ok(SplitUrl::new($scheme, None, $host, None, None)) - ); - }; - ($url:expr, $scheme:expr, $host:expr, $port:expr) => { - assert_eq!( - SplitUrl::from_str($url), - Ok(SplitUrl::new($scheme, None, $host, $port, None)) - ); - }; - ($url:expr, $scheme:expr, $host:expr, $port:expr, $path:expr) => { - assert_eq!( - SplitUrl::from_str($url), - Ok(SplitUrl::new( - $scheme, - None, - $host, - $port, - Some(SplitUrlPath::new( - $path, - Option::::None, - Option::::None - )) - )) - ); - }; - ($url:expr, $scheme:expr, $host:expr, $port:expr, $path:expr, $frag:expr, $query:expr) => { - assert_eq!( - SplitUrl::from_str($url), - Ok(SplitUrl::new( - $scheme, - None, - $host, - $port, - Some(SplitUrlPath::new($path, $frag, $query)) - )) - ); - }; -} - -macro_rules! assert_split_url_parse { - ($url:expr) => { - let url = $url; - let su1 = SplitUrl::from_str(url).expect("should parse"); - assert_eq!(su1.to_string(), url); - }; -} - -fn host>(s: S) -> SplitUrlHost { - SplitUrlHost::Hostname(s.as_ref().to_owned()) -} - -fn ip>(s: S) -> SplitUrlHost { - SplitUrlHost::IpAddr(IpAddr::from_str(s.as_ref()).unwrap()) -} - -pub async fn test_split_url() { - info!("testing split_url"); - - assert_split_url!("http://foo", "http", host("foo")); - assert_split_url!("http://foo:1234", "http", host("foo"), Some(1234)); - assert_split_url!("http://foo:1234/", "http", host("foo"), Some(1234), ""); - assert_split_url!( - "http://foo:1234/asdf/qwer", - "http", - host("foo"), - Some(1234), - "asdf/qwer" - ); - assert_split_url!("http://foo/", "http", host("foo"), None, ""); - assert_split_url!("http://11.2.3.144/", "http", ip("11.2.3.144"), None, ""); - assert_split_url!("http://[1111::2222]/", "http", ip("1111::2222"), None, ""); - assert_split_url!( - "http://[1111::2222]:123/", - "http", - ip("1111::2222"), - Some(123), - "" - ); - - assert_split_url!( - "http://foo/asdf/qwer", - "http", - host("foo"), - None, - "asdf/qwer" - ); - assert_split_url!( - "http://foo/asdf/qwer#3", - "http", - host("foo"), - None, - "asdf/qwer", - Some("3"), - Option::::None - ); - assert_split_url!( - "http://foo/asdf/qwer?xxx", - "http", - host("foo"), - None, - "asdf/qwer", - Option::::None, - Some("xxx") - ); - assert_split_url!( - "http://foo/asdf/qwer#yyy?xxx", - "http", - host("foo"), - None, - "asdf/qwer", - Some("yyy"), - Some("xxx") - ); - assert_err!(SplitUrl::from_str("://asdf")); - assert_err!(SplitUrl::from_str("")); - assert_err!(SplitUrl::from_str("::")); - assert_err!(SplitUrl::from_str("://:")); - assert_err!(SplitUrl::from_str("a://:")); - assert_err!(SplitUrl::from_str("a://:1243")); - assert_err!(SplitUrl::from_str("a://:65536")); - assert_err!(SplitUrl::from_str("a://:-16")); - assert_err!(SplitUrl::from_str("a:///")); - assert_err!(SplitUrl::from_str("a:///qwer:")); - assert_err!(SplitUrl::from_str("a:///qwer://")); - assert_err!(SplitUrl::from_str("a://qwer://")); - assert_err!(SplitUrl::from_str("a://[1111::2222]:/")); - assert_err!(SplitUrl::from_str("a://[1111::2222]:")); - - assert_split_url_parse!("sch://foo:bar@baz.com:1234/fnord#qux?zuz"); - assert_split_url_parse!("sch://foo:bar@baz.com:1234/fnord#qux"); - assert_split_url_parse!("sch://foo:bar@baz.com:1234/fnord?zuz"); - assert_split_url_parse!("sch://foo:bar@baz.com:1234/fnord/"); - assert_split_url_parse!("sch://foo:bar@baz.com:1234//"); - assert_split_url_parse!("sch://foo:bar@baz.com:1234"); - assert_split_url_parse!("sch://foo:bar@[1111::2222]:1234"); - assert_split_url_parse!("sch://foo:bar@[::]:1234"); - assert_split_url_parse!("sch://foo:bar@1.2.3.4:1234"); - assert_split_url_parse!("sch://@baz.com:1234"); - assert_split_url_parse!("sch://baz.com/asdf/asdf"); - assert_split_url_parse!("sch://baz.com/"); - assert_split_url_parse!("s://s"); -} - cfg_if! { if #[cfg(not(target_arch = "wasm32"))] { + use intf::network_interfaces::NetworkInterfaces; + pub async fn test_network_interfaces() { info!("testing network interfaces"); let t1 = get_timestamp(); - let interfaces = intf::utils::network_interfaces::NetworkInterfaces::new(); + let interfaces = NetworkInterfaces::new(); let count = 100; for x in 0..count { info!("loop {}", x); @@ -479,99 +23,7 @@ cfg_if! { } } -pub async fn test_get_random_u64() { - info!("testing random number generator for u64"); - let t1 = get_timestamp(); - let count = 10000; - for _ in 0..count { - let _ = get_random_u64(); - } - let t2 = get_timestamp(); - let tdiff = ((t2 - t1) as f64) / 1000000.0f64; - info!( - "running network interface test with {} iterations took {} seconds", - count, tdiff - ); -} - -pub async fn test_get_random_u32() { - info!("testing random number generator for u32"); - let t1 = get_timestamp(); - let count = 10000; - for _ in 0..count { - let _ = get_random_u32(); - } - let t2 = get_timestamp(); - let tdiff = ((t2 - t1) as f64) / 1000000.0f64; - info!( - "running network interface test with {} iterations took {} seconds", - count, tdiff - ); -} - -pub async fn test_must_join_single_future() { - info!("testing must join single future"); - let sf = MustJoinSingleFuture::::new(); - assert_eq!(sf.check().await, Ok(None)); - assert_eq!( - sf.single_spawn(async { - sleep(2000).await; - 69 - }) - .await, - Ok((None, true)) - ); - assert_eq!(sf.check().await, Ok(None)); - assert_eq!(sf.single_spawn(async { panic!() }).await, Ok((None, false))); - assert_eq!(sf.join().await, Ok(Some(69))); - assert_eq!( - sf.single_spawn(async { - sleep(1000).await; - 37 - }) - .await, - Ok((None, true)) - ); - sleep(2000).await; - assert_eq!( - sf.single_spawn(async { - sleep(1000).await; - 27 - }) - .await, - Ok((Some(37), true)) - ); - sleep(2000).await; - assert_eq!(sf.join().await, Ok(Some(27))); - assert_eq!(sf.check().await, Ok(None)); -} - -pub async fn test_tools() { - info!("testing retry_falloff_log"); - let mut last_us = 0u64; - for x in 0..1024 { - let cur_us = x as u64 * 1000000u64; - if retry_falloff_log(last_us, cur_us, 10_000_000u64, 6_000_000_000u64, 2.0f64) { - info!(" retry at {} secs", timestamp_to_secs(cur_us)); - last_us = cur_us; - } - } -} - pub async fn test_all() { - test_log().await; - test_get_timestamp().await; - test_tools().await; - test_split_url().await; - test_get_random_u64().await; - test_get_random_u32().await; - test_sleep().await; #[cfg(not(target_arch = "wasm32"))] - test_network_interfaces().await; XXX KEEP THIS IN NATIVE TESTS - test_must_join_single_future().await; - test_eventual().await; - test_eventual_value().await; - test_eventual_value_clone().await; - test_interval().await; - test_timeout().await; + test_network_interfaces().await; } diff --git a/veilid-core/src/tests/common/test_protected_store.rs b/veilid-core/src/tests/common/test_protected_store.rs index ef48c11a..49d56d4e 100644 --- a/veilid-core/src/tests/common/test_protected_store.rs +++ b/veilid-core/src/tests/common/test_protected_store.rs @@ -1,5 +1,4 @@ use super::test_veilid_config::*; -use crate::xx::*; use crate::*; async fn startup() -> VeilidAPI { diff --git a/veilid-core/src/tests/common/test_table_store.rs b/veilid-core/src/tests/common/test_table_store.rs index 5e3478a3..773f8b3e 100644 --- a/veilid-core/src/tests/common/test_table_store.rs +++ b/veilid-core/src/tests/common/test_table_store.rs @@ -1,5 +1,4 @@ use super::test_veilid_config::*; -use crate::xx::*; use crate::*; async fn startup() -> VeilidAPI { diff --git a/veilid-core/src/tests/common/test_veilid_config.rs b/veilid-core/src/tests/common/test_veilid_config.rs index c5c6f56f..1eab6ea4 100644 --- a/veilid-core/src/tests/common/test_veilid_config.rs +++ b/veilid-core/src/tests/common/test_veilid_config.rs @@ -1,7 +1,7 @@ #![allow(clippy::bool_assert_comparison)] -use crate::xx::*; use crate::*; + cfg_if! { if #[cfg(not(target_arch = "wasm32"))] { use std::fs::File; diff --git a/veilid-core/src/tests/common/test_veilid_core.rs b/veilid-core/src/tests/common/test_veilid_core.rs index e3ba0670..27bf3e2e 100644 --- a/veilid-core/src/tests/common/test_veilid_core.rs +++ b/veilid-core/src/tests/common/test_veilid_core.rs @@ -1,5 +1,4 @@ use super::test_veilid_config::*; -use crate::xx::*; use crate::*; pub async fn test_startup_shutdown() { diff --git a/veilid-core/src/tests/native/mod.rs b/veilid-core/src/tests/native/mod.rs index b41ab132..a7a0580d 100644 --- a/veilid-core/src/tests/native/mod.rs +++ b/veilid-core/src/tests/native/mod.rs @@ -1,13 +1,9 @@ //! Test suite for Native #![cfg(not(target_arch = "wasm32"))] - -mod test_async_peek_stream; - -use crate::xx::*; - use crate::crypto::tests::*; use crate::network_manager::tests::*; use crate::tests::common::*; +use crate::*; #[cfg(all(target_os = "android", feature = "android_tests"))] use jni::{objects::JClass, objects::JObject, JNIEnv}; @@ -59,8 +55,6 @@ pub fn run_all_tests() { exec_test_veilid_core(); info!("TEST: exec_test_veilid_config"); exec_test_veilid_config(); - info!("TEST: exec_test_async_peek_stream"); - exec_test_async_peek_stream(); info!("TEST: exec_test_connection_table"); exec_test_connection_table(); info!("TEST: exec_test_table_store"); @@ -71,8 +65,6 @@ pub fn run_all_tests() { exec_test_crypto(); info!("TEST: exec_test_envelope_receipt"); exec_test_envelope_receipt(); - info!("TEST: exec_test_async_tag_lock"); - exec_test_async_tag_lock(); info!("Finished unit tests"); } @@ -108,11 +100,6 @@ fn exec_test_veilid_config() { test_veilid_config::test_all().await; }) } -fn exec_test_async_peek_stream() { - block_on(async { - test_async_peek_stream::test_all().await; - }) -} fn exec_test_connection_table() { block_on(async { test_connection_table::test_all().await; @@ -138,11 +125,7 @@ fn exec_test_envelope_receipt() { test_envelope_receipt::test_all().await; }) } -fn exec_test_async_tag_lock() { - block_on(async { - test_async_tag_lock::test_all().await; - }) -} + /////////////////////////////////////////////////////////////////////////// cfg_if! { if #[cfg(test)] { @@ -190,13 +173,6 @@ cfg_if! { exec_test_veilid_config(); } - #[test] - #[serial] - fn run_test_async_peek_stream() { - setup(); - exec_test_async_peek_stream(); - } - #[test] #[serial] fn run_test_connection_table() { @@ -232,11 +208,5 @@ cfg_if! { exec_test_envelope_receipt(); } - #[test] - #[serial] - fn run_test_async_tag_lock() { - setup(); - exec_test_async_tag_lock(); - } } } diff --git a/veilid-core/src/tests/native/test_async_peek_stream.rs b/veilid-core/src/tests/native/test_async_peek_stream.rs deleted file mode 100644 index 2aaa5d97..00000000 --- a/veilid-core/src/tests/native/test_async_peek_stream.rs +++ /dev/null @@ -1,352 +0,0 @@ -use crate::xx::*; - -cfg_if! { - if #[cfg(feature="rt-async-std")] { - use async_std::net::{TcpListener, TcpStream}; - use async_std::prelude::FutureExt; - use async_std::task::sleep; - } else if #[cfg(feature="rt-tokio")] { - use tokio::net::{TcpListener, TcpStream}; - use tokio::time::sleep; - use tokio_util::compat::*; - } -} - -use futures_util::{AsyncReadExt, AsyncWriteExt}; -use std::io; - -static MESSAGE: &[u8; 62] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - -async fn make_tcp_loopback() -> Result<(TcpStream, TcpStream), io::Error> { - let listener = TcpListener::bind("127.0.0.1:0").await?; - let local_addr = listener.local_addr()?; - - let accept_future = async { - let (accepted_stream, peer_address) = listener.accept().await?; - trace!("connection from {}", peer_address); - accepted_stream.set_nodelay(true)?; - Result::::Ok(accepted_stream) - }; - let connect_future = async { - sleep(Duration::from_secs(1)).await; - let connected_stream = TcpStream::connect(local_addr).await?; - connected_stream.set_nodelay(true)?; - Result::::Ok(connected_stream) - }; - - cfg_if! { - if #[cfg(feature="rt-async-std")] { - accept_future.try_join(connect_future).await - } else if #[cfg(feature="rt-tokio")] { - tokio::try_join!(accept_future, connect_future) - } - } -} - -async fn make_async_peek_stream_loopback() -> (AsyncPeekStream, AsyncPeekStream) { - let (acc, conn) = make_tcp_loopback().await.unwrap(); - #[cfg(feature = "rt-tokio")] - let acc = acc.compat(); - #[cfg(feature = "rt-tokio")] - let conn = conn.compat(); - - let aps_a = AsyncPeekStream::new(acc); - let aps_c = AsyncPeekStream::new(conn); - - (aps_a, aps_c) -} - -#[cfg(feature = "rt-tokio")] -async fn make_stream_loopback() -> (Compat, Compat) { - let (a, c) = make_tcp_loopback().await.unwrap(); - (a.compat(), c.compat()) -} -#[cfg(feature = "rt-async-std")] -async fn make_stream_loopback() -> (TcpStream, TcpStream) { - make_tcp_loopback().await.unwrap() -} - -pub async fn test_nothing() { - info!("test_nothing"); - let (mut a, mut c) = make_stream_loopback().await; - let outbuf = MESSAGE.to_vec(); - - a.write_all(&outbuf).await.unwrap(); - - let mut inbuf: Vec = Vec::new(); - inbuf.resize(outbuf.len(), 0u8); - c.read_exact(&mut inbuf).await.unwrap(); - - assert_eq!(inbuf, outbuf); -} - -pub async fn test_no_peek() { - info!("test_no_peek"); - let (mut a, mut c) = make_async_peek_stream_loopback().await; - - let outbuf = MESSAGE.to_vec(); - - a.write_all(&outbuf).await.unwrap(); - - let mut inbuf: Vec = Vec::new(); - inbuf.resize(outbuf.len(), 0u8); - c.read_exact(&mut inbuf).await.unwrap(); - - assert_eq!(inbuf, outbuf); -} - -pub async fn test_peek_all_read() { - info!("test_peek_all_read"); - - let (mut a, mut c) = make_async_peek_stream_loopback().await; - // write everything - let outbuf = MESSAGE.to_vec(); - a.write_all(&outbuf).await.unwrap(); - - // peek everything - let mut peekbuf1: Vec = Vec::new(); - peekbuf1.resize(outbuf.len(), 0u8); - let peeksize1 = c.peek(&mut peekbuf1).await.unwrap(); - - assert_eq!(peeksize1, peekbuf1.len()); - // read everything - let mut inbuf: Vec = Vec::new(); - inbuf.resize(outbuf.len(), 0u8); - c.read_exact(&mut inbuf).await.unwrap(); - - assert_eq!(inbuf, outbuf); - assert_eq!(peekbuf1, outbuf); -} - -pub async fn test_peek_some_read() { - info!("test_peek_some_read"); - - let (mut a, mut c) = make_async_peek_stream_loopback().await; - - // write everything - let outbuf = MESSAGE.to_vec(); - a.write_all(&outbuf).await.unwrap(); - - // peek partially - let mut peekbuf1: Vec = Vec::new(); - peekbuf1.resize(outbuf.len() / 2, 0u8); - let peeksize1 = c.peek(&mut peekbuf1).await.unwrap(); - assert_eq!(peeksize1, peekbuf1.len()); - // read everything - let mut inbuf: Vec = Vec::new(); - inbuf.resize(outbuf.len(), 0u8); - c.read_exact(&mut inbuf).await.unwrap(); - - assert_eq!(inbuf, outbuf); - assert_eq!(peekbuf1, outbuf[0..peeksize1].to_vec()); -} - -pub async fn test_peek_some_peek_some_read() { - info!("test_peek_some_peek_some_read"); - - let (mut a, mut c) = make_async_peek_stream_loopback().await; - - // write everything - let outbuf = MESSAGE.to_vec(); - a.write_all(&outbuf).await.unwrap(); - - // peek partially - let mut peekbuf1: Vec = Vec::new(); - peekbuf1.resize(outbuf.len() / 4, 0u8); - let peeksize1 = c.peek(&mut peekbuf1).await.unwrap(); - assert_eq!(peeksize1, peekbuf1.len()); - - // peek partially - let mut peekbuf2: Vec = Vec::new(); - peekbuf2.resize(peeksize1 + 1, 0u8); - let peeksize2 = c.peek(&mut peekbuf2).await.unwrap(); - assert_eq!(peeksize2, peekbuf2.len()); - - // read everything - let mut inbuf: Vec = Vec::new(); - inbuf.resize(outbuf.len(), 0u8); - c.read_exact(&mut inbuf).await.unwrap(); - - assert_eq!(inbuf, outbuf); - assert_eq!(peekbuf1, outbuf[0..peeksize1].to_vec()); - assert_eq!(peekbuf2, outbuf[0..peeksize2].to_vec()); -} - -pub async fn test_peek_some_read_peek_some_read() { - info!("test_peek_some_read_peek_some_read"); - - let (mut a, mut c) = make_async_peek_stream_loopback().await; - - // write everything - let outbuf = MESSAGE.to_vec(); - a.write_all(&outbuf).await.unwrap(); - - // peek partially - let mut peekbuf1: Vec = Vec::new(); - peekbuf1.resize(outbuf.len() / 4, 0u8); - let peeksize1 = c.peek(&mut peekbuf1).await.unwrap(); - assert_eq!(peeksize1, peekbuf1.len()); - - // read partially - let mut inbuf1: Vec = Vec::new(); - inbuf1.resize(peeksize1 - 1, 0u8); - c.read_exact(&mut inbuf1).await.unwrap(); - - // peek partially - let mut peekbuf2: Vec = Vec::new(); - peekbuf2.resize(2, 0u8); - let peeksize2 = c.peek(&mut peekbuf2).await.unwrap(); - assert_eq!(peeksize2, peekbuf2.len()); - - // read partially - let mut inbuf2: Vec = Vec::new(); - inbuf2.resize(2, 0u8); - c.read_exact(&mut inbuf2).await.unwrap(); - - assert_eq!(peekbuf1, outbuf[0..peeksize1].to_vec()); - assert_eq!(inbuf1, outbuf[0..peeksize1 - 1].to_vec()); - assert_eq!(peekbuf2, outbuf[peeksize1 - 1..peeksize1 + 1].to_vec()); - assert_eq!(inbuf2, peekbuf2); -} - -pub async fn test_peek_some_read_peek_all_read() { - info!("test_peek_some_read_peek_all_read"); - - let (mut a, mut c) = make_async_peek_stream_loopback().await; - - // write everything - let outbuf = MESSAGE.to_vec(); - a.write_all(&outbuf).await.unwrap(); - - // peek partially - let mut peekbuf1: Vec = Vec::new(); - peekbuf1.resize(outbuf.len() / 4, 0u8); - let peeksize1 = c.peek(&mut peekbuf1).await.unwrap(); - assert_eq!(peeksize1, peekbuf1.len()); - - // read partially - let mut inbuf1: Vec = Vec::new(); - inbuf1.resize(peeksize1 + 1, 0u8); - c.read_exact(&mut inbuf1).await.unwrap(); - - // peek past end - let mut peekbuf2: Vec = Vec::new(); - peekbuf2.resize(outbuf.len(), 0u8); - let peeksize2 = c.peek(&mut peekbuf2).await.unwrap(); - assert_eq!(peeksize2, outbuf.len() - (peeksize1 + 1)); - - // read remaining - let mut inbuf2: Vec = Vec::new(); - inbuf2.resize(peeksize2, 0u8); - c.read_exact(&mut inbuf2).await.unwrap(); - - assert_eq!(peekbuf1, outbuf[0..peeksize1].to_vec()); - assert_eq!(inbuf1, outbuf[0..peeksize1 + 1].to_vec()); - assert_eq!( - peekbuf2[0..peeksize2].to_vec(), - outbuf[peeksize1 + 1..outbuf.len()].to_vec() - ); - assert_eq!(inbuf2, peekbuf2[0..peeksize2].to_vec()); -} - -pub async fn test_peek_some_read_peek_some_read_all_read() { - info!("test_peek_some_read_peek_some_read_peek_all_read"); - - let (mut a, mut c) = make_async_peek_stream_loopback().await; - - // write everything - let outbuf = MESSAGE.to_vec(); - a.write_all(&outbuf).await.unwrap(); - - // peek partially - let mut peekbuf1: Vec = Vec::new(); - peekbuf1.resize(outbuf.len() / 4, 0u8); - let peeksize1 = c.peek(&mut peekbuf1).await.unwrap(); - assert_eq!(peeksize1, peekbuf1.len()); - - // read partially - let mut inbuf1: Vec = Vec::new(); - inbuf1.resize(peeksize1 - 1, 0u8); - c.read_exact(&mut inbuf1).await.unwrap(); - - // peek partially - let mut peekbuf2: Vec = Vec::new(); - peekbuf2.resize(2, 0u8); - let peeksize2 = c.peek(&mut peekbuf2).await.unwrap(); - assert_eq!(peeksize2, peekbuf2.len()); - // read partially - let mut inbuf2: Vec = Vec::new(); - inbuf2.resize(1, 0u8); - c.read_exact(&mut inbuf2).await.unwrap(); - - // read remaining - let mut inbuf3: Vec = Vec::new(); - inbuf3.resize(outbuf.len() - peeksize1, 0u8); - c.read_exact(&mut inbuf3).await.unwrap(); - - assert_eq!(peekbuf1, outbuf[0..peeksize1].to_vec()); - assert_eq!(inbuf1, outbuf[0..peeksize1 - 1].to_vec()); - assert_eq!( - peekbuf2[0..peeksize2].to_vec(), - outbuf[peeksize1 - 1..peeksize1 + 1].to_vec() - ); - assert_eq!(inbuf2, peekbuf2[0..1].to_vec()); - assert_eq!(inbuf3, outbuf[peeksize1..outbuf.len()].to_vec()); -} - -pub async fn test_peek_exact_read_peek_exact_read_all_read() { - info!("test_peek_exact_read_peek_exact_read_all_read"); - - let (mut a, mut c) = make_async_peek_stream_loopback().await; - - // write everything - let outbuf = MESSAGE.to_vec(); - a.write_all(&outbuf).await.unwrap(); - - // peek partially - let mut peekbuf1: Vec = Vec::new(); - peekbuf1.resize(outbuf.len() / 4, 0u8); - let peeksize1 = c.peek_exact(&mut peekbuf1).await.unwrap(); - assert_eq!(peeksize1, peekbuf1.len()); - - // read partially - let mut inbuf1: Vec = Vec::new(); - inbuf1.resize(peeksize1 - 1, 0u8); - c.read_exact(&mut inbuf1).await.unwrap(); - - // peek partially - let mut peekbuf2: Vec = Vec::new(); - peekbuf2.resize(2, 0u8); - let peeksize2 = c.peek_exact(&mut peekbuf2).await.unwrap(); - assert_eq!(peeksize2, peekbuf2.len()); - // read partially - let mut inbuf2: Vec = Vec::new(); - inbuf2.resize(1, 0u8); - c.read_exact(&mut inbuf2).await.unwrap(); - - // read remaining - let mut inbuf3: Vec = Vec::new(); - inbuf3.resize(outbuf.len() - peeksize1, 0u8); - c.read_exact(&mut inbuf3).await.unwrap(); - - assert_eq!(peekbuf1, outbuf[0..peeksize1].to_vec()); - assert_eq!(inbuf1, outbuf[0..peeksize1 - 1].to_vec()); - assert_eq!( - peekbuf2[0..peeksize2].to_vec(), - outbuf[peeksize1 - 1..peeksize1 + 1].to_vec() - ); - assert_eq!(inbuf2, peekbuf2[0..1].to_vec()); - assert_eq!(inbuf3, outbuf[peeksize1..outbuf.len()].to_vec()); -} - -pub async fn test_all() { - test_nothing().await; - test_no_peek().await; - test_peek_all_read().await; - test_peek_some_read().await; - test_peek_some_peek_some_read().await; - test_peek_some_read_peek_some_read().await; - test_peek_some_read_peek_all_read().await; - test_peek_some_read_peek_some_read_all_read().await; - test_peek_exact_read_peek_exact_read_all_read().await; -} diff --git a/veilid-core/src/veilid_api/mod.rs b/veilid-core/src/veilid_api/mod.rs index a293cd46..9dd2cebe 100644 --- a/veilid-core/src/veilid_api/mod.rs +++ b/veilid-core/src/veilid_api/mod.rs @@ -14,12 +14,6 @@ pub use routing_context::*; pub use serialize_helpers::*; pub use types::*; -use crate::*; - -pub use crate::xx::{ - IpAddr, Ipv4Addr, Ipv6Addr, SendPinBoxFuture, SocketAddr, SocketAddrV4, SocketAddrV6, - ToSocketAddrs, -}; pub use alloc::string::ToString; pub use attachment_manager::AttachmentManager; pub use core::str::FromStr; @@ -31,6 +25,7 @@ pub use intf::TableStore; pub use network_manager::NetworkManager; pub use routing_table::{NodeRef, NodeRefBase}; +use crate::*; use core::fmt; use core_context::{api_shutdown, VeilidCoreContext}; use enumset::*; @@ -38,6 +33,5 @@ use rkyv::{Archive as RkyvArchive, Deserialize as RkyvDeserialize, Serialize as use routing_table::{RouteSpecStore, RoutingTable}; use rpc_processor::*; use serde::*; -use xx::*; ///////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/veilid-core/src/veilid_config.rs b/veilid-core/src/veilid_config.rs index c1469dfa..b610a989 100644 --- a/veilid-core/src/veilid_config.rs +++ b/veilid-core/src/veilid_config.rs @@ -1,4 +1,3 @@ -use crate::xx::*; use crate::*; use rkyv::{Archive as RkyvArchive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize}; use serde::*; diff --git a/veilid-core/src/veilid_layer_filter.rs b/veilid-core/src/veilid_layer_filter.rs index a40fbc7a..73e02a85 100644 --- a/veilid-core/src/veilid_layer_filter.rs +++ b/veilid-core/src/veilid_layer_filter.rs @@ -1,5 +1,4 @@ use super::*; -use crate::xx::*; use tracing::level_filters::LevelFilter; use tracing::subscriber::Interest; use tracing_subscriber::layer; diff --git a/veilid-tools/Cargo.toml b/veilid-tools/Cargo.toml index 0a223274..d85ae4c6 100644 --- a/veilid-tools/Cargo.toml +++ b/veilid-tools/Cargo.toml @@ -33,6 +33,7 @@ once_cell = "^1" owo-colors = "^3" stop-token = { version = "^0", default-features = false } rand = "^0.7" +rust-fsm = "^0" # Dependencies for native builds only # Linux, Windows, Mac, iOS, Android diff --git a/veilid-core/src/callback_state_machine.rs b/veilid-tools/src/callback_state_machine.rs similarity index 98% rename from veilid-core/src/callback_state_machine.rs rename to veilid-tools/src/callback_state_machine.rs index 64ab36c1..80085d37 100644 --- a/veilid-core/src/callback_state_machine.rs +++ b/veilid-tools/src/callback_state_machine.rs @@ -1,4 +1,5 @@ -use crate::xx::*; +use super::*; +pub use rust_fsm; pub use rust_fsm::*; pub type StateChangeCallback = Arc< diff --git a/veilid-tools/src/lib.rs b/veilid-tools/src/lib.rs index 0f16316b..ed8e90c8 100644 --- a/veilid-tools/src/lib.rs +++ b/veilid-tools/src/lib.rs @@ -1,6 +1,7 @@ // mod bump_port; mod async_peek_stream; mod async_tag_lock; +mod callback_state_machine; mod clone_stream; mod eventual; mod eventual_base; @@ -110,6 +111,7 @@ cfg_if! { // pub use bump_port::*; pub use async_peek_stream::*; pub use async_tag_lock::*; +pub use callback_state_machine::*; pub use clone_stream::*; pub use eventual::*; pub use eventual_base::{EventualCommon, EventualResolvedFuture}; diff --git a/veilid-tools/src/tests/common/test_host_interface.rs b/veilid-tools/src/tests/common/test_host_interface.rs index 8f31a953..43b9b3a8 100644 --- a/veilid-tools/src/tests/common/test_host_interface.rs +++ b/veilid-tools/src/tests/common/test_host_interface.rs @@ -467,7 +467,7 @@ pub async fn test_get_random_u64() { let t2 = get_timestamp(); let tdiff = ((t2 - t1) as f64) / 1000000.0f64; info!( - "running network interface test with {} iterations took {} seconds", + "running get_random_u64 with {} iterations took {} seconds", count, tdiff ); } @@ -482,7 +482,7 @@ pub async fn test_get_random_u32() { let t2 = get_timestamp(); let tdiff = ((t2 - t1) as f64) / 1000000.0f64; info!( - "running network interface test with {} iterations took {} seconds", + "running get_random_u32 with {} iterations took {} seconds", count, tdiff ); } diff --git a/veilid-tools/tests/web.rs b/veilid-tools/tests/web.rs index ac81ab39..8a04bce8 100644 --- a/veilid-tools/tests/web.rs +++ b/veilid-tools/tests/web.rs @@ -2,7 +2,7 @@ #![cfg(target_arch = "wasm32")] use veilid_tools::tests::*; -use veilid_tools::*; + use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); From 672d750f8f41ae6f86cc8eb5ee4f3cf93f4be1b5 Mon Sep 17 00:00:00 2001 From: John Smith Date: Tue, 29 Nov 2022 22:51:51 -0500 Subject: [PATCH 20/88] wasm fixes --- .cargo/config.toml | 12 -- Cargo.lock | 142 +++++++++--------- Earthfile | 3 + scripts/earthly/cargo-android/config.toml | 8 + scripts/earthly/cargo-linux/config.toml | 2 + veilid-core/src/intf/wasm/mod.rs | 3 - veilid-core/src/intf/wasm/protected_store.rs | 1 - veilid-core/src/intf/wasm/system.rs | 8 +- veilid-core/src/intf/wasm/table_store.rs | 4 +- veilid-core/src/lib.rs | 1 + .../src/routing_table/route_spec_store.rs | 2 +- veilid-core/src/tests/mod.rs | 11 ++ veilid-core/src/veilid_config.rs | 12 +- veilid-core/tests/web.rs | 69 +++++++-- 14 files changed, 166 insertions(+), 112 deletions(-) create mode 100644 scripts/earthly/cargo-android/config.toml create mode 100644 scripts/earthly/cargo-linux/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml index 91fa3b11..bff29e6e 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,14 +1,2 @@ [build] rustflags = ["--cfg", "tokio_unstable"] - -[target.aarch64-unknown-linux-gnu] -linker = "aarch64-linux-gnu-gcc" - -[target.aarch64-linux-android] -linker = "/Android/Sdk/ndk/22.0.7026061/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang" -[target.armv7-linux-androideabi] -linker = "/Android/Sdk/ndk/22.0.7026061/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi30-clang" -[target.x86_64-linux-android] -linker = "/Android/Sdk/ndk/22.0.7026061/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android30-clang" -[target.i686-linux-android] -linker = "/Android/Sdk/ndk/22.0.7026061/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android30-clang" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 31a72bd1..0376e818 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,11 +195,11 @@ dependencies = [ [[package]] name = "async-channel" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" dependencies = [ - "concurrent-queue 1.2.4", + "concurrent-queue", "event-listener", "futures-core", ] @@ -212,7 +212,7 @@ checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" dependencies = [ "async-lock", "async-task", - "concurrent-queue 2.0.0", + "concurrent-queue", "fastrand", "futures-lite", "slab", @@ -235,13 +235,13 @@ dependencies = [ [[package]] name = "async-io" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe557ebe0829511ddff4ad3011d159c0e6f144e05e3e8c3ab5095a131900a7b" +checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" dependencies = [ "async-lock", "autocfg", - "concurrent-queue 2.0.0", + "concurrent-queue", "futures-lite", "libc", "log", @@ -250,7 +250,7 @@ dependencies = [ "slab", "socket2", "waker-fn", - "winapi 0.3.9", + "windows-sys 0.42.0", ] [[package]] @@ -265,20 +265,20 @@ dependencies = [ [[package]] name = "async-process" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02111fd8655a613c25069ea89fc8d9bb89331fa77486eb3bc059ee757cfa481c" +checksum = "6381ead98388605d0d9ff86371043b5aa922a3905824244de40dc263a14fcba4" dependencies = [ "async-io", + "async-lock", "autocfg", "blocking", "cfg-if 1.0.0", "event-listener", "futures-lite", "libc", - "once_cell", "signal-hook", - "winapi 0.3.9", + "windows-sys 0.42.0", ] [[package]] @@ -366,9 +366,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.58" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" +checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364" dependencies = [ "proc-macro2", "quote", @@ -477,9 +477,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.5.17" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acee9fd5073ab6b045a275b3e709c163dd36c90685219cb21804a147b58dba43" +checksum = "08b108ad2665fa3f6e6a517c3d80ec3e77d224c47d605167aefaa5d7ef97fa48" dependencies = [ "async-trait", "axum-core", @@ -495,9 +495,9 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite 0.2.9", + "rustversion", "serde", "sync_wrapper", - "tokio 1.22.0", "tower", "tower-http", "tower-layer", @@ -506,9 +506,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.2.9" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e5939e02c56fecd5c017c37df4238c0a839fa76b7f97acdd7efb804fd181cc" +checksum = "79b8558f5a0581152dc94dcd289132a1d377494bdeafcd41869b3258e3e2ad92" dependencies = [ "async-trait", "bytes 1.3.0", @@ -516,6 +516,7 @@ dependencies = [ "http", "http-body", "mime", + "rustversion", "tower-layer", "tower-service", ] @@ -734,12 +735,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" -[[package]] -name = "cache-padded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" - [[package]] name = "capnp" version = "0.15.1" @@ -989,15 +984,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "concurrent-queue" -version = "1.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c" -dependencies = [ - "cache-padded", -] - [[package]] name = "concurrent-queue" version = "2.0.0" @@ -1534,7 +1520,7 @@ dependencies = [ "hashbrown", "lock_api", "once_cell", - "parking_lot_core 0.9.4", + "parking_lot_core 0.9.5", ] [[package]] @@ -1757,9 +1743,9 @@ dependencies = [ [[package]] name = "ethereum-types" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81224dc661606574f5a0f28c9947d0ee1d93ff11c5f1c4e7272f52e8c0b5483c" +checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" dependencies = [ "ethbloom", "fixed-hash", @@ -2106,9 +2092,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "gloo-timers" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9" +checksum = "98c4a8d6391675c6b2ee1a6c8d06e8e2d03605c44cec1270675985a4c2a5500b" dependencies = [ "futures-channel", "futures-core", @@ -2118,9 +2104,9 @@ dependencies = [ [[package]] name = "gloo-utils" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40913a05c8297adca04392f707b1e73b12ba7b8eab7244a4961580b1fd34063c" +checksum = "a8e8fc851e9c7b9852508bc6e3f690f452f474417e8545ec9857b7f7377036b5" dependencies = [ "js-sys", "serde", @@ -2868,9 +2854,9 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "matchit" -version = "0.5.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" +checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" [[package]] name = "memchr" @@ -3204,8 +3190,20 @@ dependencies = [ "bitflags", "cfg-if 1.0.0", "libc", - "memoffset 0.6.5", +] + +[[package]] +name = "nix" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "libc", + "memoffset 0.7.1", "pin-utils", + "static_assertions", ] [[package]] @@ -3580,7 +3578,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.4", + "parking_lot_core 0.9.5", ] [[package]] @@ -3599,9 +3597,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" +checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" dependencies = [ "cfg-if 1.0.0", "libc", @@ -3792,16 +3790,16 @@ dependencies = [ [[package]] name = "polling" -version = "2.4.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab4609a838d88b73d8238967b60dd115cc08d38e2bbaf51ee1e4b695f89122e2" +checksum = "166ca89eb77fd403230b9c156612965a81e094ec6ec3aa13663d4c8b113fa748" dependencies = [ "autocfg", "cfg-if 1.0.0", "libc", "log", "wepoll-ffi", - "winapi 0.3.9", + "windows-sys 0.42.0", ] [[package]] @@ -3905,9 +3903,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0841812012b2d4a6145fae9a6af1534873c32aa67fff26bd09f8fa42c83f95a" +checksum = "c0b18e655c21ff5ac2084a5ad0611e827b3f92badf79f4910b5a5c58f4d87ff0" dependencies = [ "bytes 1.3.0", "prost-derive", @@ -3915,9 +3913,9 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d8b442418ea0822409d9e7d047cbf1e7e9e1760b172bf9982cf29d517c93511" +checksum = "e330bf1316db56b12c2bcfa399e8edddd4821965ea25ddb2c134b610b1c1c604" dependencies = [ "bytes 1.3.0", "heck", @@ -4529,9 +4527,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.147" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc" dependencies = [ "serde_derive", ] @@ -4557,9 +4555,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.147" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c" dependencies = [ "proc-macro2", "quote", @@ -4642,9 +4640,9 @@ dependencies = [ [[package]] name = "sha-1" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" dependencies = [ "cfg-if 1.0.0", "cpufeatures", @@ -4927,9 +4925,9 @@ checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" [[package]] name = "syn" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "4ae548ec36cf198c0ef7710d3c230987c2d6d7bd98ad6edc0274462724c585ce" dependencies = [ "proc-macro2", "quote", @@ -5252,9 +5250,9 @@ dependencies = [ [[package]] name = "tonic" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55b9af819e54b8f33d453655bef9b9acc171568fb49523078d0cc4e7484200ec" +checksum = "8f219fad3b929bef19b1f86fbc0358d35daed8f2cac972037ac0dc10bbb8d5fb" dependencies = [ "async-stream", "async-trait", @@ -5284,9 +5282,9 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c6fd7c2581e36d63388a9e04c350c21beb7a8b059580b2e93993c526899ddc" +checksum = "5bf5e9b9c0f7e0a7c027dcfaba7b2c60816c7049171f679d99ee2ff65d0de8c4" dependencies = [ "prettyplease", "proc-macro2", @@ -5570,7 +5568,7 @@ dependencies = [ "httparse", "log", "rand 0.8.5", - "sha-1 0.10.0", + "sha-1 0.10.1", "thiserror", "url", "utf-8", @@ -5590,9 +5588,9 @@ checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" [[package]] name = "uint" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45526d29728d135c2900b0d30573fe3ee79fceb12ef534c7bb30e810a91b601" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" dependencies = [ "byteorder", "crunchy", @@ -5791,7 +5789,7 @@ dependencies = [ "maplit", "ndk 0.6.0", "ndk-glue", - "nix 0.25.0", + "nix 0.26.1", "once_cell", "owning_ref", "owo-colors", @@ -5892,7 +5890,7 @@ dependencies = [ "hostname", "json", "lazy_static", - "nix 0.25.0", + "nix 0.26.1", "opentelemetry", "opentelemetry-otlp", "opentelemetry-semantic-conventions", @@ -5939,7 +5937,7 @@ dependencies = [ "maplit", "ndk 0.6.0", "ndk-glue", - "nix 0.25.0", + "nix 0.26.1", "once_cell", "owo-colors", "parking_lot 0.11.2", diff --git a/Earthfile b/Earthfile index e8f17b66..6d385262 100644 --- a/Earthfile +++ b/Earthfile @@ -66,12 +66,15 @@ deps-linux: code-linux: FROM +deps-linux COPY --dir .cargo external files scripts veilid-cli veilid-core veilid-server veilid-flutter veilid-wasm Cargo.lock Cargo.toml /veilid + RUN cat /veilid/scripts/earthly/cargo-linux/config.toml >> /veilid/.cargo/config.tml WORKDIR /veilid # Code + Linux + Android deps code-android: FROM +deps-android COPY --dir .cargo external files scripts veilid-cli veilid-core veilid-server veilid-flutter veilid-wasm Cargo.lock Cargo.toml /veilid + RUN cat /veilid/scripts/earthly/cargo-linux/config.toml >> /veilid/.cargo/config.tml + RUN cat /veilid/scripts/earthly/cargo-android/config.toml >> /veilid/.cargo/config.tml WORKDIR /veilid # Clippy only diff --git a/scripts/earthly/cargo-android/config.toml b/scripts/earthly/cargo-android/config.toml new file mode 100644 index 00000000..88522ce8 --- /dev/null +++ b/scripts/earthly/cargo-android/config.toml @@ -0,0 +1,8 @@ +[target.aarch64-linux-android] +linker = "/Android/Sdk/ndk/22.0.7026061/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang" +[target.armv7-linux-androideabi] +linker = "/Android/Sdk/ndk/22.0.7026061/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi30-clang" +[target.x86_64-linux-android] +linker = "/Android/Sdk/ndk/22.0.7026061/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android30-clang" +[target.i686-linux-android] +linker = "/Android/Sdk/ndk/22.0.7026061/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android30-clang" \ No newline at end of file diff --git a/scripts/earthly/cargo-linux/config.toml b/scripts/earthly/cargo-linux/config.toml new file mode 100644 index 00000000..3c32d251 --- /dev/null +++ b/scripts/earthly/cargo-linux/config.toml @@ -0,0 +1,2 @@ +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" diff --git a/veilid-core/src/intf/wasm/mod.rs b/veilid-core/src/intf/wasm/mod.rs index 6b6e6967..53faa230 100644 --- a/veilid-core/src/intf/wasm/mod.rs +++ b/veilid-core/src/intf/wasm/mod.rs @@ -3,10 +3,7 @@ mod protected_store; mod system; mod table_store; -pub mod utils; - pub use block_store::*; pub use protected_store::*; pub use system::*; pub use table_store::*; -use utils::*; diff --git a/veilid-core/src/intf/wasm/protected_store.rs b/veilid-core/src/intf/wasm/protected_store.rs index 20f6ae89..9a2c1646 100644 --- a/veilid-core/src/intf/wasm/protected_store.rs +++ b/veilid-core/src/intf/wasm/protected_store.rs @@ -1,4 +1,3 @@ -use super::*; use crate::*; use data_encoding::BASE64URL_NOPAD; use rkyv::{Archive as RkyvArchive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize}; diff --git a/veilid-core/src/intf/wasm/system.rs b/veilid-core/src/intf/wasm/system.rs index 6de02714..95e4e544 100644 --- a/veilid-core/src/intf/wasm/system.rs +++ b/veilid-core/src/intf/wasm/system.rs @@ -1,6 +1,6 @@ -use async_executors::{Bindgen, LocalSpawnHandleExt, SpawnHandleExt, Timer}; -use futures_util::future::{select, Either}; -use js_sys::*; +use crate::*; + +//use js_sys::*; pub async fn get_outbound_relay_peer() -> Option { // unimplemented! @@ -8,7 +8,7 @@ pub async fn get_outbound_relay_peer() -> Option { } // pub async fn get_pwa_web_server_config() -> { -// if utils::is_browser() { +// if is_browser() { // let win = window().unwrap(); // let doc = win.document().unwrap(); diff --git a/veilid-core/src/intf/wasm/table_store.rs b/veilid-core/src/intf/wasm/table_store.rs index 2123e8ea..74478877 100644 --- a/veilid-core/src/intf/wasm/table_store.rs +++ b/veilid-core/src/intf/wasm/table_store.rs @@ -1,5 +1,3 @@ -use super::*; - use crate::intf::table_db::*; use crate::*; use keyvaluedb_web::*; @@ -135,7 +133,7 @@ impl TableStore { } } - if utils::is_browser() { + if is_browser() { let out = match Database::delete(table_name.clone()).await { Ok(_) => true, Err(_) => false, diff --git a/veilid-core/src/lib.rs b/veilid-core/src/lib.rs index e66eb522..e8dbcff8 100644 --- a/veilid-core/src/lib.rs +++ b/veilid-core/src/lib.rs @@ -1,5 +1,6 @@ #![deny(clippy::all)] #![deny(unused_must_use)] +#![recursion_limit = "1024"] cfg_if::cfg_if! { if #[cfg(target_arch = "wasm32")] { diff --git a/veilid-core/src/routing_table/route_spec_store.rs b/veilid-core/src/routing_table/route_spec_store.rs index cf56df49..23c6e9e6 100644 --- a/veilid-core/src/routing_table/route_spec_store.rs +++ b/veilid-core/src/routing_table/route_spec_store.rs @@ -192,7 +192,7 @@ impl RouteSpecDetail { /// The core representation of the RouteSpecStore that can be serialized #[derive(Debug, Clone, Default, RkyvArchive, RkyvSerialize, RkyvDeserialize)] -#[archive_attr(repr(C), derive(CheckBytes))] +#[archive_attr(repr(C, align(8)), derive(CheckBytes))] pub struct RouteSpecStoreContent { /// All of the routes we have allocated so far details: HashMap, diff --git a/veilid-core/src/tests/mod.rs b/veilid-core/src/tests/mod.rs index 0b6b6216..61f04f89 100644 --- a/veilid-core/src/tests/mod.rs +++ b/veilid-core/src/tests/mod.rs @@ -1,3 +1,14 @@ +#[cfg(target_os = "android")] +mod android; pub mod common; +#[cfg(target_os = "ios")] +mod ios; #[cfg(not(target_arch = "wasm32"))] mod native; + +#[allow(unused_imports)] +use super::*; + +pub use common::*; +pub use crypto::tests::*; +pub use network_manager::tests::*; diff --git a/veilid-core/src/veilid_config.rs b/veilid-core/src/veilid_config.rs index b610a989..2a6fbd91 100644 --- a/veilid-core/src/veilid_config.rs +++ b/veilid-core/src/veilid_config.rs @@ -597,11 +597,13 @@ impl VeilidConfig { macro_rules! get_config { ($key:expr) => { let keyname = &stringify!($key)[6..]; - $key = *cb(keyname.to_owned())?.downcast().map_err(|_| { - let err = format!("incorrect type for key {}", keyname); - debug!("{}", err); - VeilidAPIError::generic(err) - })?; + let v = cb(keyname.to_owned())?; + $key = match v.downcast() { + Ok(v) => *v, + Err(_) => { + apibail_generic!(format!("incorrect type for key {}", keyname)) + } + }; }; } diff --git a/veilid-core/tests/web.rs b/veilid-core/tests/web.rs index c4df57b1..5024dfd8 100644 --- a/veilid-core/tests/web.rs +++ b/veilid-core/tests/web.rs @@ -1,8 +1,9 @@ //! Test suite for the Web and headless browsers. #![cfg(target_arch = "wasm32")] +#![recursion_limit = "256"] -use veilid_core::tests::common::*; -use veilid_core::xx::*; +use veilid_core::tests::*; +use veilid_core::tools::*; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); @@ -15,24 +16,70 @@ static SETUP_ONCE: Once = Once::new(); pub fn setup() -> () { SETUP_ONCE.call_once(|| { console_error_panic_hook::set_once(); - let mut builder = tracing_wasm::WASMLayerConfigBuilder::new(); - builder.set_report_logs_in_timings(false); - builder.set_max_level(Level::TRACE); - builder.set_console_config(tracing_wasm::ConsoleConfig::ReportWithConsoleColor); - tracing_wasm::set_as_global_default_with_config(builder.build()); + cfg_if! { + if #[cfg(feature = "tracing")] { + let mut builder = tracing_wasm::WASMLayerConfigBuilder::new(); + builder.set_report_logs_in_timings(false); + builder.set_max_level(Level::TRACE); + builder.set_console_config(tracing_wasm::ConsoleConfig::ReportWithConsoleColor); + tracing_wasm::set_as_global_default_with_config(builder.build()); + } else { + wasm_logger::init(wasm_logger::Config::default()); + } + } }); } #[wasm_bindgen_test] async fn run_test_host_interface() { setup(); - test_host_interface::test_all().await; } #[wasm_bindgen_test] -async fn run_test_async_tag_lock() { +async fn run_test_dht_key() { setup(); - - test_async_tag_lock::test_all().await; + test_dht_key::test_all().await; +} + +#[wasm_bindgen_test] +async fn run_test_veilid_core() { + setup(); + test_veilid_core::test_all().await; +} + +#[wasm_bindgen_test] +async fn test_veilid_config() { + setup(); + test_veilid_config::test_all().await; +} + +#[wasm_bindgen_test] +async fn run_test_connection_table() { + setup(); + test_connection_table::test_all().await; +} + +#[wasm_bindgen_test] +async fn exec_test_table_store() { + setup(); + test_table_store::test_all().await; +} + +#[wasm_bindgen_test] +async fn exec_test_protected_store() { + setup(); + test_protected_store::test_all().await; +} + +#[wasm_bindgen_test] +async fn exec_test_crypto() { + setup(); + test_crypto::test_all().await; +} + +#[wasm_bindgen_test] +async fn exec_test_envelope_receipt() { + setup(); + test_envelope_receipt::test_all().await; } From b47b5c1e856bcdb399217763aa5ff8f8c5f1bb9a Mon Sep 17 00:00:00 2001 From: John Smith Date: Wed, 30 Nov 2022 09:39:12 -0500 Subject: [PATCH 21/88] more test work --- veilid-core/Cargo.toml | 4 +-- veilid-core/run_tests.sh | 6 ++-- veilid-core/src/intf/native/mod.rs | 2 +- veilid-core/src/lib.rs | 2 +- .../src/tests/android/app/build.gradle | 4 +-- .../android/app/src/main/AndroidManifest.xml | 2 +- .../MainActivity.java | 2 +- .../tests/android/remove_from_all_devices.sh | 2 +- .../project.pbxproj | 16 ++++++----- veilid-core/src/tests/native/mod.rs | 8 +++--- veilid-flutter/android/build.gradle | 3 -- veilid-tools/Cargo.toml | 5 ++-- veilid-tools/run_tests.sh | 10 +++---- .../.gitignore | 0 .../.idea/.gitignore | 0 .../.idea/.name | 0 .../.idea/compiler.xml | 0 .../.idea/gradle.xml | 0 .../.idea/jarRepositories.xml | 0 .../.idea/misc.xml | 0 .../.idea/vcs.xml | 0 .../.project | 0 .../org.eclipse.buildship.core.prefs | 0 .../adb+.sh | 0 .../app/.classpath | 0 .../app/.gitignore | 0 .../app/.project | 0 .../org.eclipse.buildship.core.prefs | 0 .../app/CMakeLists.txt | 0 .../app/build.gradle | 27 ++---------------- .../app/cpplink.cpp | 0 .../app/proguard-rules.pro | 0 .../app/src/main/AndroidManifest.xml | 0 .../MainActivity.java | 2 +- .../drawable-v24/ic_launcher_foreground.xml | 0 .../res/drawable/ic_launcher_background.xml | 0 .../app/src/main/res/layout/activity_main.xml | 0 .../res/mipmap-anydpi-v26/ic_launcher.xml | 0 .../mipmap-anydpi-v26/ic_launcher_round.xml | 0 .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin .../res/mipmap-hdpi/ic_launcher_round.png | Bin .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin .../res/mipmap-mdpi/ic_launcher_round.png | Bin .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin .../res/mipmap-xhdpi/ic_launcher_round.png | Bin .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin .../app/src/main/res/values-night/themes.xml | 0 .../app/src/main/res/values/colors.xml | 0 .../app/src/main/res/values/strings.xml | 0 .../app/src/main/res/values/themes.xml | 0 .../build.gradle | 0 .../gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.jar | Bin .../gradle/wrapper/gradle-wrapper.properties | 0 .../gradlew | 0 .../gradlew.bat | 0 .../install_on_all_devices.sh | 0 .../remove_from_all_devices.sh | 3 ++ .../settings.gradle | 0 .../remove_from_all_devices.sh | 3 -- .../project.pbxproj | 2 +- veilid-tools/src/tests/native/mod.rs | 8 +++--- 65 files changed, 44 insertions(+), 67 deletions(-) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/.gitignore (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/.idea/.gitignore (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/.idea/.name (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/.idea/compiler.xml (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/.idea/gradle.xml (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/.idea/jarRepositories.xml (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/.idea/misc.xml (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/.idea/vcs.xml (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/.project (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/.settings/org.eclipse.buildship.core.prefs (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/adb+.sh (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/.classpath (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/.gitignore (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/.project (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/.settings/org.eclipse.buildship.core.prefs (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/CMakeLists.txt (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/build.gradle (68%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/cpplink.cpp (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/proguard-rules.pro (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/src/main/AndroidManifest.xml (100%) rename veilid-tools/src/tests/android/{veilidtools-tests/app/src/main/java/com/veilid/veilidtools_tests => veilid_tools_android_tests/app/src/main/java/com/veilid/veilid_tools_android_tests}/MainActivity.java (94%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/src/main/res/drawable-v24/ic_launcher_foreground.xml (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/src/main/res/drawable/ic_launcher_background.xml (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/src/main/res/layout/activity_main.xml (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/src/main/res/mipmap-hdpi/ic_launcher.png (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/src/main/res/mipmap-hdpi/ic_launcher_round.png (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/src/main/res/mipmap-mdpi/ic_launcher.png (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/src/main/res/mipmap-mdpi/ic_launcher_round.png (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/src/main/res/mipmap-xhdpi/ic_launcher.png (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/src/main/res/values-night/themes.xml (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/src/main/res/values/colors.xml (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/src/main/res/values/strings.xml (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/app/src/main/res/values/themes.xml (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/build.gradle (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/gradle.properties (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/gradle/wrapper/gradle-wrapper.jar (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/gradle/wrapper/gradle-wrapper.properties (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/gradlew (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/gradlew.bat (100%) rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/install_on_all_devices.sh (100%) create mode 100755 veilid-tools/src/tests/android/veilid_tools_android_tests/remove_from_all_devices.sh rename veilid-tools/src/tests/android/{veilidtools-tests => veilid_tools_android_tests}/settings.gradle (100%) delete mode 100755 veilid-tools/src/tests/android/veilidtools-tests/remove_from_all_devices.sh diff --git a/veilid-core/Cargo.toml b/veilid-core/Cargo.toml index fa0656da..9a239431 100644 --- a/veilid-core/Cargo.toml +++ b/veilid-core/Cargo.toml @@ -14,8 +14,8 @@ default = [] rt-async-std = [ "async-std", "async-std-resolver", "async_executors/async_std", "rtnetlink?/smol_socket", "veilid-tools/rt-async-std" ] rt-tokio = [ "tokio", "tokio-util", "tokio-stream", "trust-dns-resolver/tokio-runtime", "async_executors/tokio_tp", "async_executors/tokio_io", "async_executors/tokio_timer", "rtnetlink?/tokio_socket", "veilid-tools/rt-tokio" ] -android_tests = [] -ios_tests = [ "simplelog" ] +keyring_manager_android_tests = [] +veilid_core_ios_tests = [ "simplelog" ] tracking = [] [dependencies] diff --git a/veilid-core/run_tests.sh b/veilid-core/run_tests.sh index a4bbb16f..deed7641 100755 --- a/veilid-core/run_tests.sh +++ b/veilid-core/run_tests.sh @@ -30,8 +30,8 @@ elif [[ "$1" == "android" ]]; then echo "No emulator ID specified" exit 1 fi - APPNAME=veilidcore-tests - APPID=com.veilid.veilidcore_tests + APPNAME=veilid_core_android_tests + APPID=com.veilid.veilid_core_android_tests ACTIVITYNAME=MainActivity pushd src/tests/android/$APPNAME >/dev/null # Build apk @@ -45,7 +45,7 @@ elif [[ "$1" == "android" ]]; then # Get the pid of the program APP_PID=`adb -s $ID shell pidof -s $APPID` # Print the logcat - adb -s $ID shell logcat -d veilid-core:V *:S & + adb -s $ID shell logcat --pid=$APP_PID veilid-core:V *:S & # Wait for the pid to be done while [ "$(adb -s $ID shell pidof -s $APPID)" != "" ]; do sleep 1 diff --git a/veilid-core/src/intf/native/mod.rs b/veilid-core/src/intf/native/mod.rs index 369a0183..8f7e39bf 100644 --- a/veilid-core/src/intf/native/mod.rs +++ b/veilid-core/src/intf/native/mod.rs @@ -10,6 +10,6 @@ pub use table_store::*; #[cfg(target_os = "android")] pub mod android; -#[cfg(all(target_os = "ios", feature = "ios_tests"))] +#[cfg(all(target_os = "ios", feature = "veilid_core_ios_tests"))] pub mod ios_test_setup; pub mod network_interfaces; diff --git a/veilid-core/src/lib.rs b/veilid-core/src/lib.rs index e8dbcff8..d8379b94 100644 --- a/veilid-core/src/lib.rs +++ b/veilid-core/src/lib.rs @@ -1,6 +1,6 @@ #![deny(clippy::all)] #![deny(unused_must_use)] -#![recursion_limit = "1024"] +#![recursion_limit = "256"] cfg_if::cfg_if! { if #[cfg(target_arch = "wasm32")] { diff --git a/veilid-core/src/tests/android/app/build.gradle b/veilid-core/src/tests/android/app/build.gradle index 25ad2c38..0c7d08ce 100644 --- a/veilid-core/src/tests/android/app/build.gradle +++ b/veilid-core/src/tests/android/app/build.gradle @@ -7,7 +7,7 @@ android { buildToolsVersion "30.0.3" defaultConfig { - applicationId "com.veilid.veilidcore.veilidcore_android_tests" + applicationId "com.veilid.veilid_core_android_tests" minSdkVersion 24 targetSdkVersion 30 versionCode 1 @@ -69,7 +69,7 @@ cargo { profile = gradle.startParameter.taskNames.any{it.toLowerCase().contains("debug")} ? "debug" : "release" pythonCommand = "python3" features { - defaultAnd("android_tests", "rt-tokio") + defaultAnd("veilid_core_android_tests", "rt-tokio") } } diff --git a/veilid-core/src/tests/android/app/src/main/AndroidManifest.xml b/veilid-core/src/tests/android/app/src/main/AndroidManifest.xml index a0f77f64..8f900643 100644 --- a/veilid-core/src/tests/android/app/src/main/AndroidManifest.xml +++ b/veilid-core/src/tests/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + package="com.veilid.veilid_core_android_tests"> diff --git a/veilid-core/src/tests/android/app/src/main/java/com/veilid/veilid-core/veilid-core_android_tests/MainActivity.java b/veilid-core/src/tests/android/app/src/main/java/com/veilid/veilid-core/veilid-core_android_tests/MainActivity.java index 00724e1b..43a65090 100644 --- a/veilid-core/src/tests/android/app/src/main/java/com/veilid/veilid-core/veilid-core_android_tests/MainActivity.java +++ b/veilid-core/src/tests/android/app/src/main/java/com/veilid/veilid-core/veilid-core_android_tests/MainActivity.java @@ -1,4 +1,4 @@ -package com.veilid.veilidcore.veilidcore_android_tests; +package com.veilid.veilid_core_android_tests; import androidx.appcompat.app.AppCompatActivity; import android.content.Context; diff --git a/veilid-core/src/tests/android/remove_from_all_devices.sh b/veilid-core/src/tests/android/remove_from_all_devices.sh index d3eace9d..b13b8775 100755 --- a/veilid-core/src/tests/android/remove_from_all_devices.sh +++ b/veilid-core/src/tests/android/remove_from_all_devices.sh @@ -1,3 +1,3 @@ #!/bin/bash -./adb+.sh uninstall com.veilid.veilidcore.veilidcore_android_tests +./adb+.sh uninstall com.veilid.veilid_core_android_tests diff --git a/veilid-core/src/tests/ios/veilidcore-tests/veilidcore-tests.xcodeproj/project.pbxproj b/veilid-core/src/tests/ios/veilidcore-tests/veilidcore-tests.xcodeproj/project.pbxproj index 9467bfd8..85356d38 100644 --- a/veilid-core/src/tests/ios/veilidcore-tests/veilidcore-tests.xcodeproj/project.pbxproj +++ b/veilid-core/src/tests/ios/veilidcore-tests/veilidcore-tests.xcodeproj/project.pbxproj @@ -167,7 +167,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "../../../../ios_build.sh --features ios_tests\n"; + shellScript = "../../../../../scripts/ios_build.sh ../../../../ veilid_core --features veilid_core_ios_tests,rt-tokio\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -209,7 +209,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = arm64; + ARCHS = "$(ARCHS_STANDARD)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -264,6 +264,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -271,7 +272,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = arm64; + ARCHS = "$(ARCHS_STANDARD)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -319,6 +320,7 @@ SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; @@ -339,11 +341,11 @@ ); OTHER_LDFLAGS = ""; "OTHER_LDFLAGS[sdk=iphoneos*]" = ( - "-L../../../../../target/aarch64-apple-ios/debug", + "-L../../../../../target/lipo-ios/debug", "-lveilid_core", ); "OTHER_LDFLAGS[sdk=iphonesimulator*]" = ( - "-L../../../../../target/x86_64-apple-ios/debug", + "-L../../../../../target/lipo-ios-sim/debug", "-lveilid_core", ); PRODUCT_BUNDLE_IDENTIFIER = "com.veilid.veilidcore-tests"; @@ -371,11 +373,11 @@ ); OTHER_LDFLAGS = ""; "OTHER_LDFLAGS[sdk=iphoneos*]" = ( - "-L../../../../../target/aarch64-apple-ios/release", + "-L../../../../../target/lipo-ios/release", "-lveilid_core", ); "OTHER_LDFLAGS[sdk=iphonesimulator*]" = ( - "-L../../../../../target/x86_64-apple-ios/release", + "-L../../../../../target/lipo-ios-sim/release", "-lveilid_core", ); PRODUCT_BUNDLE_IDENTIFIER = "com.veilid.veilidcore-tests"; diff --git a/veilid-core/src/tests/native/mod.rs b/veilid-core/src/tests/native/mod.rs index a7a0580d..b1f96995 100644 --- a/veilid-core/src/tests/native/mod.rs +++ b/veilid-core/src/tests/native/mod.rs @@ -5,13 +5,13 @@ use crate::network_manager::tests::*; use crate::tests::common::*; use crate::*; -#[cfg(all(target_os = "android", feature = "android_tests"))] +#[cfg(all(target_os = "android", feature = "veilid_core_android_tests"))] use jni::{objects::JClass, objects::JObject, JNIEnv}; -#[cfg(all(target_os = "android", feature = "android_tests"))] +#[cfg(all(target_os = "android", feature = "veilid_core_android_tests"))] #[no_mangle] #[allow(non_snake_case)] -pub extern "system" fn Java_com_veilid_veilidcore_veilidcore_1android_1tests_MainActivity_run_1tests( +pub extern "system" fn Java_com_veilid_veilid_1core_1android_1tests_MainActivity_run_1tests( env: JNIEnv, _class: JClass, ctx: JObject, @@ -25,7 +25,7 @@ pub extern "system" fn Java_com_veilid_veilidcore_veilidcore_1android_1tests_Mai run_all_tests(); } -#[cfg(all(target_os = "ios", feature = "ios_tests"))] +#[cfg(all(target_os = "ios", feature = "veilid_core_ios_tests"))] #[no_mangle] pub extern "C" fn run_veilid_core_tests() { let log_path: std::path::PathBuf = [ diff --git a/veilid-flutter/android/build.gradle b/veilid-flutter/android/build.gradle index e16cf76c..b59ea533 100644 --- a/veilid-flutter/android/build.gradle +++ b/veilid-flutter/android/build.gradle @@ -88,9 +88,6 @@ cargo { prebuiltToolchains = true pythonCommand = "python3" profile = gradle.startParameter.taskNames.any{it.toLowerCase().contains("debug")} ? "debug" : "release" - // features { - // defaultAnd("android_tests") - // } } afterEvaluate { diff --git a/veilid-tools/Cargo.toml b/veilid-tools/Cargo.toml index d85ae4c6..2adf291b 100644 --- a/veilid-tools/Cargo.toml +++ b/veilid-tools/Cargo.toml @@ -14,9 +14,8 @@ default = [] rt-async-std = [ "async-std", "async_executors/async_std", ] rt-tokio = [ "tokio", "tokio-util", "async_executors/tokio_tp", "async_executors/tokio_io", "async_executors/tokio_timer", ] -android_tests = [ "dep:tracing-android" ] -ios_tests = [] -tracking = [] +veilid_tools_android_tests = [ "dep:tracing-android" ] +veilid_tools_ios_tests = [] tracing = [ "dep:tracing", "dep:tracing-subscriber" ] [dependencies] diff --git a/veilid-tools/run_tests.sh b/veilid-tools/run_tests.sh index 86126884..0267d8aa 100755 --- a/veilid-tools/run_tests.sh +++ b/veilid-tools/run_tests.sh @@ -30,8 +30,8 @@ elif [[ "$1" == "android" ]]; then echo "No emulator ID specified" exit 1 fi - APPNAME=veilidtools-tests - APPID=com.veilid.veilidtools_tests + APPNAME=veilid_tools_android_tests + APPID=com.veilid.veilid_tools_android_tests ACTIVITYNAME=MainActivity pushd src/tests/android/$APPNAME >/dev/null # Build apk @@ -45,7 +45,7 @@ elif [[ "$1" == "android" ]]; then # Get the pid of the program APP_PID=`adb -s $ID shell pidof -s $APPID` # Print the logcat - adb -s $ID shell logcat -d veilid-tools:V *:S & + adb -s $ID shell logcat --pid=$APP_PID veilid-tools:V *:S & # Wait for the pid to be done while [ "$(adb -s $ID shell pidof -s $APPID)" != "" ]; do sleep 1 @@ -56,9 +56,9 @@ elif [[ "$1" == "android" ]]; then popd >/dev/null else + cargo test --features=rt-tokio,tracing + cargo test --features=rt-async-std,tracing cargo test --features=rt-tokio cargo test --features=rt-async-std - cargo test --features=rt-tokio,log --no-default-features - cargo test --features=rt-async-std,log --no-default-features fi popd 2>/dev/null \ No newline at end of file diff --git a/veilid-tools/src/tests/android/veilidtools-tests/.gitignore b/veilid-tools/src/tests/android/veilid_tools_android_tests/.gitignore similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/.gitignore rename to veilid-tools/src/tests/android/veilid_tools_android_tests/.gitignore diff --git a/veilid-tools/src/tests/android/veilidtools-tests/.idea/.gitignore b/veilid-tools/src/tests/android/veilid_tools_android_tests/.idea/.gitignore similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/.idea/.gitignore rename to veilid-tools/src/tests/android/veilid_tools_android_tests/.idea/.gitignore diff --git a/veilid-tools/src/tests/android/veilidtools-tests/.idea/.name b/veilid-tools/src/tests/android/veilid_tools_android_tests/.idea/.name similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/.idea/.name rename to veilid-tools/src/tests/android/veilid_tools_android_tests/.idea/.name diff --git a/veilid-tools/src/tests/android/veilidtools-tests/.idea/compiler.xml b/veilid-tools/src/tests/android/veilid_tools_android_tests/.idea/compiler.xml similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/.idea/compiler.xml rename to veilid-tools/src/tests/android/veilid_tools_android_tests/.idea/compiler.xml diff --git a/veilid-tools/src/tests/android/veilidtools-tests/.idea/gradle.xml b/veilid-tools/src/tests/android/veilid_tools_android_tests/.idea/gradle.xml similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/.idea/gradle.xml rename to veilid-tools/src/tests/android/veilid_tools_android_tests/.idea/gradle.xml diff --git a/veilid-tools/src/tests/android/veilidtools-tests/.idea/jarRepositories.xml b/veilid-tools/src/tests/android/veilid_tools_android_tests/.idea/jarRepositories.xml similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/.idea/jarRepositories.xml rename to veilid-tools/src/tests/android/veilid_tools_android_tests/.idea/jarRepositories.xml diff --git a/veilid-tools/src/tests/android/veilidtools-tests/.idea/misc.xml b/veilid-tools/src/tests/android/veilid_tools_android_tests/.idea/misc.xml similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/.idea/misc.xml rename to veilid-tools/src/tests/android/veilid_tools_android_tests/.idea/misc.xml diff --git a/veilid-tools/src/tests/android/veilidtools-tests/.idea/vcs.xml b/veilid-tools/src/tests/android/veilid_tools_android_tests/.idea/vcs.xml similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/.idea/vcs.xml rename to veilid-tools/src/tests/android/veilid_tools_android_tests/.idea/vcs.xml diff --git a/veilid-tools/src/tests/android/veilidtools-tests/.project b/veilid-tools/src/tests/android/veilid_tools_android_tests/.project similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/.project rename to veilid-tools/src/tests/android/veilid_tools_android_tests/.project diff --git a/veilid-tools/src/tests/android/veilidtools-tests/.settings/org.eclipse.buildship.core.prefs b/veilid-tools/src/tests/android/veilid_tools_android_tests/.settings/org.eclipse.buildship.core.prefs similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/.settings/org.eclipse.buildship.core.prefs rename to veilid-tools/src/tests/android/veilid_tools_android_tests/.settings/org.eclipse.buildship.core.prefs diff --git a/veilid-tools/src/tests/android/veilidtools-tests/adb+.sh b/veilid-tools/src/tests/android/veilid_tools_android_tests/adb+.sh similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/adb+.sh rename to veilid-tools/src/tests/android/veilid_tools_android_tests/adb+.sh diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/.classpath b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/.classpath similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/.classpath rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/.classpath diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/.gitignore b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/.gitignore similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/.gitignore rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/.gitignore diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/.project b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/.project similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/.project rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/.project diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/.settings/org.eclipse.buildship.core.prefs b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/.settings/org.eclipse.buildship.core.prefs similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/.settings/org.eclipse.buildship.core.prefs rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/.settings/org.eclipse.buildship.core.prefs diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/CMakeLists.txt b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/CMakeLists.txt similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/CMakeLists.txt rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/CMakeLists.txt diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/build.gradle b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/build.gradle similarity index 68% rename from veilid-tools/src/tests/android/veilidtools-tests/app/build.gradle rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/build.gradle index 5e30101c..ebcf32e0 100644 --- a/veilid-tools/src/tests/android/veilidtools-tests/app/build.gradle +++ b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/build.gradle @@ -7,7 +7,7 @@ android { buildToolsVersion "33.0.1" defaultConfig { - applicationId "com.veilid.veilidtools_tests" + applicationId "com.veilid.veilid_tools_android_tests" minSdkVersion 24 targetSdkVersion 33 versionCode 1 @@ -47,25 +47,7 @@ android { path file('CMakeLists.txt') } } - namespace 'com.veilid.veilidtools_tests' - - testOptions { - managedDevices { - devices { - pixel2api30 (com.android.build.api.dsl.ManagedVirtualDevice) { - // Use device profiles you typically see in Android Studio. - device = "Pixel 2" - // ATD currently support only API level 30. - apiLevel = 30 - // You can also specify "google-atd" if you require Google - // Play Services. - systemImageSource = "aosp-atd" - // Whether the image must be a 64 bit image. - require64Bit = false - } - } - } - } + namespace 'com.veilid.veilid_tools_android_tests' } dependencies { @@ -73,9 +55,6 @@ dependencies { implementation 'com.google.android.material:material:1.7.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.security:security-crypto:1.1.0-alpha04' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' - androidTestImplementation 'androidx.test:runner:1.5.1' - androidTestImplementation 'androidx.test:rules:1.5.0' } apply plugin: 'org.mozilla.rust-android-gradle.rust-android' @@ -89,7 +68,7 @@ cargo { profile = gradle.startParameter.taskNames.any{it.toLowerCase().contains("debug")} ? "debug" : "release" pythonCommand = "python3" features { - defaultAnd("android_tests", "rt-tokio") + defaultAnd("veilid_tools_android_tests", "rt-tokio") } } diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/cpplink.cpp b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/cpplink.cpp similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/cpplink.cpp rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/cpplink.cpp diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/proguard-rules.pro b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/proguard-rules.pro similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/proguard-rules.pro rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/proguard-rules.pro diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/AndroidManifest.xml b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/AndroidManifest.xml similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/AndroidManifest.xml rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/AndroidManifest.xml diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/java/com/veilid/veilidtools_tests/MainActivity.java b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/java/com/veilid/veilid_tools_android_tests/MainActivity.java similarity index 94% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/java/com/veilid/veilidtools_tests/MainActivity.java rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/java/com/veilid/veilid_tools_android_tests/MainActivity.java index 8e24334f..519bd437 100644 --- a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/java/com/veilid/veilidtools_tests/MainActivity.java +++ b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/java/com/veilid/veilid_tools_android_tests/MainActivity.java @@ -1,4 +1,4 @@ -package com.veilid.veilidtools_tests; +package com.veilid.veilid_tools_android_tests; import androidx.appcompat.app.AppCompatActivity; import android.content.Context; diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/drawable-v24/ic_launcher_foreground.xml similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/drawable-v24/ic_launcher_foreground.xml diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/drawable/ic_launcher_background.xml b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/drawable/ic_launcher_background.xml rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/drawable/ic_launcher_background.xml diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/layout/activity_main.xml b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/layout/activity_main.xml similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/layout/activity_main.xml rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/layout/activity_main.xml diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-hdpi/ic_launcher.png b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-hdpi/ic_launcher_round.png rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-hdpi/ic_launcher_round.png diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-mdpi/ic_launcher.png b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-mdpi/ic_launcher_round.png rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-mdpi/ic_launcher_round.png diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/values-night/themes.xml b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/values-night/themes.xml similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/values-night/themes.xml rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/values-night/themes.xml diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/values/colors.xml b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/values/colors.xml similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/values/colors.xml rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/values/colors.xml diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/values/strings.xml b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/values/strings.xml similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/values/strings.xml rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/values/strings.xml diff --git a/veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/values/themes.xml b/veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/values/themes.xml similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/app/src/main/res/values/themes.xml rename to veilid-tools/src/tests/android/veilid_tools_android_tests/app/src/main/res/values/themes.xml diff --git a/veilid-tools/src/tests/android/veilidtools-tests/build.gradle b/veilid-tools/src/tests/android/veilid_tools_android_tests/build.gradle similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/build.gradle rename to veilid-tools/src/tests/android/veilid_tools_android_tests/build.gradle diff --git a/veilid-tools/src/tests/android/veilidtools-tests/gradle.properties b/veilid-tools/src/tests/android/veilid_tools_android_tests/gradle.properties similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/gradle.properties rename to veilid-tools/src/tests/android/veilid_tools_android_tests/gradle.properties diff --git a/veilid-tools/src/tests/android/veilidtools-tests/gradle/wrapper/gradle-wrapper.jar b/veilid-tools/src/tests/android/veilid_tools_android_tests/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/gradle/wrapper/gradle-wrapper.jar rename to veilid-tools/src/tests/android/veilid_tools_android_tests/gradle/wrapper/gradle-wrapper.jar diff --git a/veilid-tools/src/tests/android/veilidtools-tests/gradle/wrapper/gradle-wrapper.properties b/veilid-tools/src/tests/android/veilid_tools_android_tests/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/gradle/wrapper/gradle-wrapper.properties rename to veilid-tools/src/tests/android/veilid_tools_android_tests/gradle/wrapper/gradle-wrapper.properties diff --git a/veilid-tools/src/tests/android/veilidtools-tests/gradlew b/veilid-tools/src/tests/android/veilid_tools_android_tests/gradlew similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/gradlew rename to veilid-tools/src/tests/android/veilid_tools_android_tests/gradlew diff --git a/veilid-tools/src/tests/android/veilidtools-tests/gradlew.bat b/veilid-tools/src/tests/android/veilid_tools_android_tests/gradlew.bat similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/gradlew.bat rename to veilid-tools/src/tests/android/veilid_tools_android_tests/gradlew.bat diff --git a/veilid-tools/src/tests/android/veilidtools-tests/install_on_all_devices.sh b/veilid-tools/src/tests/android/veilid_tools_android_tests/install_on_all_devices.sh similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/install_on_all_devices.sh rename to veilid-tools/src/tests/android/veilid_tools_android_tests/install_on_all_devices.sh diff --git a/veilid-tools/src/tests/android/veilid_tools_android_tests/remove_from_all_devices.sh b/veilid-tools/src/tests/android/veilid_tools_android_tests/remove_from_all_devices.sh new file mode 100755 index 00000000..e76b7cff --- /dev/null +++ b/veilid-tools/src/tests/android/veilid_tools_android_tests/remove_from_all_devices.sh @@ -0,0 +1,3 @@ +#!/bin/bash +./adb+.sh uninstall com.veilid.veilid_tools_android_tests + diff --git a/veilid-tools/src/tests/android/veilidtools-tests/settings.gradle b/veilid-tools/src/tests/android/veilid_tools_android_tests/settings.gradle similarity index 100% rename from veilid-tools/src/tests/android/veilidtools-tests/settings.gradle rename to veilid-tools/src/tests/android/veilid_tools_android_tests/settings.gradle diff --git a/veilid-tools/src/tests/android/veilidtools-tests/remove_from_all_devices.sh b/veilid-tools/src/tests/android/veilidtools-tests/remove_from_all_devices.sh deleted file mode 100755 index eea0ef7e..00000000 --- a/veilid-tools/src/tests/android/veilidtools-tests/remove_from_all_devices.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -./adb+.sh uninstall com.veilid.veilidtools.veilidtools_android_tests - diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj index 17193b37..153d3208 100644 --- a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj @@ -167,7 +167,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "../../../../../scripts/ios_build.sh ../../../../ veilid_tools --features ios_tests,rt-tokio\n"; + shellScript = "../../../../../scripts/ios_build.sh ../../../../ veilid_tools --features veilid_tools_ios_tests,rt-tokio\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/veilid-tools/src/tests/native/mod.rs b/veilid-tools/src/tests/native/mod.rs index b4d2728e..120fa9cb 100644 --- a/veilid-tools/src/tests/native/mod.rs +++ b/veilid-tools/src/tests/native/mod.rs @@ -5,13 +5,13 @@ mod test_async_peek_stream; use super::*; -#[cfg(all(target_os = "android", feature = "android_tests"))] +#[cfg(all(target_os = "android", feature = "veilid_tools_android_tests"))] use jni::{objects::JClass, objects::JObject, JNIEnv}; -#[cfg(all(target_os = "android", feature = "android_tests"))] +#[cfg(all(target_os = "android", feature = "veilid_tools_android_tests"))] #[no_mangle] #[allow(non_snake_case)] -pub extern "system" fn Java_com_veilid_veilidtools_1tests_MainActivity_run_1tests( +pub extern "system" fn Java_com_veilid_veilid_1tools_1android_1tests_MainActivity_run_1tests( env: JNIEnv, _class: JClass, ctx: JObject, @@ -20,7 +20,7 @@ pub extern "system" fn Java_com_veilid_veilidtools_1tests_MainActivity_run_1test run_all_tests(); } -#[cfg(all(target_os = "ios", feature = "ios_tests"))] +#[cfg(all(target_os = "ios", feature = "veilid_tools_ios_tests"))] #[no_mangle] #[allow(dead_code)] pub extern "C" fn run_veilid_tools_tests() { From b2c14fc56c98f08a26d75b72a146a0b667416a27 Mon Sep 17 00:00:00 2001 From: John Smith Date: Wed, 30 Nov 2022 09:48:49 -0500 Subject: [PATCH 22/88] upgrades --- veilid-core/src/tests/android/app/build.gradle | 18 ++++++++---------- .../android/app/src/main/AndroidManifest.xml | 3 +-- .../MainActivity.java | 0 veilid-core/src/tests/android/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- 5 files changed, 11 insertions(+), 14 deletions(-) rename veilid-core/src/tests/android/app/src/main/java/com/veilid/{veilid-core/veilid-core_android_tests => veilid_core_android_tests}/MainActivity.java (100%) diff --git a/veilid-core/src/tests/android/app/build.gradle b/veilid-core/src/tests/android/app/build.gradle index 0c7d08ce..9dadf0aa 100644 --- a/veilid-core/src/tests/android/app/build.gradle +++ b/veilid-core/src/tests/android/app/build.gradle @@ -3,13 +3,13 @@ plugins { } android { - compileSdkVersion 30 + compileSdkVersion 33 buildToolsVersion "30.0.3" defaultConfig { applicationId "com.veilid.veilid_core_android_tests" minSdkVersion 24 - targetSdkVersion 30 + targetSdkVersion 33 versionCode 1 versionName "1.0" @@ -38,7 +38,7 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - ndkVersion '22.0.7026061' + ndkVersion '25.1.8937393' // Required to copy libc++_shared.so externalNativeBuild { @@ -46,16 +46,14 @@ android { path file('CMakeLists.txt') } } + namespace 'com.veilid.veilid_core_android_tests' } dependencies { - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation 'com.google.android.material:material:1.4.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.2' - implementation 'androidx.security:security-crypto:1.1.0-alpha03' - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + implementation 'androidx.appcompat:appcompat:1.5.1' + implementation 'com.google.android.material:material:1.7.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.security:security-crypto:1.1.0-alpha04' } apply plugin: 'org.mozilla.rust-android-gradle.rust-android' diff --git a/veilid-core/src/tests/android/app/src/main/AndroidManifest.xml b/veilid-core/src/tests/android/app/src/main/AndroidManifest.xml index 8f900643..14b07cbe 100644 --- a/veilid-core/src/tests/android/app/src/main/AndroidManifest.xml +++ b/veilid-core/src/tests/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + diff --git a/veilid-core/src/tests/android/app/src/main/java/com/veilid/veilid-core/veilid-core_android_tests/MainActivity.java b/veilid-core/src/tests/android/app/src/main/java/com/veilid/veilid_core_android_tests/MainActivity.java similarity index 100% rename from veilid-core/src/tests/android/app/src/main/java/com/veilid/veilid-core/veilid-core_android_tests/MainActivity.java rename to veilid-core/src/tests/android/app/src/main/java/com/veilid/veilid_core_android_tests/MainActivity.java diff --git a/veilid-core/src/tests/android/build.gradle b/veilid-core/src/tests/android/build.gradle index 96496236..2e8d8f04 100644 --- a/veilid-core/src/tests/android/build.gradle +++ b/veilid-core/src/tests/android/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath "com.android.tools.build:gradle:4.1.2" + classpath 'com.android.tools.build:gradle:7.3.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/veilid-core/src/tests/android/gradle/wrapper/gradle-wrapper.properties b/veilid-core/src/tests/android/gradle/wrapper/gradle-wrapper.properties index 3a56e3d2..c5da459e 100644 --- a/veilid-core/src/tests/android/gradle/wrapper/gradle-wrapper.properties +++ b/veilid-core/src/tests/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip From edc87cd78ed5fae557404e8d1d1b78f584179da7 Mon Sep 17 00:00:00 2001 From: John Smith Date: Wed, 30 Nov 2022 16:15:54 -0500 Subject: [PATCH 23/88] build updates --- Cargo.lock | 61 ++++--------------- Cargo.toml | 2 +- external/keyring-manager | 2 +- scripts/earthly/cargo-android/config.toml | 8 +-- veilid-core/Cargo.toml | 6 +- veilid-core/src/tests/android/.idea/.name | 2 +- .../src/tests/android/app/build.gradle | 5 +- .../android/app/src/main/AndroidManifest.xml | 3 +- veilid-core/src/tests/android/build.gradle | 2 +- veilid-core/src/tests/android/settings.gradle | 2 +- veilid-tools/Cargo.toml | 4 +- 11 files changed, 30 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0376e818..eb62b7c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -737,9 +737,9 @@ checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" [[package]] name = "capnp" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4929d71efc55aa42759793d853ecdfa6bb034419d22884e3e9871f0f593ac8d" +checksum = "afaa14ddcf4553e700608c1c0ee3ca1f4cf673470462b99ff6dd6bedcdb6c6ce" [[package]] name = "capnp-futures" @@ -2630,11 +2630,8 @@ dependencies = [ name = "keyring-manager" version = "0.5.0" dependencies = [ - "android_logger", - "backtrace", "byteorder", "cfg-if 1.0.0", - "clap", "core-foundation 0.9.3", "core-foundation-sys 0.8.3", "directories", @@ -2643,18 +2640,14 @@ dependencies = [ "keychain-services", "lazy_static", "log", - "ndk 0.6.0", + "ndk", "ndk-glue", - "rpassword 5.0.1", "secret-service", "security-framework", "security-framework-sys", "serde", "serde_cbor", - "serial_test", - "simplelog 0.12.0", "snailquote", - "tempfile", "winapi 0.3.9", ] @@ -3019,19 +3012,6 @@ dependencies = [ "socket2", ] -[[package]] -name = "ndk" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" -dependencies = [ - "bitflags", - "jni-sys", - "ndk-sys 0.3.0", - "num_enum", - "thiserror", -] - [[package]] name = "ndk" version = "0.7.0" @@ -3040,7 +3020,7 @@ checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" dependencies = [ "bitflags", "jni-sys", - "ndk-sys 0.4.1+23.1.7779620", + "ndk-sys", "num_enum", "raw-window-handle", "thiserror", @@ -3061,10 +3041,10 @@ dependencies = [ "android_logger", "libc", "log", - "ndk 0.7.0", + "ndk", "ndk-context", "ndk-macro", - "ndk-sys 0.4.1+23.1.7779620", + "ndk-sys", "once_cell", "parking_lot 0.12.1", ] @@ -3082,15 +3062,6 @@ dependencies = [ "syn", ] -[[package]] -name = "ndk-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" -dependencies = [ - "jni-sys", -] - [[package]] name = "ndk-sys" version = "0.4.1+23.1.7779620" @@ -4239,16 +4210,6 @@ dependencies = [ "serde", ] -[[package]] -name = "rpassword" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb" -dependencies = [ - "libc", - "winapi 0.3.9", -] - [[package]] name = "rpassword" version = "6.0.1" @@ -5204,9 +5165,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.8.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", "quote", @@ -5787,7 +5748,7 @@ dependencies = [ "lazy_static", "libc", "maplit", - "ndk 0.6.0", + "ndk", "ndk-glue", "nix 0.26.1", "once_cell", @@ -5895,7 +5856,7 @@ dependencies = [ "opentelemetry-otlp", "opentelemetry-semantic-conventions", "parking_lot 0.12.1", - "rpassword 6.0.1", + "rpassword", "serde", "serde_derive", "serde_yaml", @@ -5935,7 +5896,7 @@ dependencies = [ "libc", "log", "maplit", - "ndk 0.6.0", + "ndk", "ndk-glue", "nix 0.26.1", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index 6ea5de42..27fc702e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ "veilid-wasm", ] -exclude = [ "./external/keyring-rs", "./external/netlink", "./external/cursive", "./external/hashlink" ] +exclude = [ "./external/keyring-manager", "./external/netlink", "./external/cursive", "./external/hashlink" ] [patch.crates-io] cursive = { path = "./external/cursive/cursive" } diff --git a/external/keyring-manager b/external/keyring-manager index 1655f89c..b127b2d3 160000 --- a/external/keyring-manager +++ b/external/keyring-manager @@ -1 +1 @@ -Subproject commit 1655f89cf2ec70900c520080819d76ffad90adee +Subproject commit b127b2d3c653fea163a776dd58b3798f28aeeee3 diff --git a/scripts/earthly/cargo-android/config.toml b/scripts/earthly/cargo-android/config.toml index 88522ce8..e2552b9f 100644 --- a/scripts/earthly/cargo-android/config.toml +++ b/scripts/earthly/cargo-android/config.toml @@ -1,8 +1,8 @@ [target.aarch64-linux-android] -linker = "/Android/Sdk/ndk/22.0.7026061/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang" +linker = "/Android/Sdk/ndk/25.1.8937393/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android33-clang" [target.armv7-linux-androideabi] -linker = "/Android/Sdk/ndk/22.0.7026061/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi30-clang" +linker = "/Android/Sdk/ndk/25.1.8937393/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi33-clang" [target.x86_64-linux-android] -linker = "/Android/Sdk/ndk/22.0.7026061/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android30-clang" +linker = "/Android/Sdk/ndk/25.1.8937393/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android33-clang" [target.i686-linux-android] -linker = "/Android/Sdk/ndk/22.0.7026061/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android30-clang" \ No newline at end of file +linker = "/Android/Sdk/ndk/25.1.8937393/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android33-clang" \ No newline at end of file diff --git a/veilid-core/Cargo.toml b/veilid-core/Cargo.toml index 9a239431..ef064a52 100644 --- a/veilid-core/Cargo.toml +++ b/veilid-core/Cargo.toml @@ -14,7 +14,7 @@ default = [] rt-async-std = [ "async-std", "async-std-resolver", "async_executors/async_std", "rtnetlink?/smol_socket", "veilid-tools/rt-async-std" ] rt-tokio = [ "tokio", "tokio-util", "tokio-stream", "trust-dns-resolver/tokio-runtime", "async_executors/tokio_tp", "async_executors/tokio_io", "async_executors/tokio_timer", "rtnetlink?/tokio_socket", "veilid-tools/rt-tokio" ] -keyring_manager_android_tests = [] +veilid_core_android_tests = [] veilid_core_ios_tests = [ "simplelog" ] tracking = [] @@ -128,8 +128,8 @@ features = [ [target.'cfg(target_os = "android")'.dependencies] jni = "^0" jni-sys = "^0" -ndk = { version = "^0", features = ["trace"] } -ndk-glue = { version = "^0", features = ["logger"] } +ndk = { version = "^0.7" } +ndk-glue = { version = "^0.7", features = ["logger"] } tracing-android = { version = "^0" } # Dependenices for all Unix (Linux, Android, MacOS, iOS) diff --git a/veilid-core/src/tests/android/.idea/.name b/veilid-core/src/tests/android/.idea/.name index 49552fb2..0a5c13c4 100644 --- a/veilid-core/src/tests/android/.idea/.name +++ b/veilid-core/src/tests/android/.idea/.name @@ -1 +1 @@ -VeilidCore Tests \ No newline at end of file +Veilid-Core Tests \ No newline at end of file diff --git a/veilid-core/src/tests/android/app/build.gradle b/veilid-core/src/tests/android/app/build.gradle index 9dadf0aa..f4069114 100644 --- a/veilid-core/src/tests/android/app/build.gradle +++ b/veilid-core/src/tests/android/app/build.gradle @@ -4,7 +4,7 @@ plugins { android { compileSdkVersion 33 - buildToolsVersion "30.0.3" + buildToolsVersion "33.0.1" defaultConfig { applicationId "com.veilid.veilid_core_android_tests" @@ -43,6 +43,7 @@ android { // Required to copy libc++_shared.so externalNativeBuild { cmake { + version '3.22.1' path file('CMakeLists.txt') } } @@ -59,7 +60,7 @@ dependencies { apply plugin: 'org.mozilla.rust-android-gradle.rust-android' cargo { - module = "../../../../../veilid-core" + module = "../../../../" libname = "veilid_core" targets = ["arm", "arm64", "x86", "x86_64"] targetDirectory = "../../../../../target" diff --git a/veilid-core/src/tests/android/app/src/main/AndroidManifest.xml b/veilid-core/src/tests/android/app/src/main/AndroidManifest.xml index 14b07cbe..c7052ecf 100644 --- a/veilid-core/src/tests/android/app/src/main/AndroidManifest.xml +++ b/veilid-core/src/tests/android/app/src/main/AndroidManifest.xml @@ -12,7 +12,8 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.VeilidCoreTests"> - + diff --git a/veilid-core/src/tests/android/build.gradle b/veilid-core/src/tests/android/build.gradle index 2e8d8f04..ffb3ba0e 100644 --- a/veilid-core/src/tests/android/build.gradle +++ b/veilid-core/src/tests/android/build.gradle @@ -13,7 +13,7 @@ buildscript { } plugins { - id "org.mozilla.rust-android-gradle.rust-android" version "0.9.0" + id "org.mozilla.rust-android-gradle.rust-android" version "0.9.3" } allprojects { diff --git a/veilid-core/src/tests/android/settings.gradle b/veilid-core/src/tests/android/settings.gradle index bd623c72..24554dcf 100644 --- a/veilid-core/src/tests/android/settings.gradle +++ b/veilid-core/src/tests/android/settings.gradle @@ -1,2 +1,2 @@ include ':app' -rootProject.name = "VeilidCore Tests" \ No newline at end of file +rootProject.name = "Veilid-Core Tests" \ No newline at end of file diff --git a/veilid-tools/Cargo.toml b/veilid-tools/Cargo.toml index 2adf291b..e426d861 100644 --- a/veilid-tools/Cargo.toml +++ b/veilid-tools/Cargo.toml @@ -59,8 +59,8 @@ send_wrapper = { version = "^0.6", features = ["futures"] } [target.'cfg(target_os = "android")'.dependencies] jni = "^0" jni-sys = "^0" -ndk = { version = "^0", features = ["trace"] } -ndk-glue = { version = "^0", features = ["logger"] } +ndk = { version = "^0.7" } +ndk-glue = { version = "^0.7", features = ["logger"] } lazy_static = "^1.4.0" tracing-android = { version = "^0", optional = true } android-logd-logger = "0.2.1" From 89dd5da52221f10a6befa1237dc93c3186f228ab Mon Sep 17 00:00:00 2001 From: John Smith Date: Wed, 30 Nov 2022 21:32:41 -0500 Subject: [PATCH 24/88] more test work --- veilid-core/run_tests.sh | 2 +- veilid-core/src/intf/native/android/mod.rs | 64 +------------ veilid-core/src/intf/native/ios/mod.rs | 87 ------------------ veilid-core/src/intf/native/mod.rs | 2 - veilid-core/src/lib.rs | 2 +- veilid-core/src/tests/android/mod.rs | 58 ++++++++++++ .../.gitignore | 0 .../.idea/.gitignore | 0 .../.idea/.name | 0 .../.idea/compiler.xml | 0 .../.idea/gradle.xml | 0 .../.idea/jarRepositories.xml | 0 .../.idea/misc.xml | 0 .../.idea/vcs.xml | 0 .../{ => veilid_core_android_tests}/.project | 0 .../org.eclipse.buildship.core.prefs | 0 .../{ => veilid_core_android_tests}/adb+.sh | 0 .../app/.classpath | 0 .../app/.gitignore | 0 .../app/.project | 0 .../org.eclipse.buildship.core.prefs | 0 .../app/CMakeLists.txt | 0 .../app/build.gradle | 4 +- .../app/cpplink.cpp | 0 .../app/proguard-rules.pro | 0 .../app/src/main/AndroidManifest.xml | 0 .../MainActivity.java | 0 .../drawable-v24/ic_launcher_foreground.xml | 0 .../res/drawable/ic_launcher_background.xml | 0 .../app/src/main/res/layout/activity_main.xml | 0 .../res/mipmap-anydpi-v26/ic_launcher.xml | 0 .../mipmap-anydpi-v26/ic_launcher_round.xml | 0 .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin .../res/mipmap-hdpi/ic_launcher_round.png | Bin .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin .../res/mipmap-mdpi/ic_launcher_round.png | Bin .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin .../res/mipmap-xhdpi/ic_launcher_round.png | Bin .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin .../app/src/main/res/values-night/themes.xml | 0 .../app/src/main/res/values/colors.xml | 0 .../app/src/main/res/values/strings.xml | 0 .../app/src/main/res/values/themes.xml | 0 .../build.gradle | 0 .../gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.jar | Bin .../gradle/wrapper/gradle-wrapper.properties | 0 .../{ => veilid_core_android_tests}/gradlew | 0 .../gradlew.bat | 0 .../install_on_all_devices.sh | 0 .../remove_from_all_devices.sh | 0 .../settings.gradle | 0 veilid-core/src/tests/ios/mod.rs | 45 +++++++++ veilid-core/src/tests/mod.rs | 4 +- veilid-core/src/tests/native/mod.rs | 56 +++-------- veilid-core/webdriver.json | 15 +++ veilid-tools/run_tests.sh | 10 +- veilid-tools/src/tests/android/mod.rs | 59 +++--------- veilid-tools/src/tests/ios/mod.rs | 13 ++- veilid-tools/src/tests/mod.rs | 4 +- veilid-tools/src/tests/native/mod.rs | 32 +------ veilid-tools/tests/web.rs | 1 + 65 files changed, 175 insertions(+), 283 deletions(-) delete mode 100644 veilid-core/src/intf/native/ios/mod.rs create mode 100644 veilid-core/src/tests/android/mod.rs rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/.gitignore (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/.idea/.gitignore (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/.idea/.name (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/.idea/compiler.xml (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/.idea/gradle.xml (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/.idea/jarRepositories.xml (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/.idea/misc.xml (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/.idea/vcs.xml (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/.project (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/.settings/org.eclipse.buildship.core.prefs (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/adb+.sh (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/.classpath (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/.gitignore (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/.project (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/.settings/org.eclipse.buildship.core.prefs (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/CMakeLists.txt (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/build.gradle (96%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/cpplink.cpp (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/proguard-rules.pro (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/AndroidManifest.xml (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/java/com/veilid/veilid_core_android_tests/MainActivity.java (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/res/drawable-v24/ic_launcher_foreground.xml (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/res/drawable/ic_launcher_background.xml (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/res/layout/activity_main.xml (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/res/mipmap-hdpi/ic_launcher.png (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/res/mipmap-hdpi/ic_launcher_round.png (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/res/mipmap-mdpi/ic_launcher.png (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/res/mipmap-mdpi/ic_launcher_round.png (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/res/mipmap-xhdpi/ic_launcher.png (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/res/values-night/themes.xml (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/res/values/colors.xml (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/res/values/strings.xml (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/app/src/main/res/values/themes.xml (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/build.gradle (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/gradle.properties (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/gradle/wrapper/gradle-wrapper.jar (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/gradle/wrapper/gradle-wrapper.properties (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/gradlew (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/gradlew.bat (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/install_on_all_devices.sh (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/remove_from_all_devices.sh (100%) rename veilid-core/src/tests/android/{ => veilid_core_android_tests}/settings.gradle (100%) create mode 100644 veilid-core/src/tests/ios/mod.rs create mode 100644 veilid-core/webdriver.json diff --git a/veilid-core/run_tests.sh b/veilid-core/run_tests.sh index deed7641..7334bec4 100755 --- a/veilid-core/run_tests.sh +++ b/veilid-core/run_tests.sh @@ -3,7 +3,7 @@ SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" pushd $SCRIPTDIR 2>/dev/null if [[ "$1" == "wasm" ]]; then - WASM_BINDGEN_TEST_TIMEOUT=120 wasm-pack test --chrome --headless + WASM_BINDGEN_TEST_TIMEOUT=120 wasm-pack test --firefox --headless elif [[ "$1" == "ios" ]]; then SYMROOT=/tmp/testout APPNAME=veilidcore-tests diff --git a/veilid-core/src/intf/native/android/mod.rs b/veilid-core/src/intf/native/android/mod.rs index 424efe7f..c6991235 100644 --- a/veilid-core/src/intf/native/android/mod.rs +++ b/veilid-core/src/intf/native/android/mod.rs @@ -1,20 +1,10 @@ -// xxx : support for android older than API 24, if we need it someday -//mod android_get_if_addrs; -//pub use android_get_if_addrs::*; - mod get_directories; pub use get_directories::*; -use crate::veilid_config::VeilidConfigLogLevel; use crate::*; -use backtrace::Backtrace; use jni::errors::Result as JniResult; -use jni::{objects::GlobalRef, objects::JObject, objects::JString, JNIEnv, JavaVM}; +use jni::{objects::GlobalRef, objects::JObject, JNIEnv, JavaVM}; use lazy_static::*; -use std::panic; -use tracing::*; -use tracing_subscriber::prelude::*; -use tracing_subscriber::*; pub struct AndroidGlobals { pub vm: JavaVM, @@ -32,63 +22,13 @@ lazy_static! { pub static ref ANDROID_GLOBALS: Arc>> = Arc::new(Mutex::new(None)); } -pub fn veilid_core_setup_android_no_log<'a>(env: JNIEnv<'a>, ctx: JObject<'a>) { +pub fn veilid_core_setup_android(env: JNIEnv, ctx: JObject) { *ANDROID_GLOBALS.lock() = Some(AndroidGlobals { vm: env.get_java_vm().unwrap(), ctx: env.new_global_ref(ctx).unwrap(), }); } -pub fn veilid_core_setup_android<'a>( - env: JNIEnv<'a>, - ctx: JObject<'a>, - log_tag: &'a str, - log_level: VeilidConfigLogLevel, -) { - cfg_if! { - if #[cfg(feature = "tracing")] { - // Set up subscriber and layers - let subscriber = Registry::default(); - let mut layers = Vec::new(); - let filter = VeilidLayerFilter::new(log_level, None); - let layer = tracing_android::layer(log_tag) - .expect("failed to set up android logging") - .with_filter(filter.clone()); - layers.push(layer.boxed()); - - let subscriber = subscriber.with(layers); - subscriber - .try_init() - .expect("failed to init android tracing"); - } - } - // Set up panic hook for backtraces - panic::set_hook(Box::new(|panic_info| { - let bt = Backtrace::new(); - if let Some(location) = panic_info.location() { - error!( - "panic occurred in file '{}' at line {}", - location.file(), - location.line(), - ); - } else { - error!("panic occurred but can't get location information..."); - } - if let Some(s) = panic_info.payload().downcast_ref::<&str>() { - error!("panic payload: {:?}", s); - } else if let Some(s) = panic_info.payload().downcast_ref::() { - error!("panic payload: {:?}", s); - } else if let Some(a) = panic_info.payload().downcast_ref::() { - error!("panic payload: {:?}", a); - } else { - error!("no panic payload"); - } - error!("Backtrace:\n{:?}", bt); - })); - - veilid_core_setup_android_no_log(env, ctx); -} - pub fn get_android_globals() -> (JavaVM, GlobalRef) { let globals_locked = ANDROID_GLOBALS.lock(); let globals = globals_locked.as_ref().unwrap(); diff --git a/veilid-core/src/intf/native/ios/mod.rs b/veilid-core/src/intf/native/ios/mod.rs deleted file mode 100644 index cf6a6465..00000000 --- a/veilid-core/src/intf/native/ios/mod.rs +++ /dev/null @@ -1,87 +0,0 @@ -use backtrace::Backtrace; -use log::*; -use simplelog::*; -use std::fs::OpenOptions; -use std::panic; -use std::path::{Path, PathBuf}; - -pub fn veilid_core_setup<'a>( - log_tag: &'a str, - terminal_log: Option, - file_log: Option<(Level, &Path)>, -) { - if let Err(e) = veilid_core_setup_internal(log_tag, terminal_log, file_log) { - panic!("failed to set up veilid-core: {}", e); - } -} - -fn veilid_core_setup_internal<'a>( - _log_tag: &'a str, - terminal_log: Option, - file_log: Option<(Level, &Path)>, -) -> Result<(), String> { - let mut logs: Vec> = Vec::new(); - - let mut cb = ConfigBuilder::new(); - for ig in veilid_core::DEFAULT_LOG_IGNORE_LIST { - cb.add_filter_ignore_str(ig); - } - - if let Some(level) = terminal_log { - logs.push(TermLogger::new( - level.to_level_filter(), - cb.build(), - TerminalMode::Mixed, - ColorChoice::Auto, - )) - } - if let Some((level, log_path)) = file_log { - let logfile = OpenOptions::new() - .truncate(true) - .create(true) - .write(true) - .open(log_path) - .map_err(|e| { - format!( - "log open error: {} path={:?} all_dirs={:?}", - e, - log_path, - std::fs::read_dir(std::env::var("HOME").unwrap()) - .unwrap() - .map(|d| d.unwrap().path()) - .collect::>() - ) - })?; - logs.push(WriteLogger::new( - level.to_level_filter(), - cb.build(), - logfile, - )) - } - CombinedLogger::init(logs).map_err(|e| format!("logger init error: {}", e))?; - - panic::set_hook(Box::new(|panic_info| { - let bt = Backtrace::new(); - if let Some(location) = panic_info.location() { - error!( - "panic occurred in file '{}' at line {}", - location.file(), - location.line(), - ); - } else { - error!("panic occurred but can't get location information..."); - } - if let Some(s) = panic_info.payload().downcast_ref::<&str>() { - error!("panic payload: {:?}", s); - } else if let Some(s) = panic_info.payload().downcast_ref::() { - error!("panic payload: {:?}", s); - } else if let Some(a) = panic_info.payload().downcast_ref::() { - error!("panic payload: {:?}", a); - } else { - error!("no panic payload"); - } - error!("Backtrace:\n{:?}", bt); - })); - - Ok(()) -} diff --git a/veilid-core/src/intf/native/mod.rs b/veilid-core/src/intf/native/mod.rs index 8f7e39bf..786b2dd1 100644 --- a/veilid-core/src/intf/native/mod.rs +++ b/veilid-core/src/intf/native/mod.rs @@ -10,6 +10,4 @@ pub use table_store::*; #[cfg(target_os = "android")] pub mod android; -#[cfg(all(target_os = "ios", feature = "veilid_core_ios_tests"))] -pub mod ios_test_setup; pub mod network_interfaces; diff --git a/veilid-core/src/lib.rs b/veilid-core/src/lib.rs index d8379b94..8d6e28ee 100644 --- a/veilid-core/src/lib.rs +++ b/veilid-core/src/lib.rs @@ -59,7 +59,7 @@ pub fn veilid_version() -> (u32, u32, u32) { } #[cfg(target_os = "android")] -pub use intf::utils::android::{veilid_core_setup_android, veilid_core_setup_android_no_log}; +pub use intf::utils::android::veilid_core_setup_android; pub static DEFAULT_LOG_IGNORE_LIST: [&str; 21] = [ "mio", diff --git a/veilid-core/src/tests/android/mod.rs b/veilid-core/src/tests/android/mod.rs new file mode 100644 index 00000000..96b79aeb --- /dev/null +++ b/veilid-core/src/tests/android/mod.rs @@ -0,0 +1,58 @@ +use crate::*; +use backtrace::Backtrace; +use jni::{ + objects::GlobalRef, objects::JClass, objects::JObject, objects::JString, JNIEnv, JavaVM, +}; +use lazy_static::*; +use std::panic; +use tracing::*; +use tracing_subscriber::prelude::*; +use tracing_subscriber::*; + +#[no_mangle] +#[allow(non_snake_case)] +pub extern "system" fn Java_com_veilid_veilid_1core_1android_1tests_MainActivity_run_1tests( + env: JNIEnv, + _class: JClass, + ctx: JObject, +) { + crate::intf::utils::android::veilid_core_setup_android_tests(env, ctx); + run_all_tests(); +} + +pub fn veilid_core_setup_android_tests(env: JNIEnv, ctx: JObject) { + // Set up subscriber and layers + use tracing_subscriber::{filter, fmt, prelude::*}; + let filter = VeilidLayerFilter::new(VeilidConfigLogLevel::Trace, None); + let layer = tracing_android::layer("veilid-core").expect("failed to set up android logging"); + tracing_subscriber::registry() + .with(filters) + .with(fmt_layer) + .init(); + + // Set up panic hook for backtraces + panic::set_hook(Box::new(|panic_info| { + let bt = Backtrace::new(); + if let Some(location) = panic_info.location() { + error!( + "panic occurred in file '{}' at line {}", + location.file(), + location.line(), + ); + } else { + error!("panic occurred but can't get location information..."); + } + if let Some(s) = panic_info.payload().downcast_ref::<&str>() { + error!("panic payload: {:?}", s); + } else if let Some(s) = panic_info.payload().downcast_ref::() { + error!("panic payload: {:?}", s); + } else if let Some(a) = panic_info.payload().downcast_ref::() { + error!("panic payload: {:?}", a); + } else { + error!("no panic payload"); + } + error!("Backtrace:\n{:?}", bt); + })); + + veilid_core_setup_android(env, ctx); +} diff --git a/veilid-core/src/tests/android/.gitignore b/veilid-core/src/tests/android/veilid_core_android_tests/.gitignore similarity index 100% rename from veilid-core/src/tests/android/.gitignore rename to veilid-core/src/tests/android/veilid_core_android_tests/.gitignore diff --git a/veilid-core/src/tests/android/.idea/.gitignore b/veilid-core/src/tests/android/veilid_core_android_tests/.idea/.gitignore similarity index 100% rename from veilid-core/src/tests/android/.idea/.gitignore rename to veilid-core/src/tests/android/veilid_core_android_tests/.idea/.gitignore diff --git a/veilid-core/src/tests/android/.idea/.name b/veilid-core/src/tests/android/veilid_core_android_tests/.idea/.name similarity index 100% rename from veilid-core/src/tests/android/.idea/.name rename to veilid-core/src/tests/android/veilid_core_android_tests/.idea/.name diff --git a/veilid-core/src/tests/android/.idea/compiler.xml b/veilid-core/src/tests/android/veilid_core_android_tests/.idea/compiler.xml similarity index 100% rename from veilid-core/src/tests/android/.idea/compiler.xml rename to veilid-core/src/tests/android/veilid_core_android_tests/.idea/compiler.xml diff --git a/veilid-core/src/tests/android/.idea/gradle.xml b/veilid-core/src/tests/android/veilid_core_android_tests/.idea/gradle.xml similarity index 100% rename from veilid-core/src/tests/android/.idea/gradle.xml rename to veilid-core/src/tests/android/veilid_core_android_tests/.idea/gradle.xml diff --git a/veilid-core/src/tests/android/.idea/jarRepositories.xml b/veilid-core/src/tests/android/veilid_core_android_tests/.idea/jarRepositories.xml similarity index 100% rename from veilid-core/src/tests/android/.idea/jarRepositories.xml rename to veilid-core/src/tests/android/veilid_core_android_tests/.idea/jarRepositories.xml diff --git a/veilid-core/src/tests/android/.idea/misc.xml b/veilid-core/src/tests/android/veilid_core_android_tests/.idea/misc.xml similarity index 100% rename from veilid-core/src/tests/android/.idea/misc.xml rename to veilid-core/src/tests/android/veilid_core_android_tests/.idea/misc.xml diff --git a/veilid-core/src/tests/android/.idea/vcs.xml b/veilid-core/src/tests/android/veilid_core_android_tests/.idea/vcs.xml similarity index 100% rename from veilid-core/src/tests/android/.idea/vcs.xml rename to veilid-core/src/tests/android/veilid_core_android_tests/.idea/vcs.xml diff --git a/veilid-core/src/tests/android/.project b/veilid-core/src/tests/android/veilid_core_android_tests/.project similarity index 100% rename from veilid-core/src/tests/android/.project rename to veilid-core/src/tests/android/veilid_core_android_tests/.project diff --git a/veilid-core/src/tests/android/.settings/org.eclipse.buildship.core.prefs b/veilid-core/src/tests/android/veilid_core_android_tests/.settings/org.eclipse.buildship.core.prefs similarity index 100% rename from veilid-core/src/tests/android/.settings/org.eclipse.buildship.core.prefs rename to veilid-core/src/tests/android/veilid_core_android_tests/.settings/org.eclipse.buildship.core.prefs diff --git a/veilid-core/src/tests/android/adb+.sh b/veilid-core/src/tests/android/veilid_core_android_tests/adb+.sh similarity index 100% rename from veilid-core/src/tests/android/adb+.sh rename to veilid-core/src/tests/android/veilid_core_android_tests/adb+.sh diff --git a/veilid-core/src/tests/android/app/.classpath b/veilid-core/src/tests/android/veilid_core_android_tests/app/.classpath similarity index 100% rename from veilid-core/src/tests/android/app/.classpath rename to veilid-core/src/tests/android/veilid_core_android_tests/app/.classpath diff --git a/veilid-core/src/tests/android/app/.gitignore b/veilid-core/src/tests/android/veilid_core_android_tests/app/.gitignore similarity index 100% rename from veilid-core/src/tests/android/app/.gitignore rename to veilid-core/src/tests/android/veilid_core_android_tests/app/.gitignore diff --git a/veilid-core/src/tests/android/app/.project b/veilid-core/src/tests/android/veilid_core_android_tests/app/.project similarity index 100% rename from veilid-core/src/tests/android/app/.project rename to veilid-core/src/tests/android/veilid_core_android_tests/app/.project diff --git a/veilid-core/src/tests/android/app/.settings/org.eclipse.buildship.core.prefs b/veilid-core/src/tests/android/veilid_core_android_tests/app/.settings/org.eclipse.buildship.core.prefs similarity index 100% rename from veilid-core/src/tests/android/app/.settings/org.eclipse.buildship.core.prefs rename to veilid-core/src/tests/android/veilid_core_android_tests/app/.settings/org.eclipse.buildship.core.prefs diff --git a/veilid-core/src/tests/android/app/CMakeLists.txt b/veilid-core/src/tests/android/veilid_core_android_tests/app/CMakeLists.txt similarity index 100% rename from veilid-core/src/tests/android/app/CMakeLists.txt rename to veilid-core/src/tests/android/veilid_core_android_tests/app/CMakeLists.txt diff --git a/veilid-core/src/tests/android/app/build.gradle b/veilid-core/src/tests/android/veilid_core_android_tests/app/build.gradle similarity index 96% rename from veilid-core/src/tests/android/app/build.gradle rename to veilid-core/src/tests/android/veilid_core_android_tests/app/build.gradle index f4069114..af4abe42 100644 --- a/veilid-core/src/tests/android/app/build.gradle +++ b/veilid-core/src/tests/android/veilid_core_android_tests/app/build.gradle @@ -60,10 +60,10 @@ dependencies { apply plugin: 'org.mozilla.rust-android-gradle.rust-android' cargo { - module = "../../../../" + module = "../../../../../" libname = "veilid_core" targets = ["arm", "arm64", "x86", "x86_64"] - targetDirectory = "../../../../../target" + targetDirectory = "../../../../../../target" prebuiltToolchains = true profile = gradle.startParameter.taskNames.any{it.toLowerCase().contains("debug")} ? "debug" : "release" pythonCommand = "python3" diff --git a/veilid-core/src/tests/android/app/cpplink.cpp b/veilid-core/src/tests/android/veilid_core_android_tests/app/cpplink.cpp similarity index 100% rename from veilid-core/src/tests/android/app/cpplink.cpp rename to veilid-core/src/tests/android/veilid_core_android_tests/app/cpplink.cpp diff --git a/veilid-core/src/tests/android/app/proguard-rules.pro b/veilid-core/src/tests/android/veilid_core_android_tests/app/proguard-rules.pro similarity index 100% rename from veilid-core/src/tests/android/app/proguard-rules.pro rename to veilid-core/src/tests/android/veilid_core_android_tests/app/proguard-rules.pro diff --git a/veilid-core/src/tests/android/app/src/main/AndroidManifest.xml b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/AndroidManifest.xml similarity index 100% rename from veilid-core/src/tests/android/app/src/main/AndroidManifest.xml rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/AndroidManifest.xml diff --git a/veilid-core/src/tests/android/app/src/main/java/com/veilid/veilid_core_android_tests/MainActivity.java b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/java/com/veilid/veilid_core_android_tests/MainActivity.java similarity index 100% rename from veilid-core/src/tests/android/app/src/main/java/com/veilid/veilid_core_android_tests/MainActivity.java rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/java/com/veilid/veilid_core_android_tests/MainActivity.java diff --git a/veilid-core/src/tests/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/drawable-v24/ic_launcher_foreground.xml similarity index 100% rename from veilid-core/src/tests/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/drawable-v24/ic_launcher_foreground.xml diff --git a/veilid-core/src/tests/android/app/src/main/res/drawable/ic_launcher_background.xml b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from veilid-core/src/tests/android/app/src/main/res/drawable/ic_launcher_background.xml rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/drawable/ic_launcher_background.xml diff --git a/veilid-core/src/tests/android/app/src/main/res/layout/activity_main.xml b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/layout/activity_main.xml similarity index 100% rename from veilid-core/src/tests/android/app/src/main/res/layout/activity_main.xml rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/layout/activity_main.xml diff --git a/veilid-core/src/tests/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from veilid-core/src/tests/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/veilid-core/src/tests/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from veilid-core/src/tests/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/veilid-core/src/tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from veilid-core/src/tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/veilid-core/src/tests/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from veilid-core/src/tests/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-hdpi/ic_launcher_round.png diff --git a/veilid-core/src/tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from veilid-core/src/tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/veilid-core/src/tests/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from veilid-core/src/tests/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-mdpi/ic_launcher_round.png diff --git a/veilid-core/src/tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from veilid-core/src/tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/veilid-core/src/tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from veilid-core/src/tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/veilid-core/src/tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from veilid-core/src/tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/veilid-core/src/tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from veilid-core/src/tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/veilid-core/src/tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from veilid-core/src/tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/veilid-core/src/tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from veilid-core/src/tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/veilid-core/src/tests/android/app/src/main/res/values-night/themes.xml b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/values-night/themes.xml similarity index 100% rename from veilid-core/src/tests/android/app/src/main/res/values-night/themes.xml rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/values-night/themes.xml diff --git a/veilid-core/src/tests/android/app/src/main/res/values/colors.xml b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/values/colors.xml similarity index 100% rename from veilid-core/src/tests/android/app/src/main/res/values/colors.xml rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/values/colors.xml diff --git a/veilid-core/src/tests/android/app/src/main/res/values/strings.xml b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/values/strings.xml similarity index 100% rename from veilid-core/src/tests/android/app/src/main/res/values/strings.xml rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/values/strings.xml diff --git a/veilid-core/src/tests/android/app/src/main/res/values/themes.xml b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/values/themes.xml similarity index 100% rename from veilid-core/src/tests/android/app/src/main/res/values/themes.xml rename to veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/res/values/themes.xml diff --git a/veilid-core/src/tests/android/build.gradle b/veilid-core/src/tests/android/veilid_core_android_tests/build.gradle similarity index 100% rename from veilid-core/src/tests/android/build.gradle rename to veilid-core/src/tests/android/veilid_core_android_tests/build.gradle diff --git a/veilid-core/src/tests/android/gradle.properties b/veilid-core/src/tests/android/veilid_core_android_tests/gradle.properties similarity index 100% rename from veilid-core/src/tests/android/gradle.properties rename to veilid-core/src/tests/android/veilid_core_android_tests/gradle.properties diff --git a/veilid-core/src/tests/android/gradle/wrapper/gradle-wrapper.jar b/veilid-core/src/tests/android/veilid_core_android_tests/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from veilid-core/src/tests/android/gradle/wrapper/gradle-wrapper.jar rename to veilid-core/src/tests/android/veilid_core_android_tests/gradle/wrapper/gradle-wrapper.jar diff --git a/veilid-core/src/tests/android/gradle/wrapper/gradle-wrapper.properties b/veilid-core/src/tests/android/veilid_core_android_tests/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from veilid-core/src/tests/android/gradle/wrapper/gradle-wrapper.properties rename to veilid-core/src/tests/android/veilid_core_android_tests/gradle/wrapper/gradle-wrapper.properties diff --git a/veilid-core/src/tests/android/gradlew b/veilid-core/src/tests/android/veilid_core_android_tests/gradlew similarity index 100% rename from veilid-core/src/tests/android/gradlew rename to veilid-core/src/tests/android/veilid_core_android_tests/gradlew diff --git a/veilid-core/src/tests/android/gradlew.bat b/veilid-core/src/tests/android/veilid_core_android_tests/gradlew.bat similarity index 100% rename from veilid-core/src/tests/android/gradlew.bat rename to veilid-core/src/tests/android/veilid_core_android_tests/gradlew.bat diff --git a/veilid-core/src/tests/android/install_on_all_devices.sh b/veilid-core/src/tests/android/veilid_core_android_tests/install_on_all_devices.sh similarity index 100% rename from veilid-core/src/tests/android/install_on_all_devices.sh rename to veilid-core/src/tests/android/veilid_core_android_tests/install_on_all_devices.sh diff --git a/veilid-core/src/tests/android/remove_from_all_devices.sh b/veilid-core/src/tests/android/veilid_core_android_tests/remove_from_all_devices.sh similarity index 100% rename from veilid-core/src/tests/android/remove_from_all_devices.sh rename to veilid-core/src/tests/android/veilid_core_android_tests/remove_from_all_devices.sh diff --git a/veilid-core/src/tests/android/settings.gradle b/veilid-core/src/tests/android/veilid_core_android_tests/settings.gradle similarity index 100% rename from veilid-core/src/tests/android/settings.gradle rename to veilid-core/src/tests/android/veilid_core_android_tests/settings.gradle diff --git a/veilid-core/src/tests/ios/mod.rs b/veilid-core/src/tests/ios/mod.rs new file mode 100644 index 00000000..bbbe7a59 --- /dev/null +++ b/veilid-core/src/tests/ios/mod.rs @@ -0,0 +1,45 @@ +use crate::*; +use backtrace::Backtrace; +use std::panic; + +#[no_mangle] +#[allow(dead_code)] +pub extern "C" fn run_veilid_core_tests() { + veilid_core_setup_ios_tests(); + run_all_tests(); +} + +pub fn veilid_core_setup_ios_tests() { + // Set up subscriber and layers + use tracing_subscriber::{filter, fmt, prelude::*}; + let filter = VeilidLayerFilter::new(VeilidConfigLogLevel::Trace, None); + let fmt_layer = fmt::layer(); + let layer = tracing_android::layer("veilid-core").expect("failed to set up android logging"); + tracing_subscriber::registry() + .with(filters) + .with(fmt_layer) + .init(); + + panic::set_hook(Box::new(|panic_info| { + let bt = Backtrace::new(); + if let Some(location) = panic_info.location() { + error!( + "panic occurred in file '{}' at line {}", + location.file(), + location.line(), + ); + } else { + error!("panic occurred but can't get location information..."); + } + if let Some(s) = panic_info.payload().downcast_ref::<&str>() { + error!("panic payload: {:?}", s); + } else if let Some(s) = panic_info.payload().downcast_ref::() { + error!("panic payload: {:?}", s); + } else if let Some(a) = panic_info.payload().downcast_ref::() { + error!("panic payload: {:?}", a); + } else { + error!("no panic payload"); + } + error!("Backtrace:\n{:?}", bt); + })); +} diff --git a/veilid-core/src/tests/mod.rs b/veilid-core/src/tests/mod.rs index 61f04f89..2a050ac5 100644 --- a/veilid-core/src/tests/mod.rs +++ b/veilid-core/src/tests/mod.rs @@ -1,7 +1,7 @@ -#[cfg(target_os = "android")] +#[cfg(all(target_os = "android", feature = "veilid_core_android_tests"))] mod android; pub mod common; -#[cfg(target_os = "ios")] +#[cfg(all(target_os = "ios", feature = "veilid_core_ios_tests"))] mod ios; #[cfg(not(target_arch = "wasm32"))] mod native; diff --git a/veilid-core/src/tests/native/mod.rs b/veilid-core/src/tests/native/mod.rs index b1f96995..04431400 100644 --- a/veilid-core/src/tests/native/mod.rs +++ b/veilid-core/src/tests/native/mod.rs @@ -5,44 +5,6 @@ use crate::network_manager::tests::*; use crate::tests::common::*; use crate::*; -#[cfg(all(target_os = "android", feature = "veilid_core_android_tests"))] -use jni::{objects::JClass, objects::JObject, JNIEnv}; - -#[cfg(all(target_os = "android", feature = "veilid_core_android_tests"))] -#[no_mangle] -#[allow(non_snake_case)] -pub extern "system" fn Java_com_veilid_veilid_1core_1android_1tests_MainActivity_run_1tests( - env: JNIEnv, - _class: JClass, - ctx: JObject, -) { - crate::intf::utils::android::veilid_core_setup_android( - env, - ctx, - "veilid_core", - crate::veilid_config::VeilidConfigLogLevel::Trace, - ); - run_all_tests(); -} - -#[cfg(all(target_os = "ios", feature = "veilid_core_ios_tests"))] -#[no_mangle] -pub extern "C" fn run_veilid_core_tests() { - let log_path: std::path::PathBuf = [ - std::env::var("HOME").unwrap().as_str(), - "Documents", - "veilid-core.log", - ] - .iter() - .collect(); - crate::intf::utils::ios_test_setup::veilid_core_setup( - "veilid-core", - Some(Level::Trace), - Some((Level::Trace, log_path.as_path())), - ); - run_all_tests(); -} - /////////////////////////////////////////////////////////////////////////// #[allow(dead_code)] @@ -130,18 +92,26 @@ fn exec_test_envelope_receipt() { cfg_if! { if #[cfg(test)] { use serial_test::serial; - use simplelog::*; use std::sync::Once; static SETUP_ONCE: Once = Once::new(); pub fn setup() { SETUP_ONCE.call_once(|| { - let mut cb = ConfigBuilder::new(); - for ig in crate::DEFAULT_LOG_IGNORE_LIST { - cb.add_filter_ignore_str(ig); + cfg_if! { + if #[cfg(feature = "tracing")] { + use tracing_subscriber::{filter, fmt, prelude::*}; + let mut filters = filter::Targets::new().with_default(filter::LevelFilter::TRACE); + for ig in DEFAULT_LOG_IGNORE_LIST { + filters = filters.with_target(ig, filter::LevelFilter::OFF); + } + let fmt_layer = fmt::layer(); + tracing_subscriber::registry() + .with(fmt_layer) + .with(filters) + .init(); + } } - TestLogger::init(LevelFilter::Trace, cb.build()).unwrap(); }); } diff --git a/veilid-core/webdriver.json b/veilid-core/webdriver.json new file mode 100644 index 00000000..c2d6865e --- /dev/null +++ b/veilid-core/webdriver.json @@ -0,0 +1,15 @@ +{ + "moz:firefoxOptions": { + "prefs": { + "media.navigator.streams.fake": true, + "media.navigator.permission.disabled": true + }, + "args": [] + }, + "goog:chromeOptions": { + "args": [ + "--use-fake-device-for-media-stream", + "--use-fake-ui-for-media-stream" + ] + } +} diff --git a/veilid-tools/run_tests.sh b/veilid-tools/run_tests.sh index 0267d8aa..ad09bb88 100755 --- a/veilid-tools/run_tests.sh +++ b/veilid-tools/run_tests.sh @@ -3,7 +3,7 @@ SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" pushd $SCRIPTDIR 2>/dev/null if [[ "$1" == "wasm" ]]; then - WASM_BINDGEN_TEST_TIMEOUT=120 wasm-pack test --chrome --headless + WASM_BINDGEN_TEST_TIMEOUT=120 wasm-pack test --firefox --headless elif [[ "$1" == "ios" ]]; then SYMROOT=/tmp/testout APPNAME=veilidtools-tests @@ -56,9 +56,9 @@ elif [[ "$1" == "android" ]]; then popd >/dev/null else - cargo test --features=rt-tokio,tracing - cargo test --features=rt-async-std,tracing - cargo test --features=rt-tokio - cargo test --features=rt-async-std + cargo test --features=rt-tokio,tracing -- --nocapture + cargo test --features=rt-async-std,tracing -- --nocapture + cargo test --features=rt-tokio -- --nocapture + cargo test --features=rt-async-std -- --nocapture fi popd 2>/dev/null \ No newline at end of file diff --git a/veilid-tools/src/tests/android/mod.rs b/veilid-tools/src/tests/android/mod.rs index c238052e..676b26e0 100644 --- a/veilid-tools/src/tests/android/mod.rs +++ b/veilid-tools/src/tests/android/mod.rs @@ -1,35 +1,25 @@ +use super::native::*; use super::*; -//use jni::errors::Result as JniResult; use jni::{objects::GlobalRef, objects::JObject, JNIEnv, JavaVM}; use lazy_static::*; use std::backtrace::Backtrace; use std::panic; -pub struct AndroidGlobals { - pub vm: JavaVM, - pub ctx: GlobalRef, +use jni::{objects::JClass, objects::JObject, JNIEnv}; + +#[no_mangle] +#[allow(non_snake_case)] +pub extern "system" fn Java_com_veilid_veilid_1tools_1android_1tests_MainActivity_run_1tests( + _env: JNIEnv, + _class: JClass, + _ctx: JObject, +) { + crate::tests::android::veilid_tools_setup_android_tests(); + run_all_tests(); } -impl Drop for AndroidGlobals { - fn drop(&mut self) { - // Ensure we're attached before dropping GlobalRef - self.vm.attach_current_thread_as_daemon().unwrap(); - } -} - -lazy_static! { - pub static ref ANDROID_GLOBALS: Arc>> = Arc::new(Mutex::new(None)); -} - -pub fn veilid_tools_setup_android_no_log<'a>(env: JNIEnv<'a>, ctx: JObject<'a>) { - *ANDROID_GLOBALS.lock() = Some(AndroidGlobals { - vm: env.get_java_vm().unwrap(), - ctx: env.new_global_ref(ctx).unwrap(), - }); -} - -pub fn veilid_tools_setup<'a>(env: JNIEnv<'a>, ctx: JObject<'a>, log_tag: &'a str) { +pub fn veilid_tools_setup_android_tests() { cfg_if! { if #[cfg(feature = "tracing")] { use tracing::*; @@ -44,7 +34,7 @@ pub fn veilid_tools_setup<'a>(env: JNIEnv<'a>, ctx: JObject<'a>, log_tag: &'a st // Set up subscriber and layers let subscriber = Registry::default(); let mut layers = Vec::new(); - let layer = tracing_android::layer(log_tag) + let layer = tracing_android::layer("veilid-tools") .expect("failed to set up android logging") .with_filter(filter::LevelFilter::TRACE) .with_filter(filters); @@ -89,25 +79,4 @@ pub fn veilid_tools_setup<'a>(env: JNIEnv<'a>, ctx: JObject<'a>, log_tag: &'a st } error!("Backtrace:\n{:?}", bt); })); - - veilid_tools_setup_android_no_log(env, ctx); } - -// pub fn get_android_globals() -> (JavaVM, GlobalRef) { -// let globals_locked = ANDROID_GLOBALS.lock(); -// let globals = globals_locked.as_ref().unwrap(); -// let env = globals.vm.attach_current_thread_as_daemon().unwrap(); -// let vm = env.get_java_vm().unwrap(); -// let ctx = globals.ctx.clone(); -// (vm, ctx) -// } - -// pub fn with_null_local_frame<'b, T, F>(env: JNIEnv<'b>, s: i32, f: F) -> JniResult -// where -// F: FnOnce() -> JniResult, -// { -// env.push_local_frame(s)?; -// let out = f(); -// env.pop_local_frame(JObject::null())?; -// out -// } diff --git a/veilid-tools/src/tests/ios/mod.rs b/veilid-tools/src/tests/ios/mod.rs index 358955a8..71584b8a 100644 --- a/veilid-tools/src/tests/ios/mod.rs +++ b/veilid-tools/src/tests/ios/mod.rs @@ -1,9 +1,16 @@ +use super::native::*; use super::*; use std::backtrace::Backtrace; use std::panic; -pub fn veilid_tools_setup<'a>() -> Result<(), String> { +#[no_mangle] +pub extern "C" fn run_veilid_tools_tests() { + crate::tests::ios::veilid_tools_setup_ios_tests(); + run_all_tests(); +} + +pub fn veilid_tools_setup_ios_tests() { cfg_if! { if #[cfg(feature = "tracing")] { use tracing_subscriber::{filter, fmt, prelude::*}; @@ -30,7 +37,7 @@ pub fn veilid_tools_setup<'a>() -> Result<(), String> { TerminalMode::Mixed, ColorChoice::Auto, )); - CombinedLogger::init(logs).map_err(|e| format!("logger init error: {}", e))?; + CombinedLogger::init(logs).expect("logger init error"); } } @@ -56,6 +63,4 @@ pub fn veilid_tools_setup<'a>() -> Result<(), String> { } error!("Backtrace:\n{:?}", bt); })); - - Ok(()) } diff --git a/veilid-tools/src/tests/mod.rs b/veilid-tools/src/tests/mod.rs index e10c08c2..e0b727a3 100644 --- a/veilid-tools/src/tests/mod.rs +++ b/veilid-tools/src/tests/mod.rs @@ -1,7 +1,7 @@ -#[cfg(target_os = "android")] +#[cfg(all(target_os = "android", feature = "veilid_tools_android_tests"))] mod android; pub mod common; -#[cfg(target_os = "ios")] +#[cfg(all(target_os = "ios", feature = "veilid_tools_ios_tests"))] mod ios; #[cfg(not(target_arch = "wasm32"))] mod native; diff --git a/veilid-tools/src/tests/native/mod.rs b/veilid-tools/src/tests/native/mod.rs index 120fa9cb..b97ac910 100644 --- a/veilid-tools/src/tests/native/mod.rs +++ b/veilid-tools/src/tests/native/mod.rs @@ -5,30 +5,8 @@ mod test_async_peek_stream; use super::*; -#[cfg(all(target_os = "android", feature = "veilid_tools_android_tests"))] -use jni::{objects::JClass, objects::JObject, JNIEnv}; - -#[cfg(all(target_os = "android", feature = "veilid_tools_android_tests"))] -#[no_mangle] -#[allow(non_snake_case)] -pub extern "system" fn Java_com_veilid_veilid_1tools_1android_1tests_MainActivity_run_1tests( - env: JNIEnv, - _class: JClass, - ctx: JObject, -) { - crate::tests::android::veilid_tools_setup(env, ctx, "veilid-tools"); - run_all_tests(); -} - -#[cfg(all(target_os = "ios", feature = "veilid_tools_ios_tests"))] -#[no_mangle] -#[allow(dead_code)] -pub extern "C" fn run_veilid_tools_tests() { - crate::tests::ios::veilid_tools_setup().expect("setup failed"); - run_all_tests(); -} - -/////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////////// +// Allow access to tests from non cfg(test), as required for android and ios tests #[allow(dead_code)] pub fn run_all_tests() { @@ -68,6 +46,7 @@ fn exec_test_async_tag_lock() { test_async_tag_lock::test_all().await; }) } + /////////////////////////////////////////////////////////////////////////// cfg_if! { if #[cfg(test)] { @@ -83,15 +62,14 @@ cfg_if! { cfg_if! { if #[cfg(feature = "tracing")] { use tracing_subscriber::{filter, fmt, prelude::*}; - let mut filters = filter::Targets::new(); + let mut filters = filter::Targets::new().with_default(filter::LevelFilter::TRACE); for ig in DEFAULT_LOG_IGNORE_LIST { filters = filters.with_target(ig, filter::LevelFilter::OFF); } let fmt_layer = fmt::layer(); tracing_subscriber::registry() - .with(filters) - .with(filter::LevelFilter::TRACE) .with(fmt_layer) + .with(filters) .init(); } else { use simplelog::*; diff --git a/veilid-tools/tests/web.rs b/veilid-tools/tests/web.rs index 8a04bce8..ae522f3e 100644 --- a/veilid-tools/tests/web.rs +++ b/veilid-tools/tests/web.rs @@ -2,6 +2,7 @@ #![cfg(target_arch = "wasm32")] use veilid_tools::tests::*; +use veilid_tools::*; use wasm_bindgen_test::*; From b6c446cd39173bd4d1699d7b2d3ed1469a25e03c Mon Sep 17 00:00:00 2001 From: John Smith Date: Wed, 30 Nov 2022 22:48:50 -0500 Subject: [PATCH 25/88] test debugging --- veilid-core/src/tests/android/mod.rs | 10 ++++------ veilid-core/src/tests/common/test_host_interface.rs | 2 +- veilid-core/src/tests/ios/mod.rs | 11 ++++------- .../veilidcore-tests/ViewController.swift | 2 ++ 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/veilid-core/src/tests/android/mod.rs b/veilid-core/src/tests/android/mod.rs index 96b79aeb..cf75d9f9 100644 --- a/veilid-core/src/tests/android/mod.rs +++ b/veilid-core/src/tests/android/mod.rs @@ -1,3 +1,4 @@ +use super::native::*; use crate::*; use backtrace::Backtrace; use jni::{ @@ -5,9 +6,7 @@ use jni::{ }; use lazy_static::*; use std::panic; -use tracing::*; -use tracing_subscriber::prelude::*; -use tracing_subscriber::*; +use tracing_subscriber::{filter, fmt, prelude::*}; #[no_mangle] #[allow(non_snake_case)] @@ -22,12 +21,11 @@ pub extern "system" fn Java_com_veilid_veilid_1core_1android_1tests_MainActivity pub fn veilid_core_setup_android_tests(env: JNIEnv, ctx: JObject) { // Set up subscriber and layers - use tracing_subscriber::{filter, fmt, prelude::*}; let filter = VeilidLayerFilter::new(VeilidConfigLogLevel::Trace, None); let layer = tracing_android::layer("veilid-core").expect("failed to set up android logging"); tracing_subscriber::registry() - .with(filters) - .with(fmt_layer) + .with(filter) + .with(layer) .init(); // Set up panic hook for backtraces diff --git a/veilid-core/src/tests/common/test_host_interface.rs b/veilid-core/src/tests/common/test_host_interface.rs index 83618b7f..d1a52cdc 100644 --- a/veilid-core/src/tests/common/test_host_interface.rs +++ b/veilid-core/src/tests/common/test_host_interface.rs @@ -18,7 +18,7 @@ cfg_if! { let t2 = get_timestamp(); let tdiff = ((t2 - t1) as f64)/1000000.0f64; info!("running network interface test with {} iterations took {} seconds", count, tdiff); - info!("interfaces: {:#?}", interfaces) + //info!("interfaces: {:#?}", interfaces) } } } diff --git a/veilid-core/src/tests/ios/mod.rs b/veilid-core/src/tests/ios/mod.rs index bbbe7a59..b00554c1 100644 --- a/veilid-core/src/tests/ios/mod.rs +++ b/veilid-core/src/tests/ios/mod.rs @@ -1,6 +1,8 @@ +use super::native::*; use crate::*; use backtrace::Backtrace; use std::panic; +use tracing_subscriber::{fmt, prelude::*}; #[no_mangle] #[allow(dead_code)] @@ -11,14 +13,9 @@ pub extern "C" fn run_veilid_core_tests() { pub fn veilid_core_setup_ios_tests() { // Set up subscriber and layers - use tracing_subscriber::{filter, fmt, prelude::*}; let filter = VeilidLayerFilter::new(VeilidConfigLogLevel::Trace, None); - let fmt_layer = fmt::layer(); - let layer = tracing_android::layer("veilid-core").expect("failed to set up android logging"); - tracing_subscriber::registry() - .with(filters) - .with(fmt_layer) - .init(); + let fmt_layer = fmt::layer().with_filter(filter); + tracing_subscriber::registry().with(fmt_layer).init(); panic::set_hook(Box::new(|panic_info| { let bt = Backtrace::new(); diff --git a/veilid-core/src/tests/ios/veilidcore-tests/veilidcore-tests/ViewController.swift b/veilid-core/src/tests/ios/veilidcore-tests/veilidcore-tests/ViewController.swift index dd663282..66ffe67e 100644 --- a/veilid-core/src/tests/ios/veilidcore-tests/veilidcore-tests/ViewController.swift +++ b/veilid-core/src/tests/ios/veilidcore-tests/veilidcore-tests/ViewController.swift @@ -6,12 +6,14 @@ // import UIKit +import Darwin class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() run_veilid_core_tests() + exit(0) } From 9a4ab59ed6a98d2c8dc413c5f86a7ec5b6c05749 Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 1 Dec 2022 10:46:52 -0500 Subject: [PATCH 26/88] test work --- Cargo.lock | 125 +++++++++++++++-- veilid-core/Cargo.toml | 8 +- veilid-core/src/tests/android/mod.rs | 4 +- .../MainActivity.java | 4 +- veilid-core/src/tests/ios/mod.rs | 13 +- veilid-core/src/tests/native/mod.rs | 132 +++++++----------- veilid-tools/Cargo.toml | 5 +- veilid-tools/src/tests/android/mod.rs | 6 +- veilid-tools/src/tests/ios/mod.rs | 53 ++++--- veilid-tools/src/tests/native/mod.rs | 48 +++---- 10 files changed, 248 insertions(+), 150 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb62b7c6..b569d6ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -561,7 +561,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd4865004a46a0aafb2a0a5eb19d3c9fc46ee5f063a6cfc605c69ac9ecf5263d" dependencies = [ "bitflags", - "cexpr", + "cexpr 0.4.0", "clang-sys", "lazy_static", "lazycell", @@ -570,7 +570,30 @@ dependencies = [ "quote", "regex", "rustc-hash", - "shlex", + "shlex 0.1.1", +] + +[[package]] +name = "bindgen" +version = "0.59.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" +dependencies = [ + "bitflags", + "cexpr 0.6.0", + "clang-sys", + "clap 2.34.0", + "env_logger 0.9.3", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.1.0", + "which", ] [[package]] @@ -798,6 +821,15 @@ dependencies = [ "nom 5.1.2", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.1", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -918,6 +950,21 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim 0.8.0", + "textwrap 0.11.0", + "unicode-width", + "vec_map", +] + [[package]] name = "clap" version = "3.2.23" @@ -928,9 +975,9 @@ dependencies = [ "bitflags", "clap_lex", "indexmap", - "strsim", + "strsim 0.10.0", "termcolor", - "textwrap", + "textwrap 0.16.0", ] [[package]] @@ -1140,7 +1187,7 @@ dependencies = [ "atty", "cast", "ciborium", - "clap", + "clap 3.2.23", "criterion-plot", "itertools", "lazy_static", @@ -1471,7 +1518,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.10.0", "syn", ] @@ -1710,8 +1757,11 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" dependencies = [ + "atty", + "humantime", "log", "regex", + "termcolor", ] [[package]] @@ -2135,7 +2185,7 @@ version = "0.9.1+1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9447d1a926beeef466606cc45717f80897998b548e7dc622873d453e1ecb4be4" dependencies = [ - "bindgen", + "bindgen 0.57.0", "boringssl-src", "cc", "cmake", @@ -3478,6 +3528,17 @@ version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" +[[package]] +name = "oslog" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d2043d1f61d77cb2f4b1f7b7b2295f40507f5f8e9d1c8bf10a1ca5f97a3969" +dependencies = [ + "cc", + "dashmap", + "log", +] + [[package]] name = "overload" version = "0.1.1" @@ -4664,6 +4725,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + [[package]] name = "signal-hook" version = "0.3.14" @@ -4866,6 +4933,12 @@ dependencies = [ "pin-project-lite 0.2.9", ] +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "strsim" version = "0.10.0" @@ -4957,6 +5030,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "textwrap" version = "0.16.0" @@ -5417,6 +5499,22 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "tracing-oslog" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bc58223383423483e4bc056c7e7b3f77bdee924a9d33834112c69ead06dc847" +dependencies = [ + "bindgen 0.59.2", + "cc", + "cfg-if 1.0.0", + "fnv", + "once_cell", + "parking_lot 0.11.2", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "tracing-subscriber" version = "0.3.16" @@ -5665,6 +5763,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "veilid-cli" version = "0.1.0" @@ -5676,7 +5780,7 @@ dependencies = [ "capnp-rpc", "capnpc", "cfg-if 1.0.0", - "clap", + "clap 3.2.23", "config", "crossbeam-channel", "cursive", @@ -5778,6 +5882,7 @@ dependencies = [ "tracing", "tracing-android", "tracing-error", + "tracing-oslog", "tracing-subscriber", "tracing-wasm", "trust-dns-resolver", @@ -5839,7 +5944,7 @@ dependencies = [ "capnp-rpc", "capnpc", "cfg-if 1.0.0", - "clap", + "clap 3.2.23", "color-eyre", "config", "console-subscriber", @@ -5900,6 +6005,7 @@ dependencies = [ "ndk-glue", "nix 0.26.1", "once_cell", + "oslog", "owo-colors", "parking_lot 0.11.2", "rand 0.7.3", @@ -5914,6 +6020,7 @@ dependencies = [ "tokio-util", "tracing", "tracing-android", + "tracing-oslog", "tracing-subscriber", "tracing-wasm", "wasm-bindgen", diff --git a/veilid-core/Cargo.toml b/veilid-core/Cargo.toml index ef064a52..dc2d896f 100644 --- a/veilid-core/Cargo.toml +++ b/veilid-core/Cargo.toml @@ -14,8 +14,8 @@ default = [] rt-async-std = [ "async-std", "async-std-resolver", "async_executors/async_std", "rtnetlink?/smol_socket", "veilid-tools/rt-async-std" ] rt-tokio = [ "tokio", "tokio-util", "tokio-stream", "trust-dns-resolver/tokio-runtime", "async_executors/tokio_tp", "async_executors/tokio_io", "async_executors/tokio_timer", "rtnetlink?/tokio_socket", "veilid-tools/rt-tokio" ] -veilid_core_android_tests = [] -veilid_core_ios_tests = [ "simplelog" ] +veilid_core_android_tests = [ "dep:tracing-android" ] +veilid_core_ios_tests = [ "dep:tracing-oslog" ] tracking = [] [dependencies] @@ -130,7 +130,7 @@ jni = "^0" jni-sys = "^0" ndk = { version = "^0.7" } ndk-glue = { version = "^0.7", features = ["logger"] } -tracing-android = { version = "^0" } +tracing-android = { version = "^0", optional = true } # Dependenices for all Unix (Linux, Android, MacOS, iOS) [target.'cfg(unix)'.dependencies] @@ -148,7 +148,7 @@ windows-permissions = "^0" # Dependencies for iOS [target.'cfg(target_os = "ios")'.dependencies] -simplelog = { version = "^0", optional = true } +tracing-oslog = { version = "^0", optional = true } # Rusqlite configuration to ensure platforms that don't come with sqlite get it bundled # Except WASM which doesn't use sqlite diff --git a/veilid-core/src/tests/android/mod.rs b/veilid-core/src/tests/android/mod.rs index cf75d9f9..92500a01 100644 --- a/veilid-core/src/tests/android/mod.rs +++ b/veilid-core/src/tests/android/mod.rs @@ -16,7 +16,9 @@ pub extern "system" fn Java_com_veilid_veilid_1core_1android_1tests_MainActivity ctx: JObject, ) { crate::intf::utils::android::veilid_core_setup_android_tests(env, ctx); - run_all_tests(); + block_on(async { + run_all_tests().await; + }) } pub fn veilid_core_setup_android_tests(env: JNIEnv, ctx: JObject) { diff --git a/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/java/com/veilid/veilid_core_android_tests/MainActivity.java b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/java/com/veilid/veilid_core_android_tests/MainActivity.java index 43a65090..b02d3093 100644 --- a/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/java/com/veilid/veilid_core_android_tests/MainActivity.java +++ b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/java/com/veilid/veilid_core_android_tests/MainActivity.java @@ -22,7 +22,6 @@ public class MainActivity extends AppCompatActivity { } public void run() { - run_tests(this.context); } } @@ -31,7 +30,6 @@ public class MainActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - this.testThread = new TestThread(this); - this.testThread.start(); + run_tests(this.context); } } diff --git a/veilid-core/src/tests/ios/mod.rs b/veilid-core/src/tests/ios/mod.rs index b00554c1..24e77af0 100644 --- a/veilid-core/src/tests/ios/mod.rs +++ b/veilid-core/src/tests/ios/mod.rs @@ -7,15 +7,20 @@ use tracing_subscriber::{fmt, prelude::*}; #[no_mangle] #[allow(dead_code)] pub extern "C" fn run_veilid_core_tests() { - veilid_core_setup_ios_tests(); - run_all_tests(); + std::thread::spawn(|| { + block_on(async { + veilid_core_setup_ios_tests(); + run_all_tests().await; + }) + }); } pub fn veilid_core_setup_ios_tests() { // Set up subscriber and layers let filter = VeilidLayerFilter::new(VeilidConfigLogLevel::Trace, None); - let fmt_layer = fmt::layer().with_filter(filter); - tracing_subscriber::registry().with(fmt_layer).init(); + tracing_subscriber::registry() + .with(OsLogger::new("com.veilid.veilidtools-tests", "default").with_filter(filter)) + .init(); panic::set_hook(Box::new(|panic_info| { let bt = Backtrace::new(); diff --git a/veilid-core/src/tests/native/mod.rs b/veilid-core/src/tests/native/mod.rs index 04431400..a05f1116 100644 --- a/veilid-core/src/tests/native/mod.rs +++ b/veilid-core/src/tests/native/mod.rs @@ -8,84 +8,40 @@ use crate::*; /////////////////////////////////////////////////////////////////////////// #[allow(dead_code)] -pub fn run_all_tests() { - info!("TEST: exec_test_host_interface"); - exec_test_host_interface(); - info!("TEST: exec_test_dht_key"); - exec_test_dht_key(); - info!("TEST: exec_test_veilid_core"); - exec_test_veilid_core(); - info!("TEST: exec_test_veilid_config"); - exec_test_veilid_config(); - info!("TEST: exec_test_connection_table"); - exec_test_connection_table(); - info!("TEST: exec_test_table_store"); - exec_test_table_store(); - info!("TEST: exec_test_protected_store"); - exec_test_protected_store(); - info!("TEST: exec_test_crypto"); - exec_test_crypto(); - info!("TEST: exec_test_envelope_receipt"); - exec_test_envelope_receipt(); +pub async fn run_all_tests() { + info!("TEST: test_host_interface"); + test_host_interface::test_all().await; + info!("TEST: test_dht_key"); + test_dht_key::test_all().await; + info!("TEST: test_veilid_core"); + test_veilid_core::test_all().await; + info!("TEST: test_veilid_config"); + test_veilid_config::test_all().await; + info!("TEST: test_connection_table"); + test_connection_table::test_all().await; + info!("TEST: test_table_store"); + test_table_store::test_all().await; + info!("TEST: test_protected_store"); + test_protected_store::test_all().await; + info!("TEST: test_crypto"); + test_crypto::test_all().await; + info!("TEST: test_envelope_receipt"); + test_envelope_receipt::test_all().await; info!("Finished unit tests"); } +#[allow(dead_code)] #[cfg(feature = "rt-tokio")] -fn block_on, T>(f: F) -> T { +pub fn block_on, T>(f: F) -> T { let rt = tokio::runtime::Runtime::new().unwrap(); - let local = tokio::task::LocalSet::new(); - local.block_on(&rt, f) -} -#[cfg(feature = "rt-async-std")] -fn block_on, T>(f: F) -> T { - async_std::task::block_on(f) + rt.block_on(f) } -fn exec_test_host_interface() { - block_on(async { - test_host_interface::test_all().await; - }); -} -fn exec_test_dht_key() { - block_on(async { - test_dht_key::test_all().await; - }); -} -fn exec_test_veilid_core() { - block_on(async { - test_veilid_core::test_all().await; - }); -} -fn exec_test_veilid_config() { - block_on(async { - test_veilid_config::test_all().await; - }) -} -fn exec_test_connection_table() { - block_on(async { - test_connection_table::test_all().await; - }) -} -fn exec_test_table_store() { - block_on(async { - test_table_store::test_all().await; - }) -} -fn exec_test_protected_store() { - block_on(async { - test_protected_store::test_all().await; - }) -} -fn exec_test_crypto() { - block_on(async { - test_crypto::test_all().await; - }) -} -fn exec_test_envelope_receipt() { - block_on(async { - test_envelope_receipt::test_all().await; - }) +#[cfg(feature = "rt-async-std")] +#[allow(dead_code)] +pub fn block_on, T>(f: F) -> T { + async_std::task::block_on(f) } /////////////////////////////////////////////////////////////////////////// @@ -119,63 +75,81 @@ cfg_if! { #[serial] fn run_test_host_interface() { setup(); - exec_test_host_interface(); + block_on(async { + test_host_interface::test_all().await; + }); } #[test] #[serial] fn run_test_dht_key() { setup(); - exec_test_dht_key(); + block_on(async { + test_dht_key::test_all().await; + }); } #[test] #[serial] fn run_test_veilid_core() { setup(); - exec_test_veilid_core(); + block_on(async { + test_veilid_core::test_all().await; + }); } #[test] #[serial] fn run_test_veilid_config() { setup(); - exec_test_veilid_config(); + block_on(async { + test_veilid_config::test_all().await; + }) } #[test] #[serial] fn run_test_connection_table() { setup(); - exec_test_connection_table(); + block_on(async { + test_connection_table::test_all().await; + }) } #[test] #[serial] fn run_test_table_store() { setup(); - exec_test_table_store(); + block_on(async { + test_table_store::test_all().await; + }) } #[test] #[serial] fn run_test_protected_store() { setup(); - exec_test_protected_store(); + block_on(async { + test_protected_store::test_all().await; + }) } #[test] #[serial] fn run_test_crypto() { setup(); - exec_test_crypto(); + block_on(async { + test_crypto::test_all().await; + }) } #[test] #[serial] fn run_test_envelope_receipt() { setup(); - exec_test_envelope_receipt(); + block_on(async { + test_envelope_receipt::test_all().await; + }) } } diff --git a/veilid-tools/Cargo.toml b/veilid-tools/Cargo.toml index e426d861..290eda93 100644 --- a/veilid-tools/Cargo.toml +++ b/veilid-tools/Cargo.toml @@ -15,7 +15,7 @@ rt-async-std = [ "async-std", "async_executors/async_std", ] rt-tokio = [ "tokio", "tokio-util", "async_executors/tokio_tp", "async_executors/tokio_io", "async_executors/tokio_timer", ] veilid_tools_android_tests = [ "dep:tracing-android" ] -veilid_tools_ios_tests = [] +veilid_tools_ios_tests = [ "dep:oslog", "dep:tracing-oslog" ] tracing = [ "dep:tracing", "dep:tracing-subscriber" ] [dependencies] @@ -72,7 +72,8 @@ android-logd-logger = "0.2.1" # Dependencies for iOS [target.'cfg(target_os = "ios")'.dependencies] -simplelog = { version = "^0.12", features = [ "test" ] } +oslog = { version = "^0", optional = true } +tracing-oslog = { version = "^0", optional = true } ### DEV DEPENDENCIES diff --git a/veilid-tools/src/tests/android/mod.rs b/veilid-tools/src/tests/android/mod.rs index 676b26e0..9a89e145 100644 --- a/veilid-tools/src/tests/android/mod.rs +++ b/veilid-tools/src/tests/android/mod.rs @@ -15,8 +15,10 @@ pub extern "system" fn Java_com_veilid_veilid_1tools_1android_1tests_MainActivit _class: JClass, _ctx: JObject, ) { - crate::tests::android::veilid_tools_setup_android_tests(); - run_all_tests(); + veilid_tools_setup_android_tests(); + block_on(async { + run_all_tests().await; + }) } pub fn veilid_tools_setup_android_tests() { diff --git a/veilid-tools/src/tests/ios/mod.rs b/veilid-tools/src/tests/ios/mod.rs index 71584b8a..888eb9a7 100644 --- a/veilid-tools/src/tests/ios/mod.rs +++ b/veilid-tools/src/tests/ios/mod.rs @@ -6,38 +6,55 @@ use std::panic; #[no_mangle] pub extern "C" fn run_veilid_tools_tests() { - crate::tests::ios::veilid_tools_setup_ios_tests(); - run_all_tests(); + veilid_tools_setup_ios_tests(); + block_on(async { + run_all_tests().await; + }) } pub fn veilid_tools_setup_ios_tests() { cfg_if! { if #[cfg(feature = "tracing")] { - use tracing_subscriber::{filter, fmt, prelude::*}; + // use tracing_subscriber::{filter, fmt, prelude::*}; + // let mut filters = filter::Targets::new(); + // for ig in DEFAULT_LOG_IGNORE_LIST { + // filters = filters.with_target(ig, filter::LevelFilter::OFF); + // } + // let fmt_layer = fmt::layer(); + // tracing_subscriber::registry() + // .with(filters) + // .with(filter::LevelFilter::TRACE) + // .with(fmt_layer) + // .init(); + let mut filters = filter::Targets::new(); for ig in DEFAULT_LOG_IGNORE_LIST { filters = filters.with_target(ig, filter::LevelFilter::OFF); } - let fmt_layer = fmt::layer(); tracing_subscriber::registry() .with(filters) .with(filter::LevelFilter::TRACE) - .with(fmt_layer) + .with(OsLogger::new("com.veilid.veilidtools-tests", "default")) .init(); } else { - use simplelog::*; - let mut logs: Vec> = Vec::new(); - let mut cb = ConfigBuilder::new(); - for ig in DEFAULT_LOG_IGNORE_LIST { - cb.add_filter_ignore_str(ig); - } - logs.push(TermLogger::new( - LevelFilter::Trace, - cb.build(), - TerminalMode::Mixed, - ColorChoice::Auto, - )); - CombinedLogger::init(logs).expect("logger init error"); + // use simplelog::*; + // let mut logs: Vec> = Vec::new(); + // let mut cb = ConfigBuilder::new(); + // for ig in DEFAULT_LOG_IGNORE_LIST { + // cb.add_filter_ignore_str(ig); + // } + // logs.push(TermLogger::new( + // LevelFilter::Trace, + // cb.build(), + // TerminalMode::Mixed, + // ColorChoice::Auto, + // )); + // CombinedLogger::init(logs).expect("logger init error"); + + OsLogger::new("com.veilid.veilidtools-tests", "default") + .level_filter(LevelFilter::Trace) + .init() + .unwrap(); } } diff --git a/veilid-tools/src/tests/native/mod.rs b/veilid-tools/src/tests/native/mod.rs index b97ac910..75a481a8 100644 --- a/veilid-tools/src/tests/native/mod.rs +++ b/veilid-tools/src/tests/native/mod.rs @@ -9,42 +9,28 @@ use super::*; // Allow access to tests from non cfg(test), as required for android and ios tests #[allow(dead_code)] -pub fn run_all_tests() { +pub async fn run_all_tests() { info!("TEST: exec_test_host_interface"); - exec_test_host_interface(); + test_host_interface::test_all().await; info!("TEST: exec_test_async_peek_stream"); - exec_test_async_peek_stream(); + test_async_peek_stream::test_all().await; info!("TEST: exec_test_async_tag_lock"); - exec_test_async_tag_lock(); + test_async_tag_lock::test_all().await; info!("Finished unit tests"); } #[cfg(feature = "rt-tokio")] -fn block_on, T>(f: F) -> T { +#[allow(dead_code)] +pub fn block_on, T>(f: F) -> T { let rt = tokio::runtime::Runtime::new().unwrap(); - let local = tokio::task::LocalSet::new(); - local.block_on(&rt, f) -} -#[cfg(feature = "rt-async-std")] -fn block_on, T>(f: F) -> T { - async_std::task::block_on(f) + rt.block_on(f) } -fn exec_test_host_interface() { - block_on(async { - test_host_interface::test_all().await; - }); -} -fn exec_test_async_peek_stream() { - block_on(async { - test_async_peek_stream::test_all().await; - }) -} -fn exec_test_async_tag_lock() { - block_on(async { - test_async_tag_lock::test_all().await; - }) +#[cfg(feature = "rt-async-std")] +#[allow(dead_code)] +pub fn block_on, T>(f: F) -> T { + async_std::task::block_on(f) } /////////////////////////////////////////////////////////////////////////// @@ -88,21 +74,27 @@ cfg_if! { #[serial] fn run_test_host_interface() { setup(); - exec_test_host_interface(); + block_on(async { + test_host_interface::test_all().await; + }); } #[test] #[serial] fn run_test_async_peek_stream() { setup(); - exec_test_async_peek_stream(); + block_on(async { + test_async_peek_stream::test_all().await; + }); } #[test] #[serial] fn run_test_async_tag_lock() { setup(); - exec_test_async_tag_lock(); + block_on(async { + test_async_tag_lock::test_all().await; + }); } } } From e1be2bac6709389a19f48b01ca3e5225636e0d49 Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 1 Dec 2022 12:26:02 -0500 Subject: [PATCH 27/88] test work --- veilid-core/src/tests/ios/mod.rs | 11 +++++------ veilid-tools/src/tests/ios/mod.rs | 27 +++------------------------ 2 files changed, 8 insertions(+), 30 deletions(-) diff --git a/veilid-core/src/tests/ios/mod.rs b/veilid-core/src/tests/ios/mod.rs index 24e77af0..31339981 100644 --- a/veilid-core/src/tests/ios/mod.rs +++ b/veilid-core/src/tests/ios/mod.rs @@ -2,16 +2,15 @@ use super::native::*; use crate::*; use backtrace::Backtrace; use std::panic; -use tracing_subscriber::{fmt, prelude::*}; +use tracing_oslog::OsLogger; +use tracing_subscriber::prelude::*; #[no_mangle] #[allow(dead_code)] pub extern "C" fn run_veilid_core_tests() { - std::thread::spawn(|| { - block_on(async { - veilid_core_setup_ios_tests(); - run_all_tests().await; - }) + block_on(async { + veilid_core_setup_ios_tests(); + run_all_tests().await; }); } diff --git a/veilid-tools/src/tests/ios/mod.rs b/veilid-tools/src/tests/ios/mod.rs index 888eb9a7..af843827 100644 --- a/veilid-tools/src/tests/ios/mod.rs +++ b/veilid-tools/src/tests/ios/mod.rs @@ -15,17 +15,8 @@ pub extern "C" fn run_veilid_tools_tests() { pub fn veilid_tools_setup_ios_tests() { cfg_if! { if #[cfg(feature = "tracing")] { - // use tracing_subscriber::{filter, fmt, prelude::*}; - // let mut filters = filter::Targets::new(); - // for ig in DEFAULT_LOG_IGNORE_LIST { - // filters = filters.with_target(ig, filter::LevelFilter::OFF); - // } - // let fmt_layer = fmt::layer(); - // tracing_subscriber::registry() - // .with(filters) - // .with(filter::LevelFilter::TRACE) - // .with(fmt_layer) - // .init(); + use tracing_oslog::OsLogger; + use tracing_subscriber::prelude::*; let mut filters = filter::Targets::new(); for ig in DEFAULT_LOG_IGNORE_LIST { @@ -37,19 +28,7 @@ pub fn veilid_tools_setup_ios_tests() { .with(OsLogger::new("com.veilid.veilidtools-tests", "default")) .init(); } else { - // use simplelog::*; - // let mut logs: Vec> = Vec::new(); - // let mut cb = ConfigBuilder::new(); - // for ig in DEFAULT_LOG_IGNORE_LIST { - // cb.add_filter_ignore_str(ig); - // } - // logs.push(TermLogger::new( - // LevelFilter::Trace, - // cb.build(), - // TerminalMode::Mixed, - // ColorChoice::Auto, - // )); - // CombinedLogger::init(logs).expect("logger init error"); + use oslog::OsLogger; OsLogger::new("com.veilid.veilidtools-tests", "default") .level_filter(LevelFilter::Trace) From e2153a34e439353c9b0a142467a308c33870dea6 Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 1 Dec 2022 14:32:02 -0500 Subject: [PATCH 28/88] test work --- Cargo.lock | 42 ++++++++++++------- veilid-core/Cargo.toml | 4 +- veilid-core/src/core_context.rs | 3 +- .../intf/native/android/get_directories.rs | 3 ++ veilid-core/src/intf/native/android/mod.rs | 4 ++ .../src/intf/native/protected_store.rs | 2 +- veilid-core/src/lib.rs | 2 +- veilid-core/src/tests/android/mod.rs | 14 +++---- .../MainActivity.java | 6 ++- .../src/tests/common/test_veilid_config.rs | 2 +- veilid-flutter/rust/src/lib.rs | 2 +- veilid-tools/Cargo.toml | 4 +- veilid-tools/src/tests/android/mod.rs | 7 +--- 13 files changed, 55 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b569d6ad..564e8485 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3070,7 +3070,7 @@ checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" dependencies = [ "bitflags", "jni-sys", - "ndk-sys", + "ndk-sys 0.4.1+23.1.7779620", "num_enum", "raw-window-handle", "thiserror", @@ -3094,7 +3094,7 @@ dependencies = [ "ndk", "ndk-context", "ndk-macro", - "ndk-sys", + "ndk-sys 0.4.1+23.1.7779620", "once_cell", "parking_lot 0.12.1", ] @@ -3112,6 +3112,15 @@ dependencies = [ "syn", ] +[[package]] +name = "ndk-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" +dependencies = [ + "jni-sys", +] + [[package]] name = "ndk-sys" version = "0.4.1+23.1.7779620" @@ -3560,6 +3569,20 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "paranoid-android" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e736c9fbaf42b43459cd1fded3dd272968daadfcbc5660ee231a12899f092289" +dependencies = [ + "lazy_static", + "ndk-sys 0.3.0", + "sharded-slab", + "smallvec", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "parity-scale-codec" version = "3.2.1" @@ -5400,17 +5423,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-android" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12612be8f868a09c0ceae7113ff26afe79d81a24473a393cb9120ece162e86c0" -dependencies = [ - "android_log-sys", - "tracing", - "tracing-subscriber", -] - [[package]] name = "tracing-appender" version = "0.2.2" @@ -5858,6 +5870,7 @@ dependencies = [ "once_cell", "owning_ref", "owo-colors", + "paranoid-android", "parking_lot 0.12.1", "rand 0.7.3", "rkyv", @@ -5880,7 +5893,6 @@ dependencies = [ "tokio-stream", "tokio-util", "tracing", - "tracing-android", "tracing-error", "tracing-oslog", "tracing-subscriber", @@ -6007,6 +6019,7 @@ dependencies = [ "once_cell", "oslog", "owo-colors", + "paranoid-android", "parking_lot 0.11.2", "rand 0.7.3", "rust-fsm", @@ -6019,7 +6032,6 @@ dependencies = [ "tokio 1.22.0", "tokio-util", "tracing", - "tracing-android", "tracing-oslog", "tracing-subscriber", "tracing-wasm", diff --git a/veilid-core/Cargo.toml b/veilid-core/Cargo.toml index dc2d896f..59c54eda 100644 --- a/veilid-core/Cargo.toml +++ b/veilid-core/Cargo.toml @@ -14,7 +14,7 @@ default = [] rt-async-std = [ "async-std", "async-std-resolver", "async_executors/async_std", "rtnetlink?/smol_socket", "veilid-tools/rt-async-std" ] rt-tokio = [ "tokio", "tokio-util", "tokio-stream", "trust-dns-resolver/tokio-runtime", "async_executors/tokio_tp", "async_executors/tokio_io", "async_executors/tokio_timer", "rtnetlink?/tokio_socket", "veilid-tools/rt-tokio" ] -veilid_core_android_tests = [ "dep:tracing-android" ] +veilid_core_android_tests = [ "dep:paranoid-android" ] veilid_core_ios_tests = [ "dep:tracing-oslog" ] tracking = [] @@ -130,7 +130,7 @@ jni = "^0" jni-sys = "^0" ndk = { version = "^0.7" } ndk-glue = { version = "^0.7", features = ["logger"] } -tracing-android = { version = "^0", optional = true } +paranoid-android = { version = "^0", optional = true } # Dependenices for all Unix (Linux, Android, MacOS, iOS) [target.'cfg(unix)'.dependencies] diff --git a/veilid-core/src/core_context.rs b/veilid-core/src/core_context.rs index aa4c2593..c8d7a434 100644 --- a/veilid-core/src/core_context.rs +++ b/veilid-core/src/core_context.rs @@ -201,8 +201,7 @@ impl VeilidCoreContext { ) -> Result { cfg_if! { if #[cfg(target_os = "android")] { - if crate::intf::utils::android::ANDROID_GLOBALS.lock().is_none() { - error!("Android globals are not set up"); + if !crate::intf::android::is_android_ready() { apibail_internal!("Android globals are not set up"); } } diff --git a/veilid-core/src/intf/native/android/get_directories.rs b/veilid-core/src/intf/native/android/get_directories.rs index 0dfc2ace..f9a8ea30 100644 --- a/veilid-core/src/intf/native/android/get_directories.rs +++ b/veilid-core/src/intf/native/android/get_directories.rs @@ -1,5 +1,7 @@ use super::*; +use jni::objects::JString; +#[allow(dead_code)] pub fn get_files_dir() -> String { let aglock = ANDROID_GLOBALS.lock(); let ag = aglock.as_ref().unwrap(); @@ -24,6 +26,7 @@ pub fn get_files_dir() -> String { .unwrap() } +#[allow(dead_code)] pub fn get_cache_dir() -> String { let aglock = ANDROID_GLOBALS.lock(); let ag = aglock.as_ref().unwrap(); diff --git a/veilid-core/src/intf/native/android/mod.rs b/veilid-core/src/intf/native/android/mod.rs index c6991235..00a34def 100644 --- a/veilid-core/src/intf/native/android/mod.rs +++ b/veilid-core/src/intf/native/android/mod.rs @@ -29,6 +29,10 @@ pub fn veilid_core_setup_android(env: JNIEnv, ctx: JObject) { }); } +pub fn is_android_ready() -> bool { + ANDROID_GLOBALS.lock().is_some() +} + pub fn get_android_globals() -> (JavaVM, GlobalRef) { let globals_locked = ANDROID_GLOBALS.lock(); let globals = globals_locked.as_ref().unwrap(); diff --git a/veilid-core/src/intf/native/protected_store.rs b/veilid-core/src/intf/native/protected_store.rs index e3a3be94..7ad89551 100644 --- a/veilid-core/src/intf/native/protected_store.rs +++ b/veilid-core/src/intf/native/protected_store.rs @@ -55,7 +55,7 @@ impl ProtectedStore { // Attempt to open the secure keyring cfg_if! { if #[cfg(target_os = "android")] { - inner.keyring_manager = KeyringManager::new_secure(&c.program_name, intf::native::utils::android::get_android_globals()).ok(); + inner.keyring_manager = KeyringManager::new_secure(&c.program_name, crate::intf::android::get_android_globals()).ok(); } else { inner.keyring_manager = KeyringManager::new_secure(&c.program_name).ok(); } diff --git a/veilid-core/src/lib.rs b/veilid-core/src/lib.rs index 8d6e28ee..56b05d44 100644 --- a/veilid-core/src/lib.rs +++ b/veilid-core/src/lib.rs @@ -59,7 +59,7 @@ pub fn veilid_version() -> (u32, u32, u32) { } #[cfg(target_os = "android")] -pub use intf::utils::android::veilid_core_setup_android; +pub use intf::android::veilid_core_setup_android; pub static DEFAULT_LOG_IGNORE_LIST: [&str; 21] = [ "mio", diff --git a/veilid-core/src/tests/android/mod.rs b/veilid-core/src/tests/android/mod.rs index 92500a01..b8ba9645 100644 --- a/veilid-core/src/tests/android/mod.rs +++ b/veilid-core/src/tests/android/mod.rs @@ -1,12 +1,9 @@ use super::native::*; use crate::*; use backtrace::Backtrace; -use jni::{ - objects::GlobalRef, objects::JClass, objects::JObject, objects::JString, JNIEnv, JavaVM, -}; -use lazy_static::*; +use jni::{objects::JClass, objects::JObject, JNIEnv}; use std::panic; -use tracing_subscriber::{filter, fmt, prelude::*}; +use tracing_subscriber::prelude::*; #[no_mangle] #[allow(non_snake_case)] @@ -15,7 +12,7 @@ pub extern "system" fn Java_com_veilid_veilid_1core_1android_1tests_MainActivity _class: JClass, ctx: JObject, ) { - crate::intf::utils::android::veilid_core_setup_android_tests(env, ctx); + veilid_core_setup_android_tests(env, ctx); block_on(async { run_all_tests().await; }) @@ -24,10 +21,9 @@ pub extern "system" fn Java_com_veilid_veilid_1core_1android_1tests_MainActivity pub fn veilid_core_setup_android_tests(env: JNIEnv, ctx: JObject) { // Set up subscriber and layers let filter = VeilidLayerFilter::new(VeilidConfigLogLevel::Trace, None); - let layer = tracing_android::layer("veilid-core").expect("failed to set up android logging"); + let layer = paranoid_android::layer("veilid-core"); tracing_subscriber::registry() - .with(filter) - .with(layer) + .with(layer.with_filter(filter)) .init(); // Set up panic hook for backtraces diff --git a/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/java/com/veilid/veilid_core_android_tests/MainActivity.java b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/java/com/veilid/veilid_core_android_tests/MainActivity.java index b02d3093..d04edb8e 100644 --- a/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/java/com/veilid/veilid_core_android_tests/MainActivity.java +++ b/veilid-core/src/tests/android/veilid_core_android_tests/app/src/main/java/com/veilid/veilid_core_android_tests/MainActivity.java @@ -22,6 +22,9 @@ public class MainActivity extends AppCompatActivity { } public void run() { + run_tests(this.context); + ((MainActivity)this.context).finish(); + System.exit(0); } } @@ -30,6 +33,7 @@ public class MainActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - run_tests(this.context); + this.testThread = new TestThread(this); + this.testThread.start(); } } diff --git a/veilid-core/src/tests/common/test_veilid_config.rs b/veilid-core/src/tests/common/test_veilid_config.rs index 1eab6ea4..b7b0db01 100644 --- a/veilid-core/src/tests/common/test_veilid_config.rs +++ b/veilid-core/src/tests/common/test_veilid_config.rs @@ -85,7 +85,7 @@ cfg_if! { fn get_data_dir() -> PathBuf { cfg_if! { if #[cfg(target_os = "android")] { - PathBuf::from(intf::utils::android::get_files_dir()) + PathBuf::from(crate::intf::android::get_files_dir()) } else { use directories::*; diff --git a/veilid-flutter/rust/src/lib.rs b/veilid-flutter/rust/src/lib.rs index 30a72cc6..bd89a6b7 100644 --- a/veilid-flutter/rust/src/lib.rs +++ b/veilid-flutter/rust/src/lib.rs @@ -13,5 +13,5 @@ pub extern "system" fn Java_com_veilid_veilid_VeilidPlugin_init_1android( _class: JClass, ctx: JObject, ) { - veilid_core::veilid_core_setup_android_no_log(env, ctx); + veilid_core::veilid_core_setup_android(env, ctx); } diff --git a/veilid-tools/Cargo.toml b/veilid-tools/Cargo.toml index 290eda93..daea35b8 100644 --- a/veilid-tools/Cargo.toml +++ b/veilid-tools/Cargo.toml @@ -14,7 +14,7 @@ default = [] rt-async-std = [ "async-std", "async_executors/async_std", ] rt-tokio = [ "tokio", "tokio-util", "async_executors/tokio_tp", "async_executors/tokio_io", "async_executors/tokio_timer", ] -veilid_tools_android_tests = [ "dep:tracing-android" ] +veilid_tools_android_tests = [ "dep:paranoid-android" ] veilid_tools_ios_tests = [ "dep:oslog", "dep:tracing-oslog" ] tracing = [ "dep:tracing", "dep:tracing-subscriber" ] @@ -62,7 +62,7 @@ jni-sys = "^0" ndk = { version = "^0.7" } ndk-glue = { version = "^0.7", features = ["logger"] } lazy_static = "^1.4.0" -tracing-android = { version = "^0", optional = true } +paranoid-android = { version = "^0", optional = true } android-logd-logger = "0.2.1" # Dependencies for Windows diff --git a/veilid-tools/src/tests/android/mod.rs b/veilid-tools/src/tests/android/mod.rs index 9a89e145..1cba5195 100644 --- a/veilid-tools/src/tests/android/mod.rs +++ b/veilid-tools/src/tests/android/mod.rs @@ -1,8 +1,6 @@ use super::native::*; use super::*; -use jni::{objects::GlobalRef, objects::JObject, JNIEnv, JavaVM}; -use lazy_static::*; use std::backtrace::Backtrace; use std::panic; @@ -36,8 +34,7 @@ pub fn veilid_tools_setup_android_tests() { // Set up subscriber and layers let subscriber = Registry::default(); let mut layers = Vec::new(); - let layer = tracing_android::layer("veilid-tools") - .expect("failed to set up android logging") + let layer = paranoid_android::layer("veilid-tools") .with_filter(filter::LevelFilter::TRACE) .with_filter(filters); layers.push(layer.boxed()); @@ -48,7 +45,7 @@ pub fn veilid_tools_setup_android_tests() { .expect("failed to init android tracing"); } else { let mut builder = android_logd_logger::builder(); - builder.tag(log_tag); + builder.tag("veilid-tools"); builder.prepend_module(true); builder.filter_level(LevelFilter::Trace); for ig in DEFAULT_LOG_IGNORE_LIST { From b5c87e4882a00c5a5fd395c6123bb399872024a5 Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 1 Dec 2022 16:49:37 -0500 Subject: [PATCH 29/88] log fixes --- veilid-core/run_tests.sh | 3 +++ veilid-core/src/tests/common/test_veilid_config.rs | 4 ++-- veilid-core/src/tests/ios/mod.rs | 2 +- veilid-tools/run_tests.sh | 3 +++ veilid-tools/src/tests/ios/mod.rs | 4 ++-- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/veilid-core/run_tests.sh b/veilid-core/run_tests.sh index 7334bec4..0a8ec9af 100755 --- a/veilid-core/run_tests.sh +++ b/veilid-core/run_tests.sh @@ -19,7 +19,10 @@ elif [[ "$1" == "ios" ]]; then # Run in temporary simulator xcrun simctl install $ID $SYMROOT/Debug-iphonesimulator/$APPNAME.app + xcrun simctl spawn $ID log stream --level debug --predicate "subsystem == \"$BUNDLENAME\"" & xcrun simctl launch --console $ID $BUNDLENAME + sleep 1 # Ensure the last log lines print + kill -INT %1 # Clean up build output rm -rf /tmp/testout diff --git a/veilid-core/src/tests/common/test_veilid_config.rs b/veilid-core/src/tests/common/test_veilid_config.rs index b7b0db01..3743ca7e 100644 --- a/veilid-core/src/tests/common/test_veilid_config.rs +++ b/veilid-core/src/tests/common/test_veilid_config.rs @@ -156,8 +156,8 @@ cfg_if! { } } -fn update_callback(update: VeilidUpdate) { - println!("update_callback: {:?}", update); +fn update_callback(_update: VeilidUpdate) { + // println!("update_callback: {:?}", update); } pub fn setup_veilid_core() -> (UpdateCallback, ConfigCallback) { diff --git a/veilid-core/src/tests/ios/mod.rs b/veilid-core/src/tests/ios/mod.rs index 31339981..fe4fc783 100644 --- a/veilid-core/src/tests/ios/mod.rs +++ b/veilid-core/src/tests/ios/mod.rs @@ -18,7 +18,7 @@ pub fn veilid_core_setup_ios_tests() { // Set up subscriber and layers let filter = VeilidLayerFilter::new(VeilidConfigLogLevel::Trace, None); tracing_subscriber::registry() - .with(OsLogger::new("com.veilid.veilidtools-tests", "default").with_filter(filter)) + .with(OsLogger::new("com.veilid.veilidcore-tests", "").with_filter(filter)) .init(); panic::set_hook(Box::new(|panic_info| { diff --git a/veilid-tools/run_tests.sh b/veilid-tools/run_tests.sh index ad09bb88..de450f39 100755 --- a/veilid-tools/run_tests.sh +++ b/veilid-tools/run_tests.sh @@ -19,7 +19,10 @@ elif [[ "$1" == "ios" ]]; then # Run in temporary simulator xcrun simctl install $ID $SYMROOT/Debug-iphonesimulator/$APPNAME.app + xcrun simctl spawn $ID log stream --level debug --predicate "subsystem == \"$BUNDLENAME\"" & xcrun simctl launch --console $ID $BUNDLENAME + sleep 1 # Ensure the last log lines print + kill -INT %1 # Clean up build output rm -rf /tmp/testout diff --git a/veilid-tools/src/tests/ios/mod.rs b/veilid-tools/src/tests/ios/mod.rs index af843827..77534395 100644 --- a/veilid-tools/src/tests/ios/mod.rs +++ b/veilid-tools/src/tests/ios/mod.rs @@ -25,12 +25,12 @@ pub fn veilid_tools_setup_ios_tests() { tracing_subscriber::registry() .with(filters) .with(filter::LevelFilter::TRACE) - .with(OsLogger::new("com.veilid.veilidtools-tests", "default")) + .with(OsLogger::new("com.veilid.veilidtools-tests", "")) .init(); } else { use oslog::OsLogger; - OsLogger::new("com.veilid.veilidtools-tests", "default") + OsLogger::new("com.veilid.veilidtools-tests") .level_filter(LevelFilter::Trace) .init() .unwrap(); From 5d8fa97360faa56365194f69386b627d5cf23333 Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 1 Dec 2022 17:38:43 -0500 Subject: [PATCH 30/88] fix server --- veilid-cli/src/client_api_connection.rs | 2 +- veilid-cli/src/command_processor.rs | 2 +- veilid-cli/src/main.rs | 2 +- veilid-server/src/server.rs | 2 +- veilid-server/src/settings.rs | 5 +---- veilid-wasm/src/lib.rs | 2 +- 6 files changed, 6 insertions(+), 9 deletions(-) diff --git a/veilid-cli/src/client_api_connection.rs b/veilid-cli/src/client_api_connection.rs index 9715a450..0c8980aa 100644 --- a/veilid-cli/src/client_api_connection.rs +++ b/veilid-cli/src/client_api_connection.rs @@ -8,7 +8,7 @@ use serde::de::DeserializeOwned; use std::cell::RefCell; use std::net::SocketAddr; use std::rc::Rc; -use veilid_core::xx::*; +use veilid_core::tools::*; use veilid_core::*; macro_rules! capnp_failed { diff --git a/veilid-cli/src/command_processor.rs b/veilid-cli/src/command_processor.rs index 88674eeb..f015db78 100644 --- a/veilid-cli/src/command_processor.rs +++ b/veilid-cli/src/command_processor.rs @@ -6,7 +6,7 @@ use std::cell::*; use std::net::SocketAddr; use std::rc::Rc; use std::time::{Duration, SystemTime}; -use veilid_core::xx::*; +use veilid_core::tools::*; use veilid_core::*; pub fn convert_loglevel(s: &str) -> Result { diff --git a/veilid-cli/src/main.rs b/veilid-cli/src/main.rs index 0ecd7b37..389db983 100644 --- a/veilid-cli/src/main.rs +++ b/veilid-cli/src/main.rs @@ -1,7 +1,7 @@ #![deny(clippy::all)] #![deny(unused_must_use)] -use veilid_core::xx::*; +use veilid_core::tools::*; use clap::{Arg, ColorChoice, Command}; use flexi_logger::*; diff --git a/veilid-server/src/server.rs b/veilid-server/src/server.rs index ca936a30..45385203 100644 --- a/veilid-server/src/server.rs +++ b/veilid-server/src/server.rs @@ -11,7 +11,7 @@ use parking_lot::Mutex; use std::sync::Arc; use std::time::{Duration, Instant}; use tracing::*; -use veilid_core::xx::SingleShotEventual; +use veilid_core::tools::SingleShotEventual; #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum ServerMode { diff --git a/veilid-server/src/settings.rs b/veilid-server/src/settings.rs index 1636acc0..9a6e6259 100644 --- a/veilid-server/src/settings.rs +++ b/veilid-server/src/settings.rs @@ -1,16 +1,13 @@ #![allow(clippy::bool_assert_comparison)] use directories::*; -use parking_lot::*; use serde_derive::*; use std::ffi::OsStr; use std::net::SocketAddr; use std::path::{Path, PathBuf}; -use std::str::FromStr; -use std::sync::Arc; use url::Url; -use veilid_core::xx::*; +use veilid_core::tools::*; use veilid_core::*; pub fn load_default_config() -> EyreResult { diff --git a/veilid-wasm/src/lib.rs b/veilid-wasm/src/lib.rs index 3fd7e891..34fec05b 100644 --- a/veilid-wasm/src/lib.rs +++ b/veilid-wasm/src/lib.rs @@ -19,7 +19,7 @@ use tracing::*; use tracing_subscriber::prelude::*; use tracing_subscriber::*; use tracing_wasm::{WASMLayerConfigBuilder, *}; -use veilid_core::xx::*; +use veilid_core::tools::*; use veilid_core::*; use wasm_bindgen_futures::*; From 9cb5e7226730bdd8c9507940be9b393c17dbe53f Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 1 Dec 2022 18:16:49 -0500 Subject: [PATCH 31/88] move gitignore --- veilid-core/src/tests/ios/{ => veilidcore-tests}/.gitignore | 0 veilid-tools/src/tests/ios/{ => veilidtools-tests}/.gitignore | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename veilid-core/src/tests/ios/{ => veilidcore-tests}/.gitignore (100%) rename veilid-tools/src/tests/ios/{ => veilidtools-tests}/.gitignore (100%) diff --git a/veilid-core/src/tests/ios/.gitignore b/veilid-core/src/tests/ios/veilidcore-tests/.gitignore similarity index 100% rename from veilid-core/src/tests/ios/.gitignore rename to veilid-core/src/tests/ios/veilidcore-tests/.gitignore diff --git a/veilid-tools/src/tests/ios/.gitignore b/veilid-tools/src/tests/ios/veilidtools-tests/.gitignore similarity index 100% rename from veilid-tools/src/tests/ios/.gitignore rename to veilid-tools/src/tests/ios/veilidtools-tests/.gitignore From 0427922116518ba5915e7ad443a9433f6b3b8717 Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 1 Dec 2022 18:25:53 -0500 Subject: [PATCH 32/88] remove cruft fix earthfile --- Earthfile | 6 +++--- veilid-core/src/tests/.gitignore | 4 ---- 2 files changed, 3 insertions(+), 7 deletions(-) delete mode 100644 veilid-core/src/tests/.gitignore diff --git a/Earthfile b/Earthfile index 6d385262..7575904e 100644 --- a/Earthfile +++ b/Earthfile @@ -52,9 +52,9 @@ deps-android: FROM +deps-cross RUN apt-get install -y openjdk-9-jdk-headless RUN mkdir /Android; mkdir /Android/Sdk - RUN curl -o /Android/cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip + RUN curl -o /Android/cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-9123335_latest.zip RUN cd /Android; unzip /Android/cmdline-tools.zip - RUN yes | /Android/cmdline-tools/bin/sdkmanager --sdk_root=/Android/Sdk build-tools\;30.0.3 ndk\;22.0.7026061 cmake\;3.18.1 platform-tools platforms\;android-30 + RUN yes | /Android/cmdline-tools/bin/sdkmanager --sdk_root=/Android/Sdk build-tools\;33.0.1 ndk\;25.1.8937393 cmake\;3.22.1 platform-tools platforms\;android-33 RUN apt-get clean # Just linux build not android @@ -96,7 +96,7 @@ build-linux-arm64: build-android: FROM +code-android WORKDIR /veilid/veilid-core - ENV PATH=$PATH:/Android/Sdk/ndk/22.0.7026061/toolchains/llvm/prebuilt/linux-x86_64/bin/ + ENV PATH=$PATH:/Android/Sdk/ndk/25.1.8937393/toolchains/llvm/prebuilt/linux-x86_64/bin/ RUN cargo build --target aarch64-linux-android --release RUN cargo build --target armv7-linux-androideabi --release RUN cargo build --target i686-linux-android --release diff --git a/veilid-core/src/tests/.gitignore b/veilid-core/src/tests/.gitignore deleted file mode 100644 index e8c47856..00000000 --- a/veilid-core/src/tests/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# exclude everything -tmp/* -# exception to the rule -!tmp/.gitkeep From 4c3ffa927bf8415bc0c5813102461b7c3855ef8c Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 1 Dec 2022 18:40:17 -0500 Subject: [PATCH 33/88] add tests for windows --- veilid-core/run_windows_tests.bat | 4 ++++ veilid-tools/run_windows_tests.bat | 5 +++++ 2 files changed, 9 insertions(+) create mode 100644 veilid-core/run_windows_tests.bat create mode 100644 veilid-tools/run_windows_tests.bat diff --git a/veilid-core/run_windows_tests.bat b/veilid-core/run_windows_tests.bat new file mode 100644 index 00000000..7ab6b2a3 --- /dev/null +++ b/veilid-core/run_windows_tests.bat @@ -0,0 +1,4 @@ +@echo off +cargo test --features=rt-tokio -- --nocapture +cargo test --features=rt-async-std -- --nocapture + diff --git a/veilid-tools/run_windows_tests.bat b/veilid-tools/run_windows_tests.bat new file mode 100644 index 00000000..66998e0d --- /dev/null +++ b/veilid-tools/run_windows_tests.bat @@ -0,0 +1,5 @@ +@echo off +cargo test --features=rt-tokio,tracing -- --nocapture +cargo test --features=rt-async-std,tracing -- --nocapture +cargo test --features=rt-tokio -- --nocapture +cargo test --features=rt-async-std -- --nocapture From bbf97a535aeb379cc6b9f60fdec7629bcdba4c64 Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 1 Dec 2022 19:08:40 -0500 Subject: [PATCH 34/88] windows support and more recursion fixes --- setup_windows.bat | 38 ++++++++++++++++++++++++++++++++++ veilid-cli/src/main.rs | 1 + veilid-flutter/rust/src/lib.rs | 2 ++ veilid-server/src/main.rs | 1 + 4 files changed, 42 insertions(+) create mode 100644 setup_windows.bat diff --git a/setup_windows.bat b/setup_windows.bat new file mode 100644 index 00000000..c502034b --- /dev/null +++ b/setup_windows.bat @@ -0,0 +1,38 @@ +@echo off +setlocal + +REM ############################################# + +PUSHD %~dp0 +SET ROOTDIR=%CD% +POPD + +IF NOT DEFINED ProgramFiles(x86) ( + echo This script requires a 64-bit Windows Installation. Exiting. + goto end +) + +FOR %%X IN (protoc.exe) DO (SET PROTOC_FOUND=%%~$PATH:X) +IF NOT DEFINED PROTOC_FOUND ( + echo protobuf compiler ^(protoc^) is required but it's not installed. Install protoc 21.10 or higher. Ensure it is in your path. Aborting. + echo protoc is available here: https://github.com/protocolbuffers/protobuf/releases/download/v21.10/protoc-21.10-win64.zip + goto end +) + +FOR %%X IN (capnp.exe) DO (SET CAPNP_FOUND=%%~$PATH:X) +IF NOT DEFINED CAPNP_FOUND ( + echo capnproto compiler ^(capnp^) is required but it's not installed. Install capnp 0.10.3 or higher. Ensure it is in your path. Aborting. + echo capnp is available here: https://capnproto.org/capnproto-c++-win32-0.10.3.zip + goto end +) + +FOR %%X IN (cargo.exe) DO (SET CARGO_FOUND=%%~$PATH:X) +IF NOT DEFINED CARGO_FOUND ( + echo rust ^(cargo^) is required but it's not installed. Install rust 1.65 or higher. Ensure it is in your path. Aborting. + echo install rust via rustup here: https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe + goto ends +) + +echo Setup successful +:end +ENDLOCAL diff --git a/veilid-cli/src/main.rs b/veilid-cli/src/main.rs index 389db983..13692b14 100644 --- a/veilid-cli/src/main.rs +++ b/veilid-cli/src/main.rs @@ -1,5 +1,6 @@ #![deny(clippy::all)] #![deny(unused_must_use)] +#![recursion_limit = "256"] use veilid_core::tools::*; diff --git a/veilid-flutter/rust/src/lib.rs b/veilid-flutter/rust/src/lib.rs index bd89a6b7..2121c5ce 100644 --- a/veilid-flutter/rust/src/lib.rs +++ b/veilid-flutter/rust/src/lib.rs @@ -1,3 +1,5 @@ +#![recursion_limit = "256"] + mod dart_ffi; mod dart_isolate_wrapper; mod tools; diff --git a/veilid-server/src/main.rs b/veilid-server/src/main.rs index 3856731d..7dd76d54 100644 --- a/veilid-server/src/main.rs +++ b/veilid-server/src/main.rs @@ -1,6 +1,7 @@ #![forbid(unsafe_code)] #![deny(clippy::all)] #![deny(unused_must_use)] +#![recursion_limit = "256"] mod client_api; mod cmdline; From 46504e44b8ce5eb8942991a19ffc1c5789257da2 Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 1 Dec 2022 19:59:19 -0500 Subject: [PATCH 35/88] fix cli --- veilid-cli/src/command_processor.rs | 5 ++--- veilid-cli/src/main.rs | 1 + veilid-cli/src/tools.rs | 10 ---------- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/veilid-cli/src/command_processor.rs b/veilid-cli/src/command_processor.rs index f015db78..39d91f18 100644 --- a/veilid-cli/src/command_processor.rs +++ b/veilid-cli/src/command_processor.rs @@ -1,11 +1,10 @@ use crate::client_api_connection::*; use crate::settings::Settings; use crate::ui::*; -use log::*; use std::cell::*; use std::net::SocketAddr; use std::rc::Rc; -use std::time::{Duration, SystemTime}; +use std::time::SystemTime; use veilid_core::tools::*; use veilid_core::*; @@ -365,7 +364,7 @@ reply - reply to an AppCall not handled directly by the server debug!("Connection lost, retrying in 2 seconds"); { let waker = self.inner_mut().connection_waker.instance_clone(()); - let _ = timeout(Duration::from_millis(2000), waker).await; + let _ = timeout(2000, waker).await; } self.inner_mut().connection_waker.reset(); first = false; diff --git a/veilid-cli/src/main.rs b/veilid-cli/src/main.rs index 13692b14..9239478f 100644 --- a/veilid-cli/src/main.rs +++ b/veilid-cli/src/main.rs @@ -2,6 +2,7 @@ #![deny(unused_must_use)] #![recursion_limit = "256"] +use crate::tools::*; use veilid_core::tools::*; use clap::{Arg, ColorChoice, Command}; diff --git a/veilid-cli/src/tools.rs b/veilid-cli/src/tools.rs index 6202d490..bf58d24c 100644 --- a/veilid-cli/src/tools.rs +++ b/veilid-cli/src/tools.rs @@ -3,22 +3,12 @@ use core::future::Future; cfg_if! { if #[cfg(feature="rt-async-std")] { - pub use async_std::task::JoinHandle; pub use async_std::net::TcpStream; - pub use async_std::future::TimeoutError; - - pub use async_std::task::sleep; - pub use async_std::future::timeout; pub fn block_on, T>(f: F) -> T { async_std::task::block_on(f) } } else if #[cfg(feature="rt-tokio")] { - pub use tokio::task::JoinHandle; pub use tokio::net::TcpStream; - pub use tokio::time::error::Elapsed as TimeoutError; - - pub use tokio::time::sleep; - pub use tokio::time::timeout; pub fn block_on, T>(f: F) -> T { let rt = tokio::runtime::Runtime::new().unwrap(); let local = tokio::task::LocalSet::new(); From ef313133cf6f33f0c00cff56b9b9a91f81c79bd2 Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 1 Dec 2022 20:38:57 -0500 Subject: [PATCH 36/88] unify ios mac build --- scripts/ios_build.sh | 9 ++- scripts/macos_build.sh | 61 +++++++++++++++++++ .../project.pbxproj | 2 +- veilid-flutter/ios/veilid.podspec | 11 ++-- veilid-flutter/macos/veilid.podspec | 7 ++- veilid-flutter/rust/Cargo.toml | 3 +- veilid-flutter/rust/ios_build.sh | 55 ----------------- veilid-flutter/rust/macos_build.sh | 54 ---------------- .../project.pbxproj | 2 +- 9 files changed, 78 insertions(+), 126 deletions(-) create mode 100755 scripts/macos_build.sh delete mode 100755 veilid-flutter/rust/ios_build.sh delete mode 100755 veilid-flutter/rust/macos_build.sh diff --git a/scripts/ios_build.sh b/scripts/ios_build.sh index 4062014b..8242c693 100755 --- a/scripts/ios_build.sh +++ b/scripts/ios_build.sh @@ -4,10 +4,10 @@ CARGO=`which cargo` CARGO=${CARGO:=~/.cargo/bin/cargo} CARGO_DIR=$(dirname $CARGO) -WORKING_DIR=$1 -shift -echo $WORKING_DIR -pushd $WORKING_DIR >/dev/null +# WORKING_DIR=$1 +# shift +# echo $WORKING_DIR +# pushd $WORKING_DIR >/dev/null echo PWD: `pwd` CARGO_MANIFEST_PATH=$(python3 -c "import os; import json; print(json.loads(os.popen('$CARGO locate-project').read())['root'])") @@ -40,7 +40,6 @@ do else CARGO_TARGET=aarch64-apple-ios fi - #CARGO_TOOLCHAIN=+ios-arm64-1.57.0 CARGO_TOOLCHAIN= elif [ "$arch" == "x86_64" ]; then echo x86_64 diff --git a/scripts/macos_build.sh b/scripts/macos_build.sh new file mode 100755 index 00000000..0ef14c81 --- /dev/null +++ b/scripts/macos_build.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +CARGO=`which cargo` +CARGO=${CARGO:=~/.cargo/bin/cargo} +CARGO_DIR=$(dirname $CARGO) + +# WORKING_DIR=$1 +# shift +# echo $WORKING_DIR +# pushd $WORKING_DIR >/dev/null +# echo PWD: `pwd` + +CARGO_MANIFEST_PATH=$(python3 -c "import os; import json; print(json.loads(os.popen('$CARGO locate-project').read())['root'])") +CARGO_WORKSPACE_PATH=$(python3 -c "import os; import json; print(json.loads(os.popen('$CARGO locate-project --workspace').read())['root'])") +TARGET_PATH=$(python3 -c "import os; print(os.path.realpath(\"$CARGO_WORKSPACE_PATH/../target\"))") +PACKAGE_NAME=$1 +shift + +if [ "$CONFIGURATION" == "Debug" ]; then + EXTRA_CARGO_OPTIONS="$@" + BUILD_MODE="debug" +else + EXTRA_CARGO_OPTIONS="$@ --release" + BUILD_MODE="release" +fi +ARCHS=${ARCHS:=arm64} + +LIPO_OUT_NAME="lipo-darwin" + +for arch in $ARCHS +do + if [ "$arch" == "arm64" ]; then + echo arm64 + CARGO_TARGET=aarch64-apple-darwin + CARGO_TOOLCHAIN= + elif [ "$arch" == "x86_64" ]; then + echo x86_64 + CARGO_TARGET=x86_64-apple-darwin + CARGO_TOOLCHAIN= + else + echo Unsupported ARCH: $arch + continue + fi + + # Choose arm64 brew for unit tests by default if we are on M1 + if [ -f /opt/homebrew/bin/brew ]; then + HOMEBREW_DIR=/opt/homebrew/bin + elif [ -f /usr/local/bin/brew ]; then + HOMEBREW_DIR=/usr/local/bin + else + HOMEBREW_DIR=$(dirname `which brew`) + fi + + env -i PATH=/usr/bin:/bin:$HOMEBREW_DIR:$CARGO_DIR HOME="$HOME" USER="$USER" cargo $CARGO_TOOLCHAIN build $EXTRA_CARGO_OPTIONS --target $CARGO_TARGET --manifest-path $CARGO_MANIFEST_PATH + + LIPOS="$LIPOS $TARGET_PATH/$CARGO_TARGET/$BUILD_MODE/lib$PACKAGE_NAME.a" + +done + +mkdir -p "$TARGET_PATH/$LIPO_OUT_NAME/$BUILD_MODE/" +lipo $LIPOS -create -output "$TARGET_PATH/$LIPO_OUT_NAME/$BUILD_MODE/lib$PACKAGE_NAME.a" diff --git a/veilid-core/src/tests/ios/veilidcore-tests/veilidcore-tests.xcodeproj/project.pbxproj b/veilid-core/src/tests/ios/veilidcore-tests/veilidcore-tests.xcodeproj/project.pbxproj index 85356d38..2b35a5b9 100644 --- a/veilid-core/src/tests/ios/veilidcore-tests/veilidcore-tests.xcodeproj/project.pbxproj +++ b/veilid-core/src/tests/ios/veilidcore-tests/veilidcore-tests.xcodeproj/project.pbxproj @@ -167,7 +167,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "../../../../../scripts/ios_build.sh ../../../../ veilid_core --features veilid_core_ios_tests,rt-tokio\n"; + shellScript = "../../../../../scripts/ios_build.sh veilid_core --features veilid_core_ios_tests,rt-tokio\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/veilid-flutter/ios/veilid.podspec b/veilid-flutter/ios/veilid.podspec index 9088b982..003442e7 100644 --- a/veilid-flutter/ios/veilid.podspec +++ b/veilid-flutter/ios/veilid.podspec @@ -23,18 +23,19 @@ Veilid Network Plugin require 'json' require 'pathname' - cargo_target_dir = File.join(File.dirname(JSON.parse(`cargo locate-project`)['root']), 'target') + workspace_dir = File.dirname(JSON.parse(`cargo locate-project --workspace`)['root']) + cargo_target_dir = File.join(workspace_dir, 'target') s.xcconfig = { - 'OTHER_LDFLAGS' => "-Wl,-force_load,#{File.join(cargo_target_dir, 'ios_lib', 'libveilid_flutter.a')}", - "LIBRARY_SEARCH_PATHS" => File.join(cargo_target_dir, 'ios_lib') + 'OTHER_LDFLAGS' => "-Wl,-force_load,#{File.join(cargo_target_dir, 'lipo-ios', 'libveilid_flutter.a')}", + "LIBRARY_SEARCH_PATHS" => File.join(cargo_target_dir, 'lipo-ios') } s.script_phase = { :name => 'Cargo Build', - :script => File.join(File.dirname(__dir__), 'rust', 'ios_build.sh'), + :script => File.join(workspace_dir, 'scripts', 'ios_build.sh') + ' veilid_flutter', :execution_position => :before_compile - # :output_files => [ File.join(cargo_target_dir, 'ios_lib', 'libveilid_flutter.a') ] + # :output_files => [ File.join(cargo_target_dir, 'lipo-ios', 'libveilid_flutter.a') ] } end diff --git a/veilid-flutter/macos/veilid.podspec b/veilid-flutter/macos/veilid.podspec index c4139cd2..6b169c1d 100644 --- a/veilid-flutter/macos/veilid.podspec +++ b/veilid-flutter/macos/veilid.podspec @@ -22,13 +22,14 @@ Veilid Network Plugin require 'json' require 'pathname' - cargo_target_dir = File.join(File.dirname(JSON.parse(`cargo locate-project`)['root']), 'target') + workspace_dir = File.dirname(JSON.parse(`cargo locate-project --workspace`)['root']) + cargo_target_dir = File.join(workspace_dir, 'target') s.script_phase = { :name => 'Cargo Build', - :script => File.join(File.dirname(__dir__), 'rust', 'macos_build.sh'), + :script => File.join(workspace_dir, 'scripts', 'macos_build.sh') + ' veilid_flutter', :execution_position => :before_compile - #:output_files => [ File.join(cargo_target_dir, 'macos_lib', 'libveilid_flutter.dylib') ] + #:output_files => [ File.join(cargo_target_dir, 'lipo-darwin', 'libveilid_flutter.dylib') ] } end diff --git a/veilid-flutter/rust/Cargo.toml b/veilid-flutter/rust/Cargo.toml index 1967f5e1..933a37df 100644 --- a/veilid-flutter/rust/Cargo.toml +++ b/veilid-flutter/rust/Cargo.toml @@ -12,6 +12,7 @@ rt-async-std = [ "veilid-core/rt-async-std", "async-std", "opentelemetry/rt-asyn rt-tokio = [ "veilid-core/rt-tokio", "tokio", "tokio-stream", "tokio-util", "opentelemetry/rt-tokio"] [dependencies] +veilid-core = { path="../../veilid-core" } tracing = { version = "^0", features = ["log", "attributes"] } tracing-subscriber = "^0" parking_lot = "^0" @@ -25,7 +26,6 @@ data-encoding = { version = "^2" } # Dependencies for native builds only # Linux, Windows, Mac, iOS, Android [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -veilid-core = { path="../../veilid-core" } tracing-opentelemetry = "^0" opentelemetry = { version = "^0" } opentelemetry-otlp = { version = "^0" } @@ -41,7 +41,6 @@ hostname = "^0" # Dependencies for WASM builds only [target.'cfg(target_arch = "wasm32")'.dependencies] -veilid-core = { path="../../veilid-core" } # Dependencies for Android builds only [target.'cfg(target_os = "android")'.dependencies] diff --git a/veilid-flutter/rust/ios_build.sh b/veilid-flutter/rust/ios_build.sh deleted file mode 100755 index 7c05d493..00000000 --- a/veilid-flutter/rust/ios_build.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash -set -e -echo Running veilid-flutter rust iOS build script - -# Setup varaiables -SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -FLUTTER_DIR=$(dirname `which flutter`) -HOMEBREW_DIR=$(dirname `which brew`) -CARGO_DIR=$(dirname `which cargo`) -CARGO_MANIFEST_PATH=$(python3 -c "import os; print(os.path.realpath(\"$SCRIPTDIR/Cargo.toml\"))") -TARGET_DIR=$(dirname `cargo locate-project --message-format plain`)/target - -# Configure outputs -OUTPUT_FILENAME=libveilid_flutter.a -OUTPUT_DIR=$TARGET_DIR/ios_lib - -# Get Rust configurations from xcode configurations -if [ "$CONFIGURATION" == "Debug" ]; then - EXTRA_CARGO_OPTIONS="$@" - RUST_CONFIGURATION="debug" -else - EXTRA_CARGO_OPTIONS="$@ --release" - RUST_CONFIGURATION="release" -fi - -# Build all the matching architectures for the xcode configurations -ARCHS=${ARCHS:=arm64} -echo ARCHS: $ARCHS -LIPO_LIST="" -for arch in $ARCHS -do - if [ "$arch" == "arm64" ]; then - echo arm64 - CARGO_TARGET=aarch64-apple-ios - #CARGO_TOOLCHAIN=+ios-arm64-1.57.0 - CARGO_TOOLCHAIN= - elif [ "$arch" == "x86_64" ]; then - echo x86_64 - CARGO_TARGET=x86_64-apple-ios - CARGO_TOOLCHAIN= - else - echo Unsupported ARCH: $arch - continue - fi - - # Cargo build - env -i PATH=/usr/bin:/bin:/usr/local/bin:$HOMEBREW_DIR:$FLUTTER_DIR:$CARGO_DIR HOME="$HOME" USER="$USER" cargo $CARGO_TOOLCHAIN build $EXTRA_CARGO_OPTIONS --target $CARGO_TARGET --manifest-path $CARGO_MANIFEST_PATH - - # Add output to lipo list - LIPO_LIST="$LIPO_LIST $TARGET_DIR/$CARGO_TARGET/$RUST_CONFIGURATION/$OUTPUT_FILENAME" -done - -# Lipo the architectures together -mkdir -p $OUTPUT_DIR -lipo -output "$OUTPUT_DIR/$OUTPUT_FILENAME" -create $LIPO_LIST diff --git a/veilid-flutter/rust/macos_build.sh b/veilid-flutter/rust/macos_build.sh deleted file mode 100755 index 29f180bc..00000000 --- a/veilid-flutter/rust/macos_build.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash -set -e -echo Running veilid-flutter rust MacOS build script - -# Setup varaiables -SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -FLUTTER_DIR=$(dirname `which flutter`) -HOMEBREW_DIR=$(dirname `which brew`) -CARGO_DIR=$(dirname `which cargo`) -CARGO_MANIFEST_PATH=$(python3 -c "import os; print(os.path.realpath(\"$SCRIPTDIR/Cargo.toml\"))") -TARGET_DIR=$(dirname `cargo locate-project --message-format plain`)/target - -# Configure outputs -OUTPUT_FILENAME=libveilid_flutter.dylib -OUTPUT_DIR=$TARGET_DIR/macos_lib - -# Get Rust configurations from xcode configurations -if [ "$CONFIGURATION" == "Debug" ]; then - EXTRA_CARGO_OPTIONS="$@" - RUST_CONFIGURATION="debug" -else - EXTRA_CARGO_OPTIONS="$@ --release" - RUST_CONFIGURATION="release" -fi - -# Build all the matching architectures for the xcode configurations -ARCHS=${ARCHS:=x86_64} -echo ARCHS: $ARCHS -LIPO_LIST="" -for arch in $ARCHS -do - if [ "$arch" == "arm64" ]; then - echo arm64 - CARGO_TARGET=aarch64-apple-darwin - CARGO_TOOLCHAIN= - elif [ "$arch" == "x86_64" ]; then - echo x86_64 - CARGO_TARGET=x86_64-apple-darwin - CARGO_TOOLCHAIN= - else - echo Unsupported ARCH: $arch - continue - fi - - # Cargo build - env -i PATH=/usr/bin:/bin:/usr/local/bin:$HOMEBREW_DIR:$FLUTTER_DIR:$CARGO_DIR HOME="$HOME" USER="$USER" cargo $CARGO_TOOLCHAIN build $EXTRA_CARGO_OPTIONS --target $CARGO_TARGET --manifest-path $CARGO_MANIFEST_PATH - - # Add output to lipo list - LIPO_LIST="$LIPO_LIST $TARGET_DIR/$CARGO_TARGET/$RUST_CONFIGURATION/$OUTPUT_FILENAME" -done - -# Lipo the architectures together -mkdir -p $OUTPUT_DIR -lipo -output "$OUTPUT_DIR/$OUTPUT_FILENAME" -create $LIPO_LIST diff --git a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj index 153d3208..c34e5d62 100644 --- a/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj +++ b/veilid-tools/src/tests/ios/veilidtools-tests/veilidtools-tests.xcodeproj/project.pbxproj @@ -167,7 +167,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "../../../../../scripts/ios_build.sh ../../../../ veilid_tools --features veilid_tools_ios_tests,rt-tokio\n"; + shellScript = "../../../../../scripts/ios_build.sh veilid_tools --features veilid_tools_ios_tests,rt-tokio\n"; }; /* End PBXShellScriptBuildPhase section */ From 60aa3fafc0c0014a527c125870b7e3aeb0bf3391 Mon Sep 17 00:00:00 2001 From: John Smith Date: Fri, 2 Dec 2022 22:52:03 -0500 Subject: [PATCH 37/88] js api --- veilid-flutter/example/macos/Podfile.lock | 2 +- veilid-flutter/lib/veilid_ffi.dart | 44 +++-- veilid-flutter/lib/veilid_js.dart | 95 +++++----- veilid-wasm/src/lib.rs | 214 +++++++++++++++++++++- 4 files changed, 282 insertions(+), 73 deletions(-) diff --git a/veilid-flutter/example/macos/Podfile.lock b/veilid-flutter/example/macos/Podfile.lock index b63b0c0e..27a526e1 100644 --- a/veilid-flutter/example/macos/Podfile.lock +++ b/veilid-flutter/example/macos/Podfile.lock @@ -21,7 +21,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811 path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 - veilid: 6bed3adec63fd8708a2ace498e0e17941c9fc32b + veilid: f2b3b5b3ac8cd93fc5443ab830d5153575dacf36 PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c diff --git a/veilid-flutter/lib/veilid_ffi.dart b/veilid-flutter/lib/veilid_ffi.dart index 63ca4eea..c3ef7a4f 100644 --- a/veilid-flutter/lib/veilid_ffi.dart +++ b/veilid-flutter/lib/veilid_ffi.dart @@ -337,30 +337,40 @@ Stream processStreamJson( } } +class _Ctx { + final int id; + final VeilidFFI ffi; + _Ctx(this.id, this.ffi); +} + // FFI implementation of VeilidRoutingContext class VeilidRoutingContextFFI implements VeilidRoutingContext { - final int _id; - final VeilidFFI _ffi; + final _Ctx _ctx; + static final Finalizer<_Ctx> _finalizer = + Finalizer((ctx) => {ctx.ffi._releaseRoutingContext(ctx.id)}); + + VeilidRoutingContextFFI._(this._ctx) { + _finalizer.attach(this, _ctx, detach: this); + } - VeilidRoutingContextFFI._(this._id, this._ffi); @override VeilidRoutingContextFFI withPrivacy() { - final newId = _ffi._routingContextWithPrivacy(_id); - return VeilidRoutingContextFFI._(newId, _ffi); + final newId = _ctx.ffi._routingContextWithPrivacy(_ctx.id); + return VeilidRoutingContextFFI._(_Ctx(newId, _ctx.ffi)); } @override VeilidRoutingContextFFI withCustomPrivacy(Stability stability) { - final newId = _ffi._routingContextWithCustomPrivacy( - _id, stability.json.toNativeUtf8()); - return VeilidRoutingContextFFI._(newId, _ffi); + final newId = _ctx.ffi._routingContextWithCustomPrivacy( + _ctx.id, stability.json.toNativeUtf8()); + return VeilidRoutingContextFFI._(_Ctx(newId, _ctx.ffi)); } @override VeilidRoutingContextFFI withSequencing(Sequencing sequencing) { - final newId = - _ffi._routingContextWithSequencing(_id, sequencing.json.toNativeUtf8()); - return VeilidRoutingContextFFI._(newId, _ffi); + final newId = _ctx.ffi + ._routingContextWithSequencing(_ctx.id, sequencing.json.toNativeUtf8()); + return VeilidRoutingContextFFI._(_Ctx(newId, _ctx.ffi)); } @override @@ -370,8 +380,8 @@ class VeilidRoutingContextFFI implements VeilidRoutingContext { final recvPort = ReceivePort("routing_context_app_call"); final sendPort = recvPort.sendPort; - _ffi._routingContextAppCall( - sendPort.nativePort, _id, nativeEncodedTarget, nativeEncodedRequest); + _ctx.ffi._routingContextAppCall(sendPort.nativePort, _ctx.id, + nativeEncodedTarget, nativeEncodedRequest); final out = await processFuturePlain(recvPort.first); return base64Decode(out); } @@ -381,10 +391,10 @@ class VeilidRoutingContextFFI implements VeilidRoutingContext { var nativeEncodedTarget = target.toNativeUtf8(); var nativeEncodedMessage = base64UrlEncode(message).toNativeUtf8(); - final recvPort = ReceivePort("routing_context_app_call"); + final recvPort = ReceivePort("routing_context_app_message"); final sendPort = recvPort.sendPort; - _ffi._routingContextAppCall( - sendPort.nativePort, _id, nativeEncodedTarget, nativeEncodedMessage); + _ctx.ffi._routingContextAppMessage(sendPort.nativePort, _ctx.id, + nativeEncodedTarget, nativeEncodedMessage); return processFutureVoid(recvPort.first); } } @@ -566,7 +576,7 @@ class VeilidFFI implements Veilid { final sendPort = recvPort.sendPort; _routingContext(sendPort.nativePort); final id = await processFuturePlain(recvPort.first); - return VeilidRoutingContextFFI._(id, this); + return VeilidRoutingContextFFI._(_Ctx(id, this)); } @override diff --git a/veilid-flutter/lib/veilid_js.dart b/veilid-flutter/lib/veilid_js.dart index 7353ae28..5beff613 100644 --- a/veilid-flutter/lib/veilid_js.dart +++ b/veilid-flutter/lib/veilid_js.dart @@ -19,59 +19,62 @@ Future _wrapApiPromise(Object p) { VeilidAPIException.fromJson(jsonDecode(error as String)))); } +class _Ctx { + final int id; + final VeilidJS js; + _Ctx(this.id, this.js); +} + // JS implementation of VeilidRoutingContext class VeilidRoutingContextJS implements VeilidRoutingContext { - final int _id; - final VeilidFFI _ffi; + final _Ctx _ctx; + static final Finalizer<_Ctx> _finalizer = Finalizer((ctx) => { + js_util.callMethod(wasm, "release_routing_context", [ctx.id]) + }); - VeilidRoutingContextFFI._(this._id, this._ffi); - @override - VeilidRoutingContextFFI withPrivacy() { - final newId = _ffi._routingContextWithPrivacy(_id); - return VeilidRoutingContextFFI._(newId, _ffi); + VeilidRoutingContextJS._(this._ctx) { + _finalizer.attach(this, _ctx, detach: this); } @override - VeilidRoutingContextFFI withCustomPrivacy(Stability stability) { - final newId = _ffi._routingContextWithCustomPrivacy( - _id, stability.json.toNativeUtf8()); - return VeilidRoutingContextFFI._(newId, _ffi); + VeilidRoutingContextJS withPrivacy() { + int newId = + js_util.callMethod(wasm, "routing_context_with_privacy", [_ctx.id]); + return VeilidRoutingContextJS._(_Ctx(newId, _ctx.js)); } @override - VeilidRoutingContextFFI withSequencing(Sequencing sequencing) { - final newId = - _ffi._routingContextWithSequencing(_id, sequencing.json.toNativeUtf8()); - return VeilidRoutingContextFFI._(newId, _ffi); + VeilidRoutingContextJS withCustomPrivacy(Stability stability) { + final newId = js_util.callMethod( + wasm, "routing_context_with_custom_privacy", [_ctx.id, stability.json]); + + return VeilidRoutingContextJS._(_Ctx(newId, _ctx.js)); + } + + @override + VeilidRoutingContextJS withSequencing(Sequencing sequencing) { + final newId = js_util.callMethod( + wasm, "routing_context_with_sequencing", [_ctx.id, sequencing.json]); + return VeilidRoutingContextJS._(_Ctx(newId, _ctx.js)); } @override Future appCall(String target, Uint8List request) async { - var nativeEncodedTarget = target.toNativeUtf8(); - var nativeEncodedRequest = base64UrlEncode(request).toNativeUtf8(); + var encodedRequest = base64UrlEncode(request); - final recvPort = ReceivePort("routing_context_app_call"); - final sendPort = recvPort.sendPort; - _ffi._routingContextAppCall( - sendPort.nativePort, _id, nativeEncodedTarget, nativeEncodedRequest); - final out = await processFuturePlain(recvPort.first); - return base64Decode(out); + return base64Decode(await _wrapApiPromise(js_util.callMethod( + wasm, "routing_context_app_call", [_ctx.id, encodedRequest]))); } @override Future appMessage(String target, Uint8List message) async { - var nativeEncodedTarget = target.toNativeUtf8(); - var nativeEncodedMessage = base64UrlEncode(message).toNativeUtf8(); + var encodedMessage = base64UrlEncode(message); - final recvPort = ReceivePort("routing_context_app_call"); - final sendPort = recvPort.sendPort; - _ffi._routingContextAppCall( - sendPort.nativePort, _id, nativeEncodedTarget, nativeEncodedMessage); - return processFutureVoid(recvPort.first); + return _wrapApiPromise(js_util.callMethod( + wasm, "routing_context_app_message", [_ctx.id, encodedMessage])); } } - // JS implementation of high level Veilid API class VeilidJS implements Veilid { @@ -133,30 +136,32 @@ class VeilidJS implements Veilid { js_util.callMethod(wasm, "shutdown_veilid_core", [])); } - @override Future routingContext() async { - final recvPort = ReceivePort("routing_context"); - final sendPort = recvPort.sendPort; - _routingContext(sendPort.nativePort); - final id = await processFuturePlain(recvPort.first); - return VeilidRoutingContextFFI._(id, this); + int id = jsonDecode( + await _wrapApiPromise(js_util.callMethod(wasm, "routing_context", []))); + return VeilidRoutingContextJS._(_Ctx(id, this)); } @override Future newPrivateRoute() async { - final recvPort = ReceivePort("new_private_route"); - final sendPort = recvPort.sendPort; - _newPrivateRoute(sendPort.nativePort); - return processFutureJson(KeyBlob.fromJson, recvPort.first); + Map blobJson = jsonDecode(await _wrapApiPromise( + js_util.callMethod(wasm, "new_private_route", []))); + return KeyBlob.fromJson(blobJson); } @override Future newCustomPrivateRoute( Stability stability, Sequencing sequencing) async { - return _wrapApiPromise( - js_util.callMethod(wasm, "new_custom_private_route", [stability, sequencing])); + var stabilityString = + jsonEncode(stability, toEncodable: veilidApiToEncodable); + var sequencingString = + jsonEncode(sequencing, toEncodable: veilidApiToEncodable); + Map blobJson = jsonDecode(await _wrapApiPromise(js_util + .callMethod( + wasm, "new_private_route", [stabilityString, sequencingString]))); + return KeyBlob.fromJson(blobJson); } @override @@ -178,7 +183,7 @@ class VeilidJS implements Veilid { return _wrapApiPromise( js_util.callMethod(wasm, "app_call_reply", [id, encodedMessage])); } - + @override Future debug(String command) { return _wrapApiPromise(js_util.callMethod(wasm, "debug", [command])); @@ -191,7 +196,7 @@ class VeilidJS implements Veilid { @override VeilidVersion veilidVersion() { - var jsonVersion = + Map jsonVersion = jsonDecode(js_util.callMethod(wasm, "veilid_version", [])); return VeilidVersion( jsonVersion["major"], jsonVersion["minor"], jsonVersion["patch"]); diff --git a/veilid-wasm/src/lib.rs b/veilid-wasm/src/lib.rs index 34fec05b..ceab87c0 100644 --- a/veilid-wasm/src/lib.rs +++ b/veilid-wasm/src/lib.rs @@ -9,6 +9,7 @@ use alloc::sync::Arc; use alloc::*; use core::any::{Any, TypeId}; use core::cell::RefCell; +use core::fmt::Debug; use futures_util::FutureExt; use gloo_utils::format::JsValueSerdeExt; use js_sys::*; @@ -21,6 +22,7 @@ use tracing_subscriber::*; use tracing_wasm::{WASMLayerConfigBuilder, *}; use veilid_core::tools::*; use veilid_core::*; +use wasm_bindgen::prelude::*; use wasm_bindgen_futures::*; // Allocator @@ -57,11 +59,13 @@ fn take_veilid_api() -> Result(val: T) -> JsValue { +pub fn to_json(val: T) -> JsValue { JsValue::from_str(&serialize_json(val)) } -pub fn from_json(val: JsValue) -> Result { +pub fn from_json( + val: JsValue, +) -> Result { let s = val .as_string() .ok_or_else(|| veilid_core::VeilidAPIError::ParseError { @@ -78,7 +82,7 @@ const APIRESULT_UNDEFINED: APIResult<()> = APIResult::Ok(()); pub fn wrap_api_future(future: F) -> Promise where F: Future> + 'static, - T: Serialize + 'static, + T: Serialize + Debug + 'static, { future_to_promise(future.map(|res| { res.map(|v| { @@ -121,7 +125,7 @@ pub struct VeilidWASMConfig { } #[derive(Debug, Deserialize, Serialize)] -pub struct VeilidFFIKeyBlob { +pub struct VeilidKeyBlob { pub key: veilid_core::DHTKey, #[serde(with = "veilid_core::json_as_base64")] pub blob: Vec, @@ -256,17 +260,201 @@ pub fn shutdown_veilid_core() -> Promise { }) } +fn add_routing_context(routing_context: veilid_core::RoutingContext) -> u32 { + let mut next_id: u32 = 1; + let mut rc = (*ROUTING_CONTEXTS).borrow_mut(); + while rc.contains_key(&next_id) { + next_id += 1; + } + rc.insert(next_id, routing_context); + next_id +} + #[wasm_bindgen()] -pub fn debug(command: String) -> Promise { +pub fn routing_context() -> Promise { wrap_api_future(async move { let veilid_api = get_veilid_api()?; - let out = veilid_api.debug(command).await?; - Ok(out) + let routing_context = veilid_api.routing_context(); + let new_id = add_routing_context(routing_context); + APIResult::Ok(new_id) + }) +} + +#[wasm_bindgen()] +pub fn release_routing_context(id: u32) -> i32 { + let mut rc = (*ROUTING_CONTEXTS).borrow_mut(); + if rc.remove(&id).is_none() { + return 0; + } + return 1; +} + +#[wasm_bindgen()] +pub fn routing_context_with_privacy(id: u32) -> u32 { + let rc = (*ROUTING_CONTEXTS).borrow(); + let Some(routing_context) = rc.get(&id) else { + return 0; + }; + let Ok(routing_context) = routing_context.clone().with_privacy() else { + return 0; + }; + let new_id = add_routing_context(routing_context); + new_id +} + +#[wasm_bindgen()] +pub fn routing_context_with_custom_privacy(id: u32, stability: String) -> u32 { + let stability: veilid_core::Stability = veilid_core::deserialize_json(&stability).unwrap(); + + let rc = (*ROUTING_CONTEXTS).borrow(); + let Some(routing_context) = rc.get(&id) else { + return 0; + }; + let Ok(routing_context) = routing_context.clone().with_custom_privacy(stability) else { + return 0; + }; + let new_id = add_routing_context(routing_context); + new_id +} + +#[wasm_bindgen()] +pub fn routing_context_with_sequencing(id: u32, sequencing: String) -> u32 { + let sequencing: veilid_core::Sequencing = veilid_core::deserialize_json(&sequencing).unwrap(); + + let rc = (*ROUTING_CONTEXTS).borrow(); + let Some(routing_context) = rc.get(&id) else { + return 0; + }; + let routing_context = routing_context.clone().with_sequencing(sequencing); + let new_id = add_routing_context(routing_context); + new_id +} + +#[wasm_bindgen()] +pub fn routing_context_app_call(id: u32, target: String, request: String) -> Promise { + let request: Vec = data_encoding::BASE64URL_NOPAD + .decode(request.as_bytes()) + .unwrap(); + wrap_api_future(async move { + let veilid_api = get_veilid_api()?; + let routing_table = veilid_api.routing_table()?; + let rss = routing_table.route_spec_store(); + + let routing_context = { + let rc = (*ROUTING_CONTEXTS).borrow(); + let Some(routing_context) = rc.get(&id) else { + return APIResult::Err(veilid_core::VeilidAPIError::invalid_argument("routing_context_app_call", "id", id)); + }; + routing_context.clone() + }; + + let target: DHTKey = + DHTKey::try_decode(&target).map_err(|e| VeilidAPIError::parse_error(e, &target))?; + + let target = if rss.get_remote_private_route(&target).is_some() { + veilid_core::Target::PrivateRoute(target) + } else { + veilid_core::Target::NodeId(veilid_core::NodeId::new(target)) + }; + + let answer = routing_context.app_call(target, request).await?; + let answer = data_encoding::BASE64URL_NOPAD.encode(&answer); + APIResult::Ok(answer) + }) +} + +#[wasm_bindgen()] +pub fn routing_context_app_message(id: u32, target: String, message: String) -> Promise { + let message: Vec = data_encoding::BASE64URL_NOPAD + .decode(message.as_bytes()) + .unwrap(); + wrap_api_future(async move { + let veilid_api = get_veilid_api()?; + let routing_table = veilid_api.routing_table()?; + let rss = routing_table.route_spec_store(); + + let routing_context = { + let rc = (*ROUTING_CONTEXTS).borrow(); + let Some(routing_context) = rc.get(&id) else { + return APIResult::Err(veilid_core::VeilidAPIError::invalid_argument("routing_context_app_call", "id", id)); + }; + routing_context.clone() + }; + + let target: DHTKey = + DHTKey::try_decode(&target).map_err(|e| VeilidAPIError::parse_error(e, &target))?; + + let target = if rss.get_remote_private_route(&target).is_some() { + veilid_core::Target::PrivateRoute(target) + } else { + veilid_core::Target::NodeId(veilid_core::NodeId::new(target)) + }; + + routing_context.app_message(target, message).await?; + APIRESULT_UNDEFINED + }) +} + +#[wasm_bindgen()] +pub fn new_private_route() -> Promise { + wrap_api_future(async move { + let veilid_api = get_veilid_api()?; + + let (key, blob) = veilid_api.new_private_route().await?; + + let keyblob = VeilidKeyBlob { key, blob }; + + APIResult::Ok(keyblob) + }) +} + +#[wasm_bindgen()] +pub fn new_custom_private_route(stability: String, sequencing: String) -> Promise { + let stability: veilid_core::Stability = veilid_core::deserialize_json(&stability).unwrap(); + let sequencing: veilid_core::Sequencing = veilid_core::deserialize_json(&sequencing).unwrap(); + + wrap_api_future(async move { + let veilid_api = get_veilid_api()?; + + let (key, blob) = veilid_api + .new_custom_private_route(stability, sequencing) + .await?; + + let keyblob = VeilidKeyBlob { key, blob }; + + APIResult::Ok(keyblob) + }) +} + +#[wasm_bindgen()] +pub fn import_remote_private_route(blob: String) -> Promise { + let blob: Vec = data_encoding::BASE64URL_NOPAD + .decode(blob.as_bytes()) + .unwrap(); + wrap_api_future(async move { + let veilid_api = get_veilid_api()?; + + let key = veilid_api.import_remote_private_route(blob)?; + + APIResult::Ok(key.encode()) + }) +} + +#[wasm_bindgen()] +pub fn release_private_route(key: String) -> Promise { + let key: veilid_core::DHTKey = veilid_core::deserialize_json(&key).unwrap(); + wrap_api_future(async move { + let veilid_api = get_veilid_api()?; + veilid_api.release_private_route(&key)?; + APIRESULT_UNDEFINED }) } #[wasm_bindgen()] pub fn app_call_reply(id: String, message: String) -> Promise { + let message: Vec = data_encoding::BASE64URL_NOPAD + .decode(message.as_bytes()) + .unwrap(); wrap_api_future(async move { let id = match id.parse() { Ok(v) => v, @@ -274,15 +462,21 @@ pub fn app_call_reply(id: String, message: String) -> Promise { return APIResult::Err(veilid_core::VeilidAPIError::invalid_argument(e, "id", id)) } }; - let message = data_encoding::BASE64URL_NOPAD - .decode(message.as_bytes()) - .map_err(|e| veilid_core::VeilidAPIError::invalid_argument(e, "message", message))?; let veilid_api = get_veilid_api()?; let out = veilid_api.app_call_reply(id, message).await?; Ok(out) }) } +#[wasm_bindgen()] +pub fn debug(command: String) -> Promise { + wrap_api_future(async move { + let veilid_api = get_veilid_api()?; + let out = veilid_api.debug(command).await?; + Ok(out) + }) +} + #[wasm_bindgen()] pub fn veilid_version_string() -> String { veilid_core::veilid_version_string() From fa3ad29d21482f3bdf72cbd1065d9468ea167b1d Mon Sep 17 00:00:00 2001 From: John Smith Date: Sat, 3 Dec 2022 12:36:06 -0500 Subject: [PATCH 38/88] flutter work --- scripts/ios_build.sh | 5 ++ scripts/macos_build.sh | 9 ++- veilid-flutter/example/.metadata | 24 +++++- veilid-flutter/example/ios/Podfile.lock | 8 +- .../ios/Runner.xcodeproj/project.pbxproj | 6 +- veilid-flutter/example/macos/Podfile.lock | 2 +- .../macos/Runner.xcodeproj/project.pbxproj | 75 +++++++++--------- .../xcshareddata/xcschemes/Runner.xcscheme | 8 +- .../AppIcon.appiconset/app_icon_1024.png | Bin 46993 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 3276 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 1429 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 5933 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 1243 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 14800 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 1874 -> 2218 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 4 + .../macos/Runner/Configs/AppInfo.xcconfig | 4 +- veilid-flutter/example/pubspec.lock | 16 ++-- veilid-flutter/macos/veilid.podspec | 1 + 19 files changed, 97 insertions(+), 65 deletions(-) diff --git a/scripts/ios_build.sh b/scripts/ios_build.sh index 8242c693..2dcbdc2c 100755 --- a/scripts/ios_build.sh +++ b/scripts/ios_build.sh @@ -67,5 +67,10 @@ do done +# Make lipo build mkdir -p "$TARGET_PATH/$LIPO_OUT_NAME/$BUILD_MODE/" lipo $LIPOS -create -output "$TARGET_PATH/$LIPO_OUT_NAME/$BUILD_MODE/lib$PACKAGE_NAME.a" + +# Make most recent dylib available without build mode for flutter +cp "$TARGET_PATH/$LIPO_OUT_NAME/$BUILD_MODE/lib$PACKAGE_NAME.a" "$TARGET_PATH/$LIPO_OUT_NAME/lib$PACKAGE_NAME.a" + diff --git a/scripts/macos_build.sh b/scripts/macos_build.sh index 0ef14c81..1a327df2 100755 --- a/scripts/macos_build.sh +++ b/scripts/macos_build.sh @@ -53,9 +53,14 @@ do env -i PATH=/usr/bin:/bin:$HOMEBREW_DIR:$CARGO_DIR HOME="$HOME" USER="$USER" cargo $CARGO_TOOLCHAIN build $EXTRA_CARGO_OPTIONS --target $CARGO_TARGET --manifest-path $CARGO_MANIFEST_PATH - LIPOS="$LIPOS $TARGET_PATH/$CARGO_TARGET/$BUILD_MODE/lib$PACKAGE_NAME.a" + LIPOS="$LIPOS $TARGET_PATH/$CARGO_TARGET/$BUILD_MODE/lib$PACKAGE_NAME.dylib" done +# Make lipo build mkdir -p "$TARGET_PATH/$LIPO_OUT_NAME/$BUILD_MODE/" -lipo $LIPOS -create -output "$TARGET_PATH/$LIPO_OUT_NAME/$BUILD_MODE/lib$PACKAGE_NAME.a" +lipo $LIPOS -create -output "$TARGET_PATH/$LIPO_OUT_NAME/$BUILD_MODE/lib$PACKAGE_NAME.dylib" + +# Make most recent dylib available without build mode for flutter +cp "$TARGET_PATH/$LIPO_OUT_NAME/$BUILD_MODE/lib$PACKAGE_NAME.dylib" "$TARGET_PATH/$LIPO_OUT_NAME/lib$PACKAGE_NAME.dylib" + diff --git a/veilid-flutter/example/.metadata b/veilid-flutter/example/.metadata index fd70cabc..32f17cc0 100644 --- a/veilid-flutter/example/.metadata +++ b/veilid-flutter/example/.metadata @@ -1,10 +1,30 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled and should not be manually edited. +# This file should be version controlled. version: - revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b + revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 channel: stable project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 + base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 + - platform: macos + create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 + base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/veilid-flutter/example/ios/Podfile.lock b/veilid-flutter/example/ios/Podfile.lock index cded9b52..b736a024 100644 --- a/veilid-flutter/example/ios/Podfile.lock +++ b/veilid-flutter/example/ios/Podfile.lock @@ -19,10 +19,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/veilid/ios" SPEC CHECKSUMS: - Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 - veilid: 804173397bd9d07c5a70ac6933cc2afbe54afc82 + veilid: 41ea3fb86dbe06d0ff436d5002234dac45ccf1ea -PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c +PODFILE CHECKSUM: 7368163408c647b7eb699d0d788ba6718e18fb8d -COCOAPODS: 1.11.2 +COCOAPODS: 1.11.3 diff --git a/veilid-flutter/example/ios/Runner.xcodeproj/project.pbxproj b/veilid-flutter/example/ios/Runner.xcodeproj/project.pbxproj index 6187169d..94d69de5 100644 --- a/veilid-flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/veilid-flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -353,7 +353,7 @@ }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + baseConfigurationReference = D12CAD1E1213967B2B34ABF5 /* Pods-Runner.profile.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -483,7 +483,7 @@ }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + baseConfigurationReference = 0DEA6A7039338BC067F4FB23 /* Pods-Runner.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -505,7 +505,7 @@ }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + baseConfigurationReference = 41720C3D885A5FD597C42EA7 /* Pods-Runner.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; diff --git a/veilid-flutter/example/macos/Podfile.lock b/veilid-flutter/example/macos/Podfile.lock index 27a526e1..e7ce6ebc 100644 --- a/veilid-flutter/example/macos/Podfile.lock +++ b/veilid-flutter/example/macos/Podfile.lock @@ -23,6 +23,6 @@ SPEC CHECKSUMS: path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 veilid: f2b3b5b3ac8cd93fc5443ab830d5153575dacf36 -PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c +PODFILE CHECKSUM: 554ea19fe44240be72b76305f41eaaeb731ea434 COCOAPODS: 1.11.3 diff --git a/veilid-flutter/example/macos/Runner.xcodeproj/project.pbxproj b/veilid-flutter/example/macos/Runner.xcodeproj/project.pbxproj index 28de84a7..a5003451 100644 --- a/veilid-flutter/example/macos/Runner.xcodeproj/project.pbxproj +++ b/veilid-flutter/example/macos/Runner.xcodeproj/project.pbxproj @@ -26,8 +26,7 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 400B233427A772C20074EE57 /* libveilid_flutter.dylib in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 400B233327A772C20074EE57 /* libveilid_flutter.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 8526E2FC060241577E5281A0 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E89F2D855D7BDA4933E1A2EC /* Pods_Runner.framework */; }; + 81649851C9DA1DA79054FE06 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52FF39A454BC3E9083B5E1BB /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -47,7 +46,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 400B233427A772C20074EE57 /* libveilid_flutter.dylib in Bundle Framework */, ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; @@ -55,10 +53,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 233B6A2BBF071AE96501823B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 269F3F1A4251F82E97C46D1F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* veilid_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = veilid_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -70,12 +68,11 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 400B233327A772C20074EE57 /* libveilid_flutter.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libveilid_flutter.dylib; path = ../../../target/macos_lib/libveilid_flutter.dylib; sourceTree = ""; }; + 52FF39A454BC3E9083B5E1BB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - B8606FB3C4AA619FC22C3115 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - E7AFD58527EF4CB477A3582A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - E89F2D855D7BDA4933E1A2EC /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B2D03765D2CC78AE1EEB2F5F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + BC8854C07DAB1433F44BB126 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -83,7 +80,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 8526E2FC060241577E5281A0 /* Pods_Runner.framework in Frameworks */, + 81649851C9DA1DA79054FE06 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -104,19 +101,18 @@ 33CC10E42044A3C60003C045 = { isa = PBXGroup; children = ( - 400B233327A772C20074EE57 /* libveilid_flutter.dylib */, 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, - CB1628CFD484E4786F2B5B24 /* Pods */, + B3779D18CAC0309C4D986240 /* Pods */, ); sourceTree = ""; }; 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* veilid_example.app */, + 33CC10ED2044A3C60003C045 /* example.app */, ); name = Products; sourceTree = ""; @@ -156,20 +152,21 @@ path = Runner; sourceTree = ""; }; - CB1628CFD484E4786F2B5B24 /* Pods */ = { + B3779D18CAC0309C4D986240 /* Pods */ = { isa = PBXGroup; children = ( - E7AFD58527EF4CB477A3582A /* Pods-Runner.debug.xcconfig */, - 233B6A2BBF071AE96501823B /* Pods-Runner.release.xcconfig */, - B8606FB3C4AA619FC22C3115 /* Pods-Runner.profile.xcconfig */, + B2D03765D2CC78AE1EEB2F5F /* Pods-Runner.debug.xcconfig */, + BC8854C07DAB1433F44BB126 /* Pods-Runner.release.xcconfig */, + 269F3F1A4251F82E97C46D1F /* Pods-Runner.profile.xcconfig */, ); + name = Pods; path = Pods; sourceTree = ""; }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( - E89F2D855D7BDA4933E1A2EC /* Pods_Runner.framework */, + 52FF39A454BC3E9083B5E1BB /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -181,13 +178,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 53C43C97D5A63C16E369AAE5 /* [CP] Check Pods Manifest.lock */, + BEF36EDF1EEEA8E0EFCE11A5 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - 985C53EA7A19701CA562A7E5 /* [CP] Embed Pods Frameworks */, + 6C9005308B324F4C4A5637A3 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -196,7 +193,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* veilid_example.app */; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -294,7 +291,24 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; - 53C43C97D5A63C16E369AAE5 /* [CP] Check Pods Manifest.lock */ = { + 6C9005308B324F4C4A5637A3 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + BEF36EDF1EEEA8E0EFCE11A5 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -316,23 +330,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 985C53EA7A19701CA562A7E5 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/veilid-flutter/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/veilid-flutter/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index fff882b8..fb7259e1 100644 --- a/veilid-flutter/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/veilid-flutter/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -31,7 +31,7 @@ @@ -54,7 +54,7 @@ @@ -71,7 +71,7 @@ diff --git a/veilid-flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/veilid-flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 3c4935a7ca84f0976aca34b7f2895d65fb94d1ea..82b6f9d9a33e198f5747104729e1fcef999772a5 100644 GIT binary patch literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY literal 46993 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 diff --git a/veilid-flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/veilid-flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index ed4cc16421680a50164ba74381b4b35ceaa0ccfc..13b35eba55c6dabc3aac36f33d859266c18fa0d0 100644 GIT binary patch literal 5680 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 literal 3276 zcmZ`*X*|?x8~)E?#xi3t91%vcMKbnsIy2_j%QE2ziLq8HEtbf{7%?Q-9a%z_Y^9`> zEHh*&vUG%uWkg7pKTS-`$veH@-Vg8ZdG7oAJ@<88AMX3Z{d}TU-4*=KI1-hF6u>DKF2moPt09c{` zfN3rO$X+gJI&oA$AbgKoTL8PiPI1eFOhHBDvW+$&oPl1s$+O5y3$30Jx9nC_?fg%8Om)@;^P;Ee~8ibejUNlSR{FL7-+ zCzU}3UT98m{kYI^@`mgCOJ))+D#erb#$UWt&((j-5*t1id2Zak{`aS^W*K5^gM02# zUAhZn-JAUK>i+SNuFbWWd*7n1^!}>7qZ1CqCl*T+WoAy&z9pm~0AUt1cCV24f z3M@&G~UKrjVHa zjcE@a`2;M>eV&ocly&W3h{`Kt`1Fpp?_h~9!Uj5>0eXw@$opV(@!pixIux}s5pvEqF5$OEMG0;c zAfMxC(-;nx_`}8!F?OqK19MeaswOomKeifCG-!9PiHSU$yamJhcjXiq)-}9`M<&Au|H!nKY(0`^x16f205i2i;E%(4!?0lLq0sH_%)Wzij)B{HZxYWRl3DLaN5`)L zx=x=|^RA?d*TRCwF%`zN6wn_1C4n;lZG(9kT;2Uhl&2jQYtC1TbwQlP^BZHY!MoHm zjQ9)uu_K)ObgvvPb}!SIXFCtN!-%sBQe{6NU=&AtZJS%}eE$i}FIll!r>~b$6gt)V z7x>OFE}YetHPc-tWeu!P@qIWb@Z$bd!*!*udxwO6&gJ)q24$RSU^2Mb%-_`dR2`nW z)}7_4=iR`Tp$TPfd+uieo)8B}Q9#?Szmy!`gcROB@NIehK|?!3`r^1>av?}e<$Qo` zo{Qn#X4ktRy<-+f#c@vILAm;*sfS}r(3rl+{op?Hx|~DU#qsDcQDTvP*!c>h*nXU6 zR=Un;i9D!LcnC(AQ$lTUv^pgv4Z`T@vRP3{&xb^drmjvOruIBJ%3rQAFLl7d9_S64 zN-Uv?R`EzkbYIo)af7_M=X$2p`!u?nr?XqQ_*F-@@(V zFbNeVEzbr;i2fefJ@Gir3-s`syC93he_krL1eb;r(}0yUkuEK34aYvC@(yGi`*oq? zw5g_abg=`5Fdh1Z+clSv*N*Jifmh&3Ghm0A=^s4be*z5N!i^FzLiShgkrkwsHfMjf z*7&-G@W>p6En#dk<^s@G?$7gi_l)y7k`ZY=?ThvvVKL~kM{ehG7-q6=#%Q8F&VsB* zeW^I zUq+tV(~D&Ii_=gn-2QbF3;Fx#%ajjgO05lfF8#kIllzHc=P}a3$S_XsuZI0?0__%O zjiL!@(C0$Nr+r$>bHk(_oc!BUz;)>Xm!s*C!32m1W<*z$^&xRwa+AaAG= z9t4X~7UJht1-z88yEKjJ68HSze5|nKKF9(Chw`{OoG{eG0mo`^93gaJmAP_i_jF8a z({|&fX70PXVE(#wb11j&g4f{_n>)wUYIY#vo>Rit(J=`A-NYYowTnl(N6&9XKIV(G z1aD!>hY!RCd^Sy#GL^0IgYF~)b-lczn+X}+eaa)%FFw41P#f8n2fm9=-4j7}ULi@Z zm=H8~9;)ShkOUAitb!1fvv%;2Q+o)<;_YA1O=??ie>JmIiTy6g+1B-1#A(NAr$JNL znVhfBc8=aoz&yqgrN|{VlpAniZVM?>0%bwB6>}S1n_OURps$}g1t%)YmCA6+5)W#B z=G^KX>C7x|X|$~;K;cc2x8RGO2{{zmjPFrfkr6AVEeW2$J9*~H-4~G&}~b+Pb}JJdODU|$n1<7GPa_>l>;{NmA^y_eXTiv z)T61teOA9Q$_5GEA_ox`1gjz>3lT2b?YY_0UJayin z64qq|Nb7^UhikaEz3M8BKhNDhLIf};)NMeS8(8?3U$ThSMIh0HG;;CW$lAp0db@s0 zu&jbmCCLGE*NktXVfP3NB;MQ>p?;*$-|htv>R`#4>OG<$_n)YvUN7bwzbWEsxAGF~ zn0Vfs?Dn4}Vd|Cf5T-#a52Knf0f*#2D4Lq>-Su4g`$q={+5L$Ta|N8yfZ}rgQm;&b z0A4?$Hg5UkzI)29=>XSzdH4wH8B@_KE{mSc>e3{yGbeiBY_+?^t_a#2^*x_AmN&J$ zf9@<5N15~ty+uwrz0g5k$sL9*mKQazK2h19UW~#H_X83ap-GAGf#8Q5b8n@B8N2HvTiZu&Mg+xhthyG3#0uIny33r?t&kzBuyI$igd`%RIcO8{s$$R3+Z zt{ENUO)pqm_&<(vPf*$q1FvC}W&G)HQOJd%x4PbxogX2a4eW-%KqA5+x#x`g)fN&@ zLjG8|!rCj3y0%N)NkbJVJgDu5tOdMWS|y|Tsb)Z04-oAVZ%Mb311P}}SG#!q_ffMV z@*L#25zW6Ho?-x~8pKw4u9X)qFI7TRC)LlEL6oQ9#!*0k{=p?Vf_^?4YR(M z`uD+8&I-M*`sz5af#gd$8rr|oRMVgeI~soPKB{Q{FwV-FW)>BlS?inI8girWs=mo5b18{#~CJz!miCgQYU>KtCPt()StN;x)c2P3bMVB$o(QUh z$cRQlo_?#k`7A{Tw z!~_YKSd(%1dBM+KE!5I2)ZZsGz|`+*fB*n}yxtKVyxB>Ar^wk2@3=alwSY;|9`*g zg!SA<>T^y!@^};P@J-as?O3u$l7L#kXB!1IF&zg(h83rU=AWx~@Dy-kzNX+jV}aVs z1v5CF*8KW9f8pa(@@+>Z+e?Ps``f*aWes~8gY~XA)9?S6e8y;c_t&@S2P0>+Dn?9{ zjOEn!Xkd*MIr9J8?8d}HXX|;sH_no6jgUwRH8456HBqAe18+w0<*)TT>+Am{7BFS? zg&bQenZnh^m>%~(z2d9v3dt8a@{ww7Kg<6a+5G(0?>M`^Q<3Ge^bEF$4r9YVKhB>@ zP(5|R;QO)ow#V`RjUql68&{l8D(BP@_14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>GbI`Jdw*pGcA%L+*Q#&*YQOJ$_%U#(BDn``;rKxi&&)LfRxIZ*98z8UWRslDo@Xu)QVh}rB>bKwe@Bjzwg%m$hd zG)gFMgHZlPxGcm3paLLb44yHI|Ag0wdp!_yD5R<|B29Ui~27`?vfy#ktk_KyHWMDA42{J=Uq-o}i z*%kZ@45mQ-Rw?0?K+z{&5KFc}xc5Q%1PFAbL_xCmpj?JNAm>L6SjrCMpiK}5LG0ZE zO>_%)r1c48n{Iv*t(u1=&kH zeO=ifbFy+6aSK)V_5t;NKhE#$Iz=+Oii|KDJ}W>g}0%`Svgra*tnS6TRU4iTH*e=dj~I` zym|EM*}I1?pT2#3`oZ(|3I-Y$DkeHMN=8~%YSR?;>=X?(Emci*ZIz9+t<|S1>hE8$ zVa1LmTh{DZv}x6@Wz!a}+qZDz%AHHMuHCzM^XlEpr!QPzf9QzkS_0!&1MPx*ICxe}RFdTH+c}l9E`G zYL#4+3Zxi}3=A!G4S>ir#L(2r)WFKnP}jiR%D`ZOPH`@ZhTQy=%(P0}8ZH)|z6jL7 N;OXk;vd$@?2>?>Ex^Vyi diff --git a/veilid-flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/veilid-flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index bcbf36df2f2aaaa0a63c7dabc94e600184229d0d..bdb57226d5f2bd20f11934f4903f16459cf52379 100644 GIT binary patch literal 14142 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 diff --git a/veilid-flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/veilid-flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index e71a726136a47ed24125c7efc79d68a4a01961b4..326c0e72c9d820600887813b3b98d0dd69c5d4e8 100644 GIT binary patch literal 36406 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 literal 14800 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 vNcLuCM3Rj?iBw@67X_p?_CF#jIyB-s + + +

+ diff --git a/veilid-flutter/example/macos/Runner/Configs/AppInfo.xcconfig b/veilid-flutter/example/macos/Runner/Configs/AppInfo.xcconfig index 538e53b2..597056ec 100644 --- a/veilid-flutter/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/veilid-flutter/example/macos/Runner/Configs/AppInfo.xcconfig @@ -5,10 +5,10 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = veilid_example +PRODUCT_NAME = example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.veilid.veilidExample +PRODUCT_BUNDLE_IDENTIFIER = com.veilid.example // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2022 com.veilid. All rights reserved. diff --git a/veilid-flutter/example/pubspec.lock b/veilid-flutter/example/pubspec.lock index 2a4e58ce..52c42eef 100644 --- a/veilid-flutter/example/pubspec.lock +++ b/veilid-flutter/example/pubspec.lock @@ -89,7 +89,7 @@ packages: name: flutter_loggy url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" flutter_test: dependency: "direct dev" description: flutter @@ -113,14 +113,14 @@ packages: name: lints url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" loggy: dependency: "direct main" description: name: loggy url: "https://pub.dartlang.org" source: hosted - version: "2.0.1+1" + version: "2.0.3" matcher: dependency: transitive description: @@ -162,7 +162,7 @@ packages: name: path_provider_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.20" + version: "2.0.22" path_provider_ios: dependency: transitive description: @@ -190,7 +190,7 @@ packages: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" path_provider_windows: dependency: transitive description: @@ -211,7 +211,7 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.3" process: dependency: transitive description: @@ -225,7 +225,7 @@ packages: name: rxdart url: "https://pub.dartlang.org" source: hosted - version: "0.27.5" + version: "0.27.7" sky_engine: dependency: transitive description: flutter @@ -293,7 +293,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.1.2" xdg_directories: dependency: transitive description: diff --git a/veilid-flutter/macos/veilid.podspec b/veilid-flutter/macos/veilid.podspec index 6b169c1d..0638621b 100644 --- a/veilid-flutter/macos/veilid.podspec +++ b/veilid-flutter/macos/veilid.podspec @@ -17,6 +17,7 @@ Veilid Network Plugin s.dependency 'FlutterMacOS' s.platform = :osx, '10.11' + s.osx.deployment_target = '10.11' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.0' From 3e24154e3d8dc8766c834f4d0ff2b35e6ea20a76 Mon Sep 17 00:00:00 2001 From: John Smith Date: Sat, 3 Dec 2022 18:08:53 -0500 Subject: [PATCH 39/88] flutter work --- setup_macos.sh | 2 +- veilid-flutter/example/lib/config.dart | 4 ++-- veilid-flutter/example/macos/Podfile | 21 ++++++++++++++++++- veilid-flutter/example/macos/Podfile.lock | 2 +- .../macos/Runner.xcodeproj/project.pbxproj | 3 +-- veilid-flutter/lib/veilid.dart | 16 ++++++++++++++ veilid-flutter/lib/veilid_ffi.dart | 2 +- 7 files changed, 42 insertions(+), 8 deletions(-) diff --git a/setup_macos.sh b/setup_macos.sh index 5530c238..7b9eeee8 100755 --- a/setup_macos.sh +++ b/setup_macos.sh @@ -109,4 +109,4 @@ if [ "$BREW_USER" == "" ]; then fi fi sudo -H -u $BREW_USER brew install capnp cmake wabt llvm protobuf openjdk@11 - +sudo gem install cocoapods diff --git a/veilid-flutter/example/lib/config.dart b/veilid-flutter/example/lib/config.dart index 6f155825..92020d68 100644 --- a/veilid-flutter/example/lib/config.dart +++ b/veilid-flutter/example/lib/config.dart @@ -46,8 +46,8 @@ Future getDefaultVeilidConfig() async { clientWhitelistTimeoutMs: 300000, reverseConnectionReceiptTimeMs: 5000, holePunchReceiptTimeMs: 5000, - nodeId: "", - nodeIdSecret: "", + nodeId: null, + nodeIdSecret: null, bootstrap: kIsWeb ? ["ws://bootstrap.dev.veilid.net:5150/ws"] : ["bootstrap.dev.veilid.net"], diff --git a/veilid-flutter/example/macos/Podfile b/veilid-flutter/example/macos/Podfile index dade8dfa..e8d38fb2 100644 --- a/veilid-flutter/example/macos/Podfile +++ b/veilid-flutter/example/macos/Podfile @@ -33,8 +33,27 @@ target 'Runner' do flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) end +require 'json' +require 'pathname' +require 'fileutils' +workspace_dir = File.dirname(JSON.parse(`cargo locate-project --workspace`)['root']) +cargo_target_dir = File.join(workspace_dir, 'target') +lipo_dir= File.join(cargo_target_dir, 'lipo-darwin') +veilid_flutter = File.join(lipo_dir, 'libveilid_flutter.dylib') +FileUtils.mkdir_p(lipo_dir) +FileUtils.touch(veilid_flutter) + post_install do |installer| - installer.pods_project.targets.each do |target| + project = installer.pods_project + reference = project.add_file_reference(veilid_flutter, project.main_group["Frameworks"]) + + project.targets.each do |target| flutter_additional_macos_build_settings(target) + + if (target.is_a? Xcodeproj::Project::Object::PBXNativeTarget) && target.name == "veilid" + target.resources_build_phase.add_file_reference(reference) + end + end + end diff --git a/veilid-flutter/example/macos/Podfile.lock b/veilid-flutter/example/macos/Podfile.lock index e7ce6ebc..89fa7fcc 100644 --- a/veilid-flutter/example/macos/Podfile.lock +++ b/veilid-flutter/example/macos/Podfile.lock @@ -23,6 +23,6 @@ SPEC CHECKSUMS: path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 veilid: f2b3b5b3ac8cd93fc5443ab830d5153575dacf36 -PODFILE CHECKSUM: 554ea19fe44240be72b76305f41eaaeb731ea434 +PODFILE CHECKSUM: 4ccbce18f83e3892ae919944d8803eb392455cb9 COCOAPODS: 1.11.3 diff --git a/veilid-flutter/example/macos/Runner.xcodeproj/project.pbxproj b/veilid-flutter/example/macos/Runner.xcodeproj/project.pbxproj index a5003451..7a30c68c 100644 --- a/veilid-flutter/example/macos/Runner.xcodeproj/project.pbxproj +++ b/veilid-flutter/example/macos/Runner.xcodeproj/project.pbxproj @@ -159,7 +159,6 @@ BC8854C07DAB1433F44BB126 /* Pods-Runner.release.xcconfig */, 269F3F1A4251F82E97C46D1F /* Pods-Runner.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -289,7 +288,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire\n"; }; 6C9005308B324F4C4A5637A3 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; diff --git a/veilid-flutter/lib/veilid.dart b/veilid-flutter/lib/veilid.dart index 219dad64..c9d83087 100644 --- a/veilid-flutter/lib/veilid.dart +++ b/veilid-flutter/lib/veilid.dart @@ -1556,6 +1556,10 @@ abstract class VeilidAPIException implements Exception { return VeilidAPIExceptionMissingArgument( json["context"], json["argument"]); } + case "Generic": + { + return VeilidAPIExceptionGeneric(json["message"]); + } default: { throw VeilidAPIExceptionInternal( @@ -1681,6 +1685,18 @@ class VeilidAPIExceptionMissingArgument implements VeilidAPIException { VeilidAPIExceptionMissingArgument(this.context, this.argument); } +class VeilidAPIExceptionGeneric implements VeilidAPIException { + final String message; + + @override + String toString() { + return "VeilidAPIException: Generic (message: $message)"; + } + + // + VeilidAPIExceptionGeneric(this.message); +} + ////////////////////////////////////// /// VeilidVersion diff --git a/veilid-flutter/lib/veilid_ffi.dart b/veilid-flutter/lib/veilid_ffi.dart index c3ef7a4f..994e61e0 100644 --- a/veilid-flutter/lib/veilid_ffi.dart +++ b/veilid-flutter/lib/veilid_ffi.dart @@ -16,7 +16,7 @@ const _base = 'veilid_flutter'; final _path = Platform.isWindows ? '$_base.dll' : Platform.isMacOS - ? 'lib$_base.dylib' + ? 'veilid.framework/Resources/lib$_base.dylib' : 'lib$_base.so'; final _dylib = Platform.isIOS ? DynamicLibrary.process() : DynamicLibrary.open(_path); From 847623f2b4e10b635528bec5c25f31b169f01506 Mon Sep 17 00:00:00 2001 From: John Smith Date: Sat, 3 Dec 2022 20:10:33 -0500 Subject: [PATCH 40/88] flutter --- veilid-flutter/example/macos/Podfile | 27 +++++++------- veilid-flutter/example/macos/Podfile.lock | 2 +- .../macos/Runner.xcodeproj/project.pbxproj | 36 +++++++++++++++++-- veilid-flutter/lib/veilid.dart | 2 +- veilid-flutter/lib/veilid_ffi.dart | 2 +- 5 files changed, 50 insertions(+), 19 deletions(-) diff --git a/veilid-flutter/example/macos/Podfile b/veilid-flutter/example/macos/Podfile index e8d38fb2..2e723704 100644 --- a/veilid-flutter/example/macos/Podfile +++ b/veilid-flutter/example/macos/Podfile @@ -33,26 +33,27 @@ target 'Runner' do flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) end -require 'json' -require 'pathname' -require 'fileutils' -workspace_dir = File.dirname(JSON.parse(`cargo locate-project --workspace`)['root']) -cargo_target_dir = File.join(workspace_dir, 'target') -lipo_dir= File.join(cargo_target_dir, 'lipo-darwin') -veilid_flutter = File.join(lipo_dir, 'libveilid_flutter.dylib') -FileUtils.mkdir_p(lipo_dir) -FileUtils.touch(veilid_flutter) +# require 'json' +# require 'pathname' +# require 'fileutils' +# workspace_dir = File.dirname(JSON.parse(`cargo locate-project --workspace`)['root']) +# cargo_target_dir = File.join(workspace_dir, 'target') +# lipo_dir= File.join(cargo_target_dir, 'lipo-darwin') +# veilid_flutter = File.join(lipo_dir, 'libveilid_flutter.dylib') +# FileUtils.mkdir_p(lipo_dir) +# FileUtils.touch(veilid_flutter) post_install do |installer| project = installer.pods_project - reference = project.add_file_reference(veilid_flutter, project.main_group["Frameworks"]) + # reference = project.add_file_reference(veilid_flutter, project.main_group['Frameworks']) project.targets.each do |target| flutter_additional_macos_build_settings(target) - if (target.is_a? Xcodeproj::Project::Object::PBXNativeTarget) && target.name == "veilid" - target.resources_build_phase.add_file_reference(reference) - end + # if (target.is_a? Xcodeproj::Project::Object::PBXNativeTarget) && target.name == 'veilid' + # buildfile = target.resources_build_phase.add_file_reference(reference) + # buildfile.settings = { 'ATTRIBUTES' => ['CodeSignOnCopy'] } + # end end diff --git a/veilid-flutter/example/macos/Podfile.lock b/veilid-flutter/example/macos/Podfile.lock index 89fa7fcc..5d7ff285 100644 --- a/veilid-flutter/example/macos/Podfile.lock +++ b/veilid-flutter/example/macos/Podfile.lock @@ -23,6 +23,6 @@ SPEC CHECKSUMS: path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 veilid: f2b3b5b3ac8cd93fc5443ab830d5153575dacf36 -PODFILE CHECKSUM: 4ccbce18f83e3892ae919944d8803eb392455cb9 +PODFILE CHECKSUM: 287ef99ea691944b4036c202758453a7b2d63b34 COCOAPODS: 1.11.3 diff --git a/veilid-flutter/example/macos/Runner.xcodeproj/project.pbxproj b/veilid-flutter/example/macos/Runner.xcodeproj/project.pbxproj index 7a30c68c..53d253d5 100644 --- a/veilid-flutter/example/macos/Runner.xcodeproj/project.pbxproj +++ b/veilid-flutter/example/macos/Runner.xcodeproj/project.pbxproj @@ -26,6 +26,8 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 43B71B4F293C1CC400EF4986 /* libveilid_flutter.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 43B71B4B293C127700EF4986 /* libveilid_flutter.dylib */; settings = {ATTRIBUTES = (Weak, ); }; }; + 43B71B50293C1CC400EF4986 /* libveilid_flutter.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 43B71B4B293C127700EF4986 /* libveilid_flutter.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 81649851C9DA1DA79054FE06 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52FF39A454BC3E9083B5E1BB /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ @@ -40,14 +42,15 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { + 43B71B51293C1CC400EF4986 /* Embed Libraries */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( + 43B71B50293C1CC400EF4986 /* libveilid_flutter.dylib in Embed Libraries */, ); - name = "Bundle Framework"; + name = "Embed Libraries"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ @@ -68,6 +71,7 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 43B71B4B293C127700EF4986 /* libveilid_flutter.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libveilid_flutter.dylib; path = "../../../target/lipo-darwin/libveilid_flutter.dylib"; sourceTree = ""; }; 52FF39A454BC3E9083B5E1BB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; @@ -80,6 +84,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 43B71B4F293C1CC400EF4986 /* libveilid_flutter.dylib in Frameworks */, 81649851C9DA1DA79054FE06 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -165,6 +170,7 @@ D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 43B71B4B293C127700EF4986 /* libveilid_flutter.dylib */, 52FF39A454BC3E9083B5E1BB /* Pods_Runner.framework */, ); name = Frameworks; @@ -181,9 +187,9 @@ 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, 6C9005308B324F4C4A5637A3 /* [CP] Embed Pods Frameworks */, + 43B71B51293C1CC400EF4986 /* Embed Libraries */, ); buildRules = ( ); @@ -418,13 +424,21 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", + /usr/lib/swift, + "../../../target/lipo-darwin", + ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -544,13 +558,21 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", + /usr/lib/swift, + "../../../target/lipo-darwin", + ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -564,13 +586,21 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", + /usr/lib/swift, + "../../../target/lipo-darwin", + ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; diff --git a/veilid-flutter/lib/veilid.dart b/veilid-flutter/lib/veilid.dart index c9d83087..e85a9262 100644 --- a/veilid-flutter/lib/veilid.dart +++ b/veilid-flutter/lib/veilid.dart @@ -1454,7 +1454,7 @@ class VeilidStateConfig { }); VeilidStateConfig.fromJson(Map json) - : config = jsonDecode(json['config']); + : config = json['config']; Map get json { return {'config': config}; diff --git a/veilid-flutter/lib/veilid_ffi.dart b/veilid-flutter/lib/veilid_ffi.dart index 994e61e0..c3ef7a4f 100644 --- a/veilid-flutter/lib/veilid_ffi.dart +++ b/veilid-flutter/lib/veilid_ffi.dart @@ -16,7 +16,7 @@ const _base = 'veilid_flutter'; final _path = Platform.isWindows ? '$_base.dll' : Platform.isMacOS - ? 'veilid.framework/Resources/lib$_base.dylib' + ? 'lib$_base.dylib' : 'lib$_base.so'; final _dylib = Platform.isIOS ? DynamicLibrary.process() : DynamicLibrary.open(_path); From 0b059e0ef9616add119074755419419a0f5940d7 Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 8 Dec 2022 10:24:33 -0500 Subject: [PATCH 41/88] checkpoint --- veilid-core/proto/veilid.capnp | 7 +- veilid-core/src/network_manager/mod.rs | 57 +++++-- veilid-core/src/network_manager/native/mod.rs | 6 +- veilid-core/src/network_manager/wasm/mod.rs | 27 +++- veilid-core/src/routing_table/bucket_entry.rs | 32 ++-- veilid-core/src/routing_table/mod.rs | 17 +- veilid-core/src/routing_table/node_ref.rs | 19 ++- .../src/routing_table/route_spec_store.rs | 70 ++++++-- .../routing_table/routing_domain_editor.rs | 4 +- .../src/routing_table/routing_table_inner.rs | 47 ++++-- .../routing_table/tasks/relay_management.rs | 98 ++++++----- .../coders/operations/operation.rs | 26 ++- veilid-core/src/rpc_processor/destination.rs | 17 +- veilid-core/src/rpc_processor/mod.rs | 153 +++++++++++------- .../src/rpc_processor/rpc_find_node.rs | 5 +- veilid-wasm/src/lib.rs | 83 +++++----- 16 files changed, 427 insertions(+), 241 deletions(-) diff --git a/veilid-core/proto/veilid.capnp b/veilid-core/proto/veilid.capnp index 05bd4de7..3a247908 100644 --- a/veilid-core/proto/veilid.capnp +++ b/veilid-core/proto/veilid.capnp @@ -521,9 +521,10 @@ struct Answer @0xacacb8b6988c1058 { struct Operation @0xbf2811c435403c3b { opId @0 :UInt64; # Random RPC ID. Must be random to foil reply forgery attacks. senderNodeInfo @1 :SignedNodeInfo; # (optional) SignedNodeInfo for the sender to be cached by the receiver. + targetNodeInfoTs @2 :UInt64; # Timestamp the sender believes the target's node info to be at or zero if not sent kind :union { - question @2 :Question; - statement @3 :Statement; - answer @4 :Answer; + question @3 :Question; + statement @4 :Statement; + answer @5 :Answer; } } diff --git a/veilid-core/src/network_manager/mod.rs b/veilid-core/src/network_manager/mod.rs index a91e55ed..d204a982 100644 --- a/veilid-core/src/network_manager/mod.rs +++ b/veilid-core/src/network_manager/mod.rs @@ -448,9 +448,17 @@ impl NetworkManager { /// Get our node's capabilities in the PublicInternet routing domain fn generate_public_internet_node_status(&self) -> PublicInternetNodeStatus { - let own_peer_info = self + let Some(own_peer_info) = self .routing_table() - .get_own_peer_info(RoutingDomain::PublicInternet); + .get_own_peer_info(RoutingDomain::PublicInternet) else { + return PublicInternetNodeStatus { + will_route: false, + will_tunnel: false, + will_signal: false, + will_relay: false, + will_validate_dial_info: false, + }; + }; let own_node_info = own_peer_info.signed_node_info.node_info(); let will_route = own_node_info.can_inbound_relay(); // xxx: eventually this may have more criteria added @@ -469,9 +477,14 @@ impl NetworkManager { } /// Get our node's capabilities in the LocalNetwork routing domain fn generate_local_network_node_status(&self) -> LocalNetworkNodeStatus { - let own_peer_info = self + let Some(own_peer_info) = self .routing_table() - .get_own_peer_info(RoutingDomain::LocalNetwork); + .get_own_peer_info(RoutingDomain::LocalNetwork) else { + return LocalNetworkNodeStatus { + will_relay: false, + will_validate_dial_info: false, + }; + }; let own_node_info = own_peer_info.signed_node_info.node_info(); @@ -833,10 +846,17 @@ impl NetworkManager { ); let (receipt, eventual_value) = self.generate_single_shot_receipt(receipt_timeout, [])?; + // Get target routing domain + let Some(routing_domain) = target_nr.best_routing_domain() else { + return Ok(NetworkResult::no_connection_other("No routing domain for target")); + }; + // Get our peer info - let peer_info = self + let Some(peer_info) = self .routing_table() - .get_own_peer_info(RoutingDomain::PublicInternet); + .get_own_peer_info(routing_domain) else { + return Ok(NetworkResult::no_connection_other("Own peer info not available")); + }; // Issue the signal let rpc = self.rpc_processor(); @@ -900,17 +920,11 @@ impl NetworkManager { data: Vec, ) -> EyreResult> { // Ensure we are filtered down to UDP (the only hole punch protocol supported today) - // and only in the PublicInternet routing domain assert!(target_nr .filter_ref() .map(|nrf| nrf.dial_info_filter.protocol_type_set == ProtocolTypeSet::only(ProtocolType::UDP)) .unwrap_or_default()); - assert!(target_nr - .filter_ref() - .map(|nrf| nrf.routing_domain_set - == RoutingDomainSet::only(RoutingDomain::PublicInternet)) - .unwrap_or_default()); // Build a return receipt for the signal let receipt_timeout = ms_to_us( @@ -921,10 +935,18 @@ impl NetworkManager { .hole_punch_receipt_time_ms, ); let (receipt, eventual_value) = self.generate_single_shot_receipt(receipt_timeout, [])?; + + // Get target routing domain + let Some(routing_domain) = target_nr.best_routing_domain() else { + return Ok(NetworkResult::no_connection_other("No routing domain for target")); + }; + // Get our peer info - let peer_info = self + let Some(peer_info) = self .routing_table() - .get_own_peer_info(RoutingDomain::PublicInternet); + .get_own_peer_info(routing_domain) else { + return Ok(NetworkResult::no_connection_other("Own peer info not available")); + }; // Get the udp direct dialinfo for the hole punch let hole_punch_did = target_nr @@ -1016,7 +1038,8 @@ impl NetworkManager { }; // Node A is our own node - let peer_a = routing_table.get_own_peer_info(routing_domain); + // Use whatever node info we've calculated so far + let peer_a = routing_table.get_best_effort_own_peer_info(routing_domain); // Node B is the target node let peer_b = match target_node_ref.make_peer_info(routing_domain) { @@ -1725,8 +1748,8 @@ impl NetworkManager { // Only update if we actually have valid signed node info for this routing domain if !this.routing_table().has_valid_own_node_info(routing_domain) { trace!( - "not sending node info update because our network class is not yet valid" - ); + "not sending node info update because our network class is not yet valid" + ); return; } diff --git a/veilid-core/src/network_manager/native/mod.rs b/veilid-core/src/network_manager/native/mod.rs index b588273b..23b05614 100644 --- a/veilid-core/src/network_manager/native/mod.rs +++ b/veilid-core/src/network_manager/native/mod.rs @@ -710,8 +710,8 @@ impl Network { } ProtocolConfig { - inbound, outbound, + inbound, family_global, family_local, } @@ -758,13 +758,13 @@ impl Network { // if we have static public dialinfo, upgrade our network class editor_public_internet.setup_network( - protocol_config.inbound, protocol_config.outbound, + protocol_config.inbound, protocol_config.family_global, ); editor_local_network.setup_network( - protocol_config.inbound, protocol_config.outbound, + protocol_config.inbound, protocol_config.family_local, ); let detect_address_changes = { diff --git a/veilid-core/src/network_manager/wasm/mod.rs b/veilid-core/src/network_manager/wasm/mod.rs index 4aca58e4..3287ea0b 100644 --- a/veilid-core/src/network_manager/wasm/mod.rs +++ b/veilid-core/src/network_manager/wasm/mod.rs @@ -252,7 +252,7 @@ impl Network { pub async fn startup(&self) -> EyreResult<()> { // get protocol config - self.inner.lock().protocol_config = { + let protocol_config = { let c = self.config.get(); let inbound = ProtocolTypeSet::new(); let mut outbound = ProtocolTypeSet::new(); @@ -269,12 +269,30 @@ impl Network { let family_local = AddressTypeSet::all(); ProtocolConfig { - inbound, outbound, + inbound, family_global, family_local, } }; + self.inner.lock().protocol_config = protocol_config; + + // Start editing routing table + let mut editor_public_internet = self + .unlocked_inner + .routing_table + .edit_routing_domain(RoutingDomain::PublicInternet); + + // set up the routing table's network config + // if we have static public dialinfo, upgrade our network class + editor_public_internet.setup_network( + protocol_config.outbound, + protocol_config.inbound, + protocol_config.family_global, + ); + + // commit routing table edits + editor_public_internet.commit().await; self.inner.lock().network_started = true; Ok(()) @@ -304,11 +322,6 @@ impl Network { editor.clear_dial_info_details(); editor.commit().await; - let mut editor = routing_table.edit_routing_domain(RoutingDomain::LocalNetwork); - editor.disable_node_info_updates(); - editor.clear_dial_info_details(); - editor.commit().await; - // Cancels all async background tasks by dropping join handles *self.inner.lock() = Self::new_inner(); diff --git a/veilid-core/src/routing_table/bucket_entry.rs b/veilid-core/src/routing_table/bucket_entry.rs index b63d59e9..56af85b0 100644 --- a/veilid-core/src/routing_table/bucket_entry.rs +++ b/veilid-core/src/routing_table/bucket_entry.rs @@ -50,8 +50,8 @@ pub struct LastConnectionKey(ProtocolType, AddressType); pub struct BucketEntryPublicInternet { /// The PublicInternet node info signed_node_info: Option>, - /// If this node has seen our publicinternet node info - seen_our_node_info: bool, + /// The last node info timestamp of ours that this entry has seen + last_seen_our_node_info_ts: u64, /// Last known node status node_status: Option, } @@ -62,8 +62,8 @@ pub struct BucketEntryPublicInternet { pub struct BucketEntryLocalNetwork { /// The LocalNetwork node info signed_node_info: Option>, - /// If this node has seen our localnetwork node info - seen_our_node_info: bool, + /// The last node info timestamp of ours that this entry has seen + last_seen_our_node_info_ts: u64, /// Last known node status node_status: Option, } @@ -427,21 +427,29 @@ impl BucketEntryInner { } } - pub fn set_seen_our_node_info(&mut self, routing_domain: RoutingDomain, seen: bool) { + pub fn set_our_node_info_ts(&mut self, routing_domain: RoutingDomain, seen_ts: u64) { match routing_domain { RoutingDomain::LocalNetwork => { - self.local_network.seen_our_node_info = seen; + self.local_network.last_seen_our_node_info_ts = seen_ts; } RoutingDomain::PublicInternet => { - self.public_internet.seen_our_node_info = seen; + self.public_internet.last_seen_our_node_info_ts = seen_ts; } } } - pub fn has_seen_our_node_info(&self, routing_domain: RoutingDomain) -> bool { + pub fn has_seen_our_node_info_ts( + &self, + routing_domain: RoutingDomain, + our_node_info_ts: u64, + ) -> bool { match routing_domain { - RoutingDomain::LocalNetwork => self.local_network.seen_our_node_info, - RoutingDomain::PublicInternet => self.public_internet.seen_our_node_info, + RoutingDomain::LocalNetwork => { + our_node_info_ts == self.local_network.last_seen_our_node_info_ts + } + RoutingDomain::PublicInternet => { + our_node_info_ts == self.public_internet.last_seen_our_node_info_ts + } } } @@ -680,12 +688,12 @@ impl BucketEntry { updated_since_last_network_change: false, last_connections: BTreeMap::new(), local_network: BucketEntryLocalNetwork { - seen_our_node_info: false, + last_seen_our_node_info_ts: 0, signed_node_info: None, node_status: None, }, public_internet: BucketEntryPublicInternet { - seen_our_node_info: false, + last_seen_our_node_info_ts: 0, signed_node_info: None, node_status: None, }, diff --git a/veilid-core/src/routing_table/mod.rs b/veilid-core/src/routing_table/mod.rs index 9b885d2f..b1718cff 100644 --- a/veilid-core/src/routing_table/mod.rs +++ b/veilid-core/src/routing_table/mod.rs @@ -374,15 +374,30 @@ impl RoutingTable { } /// Return a copy of our node's peerinfo - pub fn get_own_peer_info(&self, routing_domain: RoutingDomain) -> PeerInfo { + pub fn get_own_peer_info(&self, routing_domain: RoutingDomain) -> Option { self.inner.read().get_own_peer_info(routing_domain) } + /// Return the best effort copy of our node's peerinfo + /// This may be invalid and should not be passed to other nodes, + /// but may be used for contact method calculation + pub fn get_best_effort_own_peer_info(&self, routing_domain: RoutingDomain) -> PeerInfo { + self.inner + .read() + .get_best_effort_own_peer_info(routing_domain) + } + /// If we have a valid network class in this routing domain, then our 'NodeInfo' is valid + /// If this is true, we can get our final peer info, otherwise we only have a 'best effort' peer info pub fn has_valid_own_node_info(&self, routing_domain: RoutingDomain) -> bool { self.inner.read().has_valid_own_node_info(routing_domain) } + /// Return our current node info timestamp + pub fn get_own_node_info_ts(&self, routing_domain: RoutingDomain) -> Option { + self.inner.read().get_own_node_info_ts(routing_domain) + } + /// Return the domain's currently registered network class pub fn get_network_class(&self, routing_domain: RoutingDomain) -> Option { self.inner.read().get_network_class(routing_domain) diff --git a/veilid-core/src/routing_table/node_ref.rs b/veilid-core/src/routing_table/node_ref.rs index de456feb..ca4f0001 100644 --- a/veilid-core/src/routing_table/node_ref.rs +++ b/veilid-core/src/routing_table/node_ref.rs @@ -143,11 +143,22 @@ pub trait NodeRefBase: Sized { .unwrap_or(false) }) } - fn has_seen_our_node_info(&self, routing_domain: RoutingDomain) -> bool { - self.operate(|_rti, e| e.has_seen_our_node_info(routing_domain)) + fn node_info_ts(&self, routing_domain: RoutingDomain) -> u64 { + self.operate(|_rti, e| { + e.signed_node_info(routing_domain) + .map(|sni| sni.timestamp()) + .unwrap_or(0u64) + }) } - fn set_seen_our_node_info(&self, routing_domain: RoutingDomain) { - self.operate_mut(|_rti, e| e.set_seen_our_node_info(routing_domain, true)); + fn has_seen_our_node_info_ts( + &self, + routing_domain: RoutingDomain, + our_node_info_ts: u64, + ) -> bool { + self.operate(|_rti, e| e.has_seen_our_node_info_ts(routing_domain, our_node_info_ts)) + } + fn set_our_node_info_ts(&self, routing_domain: RoutingDomain, seen_ts: u64) { + self.operate_mut(|_rti, e| e.set_our_node_info_ts(routing_domain, seen_ts)); } fn network_class(&self, routing_domain: RoutingDomain) -> Option { self.operate(|_rt, e| e.node_info(routing_domain).map(|n| n.network_class)) diff --git a/veilid-core/src/routing_table/route_spec_store.rs b/veilid-core/src/routing_table/route_spec_store.rs index 23c6e9e6..89e1d35a 100644 --- a/veilid-core/src/routing_table/route_spec_store.rs +++ b/veilid-core/src/routing_table/route_spec_store.rs @@ -204,7 +204,7 @@ pub struct RemotePrivateRouteInfo { // The private route itself private_route: Option, /// Did this remote private route see our node info due to no safety route in use - seen_our_node_info: bool, + last_seen_our_node_info_ts: u64, /// Last time this remote private route was requested for any reason (cache expiration) last_touched_ts: u64, /// Stats @@ -618,6 +618,10 @@ impl RouteSpecStore { bail!("Not allocating route longer than max route hop count"); } + let Some(our_peer_info) = rti.get_own_peer_info(RoutingDomain::PublicInternet) else { + bail!("Can't allocate route until we have our own peer info"); + }; + // Get relay node id if we have one let opt_relay_id = rti .relay_node(RoutingDomain::PublicInternet) @@ -764,7 +768,6 @@ impl RouteSpecStore { // Ensure this route is viable by checking that each node can contact the next one if directions.contains(Direction::Outbound) { - let our_peer_info = rti.get_own_peer_info(RoutingDomain::PublicInternet); let mut previous_node = &our_peer_info; let mut reachable = true; for n in permutation { @@ -787,7 +790,6 @@ impl RouteSpecStore { } } if directions.contains(Direction::Inbound) { - let our_peer_info = rti.get_own_peer_info(RoutingDomain::PublicInternet); let mut next_node = &our_peer_info; let mut reachable = true; for n in permutation.iter().rev() { @@ -1452,9 +1454,15 @@ impl RouteSpecStore { // Make innermost route hop to our own node let mut route_hop = RouteHop { node: if optimized { + if !rti.has_valid_own_node_info(RoutingDomain::PublicInternet) { + bail!("can't make private routes until our node info is valid"); + } RouteNode::NodeId(NodeId::new(routing_table.node_id())) } else { - RouteNode::PeerInfo(rti.get_own_peer_info(RoutingDomain::PublicInternet)) + let Some(pi) = rti.get_own_peer_info(RoutingDomain::PublicInternet) else { + bail!("can't make private routes until our node info is valid"); + }; + RouteNode::PeerInfo(pi) }, next_hop: None, }; @@ -1591,7 +1599,7 @@ impl RouteSpecStore { .and_modify(|rpr| { if cur_ts - rpr.last_touched_ts >= REMOTE_PRIVATE_ROUTE_CACHE_EXPIRY { // Start fresh if this had expired - rpr.seen_our_node_info = false; + rpr.last_seen_our_node_info_ts = 0; rpr.last_touched_ts = cur_ts; rpr.stats = RouteStats::new(cur_ts); } else { @@ -1602,7 +1610,7 @@ impl RouteSpecStore { .or_insert_with(|| RemotePrivateRouteInfo { // New remote private route cache entry private_route: Some(private_route), - seen_our_node_info: false, + last_seen_our_node_info_ts: 0, last_touched_ts: cur_ts, stats: RouteStats::new(cur_ts), }); @@ -1665,22 +1673,52 @@ impl RouteSpecStore { } } - /// Check to see if this remote (not ours) private route has seen our node info yet - /// This returns true if we have sent non-safety-route node info to the - /// private route and gotten a response before + /// Check to see if this remote (not ours) private route has seen our current node info yet + /// This happens when you communicate with a private route without a safety route pub fn has_remote_private_route_seen_our_node_info(&self, key: &DHTKey) -> bool { - let inner = &mut *self.inner.lock(); - let cur_ts = get_timestamp(); - Self::with_peek_remote_private_route(inner, cur_ts, key, |rpr| rpr.seen_our_node_info) - .unwrap_or_default() + let our_node_info_ts = { + let rti = &*self.unlocked_inner.routing_table.inner.read(); + let Some(ts) = rti.get_own_node_info_ts(RoutingDomain::PublicInternet) else { + return false; + }; + ts + }; + + let opt_rpr_node_info_ts = { + let inner = &mut *self.inner.lock(); + let cur_ts = get_timestamp(); + Self::with_peek_remote_private_route(inner, cur_ts, key, |rpr| { + rpr.last_seen_our_node_info_ts + }) + }; + + let Some(rpr_node_info_ts) = opt_rpr_node_info_ts else { + return false; + }; + + our_node_info_ts == rpr_node_info_ts } - /// Mark a remote private route as having seen our node info + /// Mark a remote private route as having seen our current node info + /// PRIVACY: + /// We do not accept node info timestamps from remote private routes because this would + /// enable a deanonymization attack, whereby a node could be 'pinged' with a doctored node_info with a + /// special 'timestamp', which then may be sent back over a private route, identifying that it + /// was that node that had the private route. pub fn mark_remote_private_route_seen_our_node_info( &self, key: &DHTKey, cur_ts: u64, ) -> EyreResult<()> { + let our_node_info_ts = { + let rti = &*self.unlocked_inner.routing_table.inner.read(); + let Some(ts) = rti.get_own_node_info_ts(RoutingDomain::PublicInternet) else { + // Node info is invalid, skipping this + return Ok(()); + }; + ts + }; + let inner = &mut *self.inner.lock(); // Check for local route. If this is not a remote private route // then we just skip the recording. We may be running a test and using @@ -1689,7 +1727,7 @@ impl RouteSpecStore { return Ok(()); } if Self::with_get_remote_private_route(inner, cur_ts, key, |rpr| { - rpr.seen_our_node_info = true; + rpr.last_seen_our_node_info_ts = our_node_info_ts; }) .is_none() { @@ -1734,8 +1772,6 @@ impl RouteSpecStore { // Reset private route cache for (_k, v) in &mut inner.cache.remote_private_route_cache { - // Our node info has changed - v.seen_our_node_info = false; // Restart stats for routes so we test the route again v.stats.reset(); } diff --git a/veilid-core/src/routing_table/routing_domain_editor.rs b/veilid-core/src/routing_table/routing_domain_editor.rs index 08aba320..dce7d3cf 100644 --- a/veilid-core/src/routing_table/routing_domain_editor.rs +++ b/veilid-core/src/routing_table/routing_domain_editor.rs @@ -199,9 +199,7 @@ impl RoutingDomainEditor { } }); if changed { - // Mark that nothing in the routing table has seen our new node info - inner.reset_all_seen_our_node_info(self.routing_domain); - // + // Allow signed node info updates at same timestamp from dead nodes if our network has changed inner.reset_all_updated_since_last_network_change(); } } diff --git a/veilid-core/src/routing_table/routing_table_inner.rs b/veilid-core/src/routing_table/routing_table_inner.rs index 42a4b09b..8741451c 100644 --- a/veilid-core/src/routing_table/routing_table_inner.rs +++ b/veilid-core/src/routing_table/routing_table_inner.rs @@ -226,16 +226,6 @@ impl RoutingTableInner { }) } - pub fn reset_all_seen_our_node_info(&mut self, routing_domain: RoutingDomain) { - let cur_ts = get_timestamp(); - self.with_entries_mut(cur_ts, BucketEntryState::Dead, |rti, _, v| { - v.with_mut(rti, |_rti, e| { - e.set_seen_our_node_info(routing_domain, false); - }); - Option::<()>::None - }); - } - pub fn reset_all_updated_since_last_network_change(&mut self) { let cur_ts = get_timestamp(); self.with_entries_mut(cur_ts, BucketEntryState::Dead, |rti, _, v| { @@ -246,16 +236,43 @@ impl RoutingTableInner { }); } + /// Return if our node info is valid yet, which is only true if we have a valid network class + pub fn has_valid_own_node_info(&self, routing_domain: RoutingDomain) -> bool { + self.with_routing_domain(routing_domain, |rdd| rdd.common().has_valid_own_node_info()) + } + /// Return a copy of our node's peerinfo - pub fn get_own_peer_info(&self, routing_domain: RoutingDomain) -> PeerInfo { + pub fn get_own_peer_info(&self, routing_domain: RoutingDomain) -> Option { + self.with_routing_domain(routing_domain, |rdd| { + if !rdd.common().has_valid_own_node_info() { + None + } else { + Some(rdd.common().with_peer_info(self, |pi| pi.clone())) + } + }) + } + + /// Return the best effort copy of our node's peerinfo + /// This may be invalid and should not be passed to other nodes, + /// but may be used for contact method calculation + pub fn get_best_effort_own_peer_info(&self, routing_domain: RoutingDomain) -> PeerInfo { self.with_routing_domain(routing_domain, |rdd| { rdd.common().with_peer_info(self, |pi| pi.clone()) }) } - /// Return our currently registered network class - pub fn has_valid_own_node_info(&self, routing_domain: RoutingDomain) -> bool { - self.with_routing_domain(routing_domain, |rdd| rdd.common().has_valid_own_node_info()) + /// Return our current node info timestamp + pub fn get_own_node_info_ts(&self, routing_domain: RoutingDomain) -> Option { + self.with_routing_domain(routing_domain, |rdd| { + if !rdd.common().has_valid_own_node_info() { + None + } else { + Some( + rdd.common() + .with_peer_info(self, |pi| pi.signed_node_info.timestamp()), + ) + } + }) } /// Return the domain's currently registered network class @@ -334,7 +351,6 @@ impl RoutingTableInner { self.with_entries_mut(cur_ts, BucketEntryState::Dead, |rti, _, e| { e.with_mut(rti, |_rti, e| { e.clear_signed_node_info(RoutingDomain::LocalNetwork); - e.set_seen_our_node_info(RoutingDomain::LocalNetwork, false); e.set_updated_since_last_network_change(false); }); Option::<()>::None @@ -504,6 +520,7 @@ impl RoutingTableInner { let opt_relay_id = self.with_routing_domain(routing_domain, |rd| { rd.common().relay_node().map(|rn| rn.node_id()) }); + let own_node_info_ts = self.get_own_node_info_ts(routing_domain); // Collect all entries that are 'needs_ping' and have some node info making them reachable somehow let mut node_refs = Vec::::with_capacity(self.bucket_entry_count); diff --git a/veilid-core/src/routing_table/tasks/relay_management.rs b/veilid-core/src/routing_table/tasks/relay_management.rs index 056479e2..86cfdd3b 100644 --- a/veilid-core/src/routing_table/tasks/relay_management.rs +++ b/veilid-core/src/routing_table/tasks/relay_management.rs @@ -10,66 +10,64 @@ impl RoutingTable { cur_ts: u64, ) -> EyreResult<()> { // Get our node's current node info and network class and do the right thing - let own_peer_info = self.get_own_peer_info(RoutingDomain::PublicInternet); + let Some(own_peer_info) = self.get_own_peer_info(RoutingDomain::PublicInternet) else { + return Ok(()); + }; let own_node_info = own_peer_info.signed_node_info.node_info(); - let network_class = self.get_network_class(RoutingDomain::PublicInternet); + let network_class = own_node_info.network_class; // Get routing domain editor let mut editor = self.edit_routing_domain(RoutingDomain::PublicInternet); - // Do we know our network class yet? - if let Some(network_class) = network_class { - // If we already have a relay, see if it is dead, or if we don't need it any more - let has_relay = { - if let Some(relay_node) = self.relay_node(RoutingDomain::PublicInternet) { - let state = relay_node.state(cur_ts); - // Relay node is dead or no longer needed - if matches!(state, BucketEntryState::Dead) { - info!("Relay node died, dropping relay {}", relay_node); - editor.clear_relay_node(); - false - } else if !own_node_info.requires_relay() { - info!( - "Relay node no longer required, dropping relay {}", - relay_node - ); - editor.clear_relay_node(); - false - } else { - true - } - } else { + // If we already have a relay, see if it is dead, or if we don't need it any more + let has_relay = { + if let Some(relay_node) = self.relay_node(RoutingDomain::PublicInternet) { + let state = relay_node.state(cur_ts); + // Relay node is dead or no longer needed + if matches!(state, BucketEntryState::Dead) { + info!("Relay node died, dropping relay {}", relay_node); + editor.clear_relay_node(); false + } else if !own_node_info.requires_relay() { + info!( + "Relay node no longer required, dropping relay {}", + relay_node + ); + editor.clear_relay_node(); + false + } else { + true } - }; + } else { + false + } + }; - // Do we need a relay? - if !has_relay && own_node_info.requires_relay() { - // Do we want an outbound relay? - let mut got_outbound_relay = false; - if network_class.outbound_wants_relay() { - // The outbound relay is the host of the PWA - if let Some(outbound_relay_peerinfo) = intf::get_outbound_relay_peer().await { - // Register new outbound relay - if let Some(nr) = self.register_node_with_signed_node_info( - RoutingDomain::PublicInternet, - outbound_relay_peerinfo.node_id.key, - outbound_relay_peerinfo.signed_node_info, - false, - ) { - info!("Outbound relay node selected: {}", nr); - editor.set_relay_node(nr); - got_outbound_relay = true; - } + // Do we need a relay? + if !has_relay && own_node_info.requires_relay() { + // Do we want an outbound relay? + let mut got_outbound_relay = false; + if network_class.outbound_wants_relay() { + // The outbound relay is the host of the PWA + if let Some(outbound_relay_peerinfo) = intf::get_outbound_relay_peer().await { + // Register new outbound relay + if let Some(nr) = self.register_node_with_signed_node_info( + RoutingDomain::PublicInternet, + outbound_relay_peerinfo.node_id.key, + outbound_relay_peerinfo.signed_node_info, + false, + ) { + info!("Outbound relay node selected: {}", nr); + editor.set_relay_node(nr); + got_outbound_relay = true; } } - if !got_outbound_relay { - // Find a node in our routing table that is an acceptable inbound relay - if let Some(nr) = self.find_inbound_relay(RoutingDomain::PublicInternet, cur_ts) - { - info!("Inbound relay node selected: {}", nr); - editor.set_relay_node(nr); - } + } + if !got_outbound_relay { + // Find a node in our routing table that is an acceptable inbound relay + if let Some(nr) = self.find_inbound_relay(RoutingDomain::PublicInternet, cur_ts) { + info!("Inbound relay node selected: {}", nr); + editor.set_relay_node(nr); } } } diff --git a/veilid-core/src/rpc_processor/coders/operations/operation.rs b/veilid-core/src/rpc_processor/coders/operations/operation.rs index a594e7ec..34dfbf8c 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation.rs @@ -58,24 +58,30 @@ impl RPCOperationKind { pub struct RPCOperation { op_id: u64, sender_node_info: Option, + target_node_info_ts: u64, kind: RPCOperationKind, } impl RPCOperation { - pub fn new_question(question: RPCQuestion, sender_node_info: Option) -> Self { + pub fn new_question( + question: RPCQuestion, + sender_signed_node_info: SenderSignedNodeInfo, + ) -> Self { Self { op_id: get_random_u64(), - sender_node_info, + sender_node_info: sender_signed_node_info.signed_node_info, + target_node_info_ts: sender_signed_node_info.target_node_info_ts, kind: RPCOperationKind::Question(question), } } pub fn new_statement( statement: RPCStatement, - sender_node_info: Option, + sender_signed_node_info: SenderSignedNodeInfo, ) -> Self { Self { op_id: get_random_u64(), - sender_node_info, + sender_node_info: sender_signed_node_info.signed_node_info, + target_node_info_ts: sender_signed_node_info.target_node_info_ts, kind: RPCOperationKind::Statement(statement), } } @@ -83,11 +89,12 @@ impl RPCOperation { pub fn new_answer( request: &RPCOperation, answer: RPCAnswer, - sender_node_info: Option, + sender_signed_node_info: SenderSignedNodeInfo, ) -> Self { Self { op_id: request.op_id, - sender_node_info, + sender_node_info: sender_signed_node_info.signed_node_info, + target_node_info_ts: sender_signed_node_info.target_node_info_ts, kind: RPCOperationKind::Answer(answer), } } @@ -99,6 +106,9 @@ impl RPCOperation { pub fn sender_node_info(&self) -> Option<&SignedNodeInfo> { self.sender_node_info.as_ref() } + pub fn target_node_info_ts(&self) -> u64 { + self.target_node_info_ts + } pub fn kind(&self) -> &RPCOperationKind { &self.kind @@ -128,12 +138,15 @@ impl RPCOperation { None }; + let target_node_info_ts = operation_reader.get_target_node_info_ts(); + let kind_reader = operation_reader.get_kind(); let kind = RPCOperationKind::decode(&kind_reader, opt_sender_node_id)?; Ok(RPCOperation { op_id, sender_node_info, + target_node_info_ts, kind, }) } @@ -144,6 +157,7 @@ impl RPCOperation { let mut si_builder = builder.reborrow().init_sender_node_info(); encode_signed_node_info(&sender_info, &mut si_builder)?; } + builder.set_target_node_info_ts(self.target_node_info_ts); let mut k_builder = builder.reborrow().init_kind(); self.kind.encode(&mut k_builder)?; Ok(()) diff --git a/veilid-core/src/rpc_processor/destination.rs b/veilid-core/src/rpc_processor/destination.rs index 3c119cff..f5c7069f 100644 --- a/veilid-core/src/rpc_processor/destination.rs +++ b/veilid-core/src/rpc_processor/destination.rs @@ -217,10 +217,19 @@ impl RPCProcessor { let route_node = match rss .has_remote_private_route_seen_our_node_info(&private_route.public_key) { - true => RouteNode::NodeId(NodeId::new(routing_table.node_id())), - false => RouteNode::PeerInfo( - routing_table.get_own_peer_info(RoutingDomain::PublicInternet), - ), + true => { + if !routing_table.has_valid_own_node_info(RoutingDomain::PublicInternet) { + return Ok(NetworkResult::no_connection_other("Own node info must be valid to use private route")); + } + RouteNode::NodeId(NodeId::new(routing_table.node_id())) + } + false => { + let Some(own_peer_info) = + routing_table.get_own_peer_info(RoutingDomain::PublicInternet) else { + return Ok(NetworkResult::no_connection_other("Own peer info must be valid to use private route")); + }; + RouteNode::PeerInfo(own_peer_info) + }, }; Ok(NetworkResult::value(RespondTo::PrivateRoute( diff --git a/veilid-core/src/rpc_processor/mod.rs b/veilid-core/src/rpc_processor/mod.rs index 5a159899..49247749 100644 --- a/veilid-core/src/rpc_processor/mod.rs +++ b/veilid-core/src/rpc_processor/mod.rs @@ -189,14 +189,45 @@ impl Answer { } } +/// An operation that has been fully prepared for envelope r struct RenderedOperation { - message: Vec, // The rendered operation bytes - node_id: DHTKey, // Destination node id we're sending to - node_ref: NodeRef, // Node to send envelope to (may not be destination node id in case of relay) - hop_count: usize, // Total safety + private route hop count + 1 hop for the initial send - safety_route: Option, // The safety route used to send the message - remote_private_route: Option, // The private route used to send the message - reply_private_route: Option, // The private route requested to receive the reply + /// The rendered operation bytes + message: Vec, + /// Destination node id we're sending to + node_id: DHTKey, + /// Node to send envelope to (may not be destination node id in case of relay) + node_ref: NodeRef, + /// Total safety + private route hop count + 1 hop for the initial send + hop_count: usize, + /// The safety route used to send the message + safety_route: Option, + /// The private route used to send the message + remote_private_route: Option, + /// The private route requested to receive the reply + reply_private_route: Option, +} + +/// Node information exchanged during every RPC message +#[derive(Default, Debug, Clone)] +struct SenderSignedNodeInfo { + /// The current signed node info of the sender if required + signed_node_info: Option, + /// The last timestamp of the target's node info to assist remote node with sending its latest node info + target_node_info_ts: u64, +} +impl SenderSignedNodeInfo { + pub fn new_no_sni(target_node_info_ts: u64) -> Self { + Self { + signed_node_info: None, + target_node_info_ts, + } + } + pub fn new(sender_signed_node_info: SignedNodeInfo, target_node_info_ts: u64) -> Self { + Self { + signed_node_info: Some(sender_signed_node_info), + target_node_info_ts, + } + } } #[derive(Copy, Clone, Debug)] @@ -474,11 +505,10 @@ impl RPCProcessor { ) } }; - out } - // Wrap an operation with a private route inside a safety route + /// Wrap an operation with a private route inside a safety route fn wrap_with_route( &self, safety_selection: SafetySelection, @@ -528,9 +558,11 @@ impl RPCProcessor { safety_route: compiled_route.safety_route, operation, }; + let ssni_route = + self.get_sender_signed_node_info(&Destination::direct(compiled_route.first_hop))?; let operation = RPCOperation::new_statement( RPCStatement::new(RPCStatementDetail::Route(route_operation)), - None, + ssni_route, ); // Convert message to bytes and return it @@ -680,64 +712,75 @@ impl RPCProcessor { Ok(out) } - // Get signed node info to package with RPC messages to improve - // routing table caching when it is okay to do so - // This is only done in the PublicInternet routing domain because - // as far as we can tell this is the only domain that will really benefit - fn get_sender_signed_node_info(&self, dest: &Destination) -> Option { + /// Get signed node info to package with RPC messages to improve + /// routing table caching when it is okay to do so + #[instrument(skip(self), ret, err)] + fn get_sender_signed_node_info( + &self, + dest: &Destination, + ) -> Result { // Don't do this if the sender is to remain private // Otherwise we would be attaching the original sender's identity to the final destination, // thus defeating the purpose of the safety route entirely :P match dest.get_safety_selection() { SafetySelection::Unsafe(_) => {} SafetySelection::Safe(_) => { - return None; + return Ok(SenderSignedNodeInfo::default()); } } - // Don't do this if our own signed node info isn't valid yet - let routing_table = self.routing_table(); - if !routing_table.has_valid_own_node_info(RoutingDomain::PublicInternet) { - return None; - } - match dest { + // Get the target we're sending to + let routing_table = self.routing_table(); + let target = match dest { Destination::Direct { target, safety_selection: _, - } => { - // If the target has seen our node info already don't do this - if target.has_seen_our_node_info(RoutingDomain::PublicInternet) { - return None; - } - Some( - routing_table - .get_own_peer_info(RoutingDomain::PublicInternet) - .signed_node_info, - ) - } + } => target.clone(), Destination::Relay { relay: _, target, safety_selection: _, } => { if let Some(target) = routing_table.lookup_node_ref(*target) { - if target.has_seen_our_node_info(RoutingDomain::PublicInternet) { - return None; - } - Some( - routing_table - .get_own_peer_info(RoutingDomain::PublicInternet) - .signed_node_info, - ) + target } else { - None + // Target was not in our routing table + return Ok(SenderSignedNodeInfo::default()); } } Destination::PrivateRoute { private_route: _, safety_selection: _, - } => None, + } => { + return Ok(SenderSignedNodeInfo::default()); + } + }; + + let Some(routing_domain) = target.best_routing_domain() else { + // No routing domain for target? + return Err(RPCError::internal(format!("No routing domain for target: {}", target))); + }; + + // Get the target's node info timestamp + let target_node_info_ts = target.node_info_ts(routing_domain); + + // Don't return our node info if it's not valid yet + let Some(own_peer_info) = routing_table.get_own_peer_info(routing_domain) else { + return Ok(SenderSignedNodeInfo::new_no_sni(target_node_info_ts)); + }; + + // Get our node info timestamp + let our_node_info_ts = own_peer_info.signed_node_info.timestamp(); + + // If the target has seen our node info already don't send it again + if target.has_seen_our_node_info_ts(routing_domain, our_node_info_ts) { + return Ok(SenderSignedNodeInfo::new_no_sni(target_node_info_ts)); } + + Ok(SenderSignedNodeInfo::new( + own_peer_info.signed_node_info, + target_node_info_ts, + )) } /// Record failure to send to node or route @@ -981,11 +1024,11 @@ impl RPCProcessor { dest: Destination, question: RPCQuestion, ) -> Result, RPCError> { - // Get sender info if we should send that - let opt_sender_info = self.get_sender_signed_node_info(&dest); + // Get sender signed node info if we should send that + let ssni = self.get_sender_signed_node_info(&dest)?; // Wrap question in operation - let operation = RPCOperation::new_question(question, opt_sender_info); + let operation = RPCOperation::new_question(question, ssni); let op_id = operation.op_id(); // Log rpc send @@ -1056,11 +1099,11 @@ impl RPCProcessor { dest: Destination, statement: RPCStatement, ) -> Result, RPCError> { - // Get sender info if we should send that - let opt_sender_info = self.get_sender_signed_node_info(&dest); + // Get sender signed node info if we should send that + let ssni = self.get_sender_signed_node_info(&dest)?; // Wrap statement in operation - let operation = RPCOperation::new_statement(statement, opt_sender_info); + let operation = RPCOperation::new_statement(statement, ssni); // Log rpc send trace!(target: "rpc_message", dir = "send", kind = "statement", op_id = operation.op_id(), desc = operation.kind().desc(), ?dest); @@ -1117,11 +1160,11 @@ impl RPCProcessor { // Extract destination from respond_to let dest = network_result_try!(self.get_respond_to_destination(&request)); - // Get sender info if we should send that - let opt_sender_info = self.get_sender_signed_node_info(&dest); + // Get sender signed node info if we should send that + let ssni = self.get_sender_signed_node_info(&dest)?; // Wrap answer in operation - let operation = RPCOperation::new_answer(&request.operation, answer, opt_sender_info); + let operation = RPCOperation::new_answer(&request.operation, answer, ssni); // Log rpc send trace!(target: "rpc_message", dir = "send", kind = "answer", op_id = operation.op_id(), desc = operation.kind().desc(), ?dest); @@ -1213,10 +1256,10 @@ impl RPCProcessor { opt_sender_nr = self.routing_table().lookup_node_ref(sender_node_id) } - // Mark this sender as having seen our node info over this routing domain - // because it managed to reach us over that routing domain + // Update the 'seen our node info' timestamp to determine if this node needs a + // 'node info update' ping if let Some(sender_nr) = &opt_sender_nr { - sender_nr.set_seen_our_node_info(routing_domain); + sender_nr.set_our_node_info_ts(routing_domain, operation.target_node_info_ts()); } // Make the RPC message diff --git a/veilid-core/src/rpc_processor/rpc_find_node.rs b/veilid-core/src/rpc_processor/rpc_find_node.rs index 94549971..cd4e62b5 100644 --- a/veilid-core/src/rpc_processor/rpc_find_node.rs +++ b/veilid-core/src/rpc_processor/rpc_find_node.rs @@ -92,9 +92,8 @@ impl RPCProcessor { // add node information for the requesting node to our routing table let routing_table = self.routing_table(); - let has_valid_own_node_info = - routing_table.has_valid_own_node_info(RoutingDomain::PublicInternet); let own_peer_info = routing_table.get_own_peer_info(RoutingDomain::PublicInternet); + let has_valid_own_node_info = own_peer_info.is_some(); // find N nodes closest to the target node in our routing table @@ -116,7 +115,7 @@ impl RPCProcessor { |rti, k, v| { rti.transform_to_peer_info( RoutingDomain::PublicInternet, - own_peer_info.clone(), + own_peer_info.as_ref().unwrap().clone(), k, v, ) diff --git a/veilid-wasm/src/lib.rs b/veilid-wasm/src/lib.rs index ceab87c0..d779ba27 100644 --- a/veilid-wasm/src/lib.rs +++ b/veilid-wasm/src/lib.rs @@ -30,11 +30,6 @@ extern crate wee_alloc; #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; -static SETUP_ONCE: Once = Once::new(); -pub fn setup() -> () { - SETUP_ONCE.call_once(|| {}); -} - // API Singleton lazy_static! { static ref VEILID_API: SendWrapper>> = @@ -138,48 +133,54 @@ pub fn initialize_veilid_wasm() { console_error_panic_hook::set_once(); } +static SETUP_ONCE: Once = Once::new(); #[wasm_bindgen()] pub fn initialize_veilid_core(platform_config: String) { - let platform_config: VeilidWASMConfig = veilid_core::deserialize_json(&platform_config) - .expect("failed to deserialize platform config json"); + SETUP_ONCE.call_once(|| { + let platform_config: VeilidWASMConfig = veilid_core::deserialize_json(&platform_config) + .expect("failed to deserialize platform config json"); - // Set up subscriber and layers - let subscriber = Registry::default(); - let mut layers = Vec::new(); - let mut filters = (*FILTERS).borrow_mut(); + // Set up subscriber and layers + let subscriber = Registry::default(); + let mut layers = Vec::new(); + let mut filters = (*FILTERS).borrow_mut(); - // Performance logger - if platform_config.logging.performance.enabled { - let filter = - veilid_core::VeilidLayerFilter::new(platform_config.logging.performance.level, None); - let layer = WASMLayer::new( - WASMLayerConfigBuilder::new() - .set_report_logs_in_timings(platform_config.logging.performance.logs_in_timings) - .set_console_config(if platform_config.logging.performance.logs_in_console { - ConsoleConfig::ReportWithConsoleColor - } else { - ConsoleConfig::NoReporting - }) - .build(), - ) - .with_filter(filter.clone()); - filters.insert("performance", filter); - layers.push(layer.boxed()); - }; + // Performance logger + if platform_config.logging.performance.enabled { + let filter = veilid_core::VeilidLayerFilter::new( + platform_config.logging.performance.level, + None, + ); + let layer = WASMLayer::new( + WASMLayerConfigBuilder::new() + .set_report_logs_in_timings(platform_config.logging.performance.logs_in_timings) + .set_console_config(if platform_config.logging.performance.logs_in_console { + ConsoleConfig::ReportWithConsoleColor + } else { + ConsoleConfig::NoReporting + }) + .build(), + ) + .with_filter(filter.clone()); + filters.insert("performance", filter); + layers.push(layer.boxed()); + }; - // API logger - if platform_config.logging.api.enabled { - let filter = veilid_core::VeilidLayerFilter::new(platform_config.logging.api.level, None); - let layer = veilid_core::ApiTracingLayer::get().with_filter(filter.clone()); - filters.insert("api", filter); - layers.push(layer.boxed()); - } + // API logger + if platform_config.logging.api.enabled { + let filter = + veilid_core::VeilidLayerFilter::new(platform_config.logging.api.level, None); + let layer = veilid_core::ApiTracingLayer::get().with_filter(filter.clone()); + filters.insert("api", filter); + layers.push(layer.boxed()); + } - let subscriber = subscriber.with(layers); - subscriber - .try_init() - .map_err(|e| format!("failed to initialize logging: {}", e)) - .expect("failed to initalize WASM platform"); + let subscriber = subscriber.with(layers); + subscriber + .try_init() + .map_err(|e| format!("failed to initialize logging: {}", e)) + .expect("failed to initalize WASM platform"); + }); } #[wasm_bindgen()] From 2b9044fdfa95e067888a45d68f85354b661382d7 Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 8 Dec 2022 12:48:01 -0500 Subject: [PATCH 42/88] close #169 --- veilid-core/proto/veilid.capnp | 38 ++++----- veilid-core/src/network_manager/mod.rs | 62 +------------- veilid-core/src/network_manager/native/mod.rs | 2 - veilid-core/src/network_manager/tasks/mod.rs | 10 --- veilid-core/src/network_manager/wasm/mod.rs | 1 - veilid-core/src/routing_table/bucket_entry.rs | 48 ++++++++++- veilid-core/src/routing_table/mod.rs | 11 --- veilid-core/src/routing_table/node_ref.rs | 3 +- .../routing_table/routing_domain_editor.rs | 15 +--- .../src/routing_table/routing_table_inner.rs | 44 ++++------ .../rpc_processor/coders/operations/mod.rs | 2 - .../coders/operations/operation.rs | 9 +- .../operations/operation_node_info_update.rs | 32 ------- .../coders/operations/statement.rs | 18 +--- veilid-core/src/rpc_processor/mod.rs | 35 +------- .../src/rpc_processor/rpc_node_info_update.rs | 84 ------------------- 16 files changed, 92 insertions(+), 322 deletions(-) delete mode 100644 veilid-core/src/rpc_processor/coders/operations/operation_node_info_update.rs delete mode 100644 veilid-core/src/rpc_processor/rpc_node_info_update.rs diff --git a/veilid-core/proto/veilid.capnp b/veilid-core/proto/veilid.capnp index 3a247908..a0d13643 100644 --- a/veilid-core/proto/veilid.capnp +++ b/veilid-core/proto/veilid.capnp @@ -303,11 +303,6 @@ struct OperationRoute @0x96741859ce6ac7dd { operation @1 :RoutedOperation; # The operation to be routed } -struct OperationNodeInfoUpdate @0xc9647b32a48b66ce { - signedNodeInfo @0 :SignedNodeInfo; # Our signed node info -} - - struct OperationAppCallQ @0xade67b9f09784507 { message @0 :Data; # Opaque request to application } @@ -466,12 +461,12 @@ struct Question @0xd8510bc33492ef70 { findNodeQ @3 :OperationFindNodeQ; # Routable operations - getValueQ @4 :OperationGetValueQ; - setValueQ @5 :OperationSetValueQ; - watchValueQ @6 :OperationWatchValueQ; - supplyBlockQ @7 :OperationSupplyBlockQ; - findBlockQ @8 :OperationFindBlockQ; - appCallQ @9 :OperationAppCallQ; + appCallQ @4 :OperationAppCallQ; + getValueQ @5 :OperationGetValueQ; + setValueQ @6 :OperationSetValueQ; + watchValueQ @7 :OperationWatchValueQ; + supplyBlockQ @8 :OperationSupplyBlockQ; + findBlockQ @9 :OperationFindBlockQ; # Tunnel operations startTunnelQ @10 :OperationStartTunnelQ; @@ -486,13 +481,12 @@ struct Statement @0x990e20828f404ae1 { # Direct operations validateDialInfo @0 :OperationValidateDialInfo; route @1 :OperationRoute; - nodeInfoUpdate @2 :OperationNodeInfoUpdate; # Routable operations - valueChanged @3 :OperationValueChanged; - signal @4 :OperationSignal; - returnReceipt @5 :OperationReturnReceipt; - appMessage @6 :OperationAppMessage; + signal @2 :OperationSignal; + returnReceipt @3 :OperationReturnReceipt; + appMessage @4 :OperationAppMessage; + valueChanged @5 :OperationValueChanged; } } @@ -504,12 +498,12 @@ struct Answer @0xacacb8b6988c1058 { findNodeA @1 :OperationFindNodeA; # Routable operations - getValueA @2 :OperationGetValueA; - setValueA @3 :OperationSetValueA; - watchValueA @4 :OperationWatchValueA; - supplyBlockA @5 :OperationSupplyBlockA; - findBlockA @6 :OperationFindBlockA; - appCallA @7 :OperationAppCallA; + appCallA @2 :OperationAppCallA; + getValueA @3 :OperationGetValueA; + setValueA @4 :OperationSetValueA; + watchValueA @5 :OperationWatchValueA; + supplyBlockA @6 :OperationSupplyBlockA; + findBlockA @7 :OperationFindBlockA; # Tunnel operations startTunnelA @8 :OperationStartTunnelA; diff --git a/veilid-core/src/network_manager/mod.rs b/veilid-core/src/network_manager/mod.rs index d204a982..56b95702 100644 --- a/veilid-core/src/network_manager/mod.rs +++ b/veilid-core/src/network_manager/mod.rs @@ -23,7 +23,7 @@ pub use network_connection::*; use connection_handle::*; use connection_limits::*; use crypto::*; -use futures_util::stream::{FuturesUnordered, StreamExt}; +use futures_util::stream::FuturesUnordered; use hashlink::LruCache; use intf::*; #[cfg(not(target_arch = "wasm32"))] @@ -155,7 +155,6 @@ struct NetworkManagerUnlockedInner { // Background processes rolling_transfers_task: TickTask, public_address_check_task: TickTask, - node_info_update_single_future: MustJoinSingleFuture<()>, } #[derive(Clone)] @@ -191,7 +190,6 @@ impl NetworkManager { update_callback: RwLock::new(None), rolling_transfers_task: TickTask::new(ROLLING_TRANSFERS_INTERVAL_SECS), public_address_check_task: TickTask::new(PUBLIC_ADDRESS_CHECK_TASK_INTERVAL_SECS), - node_info_update_single_future: MustJoinSingleFuture::new(), } } @@ -1734,62 +1732,4 @@ impl NetworkManager { } } - // Inform routing table entries that our dial info has changed - pub async fn send_node_info_updates(&self, routing_domain: RoutingDomain, all: bool) { - let this = self.clone(); - - // Run in background only once - let _ = self - .clone() - .unlocked_inner - .node_info_update_single_future - .single_spawn( - async move { - // Only update if we actually have valid signed node info for this routing domain - if !this.routing_table().has_valid_own_node_info(routing_domain) { - trace!( - "not sending node info update because our network class is not yet valid" - ); - return; - } - - // Get the list of refs to all nodes to update - let cur_ts = get_timestamp(); - let node_refs = - this.routing_table() - .get_nodes_needing_updates(routing_domain, cur_ts, all); - - // Send the updates - log_net!(debug "Sending node info updates to {} nodes", node_refs.len()); - let mut unord = FuturesUnordered::new(); - for nr in node_refs { - let rpc = this.rpc_processor(); - unord.push( - async move { - // Update the node - if let Err(e) = rpc - .rpc_call_node_info_update(nr.clone(), routing_domain) - .await - { - // Not fatal, but we should be able to see if this is happening - trace!("failed to send node info update to {:?}: {}", nr, e); - return; - } - - // Mark the node as having seen our node info - nr.set_seen_our_node_info(routing_domain); - } - .instrument(Span::current()), - ); - } - - // Wait for futures to complete - while unord.next().await.is_some() {} - - log_rtab!(debug "Finished sending node updates"); - } - .instrument(Span::current()), - ) - .await; - } } diff --git a/veilid-core/src/network_manager/native/mod.rs b/veilid-core/src/network_manager/native/mod.rs index 23b05614..0ea3563b 100644 --- a/veilid-core/src/network_manager/native/mod.rs +++ b/veilid-core/src/network_manager/native/mod.rs @@ -831,12 +831,10 @@ impl Network { debug!("clearing dial info"); let mut editor = routing_table.edit_routing_domain(RoutingDomain::PublicInternet); - editor.disable_node_info_updates(); editor.clear_dial_info_details(); editor.commit().await; let mut editor = routing_table.edit_routing_domain(RoutingDomain::LocalNetwork); - editor.disable_node_info_updates(); editor.clear_dial_info_details(); editor.commit().await; diff --git a/veilid-core/src/network_manager/tasks/mod.rs b/veilid-core/src/network_manager/tasks/mod.rs index 108afd2d..e5ec11f8 100644 --- a/veilid-core/src/network_manager/tasks/mod.rs +++ b/veilid-core/src/network_manager/tasks/mod.rs @@ -68,15 +68,5 @@ impl NetworkManager { if let Err(e) = self.unlocked_inner.rolling_transfers_task.stop().await { warn!("rolling_transfers_task not stopped: {}", e); } - debug!("stopping node info update singlefuture"); - if self - .unlocked_inner - .node_info_update_single_future - .join() - .await - .is_err() - { - error!("node_info_update_single_future not stopped"); - } } } diff --git a/veilid-core/src/network_manager/wasm/mod.rs b/veilid-core/src/network_manager/wasm/mod.rs index 3287ea0b..b71aee14 100644 --- a/veilid-core/src/network_manager/wasm/mod.rs +++ b/veilid-core/src/network_manager/wasm/mod.rs @@ -318,7 +318,6 @@ impl Network { // Drop all dial info let mut editor = routing_table.edit_routing_domain(RoutingDomain::PublicInternet); - editor.disable_node_info_updates(); editor.clear_dial_info_details(); editor.commit().await; diff --git a/veilid-core/src/routing_table/bucket_entry.rs b/veilid-core/src/routing_table/bucket_entry.rs index 56af85b0..3224ac3a 100644 --- a/veilid-core/src/routing_table/bucket_entry.rs +++ b/veilid-core/src/routing_table/bucket_entry.rs @@ -275,6 +275,30 @@ impl BucketEntryInner { false } + pub fn exists_in_routing_domain( + &self, + rti: &RoutingTableInner, + routing_domain: RoutingDomain, + ) -> bool { + // Check node info + if self.has_node_info(routing_domain.into()) { + return true; + } + + // Check connections + let connection_manager = rti.network_manager().connection_manager(); + let last_connections = self.last_connections( + rti, + Some(NodeRefFilter::new().with_routing_domain(routing_domain)), + ); + for lc in last_connections { + if connection_manager.get_connection(lc.0).is_some() { + return true; + } + } + false + } + pub fn node_info(&self, routing_domain: RoutingDomain) -> Option<&NodeInfo> { let opt_current_sni = match routing_domain { RoutingDomain::LocalNetwork => &self.local_network.signed_node_info, @@ -304,8 +328,10 @@ impl BucketEntryInner { pub fn best_routing_domain( &self, + rti: &RoutingTableInner, routing_domain_set: RoutingDomainSet, ) -> Option { + // Check node info for routing_domain in routing_domain_set { let opt_current_sni = match routing_domain { RoutingDomain::LocalNetwork => &self.local_network.signed_node_info, @@ -315,7 +341,27 @@ impl BucketEntryInner { return Some(routing_domain); } } - None + // Check connections + let mut best_routing_domain: Option = None; + let connection_manager = rti.network_manager().connection_manager(); + let last_connections = self.last_connections( + rti, + Some(NodeRefFilter::new().with_routing_domain_set(routing_domain_set)), + ); + for lc in last_connections { + if connection_manager.get_connection(lc.0).is_some() { + if let Some(rd) = rti.routing_domain_for_address(lc.0.remote_address().address()) { + if let Some(brd) = best_routing_domain { + if rd < brd { + best_routing_domain = Some(rd); + } + } else { + best_routing_domain = Some(rd); + } + } + } + } + best_routing_domain } fn descriptor_to_key(&self, last_connection: ConnectionDescriptor) -> LastConnectionKey { diff --git a/veilid-core/src/routing_table/mod.rs b/veilid-core/src/routing_table/mod.rs index b1718cff..987841b5 100644 --- a/veilid-core/src/routing_table/mod.rs +++ b/veilid-core/src/routing_table/mod.rs @@ -457,17 +457,6 @@ impl RoutingTable { .get_entry_count(routing_domain_set, min_state) } - pub fn get_nodes_needing_updates( - &self, - routing_domain: RoutingDomain, - cur_ts: u64, - all: bool, - ) -> Vec { - self.inner - .read() - .get_nodes_needing_updates(self.clone(), routing_domain, cur_ts, all) - } - pub fn get_nodes_needing_ping( &self, routing_domain: RoutingDomain, diff --git a/veilid-core/src/routing_table/node_ref.rs b/veilid-core/src/routing_table/node_ref.rs index ca4f0001..b99ec68f 100644 --- a/veilid-core/src/routing_table/node_ref.rs +++ b/veilid-core/src/routing_table/node_ref.rs @@ -87,8 +87,9 @@ pub trait NodeRefBase: Sized { } fn best_routing_domain(&self) -> Option { - self.operate(|_rti, e| { + self.operate(|rti, e| { e.best_routing_domain( + rti, self.common() .filter .as_ref() diff --git a/veilid-core/src/routing_table/routing_domain_editor.rs b/veilid-core/src/routing_table/routing_domain_editor.rs index dce7d3cf..28785c12 100644 --- a/veilid-core/src/routing_table/routing_domain_editor.rs +++ b/veilid-core/src/routing_table/routing_domain_editor.rs @@ -23,7 +23,6 @@ pub struct RoutingDomainEditor { routing_table: RoutingTable, routing_domain: RoutingDomain, changes: Vec, - send_node_info_updates: bool, } impl RoutingDomainEditor { @@ -32,13 +31,8 @@ impl RoutingDomainEditor { routing_table, routing_domain, changes: Vec::new(), - send_node_info_updates: true, } } - #[instrument(level = "debug", skip(self))] - pub fn disable_node_info_updates(&mut self) { - self.send_node_info_updates = false; - } #[instrument(level = "debug", skip(self))] pub fn clear_dial_info_details(&mut self) { @@ -199,7 +193,7 @@ impl RoutingDomainEditor { } }); if changed { - // Allow signed node info updates at same timestamp from dead nodes if our network has changed + // Allow signed node info updates at same timestamp for otherwise dead nodes if our network has changed inner.reset_all_updated_since_last_network_change(); } } @@ -210,12 +204,5 @@ impl RoutingDomainEditor { rss.reset(); } } - // Send our updated node info to all the nodes in the routing table - if changed && self.send_node_info_updates { - let network_manager = self.routing_table.unlocked_inner.network_manager.clone(); - network_manager - .send_node_info_updates(self.routing_domain, true) - .await; - } } } diff --git a/veilid-core/src/routing_table/routing_table_inner.rs b/veilid-core/src/routing_table/routing_table_inner.rs index 8741451c..8338be29 100644 --- a/veilid-core/src/routing_table/routing_table_inner.rs +++ b/veilid-core/src/routing_table/routing_table_inner.rs @@ -428,7 +428,7 @@ impl RoutingTableInner { let mut count = 0usize; let cur_ts = get_timestamp(); self.with_entries(cur_ts, min_state, |rti, _, e| { - if e.with(rti, |_rti, e| e.best_routing_domain(routing_domain_set)) + if e.with(rti, |rti, e| e.best_routing_domain(rti, routing_domain_set)) .is_some() { count += 1; @@ -487,29 +487,6 @@ impl RoutingTableInner { None } - pub fn get_nodes_needing_updates( - &self, - outer_self: RoutingTable, - routing_domain: RoutingDomain, - cur_ts: u64, - all: bool, - ) -> Vec { - let mut node_refs = Vec::::with_capacity(self.bucket_entry_count); - self.with_entries(cur_ts, BucketEntryState::Unreliable, |rti, k, v| { - // Only update nodes that haven't seen our node info yet - if all || !v.with(rti, |_rti, e| e.has_seen_our_node_info(routing_domain)) { - node_refs.push(NodeRef::new( - outer_self.clone(), - k, - v, - Some(NodeRefFilter::new().with_routing_domain(routing_domain)), - )); - } - Option::<()>::None - }); - node_refs - } - pub fn get_nodes_needing_ping( &self, outer_self: RoutingTable, @@ -525,9 +502,22 @@ impl RoutingTableInner { // Collect all entries that are 'needs_ping' and have some node info making them reachable somehow let mut node_refs = Vec::::with_capacity(self.bucket_entry_count); self.with_entries(cur_ts, BucketEntryState::Unreliable, |rti, k, v| { - if v.with(rti, |_rti, e| { - e.has_node_info(routing_domain.into()) - && e.needs_ping(cur_ts, opt_relay_id == Some(k)) + if v.with(rti, |rti, e| { + // If this isn't in the routing domain we are checking, don't include it + if !e.exists_in_routing_domain(rti, routing_domain) { + return false; + } + // If we need a ping via the normal timing mechanism, then do it + if e.needs_ping(cur_ts, opt_relay_id == Some(k)) { + return true; + } + // If we need a ping because this node hasn't seen our latest node info, then do it + if let Some(own_node_info_ts) = own_node_info_ts { + if !e.has_seen_our_node_info_ts(routing_domain, own_node_info_ts) { + return true; + } + } + false }) { node_refs.push(NodeRef::new( outer_self.clone(), diff --git a/veilid-core/src/rpc_processor/coders/operations/mod.rs b/veilid-core/src/rpc_processor/coders/operations/mod.rs index 3c91d344..7caf0fb2 100644 --- a/veilid-core/src/rpc_processor/coders/operations/mod.rs +++ b/veilid-core/src/rpc_processor/coders/operations/mod.rs @@ -7,7 +7,6 @@ mod operation_complete_tunnel; mod operation_find_block; mod operation_find_node; mod operation_get_value; -mod operation_node_info_update; mod operation_return_receipt; mod operation_route; mod operation_set_value; @@ -31,7 +30,6 @@ pub use operation_complete_tunnel::*; pub use operation_find_block::*; pub use operation_find_node::*; pub use operation_get_value::*; -pub use operation_node_info_update::*; pub use operation_return_receipt::*; pub use operation_route::*; pub use operation_set_value::*; diff --git a/veilid-core/src/rpc_processor/coders/operations/operation.rs b/veilid-core/src/rpc_processor/coders/operations/operation.rs index 34dfbf8c..46651748 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation.rs @@ -16,10 +16,7 @@ impl RPCOperationKind { } } - pub fn decode( - kind_reader: &veilid_capnp::operation::kind::Reader, - opt_sender_node_id: Option<&DHTKey>, - ) -> Result { + pub fn decode(kind_reader: &veilid_capnp::operation::kind::Reader) -> Result { let which_reader = kind_reader.which().map_err(RPCError::protocol)?; let out = match which_reader { veilid_capnp::operation::kind::Which::Question(r) => { @@ -29,7 +26,7 @@ impl RPCOperationKind { } veilid_capnp::operation::kind::Which::Statement(r) => { let q_reader = r.map_err(RPCError::protocol)?; - let out = RPCStatement::decode(&q_reader, opt_sender_node_id)?; + let out = RPCStatement::decode(&q_reader)?; RPCOperationKind::Statement(out) } veilid_capnp::operation::kind::Which::Answer(r) => { @@ -141,7 +138,7 @@ impl RPCOperation { let target_node_info_ts = operation_reader.get_target_node_info_ts(); let kind_reader = operation_reader.get_kind(); - let kind = RPCOperationKind::decode(&kind_reader, opt_sender_node_id)?; + let kind = RPCOperationKind::decode(&kind_reader)?; Ok(RPCOperation { op_id, diff --git a/veilid-core/src/rpc_processor/coders/operations/operation_node_info_update.rs b/veilid-core/src/rpc_processor/coders/operations/operation_node_info_update.rs deleted file mode 100644 index 386805a3..00000000 --- a/veilid-core/src/rpc_processor/coders/operations/operation_node_info_update.rs +++ /dev/null @@ -1,32 +0,0 @@ -use super::*; - -#[derive(Debug, Clone)] -pub struct RPCOperationNodeInfoUpdate { - pub signed_node_info: SignedNodeInfo, -} - -impl RPCOperationNodeInfoUpdate { - pub fn decode( - reader: &veilid_capnp::operation_node_info_update::Reader, - opt_sender_node_id: Option<&DHTKey>, - ) -> Result { - if opt_sender_node_id.is_none() { - return Err(RPCError::protocol( - "can't decode node info update without sender node id", - )); - } - let sender_node_id = opt_sender_node_id.unwrap(); - let sni_reader = reader.get_signed_node_info().map_err(RPCError::protocol)?; - let signed_node_info = decode_signed_node_info(&sni_reader, sender_node_id)?; - - Ok(RPCOperationNodeInfoUpdate { signed_node_info }) - } - pub fn encode( - &self, - builder: &mut veilid_capnp::operation_node_info_update::Builder, - ) -> Result<(), RPCError> { - let mut sni_builder = builder.reborrow().init_signed_node_info(); - encode_signed_node_info(&self.signed_node_info, &mut sni_builder)?; - Ok(()) - } -} diff --git a/veilid-core/src/rpc_processor/coders/operations/statement.rs b/veilid-core/src/rpc_processor/coders/operations/statement.rs index 3a019dce..1c2ddf59 100644 --- a/veilid-core/src/rpc_processor/coders/operations/statement.rs +++ b/veilid-core/src/rpc_processor/coders/operations/statement.rs @@ -18,12 +18,9 @@ impl RPCStatement { pub fn desc(&self) -> &'static str { self.detail.desc() } - pub fn decode( - reader: &veilid_capnp::statement::Reader, - opt_sender_node_id: Option<&DHTKey>, - ) -> Result { + pub fn decode(reader: &veilid_capnp::statement::Reader) -> Result { let d_reader = reader.get_detail(); - let detail = RPCStatementDetail::decode(&d_reader, opt_sender_node_id)?; + let detail = RPCStatementDetail::decode(&d_reader)?; Ok(RPCStatement { detail }) } pub fn encode(&self, builder: &mut veilid_capnp::statement::Builder) -> Result<(), RPCError> { @@ -36,7 +33,6 @@ impl RPCStatement { pub enum RPCStatementDetail { ValidateDialInfo(RPCOperationValidateDialInfo), Route(RPCOperationRoute), - NodeInfoUpdate(RPCOperationNodeInfoUpdate), ValueChanged(RPCOperationValueChanged), Signal(RPCOperationSignal), ReturnReceipt(RPCOperationReturnReceipt), @@ -48,7 +44,6 @@ impl RPCStatementDetail { match self { RPCStatementDetail::ValidateDialInfo(_) => "ValidateDialInfo", RPCStatementDetail::Route(_) => "Route", - RPCStatementDetail::NodeInfoUpdate(_) => "NodeInfoUpdate", RPCStatementDetail::ValueChanged(_) => "ValueChanged", RPCStatementDetail::Signal(_) => "Signal", RPCStatementDetail::ReturnReceipt(_) => "ReturnReceipt", @@ -57,7 +52,6 @@ impl RPCStatementDetail { } pub fn decode( reader: &veilid_capnp::statement::detail::Reader, - opt_sender_node_id: Option<&DHTKey>, ) -> Result { let which_reader = reader.which().map_err(RPCError::protocol)?; let out = match which_reader { @@ -71,11 +65,6 @@ impl RPCStatementDetail { let out = RPCOperationRoute::decode(&op_reader)?; RPCStatementDetail::Route(out) } - veilid_capnp::statement::detail::NodeInfoUpdate(r) => { - let op_reader = r.map_err(RPCError::protocol)?; - let out = RPCOperationNodeInfoUpdate::decode(&op_reader, opt_sender_node_id)?; - RPCStatementDetail::NodeInfoUpdate(out) - } veilid_capnp::statement::detail::ValueChanged(r) => { let op_reader = r.map_err(RPCError::protocol)?; let out = RPCOperationValueChanged::decode(&op_reader)?; @@ -108,9 +97,6 @@ impl RPCStatementDetail { d.encode(&mut builder.reborrow().init_validate_dial_info()) } RPCStatementDetail::Route(d) => d.encode(&mut builder.reborrow().init_route()), - RPCStatementDetail::NodeInfoUpdate(d) => { - d.encode(&mut builder.reborrow().init_node_info_update()) - } RPCStatementDetail::ValueChanged(d) => { d.encode(&mut builder.reborrow().init_value_changed()) } diff --git a/veilid-core/src/rpc_processor/mod.rs b/veilid-core/src/rpc_processor/mod.rs index 49247749..77d0dda2 100644 --- a/veilid-core/src/rpc_processor/mod.rs +++ b/veilid-core/src/rpc_processor/mod.rs @@ -9,7 +9,6 @@ mod rpc_error; mod rpc_find_block; mod rpc_find_node; mod rpc_get_value; -mod rpc_node_info_update; mod rpc_return_receipt; mod rpc_route; mod rpc_set_value; @@ -113,16 +112,6 @@ impl RPCMessageData { } } -// impl ReaderSegments for RPCMessageData { -// fn get_segment(&self, idx: u32) -> Option<&[u8]> { -// if idx > 0 { -// None -// } else { -// Some(self.contents.as_slice()) -// } -// } -// } - #[derive(Debug)] struct RPCMessageEncoded { header: RPCMessageHeader, @@ -145,25 +134,8 @@ where .map_err(RPCError::protocol) .map_err(logthru_rpc!())?; Ok(buffer) - // let wordvec = builder - // .into_reader() - // .canonicalize() - // .map_err(RPCError::protocol) - // .map_err(logthru_rpc!())?; - // Ok(capnp::Word::words_to_bytes(wordvec.as_slice()).to_vec()) } -// fn reader_to_vec<'a, T>(reader: &capnp::message::Reader) -> Result, RPCError> -// where -// T: capnp::message::ReaderSegments + 'a, -// { -// let wordvec = reader -// .canonicalize() -// .map_err(RPCError::protocol) -// .map_err(logthru_rpc!())?; -// Ok(capnp::Word::words_to_bytes(wordvec.as_slice()).to_vec()) -// } - #[derive(Debug)] struct WaitableReply { handle: OperationWaitHandle, @@ -209,7 +181,7 @@ struct RenderedOperation { /// Node information exchanged during every RPC message #[derive(Default, Debug, Clone)] -struct SenderSignedNodeInfo { +pub struct SenderSignedNodeInfo { /// The current signed node info of the sender if required signed_node_info: Option, /// The last timestamp of the target's node info to assist remote node with sending its latest node info @@ -558,8 +530,8 @@ impl RPCProcessor { safety_route: compiled_route.safety_route, operation, }; - let ssni_route = - self.get_sender_signed_node_info(&Destination::direct(compiled_route.first_hop))?; + let ssni_route = self + .get_sender_signed_node_info(&Destination::direct(compiled_route.first_hop.clone()))?; let operation = RPCOperation::new_statement( RPCStatement::new(RPCStatementDetail::Route(route_operation)), ssni_route, @@ -1334,7 +1306,6 @@ impl RPCProcessor { self.process_validate_dial_info(msg).await } RPCStatementDetail::Route(_) => self.process_route(msg).await, - RPCStatementDetail::NodeInfoUpdate(_) => self.process_node_info_update(msg).await, RPCStatementDetail::ValueChanged(_) => self.process_value_changed(msg).await, RPCStatementDetail::Signal(_) => self.process_signal(msg).await, RPCStatementDetail::ReturnReceipt(_) => self.process_return_receipt(msg).await, diff --git a/veilid-core/src/rpc_processor/rpc_node_info_update.rs b/veilid-core/src/rpc_processor/rpc_node_info_update.rs deleted file mode 100644 index 427ec30f..00000000 --- a/veilid-core/src/rpc_processor/rpc_node_info_update.rs +++ /dev/null @@ -1,84 +0,0 @@ -use super::*; - -impl RPCProcessor { - // Sends a our node info to another node - #[instrument(level = "trace", skip(self), ret, err)] - pub async fn rpc_call_node_info_update( - self, - target: NodeRef, - routing_domain: RoutingDomain, - ) -> Result, RPCError> { - // Get the signed node info for the desired routing domain to send update with - let signed_node_info = self - .routing_table() - .get_own_peer_info(routing_domain) - .signed_node_info; - let node_info_update = RPCOperationNodeInfoUpdate { signed_node_info }; - let statement = RPCStatement::new(RPCStatementDetail::NodeInfoUpdate(node_info_update)); - - // Send the node_info_update request to the specific routing domain requested - network_result_try!( - self.statement( - Destination::direct( - target.filtered_clone(NodeRefFilter::new().with_routing_domain(routing_domain)) - ), - statement, - ) - .await? - ); - - Ok(NetworkResult::value(())) - } - - #[instrument(level = "trace", skip(self, msg), fields(msg.operation.op_id), ret, err)] - pub(crate) async fn process_node_info_update( - &self, - msg: RPCMessage, - ) -> Result, RPCError> { - let detail = match msg.header.detail { - RPCMessageHeaderDetail::Direct(detail) => detail, - RPCMessageHeaderDetail::SafetyRouted(_) | RPCMessageHeaderDetail::PrivateRouted(_) => { - return Ok(NetworkResult::invalid_message( - "node_info_update must be direct", - )); - } - }; - let sender_node_id = detail.envelope.get_sender_id(); - let routing_domain = detail.routing_domain; - - // Get the statement - let node_info_update = match msg.operation.into_kind() { - RPCOperationKind::Statement(s) => match s.into_detail() { - RPCStatementDetail::NodeInfoUpdate(s) => s, - _ => panic!("not a node info update"), - }, - _ => panic!("not a statement"), - }; - - // Update our routing table with signed node info - if !self.filter_node_info(routing_domain, &node_info_update.signed_node_info) { - return Ok(NetworkResult::invalid_message(format!( - "node info doesn't belong in {:?} routing domain: {}", - routing_domain, sender_node_id - ))); - } - - if self - .routing_table() - .register_node_with_signed_node_info( - routing_domain, - sender_node_id, - node_info_update.signed_node_info, - false, - ) - .is_none() - { - return Ok(NetworkResult::invalid_message(format!( - "could not register node info update {}", - sender_node_id - ))); - } - - Ok(NetworkResult::value(())) - } -} From 855a5a07564f936b2de1d63b621d5976fbb1454a Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 8 Dec 2022 20:30:42 -0500 Subject: [PATCH 43/88] fix connections --- veilid-core/src/routing_table/bucket_entry.rs | 55 +++++++++++++------ veilid-core/src/routing_table/mod.rs | 8 +++ veilid-core/src/routing_table/node_ref.rs | 28 +--------- veilid-core/src/rpc_processor/mod.rs | 2 +- 4 files changed, 49 insertions(+), 44 deletions(-) diff --git a/veilid-core/src/routing_table/bucket_entry.rs b/veilid-core/src/routing_table/bucket_entry.rs index 3224ac3a..51f79498 100644 --- a/veilid-core/src/routing_table/bucket_entry.rs +++ b/veilid-core/src/routing_table/bucket_entry.rs @@ -286,17 +286,12 @@ impl BucketEntryInner { } // Check connections - let connection_manager = rti.network_manager().connection_manager(); let last_connections = self.last_connections( rti, + true, Some(NodeRefFilter::new().with_routing_domain(routing_domain)), ); - for lc in last_connections { - if connection_manager.get_connection(lc.0).is_some() { - return true; - } - } - false + !last_connections.is_empty() } pub fn node_info(&self, routing_domain: RoutingDomain) -> Option<&NodeInfo> { @@ -343,21 +338,21 @@ impl BucketEntryInner { } // Check connections let mut best_routing_domain: Option = None; - let connection_manager = rti.network_manager().connection_manager(); let last_connections = self.last_connections( rti, + true, Some(NodeRefFilter::new().with_routing_domain_set(routing_domain_set)), ); for lc in last_connections { - if connection_manager.get_connection(lc.0).is_some() { - if let Some(rd) = rti.routing_domain_for_address(lc.0.remote_address().address()) { - if let Some(brd) = best_routing_domain { - if rd < brd { - best_routing_domain = Some(rd); - } - } else { + if let Some(rd) = + rti.routing_domain_for_address(lc.0.remote_address().address()) + { + if let Some(brd) = best_routing_domain { + if rd < brd { best_routing_domain = Some(rd); } + } else { + best_routing_domain = Some(rd); } } } @@ -383,12 +378,16 @@ impl BucketEntryInner { self.last_connections.clear(); } - // Gets all the 'last connections' that match a particular filter + // Gets all the 'last connections' that match a particular filter, and their accompanying timestamps of last use pub(super) fn last_connections( &self, rti: &RoutingTableInner, + only_live: bool, filter: Option, ) -> Vec<(ConnectionDescriptor, u64)> { + let connection_manager = + rti.unlocked_inner.network_manager.connection_manager(); + let mut out: Vec<(ConnectionDescriptor, u64)> = self .last_connections .iter() @@ -414,7 +413,29 @@ impl BucketEntryInner { // no filter true }; - if include { + + if !include { + return None; + } + + if !only_live { + return Some(v.clone()); + } + + // Check if the connection is still considered live + let alive = + // Should we check the connection table? + if v.0.protocol_type().is_connection_oriented() { + // Look the connection up in the connection manager and see if it's still there + connection_manager.get_connection(v.0).is_some() + } else { + // If this is not connection oriented, then we check our last seen time + // to see if this mapping has expired (beyond our timeout) + let cur_ts = get_timestamp(); + (v.1 + (CONNECTIONLESS_TIMEOUT_SECS as u64 * 1_000_000u64)) >= cur_ts + }; + + if alive { Some(v.clone()) } else { None diff --git a/veilid-core/src/routing_table/mod.rs b/veilid-core/src/routing_table/mod.rs index 987841b5..b51ef048 100644 --- a/veilid-core/src/routing_table/mod.rs +++ b/veilid-core/src/routing_table/mod.rs @@ -30,9 +30,17 @@ pub use routing_table_inner::*; pub use stats_accounting::*; ////////////////////////////////////////////////////////////////////////// + +/// How frequently we tick the relay management routine pub const RELAY_MANAGEMENT_INTERVAL_SECS: u32 = 1; + +/// How frequently we tick the private route management routine pub const PRIVATE_ROUTE_MANAGEMENT_INTERVAL_SECS: u32 = 1; +// Connectionless protocols like UDP are dependent on a NAT translation timeout +// We should ping them with some frequency and 30 seconds is typical timeout +pub const CONNECTIONLESS_TIMEOUT_SECS: u32 = 29; + pub type LowLevelProtocolPorts = BTreeSet<(LowLevelProtocolType, AddressType, u16)>; pub type ProtocolToPortMapping = BTreeMap<(ProtocolType, AddressType), (LowLevelProtocolType, u16)>; #[derive(Clone, Debug)] diff --git a/veilid-core/src/routing_table/node_ref.rs b/veilid-core/src/routing_table/node_ref.rs index b99ec68f..edb501f0 100644 --- a/veilid-core/src/routing_table/node_ref.rs +++ b/veilid-core/src/routing_table/node_ref.rs @@ -2,10 +2,6 @@ use super::*; use crate::crypto::*; use alloc::fmt; -// Connectionless protocols like UDP are dependent on a NAT translation timeout -// We should ping them with some frequency and 30 seconds is typical timeout -const CONNECTIONLESS_TIMEOUT_SECS: u32 = 29; - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// pub struct NodeRefBaseCommon { @@ -272,28 +268,8 @@ pub trait NodeRefBase: Sized { // Get the last connections and the last time we saw anything with this connection // Filtered first and then sorted by most recent self.operate(|rti, e| { - let last_connections = e.last_connections(rti, self.common().filter.clone()); - - // Do some checks to ensure these are possibly still 'live' - for (last_connection, last_seen) in last_connections { - // Should we check the connection table? - if last_connection.protocol_type().is_connection_oriented() { - // Look the connection up in the connection manager and see if it's still there - let connection_manager = - rti.unlocked_inner.network_manager.connection_manager(); - if connection_manager.get_connection(last_connection).is_some() { - return Some(last_connection); - } - } else { - // If this is not connection oriented, then we check our last seen time - // to see if this mapping has expired (beyond our timeout) - let cur_ts = get_timestamp(); - if (last_seen + (CONNECTIONLESS_TIMEOUT_SECS as u64 * 1_000_000u64)) >= cur_ts { - return Some(last_connection); - } - } - } - None + let last_connections = e.last_connections(rti, true, self.common().filter.clone()); + last_connections.first().map(|x| x.0) }) } diff --git a/veilid-core/src/rpc_processor/mod.rs b/veilid-core/src/rpc_processor/mod.rs index 77d0dda2..f4d052be 100644 --- a/veilid-core/src/rpc_processor/mod.rs +++ b/veilid-core/src/rpc_processor/mod.rs @@ -686,7 +686,7 @@ impl RPCProcessor { /// Get signed node info to package with RPC messages to improve /// routing table caching when it is okay to do so - #[instrument(skip(self), ret, err)] + #[instrument(level = "trace", skip(self), ret, err)] fn get_sender_signed_node_info( &self, dest: &Destination, From 8c96373cfda79f64c713fbeb3c00cc95c6365a56 Mon Sep 17 00:00:00 2001 From: John Smith Date: Fri, 9 Dec 2022 18:59:31 -0500 Subject: [PATCH 44/88] example work --- external/keyring-manager | 2 +- veilid-core/src/network_manager/native/mod.rs | 2 + veilid-core/src/network_manager/wasm/mod.rs | 11 +- .../example/fonts/CascadiaMonoPL.ttf | Bin 0 -> 388244 bytes .../ios/Flutter/AppFrameworkInfo.plist | 2 +- veilid-flutter/example/ios/Podfile | 2 +- veilid-flutter/example/ios/Podfile.lock | 4 +- .../ios/Runner.xcodeproj/project.pbxproj | 6 +- veilid-flutter/example/lib/home.dart | 102 ++++++++ veilid-flutter/example/lib/main.dart | 69 +++++- veilid-flutter/example/lib/platform_menu.dart | 149 +++++++++++ veilid-flutter/example/lib/veilid_color.dart | 234 ++++++++++++++++++ .../example/lib/virtual_keyboard.dart | 80 ++++++ .../flutter/generated_plugin_registrant.cc | 4 + .../linux/flutter/generated_plugins.cmake | 2 + .../Flutter/GeneratedPluginRegistrant.swift | 2 + veilid-flutter/example/pubspec.lock | 58 ++++- veilid-flutter/example/pubspec.yaml | 9 +- .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 2 + 20 files changed, 713 insertions(+), 30 deletions(-) create mode 100644 veilid-flutter/example/fonts/CascadiaMonoPL.ttf create mode 100644 veilid-flutter/example/lib/home.dart create mode 100644 veilid-flutter/example/lib/platform_menu.dart create mode 100644 veilid-flutter/example/lib/veilid_color.dart create mode 100644 veilid-flutter/example/lib/virtual_keyboard.dart diff --git a/external/keyring-manager b/external/keyring-manager index b127b2d3..c153eb30 160000 --- a/external/keyring-manager +++ b/external/keyring-manager @@ -1 +1 @@ -Subproject commit b127b2d3c653fea163a776dd58b3798f28aeeee3 +Subproject commit c153eb3015d6d118e5d467865510d053ddd84533 diff --git a/veilid-core/src/network_manager/native/mod.rs b/veilid-core/src/network_manager/native/mod.rs index 0ea3563b..de848990 100644 --- a/veilid-core/src/network_manager/native/mod.rs +++ b/veilid-core/src/network_manager/native/mod.rs @@ -832,10 +832,12 @@ impl Network { let mut editor = routing_table.edit_routing_domain(RoutingDomain::PublicInternet); editor.clear_dial_info_details(); + editor.set_network_class(None); editor.commit().await; let mut editor = routing_table.edit_routing_domain(RoutingDomain::LocalNetwork); editor.clear_dial_info_details(); + editor.set_network_class(None); editor.commit().await; // Reset state including network class diff --git a/veilid-core/src/network_manager/wasm/mod.rs b/veilid-core/src/network_manager/wasm/mod.rs index b71aee14..8bdc2ec6 100644 --- a/veilid-core/src/network_manager/wasm/mod.rs +++ b/veilid-core/src/network_manager/wasm/mod.rs @@ -290,6 +290,7 @@ impl Network { protocol_config.inbound, protocol_config.family_global, ); + editor_public_internet.set_network_class(Some(NetworkClass::WebApp)); // commit routing table edits editor_public_internet.commit().await; @@ -319,6 +320,7 @@ impl Network { // Drop all dial info let mut editor = routing_table.edit_routing_domain(RoutingDomain::PublicInternet); editor.clear_dial_info_details(); + editor.set_network_class(None); editor.commit().await; // Cancels all async background tasks by dropping join handles @@ -348,15 +350,6 @@ impl Network { false } - pub fn get_network_class(&self, _routing_domain: RoutingDomain) -> Option { - // xxx eventually detect tor browser? - return if self.inner.lock().network_started { - Some(NetworkClass::WebApp) - } else { - None - }; - } - pub fn get_protocol_config(&self) -> ProtocolConfig { self.inner.lock().protocol_config.clone() } diff --git a/veilid-flutter/example/fonts/CascadiaMonoPL.ttf b/veilid-flutter/example/fonts/CascadiaMonoPL.ttf new file mode 100644 index 0000000000000000000000000000000000000000..801448e7882669657550135a1a60baf2fc8005a9 GIT binary patch literal 388244 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 z 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 G`Tqd5(_m5n literal 0 HcmV?d00001 diff --git a/veilid-flutter/example/ios/Flutter/AppFrameworkInfo.plist b/veilid-flutter/example/ios/Flutter/AppFrameworkInfo.plist index 8d4492f9..9625e105 100644 --- a/veilid-flutter/example/ios/Flutter/AppFrameworkInfo.plist +++ b/veilid-flutter/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/veilid-flutter/example/ios/Podfile b/veilid-flutter/example/ios/Podfile index 1e8c3c90..88359b22 100644 --- a/veilid-flutter/example/ios/Podfile +++ b/veilid-flutter/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/veilid-flutter/example/ios/Podfile.lock b/veilid-flutter/example/ios/Podfile.lock index b736a024..e4487a88 100644 --- a/veilid-flutter/example/ios/Podfile.lock +++ b/veilid-flutter/example/ios/Podfile.lock @@ -21,8 +21,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 - veilid: 41ea3fb86dbe06d0ff436d5002234dac45ccf1ea + veilid: f5c2e662f91907b30cf95762619526ac3e4512fd -PODFILE CHECKSUM: 7368163408c647b7eb699d0d788ba6718e18fb8d +PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 COCOAPODS: 1.11.3 diff --git a/veilid-flutter/example/ios/Runner.xcodeproj/project.pbxproj b/veilid-flutter/example/ios/Runner.xcodeproj/project.pbxproj index 94d69de5..5a156bd7 100644 --- a/veilid-flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/veilid-flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -342,7 +342,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -420,7 +420,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -470,7 +470,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/veilid-flutter/example/lib/home.dart b/veilid-flutter/example/lib/home.dart new file mode 100644 index 00000000..e2cfc248 --- /dev/null +++ b/veilid-flutter/example/lib/home.dart @@ -0,0 +1,102 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_pty/flutter_pty.dart'; +import 'package:xterm/xterm.dart'; + +class Home extends StatefulWidget { + Home({Key? key}) : super(key: key); + + @override + // ignore: library_private_types_in_public_api + _HomeState createState() => _HomeState(); +} + +class _HomeState extends State { + final terminal = Terminal( + maxLines: 10000, + ); + + final terminalController = TerminalController(); + + late final Pty pty; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.endOfFrame.then( + (_) { + if (mounted) _startPty(); + }, + ); + } + + void _startPty() { + pty = Pty.start( + shell, + columns: terminal.viewWidth, + rows: terminal.viewHeight, + ); + + pty.output + .cast>() + .transform(Utf8Decoder()) + .listen(terminal.write); + + pty.exitCode.then((code) { + terminal.write('the process exited with exit code $code'); + }); + + terminal.onOutput = (data) { + pty.write(const Utf8Encoder().convert(data)); + }; + + terminal.onResize = (w, h, pw, ph) { + pty.resize(h, w); + }; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: SafeArea( + child: TerminalView( + terminal, + controller: terminalController, + autofocus: true, + backgroundOpacity: 0.7, + onSecondaryTapDown: (details, offset) async { + final selection = terminalController.selection; + if (selection != null) { + final text = terminal.buffer.getText(selection); + terminalController.clearSelection(); + await Clipboard.setData(ClipboardData(text: text)); + } else { + final data = await Clipboard.getData('text/plain'); + final text = data?.text; + if (text != null) { + terminal.paste(text); + } + } + }, + ), + ), + ); + } +} + +String get shell { + if (Platform.isMacOS || Platform.isLinux) { + return Platform.environment['SHELL'] ?? 'bash'; + } + + if (Platform.isWindows) { + return 'cmd.exe'; + } + + return 'sh'; +} diff --git a/veilid-flutter/example/lib/main.dart b/veilid-flutter/example/lib/main.dart index 568b67e5..2ea8cb67 100644 --- a/veilid-flutter/example/lib/main.dart +++ b/veilid-flutter/example/lib/main.dart @@ -1,12 +1,20 @@ import 'dart:async'; import 'dart:typed_data'; import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/foundation.dart'; import 'package:veilid/veilid.dart'; -import 'package:flutter_loggy/flutter_loggy.dart'; +//import 'package:flutter_loggy/flutter_loggy.dart'; import 'package:loggy/loggy.dart'; +import 'platform_menu.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_acrylic/flutter_acrylic.dart'; +import 'package:xterm/xterm.dart'; +import 'home.dart'; import 'config.dart'; @@ -62,15 +70,33 @@ void setRootLogLevel(LogLevel? level) { } void initLoggy() { - Loggy.initLoggy( - logPrinter: StreamPrinter(ConsolePrinter( - const PrettyDeveloperPrinter(), - )), - logOptions: getLogOptions(null), - ); + // Loggy.initLoggy( + // logPrinter: StreamPrinter(ConsolePrinter( + // const PrettyDeveloperPrinter(), + // )), + // logOptions: getLogOptions(null), + // ); } -// Entrypoint +/////////////////////////////// Acrylic + +bool get isDesktop { + if (kIsWeb) return false; + return [ + TargetPlatform.windows, + TargetPlatform.linux, + TargetPlatform.macOS, + ].contains(defaultTargetPlatform); +} + +Future setupAcrylic() async { + await Window.initialize(); + await Window.makeTitlebarTransparent(); + await Window.setEffect(effect: WindowEffect.aero, color: Color(0xFFFFFFFF)); + await Window.setBlurViewState(MacOSBlurViewState.active); +} + +/////////////////////////////// Entrypoint void main() { WidgetsFlutterBinding.ensureInitialized(); @@ -106,7 +132,7 @@ void main() { runApp(MaterialApp( title: 'Veilid Plugin Demo', theme: ThemeData( - primarySwatch: Colors.blue, + primarySwatch: '#6667AB', visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const MyApp())); @@ -220,7 +246,28 @@ class _MyAppState extends State with UiLoggy { child: Container( color: ThemeData.dark().scaffoldBackgroundColor, height: MediaQuery.of(context).size.height * 0.4, - child: LoggyStreamWidget(logLevel: loggy.level.logLevel), + child: SafeArea( + child: TerminalView( + terminal, + controller: terminalController, + autofocus: true, + backgroundOpacity: 0.7, + onSecondaryTapDown: (details, offset) async { + final selection = terminalController.selection; + if (selection != null) { + final text = terminal.buffer.getText(selection); + terminalController.clearSelection(); + await Clipboard.setData(ClipboardData(text: text)); + } else { + final data = await Clipboard.getData('text/plain'); + final text = data?.text; + if (text != null) { + terminal.paste(text); + } + } + }, + ), + ), )), Container( padding: const EdgeInsets.fromLTRB(8, 8, 8, 12), diff --git a/veilid-flutter/example/lib/platform_menu.dart b/veilid-flutter/example/lib/platform_menu.dart new file mode 100644 index 00000000..db55916a --- /dev/null +++ b/veilid-flutter/example/lib/platform_menu.dart @@ -0,0 +1,149 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +class AppPlatformMenu extends StatefulWidget { + const AppPlatformMenu({super.key, required this.child}); + + final Widget child; + + @override + State createState() => _AppPlatformMenuState(); +} + +class _AppPlatformMenuState extends State { + @override + Widget build(BuildContext context) { + if (defaultTargetPlatform != TargetPlatform.macOS) { + return widget.child; + } + + return PlatformMenuBar( + menus: [ + PlatformMenu( + label: 'TerminalStudio', + menus: [ + if (PlatformProvidedMenuItem.hasMenu( + PlatformProvidedMenuItemType.about, + )) + const PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.about, + ), + PlatformMenuItemGroup( + members: [ + if (PlatformProvidedMenuItem.hasMenu( + PlatformProvidedMenuItemType.servicesSubmenu, + )) + const PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.servicesSubmenu, + ), + ], + ), + PlatformMenuItemGroup( + members: [ + if (PlatformProvidedMenuItem.hasMenu( + PlatformProvidedMenuItemType.hide, + )) + const PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.hide, + ), + if (PlatformProvidedMenuItem.hasMenu( + PlatformProvidedMenuItemType.hideOtherApplications, + )) + const PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.hideOtherApplications, + ), + ], + ), + if (PlatformProvidedMenuItem.hasMenu( + PlatformProvidedMenuItemType.quit, + )) + const PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.quit, + ), + ], + ), + PlatformMenu( + label: 'Edit', + menus: [ + PlatformMenuItemGroup( + members: [ + PlatformMenuItem( + label: 'Copy', + shortcut: const SingleActivator( + LogicalKeyboardKey.keyC, + meta: true, + ), + onSelected: () { + final primaryContext = primaryFocus?.context; + if (primaryContext == null) { + return; + } + Actions.invoke( + primaryContext, + CopySelectionTextIntent.copy, + ); + }, + ), + PlatformMenuItem( + label: 'Paste', + shortcut: const SingleActivator( + LogicalKeyboardKey.keyV, + meta: true, + ), + onSelected: () { + final primaryContext = primaryFocus?.context; + if (primaryContext == null) { + return; + } + Actions.invoke( + primaryContext, + const PasteTextIntent(SelectionChangedCause.keyboard), + ); + }, + ), + PlatformMenuItem( + label: 'Select All', + shortcut: const SingleActivator( + LogicalKeyboardKey.keyA, + meta: true, + ), + onSelected: () { + final primaryContext = primaryFocus?.context; + if (primaryContext == null) { + return; + } + print(primaryContext); + try { + final action = Actions.maybeFind( + primaryContext, + intent: const SelectAllTextIntent( + SelectionChangedCause.keyboard, + ), + ); + print('action: $action'); + } catch (e) { + print(e); + } + Actions.invoke( + primaryContext, + const SelectAllTextIntent(SelectionChangedCause.keyboard), + ); + }, + ), + ], + ), + if (PlatformProvidedMenuItem.hasMenu( + PlatformProvidedMenuItemType.quit, + )) + const PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.quit, + ), + ], + ), + ], + child: widget.child, + ); + } +} diff --git a/veilid-flutter/example/lib/veilid_color.dart b/veilid-flutter/example/lib/veilid_color.dart new file mode 100644 index 00000000..808fef7b --- /dev/null +++ b/veilid-flutter/example/lib/veilid_color.dart @@ -0,0 +1,234 @@ +// Veilid Colors +// ------------- +// +// Base is origin color as annotated +// +// Shades from material color tool at: +// https://m2.material.io/design/color/the-color-system.html#tools-for-picking-colors +// + +import 'package:flutter/material.dart'; + +const Map primaryColorSwatch = { + 50: Color(0xffe9e9f3), + 100: Color(0xffc7c8e2), + 200: Color(0xffa2a5ce), + 300: Color(0xff7f82ba), + 400: Color(0xff6667ab), // Base (6667ab) + 500: Color(0xff4f4d9d), + 600: Color(0xff484594), + 700: Color(0xff403b88), + 800: Color(0xff39327c), + 900: Color(0xff2b2068), +}; + +const MaterialColor materialPrimaryColor = + MaterialColor(0xff6667ab, primaryColorSwatch); + +const Map primaryComplementaryColorSwatch = { + 50: Color(0xfffafdee), + 100: Color(0xfff4f9d3), + 200: Color(0xffedf6b8), + 300: Color(0xffe7f29f), + 400: Color(0xffe2ed8d), + 500: Color(0xffdde97d), + 600: Color(0xffd0d776), + 700: Color(0xffbdc16d), + 800: Color(0xffabaa66), // Base (#abaa66) + 900: Color(0xff8b845c), +}; + +const MaterialColor materialPrimaryComplementaryColor = + MaterialColor(0xffabaa66, primaryComplementaryColorSwatch); + +const Map primaryTriadicColorASwatch = { + 50: Color(0xfff0e4f0), + 100: Color(0xffdabcdb), + 200: Color(0xffc290c3), + 300: Color(0xffaa66ab), // Base (#aa66ab) + 400: Color(0xff98489a), + 500: Color(0xff892a8c), + 600: Color(0xff7d2786), + 700: Color(0xff6d217e), + 800: Color(0xff5e1b76), + 900: Color(0xff441168), +}; + +const MaterialColor materialPrimaryTriadicColorA = + MaterialColor(0xffaa66ab, primaryTriadicColorASwatch); + +const Map primaryTriadicColorBSwatch = { + 50: Color(0xffffe3dc), + 100: Color(0xfff7c4c2), + 200: Color(0xffdba2a2), + 300: Color(0xffc08180), + 400: Color(0xffab6667), // Base (#ab6667) + 500: Color(0xff964c4f), + 600: Color(0xff894347), + 700: Color(0xff78373d), + 800: Color(0xff672b35), + 900: Color(0xff551e2a), +}; + +const MaterialColor materialPrimaryTriadicColorB = + MaterialColor(0xffab6667, primaryTriadicColorBSwatch); + +const Map secondaryColorSwatch = { + 50: Color(0xffe3e8f7), + 100: Color(0xffb8c6eb), + 200: Color(0xff87a1dd), // Base (#87a1dd) + 300: Color(0xff527dce), + 400: Color(0xff1a61c1), + 500: Color(0xff0048b5), + 600: Color(0xff0040ab), + 700: Color(0xff0037a0), + 800: Color(0xff002e94), + 900: Color(0xff001d7f), +}; + +const MaterialColor materialSecondaryColor = + MaterialColor(0xff87a1dd, secondaryColorSwatch); + +const Map secondaryComplementaryColorSwatch = { + 50: Color(0xfff6f1e2), + 100: Color(0xffeadbb6), + 200: Color(0xffddc387), // Base (#ddc387) + 300: Color(0xffd2ac55), + 400: Color(0xffcd9c2d), + 500: Color(0xffc88c05), + 600: Color(0xffc58200), + 700: Color(0xffbf7400), + 800: Color(0xffb96700), + 900: Color(0xffb15000), +}; + +const MaterialColor materialSecondaryComplementaryColor = + MaterialColor(0xffddc387, secondaryComplementaryColorSwatch); + +const Map backgroundColorSwatch = { + 50: Color(0xffe3e5eb), + 100: Color(0xffb9bdce), + 200: Color(0xff8c93ac), + 300: Color(0xff626a8c), + 400: Color(0xff454d76), + 500: Color(0xff273263), + 600: Color(0xff222c5b), + 700: Color(0xff1a2451), + 800: Color(0xff131c45), + 900: Color(0xff0b0b2f), // Base (#0b0b2f) +}; + +const MaterialColor materialBackgroundColor = + MaterialColor(0xff0b0b2f, backgroundColorSwatch); + +const Map backgroundComplementaryColorSwatch = { + 50: Color(0xfffffed2), + 100: Color(0xfffdf9cd), + 200: Color(0xfff8f5c8), + 300: Color(0xfff3efc3), + 400: Color(0xffd1cea3), + 500: Color(0xffb4b187), + 600: Color(0xff89865e), + 700: Color(0xff73714a), + 800: Color(0xff53512c), + 900: Color(0xff2f2f0b), // Base (#2f2f0b) +}; + +const MaterialColor materialBackgroundComplementaryColor = + MaterialColor(0xff2f2f0b, backgroundComplementaryColorSwatch); + +const Map desaturatedColorSwatch = { + 50: Color(0xfff7fbff), + 100: Color(0xfff2f6ff), + 200: Color(0xffedf1fd), + 300: Color(0xffe3e7f2), + 400: Color(0xffc1c5d0), // Base (#c1c5d0) + 500: Color(0xffa3a7b2), + 600: Color(0xff797d87), + 700: Color(0xff656973), + 800: Color(0xff464952), + 900: Color(0xff242830), +}; + +const MaterialColor materialDesaturatedColor = + MaterialColor(0xffc1c5d0, desaturatedColorSwatch); + +const Map desaturatedComplementaryColorSwatch = { + 50: Color(0xffecebe5), + 100: Color(0xffd0ccc1), // Base (#d0ccc1) + 200: Color(0xffb0aa9a), + 300: Color(0xff908972), + 400: Color(0xff796f54), + 500: Color(0xff615837), + 600: Color(0xff584e31), + 700: Color(0xff4a4128), + 800: Color(0xff3e341f), + 900: Color(0xff312715), +}; + +const MaterialColor materialDesaturatedComplementaryColor = + MaterialColor(0xffd0ccc1, desaturatedComplementaryColorSwatch); + +const Map auxiliaryColorSwatch = { + 50: Color(0xffe7e4da), // Base (#e7e4da) + 100: Color(0xffc2bbac), + 200: Color(0xff988e7b), + 300: Color(0xff6f634c), + 400: Color(0xff53472b), + 500: Color(0xff372c0a), + 600: Color(0xff302403), + 700: Color(0xff261a00), + 800: Color(0xff1e0c00), + 900: Color(0xff160000), +}; + +const MaterialColor materialAuxiliaryColor = + MaterialColor(0xffe7e4da, auxiliaryColorSwatch); + +const Map auxiliaryComplementaryColorSwatch = { + 50: Color(0xffdadde7), // Base (#dadde7) + 100: Color(0xffa2abc6), + 200: Color(0xff6575a2), + 300: Color(0xff224580), + 400: Color(0xff00266c), + 500: Color(0xff000357), + 600: Color(0xff000051), + 700: Color(0xff000051), + 800: Color(0xff000050), + 900: Color(0xff00004f), +}; + +const MaterialColor materialAuxiliaryComplementaryColor = + MaterialColor(0xffdadde7, auxiliaryComplementaryColorSwatch); + +const Map popColorSwatch = { + 50: Color(0xfffee5f5), + 100: Color(0xfffbbde7), + 200: Color(0xfff88fd9), + 300: Color(0xfff259c9), // Base (#f259c9) + 400: Color(0xffec15bd), + 500: Color(0xffe100b0), + 600: Color(0xffd200ac), + 700: Color(0xffbe00a7), + 800: Color(0xffad00a1), + 900: Color(0xff8e0097), +}; + +const MaterialColor materialPopColor = + MaterialColor(0xfff259c9, popColorSwatch); + +const Map popComplentaryColorSwatch = { + 50: Color(0xffe6fdea), + 100: Color(0xffc2f9cb), + 200: Color(0xff96f6a9), + 300: Color(0xff59f282), // Base (#59f282) + 400: Color(0xff00ec60), + 500: Color(0xff00e446), + 600: Color(0xff00d33b), + 700: Color(0xff00bf2d), + 800: Color(0xff00ad21), + 900: Color(0xff008b05), +}; + +const MaterialColor materialPopComplementaryColor = + MaterialColor(0xff59f282, popComplentaryColorSwatch); diff --git a/veilid-flutter/example/lib/virtual_keyboard.dart b/veilid-flutter/example/lib/virtual_keyboard.dart new file mode 100644 index 00000000..b59bb847 --- /dev/null +++ b/veilid-flutter/example/lib/virtual_keyboard.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:xterm/xterm.dart'; + +class VirtualKeyboardView extends StatelessWidget { + const VirtualKeyboardView(this.keyboard, {super.key}); + + final VirtualKeyboard keyboard; + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: keyboard, + builder: (context, child) => ToggleButtons( + children: [Text('Ctrl'), Text('Alt'), Text('Shift')], + isSelected: [keyboard.ctrl, keyboard.alt, keyboard.shift], + onPressed: (index) { + switch (index) { + case 0: + keyboard.ctrl = !keyboard.ctrl; + break; + case 1: + keyboard.alt = !keyboard.alt; + break; + case 2: + keyboard.shift = !keyboard.shift; + break; + } + }, + ), + ); + } +} + +class VirtualKeyboard extends TerminalInputHandler with ChangeNotifier { + final TerminalInputHandler _inputHandler; + + VirtualKeyboard(this._inputHandler); + + bool _ctrl = false; + + bool get ctrl => _ctrl; + + set ctrl(bool value) { + if (_ctrl != value) { + _ctrl = value; + notifyListeners(); + } + } + + bool _shift = false; + + bool get shift => _shift; + + set shift(bool value) { + if (_shift != value) { + _shift = value; + notifyListeners(); + } + } + + bool _alt = false; + + bool get alt => _alt; + + set alt(bool value) { + if (_alt != value) { + _alt = value; + notifyListeners(); + } + } + + @override + String? call(TerminalKeyboardEvent event) { + return _inputHandler.call(event.copyWith( + ctrl: event.ctrl || _ctrl, + shift: event.shift || _shift, + alt: event.alt || _alt, + )); + } +} diff --git a/veilid-flutter/example/linux/flutter/generated_plugin_registrant.cc b/veilid-flutter/example/linux/flutter/generated_plugin_registrant.cc index cebc32de..3a10c1bf 100644 --- a/veilid-flutter/example/linux/flutter/generated_plugin_registrant.cc +++ b/veilid-flutter/example/linux/flutter/generated_plugin_registrant.cc @@ -6,9 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_acrylic_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterAcrylicPlugin"); + flutter_acrylic_plugin_register_with_registrar(flutter_acrylic_registrar); g_autoptr(FlPluginRegistrar) veilid_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "VeilidPlugin"); veilid_plugin_register_with_registrar(veilid_registrar); diff --git a/veilid-flutter/example/linux/flutter/generated_plugins.cmake b/veilid-flutter/example/linux/flutter/generated_plugins.cmake index 003d7b50..ec1406bc 100644 --- a/veilid-flutter/example/linux/flutter/generated_plugins.cmake +++ b/veilid-flutter/example/linux/flutter/generated_plugins.cmake @@ -3,10 +3,12 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_acrylic veilid ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_pty ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/veilid-flutter/example/macos/Flutter/GeneratedPluginRegistrant.swift b/veilid-flutter/example/macos/Flutter/GeneratedPluginRegistrant.swift index 5c63c74b..298c79e7 100644 --- a/veilid-flutter/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/veilid-flutter/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,12 @@ import FlutterMacOS import Foundation +import flutter_acrylic import path_provider_macos import veilid func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterAcrylicPlugin.register(with: registry.registrar(forPlugin: "FlutterAcrylicPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) VeilidPlugin.register(with: registry.registrar(forPlugin: "VeilidPlugin")) } diff --git a/veilid-flutter/example/pubspec.lock b/veilid-flutter/example/pubspec.lock index 52c42eef..f59dfc44 100644 --- a/veilid-flutter/example/pubspec.lock +++ b/veilid-flutter/example/pubspec.lock @@ -43,6 +43,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.16.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" cupertino_icons: dependency: "direct main" description: @@ -50,6 +57,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.5" + equatable: + dependency: transitive + description: + name: equatable + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -76,6 +90,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_acrylic: + dependency: "direct main" + description: + name: flutter_acrylic + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0+2" flutter_lints: dependency: "direct dev" description: @@ -90,6 +111,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.2" + flutter_pty: + dependency: "direct main" + description: + name: flutter_pty + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.1" flutter_test: dependency: "direct dev" description: flutter @@ -205,6 +233,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.0" + platform_info: + dependency: transitive + description: + name: platform_info + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" plugin_platform_interface: dependency: transitive description: @@ -219,6 +254,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.2.4" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" rxdart: dependency: transitive description: @@ -273,6 +315,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.12" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" vector_math: dependency: transitive description: @@ -301,6 +350,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.0+2" + xterm: + dependency: "direct main" + description: + name: xterm + url: "https://pub.dartlang.org" + source: hosted + version: "3.4.0" sdks: - dart: ">=2.17.0 <3.0.0" + dart: ">=2.18.0 <3.0.0" flutter: ">=3.0.0" diff --git a/veilid-flutter/example/pubspec.yaml b/veilid-flutter/example/pubspec.yaml index 73ee637f..e603fd76 100644 --- a/veilid-flutter/example/pubspec.yaml +++ b/veilid-flutter/example/pubspec.yaml @@ -7,7 +7,7 @@ version: 1.0.0+1 publish_to: "none" # Remove this line if you wish to publish to pub.dev environment: - sdk: ">=2.16.1 <3.0.0" + sdk: ">=2.17.0 <3.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -36,6 +36,9 @@ dependencies: flutter_loggy: ^2.0.1 path_provider: ^2.0.11 path: ^1.8.1 + xterm: ^3.4.0 + flutter_pty: ^0.3.1 + flutter_acrylic: ^1.0.0+2 dev_dependencies: flutter_test: @@ -88,3 +91,7 @@ flutter: # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages + fonts: + - family: Cascadia Mono + fonts: + - asset: fonts/CascadiaMonoPL.ttf diff --git a/veilid-flutter/example/windows/flutter/generated_plugin_registrant.cc b/veilid-flutter/example/windows/flutter/generated_plugin_registrant.cc index 72dbdef2..df015a29 100644 --- a/veilid-flutter/example/windows/flutter/generated_plugin_registrant.cc +++ b/veilid-flutter/example/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,12 @@ #include "generated_plugin_registrant.h" +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterAcrylicPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterAcrylicPlugin")); VeilidPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("VeilidPlugin")); } diff --git a/veilid-flutter/example/windows/flutter/generated_plugins.cmake b/veilid-flutter/example/windows/flutter/generated_plugins.cmake index 658ec856..4ba079ee 100644 --- a/veilid-flutter/example/windows/flutter/generated_plugins.cmake +++ b/veilid-flutter/example/windows/flutter/generated_plugins.cmake @@ -3,10 +3,12 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_acrylic veilid ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_pty ) set(PLUGIN_BUNDLED_LIBRARIES) From a44794ab98943df37c7ad74a90a7e92a5725ab7f Mon Sep 17 00:00:00 2001 From: John Smith Date: Sat, 10 Dec 2022 12:11:46 -0500 Subject: [PATCH 45/88] flutter work --- veilid-flutter/example/lib/app.dart | 186 ++++++++++ veilid-flutter/example/lib/log.dart | 115 +++++++ .../lib/{home.dart => log_terminal.dart} | 58 +--- veilid-flutter/example/lib/main.dart | 319 +----------------- veilid-flutter/example/lib/veilid_color.dart | 8 + veilid-flutter/example/lib/veilid_init.dart | 34 ++ .../example/lib/virtual_keyboard.dart | 2 +- veilid-flutter/example/macos/Podfile.lock | 12 + veilid-flutter/example/pubspec.lock | 7 + veilid-flutter/example/pubspec.yaml | 1 + veilid-flutter/example/test/widget_test.dart | 6 +- 11 files changed, 387 insertions(+), 361 deletions(-) create mode 100644 veilid-flutter/example/lib/app.dart create mode 100644 veilid-flutter/example/lib/log.dart rename veilid-flutter/example/lib/{home.dart => log_terminal.dart} (55%) create mode 100644 veilid-flutter/example/lib/veilid_init.dart diff --git a/veilid-flutter/example/lib/app.dart b/veilid-flutter/example/lib/app.dart new file mode 100644 index 00000000..84e03d8f --- /dev/null +++ b/veilid-flutter/example/lib/app.dart @@ -0,0 +1,186 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:veilid/veilid.dart'; +import 'package:loggy/loggy.dart'; + +import 'log_terminal.dart'; +import 'config.dart'; +import 'log.dart'; + +// Main App +class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State with UiLoggy { + String _veilidVersion = 'Unknown'; + Stream? _updateStream; + Future? _updateProcessor; + + @override + void initState() { + super.initState(); + + initPlatformState(); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future initPlatformState() async { + String veilidVersion; + // Platform messages may fail, so we use a try/catch PlatformException. + // We also handle the message potentially returning null. + try { + veilidVersion = Veilid.instance.veilidVersionString(); + } on Exception { + veilidVersion = 'Failed to get veilid version.'; + } + + // In case of hot restart shut down first + try { + await Veilid.instance.shutdownVeilidCore(); + } on Exception { + // + } + + // If the widget was removed from the tree while the asynchronous platform + // message was in flight, we want to discard the reply rather than calling + // setState to update our non-existent appearance. + if (!mounted) return; + + setState(() { + _veilidVersion = veilidVersion; + }); + } + + Future processLog(VeilidLog log) async { + StackTrace? stackTrace; + Object? error; + final backtrace = log.backtrace; + if (backtrace != null) { + stackTrace = + StackTrace.fromString("$backtrace\n${StackTrace.current.toString()}"); + error = 'embedded stack trace for ${log.logLevel} ${log.message}'; + } + + switch (log.logLevel) { + case VeilidLogLevel.error: + loggy.error(log.message, error, stackTrace); + break; + case VeilidLogLevel.warn: + loggy.warning(log.message, error, stackTrace); + break; + case VeilidLogLevel.info: + loggy.info(log.message, error, stackTrace); + break; + case VeilidLogLevel.debug: + loggy.debug(log.message, error, stackTrace); + break; + case VeilidLogLevel.trace: + loggy.trace(log.message, error, stackTrace); + break; + } + } + + Future processUpdates() async { + var stream = _updateStream; + if (stream != null) { + await for (final update in stream) { + if (update is VeilidLog) { + await processLog(update); + } else if (update is VeilidAppMessage) { + loggy.info("AppMessage: ${update.json}"); + } else if (update is VeilidAppCall) { + loggy.info("AppCall: ${update.json}"); + } else { + loggy.trace("Update: ${update.json}"); + } + } + } + } + + @override + Widget build(BuildContext context) { + final ButtonStyle buttonStyle = + ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)); + + return Scaffold( + appBar: AppBar( + title: Text('Veilid Plugin Version $_veilidVersion'), + ), + body: Column(children: [ + const Expanded(child: LogTerminal()), + Container( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 12), + child: Row(children: [ + ElevatedButton( + style: buttonStyle, + onPressed: _updateStream != null + ? null + : () async { + var updateStream = await Veilid.instance + .startupVeilidCore( + await getDefaultVeilidConfig()); + setState(() { + _updateStream = updateStream; + _updateProcessor = processUpdates(); + }); + await Veilid.instance.attach(); + }, + child: const Text('Startup'), + ), + ElevatedButton( + style: buttonStyle, + onPressed: _updateStream == null + ? null + : () async { + await Veilid.instance.shutdownVeilidCore(); + if (_updateProcessor != null) { + await _updateProcessor; + } + setState(() { + _updateProcessor = null; + _updateStream = null; + }); + }, + child: const Text('Shutdown'), + ), + ])), + Row(children: [ + Expanded( + child: TextField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Debug Command'), + textInputAction: TextInputAction.send, + onSubmitted: (String v) async { + loggy.info(await Veilid.instance.debug(v)); + })), + DropdownButton( + value: loggy.level.logLevel, + onChanged: (LogLevel? newLevel) { + setState(() { + setRootLogLevel(newLevel); + }); + }, + items: const [ + DropdownMenuItem( + value: LogLevel.error, child: Text("Error")), + DropdownMenuItem( + value: LogLevel.warning, child: Text("Warning")), + DropdownMenuItem( + value: LogLevel.info, child: Text("Info")), + DropdownMenuItem( + value: LogLevel.debug, child: Text("Debug")), + DropdownMenuItem( + value: traceLevel, child: Text("Trace")), + DropdownMenuItem( + value: LogLevel.all, child: Text("All")), + ]) + ]), + ])); + } +} diff --git a/veilid-flutter/example/lib/log.dart b/veilid-flutter/example/lib/log.dart new file mode 100644 index 00000000..fd305c9a --- /dev/null +++ b/veilid-flutter/example/lib/log.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:veilid/veilid.dart'; +import 'package:loggy/loggy.dart'; +import 'package:ansicolor/ansicolor.dart'; + +// Loggy tools +const LogLevel traceLevel = LogLevel('Trace', 1); + +VeilidConfigLogLevel convertToVeilidConfigLogLevel(LogLevel? level) { + if (level == null) { + return VeilidConfigLogLevel.off; + } + switch (level) { + case LogLevel.error: + return VeilidConfigLogLevel.error; + case LogLevel.warning: + return VeilidConfigLogLevel.warn; + case LogLevel.info: + return VeilidConfigLogLevel.info; + case LogLevel.debug: + return VeilidConfigLogLevel.debug; + case traceLevel: + return VeilidConfigLogLevel.trace; + } + return VeilidConfigLogLevel.off; +} + +String wrapWithLogColor(LogLevel? level, String text) { + if (level == null) { + return text; + } + final pen = AnsiPen(); + ansiColorDisabled = false; + switch (level) { + case LogLevel.error: + pen + ..reset() + ..red(bold: true); + return pen(text); + case LogLevel.warning: + pen + ..reset() + ..yellow(bold: true); + return pen(text); + case LogLevel.info: + pen + ..reset() + ..white(bold: true); + return pen(text); + case LogLevel.debug: + pen + ..reset() + ..green(bold: true); + return pen(text); + case traceLevel: + pen + ..reset() + ..blue(bold: true); + return pen(text); + } + return text; +} + +void setRootLogLevel(LogLevel? level) { + Loggy('').level = getLogOptions(level); + Veilid.instance.changeLogLevel("all", convertToVeilidConfigLogLevel(level)); +} + +extension PrettyPrintLogRecord on LogRecord { + String pretty() { + final lstr = + wrapWithLogColor(level, '[${level.toString().substring(0, 1)}]'); + return '$lstr $message'; + } +} + +class CallbackPrinter extends LoggyPrinter { + CallbackPrinter() : super(); + + void Function(LogRecord)? callback; + + @override + void onLog(LogRecord record) { + debugPrint(record.pretty()); + callback?.call(record); + } + + void setCallback(Function(LogRecord)? cb) { + callback = cb; + } +} + +var globalTerminalPrinter = CallbackPrinter(); + +extension TraceLoggy on Loggy { + void trace(dynamic message, [Object? error, StackTrace? stackTrace]) => + log(traceLevel, message, error, stackTrace); +} + +LogOptions getLogOptions(LogLevel? level) { + return LogOptions( + level ?? LogLevel.all, + stackTraceLevel: LogLevel.error, + ); +} + +void initLoggy() { + Loggy.initLoggy( + logPrinter: globalTerminalPrinter, + logOptions: getLogOptions(null), + ); + + setRootLogLevel(LogLevel.info); +} diff --git a/veilid-flutter/example/lib/home.dart b/veilid-flutter/example/lib/log_terminal.dart similarity index 55% rename from veilid-flutter/example/lib/home.dart rename to veilid-flutter/example/lib/log_terminal.dart index e2cfc248..2ba817f3 100644 --- a/veilid-flutter/example/lib/home.dart +++ b/veilid-flutter/example/lib/log_terminal.dart @@ -3,60 +3,30 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_pty/flutter_pty.dart'; import 'package:xterm/xterm.dart'; +import 'log.dart'; -class Home extends StatefulWidget { - Home({Key? key}) : super(key: key); +class LogTerminal extends StatefulWidget { + const LogTerminal({Key? key}) : super(key: key); @override // ignore: library_private_types_in_public_api - _HomeState createState() => _HomeState(); + _LogTerminalState createState() => _LogTerminalState(); } -class _HomeState extends State { +class _LogTerminalState extends State { final terminal = Terminal( maxLines: 10000, ); final terminalController = TerminalController(); - late final Pty pty; - @override void initState() { super.initState(); - - WidgetsBinding.instance.endOfFrame.then( - (_) { - if (mounted) _startPty(); - }, - ); - } - - void _startPty() { - pty = Pty.start( - shell, - columns: terminal.viewWidth, - rows: terminal.viewHeight, - ); - - pty.output - .cast>() - .transform(Utf8Decoder()) - .listen(terminal.write); - - pty.exitCode.then((code) { - terminal.write('the process exited with exit code $code'); - }); - - terminal.onOutput = (data) { - pty.write(const Utf8Encoder().convert(data)); - }; - - terminal.onResize = (w, h, pw, ph) { - pty.resize(h, w); - }; + terminal.setLineFeedMode(true); + globalTerminalPrinter + .setCallback((log) => {terminal.write("${log.pretty()}\n")}); } @override @@ -88,15 +58,3 @@ class _HomeState extends State { ); } } - -String get shell { - if (Platform.isMacOS || Platform.isLinux) { - return Platform.environment['SHELL'] ?? 'bash'; - } - - if (Platform.isWindows) { - return 'cmd.exe'; - } - - return 'sh'; -} diff --git a/veilid-flutter/example/lib/main.dart b/veilid-flutter/example/lib/main.dart index 2ea8cb67..66e8ecdf 100644 --- a/veilid-flutter/example/lib/main.dart +++ b/veilid-flutter/example/lib/main.dart @@ -1,82 +1,14 @@ import 'dart:async'; -import 'dart:typed_data'; -import 'dart:convert'; -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:veilid/veilid.dart'; -//import 'package:flutter_loggy/flutter_loggy.dart'; -import 'package:loggy/loggy.dart'; -import 'platform_menu.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_acrylic/flutter_acrylic.dart'; -import 'package:xterm/xterm.dart'; -import 'home.dart'; -import 'config.dart'; - -// Loggy tools -const LogLevel traceLevel = LogLevel('Trace', 1); - -class ConsolePrinter extends LoggyPrinter { - ConsolePrinter(this.childPrinter) : super(); - - final LoggyPrinter childPrinter; - - @override - void onLog(LogRecord record) { - debugPrint(record.toString()); - childPrinter.onLog(record); - } -} - -extension TraceLoggy on Loggy { - void trace(dynamic message, [Object? error, StackTrace? stackTrace]) => - log(traceLevel, message, error, stackTrace); -} - -LogOptions getLogOptions(LogLevel? level) { - return LogOptions( - level ?? LogLevel.all, - stackTraceLevel: LogLevel.error, - ); -} - -VeilidConfigLogLevel convertToVeilidConfigLogLevel(LogLevel? level) { - if (level == null) { - return VeilidConfigLogLevel.off; - } - switch (level) { - case LogLevel.error: - return VeilidConfigLogLevel.error; - case LogLevel.warning: - return VeilidConfigLogLevel.warn; - case LogLevel.info: - return VeilidConfigLogLevel.info; - case LogLevel.debug: - return VeilidConfigLogLevel.debug; - case traceLevel: - return VeilidConfigLogLevel.trace; - } - return VeilidConfigLogLevel.off; -} - -void setRootLogLevel(LogLevel? level) { - Loggy('').level = getLogOptions(level); - Veilid.instance.changeLogLevel("all", convertToVeilidConfigLogLevel(level)); -} - -void initLoggy() { - // Loggy.initLoggy( - // logPrinter: StreamPrinter(ConsolePrinter( - // const PrettyDeveloperPrinter(), - // )), - // logOptions: getLogOptions(null), - // ); -} +import 'veilid_color.dart'; +import 'log.dart'; +import 'app.dart'; +import 'veilid_init.dart'; /////////////////////////////// Acrylic @@ -92,7 +24,8 @@ bool get isDesktop { Future setupAcrylic() async { await Window.initialize(); await Window.makeTitlebarTransparent(); - await Window.setEffect(effect: WindowEffect.aero, color: Color(0xFFFFFFFF)); + await Window.setEffect( + effect: WindowEffect.aero, color: const Color(0xFFFFFFFF)); await Window.setBlurViewState(MacOSBlurViewState.active); } @@ -100,243 +33,15 @@ Future setupAcrylic() async { void main() { WidgetsFlutterBinding.ensureInitialized(); + // Initialize Log initLoggy(); - if (kIsWeb) { - var platformConfig = VeilidWASMConfig( - logging: VeilidWASMConfigLogging( - performance: VeilidWASMConfigLoggingPerformance( - enabled: true, - level: VeilidConfigLogLevel.debug, - logsInTimings: true, - logsInConsole: true), - api: VeilidWASMConfigLoggingApi( - enabled: true, level: VeilidConfigLogLevel.info))); - Veilid.instance.initializeVeilidCore(platformConfig.json); - } else { - var platformConfig = VeilidFFIConfig( - logging: VeilidFFIConfigLogging( - terminal: VeilidFFIConfigLoggingTerminal( - enabled: false, - level: VeilidConfigLogLevel.debug, - ), - otlp: VeilidFFIConfigLoggingOtlp( - enabled: false, - level: VeilidConfigLogLevel.trace, - grpcEndpoint: "localhost:4317", - serviceName: "VeilidExample"), - api: VeilidFFIConfigLoggingApi( - enabled: true, level: VeilidConfigLogLevel.info))); - Veilid.instance.initializeVeilidCore(platformConfig.json); - } + // Initialize Veilid + veilidInit(); + + // Run the app runApp(MaterialApp( title: 'Veilid Plugin Demo', - theme: ThemeData( - primarySwatch: '#6667AB', - visualDensity: VisualDensity.adaptivePlatformDensity, - ), + theme: newVeilidTheme(), home: const MyApp())); } - -// Main App -class MyApp extends StatefulWidget { - const MyApp({Key? key}) : super(key: key); - - @override - State createState() => _MyAppState(); -} - -class _MyAppState extends State with UiLoggy { - String _veilidVersion = 'Unknown'; - Stream? _updateStream; - Future? _updateProcessor; - - @override - void initState() { - super.initState(); - setRootLogLevel(LogLevel.info); - initPlatformState(); - } - - // Platform messages are asynchronous, so we initialize in an async method. - Future initPlatformState() async { - String veilidVersion; - // Platform messages may fail, so we use a try/catch PlatformException. - // We also handle the message potentially returning null. - try { - veilidVersion = Veilid.instance.veilidVersionString(); - } on Exception { - veilidVersion = 'Failed to get veilid version.'; - } - - // In case of hot restart shut down first - try { - await Veilid.instance.shutdownVeilidCore(); - } on Exception { - // - } - - // If the widget was removed from the tree while the asynchronous platform - // message was in flight, we want to discard the reply rather than calling - // setState to update our non-existent appearance. - if (!mounted) return; - - setState(() { - _veilidVersion = veilidVersion; - }); - } - - Future processLog(VeilidLog log) async { - StackTrace? stackTrace; - Object? error; - final backtrace = log.backtrace; - if (backtrace != null) { - stackTrace = - StackTrace.fromString("$backtrace\n${StackTrace.current.toString()}"); - error = 'embedded stack trace for ${log.logLevel} ${log.message}'; - } - - switch (log.logLevel) { - case VeilidLogLevel.error: - loggy.error(log.message, error, stackTrace); - break; - case VeilidLogLevel.warn: - loggy.warning(log.message, error, stackTrace); - break; - case VeilidLogLevel.info: - loggy.info(log.message, error, stackTrace); - break; - case VeilidLogLevel.debug: - loggy.debug(log.message, error, stackTrace); - break; - case VeilidLogLevel.trace: - loggy.trace(log.message, error, stackTrace); - break; - } - } - - Future processUpdates() async { - var stream = _updateStream; - if (stream != null) { - await for (final update in stream) { - if (update is VeilidLog) { - await processLog(update); - } else if (update is VeilidAppMessage) { - loggy.info("AppMessage: ${update.json}"); - } else if (update is VeilidAppCall) { - loggy.info("AppCall: ${update.json}"); - } else { - loggy.trace("Update: ${update.json}"); - } - } - } - } - - @override - Widget build(BuildContext context) { - final ButtonStyle buttonStyle = - ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)); - - return Scaffold( - appBar: AppBar( - title: Text('Veilid Plugin Version $_veilidVersion'), - ), - body: Column(children: [ - Expanded( - child: Container( - color: ThemeData.dark().scaffoldBackgroundColor, - height: MediaQuery.of(context).size.height * 0.4, - child: SafeArea( - child: TerminalView( - terminal, - controller: terminalController, - autofocus: true, - backgroundOpacity: 0.7, - onSecondaryTapDown: (details, offset) async { - final selection = terminalController.selection; - if (selection != null) { - final text = terminal.buffer.getText(selection); - terminalController.clearSelection(); - await Clipboard.setData(ClipboardData(text: text)); - } else { - final data = await Clipboard.getData('text/plain'); - final text = data?.text; - if (text != null) { - terminal.paste(text); - } - } - }, - ), - ), - )), - Container( - padding: const EdgeInsets.fromLTRB(8, 8, 8, 12), - child: Row(children: [ - ElevatedButton( - style: buttonStyle, - onPressed: _updateStream != null - ? null - : () async { - var updateStream = await Veilid.instance - .startupVeilidCore( - await getDefaultVeilidConfig()); - setState(() { - _updateStream = updateStream; - _updateProcessor = processUpdates(); - }); - await Veilid.instance.attach(); - }, - child: const Text('Startup'), - ), - ElevatedButton( - style: buttonStyle, - onPressed: _updateStream == null - ? null - : () async { - await Veilid.instance.shutdownVeilidCore(); - if (_updateProcessor != null) { - await _updateProcessor; - } - setState(() { - _updateProcessor = null; - _updateStream = null; - }); - }, - child: const Text('Shutdown'), - ), - ])), - Row(children: [ - Expanded( - child: TextField( - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: 'Debug Command'), - textInputAction: TextInputAction.send, - onSubmitted: (String v) async { - loggy.info(await Veilid.instance.debug(v)); - })), - DropdownButton( - value: loggy.level.logLevel, - onChanged: (LogLevel? newLevel) { - setState(() { - setRootLogLevel(newLevel); - }); - }, - items: const [ - DropdownMenuItem( - value: LogLevel.error, child: Text("Error")), - DropdownMenuItem( - value: LogLevel.warning, child: Text("Warning")), - DropdownMenuItem( - value: LogLevel.info, child: Text("Info")), - DropdownMenuItem( - value: LogLevel.debug, child: Text("Debug")), - DropdownMenuItem( - value: traceLevel, child: Text("Trace")), - DropdownMenuItem( - value: LogLevel.all, child: Text("All")), - ]) - ]), - ])); - } -} diff --git a/veilid-flutter/example/lib/veilid_color.dart b/veilid-flutter/example/lib/veilid_color.dart index 808fef7b..a5e45d1f 100644 --- a/veilid-flutter/example/lib/veilid_color.dart +++ b/veilid-flutter/example/lib/veilid_color.dart @@ -232,3 +232,11 @@ const Map popComplentaryColorSwatch = { const MaterialColor materialPopComplementaryColor = MaterialColor(0xff59f282, popComplentaryColorSwatch); + +ThemeData newVeilidTheme() { + return ThemeData( + primarySwatch: materialPrimaryColor, + secondaryHeaderColor: materialSecondaryColor, + visualDensity: VisualDensity.adaptivePlatformDensity, + ); +} diff --git a/veilid-flutter/example/lib/veilid_init.dart b/veilid-flutter/example/lib/veilid_init.dart new file mode 100644 index 00000000..373ea012 --- /dev/null +++ b/veilid-flutter/example/lib/veilid_init.dart @@ -0,0 +1,34 @@ +import 'package:flutter/foundation.dart'; +import 'package:veilid/veilid.dart'; + +// Initialize Veilid +// Call only once. +void veilidInit() { + if (kIsWeb) { + var platformConfig = VeilidWASMConfig( + logging: VeilidWASMConfigLogging( + performance: VeilidWASMConfigLoggingPerformance( + enabled: true, + level: VeilidConfigLogLevel.debug, + logsInTimings: true, + logsInConsole: true), + api: VeilidWASMConfigLoggingApi( + enabled: true, level: VeilidConfigLogLevel.info))); + Veilid.instance.initializeVeilidCore(platformConfig.json); + } else { + var platformConfig = VeilidFFIConfig( + logging: VeilidFFIConfigLogging( + terminal: VeilidFFIConfigLoggingTerminal( + enabled: false, + level: VeilidConfigLogLevel.debug, + ), + otlp: VeilidFFIConfigLoggingOtlp( + enabled: false, + level: VeilidConfigLogLevel.trace, + grpcEndpoint: "localhost:4317", + serviceName: "VeilidExample"), + api: VeilidFFIConfigLoggingApi( + enabled: true, level: VeilidConfigLogLevel.info))); + Veilid.instance.initializeVeilidCore(platformConfig.json); + } +} diff --git a/veilid-flutter/example/lib/virtual_keyboard.dart b/veilid-flutter/example/lib/virtual_keyboard.dart index b59bb847..5625e2c7 100644 --- a/veilid-flutter/example/lib/virtual_keyboard.dart +++ b/veilid-flutter/example/lib/virtual_keyboard.dart @@ -11,7 +11,6 @@ class VirtualKeyboardView extends StatelessWidget { return AnimatedBuilder( animation: keyboard, builder: (context, child) => ToggleButtons( - children: [Text('Ctrl'), Text('Alt'), Text('Shift')], isSelected: [keyboard.ctrl, keyboard.alt, keyboard.shift], onPressed: (index) { switch (index) { @@ -26,6 +25,7 @@ class VirtualKeyboardView extends StatelessWidget { break; } }, + children: const [Text('Ctrl'), Text('Alt'), Text('Shift')], ), ); } diff --git a/veilid-flutter/example/macos/Podfile.lock b/veilid-flutter/example/macos/Podfile.lock index 5d7ff285..319eb3d2 100644 --- a/veilid-flutter/example/macos/Podfile.lock +++ b/veilid-flutter/example/macos/Podfile.lock @@ -1,4 +1,8 @@ PODS: + - flutter_acrylic (0.1.0): + - FlutterMacOS + - flutter_pty (0.0.1): + - FlutterMacOS - FlutterMacOS (1.0.0) - path_provider_macos (0.0.1): - FlutterMacOS @@ -6,11 +10,17 @@ PODS: - FlutterMacOS DEPENDENCIES: + - flutter_acrylic (from `Flutter/ephemeral/.symlinks/plugins/flutter_acrylic/macos`) + - flutter_pty (from `Flutter/ephemeral/.symlinks/plugins/flutter_pty/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) - veilid (from `Flutter/ephemeral/.symlinks/plugins/veilid/macos`) EXTERNAL SOURCES: + flutter_acrylic: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_acrylic/macos + flutter_pty: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_pty/macos FlutterMacOS: :path: Flutter/ephemeral path_provider_macos: @@ -19,6 +29,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/veilid/macos SPEC CHECKSUMS: + flutter_acrylic: c3df24ae52ab6597197837ce59ef2a8542640c17 + flutter_pty: 41b6f848ade294be726a6b94cdd4a67c3bc52f59 FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811 path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 veilid: f2b3b5b3ac8cd93fc5443ab830d5153575dacf36 diff --git a/veilid-flutter/example/pubspec.lock b/veilid-flutter/example/pubspec.lock index f59dfc44..494f5706 100644 --- a/veilid-flutter/example/pubspec.lock +++ b/veilid-flutter/example/pubspec.lock @@ -1,6 +1,13 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + ansicolor: + dependency: "direct main" + description: + name: ansicolor + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" async: dependency: transitive description: diff --git a/veilid-flutter/example/pubspec.yaml b/veilid-flutter/example/pubspec.yaml index e603fd76..0d674e85 100644 --- a/veilid-flutter/example/pubspec.yaml +++ b/veilid-flutter/example/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: xterm: ^3.4.0 flutter_pty: ^0.3.1 flutter_acrylic: ^1.0.0+2 + ansicolor: ^2.0.1 dev_dependencies: flutter_test: diff --git a/veilid-flutter/example/test/widget_test.dart b/veilid-flutter/example/test/widget_test.dart index a0ef6c08..551377fc 100644 --- a/veilid-flutter/example/test/widget_test.dart +++ b/veilid-flutter/example/test/widget_test.dart @@ -8,7 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:veilid_example/main.dart'; +import 'package:veilid_example/app.dart'; void main() { testWidgets('Verify Platform version', (WidgetTester tester) async { @@ -18,8 +18,8 @@ void main() { // Verify that platform version is retrieved. expect( find.byWidgetPredicate( - (Widget widget) => widget is Text && - widget.data!.startsWith('Running on:'), + (Widget widget) => + widget is Text && widget.data!.startsWith('Running on:'), ), findsOneWidget, ); From 6753fe01a12447c245eaee8d6806f3528f97c750 Mon Sep 17 00:00:00 2001 From: John Smith Date: Sat, 10 Dec 2022 13:16:26 -0500 Subject: [PATCH 46/88] log cleanup --- veilid-core/src/network_manager/mod.rs | 10 +++++----- veilid-core/src/network_manager/native/mod.rs | 2 +- .../network_manager/native/network_class_discovery.rs | 2 +- veilid-core/src/network_manager/native/protocol/udp.rs | 2 +- veilid-core/src/network_manager/network_connection.rs | 2 +- veilid-core/src/routing_table/mod.rs | 4 ++-- veilid-core/src/rpc_processor/mod.rs | 9 ++------- .../src/rpc_processor/rpc_validate_dial_info.rs | 4 ++-- veilid-flutter/lib/veilid.dart | 4 ++-- veilid-tools/src/network_result.rs | 2 +- 10 files changed, 18 insertions(+), 23 deletions(-) diff --git a/veilid-core/src/network_manager/mod.rs b/veilid-core/src/network_manager/mod.rs index 56b95702..cb3672ac 100644 --- a/veilid-core/src/network_manager/mod.rs +++ b/veilid-core/src/network_manager/mod.rs @@ -814,7 +814,7 @@ impl NetworkManager { // Send receipt directly log_net!(debug "send_out_of_band_receipt: dial_info={}", dial_info); - network_result_value_or_log!(debug self + network_result_value_or_log!(self .net() .send_data_unbound_to_dial_info(dial_info, rcpt_data) .await? => { @@ -1243,7 +1243,7 @@ impl NetworkManager { let timeout_ms = self.with_config(|c| c.network.rpc.timeout_ms); // Send boot magic to requested peer address let data = BOOT_MAGIC.to_vec(); - let out_data: Vec = network_result_value_or_log!(debug self + let out_data: Vec = network_result_value_or_log!(self .net() .send_recv_data_unbound_to_dial_info(dial_info, data, timeout_ms) .await? => @@ -1315,13 +1315,13 @@ impl NetworkManager { // Is this a direct bootstrap request instead of an envelope? if data[0..4] == *BOOT_MAGIC { - network_result_value_or_log!(debug self.handle_boot_request(connection_descriptor).await? => {}); + network_result_value_or_log!(self.handle_boot_request(connection_descriptor).await? => {}); return Ok(true); } // Is this an out-of-band receipt instead of an envelope? if data[0..4] == *RECEIPT_MAGIC { - network_result_value_or_log!(debug self.handle_out_of_band_receipt(data).await => {}); + network_result_value_or_log!(self.handle_out_of_band_receipt(data).await => {}); return Ok(true); } @@ -1396,7 +1396,7 @@ impl NetworkManager { if let Some(relay_nr) = some_relay_nr { // Relay the packet to the desired destination log_net!("relaying {} bytes to {}", data.len(), relay_nr); - network_result_value_or_log!(debug self.send_data(relay_nr, data.to_vec()) + network_result_value_or_log!(self.send_data(relay_nr, data.to_vec()) .await .wrap_err("failed to forward envelope")? => { return Ok(false); diff --git a/veilid-core/src/network_manager/native/mod.rs b/veilid-core/src/network_manager/native/mod.rs index de848990..9a532d3a 100644 --- a/veilid-core/src/network_manager/native/mod.rs +++ b/veilid-core/src/network_manager/native/mod.rs @@ -512,7 +512,7 @@ impl Network { &peer_socket_addr, &descriptor.local().map(|sa| sa.to_socket_addr()), ) { - network_result_value_or_log!(debug ph.clone() + network_result_value_or_log!(ph.clone() .send_message(data.clone(), peer_socket_addr) .await .wrap_err("sending data to existing conection")? => { return Ok(Some(data)); } ); diff --git a/veilid-core/src/network_manager/native/network_class_discovery.rs b/veilid-core/src/network_manager/native/network_class_discovery.rs index 3f7e6122..86acc197 100644 --- a/veilid-core/src/network_manager/native/network_class_discovery.rs +++ b/veilid-core/src/network_manager/native/network_class_discovery.rs @@ -83,7 +83,7 @@ impl DiscoveryContext { async fn request_public_address(&self, node_ref: NodeRef) -> Option { let rpc = self.routing_table.rpc_processor(); - let res = network_result_value_or_log!(debug match rpc.rpc_call_status(Destination::direct(node_ref.clone())).await { + let res = network_result_value_or_log!(match rpc.rpc_call_status(Destination::direct(node_ref.clone())).await { Ok(v) => v, Err(e) => { log_net!(error diff --git a/veilid-core/src/network_manager/native/protocol/udp.rs b/veilid-core/src/network_manager/native/protocol/udp.rs index 6af66b1d..a8fe5ce8 100644 --- a/veilid-core/src/network_manager/native/protocol/udp.rs +++ b/veilid-core/src/network_manager/native/protocol/udp.rs @@ -14,7 +14,7 @@ impl RawUdpProtocolHandler { // #[instrument(level = "trace", err, skip(self, data), fields(data.len = data.len(), ret.len, ret.descriptor))] pub async fn recv_message(&self, data: &mut [u8]) -> io::Result<(usize, ConnectionDescriptor)> { let (size, descriptor) = loop { - let (size, remote_addr) = network_result_value_or_log!(debug self.socket.recv_from(data).await.into_network_result()? => continue); + let (size, remote_addr) = network_result_value_or_log!(self.socket.recv_from(data).await.into_network_result()? => continue); if size > MAX_MESSAGE_SIZE { log_net!(debug "{}({}) at {}@{}:{}", "Invalid message".green(), "received too large UDP message", file!(), line!(), column!()); continue; diff --git a/veilid-core/src/network_manager/network_connection.rs b/veilid-core/src/network_manager/network_connection.rs index 3d410615..3b573835 100644 --- a/veilid-core/src/network_manager/network_connection.rs +++ b/veilid-core/src/network_manager/network_connection.rs @@ -301,7 +301,7 @@ impl NetworkConnection { match res { Ok(v) => { - let message = network_result_value_or_log!(debug v => { + let message = network_result_value_or_log!(v => { return RecvLoopAction::Finish; }); diff --git a/veilid-core/src/routing_table/mod.rs b/veilid-core/src/routing_table/mod.rs index b51ef048..32ff20d3 100644 --- a/veilid-core/src/routing_table/mod.rs +++ b/veilid-core/src/routing_table/mod.rs @@ -891,7 +891,7 @@ impl RoutingTable { // and then contact those nodes to inform -them- that we exist // Ask bootstrap server for nodes closest to our own node - let closest_nodes = network_result_value_or_log!(debug match self.find_self(node_ref.clone()).await { + let closest_nodes = network_result_value_or_log!(match self.find_self(node_ref.clone()).await { Err(e) => { log_rtab!(error "find_self failed for {:?}: {:?}", @@ -907,7 +907,7 @@ impl RoutingTable { // Ask each node near us to find us as well if wide { for closest_nr in closest_nodes { - network_result_value_or_log!(debug match self.find_self(closest_nr.clone()).await { + network_result_value_or_log!(match self.find_self(closest_nr.clone()).await { Err(e) => { log_rtab!(error "find_self failed for {:?}: {:?}", diff --git a/veilid-core/src/rpc_processor/mod.rs b/veilid-core/src/rpc_processor/mod.rs index f4d052be..149c3b8c 100644 --- a/veilid-core/src/rpc_processor/mod.rs +++ b/veilid-core/src/rpc_processor/mod.rs @@ -1344,13 +1344,8 @@ impl RPCProcessor { Ok(v) => v, }; - cfg_if::cfg_if! { - if #[cfg(debug_assertions)] { - network_result_value_or_log!(warn res => {}); - } else { - network_result_value_or_log!(debug res => {}); - } - } + + network_result_value_or_log!(res => {}); } } diff --git a/veilid-core/src/rpc_processor/rpc_validate_dial_info.rs b/veilid-core/src/rpc_processor/rpc_validate_dial_info.rs index d1910709..df1a58a2 100644 --- a/veilid-core/src/rpc_processor/rpc_validate_dial_info.rs +++ b/veilid-core/src/rpc_processor/rpc_validate_dial_info.rs @@ -26,7 +26,7 @@ impl RPCProcessor { // Send the validate_dial_info request // This can only be sent directly, as relays can not validate dial info - network_result_value_or_log!(debug self.statement(Destination::direct(peer), statement) + network_result_value_or_log!(self.statement(Destination::direct(peer), statement) .await? => { return Ok(false); } @@ -144,7 +144,7 @@ impl RPCProcessor { // Send the validate_dial_info request // This can only be sent directly, as relays can not validate dial info - network_result_value_or_log!(debug self.statement(Destination::direct(peer), statement) + network_result_value_or_log!(self.statement(Destination::direct(peer), statement) .await? => { continue; } diff --git a/veilid-flutter/lib/veilid.dart b/veilid-flutter/lib/veilid.dart index e85a9262..9b194c02 100644 --- a/veilid-flutter/lib/veilid.dart +++ b/veilid-flutter/lib/veilid.dart @@ -1474,8 +1474,8 @@ class VeilidStateRoute { }); VeilidStateRoute.fromJson(Map json) - : deadRoutes = jsonDecode(json['dead_routes']), - deadRemoteRoutes = jsonDecode(json['dead_remote_routes']); + : deadRoutes = json['dead_routes'], + deadRemoteRoutes = json['dead_remote_routes']; Map get json { return {'dead_routes': deadRoutes, 'dead_remote_routes': deadRemoteRoutes}; diff --git a/veilid-tools/src/network_result.rs b/veilid-tools/src/network_result.rs index 306f9ffd..55145cf5 100644 --- a/veilid-tools/src/network_result.rs +++ b/veilid-tools/src/network_result.rs @@ -328,7 +328,7 @@ macro_rules! log_network_result { #[macro_export] macro_rules! network_result_value_or_log { - ($level: ident $r: expr => $f:tt) => { + ($r: expr => $f:tt) => { match $r { NetworkResult::Timeout => { log_network_result!( From 572f0f23edbfd77c9ee59ea55f6a10fde0c50f13 Mon Sep 17 00:00:00 2001 From: John Smith Date: Sat, 10 Dec 2022 13:36:26 -0500 Subject: [PATCH 47/88] timestamp fix --- external/keyring-manager | 2 +- veilid-core/src/network_manager/mod.rs | 8 ++++---- .../src/network_manager/tasks/rolling_transfers.rs | 2 +- veilid-core/src/routing_table/bucket_entry.rs | 2 +- veilid-core/src/routing_table/node_ref.rs | 2 +- veilid-core/src/routing_table/route_spec_store.rs | 6 +++--- veilid-core/src/routing_table/stats_accounting.rs | 2 +- veilid-core/src/rpc_processor/operation_waiter.rs | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/external/keyring-manager b/external/keyring-manager index c153eb30..b127b2d3 160000 --- a/external/keyring-manager +++ b/external/keyring-manager @@ -1 +1 @@ -Subproject commit c153eb3015d6d118e5d467865510d053ddd84533 +Subproject commit b127b2d3c653fea163a776dd58b3798f28aeeee3 diff --git a/veilid-core/src/network_manager/mod.rs b/veilid-core/src/network_manager/mod.rs index cb3672ac..2ba43538 100644 --- a/veilid-core/src/network_manager/mod.rs +++ b/veilid-core/src/network_manager/mod.rs @@ -1346,19 +1346,19 @@ impl NetworkManager { let ts = get_timestamp(); let ets = envelope.get_timestamp(); if let Some(tsbehind) = tsbehind { - if tsbehind > 0 && (ts > ets && ts - ets > tsbehind) { + if tsbehind > 0 && (ts > ets && ts.saturating_sub(ets) > tsbehind) { log_net!(debug "envelope time was too far in the past: {}ms ", - timestamp_to_secs(ts - ets) * 1000f64 + timestamp_to_secs(ts.saturating_sub(ets)) * 1000f64 ); return Ok(false); } } if let Some(tsahead) = tsahead { - if tsahead > 0 && (ts < ets && ets - ts > tsahead) { + if tsahead > 0 && (ts < ets && ets.saturating_sub(ts) > tsahead) { log_net!(debug "envelope time was too far in the future: {}ms", - timestamp_to_secs(ets - ts) * 1000f64 + timestamp_to_secs(ets.saturating_sub(ts)) * 1000f64 ); return Ok(false); } diff --git a/veilid-core/src/network_manager/tasks/rolling_transfers.rs b/veilid-core/src/network_manager/tasks/rolling_transfers.rs index c3774b6a..0e924024 100644 --- a/veilid-core/src/network_manager/tasks/rolling_transfers.rs +++ b/veilid-core/src/network_manager/tasks/rolling_transfers.rs @@ -30,7 +30,7 @@ impl NetworkManager { ); // While we're here, lets see if this address has timed out - if cur_ts - stats.last_seen_ts >= IPADDR_MAX_INACTIVE_DURATION_US { + if cur_ts.saturating_sub(stats.last_seen_ts) >= IPADDR_MAX_INACTIVE_DURATION_US { // it's dead, put it in the dead list dead_addrs.insert(*addr); } diff --git a/veilid-core/src/routing_table/bucket_entry.rs b/veilid-core/src/routing_table/bucket_entry.rs index 51f79498..c64cb7ee 100644 --- a/veilid-core/src/routing_table/bucket_entry.rs +++ b/veilid-core/src/routing_table/bucket_entry.rs @@ -721,7 +721,7 @@ impl BucketEntryInner { self.transfer_stats_accounting.add_down(bytes); self.peer_stats.rpc_stats.messages_rcvd += 1; self.peer_stats.rpc_stats.questions_in_flight -= 1; - self.record_latency(recv_ts - send_ts); + self.record_latency(recv_ts.saturating_sub(send_ts)); self.touch_last_seen(recv_ts); self.peer_stats.rpc_stats.recent_lost_answers = 0; } diff --git a/veilid-core/src/routing_table/node_ref.rs b/veilid-core/src/routing_table/node_ref.rs index edb501f0..8ae8929a 100644 --- a/veilid-core/src/routing_table/node_ref.rs +++ b/veilid-core/src/routing_table/node_ref.rs @@ -319,7 +319,7 @@ pub trait NodeRefBase: Sized { self.operate_mut(|rti, e| { rti.transfer_stats_accounting().add_down(bytes); rti.latency_stats_accounting() - .record_latency(recv_ts - send_ts); + .record_latency(recv_ts.saturating_sub(send_ts)); e.answer_rcvd(send_ts, recv_ts, bytes); }) } diff --git a/veilid-core/src/routing_table/route_spec_store.rs b/veilid-core/src/routing_table/route_spec_store.rs index 89e1d35a..4dea0c30 100644 --- a/veilid-core/src/routing_table/route_spec_store.rs +++ b/veilid-core/src/routing_table/route_spec_store.rs @@ -1597,7 +1597,7 @@ impl RouteSpecStore { .remote_private_route_cache .entry(pr_pubkey) .and_modify(|rpr| { - if cur_ts - rpr.last_touched_ts >= REMOTE_PRIVATE_ROUTE_CACHE_EXPIRY { + if cur_ts.saturating_sub(rpr.last_touched_ts) >= REMOTE_PRIVATE_ROUTE_CACHE_EXPIRY { // Start fresh if this had expired rpr.last_seen_our_node_info_ts = 0; rpr.last_touched_ts = cur_ts; @@ -1640,7 +1640,7 @@ impl RouteSpecStore { F: FnOnce(&mut RemotePrivateRouteInfo) -> R, { let rpr = inner.cache.remote_private_route_cache.get_mut(key)?; - if cur_ts - rpr.last_touched_ts < REMOTE_PRIVATE_ROUTE_CACHE_EXPIRY { + if cur_ts.saturating_sub(rpr.last_touched_ts) < REMOTE_PRIVATE_ROUTE_CACHE_EXPIRY { rpr.last_touched_ts = cur_ts; return Some(f(rpr)); } @@ -1662,7 +1662,7 @@ impl RouteSpecStore { match inner.cache.remote_private_route_cache.entry(*key) { hashlink::lru_cache::Entry::Occupied(mut o) => { let rpr = o.get_mut(); - if cur_ts - rpr.last_touched_ts < REMOTE_PRIVATE_ROUTE_CACHE_EXPIRY { + if cur_ts.saturating_sub(rpr.last_touched_ts) < REMOTE_PRIVATE_ROUTE_CACHE_EXPIRY { return Some(f(rpr)); } o.remove(); diff --git a/veilid-core/src/routing_table/stats_accounting.rs b/veilid-core/src/routing_table/stats_accounting.rs index 7167636e..c02ab6ca 100644 --- a/veilid-core/src/routing_table/stats_accounting.rs +++ b/veilid-core/src/routing_table/stats_accounting.rs @@ -45,7 +45,7 @@ impl TransferStatsAccounting { cur_ts: u64, transfer_stats: &mut TransferStatsDownUp, ) { - let dur_ms = (cur_ts - last_ts) / 1000u64; + let dur_ms = cur_ts.saturating_sub(last_ts) / 1000u64; while self.rolling_transfers.len() >= ROLLING_TRANSFERS_SIZE { self.rolling_transfers.pop_front(); } diff --git a/veilid-core/src/rpc_processor/operation_waiter.rs b/veilid-core/src/rpc_processor/operation_waiter.rs index 3bf14b94..3f56e339 100644 --- a/veilid-core/src/rpc_processor/operation_waiter.rs +++ b/veilid-core/src/rpc_processor/operation_waiter.rs @@ -130,7 +130,7 @@ where //xxx: causes crash (Missing otel data span extensions) // Span::current().follows_from(span_id); - (ret, end_ts - start_ts) + (ret, end_ts.saturating_sub(start_ts)) })) } } From 36b6e7446f3d4f57d45a7bf2baf26d294f8259e6 Mon Sep 17 00:00:00 2001 From: John Smith Date: Sat, 10 Dec 2022 17:07:52 -0500 Subject: [PATCH 48/88] theming --- external/keyring-manager | 2 +- veilid-flutter/example/lib/app.dart | 142 +++++++++--------- veilid-flutter/example/lib/log_terminal.dart | 50 +++--- veilid-flutter/example/lib/main.dart | 2 +- .../{veilid_color.dart => veilid_theme.dart} | 51 +++++++ .../linux/flutter/generated_plugins.cmake | 1 - veilid-flutter/example/macos/Podfile.lock | 6 - veilid-flutter/example/pubspec.lock | 21 --- veilid-flutter/example/pubspec.yaml | 2 - .../windows/flutter/generated_plugins.cmake | 1 - 10 files changed, 151 insertions(+), 127 deletions(-) rename veilid-flutter/example/lib/{veilid_color.dart => veilid_theme.dart} (80%) diff --git a/external/keyring-manager b/external/keyring-manager index b127b2d3..c153eb30 160000 --- a/external/keyring-manager +++ b/external/keyring-manager @@ -1 +1 @@ -Subproject commit b127b2d3c653fea163a776dd58b3798f28aeeee3 +Subproject commit c153eb3015d6d118e5d467865510d053ddd84533 diff --git a/veilid-flutter/example/lib/app.dart b/veilid-flutter/example/lib/app.dart index 84e03d8f..041d427a 100644 --- a/veilid-flutter/example/lib/app.dart +++ b/veilid-flutter/example/lib/app.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:veilid/veilid.dart'; import 'package:loggy/loggy.dart'; +import 'package:veilid_example/veilid_theme.dart'; import 'log_terminal.dart'; import 'config.dart'; @@ -18,6 +19,7 @@ class MyApp extends StatefulWidget { class _MyAppState extends State with UiLoggy { String _veilidVersion = 'Unknown'; + bool _startedUp = false; Stream? _updateStream; Future? _updateProcessor; @@ -102,11 +104,31 @@ class _MyAppState extends State with UiLoggy { } } + Future toggleStartup(bool startup) async { + if (startup && !_startedUp) { + var updateStream = await Veilid.instance + .startupVeilidCore(await getDefaultVeilidConfig()); + setState(() { + _updateStream = updateStream; + _updateProcessor = processUpdates(); + _startedUp = true; + }); + await Veilid.instance.attach(); + } else if (!startup && _startedUp) { + await Veilid.instance.shutdownVeilidCore(); + if (_updateProcessor != null) { + await _updateProcessor; + } + setState(() { + _updateProcessor = null; + _updateStream = null; + _startedUp = false; + }); + } + } + @override Widget build(BuildContext context) { - final ButtonStyle buttonStyle = - ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)); - return Scaffold( appBar: AppBar( title: Text('Veilid Plugin Version $_veilidVersion'), @@ -114,73 +136,53 @@ class _MyAppState extends State with UiLoggy { body: Column(children: [ const Expanded(child: LogTerminal()), Container( - padding: const EdgeInsets.fromLTRB(8, 8, 8, 12), - child: Row(children: [ - ElevatedButton( - style: buttonStyle, - onPressed: _updateStream != null - ? null - : () async { - var updateStream = await Veilid.instance - .startupVeilidCore( - await getDefaultVeilidConfig()); - setState(() { - _updateStream = updateStream; - _updateProcessor = processUpdates(); - }); - await Veilid.instance.attach(); - }, - child: const Text('Startup'), - ), - ElevatedButton( - style: buttonStyle, - onPressed: _updateStream == null - ? null - : () async { - await Veilid.instance.shutdownVeilidCore(); - if (_updateProcessor != null) { - await _updateProcessor; - } - setState(() { - _updateProcessor = null; - _updateStream = null; - }); - }, - child: const Text('Shutdown'), - ), - ])), - Row(children: [ - Expanded( - child: TextField( - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: 'Debug Command'), - textInputAction: TextInputAction.send, - onSubmitted: (String v) async { - loggy.info(await Veilid.instance.debug(v)); - })), - DropdownButton( - value: loggy.level.logLevel, - onChanged: (LogLevel? newLevel) { - setState(() { - setRootLogLevel(newLevel); - }); - }, - items: const [ - DropdownMenuItem( - value: LogLevel.error, child: Text("Error")), - DropdownMenuItem( - value: LogLevel.warning, child: Text("Warning")), - DropdownMenuItem( - value: LogLevel.info, child: Text("Info")), - DropdownMenuItem( - value: LogLevel.debug, child: Text("Debug")), - DropdownMenuItem( - value: traceLevel, child: Text("Trace")), - DropdownMenuItem( - value: LogLevel.all, child: Text("All")), - ]) - ]), + decoration: BoxDecoration(color: materialPrimaryColor, boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + spreadRadius: 4, + blurRadius: 4, + ) + ]), + padding: const EdgeInsets.all(5.0), + child: Row(children: [ + Expanded( + child: pad(TextField( + decoration: + newInputDecoration('Debug Command', _startedUp), + textInputAction: TextInputAction.send, + enabled: _startedUp, + onSubmitted: (String v) async { + loggy.info(await Veilid.instance.debug(v)); + }))), + pad(const Text('Startup')), + pad(Switch( + value: _startedUp, + onChanged: (bool value) async { + await toggleStartup(value); + })), + pad(DropdownButton( + value: loggy.level.logLevel, + onChanged: (LogLevel? newLevel) { + setState(() { + setRootLogLevel(newLevel); + }); + }, + items: const [ + DropdownMenuItem( + value: LogLevel.error, child: Text("Error")), + DropdownMenuItem( + value: LogLevel.warning, child: Text("Warning")), + DropdownMenuItem( + value: LogLevel.info, child: Text("Info")), + DropdownMenuItem( + value: LogLevel.debug, child: Text("Debug")), + DropdownMenuItem( + value: traceLevel, child: Text("Trace")), + DropdownMenuItem( + value: LogLevel.all, child: Text("All")), + ])), + ]), + ), ])); } } diff --git a/veilid-flutter/example/lib/log_terminal.dart b/veilid-flutter/example/lib/log_terminal.dart index 2ba817f3..cf33c1e0 100644 --- a/veilid-flutter/example/lib/log_terminal.dart +++ b/veilid-flutter/example/lib/log_terminal.dart @@ -5,6 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:xterm/xterm.dart'; import 'log.dart'; +import 'veilid_theme.dart'; + +const kDefaultTerminalStyle = TerminalStyle( + fontSize: kDefaultMonoTerminalFontSize, + height: kDefaultMonoTerminalFontHeight, + fontFamily: kDefaultMonoTerminalFontFamily); class LogTerminal extends StatefulWidget { const LogTerminal({Key? key}) : super(key: key); @@ -31,30 +37,26 @@ class _LogTerminalState extends State { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.transparent, - body: SafeArea( - child: TerminalView( - terminal, - controller: terminalController, - autofocus: true, - backgroundOpacity: 0.7, - onSecondaryTapDown: (details, offset) async { - final selection = terminalController.selection; - if (selection != null) { - final text = terminal.buffer.getText(selection); - terminalController.clearSelection(); - await Clipboard.setData(ClipboardData(text: text)); - } else { - final data = await Clipboard.getData('text/plain'); - final text = data?.text; - if (text != null) { - terminal.paste(text); - } - } - }, - ), - ), + return TerminalView( + terminal, + textStyle: kDefaultTerminalStyle, + controller: terminalController, + autofocus: true, + backgroundOpacity: 0.7, + onSecondaryTapDown: (details, offset) async { + final selection = terminalController.selection; + if (selection != null) { + final text = terminal.buffer.getText(selection); + terminalController.clearSelection(); + await Clipboard.setData(ClipboardData(text: text)); + } else { + final data = await Clipboard.getData('text/plain'); + final text = data?.text; + if (text != null) { + terminal.paste(text); + } + } + }, ); } } diff --git a/veilid-flutter/example/lib/main.dart b/veilid-flutter/example/lib/main.dart index 66e8ecdf..b93e91bd 100644 --- a/veilid-flutter/example/lib/main.dart +++ b/veilid-flutter/example/lib/main.dart @@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:veilid/veilid.dart'; import 'package:flutter_acrylic/flutter_acrylic.dart'; -import 'veilid_color.dart'; +import 'veilid_theme.dart'; import 'log.dart'; import 'app.dart'; import 'veilid_init.dart'; diff --git a/veilid-flutter/example/lib/veilid_color.dart b/veilid-flutter/example/lib/veilid_theme.dart similarity index 80% rename from veilid-flutter/example/lib/veilid_color.dart rename to veilid-flutter/example/lib/veilid_theme.dart index a5e45d1f..ef7b5b0a 100644 --- a/veilid-flutter/example/lib/veilid_color.dart +++ b/veilid-flutter/example/lib/veilid_theme.dart @@ -9,6 +9,9 @@ import 'package:flutter/material.dart'; +///////////////////////////////////////////////////////// +// Colors + const Map primaryColorSwatch = { 50: Color(0xffe9e9f3), 100: Color(0xffc7c8e2), @@ -233,10 +236,58 @@ const Map popComplentaryColorSwatch = { const MaterialColor materialPopComplementaryColor = MaterialColor(0xff59f282, popComplentaryColorSwatch); +///////////////////////////////////////////////////////// +// Spacing + +const kDefaultSpacingFactor = 4.0; + +const kDefaultMonoTerminalFontFamily = "CascadiaMonoPL.ttf"; +const kDefaultMonoTerminalFontHeight = 1.2; +const kDefaultMonoTerminalFontSize = 12.0; + +double spacingFactor(double multiplier) { + return multiplier * kDefaultSpacingFactor; +} + +Padding pad(Widget child) { + return Padding( + padding: const EdgeInsets.all(kDefaultSpacingFactor), child: child); +} + +///////////////////////////////////////////////////////// +// Theme + +InputDecoration newInputDecoration(String labelText, bool enabled) { + return InputDecoration( + labelText: labelText, + fillColor: enabled + ? materialPrimaryColor.shade200 + : materialPrimaryColor.shade200.withOpacity(0.5)); +} + +InputDecorationTheme newInputDecorationTheme() { + return InputDecorationTheme( + border: const OutlineInputBorder(), + filled: true, + fillColor: materialPrimaryColor.shade200, + disabledBorder: const OutlineInputBorder( + borderSide: + BorderSide(color: Color.fromARGB(0, 0, 0, 0), width: 0.0)), + focusedBorder: OutlineInputBorder( + borderSide: + BorderSide(color: materialPrimaryColor.shade900, width: 0.0)), + floatingLabelBehavior: FloatingLabelBehavior.never, + floatingLabelStyle: TextStyle( + color: materialPrimaryColor.shade900, + letterSpacing: 1.2, + )); +} + ThemeData newVeilidTheme() { return ThemeData( primarySwatch: materialPrimaryColor, secondaryHeaderColor: materialSecondaryColor, visualDensity: VisualDensity.adaptivePlatformDensity, + inputDecorationTheme: newInputDecorationTheme(), ); } diff --git a/veilid-flutter/example/linux/flutter/generated_plugins.cmake b/veilid-flutter/example/linux/flutter/generated_plugins.cmake index ec1406bc..3989c459 100644 --- a/veilid-flutter/example/linux/flutter/generated_plugins.cmake +++ b/veilid-flutter/example/linux/flutter/generated_plugins.cmake @@ -8,7 +8,6 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST - flutter_pty ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/veilid-flutter/example/macos/Podfile.lock b/veilid-flutter/example/macos/Podfile.lock index 319eb3d2..0626573c 100644 --- a/veilid-flutter/example/macos/Podfile.lock +++ b/veilid-flutter/example/macos/Podfile.lock @@ -1,8 +1,6 @@ PODS: - flutter_acrylic (0.1.0): - FlutterMacOS - - flutter_pty (0.0.1): - - FlutterMacOS - FlutterMacOS (1.0.0) - path_provider_macos (0.0.1): - FlutterMacOS @@ -11,7 +9,6 @@ PODS: DEPENDENCIES: - flutter_acrylic (from `Flutter/ephemeral/.symlinks/plugins/flutter_acrylic/macos`) - - flutter_pty (from `Flutter/ephemeral/.symlinks/plugins/flutter_pty/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) - veilid (from `Flutter/ephemeral/.symlinks/plugins/veilid/macos`) @@ -19,8 +16,6 @@ DEPENDENCIES: EXTERNAL SOURCES: flutter_acrylic: :path: Flutter/ephemeral/.symlinks/plugins/flutter_acrylic/macos - flutter_pty: - :path: Flutter/ephemeral/.symlinks/plugins/flutter_pty/macos FlutterMacOS: :path: Flutter/ephemeral path_provider_macos: @@ -30,7 +25,6 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: flutter_acrylic: c3df24ae52ab6597197837ce59ef2a8542640c17 - flutter_pty: 41b6f848ade294be726a6b94cdd4a67c3bc52f59 FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811 path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 veilid: f2b3b5b3ac8cd93fc5443ab830d5153575dacf36 diff --git a/veilid-flutter/example/pubspec.lock b/veilid-flutter/example/pubspec.lock index 494f5706..74aca102 100644 --- a/veilid-flutter/example/pubspec.lock +++ b/veilid-flutter/example/pubspec.lock @@ -111,20 +111,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" - flutter_loggy: - dependency: "direct main" - description: - name: flutter_loggy - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - flutter_pty: - dependency: "direct main" - description: - name: flutter_pty - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.1" flutter_test: dependency: "direct dev" description: flutter @@ -268,13 +254,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.0" - rxdart: - dependency: transitive - description: - name: rxdart - url: "https://pub.dartlang.org" - source: hosted - version: "0.27.7" sky_engine: dependency: transitive description: flutter diff --git a/veilid-flutter/example/pubspec.yaml b/veilid-flutter/example/pubspec.yaml index 0d674e85..fee9152c 100644 --- a/veilid-flutter/example/pubspec.yaml +++ b/veilid-flutter/example/pubspec.yaml @@ -33,11 +33,9 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 loggy: ^2.0.1+1 - flutter_loggy: ^2.0.1 path_provider: ^2.0.11 path: ^1.8.1 xterm: ^3.4.0 - flutter_pty: ^0.3.1 flutter_acrylic: ^1.0.0+2 ansicolor: ^2.0.1 diff --git a/veilid-flutter/example/windows/flutter/generated_plugins.cmake b/veilid-flutter/example/windows/flutter/generated_plugins.cmake index 4ba079ee..4c215c3a 100644 --- a/veilid-flutter/example/windows/flutter/generated_plugins.cmake +++ b/veilid-flutter/example/windows/flutter/generated_plugins.cmake @@ -8,7 +8,6 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST - flutter_pty ) set(PLUGIN_BUNDLED_LIBRARIES) From 2e1920b62609e6ca12b879db44ace3e20f38b8c2 Mon Sep 17 00:00:00 2001 From: John Smith Date: Sat, 10 Dec 2022 19:11:58 -0500 Subject: [PATCH 49/88] route fixes --- .../src/network_manager/wasm/protocol/ws.rs | 30 +++-- .../src/routing_table/route_spec_store.rs | 7 +- veilid-core/src/veilid_api/debug.rs | 15 ++- veilid-flutter/example/fonts/FiraCode-VF.ttf | Bin 0 -> 259912 bytes ...talic-VariableFont_SOFT,WONK,opsz,wght.ttf | Bin 0 -> 407152 bytes ...unces-VariableFont_SOFT,WONK,opsz,wght.ttf | Bin 0 -> 355624 bytes veilid-flutter/example/lib/log_terminal.dart | 2 +- veilid-flutter/example/lib/veilid_init.dart | 2 +- veilid-flutter/example/lib/veilid_theme.dart | 2 +- veilid-flutter/example/pubspec.yaml | 8 ++ veilid-flutter/example/web/index.html | 106 ++++++------------ veilid-flutter/lib/veilid.dart | 10 +- veilid-flutter/lib/veilid_js.dart | 5 +- 13 files changed, 89 insertions(+), 98 deletions(-) create mode 100644 veilid-flutter/example/fonts/FiraCode-VF.ttf create mode 100644 veilid-flutter/example/fonts/Fraunces-Italic-VariableFont_SOFT,WONK,opsz,wght.ttf create mode 100644 veilid-flutter/example/fonts/Fraunces-VariableFont_SOFT,WONK,opsz,wght.ttf diff --git a/veilid-core/src/network_manager/wasm/protocol/ws.rs b/veilid-core/src/network_manager/wasm/protocol/ws.rs index 0315d3e5..56082a86 100644 --- a/veilid-core/src/network_manager/wasm/protocol/ws.rs +++ b/veilid-core/src/network_manager/wasm/protocol/ws.rs @@ -11,12 +11,20 @@ struct WebsocketNetworkConnectionInner { fn to_io(err: WsErr) -> io::Error { match err { - WsErr::InvalidWsState { supplied: _ } => io::Error::new(io::ErrorKind::InvalidInput, err.to_string()), + WsErr::InvalidWsState { supplied: _ } => { + io::Error::new(io::ErrorKind::InvalidInput, err.to_string()) + } WsErr::ConnectionNotOpen => io::Error::new(io::ErrorKind::NotConnected, err.to_string()), - WsErr::InvalidUrl { supplied: _ } => io::Error::new(io::ErrorKind::InvalidInput, err.to_string()), - WsErr::InvalidCloseCode { supplied: _ } => io::Error::new(io::ErrorKind::InvalidInput, err.to_string()), + WsErr::InvalidUrl { supplied: _ } => { + io::Error::new(io::ErrorKind::InvalidInput, err.to_string()) + } + WsErr::InvalidCloseCode { supplied: _ } => { + io::Error::new(io::ErrorKind::InvalidInput, err.to_string()) + } WsErr::ReasonStringToLong => io::Error::new(io::ErrorKind::InvalidInput, err.to_string()), - WsErr::ConnectionFailed { event: _ } => io::Error::new(io::ErrorKind::ConnectionRefused, err.to_string()), + WsErr::ConnectionFailed { event: _ } => { + io::Error::new(io::ErrorKind::ConnectionRefused, err.to_string()) + } WsErr::InvalidEncoding => io::Error::new(io::ErrorKind::InvalidInput, err.to_string()), WsErr::CantDecodeBlob => io::Error::new(io::ErrorKind::InvalidInput, err.to_string()), WsErr::UnknownDataType => io::Error::new(io::ErrorKind::InvalidInput, err.to_string()), @@ -80,19 +88,19 @@ impl WebsocketNetworkConnection { let out = match SendWrapper::new(self.inner.ws_stream.clone().next()).await { Some(WsMessage::Binary(v)) => { if v.len() > MAX_MESSAGE_SIZE { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "too large ws message", - )); + return Ok(NetworkResult::invalid_message("too large ws message")); } NetworkResult::Value(v) } - Some(_) => NetworkResult::NoConnection(io::Error::new( + Some(_) => NetworkResult::no_connection_other(io::Error::new( io::ErrorKind::ConnectionReset, "Unexpected WS message type", )), None => { - bail_io_error_other!("WS stream closed"); + return Ok(NetworkResult::no_connection(io::Error::new( + io::ErrorKind::ConnectionReset, + "WS stream closed", + ))); } }; // tracing::Span::current().record("network_result", &tracing::field::display(&out)); @@ -126,7 +134,7 @@ impl WebsocketProtocolHandler { let fut = SendWrapper::new(timeout(timeout_ms, async move { WsMeta::connect(request, None).await.map_err(to_io) })); - + let (wsmeta, wsio) = network_result_try!(network_result_try!(fut .await .into_network_result()) diff --git a/veilid-core/src/routing_table/route_spec_store.rs b/veilid-core/src/routing_table/route_spec_store.rs index 4dea0c30..5f7d1b64 100644 --- a/veilid-core/src/routing_table/route_spec_store.rs +++ b/veilid-core/src/routing_table/route_spec_store.rs @@ -1087,7 +1087,7 @@ impl RouteSpecStore { && detail.1.sequencing >= sequencing && detail.1.hops.len() >= min_hop_count && detail.1.hops.len() <= max_hop_count - && detail.1.directions.is_subset(directions) + && detail.1.directions.is_superset(directions) && !detail.1.published && !detail.1.stats.needs_testing(cur_ts) { @@ -1742,6 +1742,11 @@ impl RouteSpecStore { F: FnOnce(&mut RouteStats) -> R, { let inner = &mut *self.inner.lock(); + + // Check for stub route + if *key == self.unlocked_inner.routing_table.node_id() { + return None; + } // Check for local route if let Some(rsd) = Self::detail_mut(inner, key) { return Some(f(&mut rsd.stats)); diff --git a/veilid-core/src/veilid_api/debug.rs b/veilid-core/src/veilid_api/debug.rs index 626f2860..23aca13e 100644 --- a/veilid-core/src/veilid_api/debug.rs +++ b/veilid-core/src/veilid_api/debug.rs @@ -701,10 +701,17 @@ impl VeilidAPI { let rss = routing_table.route_spec_store(); let routes = rss.list_allocated_routes(|k, _| Some(*k)); - let mut out = format!("Routes: (count = {}):\n", routes.len()); + let mut out = format!("Allocated Routes: (count = {}):\n", routes.len()); for r in routes { out.push_str(&format!("{}\n", r.encode())); } + + let remote_routes = rss.list_remote_routes(|k, _| Some(*k)); + let mut out = format!("Remote Routes: (count = {}):\n", remote_routes.len()); + for r in remote_routes { + out.push_str(&format!("{}\n", r.encode())); + } + Ok(out) } async fn debug_route_import(&self, args: Vec) -> Result { @@ -858,9 +865,9 @@ impl VeilidAPI { Ok(">>> Unknown command\n".to_owned()) } }; - if let Ok(res) = &res { - debug!("{}", res); - } + // if let Ok(res) = &res { + // debug!("{}", res); + // } res } } diff --git a/veilid-flutter/example/fonts/FiraCode-VF.ttf b/veilid-flutter/example/fonts/FiraCode-VF.ttf new file mode 100644 index 0000000000000000000000000000000000000000..fd5941396c7e664e51f06012e8f517321bfd9f4a GIT binary patch literal 259912 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 o59_YOl9`!1kqxb?IufE#jlDVFq~g7 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⪻@oQ{e*}_y4taJr;hvU^!Cwbmdo@ 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�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ⅇRLpLh$uh-+c2KFd{$vmSlN`4^L6rXM2A57F(D?1;A;Huf(EhECv#x zxKU$~_{Jm-4MEN1S_LH+l`w~RV*}m&xce4v-b0+$VDv|B_=!_z&YiuQ6_O7z+LeFx z^yyP4PpnYP1V@PMQQTh)I^baC-1SLaE=?41;TsUK+1}uIDx8ZJRM?AMMU?uYI-AuooogY#P zt){0m>*mcH*?E(v%}z;92*;9A*s;_F_im?WJjkyi-?yajL2hnN5uNB8K65ehYZoPj z`MPk&;3ppAx{@%YLZS5b^`8XVlu6(*h?*7^3oUkTT4} zI-xi!A}lP#7iGW637&z0ll(mC#M(Z-D-c#LoZi}p7Y_q>a_D@1ZA~4PQhi-5p48RV zHxw6-IGMW(TUuJ{TN}W5P*BjL#f_;ftt>AID=E3IhY8uz-${0op5Ye!VHh1MmJ07b zD5?aXdEU(nY-|xNE#<}4)D2Z)4Jd*bLYdqPBjhDV7;(6@hR+w9#s422v5HNsEFL>VCXGLP9jBn6K5>JAI~kE=@A_0;39XH z!(}QMUje)uvSB=YskN%a?Cin@FjablK=lvt5Hqw5h1oVXVo-Kj382OSII&@i1X3Z< zkTk)QAg|=6W}xGe65?W~g!_glp*u2_k#TX;5|VD*&c2sj*bJd-Gzi2=2?=qivSDjI z!8a@-BG?=Eu;lIqWT#!YQ(W7r=V9LS#@cI}`yqP=TWU*DUn488qPqHFW_}qUzSWI& zwbj+Qzhxy@LCsxF4NYNCWy+z?)RWte8(&NCVr50e)ddeK%d1OCu4-*l4t`dEOW1l3 zLFwx%6H>$NI1$B@1U9Z7UQCb>33&`CIKT}{FkN+nz5TeOW5%u?giD6TMHp6u`x_ct zxEUGJ)K!oR$X-N>yL<8gz(C0#0X<7QY69XLDHCMZKYs%EJ7C@SL?LpT6u)@oV^6MoE;Bo? zu()B|`o^Z>!n}K#e7k-p>?ZPVcmKBLLe%Is$jGZQQ*UKK{=*t+d+gk&o>;j!eiG@& zXFkpOuCwr$1J<4V8C?8xyia$Zx_J5K?W?KyuB0V{zu|2FU(gi0JF)iIk<(IAQnGH{ z%Di9Lp=CMRD(v+QW$@^^pz;NyKj-)gWNr68s`J{2I&BXT`nru=ed2p-MhOZAiq)Z6 zJcWmGGj9}wPx#^IGm*;hESElTqeXxHlN2ZhuRt-_%=rQgBL_f6{I>m9P(gn9=Ipf# z7b_|X?)-ipV83rt%>Cv(^DkCntu|F!BJ?4z8B5-H=e@T#KerP8mYO;&wm~)-0QPWG zeN|lz4nDTwrMKUE=dEX!%<#hb)VH6_b0Tj&C zsVS2Q{FZIpxN((tP(WctQ+xXW!=Aiq&l+j1Yp$;-cS@WA+0ufMeb z#IIIF1l)$-pjz@yWa@s4?BZ>*td2F<;)G65A88;%vZ_b7{;rGf_B@!jlV(BUf1vj$9 zaWD_tvWpYSehNk*0jo!OjxGu(7aYtVbd-w360fj=E}YN!`apIU*|LTcK?eOGGc&Ks z#@W@yQ9N#am(bA#MPl9Urr2YXvxzsV$62mQDOXRpg{yl;S+*{oD6rjMnVz1N%d2Q1 z9kt!XknM^&l~vKh_Y3n2W2!6jyQE&;Sli*Da#)0nM2KmBI~jhuE5lZ8{masXNse-R zdF;wfA7IE>q0zHmOrmx&Hhho@Hz8>sqYTCuNOFb?ln-CS>l-j+^N`lcf99EI-g)W8 zO&gy}fuklH4%P42@2-pYTbP3JCdA2U+-F|^sqyM0+(eg+z`d^DviY&;(?TW%gvJw9 zV!6lc&F^FQjzY8nm)~0u0$wMJUHkP@r%ug|j|dC(V8hi}%Tk7hMa0j>!CT#SlumDS z2`%Ww`4p+{{eEJ4E5@GDTb+C5+?i9q9GVsv6&~uzwZ73aG(2kRw6OvEe(ZPM&TVya zWq7#1oI{8lYlm450jT`wD|IbOKl%H)E4kIApT-+>Vvay>!UP7(f7g&sKiFd3{aZUg zPu-$vg38!8sD{BkGCJH^RnvkxP`a^^-sZ~MgST&7KJnw>eY+BO?b^L}|DNyn{_w-k zhxh;R!w)BZL|EmoD7Nzlk`(-Mm}(p{3?z-^vEsudJ}3 zxJ-cCd;i{*6Gy0>ScSMM8@CSNt_>QI2Zjx7j3c8K9&9{kt{G50-6#$!1zH10>4rMo z5Ibg22~#{IbRr+rU||1owE>t%J8EXbi{QkyjB0^J>s2F`Y&NTFEnJ=jT&E0CUJfG*_gYc)DlMT&~dDGx0o9rU!YJ70+#KqGOE4$TYH*RJfJ$m%SDai9%4SN8& zKbQLu&ir$??;hLn#ZSLp&c2(Mb!P9Eq=?-n*3IC~T-uv#@ zjy<4VJ-Bzr52w#oRM!^g7Q-2HG50s{7J^vGA_BDs+)5$NQ6)o~5lxRIDlsv!4Cu<< zQEQECfxZ`-R~ZgQN_*5Jn&HOcDCe|9%d6|-U5Ip-x%o#=Nl2O;(AkFf=(teL{RO(- zt`5wGft)@Fyy1InDWTmQ*7F?6t4;#*P?v2}OOM7bFga;jOrSgI$L$Z!uj{ae36UM7i8WplN{Si@;mGTHqoH3t4Bw}i)wYin+6dW8Hf;ZEcd5q8(k|9;m7>rwwTsUq& zl`|=*b{1vK*4g1>HJNv-MlIBM{yx=L7lWToqP`Hlzj=n;xP{{O-Te-T>GiA%@iP$I z+iYK$%w`BDCCv!vzIozwejC$9xZPeZ_l--6lH;n4@~EUZWQ8jiq#ywFD*`lslXCCx z{O-pblP7i?MeI{slBgrLOD8oyxRlZ3AXnITXIy$ zP6`Mr4x@|h%=rozI*|us0ua7YCI^`ZB0djLZu6*mXcU^1ZWQ^;CNA+QGT5eZ_3*IH zU^a0%Jh82v4Ht>mV1zW-P-X^KEa3=1TMm*MCJUaR+y?~}jm^01`sT)pf(OM}Ss9sG zS$7M|8X6m_%Zdu`=VabEfA(^EM)uvzoPy%}g~bIqnRoA}-@J&*&Y3wj#YJV+IJGD@ zBP%Nt|LN#}z(6lo81;dXn7C8I111G(ZvTGzY<*i-dHT&ucQrwi`2D?_5j*o}V_{)Q zD?ez`{MVAP;n67Rw_(MiMQhf*xM5-7x)g+WeuRMkmifZVTV_GNA3|X37`2PFZW6Uz z<~VLZj-nzp)z_P+jZgZEz zn>xC{fj2rhpraa8iQ(ZPkuw%7ftERU$->0Qka!<&FXUXfO-@>}Y}vfT_*nlyZwDS* z?Cv+o$3HY?s(62<_(=d3^8iGbg~j@N z+;W|qb~{--nG`(ra*x~C(K#?Yj0^ENhkAzPp04WVA&bLm86EDct?2YHu( z*{Ua=UMf#d2oDJj4^N&tdsb#zVp0Z^B~qz$#8;H@NC=WEk|-8Dw|w#xnUJfEjam5A zJcTGRGghUtOO%mmXpESy5cO6z4fL^<8jVme+A*k9)Lg1EkNT<m}g|tG#o?BK6nCrufucI}XN&^A(NVZS9qXx2e|q2#r&agBKtNcR>#R206G3a&Q;q z;4a8P_MzXt(JzFf_L?`;_g80`w<7i5{C4Qr^;*LKF&O*$d#m%0-2#U+svimteUC8U z$6ugv$YLzJm*6^)u?D#ru#J%+J&53y$wErzM;mWJ5I(NYDl5BPh(e-@9tU>25F8jD zwh+&py%l&DE>e%l*wxrT2NQWV|Dg*}zS%qim zhVOIl{mAoC{PD&*XVkRmQv^77>D0{WQO>%mqJn1HFLg?^%#CccQ!*(HP|%hex2l;e z4o4_cDP<(Cq)psLvk=G%J@H-7E@18kZhA#er|PvQ;_i-(7dgd}y? z;y@{J77!97NMS_bbZu>2Ep89K*4DQ4cw-l%qdl!nEp08B()y;>w$`TB9wVs!t2+jV zt)dW|T8L0&9X58>Huai}3_bOE(tLIg^%d{G-o*61`4MT4vYXl#`TSI6S1l+Dal<$O z8g?sV(U}zykmxCMVQMZV-8~+D{eFbi=fk&Ng0(sgzsvPDIiYm1)H=N|EG#MkmhmKC zQ63KnXQ@=k&Pzj=WxSUV6&AB_?Q@GJCt|0@r!Ri~g@rLeK1#KE`V&tt3T9zOIH8N4 zdwQluC07O_CFk^T`5YTA-NEK_@rpy)C!Ab-B-dqw;(*J92^uha2+5k7a2$jsBhx;8 zN-9F0bZM!$v;>_G3k%Cz`Y0Fjuko;XQ&J*zLJ|(~WFq)By2UakGf2CJ z#!E~TC#FuGh74y)mHjLshz^A? zQy)5(U)>9lhsS`SCjZ!967nzENRlG2dER2{zacF{Ale!mYwyCVXc^&Qi)!T9+Hwt^ z@kd+m?%mqPgy}1vd+FInRzLm9>o2|j`fD$}`nRVZTK(*cPc54g>ra{bN3=HEP;XBU z9QB^AF4Kfva|3e_|-}RpYF$a|UB2N})Bb=aZgg7R{JN_xYtplDUg9Afe`EU*rAsFI^ zl!&-+(^OFlj7t3N6YVAxm1&wxH=c-O9*6-sa^^w*|K@_ z@gBKcVK}~d^Oll;mvZm@h}ulAPK4a${x7-91)ukS%Uy0rh`;(Ti7Vu|?~7qLX!m5T z1DrJE@jvA<$NM3qHJO75Z?qs45UGw8AXBPf5)cWEzena_u&1R4KHJ@LL`dDh%DbtZT(-eJPWeC8zIO4mgWEY*K4c6 zh__%F!dLUA$0Feq8I6pRKN}K>?F;B!R5&VlVyEOFFt=!4wl+Kap;!JjP1aJ;W@cf= zNZPMOHtp#g1TKD{HuK(neeK${ue?VV)A!V8tf#0~NWLsC6>;hKUinmk z%nDKpxO}6jzqhlabJQtQt9|4`He&yBAGKQQ7{$B2{U#_1v-VCdLCCN7SB#WS}Ovnxo9s5QSvNO^YPHCm6L+R`YfR?EW|$`DiRsXpuo^@XDw zsPvvcZ{Ct+%a<)&G-vjltXVT>&Ym?ZD+|vTEyTM^=FNjG-g4q_VQnuIT~A%%krU0L zw*3mk`DtH}v+QpAH?G;A`!?eY32prlJ1!xyjqte}3fJ zEy#k~R}Ou)e*3xGwRP2KwkeP+q;7-@>S`O>fK_U5sI993hFq$U3%K-sYkncD`6r+y zNgd#PXvz7|lB5ohWN1SF*!4L`@3-N^TCkWKF8=o0Vf4hz2PWkD)$13J{04>fwmzi4 zf5_&Z;9x3zR7#$?ztWbneEISRa^po6V2nF>7Il&zc(Rdzx#Jx+0K7gFpCHk8=i!wr z123aEasko=`@Ewb`r$41VV{-M9JiT#cJGjz65*qDa`c|+_fbR;j6U{bBc^HCm(j}I zc341mhY&a+cmbf1_Ml?`Cl4MxWtpHEJ%_D{le!3g)rp?b(Yz+W1&SL?tVk51PYPgA z%BcmM89h!kMA9mJVQH#^l%yI?D;3Zj2^Faav91o00@5+wIAXSoeW&;vVU^siY8_C* zszU{C2uCi0$--fyMKfZ#Y`_7X6ps&DHx}D5j9tut$jt-VCwZRQX&ExRSbnBIQC;`h zpH=(^k;qPSb3x=H;3nP6T1#DbUd8y4ICOJx*l8#aynz|JPpm}uxYBvBt!Tx zR4e8ULE##QnDxOf;v}(HJb2(pZ=u5&P%vCJJQBA^ArlLk5QoDl4dH;V3l#|x3EN>& z%tb4$$ENw(uOU%>pz8jrcm=ECU95`bSQSrVRXl-Ju?(w%RENsHJWtl;t}j2?jTLuf z^S8N(6nZN|X+7W~|3>V}JERxO%%b?AouB*(h{Opjl{Eca1Z>DSM&03WVm2_JY%gkO zH0_1kK4F+mn;wC+7#!_~FCDZ zgUp_HAk&tJtkrYE-#GKSJv#K0x)@&P?Y-Y^{Tw|2 z-jmM2-G;S^kh(l=@iSmeWLR7?N-DSl4{o-NZx-OBoK80bY1dpm=%fU87;+PgF+N-wU1$9R|h2^3p91^@&%Z)%qK*Xz(Nremx45ZrfZ=1%s2JZpO^`W zGzCon<|3d&#_OI{{I}nG=iN8gP7P!)g`R==KzjYlF%RWR{ogFn7cB90MVOcmd zBOf<9i48uc^x%R73O*wi3xskTQvAdd-5^0?C1-9&bz$M1ik=ZhJK9@cT6n#>JruJl z5($txR0PLO$zfvuY_CD$@AmPXXJJ{K|9Q9(rgMD<;%Nm1PC?|<{C2x3h%pj`eQ2kh| zZ)2@~kF~lLYxNVX)$LfTDOX&5&2>l_RF>Q+IE=^f+vv>J)o->}okUjR$m#P213x}k zE3-DCw{YWy-?q+0i26hCJ@^-EL*B-@h;F|CJN_NdzeS(_^wX~02T$EFAhtg}#$)Ul zl82-ci2j1i03R$MOM7cK7L8LI5g8Q|(=vv=>w+O?6NHEyPFK{*L?R(0*33iP?sJ?W z86Wfhz91OyIvKijY#vVk6lk7*A%pdfe=BJWLTXdUc!dAUAntDB=&5-K4<{i#O~!Km zFL!csY)5x(T!Bc*Y6ROCAVHmhS`;YAOI=Ay?g@#BLf425DC<&B^#t&v`8+PFtrh) z#j4f~SPdU@9fKWRwDV)34a})*_CRI7D-R4jSB{|d`pHX86L}E5S?a-0e!T{*Rb0_J zf^{oW*h%-6-r_?=%~%b$_iPy*&EK$P`){`lH}gx;qe~j(uaNnL#HGwwx-fHEv_b#} z%tyf*rF!+#MSkbd^wiYV3g4r;)h5I>g(b#^A_F6|m>N!hvpGTxqldC1&D3mJz9KHS z{=lJMe%gO(@3-IV&#&r(TPzF+PsmvE^wUe1&&}Ya#3cZw&SkV6!svN9;+BGa@KL|o zUfB%q&moob^ZdtqI|kj(!DL@@-Q;0wafyZe&U5YVJkjvQ&07J5{dP~OC6Dr9IHO!2 zmLretcG$t80xWGF#{;vDoyW4;rTTDLWMphYY(!#oY=pl?&O;s#%n8H&eS_V7J^lTL zKI72X2Cd8{m>Bg4RbHf*YJ3QG!|;Ry?Umj3aSJ;zk;i5NaE-J!JXELE#7qw(4PN;hOGG+D z2;|Cz(qUX3YXDWC|3IYkEqbGuU*>J)wi6YO39+#0bZ{@c<60i|g?^gAZ0Z3TXvksp zNU+#+8U>s}g-^gx9ddW)tE>TV0#;88;2WxUzNckAz*gG3H*K6 z*<(n|Nw@6$aqBmSZg#+`uvTB1k1Lfz1d!qI1OPwp(4(m+i>!Y0VCOh7+!XW(vP#}+ zSM|vOX&-n|26$ou*L*xH+Q)5MN*zGu4jEGPvTde@X+DMkahf_hJ&56Gl4d^p0mF|E zo0K>$CsCeKL)pHV&+rX~fQEv+@E^Dn$A&NO=^R zP7fMJxn7z(KLK}Q|rC|gTaMU0XfcHFM%8Xn_e&g=-GlzHIAn-f1r{!|Wa#2)$)|t)?)MWXcmHy!W{{5#x32yd(@D&BA?4Iho9OL8Z{^0O zc2s9$B01oh#A!>kHP^Om*}UySj~l*bhOfZZ+p!JDp@fy)@b|jP{NxOW?;G{VNB1m_ zgOCCxkOJ>Q3haUu_!&}Q1Ej!rkOGSR`+xX+to_!dgWrC;efyW6{fb1Jw|Y5n+xbmc zb32axxqrW40vV0seMl95`q{@{!|>jigQCYLJuk@g<;PImUb5p7^it^&$dt8uk=)Tz z8Lou54NUX$=JE6!Hf=pz(8h2IzS+pEpAX|?tw)cq-i(QS)9y>R%R8_u$|^4G-?nLU zF1W$Tc$@b*T&AMkOb*P76`nK(w9ga6E8TYNT>{uT>fVL11LGGwFG1DnLGLJZcM6KJ z`nm?kIh@5*1Gs>^$zpUzIf~joGQMCXb%8i!s&`e!%|Cv`Fu*)HVTp5v=4|Y%rwM4I8xN_~waT&nj zCIvpTcHPU*ugOUbVECzztX;S6h3Ed23;*RucrcqK|6IFhdWsI~HD$_@b@QQr_EGP6 zVLsnb8+h*kS+!v8`#A{menahNP&ms|9lcuBY6A031gD3FbeX7%fF4nfPq^J}BamRi zc$8?&OiM~aR3+zc*~CwhK3~!ciC4ZX0jeY{lM&ULxQZLK5F;gLvvJ!$;r;!cw_jR3 z4PtrP+IQZ4e;&fd+tEp3D^75|;-Amt#)Jit20uZ5@wrdG`))3Hoyge1`v5VL8IQl4 z6KAV6{-K?@Yo(qeb z+c8KaKrf^2>8!caLF#B4y6%=1-7KkSh6|$XuDX4rsHE)D@iUh|4Gu`EJI}}E9B69l zYQO@jYMYn)zJZJ)tsrQxy^lu+4Aji>QS7yXSMlXo0bgw-K8D!rm`QleJeoPu-I+zd^#3 zLBhOuU&8FZ@6(VRt9p32qvi}TZ_k}080o^h`Fr5Yejxht(B&!wuPnE|+mZ{o&LZsj z*HxSMoc0)tfBSLU$DeKAx97|4M{ZYS!@>Db`RCyShfg2<6@8`-U%J{nJWOrS2l|Pj z6`ZzH=dru&+EkRFE=|xx!1eZXtxC+(vP3Mjd7jY5X_UIaX{+NX=((W*1NadXmDekY z?=#|{Y_?z1$)h)#2d!9_TJxP7XV3md zD!GC|w{F}#_r+XHU8Hv`z>Tq_4-GHKuW~{gfFq5kHv2ZbsSJKx5&;c-T6$sj~zZ*zF_WiKM})3sFK- zP8qzzMr-Lf>bT;{k4GCJF~%n-g$|da7BioqU{07PWC=t*;qf7e6*1wmkf_*z@DLd_ zI%e$~usG4X$m!ws zl_zw2%#QJqwz5uJo&*S#<^JJ*atJ5^`_rtBj)6ghl3XC@^k5rUS~`YAp-N!KuvFWe zf&TvLz^r*uI@6FiBs3x>IwCkCIaS)#mYyzk+TG5HaS`@af4{A(xdXw4sm%lwK1SuF z0xCMxR0T}qh5T}Y{i^P{RtIIz)+yESLM%N)#=f4q8>3;U0Ei9t2iiyB@K;C(T@Tx3 zG~hmWH8J{Nu9%d_gvbB_al37_CS&Czi<3eH*cL;&oF#A0p}vj!Xhoo-huK-s+A?HA z0URqlDo`PN{q@wO^jM9I*x62xz-HaR8}~rH$^GU6lN8X|0+byeIbVqU5d4=?lx+iMar)HBOLyvI{NAD~XMWrH?N{F& zIMYhXORde+X8n{wfP74YSa5FPd{P#KG2hEOC_3lfp#ho(&kY zpMryw!JB#E*3q&O7+obzHd$aQ?ox=O2O6uYuHG=@#7ZWG*$nwYZ_H3} zIN0^b)nj)?D)>$}LIS?%Q9&UB=i?h4<0~Dr%Qbwrz&|8%Qm`K%guU!>8wgpZXtlb~ zgvp4Dhp0N6A!CXw`Z@AwT$RcN#n;r`UEAx?!77OiQ)$`Qy|jWe?9wtUp#!AB9*2>M zal?D#;pk2iE_isDC6pp-f3q6JjuXi2fGI*aL@I-k1=E9f7!8bp#;|_sW7~Jib zECI7#xnEqWUdS#3lgI8qjto#L7-%Se^^xXrQ~}}%>juQ7nQ*6*WJvG;IS%F!o{EP;k>HMJ&K$3Y^BtI=s;Xz;n?X0|lSS1j@IS5OX% zY1k@M%VpA#2xf}Q1sY}|Vta<>t$O|S*Pog#?z#IX#8J5|9y@MH)_m1S8&UC0_ygq+C+-d*{EvS?<-g;3U$E`ZVuPW# zxAfveP_m)-8Cb-g7#R9@5WQ;wEeaWD+~2tW=^X=}XZhE#vL*}eLc>Psm(r8y6z|BO zv*~bpD!mM2Ih{@i(UXH2cHF|p{N?CCccD_LN|hWlReK0_h6sb9=%~z z3790cV%4JDoW%=}G*xSq>aZj$p^Og{q|VBnLlSDC_+5=3F(yE~jApsiAgL3VkU%uc zr`gEJ`l?F~?KKI4{MaMb(ShL&T(N)RQ*W(ilJ+z$$XT%Hk%uxP_(o%SS*u}J5tt6@ z3&-2r%}_HkZBS64+Cet^)mjdk`^iX|U%MetWU-7(LOV$epy1qIfFR$3U-g>c^UHOE zY?)Lf%)=3UJe(-NoBy3yweG?B^B#X>da%2#{o;jI=O-s2XbTRgbg8ail{6C_|8v;Y`X}{^EZTd;Vu? zoAPhTGOlHye`pA%jg2FY*3{n9E#F};{EofwEcU`i?1fF(3-4nutjAv9A1ET&vWu{) z|Dj&L6?pOQKiiJH(rHZ z*m}zlp1%i7f05pTB^sxnsaEr?qhrGp0$+((Qg$6Jf*Pv}&z)(Y0s~3arc=msOjuB{ zUQ>IQBINQpQ31pr{LN4eOqWFT<%gk&%A*^JF3b~pCb zB2SEm4Y&C_@Awt9V@o_Mc$;dVgYRx!4re|Q1yMhF$9L`@@ELpEjCY*&5T1#zz2|AO zj&c)nW~I7Yt6{9NlM-T*W-gqPovDDeI%r{!wG|yaa45gq&Gm=VD5^YqrQpor-wt0c zv#QZF8pQ7+-;f|x9_9R$Lvulyo5w*ba0lM!i6wki9$N@E@Dn{!%o^}Jutvx1_An&H z!@Svfj}lPFJJ#YD9c(RGC zUKiesp8LY$)nJlYH%lhfO+}z^)zX=nKoVsVv+pzB^KZWS<_iy?KIC)#%z*)iLL(9L z9c~}NLgq4yNrZIIOFyswRv*Emb%3ufxf{jfzxk z;IdnHD{fxdy!CuTd0ju`0(!xo-Lr4szMoEj0wEpbV-l4D&Y!#Id_E}<8uW|FMUUr@ z(HkIb{PpRZu>ejcg4FINjAm%1hu6>X(0Z;$BX*e25o&G> z?FDNL1*V?zhNeCZbS`)BIvR8~Ez5?m`$6@$WY{!tyo3|jqm)uS;?NLNj{*BOkMRc$ z7GVf-nvugoGlkyXzM*ju-)f`k2(V!KPI@fCXVgy?Ai&JW7QRxo)nEEb`zbB$Lu0Q=BBM9ay}mvcO@ z0^Qa=O00ri=;I@m@$xt>tJzI&m^$9uk9}htFc1s3f2eZ^?LIAiy z_{-HK_NDNZ!bG{J|H83%reg2-L;rUQldp&Ink z;9y7Nnz;xh&0Ev#3TF6w&Ng?2WQ?J`<5W~Ss}L4h1KiFWPb!cUO&I49e*TG*gBd_M z&5sDkUZ$Fu^(@=)5FjIH>cacZP_>24J&?H!C@q2~-sM4ZE_A1ln8c!#=KCq{dZDLj$f7;W{%U_Aem7g6OY>+RfCm-4XVj=s zveT!_x_!`;&>;#4HkO@c#96^C;hg1;4AnYW6=?fnZgf7!P08ug1_wtRVduo=DUxVC|Q?Ud#B?hRq~&zT_F{DFnvOCkUG_ zt{y58SG7Tp)84@1;;hJ5(Ej4>U-TPD?LYqFzuD^l`lSEh2xiD9xgWeo68s){L0RvU z1WWFJ0%0r_kaZGzkMACAs(9}03;`D$4FhA{4CF2O{vf{77}Y4|N_~%Z1JCvyWyN>$ zriOBo?t2m%h9^0*CHUNQd~Q0$6dH{H;&IW9uGWBr~qiBG0c~F``_u=9R~j8DHb1 z2U9JrrMDY@KHn#bTk~Gl1IuPa_TNIILG1@0NK)rajGD6QyRx((y~%SjPK&; zwCu-UeB;fRUway@$JJbgK%@$qzJU6NUVHFh@126O+6i$babwJt0!vNl^^$JJm)~%z zS{#~`61F@;I$U|+>w;5fiW-zDkX_o(J~LMqRW^&lYHcL-T^k~5swk`&W9CqKth9*v z^S#N1V}PCPrS4KkMISvjmE94p@QKO(*Bom1deZbU8;;{UK7PY(!|o{$ogReeKY03q zG^eR$NXz(Mtjs^XZ^yQ;zWaLjxf|6c=ZM8@>Mk#&KGAElvVtd1PmNcxlO=H6CCNOW zgtW|bBu_RsHDB67Lf|jMzp{{TLpkDz3qq(@7sc=spfPc3J9>gR#svu6r==bKZ&+Ee;dE|-bP=ucuG6gVY z&uDdie*UF1$B!Sc@sR#Vnt&OPFcD}dFl=aTZEWvntAKb;;G#UnKxYRlO=47U4%vkae;t$WQpkCF7m6es;LM*9er~^bg~?RGAuv;{9*7NA1SEkbUTr9^bV4LAQgq=Nm zcn^%DTgH0KkQjJ_`3qM){qoB%&k#kSz|k^Nd6tQ|R(Pknwfini#ilNN()dtMcVpSD zJH_X(UOsc=#~**(hw^hh9Ok$rXgV?ZbNGg*oTq~>)kt%c&6OlSSirvw4 z{mcDF3oBrInEOo4{qpqbE8ci<&FoaXp^Bs2`mkBE7C!p?^Up6%or?UHOf(2?l#2&& zoXduKl0)Q2%(`#c68Z9%tRww22#Yz9b>dJG0A^8N0vAOC`q)=WM^SXbBKi(R>(b~( ziZ-)oe~NA@0QDioc2-grXd~YJPn{Hcj}bm3^uEpyrK#}HdpaNF;drnS@nf^b%VBUn z<_Y#ZMxQUokpyjs`cNo=q~r++I#IzXq-IVPh;#|y#a=Hl5d55h(gALoq9er8I78B} zk0WphR2kXJGn9+g^I-x-n;IJ_c|*gXW#%FPSQgE+3^stqd%Z+M{!&8?k`VA9BO)FJTEIutt_PgXojG_Z!}cU^qdHR!?)X z5C(OD3EKN?!bUC7)2?O)*&pPf_#A2z!|Av6Q;YO03w4;` zobsH)jImf$9>e*O|0BiHv&yL53`gZrQ3ZOIo0`vXPz6to>RHpM7w#YX0UUb?j-82P zFTt@haqLVSn{)wZ?`zn-WeMWf(~!u5+XyP-$*79oh*ue&cR5@3UAVY!E~3~OU^xbQ z8H%M)@2BfJ)?HTK$4XwPNl4Wu#rrOt>^7Y3IbR0%zEje6boX4CFV6zjv()n}>+4?; zS6t(Hg}>uK`{-!<`E5A>AuaOuhg_id^BcW^R6>tEFxfvL*)JhJC_Zqk{cOYCRz4FD zfHvJpb62c+e8v2kQ7h&qMEWZf{;6{xSo0|LlHPAjo*)kh0K%uv+|xbQ-8WX-Ny%bm z(E&J#a97l-=Sc1GE|kC@rgpMkU6YXwJ@Pew-D43x>hSr0%c1rYwqB@_b9%o)RYpi+ zoQB<5+22q**wENp*Ch_fjz|o1F#$~nTW__f;&XH3=EPXz)y=n>iVB-VGn3}#QX;+I zK)@2;#6*p|@`ACha zuhb5`pV-=k`ulN@R4ajp=M6;Lhnoibh5$mFv^YE^e4@9i+R!kltnF2Vslp@FrsArC z0!VFN)nLJBXO~@?G{+|@Mim*UTacmf)Y(cK@7Xs0uWQ;y)-)DcM>y6rVYR2t3anKd z78tEGV*L_ceOfZiAW(qCMMt-K-z(754!-x~WrEBn9wy~Pvu=sI;g*nopQ1D6gk!J9VOp_c9#_#qFx=T^aGR=M9@5zI4ys<+)v z-O>k+$ueSDqxJavJMAa087RLw%R(lDktRm!s=dVoKltFJbih>8B7Hrka5Zd8b$GuM zB@zHmpd)*xK2R`mx6wkA-V1)xnS?2`tZ=*!VV_}29F@WZzxN(DAR!@9ZEZdFrP~O{ z#^~O9vSUJ%oRk#Ar84w^(y==Y9%&TxZA#GG=~{Z=#-*-usMDr#PSj*38+6l01iifQ z@Ca^aA($lA^0LC9QNyy}O^7yJZg8PDtGfl3QJ`$}XweWW81gpB=YCV^%2mfUM3rHh`$ArHhQJqwD0^EiW-pmt1nuctD!nEmn~*WgSnK>j1#$f5NfD3 z9=z1;37q$kDjvahKYFyL;QsyptLGE3fd5lR5qlhclGc-(x`A~Qw`Kz?8xy<%GARRJ zTXBt~7gRC6YN7N0zt`=0dK90CzS?to;Y0R&W$`hF6g14&(@iqGr}fI>6?isCjp*qs zlkjYU$YN(H!xltQTs^(ccK^J^ShxRM>XWW+MC!xwoa52x>E3LH^3jtGa*PW!l7(8R zr)7-{(wt<2tZ*>wcnh=&t-iwKLGse2E{0vBB8O4G`2RzI>?fS^6>p$ zzpOR5o;A3hz<_(&m?jUeUM312&)MWV=dNh{^xhoUnTekH+)wvk?ae=!gH5UMEaCi_ z|KBg@Kl33r0%mJ0ZTUNH8=axE^AQd2K-`^$xKKC(4pe;hinXhoO0OOLdCM+c?9`+r zKS|LUgw)OyN&FDl{m)mJRoI3yTUR+q{Hu*FU4pp@3I8iiW-$V2kz&OJ0?v&SifHWC zInhj@7nFPN?9s4-oV&Lju=hJBz%%Ml2S&|Ym=)=7v~yx7&xvLK^?gx5vzeztRZg&o zJ)}g^aT+Qoj0ju#G~T>P#a}(S_i$WB_KGLpe5SMVW?fysIdcJ8oi4~U_akulpRaJw zW6@lM8=Q=GEAcwkK)oak8Pl0zk_ki~Iwpp>uyRAW!%eV(i<+2tXLoVanVTJMO=>`9 zq`0@&KeLOEXdSO>x_|Hge1&ZhYc#4;JKVT(OLc|eC<>9gSauamO@O1=?=XhSz3zkhL%R6o5Ym1Q3g2KVr1jxh&l&q5R%Dub6g#;wg z3lcjab}xxu{3QYPFdjF+k{l%+Exj-3LI|trq=_ZngQE};&Z1=A*I`ijx4o?|N%VrA zin{g;eC`Ovqsiym@wp>7$_OM8`^=f0Af+G>^i1z~#Pe3x#vjjo{PDqxtw#|6{`seK z$XoY2M3vo$JCq!oH_6hj0ls3SrgjM0y6wEE6GFmhsz&m8@2+o89fX@GxZSf2PH#zb z9zm&Muy>t4_30v{pP_Af+W z{t1WtIjF2GyG^7wt^p^7XC}T{VE)FXl+~9C}L`3owd~^1%#>iebI(LD}}Rj=S~*)p(x`chP`|D*Bj@9F+L1JnT$|R zO2Ec#yJfPN`2lG#HKLw>6>0y+pM7rrYIx%d6Law!Vj$?-!uW{Yhv@1J2ywJrvp2yB z?%4k$R_UL6cm4boN-O@%pGP&)0`CyhN`4B$c74N1vPI~%l?a#$VW;c{#BDm(>AyLL zP$~R-HYRB$_mfslKzKfBg%{zziO!>+Aa^*1i;C6Dz~?=g1Jo?HtK`?M zKY)e#EziS(HOm$~{NBrprUWsxM~jwFp0Cgl>`UHjFTJ^TA!-7+f$3}BeChRh2v#1% zhd+rh$vd3Q`_7^ekK58zd}Xl@32wa|w(WVZLO_~IVhn1BYxL!@cyb+Ak*gm&_I zP9&;Ws!`Gl+wJ0KA{!VG7~-P?=uAwLqA@WC>F?Aawa=u)wCRxjC|8b+fYc9+|KqI29)aJ#DAgr?U6PsFv9 zo;`y`)h$LwVQeWncjjzqO9a>oGt%M!^o`X8N2VWaj!0XyXhCLpcnIn|g$SF1l-xHs zeCe|JNq)}e#)dA3G=4FoK7amtNmDO2j-ja}|9oOnJfYc0U@x4(keyIgbE6E`abh=(q~6`z zYRpXikLu*|x=tvNwyHa~3XsUYg1laJTlWyyr|y;%U%o-@(aZfOqqOD0$^Ks49Y4jN zQTV5or(LjaA7cQV z{O!q`U5ujZ=5FwnA1#?26X>(%fw>Sb?5Nb)j{()IyPp04{{ES1(^4k|`%9qZB>|IS zQl=pasu%zL!GwvTYgg`!suz67WLd4l{ms>I4XT^_N30DEy<;X)li_G=CTh{_Hkcq^ z+Ztnng%sic9q8f7u&@Ch8%0glfLNG`Z{5QqqeIZL4t5>+ZUsMawBQXUDjr_l5Ck3fM>->Pi@Jyt393su)(Az!~~+# zJKJ}EBkiA^m7W;pFJ}~T|Jd}bY=63>ZJ@uZzPWppiR~MlFg*;;+{@QWY7u1v>Wdt2 z1nkCxgBLH~=@}D-1p14|4Y$vOEj&_cvvxoMv{`Mk$nU>Da(mB7$f*OL7m^E>D2H4~ zwV7(NW9Xp=Vc7*69(WQ6x`#5~T8#+cYijKYjc{aZOWOi%p}|+FBZ_ON-7OJ4ni3 z;lY0Z7Qn~x{{!y!ufO`{#{(eh_kb+(PeWsy~qu*}$Y|ADlZ~c1Y zU2{?D`%lE!--60`U7y86>s{waPn&}0X}?um+1v+4#gTE3#5X*0+{2BWIW^i(!exU? zy5wdNI?IiU!cbTq5{$yj;NTz>U}o`#YOh_r3X;1DFI@!eXsM+IYvT4@LPl6yRSryO zmt7ST7Yn3i)cxaBH1`o=MvF)4rw)%adk+-jC*`nkI$Y;)H#GmshX<0<+rZ<`Q!GjU!z*?@6e5ZPuj5U`@MgnJwrvi$sv#h z$Hm77%j805fAy(LCG|aHHX)`_XtSfS#?_MtzWZb$ERPQ{A8)`OAvOu-kF(*6@An*r zpbpUgezxfP_57=sE}lDk=Iq7%TV;1|?fEM2jJbXHS>_?=DHf@P%?U{kOQLi7*F^=q@67X_OK|CT4@7VPkXnXdw(5+@Rpl&^Xka$AiNzG{`SG zRx9QT(Ad!1LP~%8JxPr2K z+sNR6fN~AAbO5yMasyiygfk1%2plGqWSEFdB4ToQILjr{Va1_DFB%@!Bv61 zDi}n)TCL760B(YkV}+H`kDvdq<50FywQ-P7xdvN-rw0D!{^>ATy+g*%>e7nV`hrU* zPaQmP{BmKzg_9@FUA}Vm@X6we#=7?2?zWcphN7E|hNcVGif$BLJKxYh*A3bvBdU5f!^XIM<6lyM?JX==V*4#5V(B0KDXdX2*RMt{I>epxa>&EY1y6YKt z0Ak!(Kdg?A7LJZd<7dWkWwjmf;b?MZ>z3``g{&Cl?ox67b^84?< z9nCs-z4-R+;-Z3lfaHM~o-`?WS`Iwxg>!SV$s;ExH+RP5SmcTb8{GZlGz_Z~aGX(T z+>AK%OZ6ToG05Fxp!{`~yO%4NtkAHCn8frMvy!+&tz8}MElu^cHPv^Eubn$_>~fz+ zj(SH8h-+mkwH(FEfm1W426BW-9ab2h0<)TMBn#pKE}($N6Ujv^4>I>-SnOsoIx6uc z3Lrrxd_l;x>C=MwVjr}cUN}>39~cIX+|b?C+0jy4TBNyh>84p6;G+!;i$bf4grxN8 zS@S8SKFe+yw{cLljmo~@Dd=Ac(3w;&rp< zgdV61psB9zp6)D%B6kAfA4i`HXyc2wZ z)zH@3($?8)1TXP~d9ba%5*ezBy1JGg(!tsv^^Xso_na=WUbzO{FFtg5!hOjrj%FKkBHG%Rq9eZp#}FE1-Cuc`wZ zX>&bcf1(<}d~F{FpUm)tkV_96`UZ!_Ccr?chJ=uE9Zt7gAp?m6AFWIW4c(ZiAqN-s z3?wwnbcqy2waP5MYZUUz&_76887tIqibd3Tp8; zXLnhK>u;2qdeMEMXXtKmlNlm(Ks$S0>|{cXo)))oj_*MAM9)OfocS?mu{bp~c0q2K zt;Z^&dMoNJLxx_%khQU@pDlLMf#LD7=~I#-f`VgG(m*n#nLK-Xs7^*&$8)(X$LN?- z0AP{`GuG3?i<%lbLfOot{SrTJcX{==RAFzgG{`B4O`W-OL=+l;^03yHmgeSOOc}5w z0x-V=0Ev=WPuBQAYi&(UZDUJ2j3x;mQr9uuiGWd4TW2@QOt2O^ItIsSu~Z_JE0p(- z2N1az27VN3q*gmBQEecB9z+^7a%+sTM-x73X3Hk zt*P-qT+bWZBhS4L@&=wDyp3ki97K2UV;-l`?CL})=xwgv=xQ{=DSunHrFh>(w~oK_ z;Ff(`4({aZE*{lak8uuc!p}IH@}2UJhDEN;a0YUu{R+#XL)RFFn_$G82w0YwT}xzV!OM0fa`>x$7Va( zHWM0K6fyj9hrlnZEMBk5H@PDjLe0oUH>AUs+veO~p8G zyr(??f%hz9V_Dls*yLH(wz5W}dCugpk(@NQu{U|j{8c%t=1+<5FuKz;;Tdqfr-u3n z%)phJMViP=Oz@*Gq5^Y0Dtwsa_t!l) zDQV`*R!;J)MKab{UwnM>)Xdqr_=McqnNySFUwC27yv#&Gm9I<6od4JhOqOKa#0_|8 z-EtGxG%hhMU-wV|J#ci#X{}xEOZ&?0+S5CZ4sc|_(ECABwhh3D)_yxDWbw1lKD#)C z(|6?$?jL_#=|k{7Yw0vke|6Jv>+~2^V_{udSo*Bktc-}M&Bd)6gEMWxltxp1CFu_@ zQinimg{UE02bL|H5h$9dgDX%s!4?H(E@QGBw0+Fj)z-^1J7^bou(ivGdulh=G}pDl zk7}+Qc31|6`&$uZZUtJ3G`QnTj1F`lr_?bpIzjQUHK_@zTc0&(=E?nnw4n}xA}rc} z*fZgRyW$!Phz^m-c-A3jls3c%(XjZ!EIr(B)xTweZoTF1dTi(KjJ9OkO!hLRtHeKJT~DA|1SC9p61bDiKQt z+^Uo;E||=ffXrnT)^L5obb_FU%1&j3sHE*~VI4|w^63b5PmrvuvNc#47LO{;QByx@ zzt+=l8l7;W{=d!S(n3$^oNQC;&9nDnkjD{&9Dqz9of$7dCftBbI18Cj1euU^8T~wN z+^D*88eebRymPY%-||l%+_ekbMh7mG6p){i_wcxW<2pR;VpDf3PQRw;P-`o~LP2hK zYvB=;tAD%yP~(9Ed$u9o^+$dKVr?VcjTD>^Hs;H{Q4yG&oSd2*6Qc3)3keij0h>lq z&nDkC()hb$m`0|aClv8nRBP)`-`7l<5u7Ito|W9WcTZT@Q%?m|6n5lsJFeHoz5EUP%BChLifGpyBoQbU`S42AFhHkzo(@jzz+qb-Kks-l*$;`HN$B zRqFN%5u%QnLD6K#M78cdi%JW?E3O>bi*9A#?)^5yg1&Z2i3VNrH9RZ}SW&~u$m zu3tQOE%%TiemtVzjhpDaaquUU)V~h?jDPulz2{&F>7_wCt+&y~ra_ag#(MKv`a(9! zA2!5%@X8}|lcdfI7yO@oK-^xATVLKOSoD~$Tz+BWkL`S40_Nd1e!CuCw?ZLD09&9? zT-x)gugVu*pF-%dQU3>F?*ZP_ zm39lO_hJ=W#l1IdQw)YsjOl&CBq5EINu4BGZu%**7U@#(T zh`KZqN^EOr>atjS8(Qr&AoHiQctSd|2BXZ_NEo{^8Q0DgGDo4+Xpxc6u3(?va|%;^ zYUjz31*_WHPMk8$Ts{jprnaAEKwYr|$#-$DiC`OK20K}&yEx1H@Ftp`1{M->H&R}d4}~L)e7mRmX@OlV}IaEN9k;z%1z- zwBtfiiI3RRI->U0DHu!`?gEhHE2LbG*iXU6vO^PSr^B^Q3lg4g+&Smas0d`4Ob^zM z!H|8Ujw9o*6Z7PP*1Pw#+IcGyStIUsxO86f6AQ3*h6Gr?nh`I>v~Ac!no%+A)-DTf z1LdZh8Ua{pF_V(n>{I7!(xa7Gy*y?##z*`ox&9{-RG{tKqzl?`cf`B%Yz_f zef^;$9h1e23FJR%d8+BRBQs`v4ZM(G;phMldy8*k{5cJg^mSv>wh@^bDWvT^NZWEq z+XhJ6dyux(khTd`J8$IXmR5BjYgcsAcpq8Y`zLQ+Iaae=D(!txSOcf_{-KuZNOK*% zc;*mkh&AckdjN#pxZ-`QaSK?;8?NPNAv%ND^R?4R4PVP?7_1$8MqXPOm(u__^|_8d zZRQ->8Pp*D!2bHP_g5fa!U~!B))$|X>X&1*OhKo5=8}JW@tHT1A`F?i^6gJP{p2qz zR`d+aL73@v>L0AN>-Qj=!fMIcvwkh9hk2D+?H?pZoXaB*0!_uNWZYw(GC3KRhs}n# zJfPY$gs&z|nLK^sc;tTrfjOB=K$YI%2N-P=`qXAnlf;J++8w=m4Wi=L?=Ed8MI2=4 zy}2P|i0D6TSxQ?E*-b_;B>G}dm65TMdY?UU<@@iy{|wIl*$Vh{y4!KQtaO;yYrsOrhn}fM!7`;9w%f0ZciRhZbnj zh}bAG3M}Lb@B2KVr~?jX$x-ScW}$JQT!qMnT+DK_$DWbb>bt}&TBuIY(s=OX(e1b+Ft{wlUzr6kT&;JS0MWjRzK{?hVs%$aV*BdQ( z0%4|C)lY~G4n=1ttir(57AEqZ%y5M^rUUGBse5*1v@};DkJG`uS_}W#*xYFqX%u9M zTkNfvr`C*F0l2~bJTZ^Q=lcb>_2ZM;0{I!`-e;^f@*=X1tet^;0_!SxK$6cNaMu2? zc~?5p5A62)hc;|nOQiJ@fxn?3W7CGsJ1*fD!nXUD_ifp-^~B9vT16V*bX(Y8t@-#R zLb8Ii#>Z<&|Br37ef}MTL_rO9K&|Gfrgf-mI(1?Vrk&z))0%Yxcf_&j?BWOUCHn|K^M&en=n931)CvWp)L6x0rkFu@ z>QqeTh%1WlJ4d;htCPo`k=MYo%6iR>0KSesE|9d>Tg!Rx_2-_No}|)q)JZE}e(7!W znb|1tXcMNs{>tiS(Dhy~)J#Zw;`!%SKRG{@;UO8;uh|=S9!@Xm)w6p_FCX5CI_@vg z#$7EV_eMr6xN))A-y$H~`%|EYc0tEkvC`z^K*ek7%|q}OFyvQX6vYT z63~gW-Zlab8@?O2n%_18RqzQ~=|;N<&aK6h`jz$QQpO{^m@6GZ!z29)9=z z58uv5X#5>u0Gb+t23~-oJTZDWv35&^TQ7Z1#>~DjF5%FHoPbw(Z&W?YcC) z`577Nx11~wRv^pC=WvFtzLIrJUpsJ))SEiL1Gm~i=0x#CY#|$q+0@+7NC}>KcMdV6 zY3oIAuYC5YrAwElXlbstql=MLC1V*tR!rfNuum-KbCP>*_?6Xb0hUFDoytX>M(^OZf>gff8qX z{lohY^YS0$Bk_`7)@W$FcJXS~y&4!!uHl_K4;?zRbJ*J7FSOZy*^I)pUo8E!3SDYt zdE)>+(P14zg)ocdqw(`KcRsr`C1uK@XQSExSSaamxYz{uKPlXwNXDMT$#d|SoJjH# z6f9vp*n|zYUmx4qJ;Y%T!O&(hhX>7ulAAZ$MlB)!XsVKZ>(>iq%{^#oHZ;^%Q+$=s z8+;!8)8Q)_6+JE&->0w|jz*!6%W7o^GYha>Gx=6ppDwkF^6^1bltUXqlyYc@s|}Y_ zUP_Hej4^ET=y`Mj-F>B%7W6c_qy)D*Nt;wk6Ag&uv}g?`e7Jv@Pt5XR(k!@8pW4vY z+1YKh5-Y>mhmNH^-OW|i`46fadiqRudv8;Rk+gqv3^@h{46q0L+iKZtF77AR*9Zft z3=m@ie~lOzn@745#e^t?xIf69jX3aOmfju{i=SUo-_TH+*U881Co%jPAg zPEU%2rjJZW3SIK%o6jv-loI83+Z-t=aGp~f=x`w%8JRT$#@4J+CM`l2#d2A21twH% z(vSy|3FuCC;^MqoTwO;scNr3}tCA(|u2Uq1h2gt9J+ll3q@}A7%A&Zj3o|qd4kJ{D z0qhj_NJ5pvepNz3hKwD!z)}?b{DOOP;M7I<@C6Su2&U@B zb+8bg>h1M}O`ke3J|f&7bcMCKS9b6I<(GXYw*Ew0Pk=3-1?Ya+erW&BZ3oYkpsh+( z{=&h$D#NW?18vwP170^fb$mjEQYjY;C4RvwCZv$j=;C=p%RItI5GUr-oMt0Epw~fQ z0wlog<^}k`lTnTnGa&1NX{+Wzv~L1I8ix9-zfK{npCc?sM_V~eE}sxsH+FhR2&~qR zBpCx*je6YTzk-7M$)Qu<&xMGaPX=R0Q~X?D+r5JxaBDvG3i``pAhz&N89-~BX3vfr zsmaf{b?w5LL%TOGKw#n>WEYphW}Qi`<{U0+bUK<2FZ7--oKk^mQw-4mm%bN0vnNal z(Zn~aLnox47O)_o1js4Qf&6@l`ot&G01Ar4gfOibW|&{%W9XUVrBD;Ts<>!EO@+I= z=ISp`1GMu5;QoI>3BkMI%bMUFGEWbXLS!_8)6WKAQ9FhW;G!1<<JV|Hzzr)x0g!(A>`#p!R=M=TIx3+Zj^3N)YT!u;FmncH`uSbaO~J6$wPUy#PxhMhL!paaX2$#Fxm4Gx;y zngQu(0A-~}p)=KG2jRUITD{>T+)yU zF%K4nOG-`3xl33+TZb`=B9Q731qGpr$VHzCtYdt>0BHtc0E^{rx^?5qrSoTw?%H=I zqZnPA!70WMn-G%vI7&Q{6UI-OJ^QgxR&%pu^p|Fj8F-AWB1ncT(t{_jpbBXqun{|q z{iMK@Mz{B2xAYj#9YAdFR8t2cM3D87A-?!p5&^!6yW4{gHdncQaNr2K{jDBv+M14OC~fa6pX z9TO261fC->&00-utx4>|CKhiI+3RH<4k z774iM^M)inZiUmuU}9>xbUDdgN!bWX+O3FCfcpy%A_>vxLnIQ&)qJA8WkGTCW?`$% zS`a0|A&Wlhat`2f*j#+T2#CtSC;iT=C<{!DCIt>7#(o>fz8F$pUqoLcwG3qVk>e+V z$YvYyOPM}<*38+fURg0KIbH(|!!)&Io)0;3;MA$hdCfJYxmmdlwx}qL@880e+9)|c zbp~KAsbrIc#K)^i>AzGNs8T`iNjPGElwD~x&%d^2&6;N?#VI@k2DiEg*r2{XaBbOa z0v`sLn}z;v6LtkBDCNz+tX{qPg*T=OaA;$>`ugoeiYj4bjL>0hkJZAOAoC|d<~Kv; z8xXT-fy{4)%x@tP;_qskkXqqz>#|GQf7v|t8Z^KMI%t4!bY5Xo zF1uxjFp70l*R{cV>*?}p0Dn+ujE;JE%HCl@LhVH#_{IjpRS2ExOFF3p&{qCI14RB? z1JDWUYLAmn>>3f>(b-W4TTq~aw`v=OQ8=4=QF643&0_kezBZ-~K%+QTV6}{~zcc9% z>fp|N(t6yh4kD?S7(e~^BcLSW|CdH+a+s@jFNC0^02E7mpOg$3A!a23bn2$nK&;aEjI1RdbNw!w{wodliJS zFz?d#r(p}fhVT9?6vPK`A!d0;5R^iNcl6=I>FA#@=o!E+kW#M;s-PfT+5e*;!p9VZ zlZ1!62A$YGLb|cNrn(UO*5-zr-`eYT>lu+8=DJiM!pD?FUF!&;_04^O@$U$0?C+=oT1oW`5vDaYr5Y1r+a|(^g z<*`|q0IAwfhHnmPS)w`2*sPERBX%*11UB8oth4nF;(t?%4Q=s;&GBUy&iz4iP%QGd zEOgN|n}=y^i4Q5@c3C0ZICQI3A6K+}H=tgp&tDwVA(eRu4_rBO)~iDvY6^IjWDT)`LQ6yBw-O~)f1^z^mRa?44}=7fXDHe*`vhBwDbW~SW#8m(cN2D zWE{a&bj7Ft9~!0CfPG{$^paRlr`#_jlrUlX5D2Xrac+HT^!2MKS-#sqh;*P{l*rdh zm>xG#F9V~5Y6HE9MqG;js$LLHUp{x*#JGe>GiO1)bagqIr40zyma<)#qG84*9>dWc z96a_Kfonk5^cearc5{109ihW_+uAFTEvRVr>Y9Tk^#hR zJDcb-d`D~i&2PW?gRZgC5b*V5kZOtAp2j{pH4N9LEDHvRp*1T@&**ehXb$Wl>jHYG zpActv_F^%??C|QHK3MLJUH?_@G}YAA);C5sfzQ7KddK5oFpWl1Lcn06&kB@Wzf+1A~aYx5qBexIg zN#pA)6H8U08ls+j#Hv{MkWs-;yae_1^tf>GPM7hG%Tb^SBJtE1DGv=>VXFIvU}p_ifv@@7VP!$g9@!`&k*cukW7kWxiVh=7zsgUqzl=5w<(wDvoo@-s*HIH74J0UDMwJIdML z+R^P|PxQ^>DuShAXMcI3p{1g_-sqgv?BdE%F8C$&59zlDZ{BZ{A%Yy#_1m%iThM&u zV`vKyBv7C8e%y1VpsYAA{qSP&F@(do&+-m8A}=3OA9J>)XIwk=D+<=XX6nD+MB?A< z{f7>0-hlY&SM;@;=OTvmE-c(H#hZ8R-=jZq_VmHsTQ_XnIS2OcKdCPmaC3CNM>Bg! zo=%7!$RwMF%XIrkeKZeV)FF(6v~MMU``}qrizO&VUESwFUcLa|ZY7YdZ(^P?A~tE% zJVw2SJ?d}{bA<`bLWMv4EkV8*tve|?`jl46{loOBq_KIRR!+KDGI=8Or<4aJ#!sJy z;EPhqqOq7*;8^QuBRpy}SzJt>TniyD)xi4zjx8oCP!$sq<}W~hCjrno61h4+>jTV^ z6mK$SW;G%7YGKot}d9qbtyaim2JcX~}upw0#n!eO&HEV{#KccE>wv8$`! zWUTG$>FaE1tZ!^mRkXIXJBCMGL&NqVD`;K&Y!=gi89iN6H8K`)EJ3jhvQQq6!{*5% zV*3UW_cr%Bl>ngx%D61FBj`aW%VNQ#38i8oCrf6t3=X3GfzvlnC_zs(6mLk z9m7L5jgSu?(e6;Hx?5U%m?DXYKP4Q=BDfYw9*2WM{ysrLA9iEvMPi=Y1zq6itZ9?P zp12NFc+OSZY@lwICGg$->EKB`g{E)oXabZnu)bdN!Sg+1v5LRIKKu(}79TOKi{ANk z9$-P6aX)^f=@_pGewc?GGwHqj5%mRgy=>J#NkgVz9!cRWNZ}4h;k%H+HITyjkisO( zSwf_6RaIF*E=d{H7->K%pyMLkN=&NM7e}6+D*Prhx1g-@LQX5>=#aImy`dy0J-fc6 z{Ko$Md-t9zFoz^e3>fMiaAVc-q$>Z2aFw0kntS2=Z-)<^r0vtCDuGO9Gia0_HnD0H z8WCOGm2#k!p;5cATe>M4sKG=aaiR^`(X!svUecw>NO+_ubY#2&^7Dc#s%<2g%bzhjwj1zXtEGRcm$Y z^!l!maAZXz4L9m}fm)@#!# z>7$GEshC;;dfmum;PrrJHVsS3-z-T_ZF)i!YW-zyv2F zB>{dxLN_q2hG7cSYBn4G%z)772oo?AbwGdyHQgb0Y%~uDlC7{yZFCytaRZP_x&Q-Z zCxIxF>i8Zs$3iTe#^UHApms<^z-zG&GzZJ^OcpIOkYX5M{Dp`(*;}pa*rpbPW5lgT zi`+)Up5PI-)>dF%N@4ld*4EWlmzUwFs+p8bb+!YW%%z54bF~zcc0fhwXj)O*OTo*q z1+`@F-@GxssVws*GX58i9Ne{i%l`D-27+6)jzYlE+;fpEx6yiOdjyX;8E%}##dghD zgrs^OUj81uDYn0Y!A*_E~4&@#i>}hD~2YD2%#r9q+6(Cdk z@jVoq#8RXRx$h|2Z;e`k7aBD7fggYov;sJcA4UIoCSIwAnW|PLJo67^_d>o}9@KvH zr!8ly4BhbSyA9Rnwr)7qflwyj!=NE*K&^|SlcuEdfDR+XdtYrA8^)`j!cXl3f#VaW z!sls|&{>wHpTXm?U=6?l8kL5@6rYk56B3Bls!>VF$Uj8M>F{n$wh;z*z+%JXNYK|d zB04;BTw+2D5|FxJzwv1=zWd>y1Go|rMnV(PC2zg`{NfprYB*QI5pdYig$L7+vU*Om zuQ$jq39~*?;orR{4F=V6#sT4HY3O`%S@do|TTdC%b7g()fghxSs4IK&r=y@qa`8x& z8Vei8O9!bQU)YAeq>VRPg2B={cf7ycgLM@?I}C}X_{oz(1pj8IZa};z7}C1|(%S{; zZGiL!Dw8$@>7A0Fe=plB=@0 zdn>V)i+;ljOEKI-#qgD!tG7sWi1Kk)q-Q*6>@)S+t;k0jn(pV_yME@-+1%oc6Y$!8 z*ngwNCqTt9(>CeW3n6{gfKO?l*(rSzcvMj|G+2ueQJn=Cb_kN?%V%h5{l$0%kEy~k zJS?J+>2uTOuY9p^9S6cmEEX`o(Qp_<+{1|C@geYorsp+txS&<)ie0WBOA&qqk^JG9 zwMu*g#Wz0zrMLN)Ur@)s=@;^J^X4B`!WRGZ(R+vp{|Hw2$JACpOqh>a-Fc`tq^Iur zB76E|vLNFy5XxKNYrKoy|HRnPY~1(29T7i%>O`4|9)?02ZS}s3156TN#A!` zc*(0?Uj<&VAeW`-KEV)L_|i_e9B0syge{H|AZOJBcR3&d%QRkv*$~MTOh_PSAAJ8N zg5b3Mt}QmO-dM13?ku2t0XUZ{;X(_UBk*gjcBc#W9c)R|9GKl`kwkjOqauLrjhc>* zIDvP^Lmlgdwj+J) zTTgn$Td+;7I&?Fo({)LJ-f;-F0`F51GmOQcS5`KeX`%q~rx6h?E^JmU-(>3RJ8l1UthBtm@p~^~2j1AW`FKuCXM1B+Nl|$( zk*%FZQ%`GiOEVOXgIIC2{-P86j^1wqrvS}oE^RlR6zLxnOdq1TAn}~c;hA&cNSX2S zZn_Vu%akC1dPh*;!<8ZjBVkJ(lKOgoN*TSbsHmpL?vla>j(cqOv3vM}mYVEKhxY8+ zbMS~5SIi;EPL*g~0AB}@QohK#2ma_p^*Ri82R4$a7g$`Lnm^D2VrbWmi--pKl2ZuFbF+tP}d)Qe0 zVgZYA($bhDneW4*4dXY)LA!Ir0rv|!M+gH7m+a_oqAVn|JTM;ZUhrTbeA0<_QGhNY z?~CFM#;|>$r>VTUYtYc#&;WEK>=B93V}}}LDCVu14PM*No{sk=J<2Y6OJ1+HXQMA%c7IxkWKz0jegM9;cB+#pRmN z2`Q6PQs<=2Ow?%9>VyT);8MO}ef>-%jSrTb4Qa@0t`Kc*Rr$n#5g}ge+p;MQaEnyN zhM_mo&=c&O=x;&Y1Lf)I>6PZ*koVKj9PC=^8;e0i;1}pXU@t5EN*tdoE~m&p0To!Y zmzZ%MhPq(6p+;zQ)YxgJU>|uO9D-TVBo(xBox`ri>+KPXANd|=Wd{s94X(;quh+fM zUYnu4c9Coa&@WA+1DNp9mXT#Cp?vGTy&Deix>aupif3f)+=zJKotsVA$4#ugnJ`N; z%gTuIDl79I?4?m$H7Be1lC7i3n!Vlh|nR(|%`X9Owv zF7-KU?T>qZM@pirHC|f{h~-vbRMs-Tcwj0#T~TAJ9&fXJAr=oey_$Wv1Z z6f-rIP-zMKNn_W~p{AC2#|zYZYiCnSbwZaJ>9;nsi{MLiT+)y?<>kM9IT!P_7Z{}N z?61H00K7s3<@o)EEqe*dk<(ECl;Z}Nkzdi)an^kN-aD(319aSQM83_^@Nj*q(@C+M z#B6giowcofR<}4X+&{>N?KX7U(L4iTW2uYl3ul)+Pc;m=DK&7BIwqST96T>9tL$If24cJp47 zQO7kv9S{m`y^hZloIZ%Z->Wd$*!A%A;A6C{U zZ2k8ej^uUdxQ%cMeJqWQ2utwz95=D*-7H>+kU<|dwl(&2nO!WIlG&7-htFmBIN_g< zaGOaw+Em$zDYSJ~ns8uf#>|ln!{8A4YIGu>oudqx8GPJBtP9_`^143bprOSRhG1@E zMP6P}H4uPOF9PsEMHAZBqTP;Gs^fAXcpT7P`cwvE*p?6r5t=Z>`1@a1PM;_;Gz}Oy z{?YUP6zgg%LQJ|Ge`=I=BQlJ*C$U@7xGD+B>xEoUcB3>!n3W zY8_XE)dVpm8bfuD!eWkw%qfJ-DTKw`0hv<>nNtUu!xV{}eFV|@GQD2}9%5=nYAv$# z1$9|j_j5YXBdJsPpJa`_7=7D{@7&4Cs=e3cti5;>oAmaM`pXE`TxM1krK1_#GH^jA z`_=t=^2ooofr9AQ!XGaHPn&b~;k}|N7>e_BQn4FVY$;Qw#MRZ+F%)reX%HPBve$n1 z&HsS6|C2}}0Stz|Y3r+$vu5cBgl3A0Yf%qP$?R>%A5-Q~UpzUyb; zRX;-z=!dg8&3blA&biIse@kK??^7S@x#09ms47mHxzaP!Gaei@>%3!=XNxz@un=*;?^^TKp9O?h3+*U- z%{y;F9ugLvg|EE-$z1S0{YtBam{UsN193KO*|zT{gebS;;k84%_wPa+{!@J_a*Nc& zWRH2tTn;w4kPQ+5G3hu@xBMpw>Ea{b#Rt4X!WPq99Hoo_u6v(gmI7#HU#>tAKI8HE zY>mcW9fBFu2vF{aY>`4J5pZdfbt)$V6}qi}Y`|Wp>^(iJS1z14KjlA(P`!*G#;;vD zdyJ-F)!$6Nd4CKrUYe19J*(0t8eyg_UbbXTBL0>q4^5mocm9%*ukZURYh?fIADcjHNsf!M`O-@L+!QWdG_M$nC$EZ z*PVLo+ve($lAfp+pUu2`qofX?)(01NG-g1=o;jOw>-r^HfKFu{6^ljHH8mYDk3F`I zjx}$;_3F!i`cJ}FFY7M3aB#=gE#K2;MK&JZedwGQDqfs%e9!)iRf^dog2!Ka_sy5) zqOm4VJAU@lFTVM=Pe0akTU#yE`yYE+QPPq6@kPXr?p&m(VOK3ATJ_arIpm-W98Gw* zJu)1~lOOcMhU!PooHBIWJyYlyfSX|Sx!wp#8r*Ovw~@K%%g?KG?iS|MLbT-{r#B(I zR9KsJ@9bG%9i?`*zrVJqxVRP(9%`VrmWb`|*lRxg=w(pR{1>qe*m&OI4c~jgbFcB4 z<75vvez)-uP~+nE`t#NG#EUs0hVcogFQW4V_(}C{yw1aKHS})CJmtjuyX7l|=;w zRbAHqzwp<~B-+H}sVU>aM(cXzlcpxe66m;88$K~*a-4jyn(nMByq{m#0>!~IcQ;oS zxsY3OR{oz>Sq2X@#s8s@MICyHk9h-^?RslQ!d&hyjK0*M`%RT5o z!d9G^7Vahtof2~-FATSnE%H?{b2sh@nU)qEl!$QhyqAM#z=mGFY~JE09w$6QVg>+Q z@o{m%xJrgN80h|YINxnMc;s?v*S~cKQg?sQ9Rxb%|hhk7I?>VurgOu ztLeIvz_u{I0hEE%j(r}mo%nRson`Hqgo5+a11vQqaELGO+Krcw0&GIRcVLd!p8TBo z^U2=;H+Kz}^HvkW_8Ww1vE_^)VXOuXQ z_#ETK6b_03LZVyRii?#1qFMk%f&u+U?zc71PEY#Vi_515>*-w7-z@x{`;GZE3~D_q zc*YZdALD+bKQj}pqqtjd6&^VGi2m&^=-=)zUmv4?qwDtQ=|2OS`2oHnXaF(ZK?B!6 zMS~t7F7gGJBP%w(M6&hZI58!!mO&dYL4b+fGS3Rsex3#CfUVtEl)h!VG578UxO!gE~;%~k6 z&rjZ1IvFf^bTGZi$C%#OV0wENDhk?b@$0`cy-fo$73DRoAC7pL-s-AyGv0g5%k;(s z)7uGsYFKE}6xfGTl9tI}hl}O2A|mBp9xS=5elnGO&#Aqf*K@`Wug}{UtbLcz< zdk%Cwmil3`NE1kJ|^hzXty>XC$qh$*!B zy6{fCn?5{<&DGgCOecQ|xk2N}{QYf+>GEhc8Tn22t=DuY|DdR(>C@$4 z+K&p7%m2vLcBihT{P@`$g&lgj4P0$Icw<~`XCWn^+U^|pa<#qt;*9YtmlCcvVOV5j z{I@r}Ty2l*c^Xy1V~8I=mVl01{)zaU@bT|0TQNQqPE6?djKdRAkUDexTW>9joK4fG zzKoUp=3ke>%24q~eK}Cj{>i8A^)s5`4AX*w@pb*CHPBF=BXJBVNML69(`Nr{=lK_YxDN*s`wXatx+4FB$r6m471 z71Zqe?K+k-eFPM3CwQP}`~I6BjzOo%I`S^<@KUsW{+Gw%pIiBeqHQK9+Lm(;c`4dn z(x+Bc-@6Omn>*PTNxsNEa@N@6BF{W-jtVAsW=7V72UTtLJ-Ca;q7v`jWZ4K(*wB>} zq2LUShdW-=Nz%2n)8~%d(J?JeOtmvP&o$ zy>MmSy4{!c`+-Q?yEy~?t9h{B;zqQ)uQc9wq*E~-;Kf zIhUP*AJx6z1HGP8updeKe8;|ZXe=2nhFfWL(8;D2JB&EU#%bH@O0u&DG0%1FgM+{G zuKj$iW_Co-xMd5cgLjQCj03wE?^evB57xZC49sgXbyC_hudn%dVa)bp_e;;G6Y4b~ zLSR*E=GR=`^!?sTV;3gZh}Jx4$c820K5Fg*uPy`q&;?TxbtL8uhyhnY(TKmKAK^@(E zD9rZ|)H#SHzn=B=7jMo8Mb|y0f=^$^Lxdu~Uz)gVm7b|qbKDD%4rBsSi9u3>!cnf; z56C?k+s*M?N-gr-^^QB9TNK07PX=;@v5F2RAb0{ID9J;J17^ok>M^XaJ7b*(JtsZo zK#P6|P5ush>wUcR$5aPMEJpp)^YnMgXX>VSLi*o5550 z^XN_`V($KdM5YHyyzYiCY)U-DVn6sEz7g7ha4ff^DJf{j913KQR;%$tXdncQYw(Z@ z=-6*VbY%ei6(|W|aq;tiUb1NJG_O+rr*xy>d$XpI~rdLFE_V3D({ zvREI;;V86=SHN*!u~;MLfKcAwN2QUoW1t=>`{25`_Phzc*xNjR(zb~s59H&(Z{n0_ zLaP=zbsk^Lm%}3U4Ns&^(}l=2Fufx+avrwxC~ww$G$Ka$EIB{_-9wd+gP54B}#);u-K61<`)ksmY zEdqcX0s>S>7HWo`ngl^O8Tmpy0s>NSki1GIB8jx{h0hA3BcsFRP_$e=SX$IRUi&D} zheqxrEi4Jr7Bi(dcP@>#hxQ};i#6|)YEXCxFMa&kKj#9Zu@9Er8um9oY&ewHrUyjF zu)QF)A;R;C4!H|&Viu}By zDsObB8}*$JDw{&$Ql?H$&`9(?3@(b2piOX=PBYiXKSZC})Kpe*>>zylgU8TmvbnkJ zVP$b)MRO}+t@YI|d^vXT)l#Gr8hS{T5yTbrmMQi|2vNUPSV z2f7K`vwOgWWyp19-#mT#Gz{XxnJ^3A=d9bbz%&)!SYKhWvvRc2Ab%tsOCHrU`u(9mu|HW9{@9LT1dkWHwMprJ0hG-v0L9|<~U zEQb3<$d5<2-mh!}cIDWWU$cv{@(Z&r9@~wMIJ=KshRiv9;Lsf8<1y$5a^Sbfj-hn# zXnHy+u}r^qh#X+63XX5vx}Ti;=;1$}oAcwLV>maYmRc9>%d;X$W3%#nBU7ft#Z5_x z9CFdHfOw+p8&{#Ovg)lt%LpIv??UYNFwlP#VhZ}Gqo=&0wY8$W$H7*s1Oku9GP3vPV>&aI zAvm;TtN|nB-EE2mnq&-AKgBDm9Z*i5*f?xC{O7@eqk1j~TNtpY_dfdQ&!~w1_0bX3 zKbiPwL~)j28bt0dPM+&r!>mdkV7 z`deFjds|!k*(`!ga1V#a#6il0`|<{>prE`$axD1$zlEd%9s0x?wM?egh&iJ`tfQ(D zct5FBr5&eEO~d7{~20%Q}_9&AOSES$Kwfg>nW=a1SnC^Ev`wh4O06dNMbk^5kAJo)$G`U7S?d>gXy=K4! zX;NBuTQfX|hRPDiIVZOUndQ2!sy2AU9fb`@P8u2t0|}{FTh+tj`gU@v-i}A7YVX3S zAcvEL96AE+3_4dNMb$hi=TRy_auma!PE=&Hwzk`ZzQIUYdByTr(EWdlWd(yjY#&5% zmlOcm#13{AB5n4NqWC^6{6{Cv#%}`~P z8Ts|?owx}m3lJ}Dl^GX+_RFY-@6cA6dkup8T5dHK?GHNXTqtHBmc+s6M*Qog!bZKa zt>AA@P6`r1LE=OXaWGifLX~>CG8F7=bCq1;e{hu%#JbZnSFh6MucYRA9Ny9GDMEYn zZKw@pj=e$!qg(u1Kw~c{*(%5a3Ef1Jd{7Q)q6_JU z?k>b&ic8De+YP4l>qtV0 zPu-~Ow%{w^@_@7LYHTz?w?ey`U_R6t%#aEmBH{1=gu-rWCX_-Cav!vxN{2tRUuY@5 z8OVccC5uoohX=^M>+S*1fV`z2a=b-`=rKoL*9oJyvrgvJOYh=zm?(RDWo7r1i^hlQ zl>rm>UMT6X!%>H_5Zc>O4hOP&xv3R#pS$KU$Hd%mSy(m@9(jK%2g0Ggnqn+9Y}r%oz^Y6Jk8&X-7db)du3 z^KFI%^a1DNZm#13s3?-j_$4BhHv;tcvKEVFs{0I%y1X&^<1;UvRMW zdUkcYKE9{+srhjl3FZ-3=cWk*V`3B{vK&PU5_6OB$;*5haT}t?{KoIGyANS^e}dh; z2D|$o*xjFCcYliAE!%tS{1t!?_W#gQgdcZuGLb*}Ouh%t(|7CI3&_u=u7-j$`v^7m zCkUUe;s0>zW>rzu!<+jz%!QNpI_#J=tlg*X(a!5QyLKJCIg9`e%Wr%Dm(nxqZzO8; zbZsI6j{dtG-G#Z%!6CLHA(<&+27&f5R1=c0^o4QBA0VbL}+YjeBrDsyk?!r{*B`I(fGW^0@F|72jcTGZUsPT=qn&T;N61c3S$xm=l57*aLl$C*q$KeKPhPARB{#r@D*+D*-w6x~>OZ7~#zm&C}X{eD4 z^g%*-L!-+!Xc!p=%ZS8>#`MTQ+U^6RG$P7R>#Lt};_L%dWxD$sCPiUReQ1rD-yK4n z=*oegnuH`BzkhBvUUkHt$Bd;h)%nYv{)d(m);d z=HvsXj-S3;d>b~!jm<+vu*!;0q5`0{vbFZkNk-rO8=Y-VXW6Cn?2517O}`Hx@P7J@ z`>IfazkVL)`JB!RB}F68`<&df<2W|{j&0jcUhZqZxNYNX>McgbQE13%Iy$lmyxJ4q_bf9HJ&ul2wU!YCd2n|N zu&7NP1qB@@Jb`P}>Fe7`iWvMNr9se3ftujNabarr{3&6+%aWJhm{x=#)`lFt*ZF$oy>a`Tv#5@29p|SDHY-`qb!KPQ9Qxb zGx_c9e7&@&sGGj^EF#c1&))26&qkl3eS?HbELxtVs^pg+k-aS?Sdm4l@&<+f zD50+zt*p$?|L&WImE6e6KJ+xBdy|tTmww&5_u8!~dbTS1`Yq5*)3wt;D#cm9b??D5 z#a()KSMlXz$ADq~n)-o!k-JwxI2UDAcdy2^Fc|^$nLX+H3yEY)1Az}fBOvSg!+~=DHN7WXp*%j z{Tzp5>-!njfU2JpGkMu8eB!dQvb$Gz?7LPbO0Y{%uR=%if!CgQeCx(dXU;FuvxBGa z+B-j5PY++5h8Xe(Z2g9x_up?P6mCa1Z{3!LjN+g8D^?VC5lgY*N*d(F*`zxccWl{t zAYFt7iqZ~1Eo*AjphCj`YIOv}!4Nwlf<1&M&_9ymVhNZp2imJ0eGqcp#_piO;hW@Xl7AMhY^JAWXD@Ng@htxWB^TCfQK9!G)UMk6v4B`vxiK*hJn;sNP>*u zpTig&R9;?M?I)`(XNZl2O2gRK*KMR|1AQj*Kr^jaM@JTdC)GxbPf3Z404JTJFFdRr z=Afl@>740PCdEVNq)PpxCQO*JU};&I%M4l3J;dL)7cWgrhz(}Iyz@bIA!A4&7=nc; z7Q+Z6u>_|L_nDvqv6`3+m5QNH_4OU?czg=7V{ubrDSE)TS5DnGS;0Wp%n17VWO0qj zY9FOze_N`aOu;@I=i?R&L;b>nrJSecMh88&;A!@=k44F4z62z(NmAR0FVa}6&^+@Z zpYN+AvgN2!&M$w^I{-VoSBfsR&%dCSwq*P2Xkr~5xd@&rY3Z}CzPez}LkOn2*|fEc zFCRiKb>*QKXaAZ3nX242Fnu645H@?k?HA z8|Wg`a7Iij*Kx`xz4X_YCROPK?NyTk16{$x!EV|jXuXM^X3xb(Bga$3?F8afsqEUn z5ETr|aEtOE4JSt&N(}5u(n;nsCMv|xG43-wf5WRjp+b>dXw<=-lP0fhHR^@t=4L|> zs!D8&(H`sTxCf`5h4~@MHTfVhYWWQjBlofLipc8N$C9t zcduT(n)%>Ah2+loXr)>H}kpk=#VPT??C#QIWdQvV8Z?W03`UjXal92PF!3UifZt%TL|{L2c$Ld-}t_P12(TQ4+KPD@Ee50F_Y(`t}xt(g#je1ymDONa_ZzC)r%QIrl?bFo;% z`B=jRSi|L5!+BW4g(QknTzo(K;R8F@?e4jslMffQqQwXihOQW32(KPJl82D}{rY-x zcj_B*$id;}q6fv*m#^G8_2XI*UDR2fjiCF#E?xR{&qh75a^3|j<7Mhg;c1f0x_!GS zzx4P|bMPt#<9HuzwRt+FOi3cI8qKY%uwx4cL{nb-^wUrOC)WLcPuL}JrHP5CCCs|q z-++*Nn=Adqu3fu++;Z_+QA2G%*~%RYrYFwBI}B9dym|V?KpPrhn|WY>O$DswBgp$V zdAl}eI3Wv6`^^A`XhkN>ndkXzpe?_nH3kOnFS zRvSTZ2g2MEdVchL?s>+u!ZX7Y@o2<)Vm*^RaUPjR;DH~7G3=S-+33ke`j52vCS#%J zUC W_;M43EF(IqcR1ol+YH|9s-O(3%EloB_Txa+w#{{r^e>O4+isG`On9!KPXe zWOV{2vII5#V|7dmP@@!#uN#M1_)6A-AaUj9=)|8Yo{dCwO~gd&#}R8e>W zZO&s7Za@}?4bj6H@)u#ziqTOB4>n7T1Cc+IFXD5QN*`5tjM))E=SYam1vPw7c=WjO zUiO%<_Y3{1SJ4E!sx;p*Ln>UNQ`>59J+=_aG3BlCrhgVJ@ zeU^3Y5+Xjo{-kze51SZ6Bn{?d5m7X%xq`&;z2*kM`C8JD4*Do!{ZBgqUp#T>$iDNX z^`N(HyS01QUc?TMpEz|S7j4%1vwoo6(FOGPw%`_3cLw^xEeP_lRbPdF^MA=dCQy#; zNAZb$eHB&MXhpZjO#|`elh3SNk(wMKz|I{WI&*y6Pk2Ys-QRxt2`q>W-2(<&^}~wN zCg+e5;i-ydjAg>GrK$?NgR_>%g8{z@V9#2C)YV%rr%jJk2g&ihgnqa4sxt8oUq)AE zCds#&`;B0GVks5<1}ika-8_IR7yBC_p$A6VO!!iRwLSO+I^7K7z|_`@(kI)HqqVO1 zA!9UR*yfeAJ?&-Xb>(H%rFjqP@diA)+PfR85#DQc0Kgc)=Yq`))UquE5Z7VhC}Xjy z{(F*h{Wz9b3<2KW;WR=2nC$4?R#|!fUUgk#L%oq`z)^{>*bnb84J^-H`jioqgMXe2Yh{osXr*XL+Jhj$Li3h~e8K9V6wM*V*M#S;yU*iW=O>q|+{?DWMex7vn4K*{0@Gm}$B;1zvt@qche-b769 z64r`elDU9FGx5v7F9-bNtf*J=I!fAJ1|{p$(e{Dzi=PpYIJ|!kCd%Fegjeb3Pwq!# z;Le|)!%P1HSLRW)XHmOwC&`MVY}LALXOUPCi;l#FYKADSs?#pP8A>H|&;Gdhz zaQhoiZvK||G>>9~I^9s1qzv}oF1^wWYt?+|T1}Tvo}>|htHWFmP;Q;o?xRH7R#Fwx z(!dg9!4$KZ-frBnHah~styY%QL73Ir8cmIr*oIX$8q*FOIC$hJT#B=oF2hzpv6br68uD;CSK{CzvBVn(!%ov2?@}Bi&H{%2^6WI%#BNiagWsiUm+0^ z1Yls$#SzIBYK@9~7Mm}S#uhXOgqb>wY+aVP55@W&fp$Nsq6$GOskX7JpDE@AIh>G# zhLD9YN<-qO3Ka?!N@5`0$0T(*4nk??BGsozIPyez9&t;=)}d^Nbp!!*pM@By#?s$c z9=TCl*VNl9?-_ump_k0Z-l2Z0kLooPUT5ZJC?si#xRLryrDnt^_VulsOTs!S5m=28 zX@fU5Z^;`2@ZOZk3LmPmb40 zu_DqTaC!I<94{=}{oPOdZqyzD0p!u@l7kz6*jpAEH-G-j8BhHE?|*-yaOV8^pBRdd!5b1Xa8+ zb)@7z9ClfHuv8G)7=ZHod$2rofVG!w-5BAZl*guVvcopH&2H@<^5RDOh6ljjM5uc6 z8SSS20SGK2%++GoF&9Qxpy=JjoA)Ie`^&cmzk|c4u3+f9{!a+ZxS9SvIp^e6$B&|ia4FZmotwpdj6-m;LTovS3iMzz{|N7Nz1Wo@2d-*i$=+P}}=fO=&K*W!<1bPH>-BGG2(r%Ds z#Ng(KVlpOR%q|uKug6>S{rU~tPD2)0d~NHY3{F#}jhH<*B1}2k%F&hHp7vW8VVUdQ zbMmpP%V533y#Cew=qUBStdftX67Kb?bI%T)YD7#ULL1(>u_9f#Q{vo|z(?K4N5A$pw);a{JP zwjo^i+nnv}l$03JJbYv_0;QxS(m6~Cq|s@mAvPS9FtCM11aMaK#pd7s_~U3v-JmhE z`ntPFKh$x24_I5yl(*P$b$m|P-3Hfg*AJy0JS9gd z;Y0WZbk9Pl<*C#(hH!ioR}#;ssnLd5SBRLFkc&is+AhqWF=@ht(dh`lOiUb;dHh;y z7f3>yno4Kp#bFjQ@v&HfS+2fw*MU^6?BymDOqw)lS{+Hu546^m6X2_{5UJ3ioLnSU za&rc|-~^~eQW#ZLXz+#kd_D|;e*BKJ?H*nVCpY3UhY0*ZtW~EKF4HR2QZ2+lV^TBo za%FT^m&G2Rh$wb)yseAMHpI%}2u*aHj6-a)=jo+O7cZJOHcM@sIDWx|jzD6zM1$QK ze_VlptT&-Rgcwh+zm(AI9;xQ!gFyfBDTC2)sB?fY*;E+|^YSLo3`7e>14VgwIh$n# z$1k(DYQ;mNQAQF>nDFI}Yt0spc4~n<$dSV`C}fJ*@%JeCOrAU;EiEQ3BP%^s?{dkK z%``kI+MMX39x+I)VY2%TTiXbf*N?al@3rTieDa~fQCj1~g3g9X47O(?Y*4@}OcT#rBgnVQ_3z|D@tI|=-9s|axTgA*iJ9?_tjjYWegAL3KxhS*pMF(Z=L zWH`hHn>Xy)f8^+yibgY3Vz!kX**Fig<~90L7_aE;VSQCu?*0T+y=Y46?Szy?COjy+qdsNe4-SF zu(1Wa4-HjUjvY9117MA|@>Bcv9XV2NE(Tce+?Ea7f7!BQ-|;OVI@q~u%a)zH_7`8h za_mp6Fv8Ybwr@DKk?<6tF?U{t?U-wz`qog9)r6X@_x4fHW7m76NqR_1JiGjn1q-L? zxo`pLs{YU_J%dg7j(J>xDt;`=?kDTzo-Wf9_%kUxrM5L}2_Hv=(>r*G%+XJxyI8;8z#<~jrbW@_%(ShxmPNy1=-Xyw-0SMVC$q{Xki0?tXDE-pq%81gxUQimgu z#HeG)CZ8oz#>U2|MG;8Zu;mF!01wA&2p3>7P;=0fk5x&eG5TCtZf;gef-X^!h-hS- z1p9nSx`tw`PGYh+Qq2eyY-qWD8(>P_(wmj_btYQ~1DOIx-6}n17k)zFpxfSwZ9%)u z!&Rs%@MmZVLkK|k-bWvP_~F05jh|;8S_Ec^`O98>=lxgbJh<%Hw@^Fu8TC2;_}=>;j9c*BbI;8I;D%~6B-s?aAZB_nWQ92&aE9Qi07@;E2Zm5ht@<32P9LSM zF@Cf21k7K9q`wodbqsldLXj#&GJhL_se5j=SUgyVKj7dcr?!J%0T_4iB_`;TGr*~y zlA@7G(WA1s%n@lKcqMLeYC%Cxj><>#b7Rwed zo-ra$pXpZMmh#Qi5(6vF;RRj|B#EGn3Nuuxb5XdG1>%j40ZaeR0|!o>y3reg$IU8; zBhZsb$V={a2y(GE_wV0-wPlbUm%JB1z1UFOjlFw~g|Lc!W{V4-7NVl52%p_*vGsw< z&N0;AKY$p$70$o43wXz#?m;JUKs$iH>gj=_KJ2o#%WyHheIR~n!zAxCV<*m%x`ybN>d|3 z{M`)|*Rg48?Cq;NzfZRNx8kzq`Z_HWPPjlr9Wt;=`s~3~xP>U^m9m4b!LDmqf&2R+ z5HV5p1EK$2@;6`q~!}*k6Hp|2*FNdbGV7wF8ew**hOVwC^Eg%1>Xw)s|hq zaQeg{bn&4RFph;_u~jG}P--8bMGW_?a~DeM+6S$cA)7nQkt%}F3eTE8K084rl~h)i zTs%{}AAzj>IE#|XIdcjp7K}_!;(<6}WWmJ3ImXf7f4}ypbw7Rg)sMdwU$26W{4MO9 z%gaj3Z`QSRHs3h8d&_Uze_6lwhwr~W*bA%Cd+_Ud2mp@<%9ynA^j`%#uZLlA!q<1M zo&z^}WV9`0tj38xV8|n;nH6>ndID~1XMMZH?E}~B@KArB*~|1mTU?}(atLsbrdzig z<{=)D0}5=?a_Hw+hV8BNY+wSrsEgDm{1@j*u(n9%y*P{d1W$As3&)HeGp1nN_=yD> z2`cE`!7qSMh0Ul;g2|YWnLl>aXtF96=o8^WrKUi#U5nLRo8SX85droJd|oG_5Kb>O z(NG9Tyf4HONEo1-jLS$%)I#Y(1W_<%$md}S*)U+8P~G$SQ&Ip!*2dt=VzhC(q!g2> zr2$IYH+g`()zvq&n2d$f@0pgybp_<;BB#~f+SpI26BKNYGCeO%3O<+I{1G#kESNTW zG{u&}eUfn~|1g}zVP9_kWNfjfj27gk>#*|Z(t!RbOq!6(ynXH#o5n&XR`k|cbu;G| zXq4G=mW<_!!L}OqjhJ7Q#^dJa=VfQ5rNG}yPRq*9%eULDg9BjOLosz{52!Dp-<*<~ zo-=x4(VQtd9{eGj8OYa`mgd_xubnMEa<*ldE9VI08eL+NoLo7khFlT7rwVLS$&Xv$ z14}2Sv!5z46gq}&0K3U!86H|VqHy}SoJ6%ksgjGB0f&tZ+L5NK6-{7*aN+BFAzlFA z%m!-E&W_a7G)Q)jF5nduj2<~6D-Fc6x9h9Qu3fr(^-5(!rG z$GdCD?GLlXayg%hxPz=1T}pb2P6+{}j-I~$AtY=t8hbk0Qd85jN95*@;f)!SpF09n zw#LGS^6D-hC)79Sg!rK-F(ZzTp`ZX6E>o7CtdTIoEa*FRoBJFN8>|$Vteiw)!sCLF z0-Q$<7_)?=GEE|)@(qO@rM14KNoigzk#1|BERknAeY|qeVY+svj`ny%09tikF6#~j z(18isSaRcIVzmiLNm1ADR&^B*vO~A(n?Mt?l1C*|3RolsERq}+Ndb!_heeXYB1vJ9 zD921XHBt@QT;}$5s03M9Z%dxo+vdkILnHtl`B!@XJCdXiIFOr35Jx;l|>AV zf$68T#+6h;5%mLJP-kHFQbj5m+>qj z29UU#0e|m7WKbX+z}wI)&rK0kY$w#WuQFDmvg3W^l@o9_gN${?hhdE$0wiZTDzz8W zqwwsp$j6cAk=|L2&$9prNCXtQz~z7inz?ZoRTO#KfMR|=VJ*gn5|q>VNCV`EiS*GR z2(5=mpv>t()GP+eyv$Jpp8M*5eZB6_H`Mk!`r9t=UHj`*fBLv|%1p;B1cktA?VE+{ zTNhEjqfMLf@N3Uc;Wpi92&=*a2f<|V5&PSV;gN;Ld*}&pKYK|7HfRd{JQHCnNTM({ zZoC&4HIP7}=EoqT95gGNB(9ChUY4L5hDsDA_SjSH>Lw&bw!NI13KW;s8 z{zg@!vGCa!UV7=J7oL6sOleO&`Q+12KLbDWnP;AUG8&Z9D2X&MvShG>uc%Khk*kfyfGv2WpOPA3q*meEh$@e(IlZh?DZAs?^cbCZ@255MmWk?z#)b7h5B7 zX?+f#!-Bm2jka)XvWDSih)2y@P?W~(Yc&?W{@e>Myzuf1Z~o;ke|i1omtTD4FR$X~ z#b;lhP8Qq=V`m_8K5hK?ag(MX7BXpU{`kihW1soplE;@XU;fzAr4Kxuv;5(QA71vr zqNPtiv|!r!{DP6%i0RUop5 zThuAI`afYSx{3NH=k?`tr%~^NVBGBU+mJFFa5>GEK13M0LmoKFL9sY4UXBN%ShZLx zg#W-z)Z&y9{`K`!|9peF)70G$VK$HxGc8S4pQYo*;qx~eTH1PaX*p^7SRu4)f|NR2 z#aE|HUQ|?&o1KXQy7Ux8I+9Z|u>a4}XcCgsk^IR@P1N|^$YVfK3GPPFY__|IG7iL3 z%qb-Bov2Si5FtaQiqq$%AdHiqq*FaPFE3uIOvz7jonO29`$JaAh)k}Jopt}~ZMD0``>Q&28hm^bJ%HjR3i$0+ogidQTu!- z<-$+M7xD#~k!5KMk=%yF? zIOyoBEdF&@C1X_4Xo+8v^e`CdK4HHzi{Dsc9Jg;1(YV`&TIpT8w{QP->lRdO?Ap0* zd!QdF!B!h_Bs{~QnzW!ZoR03{jh}t`>8B{c8~4o*2oS9M_S-MN`5K}0AAeZ$?TK@j zN~&+(?BOPj04QD+vbEPVcG(=(_LBDI`kN(XWyR}eBeX$iB;J)6R&U&0eEHnb-M{ZY zQ-j6n0|StVGAW6zFk?{T6^}cv*80d8@%tH+9rwx2qx>%TibB4~j2%JqzrKFzpKq|b zJPu1ogEzfs{!GXk`B{?m3HLtr+_EW>7^z66O3oH{T{{I?+;OS7VEIJ_tw=mv#ILPhHVE< zUMj1<YiZsf6AVaf>NLjSr8gzHY8C+3N%a+a(#&a*64ui28fMN z$3u?V<8n@#r1Z99KC}nqlZ;Xcz6Zdr=r^;NY)O1}zA)qt3v^(<3a3kWbX!@CgXUEZ zcsK);-fAK2Y8Eh1lT7uX#VSS$MP@Cf~My> zQ6-&_H;a@fk4g}cay*YLVG5Rx@d<2rbqwq}P?X3RYOA}9sMO{9E(cq@cW-4^03q#Q zSJ_^p-r8LcAWvPVHNZn3vV7J-taWf$vFo=%%tVJ2i&7m=*iv#6Vx&aE6 z<{9-hH%|QV#~-IkYnlk7^>9ngIh5+0uCbuo2dQ1;E@9QnD!{u(rKp8L=aeZKY6<3JD{Hj*Oij0I%76_kttct0xL(@aF#vD*=0F##yab$%g>o@=fDFoa z^#UM7Wq@D|x}vcXPggC<|K`n`TA)qB9g&X&J~UtJGa&|3-x{YPb4bWS7c<-rBj?#? zXZwzPHV+%{JMA@Wvp-_4UcF{E9GWO(iStDnmG{z1cewzW(|w2jBc;GP6B+X?{9)F! zTHD6iaQ#SoSNTiR{Ez?s(jyNn!>Tn_s$cXHIa`s4s1gOme{2M(;ztw||90?N04pyD zxp#I0XKT6DDMOEsWN=|`TrGr~q$^?Gc)UW20=eN~Mpqjt=%=J{WOgf&1;DOVi^Ppi zm#W7eg%65k!m=_6RczN~5GupK|mL5ICP@pu1;rUe237ir0fVez>jip97U;Uff`wcB90j>BA5qDK+yE4@_m$PzdMlX9dQ zoP-$BNq~pzPGMG8_bL3i>~`}I%rL`lyK58X!6rNen=lVHVIFM4JlKSJunF^E6H@9+ zPU04yDyi#qU}`#hDynbYs&8oPG-3N*UyBL9*N{Zh*cE3cZRg>JaN>gJ zUwCm@zRYsrcca4JMU;;#?82mJKt)ba(sSpalA#AFUJG@IIgfE_4)$pAfa|H~CCt+l zNZsvrAJit}l7K}NgS66Kamy2{OPzf0tf?B0%Tj;cm;$w6tiMWtq(fv~U?6M2L-#O& z;)Jg(6azVxgyP9Cg4Y;Ppp?aF7L3bNGpzL`*RNIe`h@9Yj472@4kARq_vn!eb+|RR z%YR?{=~qAge5kx_2-pI1{VD2zA&JG3#OYE<{c}b_oPx)C@6F};Do4rjGZz~vUEyCo zGN!n^gn$8hw+O1ZG1MWXh=GRT=R;H%-;7}lz8|;4HavtthndvAdKN62l@x3~xPIMF z+pqWX3g#PA5Jy1N(}Fss=<35@M2L6eM+VFXZ3ujhliX_x+@Ay=f*>cPIFN`8@>FSA z8EJ7srmMT%n4(D;hb;TlF$Eb4j4XNF^d*lzxM22Z4L3ME>}SM`M7d}ZYCr9^0ivPX zYq9%TyzkZ@?Vyt8EW3X}KD+nIrqAz=48qZnjFHiRkwFwlnlUmOFfwjqWK3wjV#NS- zpIW{B3`XCzikmmi9@@3-bWO>jpFUal_3o=~VbiHor;lOZ|J&g!@Zx^|{aE>#)7xjk zkfX(|Jkncz6${Fh%Qvo2@j_9_^t~LIMG{I z-~;}fp^$m4q`bU&m>Z{+Ass^D{t`NAB2=iGRo*R>X)M`5qdz}?ObX9oMs^?o?0ed? z&p*1baCDMX8XKRbRzW)>Ap?s((cR(IMyV+l-? zradhe?FqW`S%eI zo{b$cY0E&}_6lC{B)|ovJ&AbnKd`r6c$d!Ym5hD$7k6#kw96#oBovGvHAc?ptUY_B z%b7&@PeL~W8F1^uJEFINM_1PJawb3g?o2G-->BYRvS5l9z3oDHe;BhNcKUqOCz?W;|dbBt|Qd z17M)wgEJyCDOD>8IoODn5ekQRwUqoXE=B}xDYBJ{L4~X&M`q9ig^K7 zn=*Q0h7z_7!&{_`lMy)!o`gz)woP)PJ~37payYU1icq*%Omv74F&K_!y)58!i3TfR z|LeOgL7rI3XR_#E&{BG%vaYqq=7SPlmdowJLO+B%L?Sp5Nr|a+^Tl)eiOx;>OL4>22vY!B_9Ihx(c^qI>$O zb%sLz$Z;cbb+HnklVZDEWMfHOVGsV_ds)ut?d}TK90QS5X;q&m$dplDzEr~RJbtG5 zTx~;RziXf~i=Z^UP^fZ4Z`+p%qRVGfEP($iCwA&O`zIn7e(2+XF%F|EDbrWh0O z7a<0h%?gN9llU%Mta!>|LSn00qNM{)ST>g=CFV}pjFKA)z4>{0BaUaJ8^UsHG)LSVoBxIo7XY2fZ=N~67SDCEV{+_Hs%OEYts5V1kV4%CNy)PiqKoE(~jk4ckIKouWq6E7Sigf`+It)faLOkCE zwiK_+>GsEF%>W%sqL?F=aD=v1}5{S*pNy*4i%VjY} zp}RF@>Cy)aWhO6YRUuobqy{>>;YY;@Tzz&kLp^3jtlf^RgH$Tu%OqG2)dD|X)s2Wi zxhp{(r`IRMav}__m=D3NAxbNg2SP0sM|YJDQ%nK&(Uiwbk?0c3K%x!ohU%6Ui!-c< z<+GGZIr>onH(iPUC(rS6l2X$ZGad(exM*TBs6XM9QX!fhzhK_{nIkhZ)IzmBO9EwB zxVv}*`G3GNCM`KJeN6BMSaX`q5lLM7h{paTTk}dstomiPhzEtS;|i zb@>>pOD0q{j$FA-06PuWkL|litAV-D@|Z+LDZ=-Kh{>1boWg1n1sRAwU;Et#K##U; z-thC6cMD({dtfO)GAyE}fLmxkA}pjGrLG`SIseWMf?l$U-BWS!bK@O=*K_nTetiQH zqxGGZfRUXr<$*WezXwM2E8uL40V{h9;Iyz+%tPjx_osm{865-o4C2%KAwbI zeFX|k=dX73;LB^Tpj_Hw={UVspuv4AC@>Itc5c9zKNe)xD#~h@NqrCUfK^|8_0>vn z(*ZQ_1@q&NiA2#Vg*t1-9d_ciwpR zvAN^4tEXaP;4?R4D+9?rPq4cBEL3&Ql$O@ww92mC?!=$1B|C2qA^*tqGYH-Q5K<=v zkF$pg;w(OeD8V;K!+wUW1NABU18gbZUB!tTz2NnCKbnOw?^48lV%b-2W ztLyebqZ}|cD;ywB$ z{>__k+iE+8{6?;3!^z|tg^$mk9oGe59=`}I@MY1qTlV+ICu?LQmOuXJ6Zjdc zUVk49*barw^<@~B;2?(o90G* zNWr0@(EUkVM4mx(`}WQK-x@wUjrFLQ7@&XCtlo5CYss}fTu8|E^b9$tfuI=MT!-es zqD_vrR}>q$+{WVCXA1x0uZDXNhJ8NT{)&D@WB>Ep-%LbAASNb`)FH;Hgs>Dev{O_{ zq)7xAuuOTv?T%p|g*8Bk?HHSbw5k~28O#^6f~RapT$QZfqGshE(Rj^$}`2sihxl+1W|nTg|a(nl6$K`RYcnKj~48(-y$V6Sg& zeS9hMgOiewqjrzE(!p2WeR{#dS+fMQCXi%}9H1GL`%a!b?_uPZ=1xc>-mO^HCzgqI zNgAx)($Nbx7dKic1UEEt4&}7vXdxR8BJ@$lpU>8G!59TSUXj~ovD0uEgesU{sFI8y znVk|Rz=|l&%pN;q)+|j78<(a}E^c5-2OMx$x~k@4yM5p6xqv5fl_#5+k^yJ*>4gtI z^pN18xgb+Wje!CV7_~?x52h0!k+?iaPeJZl;`b8YCgSNYNR_a{!~!~PE@q?+AOI{4 zj-FA80?HqaoLEH&(pX3saIhQI0J*ib{S<;*#4%y{v4v`P(moMB%G{o+qWsa3B2rg3 z;pX=b8sfbjOuaPZ7IW|lo}z;6_Dn!7K{YzEy3pR#R7t|AH=E2D&;gGk50?c5evr+i%2*+Fk~6_3{jni!lR%R?`yf; zhtBRUxz$vAx!TJ1<9nML=48>X7$T7tpY!_x#Fj61aS@WKo}Ec~Vg%&r5yi*OpBJ1j z$4Kh4N1&z=wP90X!|s6%n+zK^1vYF7Y}gdouqm)%@kchJ)cA)#&ep>ztvY)2#Mvtt zlow7F9|LBbejgO67I#Rjhsnie$s*EWFss>K66RUWIQq%#yL0me(YAA8#7*tFxz2N6 z&$?4H$oc$qXG|_WSD;&H5mjm7#Tu@ zONiXKR0b?U43%ooA|A&K*hnnP&lCj+K{OCY*t3aN>$p6GI3?lG5CV^o4GNGTi$4s} zfiQSe;#8^_i3`!DuAY!k72sgarUWj`d9TH&%N~uTdt6o$AsvVVOeaZ)G)PivT52ja z3sNr;pQGy*a_Nno_DFXvLI;h5UYG^Ouu(VEfg#w~=^~*;czQjbnQX^IIb3}4F+)Tbu;KWuNk*-PA_YY(k`W;V@X_ibBuCgRji(=r zsN3G{^NCU8%(7d1+TetBIW@O#Rb7wM#ly!?gGteX@O^~MWOh)xqd$C)EZ&d1j{Sj3 zh%?^<>#*hL{fOnATK^ODfA+8E7(dZLn zWIeE8EEe0qp=o<}W2rIAUuPnJwz+JGOGi2}0BqQH;itah^uFJ(_S5}H3v^R9Lk8yl zfz5gO`J+c=3*pi93o}NI9y2npjo7IWaDFV7wWYk+n8gE73_oJ77(;|3#ApSd7rE&i zsC31t_#6q`T{)tHQ5bg$bThFnV~`S9W&rUxZ;&X1@Sp}K95>#&`+5*ZRWi0PDtGkF%eYK(ZfA$rmij>tPD=T zfP9ENam1fFm^Bz}>oD5Zz+w>YyiYLN)?lYL`i^CoZ*e_2t4$D@qG0l|zJlYprW z*6kx6ShVpEn}is&qHD;&EQ+@0B&#<}o$B7RYwc%WeD~F-KnuPL*K8r&nI#ItA9YSw z_nCdae*f)vC;Mbmsec+~^fl(se_))BlmG_o;#_#lV-$RxmLMT>e6E~F1j^1}3%Bjp zbwB*@<0%f34r9rdHb%7PEU$qIzfdFz5jBp5X zRxbsj)mSBJz+!+M6bXf5A)5|Rpca_XZ9`QQBSi3B0~Qxqy!;Txqlt(r>ogdYc<4s?;#U6{@OiwE6pBqXyg_3)VVYFo=QT zarfW6Tw)<0V8T3LgMuE1Wg^n#S56$orvBLW8z>lUHR>g8EMAl#P$D6QMknXu-99%l zj)?^`4f-&HcJ#DWgJ-?dqEp8zg!06kw4{VswI(4>$}v!}hJw=-wqV3HKuRvJloC(6 zsoR3`a^FB_XLncI&D-b=@?>=d7$3S0T!dQ-ecOKsSAPEa=W9MagnwQ#7BUpVFp0cE zNt9S>7qTIQ5ZQs7+i!MqxlFGUQ4?mE$qM`X2hBFyK*#M?tJ8#}5OYW9n@wRmHzP?i zYu?nfxRDtCxrxenOdPEQ(E3=te#Go0^D}k$ctT;39;cGaq+`cpsa^EgGZ?^6y)l2z zoH^s~tRjRp?^n$04>7O*9rOAl%Pc1a?!8x3ht^=5NSpq06IyC3KVJ?p;lrG z3dW5{Oawkh&I^SBc2I}_JWl~>LFTdP8TUJY>Cop_jIE=9Y2FzWKY`CXARKl z7_crL4Z}kVQ2@kt4s_O*7^jZWrcYj)g`p#+htM}jrIQpL(k+;XgMzRJzK+>8XI_>z zEXdH%&` z?w$AWgEMks11R;BXL)6aTdP7;fuW##m=HUfZ7rC}*KR;(v8J}BZ2;HjG*w~oEWUcX z%gdOJ;owps!tCP2x7&>DfyVMXr6^zHY=z{a7v70mo1e?k*5+ZHB2trxrf|F{Y(uq$HvsK!>O!Zi!l#fGL`tk(8*F z_rYSb5j8*v(jy^`G(!{V8J&Sqla!RE$23r;Kz0l>IYGte z-^4wac|ipRY9S4PA`nkW9XaE^J2Q4AW-MW`U5OdH5;JxsX6#DL*p-;EGs?@aobIZ- zetzf9?c0C*PSbq~|P(;^}cjli%IH529_d~Q8R zw5l%|St1b!ODO?U-h%{D4q6skF5vj%q7|qKNIlKw6PhoCTt%WHYNg^0glAtzIIJ*S z0yDj#qR&b2m!t7Q(u#Oo4F*k;6jo%RVKzW_n@G`(RzQ|fupvkk+QboKi2)g#qE$!* z)Et!C?t!cFC1r?Tv2dyu&X;t=$a_}gV}G4H5m}-K3df9ykr-zn^P@nNS%ySUyjG(m zSzA#ox$$z8(e%L|kFux=6e`SSjY=U_kO8A$BOy8Lp{Y2-G!&7Cx>5Z+WP&=fRLqZf z5e)3~B9SANh&dvKTqa zE_lV&E`=s8PODbLqYNMhaeT~jMu;l{=S)V1h7Tjv7nW*AO*hVf%VP`soFEbF?S)`C z3Vga;gK#43W-t)PKwKW=!YJssfuwBE0fgxgAzm0_Li`R`9jecOC_kT_5R2QI3Xna; z;)&&mK_<$1EJF%cg47pQN=y&*XqXM&0dgaJVN3u4_8-Ijb*D;r0zq|UP zzzvT%;I3s+!k-V6wApYCg$IH~dm^bDoh*R_8Z0FX0IXOe@>2B*Eu3A4A z^DTwu&@#ZnUle^)eBY8s?m0vltfBW%ncG}4Jl+3imSSi(Q82dkhHA4zm#0mu?47cgp!CiZf zlr@_j13i|oJTa}k&jFEVD$GxswD^&S7K}|*N@BDM%3^AwB8IX5Bk-^&8N61WVW+k6 z{AMFbF+WMas$R3@^yT97wL=_DN?4#u&KWr(f9_L%pM!<}7RNsg!NRH=nwZQ6;{ z%A4BNN3Ht27F)pHTXFoCofXu`!hEq$ocPdtP%>n{J(JgP!#Ey0avE4c-L!|Fd4BnV z`wB8*`Lei->~z#qfj1WWNlWF?U0e4SpQ>uJ_)&yz>UQucreW;=2!xutZr71>7cZVT zeyM(#qlu@QuIxWiU3K;7*0tZA{{5F9zxj5GRm4O$F%S0s151ce{QuuKs4r zfive}`5!cl%^5R(bY2EZr_^yN<7Ve1gNtLf=3EWnE z;r)h{jDu`Q_q;q2_j*14l(?FG-xps~Kj07JLPknmgy2RIwF!U8U(M=Ye;sdHfxmTz zjBL~X6gZeG85Qh1v;1GbU;LlH|2Q~dBJKG86$X?W8(C~?B|a(zZ)YDivQ!ga!uNlH zKY_}*@auX|M6G0m(u{0D=fl(-JmyU{vMkp(;w|rzzc`HS{(Gyb5FUq5-uZSJu3mzx zm*VP0xO&vYKyQG9|Ffu>k?(0eql+pEhYVEd8dmY|UVM_uz&ONS4WtEgF$3Rz%s?fr zVT;XXY(*IpL#x>^Cw^CaiOzk4p+|KnVG4XY79Dx&krTksDiO^ybrKI0%JhbRBj=loaK#Eo84 z4PIJAGsqjF@9!gzZjpl&9|{lc|Cdj>bAQXC=aNOf+)NG@a=1wjB667b=l%Wfdg;y; zwd1u#w2r)kJOt1ZhhNAc0S932|1Vc`=Ugt4*G78C+Xl&@gB*^KgMu6$`}17>FMT}$ zFD{CFNZyi24vWY^MGn!6v)w%x@+qXRxzWbNkr5q%DBjU)W^8P%h|L*?Wxw7;4_9A*JIR_^>M2B*;$64f`|JS>G=To|)pF%DV3n&iJ zp?iQll9L0*^quVEf4)Ee$0#nMGx6fPQ;Lk_BJiW*-LH^C^wHKoKPMM=Xaep~HttI{ zW%RUhsfw`iukD4S&Lk>%K!})_oWq{^_ z^+YBXP-+>4au5_A8iYZb&!(Mb^Uwg>z~YJ%GL1?vOE+fXtZ5TQ$>xreLy3S-O*P1? zP4HmkNx6Jw;hU877Sq<-*;IA>w>$Rr3EWBI!_CK?oR2#>A9r#-?&N&jNtI`){o1+xhl2sGVcO5*k zd+VAa`W@*XTfSNM%h4;>Z&YvFyKmciqSN}Ckv%duQnPgyQq52k6|Omap>oI#L5FqU zEk?4q0JS!iXcZ9`oq;Zxi?#JH$lAV3zlo*hrh(c+MD*sO{H>6FU$pkLjo&%Y-_dmK z0Tvh>x$;Cb=7~wI)4rH<{2os3pN!i9Uut6q4a5Ha&e0si8zzETKljcA1 z%&RXw`^c2%AD%oWD|-}VBU4K2hX(puYAQ-jTYLMgE`Drv^H48>9#<}(aF{LbutIN4 zv$(u`o>au;`^RSsXt%=?#XgYU{EL+no-C4pLGRL7#`x z*Tcb)NJ6I`Rstxn5I*M*3#z-2j!+gwpfMY9Wle@wt?9yw$CM0 zCB&;S)>C9`o5M|G_nJ8993Jegz9OLQo&aANt4-}4GIzCH|2W6;u}_7kkZ$CAx|6rViQSXEJ4yz%=_el^YoQ|B^l%@<;;@GSqI zKdmdnKC$}enecowR~FIp4dQJF&o^{i979PG)6uVIV+0gp+`M5_N(c)Z*Kg+ml8nl8 z1`nKM>|kbsm}?NQ2Zm5Tg+M56RL&eT88J?_J6FOCc^Lxz1Zo84s$S+XceeE&I(7P7 zW!He+WK_nBV>A+0M~yt_x1(H{p`Zc=xubB53SYpgZLF{F39%$$qjG52Y8`e)xb{#O zAcKh4LD?}^A&@0eDWrLrt|_r9h0drnhg{AeJ8iT}tkmj}O?F~$E~gKkZ$#*;U$2Hl zy0xzz)xre3u^e~vo&RF4-iNs=FFv&A_hT2j-Bo6HnB}$AU!4uoynjR3@ja>Wo1<4Y z9V$M1;o$GztfJtJ{Q^6zqSAJbN8#TXVf_@;{bMjhKNB=-4-*$Q}sUI%Hbe>E9 z-I&G~h(v&$*|D7h!H_q?R>_r!jf8yA>}4{zJPG6>3@O2Eq+9_y$J`upgqggEFD+io zWpV^ohr{j##a+mlHg4>sDWkK+;S3(__tNax?3bT?{@LY=$D}_nacqA6)S?HU#uA)T ztyhHI7PI5T6}rEg%U-k2r@2_P@hQ6utsY}oFH+40KQ z7y;c;D`POj0g9oBf!CK}pFS#9!sJM>wY=T!4lsP?30V?egcdY(v;*B9W@3g7+bmYQ zE5dLRL@k2~y4$;uu;_DmdWVOBl^yEuqWTRfNzMQp{KcZ|)bzAuFeN0U`e?pHAr)t# z)c*QR%Szr3$TD7Q>!C z1~(aZPi~OcceqegSXJ9tofePzGiC-B-Jm@JMo$JaGA>uf@!6f|7`aN5nU#^Ej!RZ# z>%q1>aUSG=SnOaJ1TkR-;Hd#by0Gn#OA+#63n3CDYU}Ci?r5ksDih-4gd!nG(Xpkq7h->IC}2dyIHu4^Klo)VKp6xHDw%D({Wf$$6KyWhhKyQIpw5I%wBM1W;rPr=HM9?-Q>BBEiq&Z3T%J^!Ic7M5 zj9+AUOr}!GKO8WT(!RZ=*>#@yjC#rKs-9x^ot&Ib;i#V^avIo(!p31^JF$2nW zMlG98i4y{aE@(oyUzU>NM_Ld$GEDkduUCZP8ew2KgXsCiW()^N&xnj#C1>vFI2m6U zx9_T1nDg*!ETvq)=&PahhFG0$&@tTKcI9Y>7;tG(#?h;${$a=9-5#5c9$ShY8~b1M zn55yS{jDfU>AfdE+*JHeberJjnX@GgyrdTN81K}tUwpN3M@bu@4_FVt?Rq@mwUzAH z_|+G`o-)Fl75;X1(8FNbJ1Td5avykV3ej5$=ru8VFO3A68NJcoqJEhJblm-@?tg}U zMfmxz*GveSny&o{T7_5e)Wi7GGt@8WG@S~*i;u2pGW$51B!?f$1g5$GZl(a%fyW39 zm=fiy1oA}F0Jw#)z@yKWPFL|Gv~1nJHb(lgzq;6*pzYcr2I;e#PBz11y?Hv3LWG@4 zJblyZVX#qeskYA>DT=t9>|c+z_|?;vTIi6Y=h7YnRcwgO18HL zpjL8n(Py0l@JMNYPKu1_?rajUSX^OT3gViE*km!ABb4g1oD5{48O|)dRLEhAlZ{&S zZ3`Q#A?x+!Bjxt%2jT>fyqhpzKrJ`Ko`80w*&$8ZcMV;3eP5E)VTPdOiMxGPfIeG@ zJ}W?N^B@4iM{ZsK zLh!=PBY1pp>kmfO#!Z{PoCn|XDbQ8q!}pv5zV?mB{S1z+{@kwhYtcmopw%G>_~+=A z=teaD7dvhv7Jhrj7jy4Cfj_)RFEVO^wvOg5s~5{3h&FvFtb}q>EI{7|wMZ24;HJ5I z-AJk7h{qm=$f)6TUpaie-6Me-6(_=^gNp1~kId2|7NBOa23ir(Z8%qtEI|Fa20U)H zu#6h-t;4%cRd)w{C{%YlU3PAK!Mt%2YRI6Di3wW?7^}vNU@wk%Y-Pk4)wBq?Tq)vW zAjM{cfixZlnBHSlLvc6`>9@EXEmBI_oakdY$Eb-JbKm3hCuAtq8hK1S7%cg&)`}h` zm19shHo6%&8-9`i(QQEzACDRK#=HG83H>tbzwMVR+ke_$+KIxj?)uW>$B$I>037c` zzwizm*h)Yy&@Xz2z3Q{iK0}%OJKV4F%=bSZFp4&=UBByGy^X{09|slR-_bh-=%sn+ zt#?^pZrT*>9f4TvYCwd#@+j(UUL}pc>S!gNXaMiOCdi4^LDbssM|EKcJp-~XkPebQ zfox0+;^XRc^o|HeJdP33=o3BqAX^c{HiRSM2_>HP>xZv&lRl}d8w??9MCph$I#WmC zamZGOK9P=}R}jy&qwWAAzP>PVm1?Q9K_7rL=J+-V%~SR=5bPdw;@VWcnt z90&vc$B}B2H6kO@>JmaQ&vJ8+D&V+@&;>`RHEPt%p2~JNKTfUGsj&_cSkn{tjUjy! z4EEFj4OV`%3@L`Pqvd#9(;mFrCuBu>9DOnib7dCh$}G&4S(q!cFjtb+u3f)jpg()|+&N^*5AQg8_Uy_}z?AyU*T8{q_#KbR zubwq(KKa9DqUJ==_ETd@*qQU(bq;mjm~V#dh1 z{`n&Kfu!9Fwv#)V?WM4Yv!m@D#sQBJ`n6#1(dZK5V)!_@2ujfHHmiAH$WEy85Re1p zA{_L?Qc{E{K0$@thzQ9mDTh#x4m!fdr1Vi!ie?qvyJ*h%G-QTkf#GgbOKV+q(?AH> zG#NrL`i_qJGQi<(cB2G=??auEJ&a!R6XEFd)g8tpNDt%|j4!&UupkZzm~fY=uKe1i z;zK8{wE<=nqz9ARR0T2t3 z44?{dJaNcH!Pn6%6q;ltq7&uF*F=b7oC4O9C5}Z_H9JvZOcF$_-R&K>tDE{f$mNOB zM;G3^V9~P2ADlZL|4c!gM{lwCp)f3?&G^V`4L}gI)gmu_+lpM5B4W9-cNl|lFd~_v zQZWuvF%D8O4pK1=QZWwLet+G$&*lIPw>a9~W`B0Bjs!vOF)%iMMxr3m#`|L9A1Fb; zy2mKs6Sx~L5ggl0X31Q7p!bI#T|gPNf?(wYA_0 z!)&;Hi!i|d@5e@-pFp=^ zja3gXr|HNlW`v>*$jj;z5|Fh`NYG>8>k|lKl{_{glZ;DVlmZ=$0%et!66Jw+sXlwItKYv}~+!&3k`=0fl^PJ~^eNl@&ln!R@ z=kqaZ7mD?p25T3B^$W%Ng<}0eA?2Z1KXGvv)~~xb3xKyQYP(8=wK;KM&z?O8PF%Q# zaP1H5v$)K&OOR6t^<Q)MS+ice0i=E!NdmR?%WEL2zS^Ds){2ry>ITq-S+l zXsxBi4!jgRGicwlnBk;ExH%yN35A~lv=~4Ru*HL~9uK4nL|VW(vEwxiX${&@8BPyp zQ}b~TCWnv8@RV@mM#DLfXAf5hc_EQ_4_63NqPCXZ=)!7t5tC7Mk#}+1`V+-Be)!bG z&&mGehik`73lb5;IOje;N0!+EekZOzNO zT2KMhZdt+AJi-;?`Aj19U@!5E17NKYLVW;7WFx#{hysJdf@e$-qN{K!6&MzT zdmR)O4FpGYXe1D7k)Z^Z7KAd0Ki(X`w?V?d(~F|G1|iLbB8?z)bPY%(^c>b;2IO!K z!gLuMJpjhVMO`d|8c^hWsT^M#kyZL8Y$s-&PBZye7R&|dSfzrs( z2p?Zx3H=T*7{JQ_MYKDNdcbBhowZeEjYH^%qLqj^(RObnc>cx~Jzpqx57|Ots^_B& z;dAj~Ze9+-)LuJ!AnV6tyEoyY{*EZ`(<9j+A6NRAy71^DR1>*ZDl|j-&ay5)por#E z^{N0&4x|WrfuOVUhiuWL0~HQh!^6dal8Yv^ikAM*^*DzdZxDUMoz6(tNjB#x5Hh-h67jg9PaNDiDhAtzi!#HbL@wls4rh zzhgD{R&95HZu)vh)(zl73ya%HVRybNkJGAQvGmUyfg;)Q%yXkiFmQ=!8oBbEdmPJ#|6Va1cM;z?L>(slQ)_c!NB*xa0#g$Vh^@$Zo!coz}C>nd4x zWxru~sPgvdtgO?AcWt0)dM8jCBt-a|gSG2cO0Vqpv4<~H>9boGund0akHePooifc9Ova(PB1 z?v^ZW5ecx8W-MI^I%Ju>uf4Ug)x_}j5`)T%puoIo-*5t_(NdYD2B z?c3emGho5Xc*8Bo>9Fm_zHa2lWdW1YGG@<;RPclX2{I6fuE($i#9zmRQ}ECKg}(+w zRpMc0k_3h!1SPuWIau)9=&SQMZTu|ff3&n_$Bvr&KBE5JNsADEU?w3G69?Sx|Mb$c zXU~{BA5E|G=gyq{pI+K(H9INX2g+%&{->9&u5PF&XfklLRR5=!4iAqG4}}j84UY`} z*+VQC^AL0Y-9va0$EVfyh z7A+Nu#kA5$HmhO~={lWJVIr^-J0N!4Y`X3w98x1YJ-vFBDSd;E8=Ed<*0 zW9m!xdrz)dwv1Mmc0m$#Y63r$2C({jF`G(%6Y=v_@@>&7!Y1LWoAU8xQ{Ewoi;r=z(6y_F{=HLAB+?iw91*MqI zpq%6f(ezeE0mI64dPhdZD7^v~{1!x%)FM@YTn@tmOA!^sbx3^JmZ5vqWnCz^RtAv{ z8GAq6O;hIp9n~H&i-o}(DX^D{3HBY;^I4FWhaoSsVB!9~S23vc(lvOF z;);9S?REK=Pi#AIvf878>MF=dV4Fa~8*adcyQ2DTZb6Zn@to_4IN}-g5ikmvZ1GB#6cmP!WhvJSOU<8Fjax5buzrevD&fG@kxpC0bU%$6il{A z?n?*xg@-|iL&5_@5cx;Ukq8_wrDb(pLslVW>uK(WC9s%~R|yOPK&`v2l-1x zkSvi<9v%vGg-h>*1&zjrQ%0K90*81^TYJIf%a?Bz-xBhFQIdCk|+{GYCO@SaL1%brORkL6)X04iu z^OOj|=sFPe&t$B_|Cq}X$0H}=bOy%H1bf3HGiU??U4?|9I@OcC^26T`u?oAGP#1rd z*j|R9Xxmt1C1_rbbz5^;b>o}?AueN9ecKYzGcVh>sPN_-+OTP zCggkGaKA`%50U1bvO|lpO&=ZY_IJjCEn_3ZL|YDI!M|mN$r{dnMi3g|^1DzPi4bI4-$|lT29q3n=276&{i)I);#=opA|MoTT zi(I>%U$$=DuG^hh`Oe$BfeZeKMX&!9rE|i2`WIaHYup`McON=^qlixAabRaH+SpZNc9oEu7^MX5 zuf$s#*F3-xNko8FtKyLiX(N+lFe|dl$A{6DgKS{zck3&{hLsnNqR#!t-dA0XK)CE`0-_3Nt688D|dVBnmF8fG-=^FK6vQxiJM=1eR<;Zc9Fxf%03(V5Yxtw(LD|d?%_C zgtza{kh%|8pMSpodyrEzRO=RjsO2rBvmT)D2QOe3VxVN$av?}sldiBJ9sLNaA z5PrM=#8pf^GW8und2uy@tfxfKAS3(A?VPJwdp6VYW_xX)$!ar(`Ejl2Fr+NVTFQfw zqXvPI#R7h89;R>@O;olj7IC9n>=T>Z(vO(Eza=@=N6h7U^F)3!xrE0TaFqUj%Fx6V zI*!B2>TvQzBX2w!%WBWw?&D+#{pPH0XdSdW!3O8-QSb-4TUrqRySh}dD8rU^YrVsE zT|m>-g>6vT_j>Ng|g_Ww6fp!?T>ZsJ(Vlcy$v-dxB0kOUIFtS z9n|U_R+Y%j9G*MP&kG~2sTHbNyPen1l1y1}r3(A?$}0<|fLFxA)94v4=Ww59utN;$ z7I|U@9nW%wdt+b7zt=U)kc7mei`#DQYnKAq2r?QDN9<&wEP)8?T~#b{GT}0B*YNBq zXrZ1md)VbI3-p(U1ohhJr z{yZLw#b$ZmsTwdMhm1%e)@tq0x7A%ekrDt08IS_zkG9@{4y&=Qv8h+5)!Lf6x|(Zp zejJn7SH~pw5lHMKkl0`HPW)10Lw}ao!p)zcJ%0C*I}HaeUA=bw^sy~pQgssxEPKn| z%R5muBWc#R+#fL30n+?F>-+VeLxw+q{F0f$|C~r(1wYAa-NBWvJDBg`#=Jcqre#b* zYe^EIAnzda^geKqk(l~X1c6;Nj==hk3M`}V$mTs+S95M(VH-<{@qY@UyP8@{$BiSe zf?zM$GsQwjtJa2Al$dcuwNza{HCiUbj-m*C{jGhPp=LS`l6s}Cy|;1&+Ba)Xdn7gfQ7QCE zr!Kw8v$>oOHbdeKe;Q-4NQ^?i>B}yaA{AYFY1wo?p;6+#e_mX;e_+H2s_T4Y|2TBq zIC5%e2WJvYqko8(+!y`Sem-8FC*Wr%wNH_C_;eQWK_5peW?hw&kFO_9O^(Ior4Iy0{OO+1FuS3$u_{&MGA4 z^>xA4eHV9q^2vJ7oRjyN$Pzt>>07@WW9$19y!L2q^B<5}9L(>({bWpbUw};dV$vVe z9SB3Z$o&#zH@9Vo<1#iKSO`n}C-*0AE>g;d3a^mJ`KnqUaJaMHBvuq9@J_R1m{U9c!AHu~>@f?Xj!9{wG6qGZ_ zvC{n>b(l`JIBNS%R);a%k82x*6*k*kf?z&}FP130g%nbk7-6X&8qk2h(gH8bLN0u& zNaRhy98c4j;S z-^&~fQTSsqamg9UbC3;z1UfOmeW16mUFVjN^bQJqF-9ZEEQh3F=L+%v3(ti~hq@|y zySmEICEJFoAT@-#L$`4)w+Y39>y=$}a=&fEt@@6>sud|xLrvCp8BA#8YA+@#FZ zSPAmghVJgx+M@ikH|}=gtGK9c)l}qI2He8Q^YZE;V)c3RCJWsIn1V_;LWKyV;z)Ne zW>fk5lj{1t0T-RjbcRbfOp4#og;{bi+?301XdgD|28a6fBMv-^f${*TJj4PHVaBom zIz1K5@?o&xrYRACT0~4S!GeckI7kGt1EfwVP$0`0~e!o@)K{F_)NPtkL3a~y{1z~rkhHYA@Qkm=iE0VF$|X|0lso7Fn8Ln#%rhJ<+x$8 z_|w<#bs2i9tJMh5>QPI%RD?`abv|HREeJ5}J=LT8!p+c|{m`3Npf|5WZ(fDoyb8T} z_1~fq?_b*XGiSD`NKeD)JobM|HytF^lv609?fQPpMvq#2!2WpG87g&RlP~m`{Oy<& zuvxVnM*lg_@khimpsyqcckS2Pu;W3i>qp4VK8LK{pZ+T!@EqRkZM@=l?iANO&tb*f zpQlE{c;tKTAKSV5p-o~gJkHCWV-=7zFS_4^V*Py_@8JEDci4UW()IG2+c%8PYyU{5 z`gj{y_CEP+3%1l4f3k%R8`o7JVJH=oj4z+F*2gnVoP($sL9rAGdg_KMWn8!6 zP3lfOi7)bzf&Rd%`HIo@O=$eXVj@v@N*yQSiCJwjV)3v|^5fc3_8%T0djxRzLr}sM ztPw;%t*Vr9T?*5z>1pj_czMWL`~h{~+dKo;Ss`GW7SL(q`j_Eh9$<)sqKjOahiRW| zP;egT&l;u{s8YstHr^Bh4VREqm{1{x8xh!bP-UH0!t|+00d(59o(FM0Fhr=UVlF8j zWNtVQiGAcbZ4MTXdPkKqt{YPnhQy zpmpn^bx%U;2$L7tuRi{N`_-1p>pvbpaq?{5H+yas0sc^Hlm>_lBu{oeueRSx@R?T* zZ2f@%G#TH0xe(I+zWZ_d9lewdOu(+E2jFneO+!zQ1ZCz|O+# zL*M;`?EEcWD*~6J6sW=%nI9j!yc}){R{`Do6C$Sj5p_N0{uAR1u%CMN1N<$g2u(}P zDQt{AzrX;r1#p;;(TQe@6@*q0PNzZ&GCrrBz)j>Dn@*IJ^J7#|lYnw{hg(nm1q<(#`- zL$Y~%DaXPfu+j7fo?Jw5qw(37i}LfXV%pnbHOR2Fl$Ty#Us#94Tkf$^7|+sUxj3&Y zY}C_<{R3K?1)-GNIA@AXNHN&fs^hzlUN3Ai>M(l%L=jF|Z0dcFER-X89^X$wf1`Rp z(*dvHB~M6qyZh?8ky>Jpch?QL>3FvjnR+7x)H!IdJ1JD#L;>MJzMjmW z-3``AKu|TEBsCbHo|ZIua`JTKY5c)iE*FBoJOp5VnJg+5&^Vuv)F_-QqXK>C#7Rks zk&&Rg@Qti$v$)tiwk&1k@1MGF-ehG+m{=BwJ+4FDTzspJv_r;gdnzkRYf*aFRU_R{ zas6ChAIF!>@D57xfeibk1mRrZ%j_HV9lt8YS4u!9)8z7Q04sx`tF`#%uShZV-9iLV ze{g@q|8&FGA0OVabt{DUZ;+?A1HPP4iV5!VAJ`rIL%8mX!%&ACiC*KkC#C$?t@rMAl@_n2Px(NriHf29=Da`*g^mCt5O^rCX)z6 z5-SkTn4utWPb|eja)qBnFwi6her(8W<@^1r6njzH-qv>YVF>coy!@ie6H2kb$859d zHKjKNPE{y4^u*r3D#hsuaxW%F(xDkpqjbg4Pb|fDyA_~zo|lsYI1-0LIe$fp*+RLW zGSGiUYR2^B$&-`PCYIs>Kbc=h_^c%+WPC8Sa!50w6cF{z77Y=C+Mr((J=W zk8WST1iOG&z=SmiMXRMzA0IA6uZg3x_ICEcAHLpl^g{Lzm-4FG;Fu~epFg~Ptap>X@4YmFS!kvEzK z6jd5P{(qvrqBF*qcs~yI#bQZ-4|sxL@c`q~5B90sy8HDOxNR5EdTA3$zfcGsMz@%U z1Ydv;{Iy7m4Fy*ONcZGc1+I zb{)NTr^x_2ZvkS~=@Xl>lWbrVBDDVf?JYefw9^f0 z)mk!L7WGFan~Z8g`xTRv_8p*!n^2F$;sBH+`~m_46hdFz43zVDJj{!fE5a3GI)f9t z=or%j6DS{Ky=geI>dumKDmc$pW_-PaA zS7)ac8=gV_4ZajMrvDY9-f%^%qt{Esl1U0JbEGchj?wCktM*sm<`+A{WQYtW%H1#=!43UDtVcLf3i$?F_gSd+m@kF4; zDb#l~xA!y;p1zJji&kSp#f?MTw(U9eLpBH^c%PrVm~-hDxk z-nn*Q)7|TbPhTvnudQt@s;tPryl(?|DMfXs=|xEDt%B9~C+|#AE$9K5`1jvoLsH#B zR~v>`UvT}1LCPW6^AQL%iF$!#MydINig9DxLYSmi-LHv{7HY$iZD7q9Xehby{XzsH z@3=n@U21S8%z$q(_SF{AQJ6o302e(l*s(Ey>~nAh(!kKL;J{$tNiqI1iBtyW)`;Mk zjMQmSVd0_vVPI&8Nt+uSlR7zpj6;=$DuW{8GUnl(44i=I$gsc&VMcwkRVndg?g{`}&388b4e7gQ0wCNZCF)awS?z<(l?MQ|YME zvYI{!x5he@9*^ZloMB=4M+Esv0z@kpMh0R=8zEt6)51*-bgO$Bds~`%^!$LfL59`P zQhTqdy1ThWZ4@c#C?3mYLTbXGvvzAB2`+nIcXMrdMOAH0YiU($XAhvym8uATDTBq8 zLW2WQtE50-=PzXo#9$T-2xW8GE~~bmj(Xy$N9LzaNtzNewY85W@>TjneHk#s4lVQi zt$Xc#gS`NzMsST)m6a_W-9t_%ib$5;dV7CqMRiB7dJq*+40mfN{#hp{pUPo_~_8Op#Q=&$S0JeS>D*7-824o7Zz>Cir83GL&fJERk_2}iIS&300 z%1}j6Fqw!oZz?UDh3*cvqxo(iOGsHQ%#bvXUe23_4m~Cp``Qs!Ktz(9mPLU*-KH(O z!5v|k2i00$5Yfx_K%uUqq558VLw#8j<_Ce7rE|on8#c4O3=Ru;w;g)5M=!fHW+(W1 zBpBz%#t=JjN3{7%#lFF_m%HRbUuB4&qlb!uUKZ64*_a##hCTUk^y=o3pY`%BShw-; zh|=q=^;NC?;?T|!a4WlXbp_~9_BpV7=aK9N_0h{`E}#ng)f$gpZrFD@zoN3JwYIjt z?BeOe*Y9pRaP3ZI#hrRgFS&W_(7vk}jZOx@-w$8@FlwN0xIg6WCOpK$c@hf~(=uLTq^7ala}4 z{zgf|fXU&uC5LGXk1j+E^Q!w5Y8xFTWpQ~dw_d}2&W|oV_@Xt#JfFEfiU2XIqdG(u4to%hK8NULdyv;~Uu#QGO><*itI@_B zY7Z0`dO_g|I*Fb^H;b>Mqk6RinjwRWZE#{xA_K0cxTdnI_FlK9p|iEUqrJJN)T5jI zS}Uk)*r5T+z(BEpE%n#v?Gz9NfzVeem&K6MQGGFy5tC9^JRu4N+ozwvJXnZfAAX@T z7U>xnd#LRwEkI}2n(*>gw41uEZVck>y;obUr7AlNHp)3-R<|_Omz3Nc>l7ro_eGGX zX^3K{A&Q-bD3)|rPa|3M&dw>ehSDPRy#GzS=~gEv<-s1iWuRm`m8bGOcP_89@Wzf$ zItr0SF6{VZ$Bn|yJXC$AxizrP^U?CIbSnT#PJc=c_$7_XMYokB6tO0SOPRx+4Wzy<3`1|ZZ&Ff(G%R}NVxKU8 zA;NqX&x{U}M$ihY<8ZasZ0mjT{vd1p$&KDlH!tEr^ji9Qd-Kq9K51@6KianXE9S!8 z4)SP4zhzrK$YWX`p5d*pI=%^;V0Z9xfZTnZ&Vh2ZK^|MuiJV?%NvzzUE+4@ATNNze zOYP-o3FHNW63C6wz)*T$v`hG~+(TVazIeWGRM(K3<-?#AA<`$(Ljwcivx-OjGM@iC z&)-*KsYPFyZ%2oPKQdHxTc86%SSz?)H8>))jO`}KIxLxB9lScX6%Uy0JrAaEI&W>l zV(?pw?l#yacan7|y|U}#tsdL%3#ckx*lp{%b#d2~QZ%?t6P#!?Q!IT?`N?mV!5Q9< zl}NyfD6uk`n3<*VECgfk=VV;;=Nq%~8U_(M4>sgwZ6v>9e7+Ye5e9gtg=**=W=g{+ zu|z_S4UOSgiQu>y3DMr{>G9CQ_~~r#=!6+@nCEDHD3V7xy*Hk$w+7w+Vz14jtv=!) zD>3X)xXtzXsY-;d%GCUNvs(e`DnjrVWT+1!*RDv*cH%yJ~fv9Ys~l>kWu-zy}p&w{UD>5B{T;)5!J zR)p4DrM?R8-+wnlX)Vc$Qv`-je@aGHLS1pYy3x$P*?@@l-c9~UWA)7n_1H>ejIG2^ zSh@^oLlaI4{zzhDF1AZc#cTH6LTXyqP;%+m?wweQ1!$H+dfE4@QwO$g z-Fm6Qh#j)A;u6jeoIDN}NXrMMeQ|*L?9vc43XiZMX1UM$BxK#-uD=y-0!Gwm+s7nON9F>+AHWV&f;y zStg202U_b+AI>VMQlog_vg(J2^}L{jIm_pJBPZam(+#OjsQz@7v;#oYUV_G7u1Rgs z(Sa7WOyR?4+4P2CBl65>A{kV-HnnL%S>X@F2!dUQLlR1;)y1I_Qa2ZKnY6+uY55Z? zW+#V=eSA4Sq1e0ehSjyjjVdZe<*!x`4iRE;lNa92<@7S)ymP2;453R_Dib<0?n<@Y z-oO3uwGu*{Fw|C-o1fEYqyl0Bjbx>2YjZE;VU4o3UC7DFxv(t@=XnxVd36kf3fx8LC;Eo%_+wqW(rlo)Tjn=$MV z*z9giO=0$tYdvI<`umyQ3}ABPlLgqC3MR{O&hU2j(}CuIxyvzy2Q&nj;eu$-rLHQ- zI()v3EK+Z;g*Ac_5I2lMEtd-8;(UZkZ?O)7UQr%Z)5R&6y88ltNf9p za`Et89D}68oHgjcPo7I4rvrU0EhY8+9zudpUyjU29F{Wop(htdlSMKb+ZsqyN7a=Y zq@HT7RN=gBwT6dj&0D4G{w-DBN=A0v;;rW}nvrJ6OWf-swM9gSwA8GN! z1B1YWBZI;3VMk}AZP+FX@m0v6XaSMF>6lG34VC}cRCxdu5)hp{Yxc~C=S>ZRxO$oT zo2o0TZk9F=5%~b-90TP0JJ4XG2O`>+kF7hKj7*`BJ814R(&(^B&X_f4)xvZ<&q($4 zv^Cb(-Y#plAekXU_AdziR=wB_n~_WMCv(p|MOX{oyRNyL4pH}XcC@uOw)g1p*=BS; zakv~z371-jSeR}YZAsqAJ(a3t*2hHlTvNE-K(5?V3Lu?*I;j~d}MjN?8!5EF& ziJf{7cV=8W)vJf(>w5{LxalAni*@GUcHj`UgW%r3uCAse2lqG!1z2*PJ9a)Ux4;c1 z^TrA=Y;bWbgR>{ICemD2Tz|uH#r1b)>@ezcxv2!}m1|BaL0Md{MhfW9C(CUo9su;nU z3JMfJbKvV8Xcr{sq`Qcm!)QUY$t)m}&m$itmnnjgUILa^<&PWJj$&nRZ+kPjiS0dO z=P;1?C;)x&B8)}zFcU!Rv|1gQRQ-`%7kc{ggos;Z1UQ>yXQN$p385-zMi}|Dj(ak3kEd%$g$YvX{kx7{tq+_GSEDO1lED8Q67@}38 zj*ia0L9D<)Usne$9w9?U?IU{JCOyV_V&1ct&;ttwxR@3L#DhrzQi6UEfHzKrX|JXc z7$mdFY#OuMQ^H62eUf3f2|GqwID?uJJ{ku}#1t`+9&Q2hw;01K^qq|ZC<>^%yQ{?& z6~%>xWh~v@liN-XQM{8?xFc03*DXXA7zcCR@ttEZ6yDC`R}i>ffiZZJEG45e7l53C zP8Ywwzqyl)8R_e6?x(#a9*ayGAz>Hrff9_xVQ^4sCizUXuSt}l$oMMVZsF9~v!@8C zKofyJSWJ4Tx*o7`+&Ry5>R}?C7K^r@%-HDe*IG`U%I)_;2bWiW{wZ4O?RkHX{r7wz z(;JCLbismEQ?J+RblNs=(D(K9bauAi>SFn`bI(w?6-HT*FrdAIDCFY-pAYaGVTvfP)vBm_s*sac zVWskKUI769;-yRYdAnl}?zZ#lCe`Qb_t9(@WisXy*gg)0>RKx=avGmv1H?()D(Fr)9LD!(J{q`kb-*2mZB9#gjw317a7F%3DQ9yJDkp~m? zZXQTFxO50YJCQih*B|u-8ERg9zEAWd^e4vqSTtILg$|2Lh>VyHEe;RyG=>n$6Tsz^ z@ID+tNGvr|71A{Xx)w_>X54h3XorGKpU@i+D^~mMTP5X5|E}GKP9Df2D3yP5cON`+{2rgPQ0kwZrh?%YIJ5#Ocb#@+w(1U^9_!GQ{;@6-hR1!Df|s+e&T z0*Psvv(ge{5)z}65)!7SEnH5=j+-HfnhTQGZC z+AQjMRWwEimG$anF~uE_)sEsAnZCD7Q_`sIYQCF$x4fXNu(+n#xGrwoG=s}AtN}t= zKQ!FZrPae0QNLHkjGK2rN=a`*`qSz*S&Vt~HtN zSI3b`*-uibqSx;`a{A=q+T6VayZHhT$dhqAGl5JdL`KJi1xp2C7{=`%6cQbeF0t`t zGUkDW>B&h`QbMC+V`6;O>$+KqOw;Yi$@af}7WVIh3yo;R!#ena(VSv&- zT;q6|FSVw>Z%Ain=uq3Un1+Y?Pzo4dB$>_K1KMGu7FY<%YC?f(0+BqBoptegPD|g} zo40OW%gQ=<2&DHDh~&L)^m|iQUoMAh2OLs7W-0#!nIsb*35Nbm%SOLTX9EFxCNoev`Vbnwp!N-&S;U z`=`S#2=rQpKiz(_s0}e+4sv%V(Z{{v2Y}U29YO2TE#!HcOD;n~Gu(O)ZBGg^Z808< z;-l{O1Y5Fd26SxKNYgPwv-F|Q`tP=GK5#AvrvJ+RJ)1sT`|&6D0j%;z0DWhA4nh$U zrxG+bgI2{@esez{yKj@jXT$!RR`^ zJo1&)!Qh8KEX81&=xGshHqs9!4M^K->q;B@Mo-BxuM!OIi1aJ{1 zp(umfgixcnMNcQ&?Mkl0Y;sjL584HRF=kMm>Y4`0aHrk@y#+vC3h^GB%R#|y1b}FctbDXTP33ZJJd3U}cd`Njso&(> zO07j;tExh?UVk0F&y`$6PKCE1u*KMr^fp$F=?9sdxC{F6D)fV3;$B&=Bng?adJ+|SC4O@==7Y6=054dCw%D_C=3C~gN=A%z) z6eE)cHW}h@5o~7r51hdC zvh366&mBK`l};NsD2a+nO3%Oy`RP;QLPJ9%q6kglq{#^Fr%X>xjKT=Ll<^}J!buf9 zB_lHvU7M-MA5D%8hhjw~&tEuib~2qhu50l33k?lhv}n=vxL_nH6$p_4azZ|dS+x28G%!`VT3YXKV;|4O*rY(AM)icjL^YG%C z0Bl|~Elu~31E{QR>7pK2fwyR!QVXanK-I!yua((t1en4S z$zWNe>|y<|*+Hj`8~)_vUB7V~7{P;Qu3}43kPD2~rR=N|fSK<;^y6*noGNA9Sg5%d z;I;m;9N7FDdBp&xRKt9CbhH*-x|DmTW=zX|3oUyGTJ}q9{QvcGPgFVR?<&7^F8gZE z_Lclwml4yA}vY}+M=^K-*x!Q`|x@1;}z4; zi890e7vVSOs#;E+J9}jBS1LMjqLPH0A;4T=ABhYE_V~-&2kCFjX&k>sF>CR|%NNf| z$3*klGiJ_T{K!*u;?G43>o1BH3l(jh!+a5!;oyyA#ADJW`X+`{@!$i}SW)-sy{o~_ zQggQ#=T?oK+oA#QO=nYQZ?mSiM{DBC=|pQ?_AkmB;JEOVHD-C|-KkM)x|(ad8xw3P0%;zWfHhTE(i z90XUR3DLX3h`}5tRXh`9N`%qTFUALz5T6)ABP;S1Ffnk|FCZiW?2pReAb+_4jQ~DL zi^obMa+F4V#!4gY)a=>Qg8hTTqI@G8dYN1_b|XU@uU1oDo>-je^=CicCAm{XfF=-{t10Vda(t1!q-SzDflz#sLx3LHg<{__74;L_aB6Dly^|J?m zSOQ^t9Z-)y!n?e~->~=dv-g@?E^S@_Z ztP31949lRyY7&GcEqHLYbP~AHna=iVUye5)21d8JUEdE3 zq7_hH6Gk0@*L-*ojgu40EL$K4X|ob>VMIuPpEQt8lq4*CZ262S!9s}-OA-W1UXH%I zs<725N}e()CF9ZGgiVP=^Kx&M z<`k9}UcGSQ_L$7R0h!$fnH?ALK9PmZDY$*sQoRgZtW6@A4!*&hGl$i^q>{-FfQjm7_QB-s{1as=0po&=@tefuz8`!seLh(K~H?P9Btw+FxDKTGbXai5-$*_ zC^7AwftViy!mZ|E;{Ym$TqKZ1koL5REcEp0DF|t1rzfYREnSE}VE!VIZn@JT7TzN}MkT(Xgqnuf6te z;pv?6b}c@SlsD5RvglSTCV6SxCa~(t1hAc8_4e|zFfl)>YtRbxJj=(=htII+4Mu~- z<#4)8C~G&i4(YW0y=szjH6yQMce%kU%L7#&3clbfCg}NzEIsD-qAQZa7x1}OF2XY4 z&8${2T5r7q#Z27KkhEoUCPhh|3^s7ZOs>GvRiA&g3LoFw*jm)uZ($&MCf z;&q>XiEY96N6upFa%L+b`XJr3e?xP|IN%ANe~D~b+w1C2krWR(-u63IfVk|=`Oom! ze(ol+`~|nFTcBq@0W8Liazl34+g=%?48MRKWfO&!x52VC1R&%Mk~d5 z#tTSt{szy(C{{WO6zdE&Np&M*OtQv!kk-!8zey#N!C(in`qUom2A~^E4{*aS5{OCp z$EM;UUC#6paw8EUjbb`jG=(=3&FaS!BK9Rn0>|hS*wUDL^3F!Y&t2U|;fp^;ckq#Hp z(jZ+FV2}_XJ5C* z2u2JYq4n(P#hB<39oWLb+}KfGeXpg%lS^d)t2Q9XZ!9Exu)e0?4uf=d_k9Q@g{W~9zpx^!ujvx0;@@9;_**8Fg{ z^!hi8fRud_Em?1SAFMalpWXar!EWu#^meBZTthEU<|^nK{H_uhMt@Zi5B!;Fy6kF%eepENDwcmKTH zfekE=X1C>TS+`~Z%*3B)<_(U0IdpY3o}B@s@jAX2IX)EbxYmT*-p635+m>M}@^G5nYR_~hF*j}e@_u;nmw;Lf!A+N9I&~tH9Ww}-pY1%2XRv~Tj&FJh zjcHFHRt<-T&;j<^=<0L1kZ+rhyZN;Hedgw)rx&|+c#aLOjf@KiA40JEGz`dMi~(XH zF;Rt=Xk8Bci|`7)CR%@_P>ma*4FQK~raM}-hVj?GEnkcOJS@(nWK+O3r-0 z4Fm3;cRxzyQb#x+y!QM1(t=frqeo>jSjvmp>o;z@qz;Iii7Gcl5{t2x$A!B~d;a$y zE}{0|>t@5Me@%VP{`>1Mz4X!oWN1F(B89VK(^rs?zk!|R4flJzo!Q50@TJ@|FkJ_? zZ2WQ^JwKX1G;cvtSX3g)T2a1!T;hc})L{47Gg;Z^5ENZ05=@yatYkU*I(vpiQl?SU+4ubb+usVTT!J;?AS_slI*h%NQkZ&kHuMhq}X8g+B|9T^LG zK9o% zOa{(yCxAiEeiAlV?<=8LLQycMy%6=|SDrwkWYJ_>X|uY!qwvRg`KMf2WX6(Z_di6> zTEPrRo)R)rQ4QCSe|(Y@No=Q~_2mA&J9q6niDW=||2>hbZ=+YPUVIfn(=(u(W zgT3ByKPZeK!HAj1G*dPBg2#_P^4RZRczV_RX-e8#Ic?dqzkhjUYT>n`M^D}=?^gLw znNnVkV9Z`^(hZb^8hRTH_ikUkdi8zU2iC6T!UNL-WjsZ^;p&mYhYx2`YrS573Xd5* z+fQ7*bPgcF4>${gA@FvZtDLs_cYprNTXZJ1p7BuB0%{%gN%}MCvzKG;#=M|sW+45P zo)+ETy#A|izWer5fNBbqx3$!IW^J8L=G*AC*%aJ^dG*~2O zYf5%+y5l|P@n=DjzwGyaeB%MbiSx~7`Q(^IGkkhWubtlg^bAmZ%)opJ>~hu!ckO!a zg}2^*?ZpRXgwWn0GoE<;?RTG>f8y|_?{=NP)1gw#om)_VP)2_nGEf-QRhu_dl?5k^s>ZZP@uH;T} zJ)N?nv*hxv7E^!%QEKFz+f6`$3|W1aE?c~O+2R>U$19wo#umquOCNvjmB;2rC}&Mu z`6`*g_s(+YMm?o?8;lv2fgi;M7&m#Ayn%4kO2L`*30QUFBmd5`FBw zL$&Qx^y~-zlu3OnEFk@$Lv|wp&a4qW`smL6yY?PFodd+Ix4PoTLwk0ezLLK1@y8yS zJ5{Mtl$ItZBLZMe7W(+5bxHzbC9w;ZFJBG^nV!BkDTwqNksI_SsDL z3tnsY-kiI7+#D~Oqpmo+?>h+gN28U;DtJpFyybp)%WLqKx8N;L z!dqU0x8(XKpu4|l?_y6WEi?{8-b|$FFZ%I+l<(fGWyS4xNsqSOEHN+ zB1r%j*a?{2K%Kk)7X@Om9#uUxtE$tM<4yWx!2&WLO8lf=z` z?7^p2Oy=ubZ*IF@M2b#>^_W_O?Q4Kh6c*k&FF|0^QQ`BJ#7q-wYBd67Kqnwt7+*$^ zPXB`&P{pE!)!sd1aoZABKKbO6a2mN9xEKD=zxfxM+D#&om)vg&K0Q!4Wbe~=6hhel z;eIpd&|py7EZpc=@G>V>e6i_Djlqi@q0r@@+_@e#{qPfc1Gpw*A?BFr(cN62mj@i| zxqEc;#(l+l``vq_pZSm6k5AsccMwy;$yfMa>^k`Z4nt4huv;_fcdx(xI()#PGB^xQ z`@eY%dcJprjO}LVI$BI4(%_)@M1OO0aRH=&ZMjuE5RjG{6C&mCdj}mtFGXA$I5y$3 zxgrk6%$Xc^m)*_rORj7h5o2VNRx_f<6hFD8qd6IO9Rn!-dlMaPXL^g6ZdQLKP!tH9 z!U3x@4O{=s=+o5Xg=ie#L#g6kUX|>_4pxMxF8{-u^QeX!XRt}iKU;URF5;<`Q~fE8 zp;^sMT(J7>Oln7bMhpYRN}FFrMN_X@p}4o?%RpZ71n24Hhe{btKUn%&S^mPCjSS2L+w}j=A1=hq7|FCd+LTuuu`=#zkm7F#neUwBjorbV0p&! zhgU3{J#|t53;V?35gVEE&E*c3()cTCo=6lZ!9Q7q43td<9Z8fCQ_@n?VgrJL;lO=@ z;-|*Qh$#c*l^t!Ad{uPK2+~8yfHjutLT4h?wq(Kd$)avh$F*6-Y#y+6q|IV2hb%-wWaYir13y}e8gw(V5~`IUW$9UB^k-1eCtyt`soy zZ6)QcfYPlAocq_m{uQ~s5Leua$5yRc^}-u}cp#R67;@4_OR-DZfnzt~g=5rN>ND>L zB5=KOwAslCuRBXftEi=jLeX+QEfA>#Q(JjyMO`DVz#vwxUI+m{PJQb4w`cCl0PfAt z+Jwv>kak96Uvtlh3Qa`Hn>G+OBMQHp-5vYD(@#JBn-_kwI*H^aLte_HHn9Ho>KpGY zpuVDZN#C3sfp`EQ8$UFfsMQ@6G*Wr>*9QiA^e~=QvrgUBqrrQ3*Ouqzl@!)?G&S_~ zwqRz>&EgJ}{QG-#4l|HbU8M-hI! z@15O>?>^Ys*5+aF@QNAJHOx6eet^PqaH*@%wbRhGqtLap(6z z>Toc2=3p}rxeqAz@3x*L-_uA9xxh`(Tf8ih>MnwnIrrT*|2A-4S$|jQSq77|0C`_z@wHRC))2P(l(?NJtOqb<^8! zwq$qi@0^_t0RiLxKKHqq2PRwcn^WKO_IIUl)WzL+^*>?UCdmH6vP&C5`v0el{q5qi z$0v<^{poq*12wD+;>3JG!Ia0NQ4>TX{Osk)ff`}pI_WA zbxnW#fowo!tOR*;RGKJuRFDeNURt&bID3a71`im{ogR2>27d0JJMF=jUw(P|;231c zW33g|Yd)j!Y1DWVT+6u%H$7Ip`Rwt;#7mdVqnE_sBtAzj)n{{@K&jKK1uB@UXHCOm zD`5R%;s1i-XHXX`0;}|{`1_6D{brNJcYU|LI0IwAtqGbAeSfum6FQlULk-EZ{l*)u0Wns#ytnK*Oy%+Vod z(o3r^rRJ8mXq4uL7d=YSPaHpaxui)G+*Fcy^7x7Ll7YzUz==>?6T3pC7#RkE!kM$6 zs)A7yJX8#4fVzF5a03|RFf|0C)qwly2qq?EU4fultV86WeAUkLpMUq=cNq$V=6^yi zcHIF|L2(mOg>Q)B-VXR3ZsN@fS^sHs=gxiLk^f-pKk@L`4!ATO%`pamEGgKez4n>b;Dxv^6n(oe35oIe&E#DqX3(4IL^^xRbF{$|V=3`(c3 zz0pw(^|;A3ZgWCG653cT5aQf2`=74@Rl8B7N2d9q167_uLow_lBPK%)IBEJc1*s-! z4!RF95{k^_+2Ic^{dnm!5ObaU$3Kf-9|~W)KQ({zo|9NPtg#p(t;{cV{}(I1*q&6S zkyj;c`(nk)1F2J;%!o&L*VhLQZue5@o{q8gAOK0QXTt2lzQ^lwz+f8LUrqWcUytM@_J! z&*1RLXdku`yOI?u9ZAw$SEt%4L>frdlCGvktckpO1On+pQs4x4x%|LY5SuG!8!@^8 zp+iDPM@L78NTV1{_>u?0Uxc(bQt;?I@4WMP-U=YQJ;ntBN| zbroppYS7dlK~qoB|$e3?$nn+)JVpAU|v7azm_%xGq2rY=FJPOJv^&Szu z@WnTmE(7DB83xmr3n>v4~h)B1$Xf9|z6-(0q8?f2_{J_zz9tIth5ylee;YgcI^Lqc>$l9R{M zR^2J)8gvcSjiw6h?ZOrm8y2-$;5^ux9_f!B`zuI20{ll`M5edmBTJiDgp}z}NqKJt zoiKHE8b$byz#@Eb(SH^__UJh6)va5jna#{<$!E*ne0Js-oK^ngpLu`TCowquwnEwP zfq40>r?1#4zNSw!pQkpsdBr3Pm!pTd+Lox@CfT5$tj~^DIiA+mt>LQ7tOfpYPbqA{8 zI;iEIIO~I92WmyJX~;;@;A;(73SC0mR0+HAqTyPw6dxH3;)YgCRbia>LU3V78f~fr z?XS@m?b*9y>(4)*k%uf?2vwCd{;LhY9lMgN)kx|xk8j`bEsXT9GV3ItzV+%evq5L1 zoqDR8}VjPls7<8J_SWt4vO+2D9UnBlmMBmrK}C{pKscvaj{q=4oU>w`no6`#1`y+u3Hn( z&5_+NabsW+$C{Q28Bhkb1K9%#2bDTrsPaWVR6kCRjqEPx$OhHwZCd{)cN{u);&e($ z)7OqJF)U~X^Sa5e}gJ92pvQ+BE)!_m|$s#Nim* zNEg#*qAyl&xl*YS8;njPG3rYH#fzUB=TLoo`^JqMqhS(T%`RT7aS;l;4RsaZkt^y9 z79n7M;Y0{`UaVpBehvLkpR29QD?&Ms1DTy;%_y=ZOBhZlJ|94g+#IAD6{FP zOF{-kj1M1-tt;lNj8G!NrcO^ULM``@5nxl#Ca0%s26}qRVEBP`#AFf6J;h?Z1PRde zdY!Bt`&FB9=@X=<7aB;~@^>LFzRP{) zXWCeI67SFTvG z@=H=iW%K4)c<%>%HV5B$65o7|o1h>$;|&8)F!%I$QYhfRg4pHj;smgeveAZX9wrDKjon>1 zW*fV@8}Y*ALHQ$7eT&JYRtr%$(pTZjRSi)?Ac4+b$Rxbvz@LmfcwAZ5`?AfyyuIRcuOG-NRdZ7mi#pjgeB;dC-S4(=R&27btrgF-kbtAVv ze(3i?{_%^b2r6!Y7^K8aXYNot-1UM2Ya(u*~kb8VbYX6QV z5|msTHWb4-G)(RTXCxPqNXe1*xel%BhLTsQCLFCo=B31o?y8}#q~$sk=xS~4YG|pb z2&v#auhpS-T_zM{((5}#g#W9%=7|zz6)olMt*xX=ZJ;aZfNS6|!tzX}Eqv=8X?lbSD1v z%~zjqJ6|ku>pHt(!MW=6^FG z&7Xu#_yThrs_QMxasd=S_#?56ruUF^yhT2l2^M_|zIYse@E88@N6G)52?vsFd1P4F z#3*emgpF2hWmIf4lW`?Q%Qb{R7Lds3Mt71)b67MzotZ{K#7F4iv7b! z2D!-;CZ?Np4ILg430<&CM5-10s6}R?{W8aU5plISf9wDUxzstxmQ}_s6f#Bzd;wlL%WTwbtV>bWwkRgw!=)6O3J z`TLomeGkKIIS^%*XCZuQsn^!sm+IMeX9%}mGh(LWCuW8hUO2Z;=RDv~T0kz0{IhY+ zkIWfArr>%}c@rmk`o*UR&j>zH_C0aILm(v_Qa-{4egvMxuSi#J-*cVppB@5rE6*ta z@zJ2-n3mNhoFHC6}vIh$FL#$TdzadMQHD(kxhDnG9=!~K;)V{4sFYX-SlZIheD zZjELLlCTSey88_t-rCVpSC*Bbl&L85_sCrM%f(C`;#7v5c7~#>gIrcZV|E*>C;%I% ziG`h5J_BmT!5EQ9koHO3SSMdMad#uhA=lkfICYF912WtH7coS?-S)L6P|ekZJ^%Ia<^s$N7YM%k;Ua z+OmePR>knV**9*V5=XS>CwW{bX{jqcvn|RG8_IYO-cA7CP7~SD+1%JK1bI7s0_wDF zjE|IP>CBem{Gt|%yQ8$bt)e<9y{N9;C~~iC!2#Qz51p35(2(Db7cG^JVj&0;Qxq3k zTXmz*-Oa_zxBtM#rY6c?{M^x2=RO?KW;q08UHeYqYdfjOwQp zaVp=`;*QQbM1R9Sxh2@qbHe;6CRV=m!HI+Y`VAcQ;M*}c9=9@yOoLB;D+ZIJiTIF- zkHd@Be19=Gbp!}GnHqAkYM>ib1co|$%Aa`^mfKa#I``G<;UfIY-YZqZe4V|6qZTF+ z{}VakuMTbBfV>PKFW`h$i9dMtnFqjM3BX_ReA0)YEn7ib-UMy=2(;yYpe>}f1&K#c zeErkU>*voN9^|5SBR-ig4T(Lzf7dTKigxU~aJ{fn3nQ<#{AxxBH0ISPhR=oOcHxXE zLzw*I(AMuhiwfR5|NJc)hX3zGpT76Xvo8?u<4@hb>nuEb7-q^tr!#8=!6RT99ucaX z&Q8KLl$9~5g7?N(jqOXt;k0nY-$6*5p z3@)#1=`@-RkyA!X1aV%FCmEz7!t#P5$3h(D@!AM&Y`hdIG^HH+2#u=XMtz%4P`^N5 zwX8TV8->*wL}amCyf2AgK(GtrDQ~@z20<*nSlg)#9)oD-;l3JS^n@|vrh{}ngI)Cj z_vwiE_=O8+xhcEbAp9ZDzo4|9EZ8oiBrPo~zq$+Ypm5(nMIc>caYhjs>ZC+6!5o zoKn!?A+9i4i~yb$sHO$|ijb?S(y0cF8#`toWC(dvPGV|$eifu*)sV1YPV{*gynY$tmr~$nhG%ooZ^3zk>T?(qd4rY6B*J((au?~kF!az9dGrhQ_pU50E-kCB z3+j&+3m9FkjuFKBqG%LCKUl7|g>?yneKEk($4%H=oSB(f-KrVaZ&;|CN{Gn4%4U-| zpt7<8sfv-dsIjS`p%JG!_w0K$q_?>Rh=Np zA}7%`s7X=g>i@PQJw}WeGN8XV%nZH*z16rmc=!(-(btXXCNY*}*_2;F+5hHuyjIv()V=>Pwnp3m0hX8 zzAHH=M6~dP2}6-uq|ZC~eB5Mb&+z-_#7qbal%c|ix3{MhdK(l+>Eni`e0mD@^EK?} z?Yz!`co6-9{k#YJ*}1yB^yCj)VA(tJec4qqQ$NmtqWNW7tz{v?gx+Dl`(hSoJn;yA z3=T0ggCFK(~YOY?r6 zkCv0`d7M>553(2mFVL;w)${oOHSROlBaLi8SoqlR(MXUqAXECNZ1^hIwGq_H{m--M)V~eHVT*%AH$YfT;F{#naAIx{+*Is((1$YEO{19XyFYtAl z%qi@VMbAewYnWr6SJN_c%5;!$%~i$ge&T#_lV*^dCn*wZb3to8Ij*Eg`rI(%H-Rnmz`?rzR- zE(4!~!&}xW(C1xFNlS)t1?4g>t@~o*P8iMx$(qF^Wver1YQA=8=PzkxVj*XND_=*4 zo>RyK7W0oAH*DCr17CHo&N#Jw(}s;duB|b94V(4oqmRxWr0kYxA`hK7RUsFEEpApG zB-MqFq!hPeEZQ17x?M+(95ou2W7oROMi!#CNFYFphDHu)L7Eg9jTpJM!Zs1o-ZE^w zT+g(p9y)aBAc{uGRM(Gvw_@e`E9IIXNh9M3x-9Qvn$j+uJzZ2OldBySgv!WV`-O&t z1V_WH`@VR^s+H^TbxHY^ec!EG^*J@Ei=2ZdPFqY&B)CLNriJ>sXoeyWXB6GL@ItG?wM$WMyS$T`xx>AjBYBxwrs<&}tDu1*YHx1~}{LtwO8W4C|y` z*VWQyWa9)ZV~mq^ivlJ`M~w1y)C>aKt|Qucfl%%>c+?nQwHaKYK#b-hsfLvmiNisn z#*M~T-5q`Vheu2pA3okm+*wxubxT2mSBxoEH4JF<9evW3?DGQfw) zh?8jt&w2C1mluZlYJ$3)jP*$WRM**H2^ukRN~nKbeN#)T4y#h%R9RKu(ndmAmc9PU z%Zu=Bsc+bQPrmfZYac~1s~sfm*Asq)i}<#S-!V|Wi{NCgg7TdJ zG7(Sd&a!Z9!S$sbS=J==a{wPSvq?G%^pgixu1sP0M3_kh*p4cA>kx&4M z_Lqvwu7b(!jKVS@>$4WLa%fzuOXO^VZNXgjp?I2!G5cQ{PfqM7S1T7W@2euS;8MNOb&Azm5Dy>4A*eZ(3@uL;)qmKv*z)y?W_4YWpDpN0Psx!$!d zwf@)ow)JuAJbZUCeqWC;m9y3>a3N~tuX1aO^)Kr_>uv-{Zh*g}hzl2e4qejbnb0DH zAwcITdM)5$#4bX};34-}f1vc1(A*Mu9({#KlPaVgCGm+=Ga@Cv1kcZeSzG8ce$EoC zi+)Jli62onoK-L!YB$8WxjA7Yw-yy8Lq_{6G5OFpe7yL3fx7m{w0eDA3Y*eO_+U#lPQ$0 zA;ZIl1o=4;_Fw5SAS@zcM39?GhF7lrNA>sgbn*-g%_@LNEX9j_r36`M$3)*h zYtF)Ff7*Ba{P|0{MJ*b~)~1q#gGz{^sFy5L$VBlHGx92kksy)$bjXtU&gwE#0y-w6_K2)0Kd9MG+DDqY~ zt4J^E@^EV9RKpp;b!1u%y4PG>E*=1V->}dQV3(O3113KhgZwfNfLy#I1PNM$v>xoE zLzl}#8&GhR>uT??Xy9XE?T(M^(v2O1GcA0o83`QDA zM<~q)jzJ0=%$mgt;Xi&P5@#C?CXx@jO)Dk#*-k4W{)qrtPg3H$ar%~;au+3M!XeUx z+DFUf(+WF5MPve*Koco)6cFEz&ivNYJ$v>XxSEojTwR@Z5y$?Ol#HZ5H~oC{a>}LC zXS2W-*B2+9*|%%gp-Y)LB&;c`AhDpa(W$XI&xye>geH?UY1NiM|BRCo)$Vd1V5@Vp~BZT8X67krS=O#om%|Z!$ zR$$MqR+G6>b+_auMuSeS5&!Q);!up(7WPCj_00h7w|RKTf)HL2B)~rw|HSwQcMMh# z%{`6m+|e*pLDG*ED4-)i;$5q&Imsw}T zMOzJ)S#it?0%2l4B@nD95~$@-ux2P3$vcGXk0-woz54&xxb|_lb9=c(mfd6=0{C&r zWuBl-Y(}H^(MDH7ZABXGj7C4tkA)IrO#EM@XuT9CtsEz<6eq15CoQ8y`VEk9$G8;& zrHYBNJ{7k@s2szL!A*CWnT@w~Pcc!fKY5);Mn<4K0Bn}WmH}G^Yzf9$0&EGeC73@6 zL+6iCAo&1NDKKLYMb8?DJp$G3+2kD7#IVUawu`(IjCaPV;Gz>tM);o)k=DS6AR>@S zWkVc|zn7pkJj0HjVna;FJL&a(M2trA0E&3x7DO3Ee54oR`Mx4DFoaS>_^b44<0*<5 z&#d$Bk%VFI-6XZ!4te#o(($$IGBkDdI;i20F;6?l+dU=`4(XxQm| zN9t)uvPnm7T^Dy7p}!-0(eaVe@rmJK>nQBs*T&sY?cUqDJx~&`MFS_Mob;gH?A} zAxf-}Si$e40Fn>pchK@!LBq?v7Kr-*+X9h0_biae;GPB2T8zhF`CLuA+v>Q}xY&D8 zd0P)0Zs|b_?LlTw5B_Pi?WlEkM4ifyiw!mHU!&f6wC$+2(N6u>s1>)h71R{;&5qhe zN1*Uuqu#l#x2}xKeMUXVueaQuwsLfDsAyX!^W!R{Z9Up&RJf;5+G*`Ul~U9~it50l znki~=pHWkIRQQ+j``DSHDkLo+W0Ns&Z|A`bfCQ- zPTTo7-%dSk=hmKf%0U@=^9@p-Z;%qoz}g7Iidfc06jsCv2tmM&&;#5EEx?V?0o(`~ zuyJN6X2Uvx-VkrxOE{4-fO;OizYJ9MZW?Ix>)u1>&K%mlZeWlx zLkEzOmUuSdQiV|l%LXiEl_i%hMMoo;61srHKYj@r{WJNlvJO9#IHaz%VJ;tgLEsb*~0xEg;dBw3j>>>A`B{4c~5 za(yiMa^0W5|MKnU#J%fn?la%@X{|vBj~jZURq9l^V#W5e>Gcq&Ek;xaGtFc+LSXt5 zT8Gt$6v4&5vtWTWA4<`!^xA>{f3&W%evN$0foRWYINbWvYdrU;WaRu;Uw!qdM`FP0 z{{XkQU978hI((Nt!h@G^i-a40SV4(?V1?PJEiZx#NM1#8UQrq6F%+`iAa{|W`P{id zgNQ$F<<-+WHf{Pk2Kp3|5@{<^B0WDd!XIHQj+C)5Z{L~n) zs7Rktqe5KA4}!cJG;olQT8;F7`c8N*bSoq7Td-ik-010ZgXbt*Ts(sy(fKGxlG0P& zK|ulD9%K5)OdshN5HffamTRE1*%{kmKIxd=Fkb# zW<(7eoO0=0!bK!IB7V79$m6|l-^VZ9KQodfL~?Nn89p`Uz8McLGUiX3I(;U|lcb5X zN+r;8pqOlzfkSZiAYX;PsUD)8f$^L^VL(eu?wJQ7AP5HqhXpj$9@u~Cz?myou9U)> z2w(5X4^5uz5OuY%qL$P^DL6OUGu~ODY^uo18!+(2=cC+35fk39L*v#-A)zWVU|r(S#EiH9RK;}Z*Sz^<8@bZ+0lQ%ToLTdPa2 zocs$>g=yD9o_b=y%$b8kkA}ldEHEr=P)l7>^7ZWGix)4}h^IcyYfqj2`_bZO7q=m$ z&HzMyxwK6SjF&nGx=Us3oTsChSlRzxn-j}H0XsHBx?bZtu`CGL=sL!O8#`!Ur$5;?a7OYXU?|N)mIe5AXD9zkOpm2US?iF zNoiK4JSDlfrMaV#oF9SRVxbkujc?p2tt>AouWW-QRV3mph7Nskv4aB%?rg8ED!i7I zT3zioaM9VQTFSUNIAvV;e#7pJ4&)yNnGVz|BwTfBLPA1fYVxJT ztJPf$v^Fdt5Ii_~+Jc9lc|xQpD#4vc4qGwg(Ytc6I3Qphy> z+E^XKR!VIWo)Pc(XV@h?yHtJ`E}nIZhs4N!w2#P(u>vc_yV^!LDo2VJE097Rzk;p% zH;8B-5YfI~h-l%hh}v5aiKKkDHU^L$M>Cr^9uuyLXwhG^*A~8&a@xw)9!x1CVx+*d zOZK;%&`@c_lcRSgQ4R2Ij1^QM6C+*;2zsnQMgU_4ozV2#PG_PkQ1Xa&d6DP}s5BrP zNFZaG;gA5aXZPV(%knrno?!L&5$IS(hYu?WJdqUR=o9dvKY7F*r9?Xi)LTk~liJgm zF(wc>3G8#Gz-WXv(>Ze@YkB7`IW0U#-2tm+3EOYKIyd;QA2bLn#s+C z$RXUs1eAR%azx*rG ze*=o**mNAwd$HH9lHONV+jf8!@A$je`~FXOg%nSI3!a+diF@H)?=zkw!HZ^A_HN%X zikHy~&-HJ3cU%2>+P?98`^emo9yd`$M~X=1oAkM^2a~MTyGpJ?y$WR-ZF1{)Xs}RS z2x=3oO)G)MifqbcuHr4C*l;wVJfuyrAtl96@M&Eq$$DtlYW&ma+FBoV%jlNTwkvy$ zhPkKZ|Af~`M`P+Ocv_0*&Rg;8jyRc`rQu-|+4>TLy|Z`nK8XponUIw#Y!P zU>o|3+4mKo?K7sW-=RJ2w9r-6K-($f+bN^%Z0^Cl_o_0fdsbDbq}Qsl@T;mJt7lco z1U;)tMjnDy)qKZQb+=LezqH=IvhIedrZeZ?Ga@=VBIR^My7&=M(Gh9w8Iga&98NJu z^Omz;t2H(m`5&HG*z1Y%yF8K3 zE&oLE9iC{u^@;y9Zmwk9;EZddp|OGvI&QXIWa7t7PRGsIGj8%A+qrR<`30{;iJpde zyk|Ph6tolpxl>zv>G9b{I>#a!8?)Ai=|*P{e@?AMv4RX6W`hmWLYf&X z2ct9vAMhb0Ti!

xL}BMd8pz4i!dwo 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

;gAF=Jg;bSMR7Hh+!qsKh@=p*ye#>9dp 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�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 B5%2&2 literal 0 HcmV?d00001 diff --git a/veilid-flutter/example/lib/log_terminal.dart b/veilid-flutter/example/lib/log_terminal.dart index cf33c1e0..b19d4af6 100644 --- a/veilid-flutter/example/lib/log_terminal.dart +++ b/veilid-flutter/example/lib/log_terminal.dart @@ -42,7 +42,7 @@ class _LogTerminalState extends State { textStyle: kDefaultTerminalStyle, controller: terminalController, autofocus: true, - backgroundOpacity: 0.7, + backgroundOpacity: 0.9, onSecondaryTapDown: (details, offset) async { final selection = terminalController.selection; if (selection != null) { diff --git a/veilid-flutter/example/lib/veilid_init.dart b/veilid-flutter/example/lib/veilid_init.dart index 373ea012..7d0f8fb3 100644 --- a/veilid-flutter/example/lib/veilid_init.dart +++ b/veilid-flutter/example/lib/veilid_init.dart @@ -11,7 +11,7 @@ void veilidInit() { enabled: true, level: VeilidConfigLogLevel.debug, logsInTimings: true, - logsInConsole: true), + logsInConsole: false), api: VeilidWASMConfigLoggingApi( enabled: true, level: VeilidConfigLogLevel.info))); Veilid.instance.initializeVeilidCore(platformConfig.json); diff --git a/veilid-flutter/example/lib/veilid_theme.dart b/veilid-flutter/example/lib/veilid_theme.dart index ef7b5b0a..eab7564e 100644 --- a/veilid-flutter/example/lib/veilid_theme.dart +++ b/veilid-flutter/example/lib/veilid_theme.dart @@ -241,7 +241,7 @@ const MaterialColor materialPopComplementaryColor = const kDefaultSpacingFactor = 4.0; -const kDefaultMonoTerminalFontFamily = "CascadiaMonoPL.ttf"; +const kDefaultMonoTerminalFontFamily = "Fira Code"; const kDefaultMonoTerminalFontHeight = 1.2; const kDefaultMonoTerminalFontSize = 12.0; diff --git a/veilid-flutter/example/pubspec.yaml b/veilid-flutter/example/pubspec.yaml index fee9152c..a781e59b 100644 --- a/veilid-flutter/example/pubspec.yaml +++ b/veilid-flutter/example/pubspec.yaml @@ -94,3 +94,11 @@ flutter: - family: Cascadia Mono fonts: - asset: fonts/CascadiaMonoPL.ttf + - family: Fira Code + fonts: + - asset: fonts/FiraCode-VF.ttf + - family: Fraunces + fonts: + - asset: fonts/Fraunces-VariableFont_SOFT,WONK,opsz,wght.ttf + - asset: fonts/Fraunces-Italic-VariableFont_SOFT,WONK,opsz,wght.ttf + style: italic \ No newline at end of file diff --git a/veilid-flutter/example/web/index.html b/veilid-flutter/example/web/index.html index fe1c65dc..128d80a0 100644 --- a/veilid-flutter/example/web/index.html +++ b/veilid-flutter/example/web/index.html @@ -1,5 +1,6 @@ + - + - veilid_example + Veilid Example + + + + + - + - + - + + \ No newline at end of file diff --git a/veilid-flutter/lib/veilid.dart b/veilid-flutter/lib/veilid.dart index 9b194c02..a46993ca 100644 --- a/veilid-flutter/lib/veilid.dart +++ b/veilid-flutter/lib/veilid.dart @@ -1474,11 +1474,15 @@ class VeilidStateRoute { }); VeilidStateRoute.fromJson(Map json) - : deadRoutes = json['dead_routes'], - deadRemoteRoutes = json['dead_remote_routes']; + : deadRoutes = List.from(json['dead_routes'].map((j) => j)), + deadRemoteRoutes = + List.from(json['dead_remote_routes'].map((j) => j)); Map get json { - return {'dead_routes': deadRoutes, 'dead_remote_routes': deadRemoteRoutes}; + return { + 'dead_routes': deadRoutes.map((p) => p).toList(), + 'dead_remote_routes': deadRemoteRoutes.map((p) => p).toList() + }; } } diff --git a/veilid-flutter/lib/veilid_js.dart b/veilid-flutter/lib/veilid_js.dart index 5beff613..d19ce2a3 100644 --- a/veilid-flutter/lib/veilid_js.dart +++ b/veilid-flutter/lib/veilid_js.dart @@ -185,8 +185,9 @@ class VeilidJS implements Veilid { } @override - Future debug(String command) { - return _wrapApiPromise(js_util.callMethod(wasm, "debug", [command])); + Future debug(String command) async { + return jsonDecode( + await _wrapApiPromise(js_util.callMethod(wasm, "debug", [command]))); } @override From 78d06fb18705b1bb9c87f0faded31b70eaa359a9 Mon Sep 17 00:00:00 2001 From: John Smith Date: Sat, 10 Dec 2022 19:22:23 -0500 Subject: [PATCH 50/88] route fix --- veilid-core/src/veilid_api/debug.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/veilid-core/src/veilid_api/debug.rs b/veilid-core/src/veilid_api/debug.rs index 23aca13e..06e884e7 100644 --- a/veilid-core/src/veilid_api/debug.rs +++ b/veilid-core/src/veilid_api/debug.rs @@ -707,7 +707,10 @@ impl VeilidAPI { } let remote_routes = rss.list_remote_routes(|k, _| Some(*k)); - let mut out = format!("Remote Routes: (count = {}):\n", remote_routes.len()); + out.push_str(&format!( + "Remote Routes: (count = {}):\n", + remote_routes.len() + )); for r in remote_routes { out.push_str(&format!("{}\n", r.encode())); } From f0674e46d178eca8c5f30bf662662cca12f160af Mon Sep 17 00:00:00 2001 From: John Smith Date: Wed, 14 Dec 2022 16:50:33 -0500 Subject: [PATCH 51/88] xfer --- veilid-core/src/intf/wasm/table_store.rs | 6 +- veilid-core/src/veilid_api/debug.rs | 9 +- veilid-flutter/example/lib/app.dart | 116 +++++++++++------- .../example/lib/history_wrapper.dart | 68 ++++++++++ veilid-flutter/example/lib/veilid_theme.dart | 33 +++-- veilid-flutter/lib/veilid.dart | 63 ++++++++++ 6 files changed, 235 insertions(+), 60 deletions(-) create mode 100644 veilid-flutter/example/lib/history_wrapper.dart diff --git a/veilid-core/src/intf/wasm/table_store.rs b/veilid-core/src/intf/wasm/table_store.rs index 74478877..ba2dfadb 100644 --- a/veilid-core/src/intf/wasm/table_store.rs +++ b/veilid-core/src/intf/wasm/table_store.rs @@ -101,9 +101,11 @@ impl TableStore { let db = Database::open(table_name.clone(), column_count) .await .wrap_err("failed to open tabledb")?; - info!( + trace!( "opened table store '{}' with table name '{:?}' with {} columns", - name, table_name, column_count + name, + table_name, + column_count ); let table_db = TableDB::new(table_name.clone(), self.clone(), db); diff --git a/veilid-core/src/veilid_api/debug.rs b/veilid-core/src/veilid_api/debug.rs index 06e884e7..724cf6d8 100644 --- a/veilid-core/src/veilid_api/debug.rs +++ b/veilid-core/src/veilid_api/debug.rs @@ -445,6 +445,10 @@ impl VeilidAPI { Ok("Connections purged".to_owned()) } else if args[0] == "routes" { // Purge route spec store + { + let mut dc = DEBUG_CACHE.lock(); + dc.imported_routes.clear(); + } let rss = self.network_manager()?.routing_table().route_spec_store(); match rss.purge().await { Ok(_) => Ok("Routes purged".to_owned()), @@ -865,12 +869,9 @@ impl VeilidAPI { } else if arg == "route" { self.debug_route(rest).await } else { - Ok(">>> Unknown command\n".to_owned()) + Err(VeilidAPIError::generic("Unknown debug command")) } }; - // if let Ok(res) = &res { - // debug!("{}", res); - // } res } } diff --git a/veilid-flutter/example/lib/app.dart b/veilid-flutter/example/lib/app.dart index 041d427a..f630303d 100644 --- a/veilid-flutter/example/lib/app.dart +++ b/veilid-flutter/example/lib/app.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:veilid/veilid.dart'; import 'package:loggy/loggy.dart'; import 'package:veilid_example/veilid_theme.dart'; @@ -8,6 +9,7 @@ import 'package:veilid_example/veilid_theme.dart'; import 'log_terminal.dart'; import 'config.dart'; import 'log.dart'; +import 'history_wrapper.dart'; // Main App class MyApp extends StatefulWidget { @@ -22,6 +24,8 @@ class _MyAppState extends State with UiLoggy { bool _startedUp = false; Stream? _updateStream; Future? _updateProcessor; + final _debugHistoryWrapper = HistoryWrapper(); + String? _errorText; @override void initState() { @@ -136,51 +140,79 @@ class _MyAppState extends State with UiLoggy { body: Column(children: [ const Expanded(child: LogTerminal()), Container( - decoration: BoxDecoration(color: materialPrimaryColor, boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.15), - spreadRadius: 4, - blurRadius: 4, - ) - ]), + decoration: BoxDecoration( + color: materialBackgroundColor.shade100, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + spreadRadius: 4, + blurRadius: 4, + ) + ]), padding: const EdgeInsets.all(5.0), child: Row(children: [ Expanded( - child: pad(TextField( - decoration: - newInputDecoration('Debug Command', _startedUp), - textInputAction: TextInputAction.send, - enabled: _startedUp, - onSubmitted: (String v) async { - loggy.info(await Veilid.instance.debug(v)); - }))), - pad(const Text('Startup')), - pad(Switch( - value: _startedUp, - onChanged: (bool value) async { - await toggleStartup(value); - })), - pad(DropdownButton( - value: loggy.level.logLevel, - onChanged: (LogLevel? newLevel) { - setState(() { - setRootLogLevel(newLevel); - }); - }, - items: const [ - DropdownMenuItem( - value: LogLevel.error, child: Text("Error")), - DropdownMenuItem( - value: LogLevel.warning, child: Text("Warning")), - DropdownMenuItem( - value: LogLevel.info, child: Text("Info")), - DropdownMenuItem( - value: LogLevel.debug, child: Text("Debug")), - DropdownMenuItem( - value: traceLevel, child: Text("Trace")), - DropdownMenuItem( - value: LogLevel.all, child: Text("All")), - ])), + child: pad(_debugHistoryWrapper.wrap( + setState, + TextField( + controller: _debugHistoryWrapper.controller, + decoration: newInputDecoration( + 'Debug Command', _errorText, _startedUp), + textInputAction: TextInputAction.unspecified, + enabled: _startedUp, + onChanged: (v) { + setState(() { + _errorText = null; + }); + }, + onSubmitted: (String v) async { + try { + var res = await Veilid.instance.debug(v); + loggy.info(res); + setState(() { + _debugHistoryWrapper.submit(v); + }); + } on VeilidAPIException catch (e) { + setState(() { + _errorText = e.toDisplayError(); + }); + } + }), + ))), + pad( + Column(children: [ + const Text('Startup'), + Switch( + value: _startedUp, + onChanged: (bool value) async { + await toggleStartup(value); + }), + ]), + ), + pad(Column(children: [ + const Text('Log Level'), + DropdownButton( + value: loggy.level.logLevel, + onChanged: (LogLevel? newLevel) { + setState(() { + setRootLogLevel(newLevel); + }); + }, + items: const [ + DropdownMenuItem( + value: LogLevel.error, child: Text("Error")), + DropdownMenuItem( + value: LogLevel.warning, child: Text("Warning")), + DropdownMenuItem( + value: LogLevel.info, child: Text("Info")), + DropdownMenuItem( + value: LogLevel.debug, child: Text("Debug")), + DropdownMenuItem( + value: traceLevel, child: Text("Trace")), + DropdownMenuItem( + value: LogLevel.all, child: Text("All")), + ]), + ])), ]), ), ])); diff --git a/veilid-flutter/example/lib/history_wrapper.dart b/veilid-flutter/example/lib/history_wrapper.dart new file mode 100644 index 00000000..da1a283a --- /dev/null +++ b/veilid-flutter/example/lib/history_wrapper.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +// TextField History Wrapper +class HistoryWrapper { + final List _history = []; + int _historyPosition = 0; + final _historyTextEditingController = TextEditingController(); + String _historyCurrentEdit = ""; + + TextEditingController get controller { + return _historyTextEditingController; + } + + void submit(String v) { + // add to history + if (_history.isEmpty || _history.last != v) { + _history.add(v); + if (_history.length > 100) { + _history.removeAt(0); + } + } + _historyPosition = _history.length; + _historyTextEditingController.text = ""; + } + + Widget wrap( + void Function(void Function())? stateSetter, TextField textField) { + void Function(void Function()) setState = stateSetter ?? (x) => x(); + return KeyboardListener( + onKeyEvent: (KeyEvent event) { + setState(() { + if (event.runtimeType == KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.arrowUp) { + if (_historyPosition > 0) { + if (_historyPosition == _history.length) { + _historyCurrentEdit = _historyTextEditingController.text; + } + _historyPosition -= 1; + _historyTextEditingController.text = _history[_historyPosition]; + } + } else if (event.runtimeType == KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.arrowDown) { + if (_historyPosition < _history.length) { + _historyPosition += 1; + if (_historyPosition == _history.length) { + _historyTextEditingController.text = _historyCurrentEdit; + } else { + _historyTextEditingController.text = _history[_historyPosition]; + } + } + } else if (event.runtimeType == KeyDownEvent) { + _historyPosition = _history.length; + _historyCurrentEdit = _historyTextEditingController.text; + } + }); + }, + focusNode: FocusNode(onKey: (FocusNode node, RawKeyEvent event) { + if (event.logicalKey == LogicalKeyboardKey.arrowDown || + event.logicalKey == LogicalKeyboardKey.arrowUp) { + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }), + child: textField, + ); + } +} diff --git a/veilid-flutter/example/lib/veilid_theme.dart b/veilid-flutter/example/lib/veilid_theme.dart index eab7564e..53489d09 100644 --- a/veilid-flutter/example/lib/veilid_theme.dart +++ b/veilid-flutter/example/lib/veilid_theme.dart @@ -257,29 +257,38 @@ Padding pad(Widget child) { ///////////////////////////////////////////////////////// // Theme -InputDecoration newInputDecoration(String labelText, bool enabled) { +InputDecoration newInputDecoration( + String labelText, String? errorText, bool enabled) { return InputDecoration( labelText: labelText, - fillColor: enabled - ? materialPrimaryColor.shade200 - : materialPrimaryColor.shade200.withOpacity(0.5)); + errorText: errorText, + filled: !enabled, + fillColor: materialPrimaryColor.shade300.withOpacity(0.1)); } InputDecorationTheme newInputDecorationTheme() { return InputDecorationTheme( - border: const OutlineInputBorder(), - filled: true, - fillColor: materialPrimaryColor.shade200, - disabledBorder: const OutlineInputBorder( + border: OutlineInputBorder( borderSide: - BorderSide(color: Color.fromARGB(0, 0, 0, 0), width: 0.0)), + BorderSide(color: materialPrimaryColor.shade300, width: 1.0)), + disabledBorder: OutlineInputBorder( + borderSide: + BorderSide(color: materialPrimaryColor.shade600, width: 1.0)), focusedBorder: OutlineInputBorder( borderSide: - BorderSide(color: materialPrimaryColor.shade900, width: 0.0)), - floatingLabelBehavior: FloatingLabelBehavior.never, + BorderSide(color: materialPrimaryColor.shade900, width: 1.0)), + errorBorder: OutlineInputBorder( + borderSide: BorderSide(color: materialPopColor.shade800, width: 1.0)), + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide(color: materialPopColor.shade600, width: 1.0)), + errorStyle: TextStyle( + color: materialPopColor.shade600, + letterSpacing: 1.1, + ), + floatingLabelBehavior: FloatingLabelBehavior.auto, floatingLabelStyle: TextStyle( color: materialPrimaryColor.shade900, - letterSpacing: 1.2, + letterSpacing: 1.1, )); } diff --git a/veilid-flutter/lib/veilid.dart b/veilid-flutter/lib/veilid.dart index a46993ca..57598885 100644 --- a/veilid-flutter/lib/veilid.dart +++ b/veilid-flutter/lib/veilid.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:html'; import 'dart:typed_data'; import 'dart:convert'; @@ -1571,6 +1572,8 @@ abstract class VeilidAPIException implements Exception { } } } + + String toDisplayError(); } class VeilidAPIExceptionNotInitialized implements VeilidAPIException { @@ -1578,6 +1581,11 @@ class VeilidAPIExceptionNotInitialized implements VeilidAPIException { String toString() { return "VeilidAPIException: NotInitialized"; } + + @override + String toDisplayError() { + return "Not initialized"; + } } class VeilidAPIExceptionAlreadyInitialized implements VeilidAPIException { @@ -1585,6 +1593,11 @@ class VeilidAPIExceptionAlreadyInitialized implements VeilidAPIException { String toString() { return "VeilidAPIException: AlreadyInitialized"; } + + @override + String toDisplayError() { + return "Already initialized"; + } } class VeilidAPIExceptionTimeout implements VeilidAPIException { @@ -1592,6 +1605,11 @@ class VeilidAPIExceptionTimeout implements VeilidAPIException { String toString() { return "VeilidAPIException: Timeout"; } + + @override + String toDisplayError() { + return "Timeout"; + } } class VeilidAPIExceptionShutdown implements VeilidAPIException { @@ -1599,6 +1617,11 @@ class VeilidAPIExceptionShutdown implements VeilidAPIException { String toString() { return "VeilidAPIException: Shutdown"; } + + @override + String toDisplayError() { + return "Currently shut down"; + } } class VeilidAPIExceptionNodeNotFound implements VeilidAPIException { @@ -1609,6 +1632,11 @@ class VeilidAPIExceptionNodeNotFound implements VeilidAPIException { return "VeilidAPIException: NodeNotFound (nodeId: $nodeId)"; } + @override + String toDisplayError() { + return "Node node found: $nodeId"; + } + // VeilidAPIExceptionNodeNotFound(this.nodeId); } @@ -1621,6 +1649,11 @@ class VeilidAPIExceptionNoDialInfo implements VeilidAPIException { return "VeilidAPIException: NoDialInfo (nodeId: $nodeId)"; } + @override + String toDisplayError() { + return "No dial info: $nodeId"; + } + // VeilidAPIExceptionNoDialInfo(this.nodeId); } @@ -1633,6 +1666,11 @@ class VeilidAPIExceptionInternal implements VeilidAPIException { return "VeilidAPIException: Internal ($message)"; } + @override + String toDisplayError() { + return "Internal error: $message"; + } + // VeilidAPIExceptionInternal(this.message); } @@ -1645,6 +1683,11 @@ class VeilidAPIExceptionUnimplemented implements VeilidAPIException { return "VeilidAPIException: Unimplemented ($message)"; } + @override + String toDisplayError() { + return "Unimplemented: $message"; + } + // VeilidAPIExceptionUnimplemented(this.message); } @@ -1658,6 +1701,11 @@ class VeilidAPIExceptionParseError implements VeilidAPIException { return "VeilidAPIException: ParseError ($message)\n value: $value"; } + @override + String toDisplayError() { + return "Parse error: $message"; + } + // VeilidAPIExceptionParseError(this.message, this.value); } @@ -1672,6 +1720,11 @@ class VeilidAPIExceptionInvalidArgument implements VeilidAPIException { return "VeilidAPIException: InvalidArgument ($context:$argument)\n value: $value"; } + @override + String toDisplayError() { + return "Invalid argument for $context: $argument"; + } + // VeilidAPIExceptionInvalidArgument(this.context, this.argument, this.value); } @@ -1685,6 +1738,11 @@ class VeilidAPIExceptionMissingArgument implements VeilidAPIException { return "VeilidAPIException: MissingArgument ($context:$argument)"; } + @override + String toDisplayError() { + return "Missing argument for $context: $argument"; + } + // VeilidAPIExceptionMissingArgument(this.context, this.argument); } @@ -1697,6 +1755,11 @@ class VeilidAPIExceptionGeneric implements VeilidAPIException { return "VeilidAPIException: Generic (message: $message)"; } + @override + String toDisplayError() { + return message; + } + // VeilidAPIExceptionGeneric(this.message); } From 8d80fbb2283dd90112dfff3b65aa5bdea3d029bb Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 15 Dec 2022 18:41:44 -0500 Subject: [PATCH 52/88] route grooming fix --- external/keyring-manager | 2 +- veilid-cli/src/ui.rs | 1 - .../src/routing_table/route_spec_store.rs | 34 +++- .../tasks/private_route_management.rs | 157 ++++++++++++------ 4 files changed, 139 insertions(+), 55 deletions(-) diff --git a/external/keyring-manager b/external/keyring-manager index c153eb30..b127b2d3 160000 --- a/external/keyring-manager +++ b/external/keyring-manager @@ -1 +1 @@ -Subproject commit c153eb3015d6d118e5d467865510d053ddd84533 +Subproject commit b127b2d3c653fea163a776dd58b3798f28aeeee3 diff --git a/veilid-cli/src/ui.rs b/veilid-cli/src/ui.rs index 9f127056..39909958 100644 --- a/veilid-cli/src/ui.rs +++ b/veilid-cli/src/ui.rs @@ -353,7 +353,6 @@ impl UI { format!(" Error: {}", e), color, )); - return; } } // save to history unless it's a duplicate diff --git a/veilid-core/src/routing_table/route_spec_store.rs b/veilid-core/src/routing_table/route_spec_store.rs index 5f7d1b64..fbf21951 100644 --- a/veilid-core/src/routing_table/route_spec_store.rs +++ b/veilid-core/src/routing_table/route_spec_store.rs @@ -188,6 +188,12 @@ impl RouteSpecDetail { pub fn get_stats_mut(&mut self) -> &mut RouteStats { &mut self.stats } + pub fn is_published(&self) -> bool { + self.published + } + pub fn hop_count(&self) -> usize { + self.hops.len() + } } /// The core representation of the RouteSpecStore that can be serialized @@ -1082,6 +1088,11 @@ impl RouteSpecStore { avoid_node_ids: &[DHTKey], ) -> Option { let cur_ts = get_timestamp(); + + let mut routes = Vec::new(); + + // Get all valid routes, allow routes that need testing + // but definitely prefer routes that have been recently tested for detail in &inner.content.details { if detail.1.stability >= stability && detail.1.sequencing >= sequencing @@ -1089,7 +1100,6 @@ impl RouteSpecStore { && detail.1.hops.len() <= max_hop_count && detail.1.directions.is_superset(directions) && !detail.1.published - && !detail.1.stats.needs_testing(cur_ts) { let mut avoid = false; for h in &detail.1.hops { @@ -1099,11 +1109,29 @@ impl RouteSpecStore { } } if !avoid { - return Some(*detail.0); + routes.push(detail); } } } - None + + // Sort the routes by preference + routes.sort_by(|a, b| { + let a_needs_testing = a.1.stats.needs_testing(cur_ts); + let b_needs_testing = b.1.stats.needs_testing(cur_ts); + if !a_needs_testing && b_needs_testing { + return cmp::Ordering::Less; + } + if !b_needs_testing && a_needs_testing { + return cmp::Ordering::Greater; + } + let a_latency = a.1.stats.latency_stats().average; + let b_latency = b.1.stats.latency_stats().average; + + a_latency.cmp(&b_latency) + }); + + // Return the best one if we got one + routes.first().map(|r| *r.0) } /// List all allocated routes diff --git a/veilid-core/src/routing_table/tasks/private_route_management.rs b/veilid-core/src/routing_table/tasks/private_route_management.rs index f9929e2c..2b239e7c 100644 --- a/veilid-core/src/routing_table/tasks/private_route_management.rs +++ b/veilid-core/src/routing_table/tasks/private_route_management.rs @@ -4,7 +4,70 @@ use futures_util::stream::{FuturesUnordered, StreamExt}; use futures_util::FutureExt; use stop_token::future::FutureExt as StopFutureExt; +const BACKGROUND_SAFETY_ROUTE_COUNT: usize = 2; + impl RoutingTable { + /// Test set of routes and remove the ones that don't test clean + #[instrument(level = "trace", skip(self, stop_token), err)] + async fn test_route_set( + &self, + stop_token: StopToken, + routes_needing_testing: Vec, + ) -> EyreResult<()> { + let rss = self.route_spec_store(); + log_rtab!(debug "Testing routes: {:?}", routes_needing_testing); + + #[derive(Default, Debug)] + struct TestRouteContext { + failed: bool, + dead_routes: Vec, + } + + if routes_needing_testing.is_empty() { + return Ok(()); + } + + // Test all the routes that need testing at the same time + let mut unord = FuturesUnordered::new(); + let ctx = Arc::new(Mutex::new(TestRouteContext::default())); + for r in routes_needing_testing { + let rss = rss.clone(); + let ctx = ctx.clone(); + unord.push( + async move { + let success = match rss.test_route(&r).await { + Ok(v) => v, + Err(e) => { + log_rtab!(error "Test route failed: {}", e); + ctx.lock().failed = true; + return; + } + }; + if success { + // Route is okay, leave it alone + return; + } + // Route test failed + ctx.lock().dead_routes.push(r); + } + .instrument(Span::current()) + .boxed(), + ); + } + + // Wait for test_route futures to complete in parallel + while let Ok(Some(_)) = unord.next().timeout_at(stop_token.clone()).await {} + + // Process failed routes + let ctx = &mut *ctx.lock(); + for r in &ctx.dead_routes { + log_rtab!(debug "Dead route: {}", &r); + rss.release_route(r); + } + + Ok(()) + } + /// Keep private routes assigned and accessible #[instrument(level = "trace", skip(self, stop_token), err)] pub(crate) async fn private_route_management_task_routine( @@ -23,9 +86,10 @@ impl RoutingTable { return Ok(()); } - // Collect any routes that need that need testing + // Test locally allocated routes first + // This may remove dead routes let rss = self.route_spec_store(); - let mut routes_needing_testing = rss.list_allocated_routes(|k, v| { + let routes_needing_testing = rss.list_allocated_routes(|k, v| { let stats = v.get_stats(); if stats.needs_testing(cur_ts) { return Some(*k); @@ -33,7 +97,45 @@ impl RoutingTable { return None; } }); - let mut remote_routes_needing_testing = rss.list_remote_routes(|k, v| { + self.test_route_set(stop_token.clone(), routes_needing_testing) + .await?; + + // Ensure we have a minimum of N allocated local, unpublished routes with the default number of hops + let default_route_hop_count = + self.with_config(|c| c.network.rpc.default_route_hop_count as usize); + let mut local_unpublished_route_count = 0usize; + rss.list_allocated_routes(|_k, v| { + if !v.is_published() && v.hop_count() == default_route_hop_count { + local_unpublished_route_count += 1; + } + Option::<()>::None + }); + if local_unpublished_route_count < BACKGROUND_SAFETY_ROUTE_COUNT { + let routes_to_allocate = BACKGROUND_SAFETY_ROUTE_COUNT - local_unpublished_route_count; + + // Newly allocated routes + let mut newly_allocated_routes = Vec::new(); + for _n in 0..routes_to_allocate { + // Parameters here must be the default safety route spec + // These will be used by test_remote_route as well + if let Some(k) = rss.allocate_route( + Stability::default(), + Sequencing::default(), + default_route_hop_count, + DirectionSet::all(), + &[], + )? { + newly_allocated_routes.push(k); + } + } + + // Immediately test them + self.test_route_set(stop_token.clone(), newly_allocated_routes) + .await?; + } + + // Test remote routes next + let remote_routes_needing_testing = rss.list_remote_routes(|k, v| { let stats = v.get_stats(); if stats.needs_testing(cur_ts) { return Some(*k); @@ -41,53 +143,8 @@ impl RoutingTable { return None; } }); - routes_needing_testing.append(&mut remote_routes_needing_testing); - - // Test all the routes that need testing at the same time - #[derive(Default, Debug)] - struct TestRouteContext { - failed: bool, - dead_routes: Vec, - } - - if !routes_needing_testing.is_empty() { - let mut unord = FuturesUnordered::new(); - let ctx = Arc::new(Mutex::new(TestRouteContext::default())); - for r in routes_needing_testing { - let rss = rss.clone(); - let ctx = ctx.clone(); - unord.push( - async move { - let success = match rss.test_route(&r).await { - Ok(v) => v, - Err(e) => { - log_rtab!(error "test route failed: {}", e); - ctx.lock().failed = true; - return; - } - }; - if success { - // Route is okay, leave it alone - return; - } - // Route test failed - ctx.lock().dead_routes.push(r); - } - .instrument(Span::current()) - .boxed(), - ); - } - - // Wait for test_route futures to complete in parallel - while let Ok(Some(_)) = unord.next().timeout_at(stop_token.clone()).await {} - - // Process failed routes - let ctx = &mut *ctx.lock(); - for r in &ctx.dead_routes { - log_rtab!(debug "Dead route: {}", &r); - rss.release_route(r); - } - } + self.test_route_set(stop_token.clone(), remote_routes_needing_testing) + .await?; // Send update (also may send updates for released routes done by other parts of the program) rss.send_route_update(); From e2b4e1fb765594b086ac7d44acb1c92a7f2b5194 Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 15 Dec 2022 18:45:41 -0500 Subject: [PATCH 53/88] route work --- .../tasks/private_route_management.rs | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/veilid-core/src/routing_table/tasks/private_route_management.rs b/veilid-core/src/routing_table/tasks/private_route_management.rs index 2b239e7c..057575c3 100644 --- a/veilid-core/src/routing_table/tasks/private_route_management.rs +++ b/veilid-core/src/routing_table/tasks/private_route_management.rs @@ -14,20 +14,19 @@ impl RoutingTable { stop_token: StopToken, routes_needing_testing: Vec, ) -> EyreResult<()> { - let rss = self.route_spec_store(); + if routes_needing_testing.is_empty() { + return Ok(()); + } log_rtab!(debug "Testing routes: {:?}", routes_needing_testing); + // Test all the routes that need testing at the same time + let rss = self.route_spec_store(); #[derive(Default, Debug)] struct TestRouteContext { failed: bool, dead_routes: Vec, } - if routes_needing_testing.is_empty() { - return Ok(()); - } - - // Test all the routes that need testing at the same time let mut unord = FuturesUnordered::new(); let ctx = Arc::new(Mutex::new(TestRouteContext::default())); for r in routes_needing_testing { @@ -97,8 +96,10 @@ impl RoutingTable { return None; } }); - self.test_route_set(stop_token.clone(), routes_needing_testing) - .await?; + if !routes_needing_testing.is_empty() { + self.test_route_set(stop_token.clone(), routes_needing_testing) + .await?; + } // Ensure we have a minimum of N allocated local, unpublished routes with the default number of hops let default_route_hop_count = @@ -130,8 +131,10 @@ impl RoutingTable { } // Immediately test them - self.test_route_set(stop_token.clone(), newly_allocated_routes) - .await?; + if !newly_allocated_routes.is_empty() { + self.test_route_set(stop_token.clone(), newly_allocated_routes) + .await?; + } } // Test remote routes next @@ -143,8 +146,10 @@ impl RoutingTable { return None; } }); - self.test_route_set(stop_token.clone(), remote_routes_needing_testing) - .await?; + if !remote_routes_needing_testing.is_empty() { + self.test_route_set(stop_token.clone(), remote_routes_needing_testing) + .await?; + } // Send update (also may send updates for released routes done by other parts of the program) rss.send_route_update(); From fc804bdf242a4e326baf0e2723ee3ec4b092203f Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 15 Dec 2022 19:36:45 -0500 Subject: [PATCH 54/88] less chat --- veilid-core/src/routing_table/tasks/private_route_management.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/veilid-core/src/routing_table/tasks/private_route_management.rs b/veilid-core/src/routing_table/tasks/private_route_management.rs index 057575c3..f790afe0 100644 --- a/veilid-core/src/routing_table/tasks/private_route_management.rs +++ b/veilid-core/src/routing_table/tasks/private_route_management.rs @@ -17,7 +17,7 @@ impl RoutingTable { if routes_needing_testing.is_empty() { return Ok(()); } - log_rtab!(debug "Testing routes: {:?}", routes_needing_testing); + log_rtab!("Testing routes: {:?}", routes_needing_testing); // Test all the routes that need testing at the same time let rss = self.route_spec_store(); From d089612cbb90090f9455a3816143f6e0f1067eca Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 15 Dec 2022 20:16:42 -0500 Subject: [PATCH 55/88] wasm shit --- veilid-core/Cargo.toml | 4 +--- veilid-wasm/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/veilid-core/Cargo.toml b/veilid-core/Cargo.toml index 59c54eda..78fccd45 100644 --- a/veilid-core/Cargo.toml +++ b/veilid-core/Cargo.toml @@ -64,6 +64,7 @@ keyvaluedb = { path = "../external/keyvaluedb/keyvaluedb" } #rkyv = { version = "^0", default_features = false, features = ["std", "alloc", "strict", "size_32", "validation"] } rkyv = { git = "https://github.com/rkyv/rkyv.git", rev = "57e2a8d", default_features = false, features = ["std", "alloc", "strict", "size_32", "validation"] } bytecheck = "^0" +data-encoding = { version = "^2" } # Dependencies for native builds only # Linux, Windows, Mac, iOS, Android @@ -85,8 +86,6 @@ rustls = "^0.19" rustls-pemfile = "^0.2" futures-util = { version = "^0", default-features = false, features = ["async-await", "sink", "std", "io"] } keyvaluedb-sqlite = { path = "../external/keyvaluedb/keyvaluedb-sqlite" } -data-encoding = { version = "^2" } - socket2 = "^0" bugsalot = "^0" chrono = "^0" @@ -99,7 +98,6 @@ wasm-bindgen = "^0" js-sys = "^0" wasm-bindgen-futures = "^0" keyvaluedb-web = { path = "../external/keyvaluedb/keyvaluedb-web" } -data-encoding = { version = "^2", default_features = false, features = ["alloc"] } getrandom = { version = "^0", features = ["js"] } ws_stream_wasm = "^0" async_executors = { version = "^0", default-features = false, features = [ "bindgen", "timer" ]} diff --git a/veilid-wasm/Cargo.toml b/veilid-wasm/Cargo.toml index 758f4f88..2d21b445 100644 --- a/veilid-wasm/Cargo.toml +++ b/veilid-wasm/Cargo.toml @@ -25,7 +25,7 @@ serde_json = "^1" serde = "^1" lazy_static = "^1" send_wrapper = "^0" -futures-util = { version = "^0", default_features = false, features = ["alloc"] } +futures-util = { version = "^0" } data-encoding = { version = "^2" } gloo-utils = { version = "^0", features = ["serde"] } From 10a0e3b6295431ccfbde4d3c0f4c4e1312b08251 Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 15 Dec 2022 20:52:24 -0500 Subject: [PATCH 56/88] bug fixes --- veilid-core/src/network_manager/mod.rs | 6 ++- .../src/routing_table/routing_table_inner.rs | 4 +- .../src/rpc_processor/rpc_find_node.rs | 19 +++------- veilid-core/src/veilid_api/debug.rs | 3 ++ veilid-core/src/veilid_api/error.rs | 13 +++++++ veilid-core/src/veilid_api/routing_context.rs | 2 + veilid-tools/src/network_result.rs | 38 +++++++++++++------ 7 files changed, 58 insertions(+), 27 deletions(-) diff --git a/veilid-core/src/network_manager/mod.rs b/veilid-core/src/network_manager/mod.rs index 2ba43538..60d26957 100644 --- a/veilid-core/src/network_manager/mod.rs +++ b/veilid-core/src/network_manager/mod.rs @@ -1682,7 +1682,7 @@ impl NetworkManager { // } inconsistent - } else { + } else if matches!(public_internet_network_class, NetworkClass::OutboundOnly) { // If we are currently outbound only, we don't have any public dial info // but if we are starting to see consistent socket address from multiple reporting peers // then we may be become inbound capable, so zap the network class so we can re-detect it and any public dial info @@ -1710,6 +1710,10 @@ impl NetworkManager { } } consistent + } else { + // If we are a webapp we never do this. + // If we have invalid network class, then public address detection is already going to happen via the network_class_discovery task + false }; if needs_public_address_detection { diff --git a/veilid-core/src/routing_table/routing_table_inner.rs b/veilid-core/src/routing_table/routing_table_inner.rs index 8338be29..b867bc03 100644 --- a/veilid-core/src/routing_table/routing_table_inner.rs +++ b/veilid-core/src/routing_table/routing_table_inner.rs @@ -799,12 +799,12 @@ impl RoutingTableInner { pub fn transform_to_peer_info( &self, routing_domain: RoutingDomain, - own_peer_info: PeerInfo, + own_peer_info: &PeerInfo, k: DHTKey, v: Option>, ) -> PeerInfo { match v { - None => own_peer_info, + None => own_peer_info.clone(), Some(entry) => entry.with(self, |_rti, e| e.make_peer_info(k, routing_domain).unwrap()), } } diff --git a/veilid-core/src/rpc_processor/rpc_find_node.rs b/veilid-core/src/rpc_processor/rpc_find_node.rs index cd4e62b5..098c9714 100644 --- a/veilid-core/src/rpc_processor/rpc_find_node.rs +++ b/veilid-core/src/rpc_processor/rpc_find_node.rs @@ -92,18 +92,16 @@ impl RPCProcessor { // add node information for the requesting node to our routing table let routing_table = self.routing_table(); - let own_peer_info = routing_table.get_own_peer_info(RoutingDomain::PublicInternet); - let has_valid_own_node_info = own_peer_info.is_some(); + let Some(own_peer_info) = routing_table.get_own_peer_info(RoutingDomain::PublicInternet) else { + // Our own node info is not yet available, drop this request. + return Ok(NetworkResult::service_unavailable()); + }; // find N nodes closest to the target node in our routing table let filter = Box::new( move |rti: &RoutingTableInner, _k: DHTKey, v: Option>| { - rti.filter_has_valid_signed_node_info( - RoutingDomain::PublicInternet, - has_valid_own_node_info, - v, - ) + rti.filter_has_valid_signed_node_info(RoutingDomain::PublicInternet, true, v) }, ) as RoutingTableEntryFilter; let filters = VecDeque::from([filter]); @@ -113,12 +111,7 @@ impl RPCProcessor { filters, // transform |rti, k, v| { - rti.transform_to_peer_info( - RoutingDomain::PublicInternet, - own_peer_info.as_ref().unwrap().clone(), - k, - v, - ) + rti.transform_to_peer_info(RoutingDomain::PublicInternet, &own_peer_info, k, v) }, ); diff --git a/veilid-core/src/veilid_api/debug.rs b/veilid-core/src/veilid_api/debug.rs index 724cf6d8..2349a97c 100644 --- a/veilid-core/src/veilid_api/debug.rs +++ b/veilid-core/src/veilid_api/debug.rs @@ -541,6 +541,9 @@ impl VeilidAPI { NetworkResult::Timeout => { return Ok("Timeout".to_owned()); } + NetworkResult::ServiceUnavailable => { + return Ok("ServiceUnavailable".to_owned()); + } NetworkResult::NoConnection(e) => { return Ok(format!("NoConnection({})", e)); } diff --git a/veilid-core/src/veilid_api/error.rs b/veilid-core/src/veilid_api/error.rs index 6d6f1a25..aece21d4 100644 --- a/veilid-core/src/veilid_api/error.rs +++ b/veilid-core/src/veilid_api/error.rs @@ -8,6 +8,14 @@ macro_rules! apibail_timeout { }; } +#[allow(unused_macros)] +#[macro_export] +macro_rules! apibail_try_again { + () => { + return Err(VeilidAPIError::try_again()) + }; +} + #[allow(unused_macros)] #[macro_export] macro_rules! apibail_generic { @@ -95,6 +103,8 @@ pub enum VeilidAPIError { AlreadyInitialized, #[error("Timeout")] Timeout, + #[error("TryAgain")] + TryAgain, #[error("Shutdown")] Shutdown, #[error("Key not found: {key}")] @@ -131,6 +141,9 @@ impl VeilidAPIError { pub fn timeout() -> Self { Self::Timeout } + pub fn try_again() -> Self { + Self::TryAgain + } pub fn shutdown() -> Self { Self::Shutdown } diff --git a/veilid-core/src/veilid_api/routing_context.rs b/veilid-core/src/veilid_api/routing_context.rs index 4e712330..3cffd867 100644 --- a/veilid-core/src/veilid_api/routing_context.rs +++ b/veilid-core/src/veilid_api/routing_context.rs @@ -153,6 +153,7 @@ impl RoutingContext { let answer = match rpc_processor.rpc_call_app_call(dest, request).await { Ok(NetworkResult::Value(v)) => v, Ok(NetworkResult::Timeout) => apibail_timeout!(), + Ok(NetworkResult::ServiceUnavailable) => apibail_try_again!(), Ok(NetworkResult::NoConnection(e)) | Ok(NetworkResult::AlreadyExists(e)) => { apibail_no_connection!(e); } @@ -181,6 +182,7 @@ impl RoutingContext { match rpc_processor.rpc_call_app_message(dest, message).await { Ok(NetworkResult::Value(())) => {} Ok(NetworkResult::Timeout) => apibail_timeout!(), + Ok(NetworkResult::ServiceUnavailable) => apibail_try_again!(), Ok(NetworkResult::NoConnection(e)) | Ok(NetworkResult::AlreadyExists(e)) => { apibail_no_connection!(e); } diff --git a/veilid-tools/src/network_result.rs b/veilid-tools/src/network_result.rs index 55145cf5..0ddb3973 100644 --- a/veilid-tools/src/network_result.rs +++ b/veilid-tools/src/network_result.rs @@ -68,6 +68,7 @@ impl NetworkResultResultExt for NetworkResult> { fn into_result_network_result(self) -> Result, E> { match self { NetworkResult::Timeout => Ok(NetworkResult::::Timeout), + NetworkResult::ServiceUnavailable => Ok(NetworkResult::::ServiceUnavailable), NetworkResult::NoConnection(e) => Ok(NetworkResult::::NoConnection(e)), NetworkResult::AlreadyExists(e) => Ok(NetworkResult::::AlreadyExists(e)), NetworkResult::InvalidMessage(s) => Ok(NetworkResult::::InvalidMessage(s)), @@ -160,6 +161,7 @@ impl FoldedNetworkResultExt for io::Result> { #[must_use] pub enum NetworkResult { Timeout, + ServiceUnavailable, NoConnection(io::Error), AlreadyExists(io::Error), InvalidMessage(String), @@ -170,6 +172,9 @@ impl NetworkResult { pub fn timeout() -> Self { Self::Timeout } + pub fn service_unavailable() -> Self { + Self::ServiceUnavailable + } pub fn no_connection(e: io::Error) -> Self { Self::NoConnection(e) } @@ -201,6 +206,7 @@ impl NetworkResult { pub fn map X>(self, f: F) -> NetworkResult { match self { Self::Timeout => NetworkResult::::Timeout, + Self::ServiceUnavailable => NetworkResult::::ServiceUnavailable, Self::NoConnection(e) => NetworkResult::::NoConnection(e), Self::AlreadyExists(e) => NetworkResult::::AlreadyExists(e), Self::InvalidMessage(s) => NetworkResult::::InvalidMessage(s), @@ -210,6 +216,10 @@ impl NetworkResult { pub fn into_result(self) -> Result { match self { Self::Timeout => Err(io::Error::new(io::ErrorKind::TimedOut, "Timed out")), + Self::ServiceUnavailable => Err(io::Error::new( + io::ErrorKind::NotFound, + "Service unavailable", + )), Self::NoConnection(e) => Err(e), Self::AlreadyExists(e) => Err(e), Self::InvalidMessage(s) => Err(io::Error::new( @@ -230,21 +240,11 @@ impl From> for Option { } } -// impl Clone for NetworkResult { -// fn clone(&self) -> Self { -// match self { -// Self::Timeout => Self::Timeout, -// Self::NoConnection(e) => Self::NoConnection(e.clone()), -// Self::InvalidMessage(s) => Self::InvalidMessage(s.clone()), -// Self::Value(t) => Self::Value(t.clone()), -// } -// } -// } - impl Debug for NetworkResult { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::Timeout => write!(f, "Timeout"), + Self::ServiceUnavailable => write!(f, "ServiceUnavailable"), Self::NoConnection(e) => f.debug_tuple("NoConnection").field(e).finish(), Self::AlreadyExists(e) => f.debug_tuple("AlreadyExists").field(e).finish(), Self::InvalidMessage(s) => f.debug_tuple("InvalidMessage").field(s).finish(), @@ -257,6 +257,7 @@ impl Display for NetworkResult { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::Timeout => write!(f, "Timeout"), + Self::ServiceUnavailable => write!(f, "ServiceUnavailable"), Self::NoConnection(e) => write!(f, "NoConnection({})", e.kind()), Self::AlreadyExists(e) => write!(f, "AlreadyExists({})", e.kind()), Self::InvalidMessage(s) => write!(f, "InvalidMessage({})", s), @@ -275,6 +276,7 @@ macro_rules! network_result_try { ($r: expr) => { match $r { NetworkResult::Timeout => return Ok(NetworkResult::Timeout), + NetworkResult::ServiceUnavailable => return Ok(NetworkResult::ServiceUnavailable), NetworkResult::NoConnection(e) => return Ok(NetworkResult::NoConnection(e)), NetworkResult::AlreadyExists(e) => return Ok(NetworkResult::AlreadyExists(e)), NetworkResult::InvalidMessage(s) => return Ok(NetworkResult::InvalidMessage(s)), @@ -287,6 +289,10 @@ macro_rules! network_result_try { $f; return Ok(NetworkResult::Timeout); } + NetworkResult::ServiceUnavailable => { + $f; + return Ok(NetworkResult::ServiceUnavailable); + } NetworkResult::NoConnection(e) => { $f; return Ok(NetworkResult::NoConnection(e)); @@ -340,6 +346,16 @@ macro_rules! network_result_value_or_log { ); $f } + NetworkResult::ServiceUnavailable => { + log_network_result!( + "{} at {}@{}:{}", + "ServiceUnavailable".cyan(), + file!(), + line!(), + column!() + ); + $f + } NetworkResult::NoConnection(e) => { log_network_result!( "{}({}) at {}@{}:{}", From 221c09b555d42de9f0e026101b789c0fb2462fe4 Mon Sep 17 00:00:00 2001 From: John Smith Date: Fri, 16 Dec 2022 20:07:28 -0500 Subject: [PATCH 57/88] checkpoint --- external/keyring-manager | 2 +- veilid-core/src/attachment_manager.rs | 12 +- veilid-core/src/crypto/envelope.rs | 13 ++- .../src/crypto/tests/test_envelope_receipt.rs | 2 +- .../src/network_manager/connection_handle.rs | 6 +- .../src/network_manager/connection_limits.rs | 6 +- .../src/network_manager/connection_manager.rs | 8 +- veilid-core/src/network_manager/mod.rs | 46 ++++---- .../src/network_manager/native/igd_manager.rs | 22 ++-- .../src/network_manager/network_connection.rs | 18 +-- .../tasks/public_address_check.rs | 4 +- .../tasks/rolling_transfers.rs | 4 +- .../tests/test_connection_table.rs | 30 ++--- veilid-core/src/receipt_manager.rs | 22 ++-- veilid-core/src/routing_table/bucket.rs | 2 +- veilid-core/src/routing_table/bucket_entry.rs | 62 +++++----- veilid-core/src/routing_table/debug.rs | 4 +- veilid-core/src/routing_table/mod.rs | 10 +- veilid-core/src/routing_table/node_ref.rs | 22 ++-- .../src/routing_table/route_spec_store.rs | 63 +++++----- .../src/routing_table/routing_table_inner.rs | 20 ++-- .../src/routing_table/stats_accounting.rs | 16 +-- .../src/routing_table/tasks/kick_buckets.rs | 4 +- .../src/routing_table/tasks/ping_validator.rs | 2 +- .../tasks/private_route_management.rs | 4 +- .../routing_table/tasks/relay_management.rs | 4 +- .../routing_table/tasks/rolling_transfers.rs | 4 +- .../coders/operations/operation.rs | 20 ++-- .../coders/signed_direct_node_info.rs | 4 +- .../coders/signed_relayed_node_info.rs | 4 +- veilid-core/src/rpc_processor/mod.rs | 80 +++++++------ .../src/rpc_processor/operation_waiter.rs | 10 +- veilid-core/src/rpc_processor/rpc_app_call.rs | 4 +- veilid-core/src/rpc_processor/rpc_route.rs | 1 - veilid-core/src/veilid_api/aligned_u64.rs | 110 ++++++++++++++++++ veilid-core/src/veilid_api/api.rs | 6 +- veilid-core/src/veilid_api/mod.rs | 2 + veilid-core/src/veilid_api/types.rs | 70 ++++++----- veilid-flutter/example/web/index.html | 1 + veilid-wasm/src/lib.rs | 2 +- 40 files changed, 428 insertions(+), 298 deletions(-) create mode 100644 veilid-core/src/veilid_api/aligned_u64.rs diff --git a/external/keyring-manager b/external/keyring-manager index b127b2d3..c153eb30 160000 --- a/external/keyring-manager +++ b/external/keyring-manager @@ -1 +1 @@ -Subproject commit b127b2d3c653fea163a776dd58b3798f28aeeee3 +Subproject commit c153eb3015d6d118e5d467865510d053ddd84533 diff --git a/veilid-core/src/attachment_manager.rs b/veilid-core/src/attachment_manager.rs index c47b9f52..751cde8f 100644 --- a/veilid-core/src/attachment_manager.rs +++ b/veilid-core/src/attachment_manager.rs @@ -103,7 +103,7 @@ impl TryFrom for AttachmentState { pub struct AttachmentManagerInner { attachment_machine: CallbackStateMachine, maintain_peers: bool, - attach_timestamp: Option, + attach_ts: Option, update_callback: Option, attachment_maintainer_jh: Option>, } @@ -142,7 +142,7 @@ impl AttachmentManager { AttachmentManagerInner { attachment_machine: CallbackStateMachine::new(), maintain_peers: false, - attach_timestamp: None, + attach_ts: None, update_callback: None, attachment_maintainer_jh: None, } @@ -183,8 +183,8 @@ impl AttachmentManager { matches!(s, AttachmentState::Detached) } - pub fn get_attach_timestamp(&self) -> Option { - self.inner.lock().attach_timestamp + pub fn get_attach_timestamp(&self) -> Option { + self.inner.lock().attach_ts } fn translate_routing_table_health( @@ -252,7 +252,7 @@ impl AttachmentManager { #[instrument(level = "debug", skip(self))] async fn attachment_maintainer(self) { debug!("attachment starting"); - self.inner.lock().attach_timestamp = Some(get_timestamp()); + self.inner.lock().attach_ts = Some(get_aligned_timestamp()); let netman = self.network_manager(); let mut restart; @@ -306,7 +306,7 @@ impl AttachmentManager { .consume(&AttachmentInput::AttachmentStopped) .await; debug!("attachment stopped"); - self.inner.lock().attach_timestamp = None; + self.inner.lock().attach_ts = None; } #[instrument(level = "debug", skip_all, err)] diff --git a/veilid-core/src/crypto/envelope.rs b/veilid-core/src/crypto/envelope.rs index efdec697..7d0b6734 100644 --- a/veilid-core/src/crypto/envelope.rs +++ b/veilid-core/src/crypto/envelope.rs @@ -44,7 +44,7 @@ pub struct Envelope { version: u8, min_version: u8, max_version: u8, - timestamp: u64, + timestamp: Timestamp, nonce: EnvelopeNonce, sender_id: DHTKey, recipient_id: DHTKey, @@ -53,7 +53,7 @@ pub struct Envelope { impl Envelope { pub fn new( version: u8, - timestamp: u64, + timestamp: Timestamp, nonce: EnvelopeNonce, sender_id: DHTKey, recipient_id: DHTKey, @@ -128,11 +128,12 @@ impl Envelope { } // Get the timestamp - let timestamp: u64 = u64::from_le_bytes( + let timestamp: Timestamp = u64::from_le_bytes( data[0x0A..0x12] .try_into() .map_err(VeilidAPIError::internal)?, - ); + ) + .into(); // Get nonce and sender node id let nonce: EnvelopeNonce = data[0x12..0x2A] @@ -217,7 +218,7 @@ impl Envelope { // Write size data[0x08..0x0A].copy_from_slice(&(envelope_size as u16).to_le_bytes()); // Write timestamp - data[0x0A..0x12].copy_from_slice(&self.timestamp.to_le_bytes()); + data[0x0A..0x12].copy_from_slice(&self.timestamp.as_u64().to_le_bytes()); // Write nonce data[0x12..0x2A].copy_from_slice(&self.nonce); // Write sender node id @@ -260,7 +261,7 @@ impl Envelope { } } - pub fn get_timestamp(&self) -> u64 { + pub fn get_timestamp(&self) -> Timestamp { self.timestamp } diff --git a/veilid-core/src/crypto/tests/test_envelope_receipt.rs b/veilid-core/src/crypto/tests/test_envelope_receipt.rs index 723eeaff..a5b2e45e 100644 --- a/veilid-core/src/crypto/tests/test_envelope_receipt.rs +++ b/veilid-core/src/crypto/tests/test_envelope_receipt.rs @@ -12,7 +12,7 @@ pub async fn test_envelope_round_trip() { let crypto = api.crypto().unwrap(); // Create envelope - let ts = 0x12345678ABCDEF69u64; + let ts = Timestamp::from(0x12345678ABCDEF69u64); let nonce = Crypto::get_random_nonce(); let (sender_id, sender_secret) = generate_secret(); let (recipient_id, recipient_secret) = generate_secret(); diff --git a/veilid-core/src/network_manager/connection_handle.rs b/veilid-core/src/network_manager/connection_handle.rs index 9eeb1b63..3dc0adb5 100644 --- a/veilid-core/src/network_manager/connection_handle.rs +++ b/veilid-core/src/network_manager/connection_handle.rs @@ -2,7 +2,7 @@ use super::*; #[derive(Clone, Debug)] pub struct ConnectionHandle { - id: u64, + id: NetworkConnectionId, descriptor: ConnectionDescriptor, channel: flume::Sender<(Option, Vec)>, } @@ -15,7 +15,7 @@ pub enum ConnectionHandleSendResult { impl ConnectionHandle { pub(super) fn new( - id: u64, + id: NetworkConnectionId, descriptor: ConnectionDescriptor, channel: flume::Sender<(Option, Vec)>, ) -> Self { @@ -26,7 +26,7 @@ impl ConnectionHandle { } } - pub fn connection_id(&self) -> u64 { + pub fn connection_id(&self) -> NetworkConnectionId { self.id } diff --git a/veilid-core/src/network_manager/connection_limits.rs b/veilid-core/src/network_manager/connection_limits.rs index f25f1654..d62cdc01 100644 --- a/veilid-core/src/network_manager/connection_limits.rs +++ b/veilid-core/src/network_manager/connection_limits.rs @@ -41,7 +41,7 @@ impl ConnectionLimits { } } - fn purge_old_timestamps(&mut self, cur_ts: u64) { + fn purge_old_timestamps(&mut self, cur_ts: Timestamp) { // v4 { let mut dead_keys = Vec::::new(); @@ -78,7 +78,7 @@ impl ConnectionLimits { pub fn add(&mut self, addr: IpAddr) -> Result<(), AddressFilterError> { let ipblock = ip_to_ipblock(self.max_connections_per_ip6_prefix_size, addr); - let ts = get_timestamp(); + let ts = get_aligned_timestamp(); self.purge_old_timestamps(ts); @@ -134,7 +134,7 @@ impl ConnectionLimits { pub fn remove(&mut self, addr: IpAddr) -> Result<(), AddressNotInTableError> { let ipblock = ip_to_ipblock(self.max_connections_per_ip6_prefix_size, addr); - let ts = get_timestamp(); + let ts = get_aligned_timestamp(); self.purge_old_timestamps(ts); match ipblock { diff --git a/veilid-core/src/network_manager/connection_manager.rs b/veilid-core/src/network_manager/connection_manager.rs index b8c62e84..342925c9 100644 --- a/veilid-core/src/network_manager/connection_manager.rs +++ b/veilid-core/src/network_manager/connection_manager.rs @@ -48,9 +48,9 @@ impl ConnectionManager { async_processor_jh: MustJoinHandle<()>, ) -> ConnectionManagerInner { ConnectionManagerInner { - next_id: 0, + next_id: 0.into(), stop_source: Some(stop_source), - sender: sender, + sender, async_processor_jh: Some(async_processor_jh), } } @@ -149,7 +149,7 @@ impl ConnectionManager { ) -> EyreResult> { // Get next connection id to use let id = inner.next_id; - inner.next_id += 1; + inner.next_id += 1u64; log_net!( "on_new_protocol_network_connection: id={} prot_conn={:?}", id, @@ -398,7 +398,7 @@ impl ConnectionManager { // Callback from network connection receive loop when it exits // cleans up the entry in the connection table #[instrument(level = "trace", skip(self))] - pub(super) async fn report_connection_finished(&self, connection_id: u64) { + pub(super) async fn report_connection_finished(&self, connection_id: NetworkConnectionId) { // Get channel sender let sender = { let mut inner = self.arc.inner.lock(); diff --git a/veilid-core/src/network_manager/mod.rs b/veilid-core/src/network_manager/mod.rs index 60d26957..e5b27aeb 100644 --- a/veilid-core/src/network_manager/mod.rs +++ b/veilid-core/src/network_manager/mod.rs @@ -67,7 +67,7 @@ struct NetworkComponents { // Statistics per address #[derive(Clone, Default)] pub struct PerAddressStats { - last_seen_ts: u64, + last_seen_ts: Timestamp, transfer_stats_accounting: TransferStatsAccounting, transfer_stats: TransferStatsDownUp, } @@ -99,7 +99,7 @@ impl Default for NetworkManagerStats { #[derive(Debug)] struct ClientWhitelistEntry { - last_seen_ts: u64, + last_seen_ts: Timestamp, } #[derive(Copy, Clone, Debug)] @@ -400,11 +400,11 @@ impl NetworkManager { let mut inner = self.inner.lock(); match inner.client_whitelist.entry(client) { hashlink::lru_cache::Entry::Occupied(mut entry) => { - entry.get_mut().last_seen_ts = get_timestamp() + entry.get_mut().last_seen_ts = get_aligned_timestamp() } hashlink::lru_cache::Entry::Vacant(entry) => { entry.insert(ClientWhitelistEntry { - last_seen_ts: get_timestamp(), + last_seen_ts: get_aligned_timestamp(), }); } } @@ -416,7 +416,7 @@ impl NetworkManager { match inner.client_whitelist.entry(client) { hashlink::lru_cache::Entry::Occupied(mut entry) => { - entry.get_mut().last_seen_ts = get_timestamp(); + entry.get_mut().last_seen_ts = get_aligned_timestamp(); true } hashlink::lru_cache::Entry::Vacant(_) => false, @@ -426,7 +426,7 @@ impl NetworkManager { pub fn purge_client_whitelist(&self) { let timeout_ms = self.with_config(|c| c.network.client_whitelist_timeout_ms); let mut inner = self.inner.lock(); - let cutoff_timestamp = get_timestamp() - ((timeout_ms as u64) * 1000u64); + let cutoff_timestamp = get_aligned_timestamp() - ((timeout_ms as u64) * 1000u64); // Remove clients from the whitelist that haven't been since since our whitelist timeout while inner .client_whitelist @@ -526,7 +526,7 @@ impl NetworkManager { .wrap_err("failed to generate signed receipt")?; // Record the receipt for later - let exp_ts = get_timestamp() + expiration_us; + let exp_ts = get_aligned_timestamp() + expiration_us; receipt_manager.record_receipt(receipt, exp_ts, expected_returns, callback); Ok(out) @@ -550,7 +550,7 @@ impl NetworkManager { .wrap_err("failed to generate signed receipt")?; // Record the receipt for later - let exp_ts = get_timestamp() + expiration_us; + let exp_ts = get_aligned_timestamp() + expiration_us; let eventual = SingleShotEventual::new(Some(ReceiptEvent::Cancelled)); let instance = eventual.instance(); receipt_manager.record_single_shot_receipt(receipt, exp_ts, eventual); @@ -717,7 +717,7 @@ impl NetworkManager { // XXX: do we need a delay here? or another hole punch packet? // Set the hole punch as our 'last connection' to ensure we return the receipt over the direct hole punch - peer_nr.set_last_connection(connection_descriptor, get_timestamp()); + peer_nr.set_last_connection(connection_descriptor, get_aligned_timestamp()); // Return the receipt using the same dial info send the receipt to it rpc.rpc_call_return_receipt(Destination::direct(peer_nr), receipt) @@ -741,7 +741,7 @@ impl NetworkManager { let node_id_secret = routing_table.node_id_secret(); // Get timestamp, nonce - let ts = get_timestamp(); + let ts = get_aligned_timestamp(); let nonce = Crypto::get_random_nonce(); // Encode envelope @@ -1136,7 +1136,7 @@ impl NetworkManager { // ); // Update timestamp for this last connection since we just sent to it - node_ref.set_last_connection(connection_descriptor, get_timestamp()); + node_ref.set_last_connection(connection_descriptor, get_aligned_timestamp()); return Ok(NetworkResult::value(SendDataKind::Existing( connection_descriptor, @@ -1168,7 +1168,7 @@ impl NetworkManager { this.net().send_data_to_dial_info(dial_info, data).await? ); // If we connected to this node directly, save off the last connection so we can use it again - node_ref.set_last_connection(connection_descriptor, get_timestamp()); + node_ref.set_last_connection(connection_descriptor, get_aligned_timestamp()); Ok(NetworkResult::value(SendDataKind::Direct( connection_descriptor, @@ -1337,28 +1337,28 @@ impl NetworkManager { // Get timestamp range let (tsbehind, tsahead) = self.with_config(|c| { ( - c.network.rpc.max_timestamp_behind_ms.map(ms_to_us), - c.network.rpc.max_timestamp_ahead_ms.map(ms_to_us), + c.network.rpc.max_timestamp_behind_ms.map(ms_to_us).map(TimestampDuration::new), + c.network.rpc.max_timestamp_ahead_ms.map(ms_to_us).map(TimestampDuration::new), ) }); // Validate timestamp isn't too old - let ts = get_timestamp(); + let ts = get_aligned_timestamp(); let ets = envelope.get_timestamp(); if let Some(tsbehind) = tsbehind { - if tsbehind > 0 && (ts > ets && ts.saturating_sub(ets) > tsbehind) { + if tsbehind.as_u64() != 0 && (ts > ets && ts.saturating_sub(ets) > tsbehind) { log_net!(debug "envelope time was too far in the past: {}ms ", - timestamp_to_secs(ts.saturating_sub(ets)) * 1000f64 + timestamp_to_secs(ts.saturating_sub(ets).as_u64()) * 1000f64 ); return Ok(false); } } if let Some(tsahead) = tsahead { - if tsahead > 0 && (ts < ets && ets.saturating_sub(ts) > tsahead) { + if tsahead.as_u64() != 0 && (ts < ets && ets.saturating_sub(ts) > tsahead) { log_net!(debug "envelope time was too far in the future: {}ms", - timestamp_to_secs(ets.saturating_sub(ts)) * 1000f64 + timestamp_to_secs(ets.saturating_sub(ts).as_u64()) * 1000f64 ); return Ok(false); } @@ -1497,8 +1497,8 @@ impl NetworkManager { if !has_state { return VeilidStateNetwork { started: false, - bps_down: 0, - bps_up: 0, + bps_down: 0.into(), + bps_up: 0.into(), peers: Vec::new(), }; } @@ -1650,7 +1650,7 @@ impl NetworkManager { // public dialinfo let inconsistent = if inconsistencies.len() >= PUBLIC_ADDRESS_CHANGE_DETECTION_COUNT { - let exp_ts = get_timestamp() + PUBLIC_ADDRESS_INCONSISTENCY_TIMEOUT_US; + let exp_ts = get_aligned_timestamp() + PUBLIC_ADDRESS_INCONSISTENCY_TIMEOUT_US; for i in &inconsistencies { pait.insert(*i, exp_ts); } @@ -1664,7 +1664,7 @@ impl NetworkManager { .entry(key) .or_insert_with(|| HashMap::new()); let exp_ts = - get_timestamp() + PUBLIC_ADDRESS_INCONSISTENCY_PUNISHMENT_TIMEOUT_US; + get_aligned_timestamp() + PUBLIC_ADDRESS_INCONSISTENCY_PUNISHMENT_TIMEOUT_US; for i in inconsistencies { pait.insert(i, exp_ts); } diff --git a/veilid-core/src/network_manager/native/igd_manager.rs b/veilid-core/src/network_manager/native/igd_manager.rs index 3765a4ae..ecf60618 100644 --- a/veilid-core/src/network_manager/native/igd_manager.rs +++ b/veilid-core/src/network_manager/native/igd_manager.rs @@ -6,7 +6,7 @@ use std::net::UdpSocket; const UPNP_GATEWAY_DETECT_TIMEOUT_MS: u32 = 5_000; const UPNP_MAPPING_LIFETIME_MS: u32 = 120_000; const UPNP_MAPPING_ATTEMPTS: u32 = 3; -const UPNP_MAPPING_LIFETIME_US:u64 = (UPNP_MAPPING_LIFETIME_MS as u64) * 1000u64; +const UPNP_MAPPING_LIFETIME_US:TimestampDuration = TimestampDuration::new(UPNP_MAPPING_LIFETIME_MS as u64 * 1000u64); #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] struct PortMapKey { @@ -19,8 +19,8 @@ struct PortMapKey { struct PortMapValue { ext_ip: IpAddr, mapped_port: u16, - timestamp: u64, - renewal_lifetime: u64, + timestamp: Timestamp, + renewal_lifetime: TimestampDuration, renewal_attempts: u32, } @@ -276,7 +276,7 @@ impl IGDManager { }; // Add to mapping list to keep alive - let timestamp = get_timestamp(); + let timestamp = get_aligned_timestamp(); inner.port_maps.insert(PortMapKey { llpt, at, @@ -285,7 +285,7 @@ impl IGDManager { ext_ip, mapped_port, timestamp, - renewal_lifetime: (UPNP_MAPPING_LIFETIME_MS / 2) as u64 * 1000u64, + renewal_lifetime: ((UPNP_MAPPING_LIFETIME_MS / 2) as u64 * 1000u64).into(), renewal_attempts: 0, }); @@ -302,7 +302,7 @@ impl IGDManager { let mut renews: Vec<(PortMapKey, PortMapValue)> = Vec::new(); { let inner = self.inner.lock(); - let now = get_timestamp(); + let now = get_aligned_timestamp(); for (k, v) in &inner.port_maps { let mapping_lifetime = now.saturating_sub(v.timestamp); @@ -357,8 +357,8 @@ impl IGDManager { inner.port_maps.insert(k, PortMapValue { ext_ip: v.ext_ip, mapped_port, - timestamp: get_timestamp(), - renewal_lifetime: (UPNP_MAPPING_LIFETIME_MS / 2) as u64 * 1000u64, + timestamp: get_aligned_timestamp(), + renewal_lifetime: TimestampDuration::new((UPNP_MAPPING_LIFETIME_MS / 2) as u64 * 1000u64), renewal_attempts: 0, }); }, @@ -398,8 +398,8 @@ impl IGDManager { inner.port_maps.insert(k, PortMapValue { ext_ip: v.ext_ip, mapped_port: v.mapped_port, - timestamp: get_timestamp(), - renewal_lifetime: (UPNP_MAPPING_LIFETIME_MS / 2) as u64 * 1000u64, + timestamp: get_aligned_timestamp(), + renewal_lifetime: ((UPNP_MAPPING_LIFETIME_MS / 2) as u64 * 1000u64).into(), renewal_attempts: 0, }); }, @@ -407,7 +407,7 @@ impl IGDManager { log_net!(debug "failed to renew mapped port {:?} -> {:?}: {}", v, k, e); // Get closer to the maximum renewal timeline by a factor of two each time - v.renewal_lifetime = (v.renewal_lifetime + UPNP_MAPPING_LIFETIME_US) / 2; + v.renewal_lifetime = (v.renewal_lifetime + UPNP_MAPPING_LIFETIME_US) / 2u64; v.renewal_attempts += 1; // Store new value to try again diff --git a/veilid-core/src/network_manager/network_connection.rs b/veilid-core/src/network_manager/network_connection.rs index 3b573835..6c832c42 100644 --- a/veilid-core/src/network_manager/network_connection.rs +++ b/veilid-core/src/network_manager/network_connection.rs @@ -78,19 +78,19 @@ enum RecvLoopAction { #[derive(Debug, Clone)] pub struct NetworkConnectionStats { - last_message_sent_time: Option, - last_message_recv_time: Option, + last_message_sent_time: Option, + last_message_recv_time: Option, } -pub type NetworkConnectionId = u64; +pub type NetworkConnectionId = AlignedU64; #[derive(Debug)] pub struct NetworkConnection { connection_id: NetworkConnectionId, descriptor: ConnectionDescriptor, processor: Option>, - established_time: u64, + established_time: Timestamp, stats: Arc>, sender: flume::Sender<(Option, Vec)>, stop_source: Option, @@ -105,7 +105,7 @@ impl NetworkConnection { connection_id: id, descriptor, processor: None, - established_time: get_timestamp(), + established_time: get_aligned_timestamp(), stats: Arc::new(Mutex::new(NetworkConnectionStats { last_message_sent_time: None, last_message_recv_time: None, @@ -153,7 +153,7 @@ impl NetworkConnection { connection_id, descriptor, processor: Some(processor), - established_time: get_timestamp(), + established_time: get_aligned_timestamp(), stats, sender, stop_source: Some(stop_source), @@ -185,7 +185,7 @@ impl NetworkConnection { stats: Arc>, message: Vec, ) -> io::Result> { - let ts = get_timestamp(); + let ts = get_aligned_timestamp(); let out = network_result_try!(protocol_connection.send(message).await?); let mut stats = stats.lock(); @@ -199,7 +199,7 @@ impl NetworkConnection { protocol_connection: &ProtocolNetworkConnection, stats: Arc>, ) -> io::Result>> { - let ts = get_timestamp(); + let ts = get_aligned_timestamp(); let out = network_result_try!(protocol_connection.recv().await?); let mut stats = stats.lock(); @@ -217,7 +217,7 @@ impl NetworkConnection { } #[allow(dead_code)] - pub fn established_time(&self) -> u64 { + pub fn established_time(&self) -> Timestamp { self.established_time } diff --git a/veilid-core/src/network_manager/tasks/public_address_check.rs b/veilid-core/src/network_manager/tasks/public_address_check.rs index 2f7ccc46..91507e37 100644 --- a/veilid-core/src/network_manager/tasks/public_address_check.rs +++ b/veilid-core/src/network_manager/tasks/public_address_check.rs @@ -6,8 +6,8 @@ impl NetworkManager { pub(crate) async fn public_address_check_task_routine( self, stop_token: StopToken, - _last_ts: u64, - cur_ts: u64, + _last_ts: Timestamp, + cur_ts: Timestamp, ) -> EyreResult<()> { // go through public_address_inconsistencies_table and time out things that have expired let mut inner = self.inner.lock(); diff --git a/veilid-core/src/network_manager/tasks/rolling_transfers.rs b/veilid-core/src/network_manager/tasks/rolling_transfers.rs index 0e924024..219790ec 100644 --- a/veilid-core/src/network_manager/tasks/rolling_transfers.rs +++ b/veilid-core/src/network_manager/tasks/rolling_transfers.rs @@ -6,8 +6,8 @@ impl NetworkManager { pub(crate) async fn rolling_transfers_task_routine( self, _stop_token: StopToken, - last_ts: u64, - cur_ts: u64, + last_ts: Timestamp, + cur_ts: Timestamp, ) -> EyreResult<()> { // log_net!("--- network manager rolling_transfers task"); { diff --git a/veilid-core/src/network_manager/tests/test_connection_table.rs b/veilid-core/src/network_manager/tests/test_connection_table.rs index 6f720107..8a48c79d 100644 --- a/veilid-core/src/network_manager/tests/test_connection_table.rs +++ b/veilid-core/src/network_manager/tests/test_connection_table.rs @@ -50,13 +50,13 @@ pub async fn test_add_get_remove() { ))), ); - let c1 = NetworkConnection::dummy(1, a1); - let c1b = NetworkConnection::dummy(10, a1); + let c1 = NetworkConnection::dummy(1.into(), a1); + let c1b = NetworkConnection::dummy(10.into(), a1); let c1h = c1.get_handle(); - let c2 = NetworkConnection::dummy(2, a2); - let c3 = NetworkConnection::dummy(3, a3); - let c4 = NetworkConnection::dummy(4, a4); - let c5 = NetworkConnection::dummy(5, a5); + let c2 = NetworkConnection::dummy(2.into(), a2); + let c3 = NetworkConnection::dummy(3.into(), a3); + let c4 = NetworkConnection::dummy(4.into(), a4); + let c5 = NetworkConnection::dummy(5.into(), a5); assert_eq!(a1, c2.connection_descriptor()); assert_ne!(a3, c4.connection_descriptor()); @@ -68,8 +68,8 @@ pub async fn test_add_get_remove() { assert!(table.add_connection(c1b).is_err()); assert_eq!(table.connection_count(), 1); - assert!(table.remove_connection_by_id(4).is_none()); - assert!(table.remove_connection_by_id(5).is_none()); + assert!(table.remove_connection_by_id(4.into()).is_none()); + assert!(table.remove_connection_by_id(5.into()).is_none()); assert_eq!(table.connection_count(), 1); assert_eq!(table.get_connection_by_descriptor(a1), Some(c1h.clone())); assert_eq!(table.get_connection_by_descriptor(a1), Some(c1h.clone())); @@ -81,41 +81,41 @@ pub async fn test_add_get_remove() { assert_eq!(table.connection_count(), 1); assert_eq!( table - .remove_connection_by_id(1) + .remove_connection_by_id(1.into()) .map(|c| c.connection_descriptor()) .unwrap(), a1 ); assert_eq!(table.connection_count(), 0); - assert!(table.remove_connection_by_id(2).is_none()); + assert!(table.remove_connection_by_id(2.into()).is_none()); assert_eq!(table.connection_count(), 0); assert_eq!(table.get_connection_by_descriptor(a2), None); assert_eq!(table.get_connection_by_descriptor(a1), None); assert_eq!(table.connection_count(), 0); - let c1 = NetworkConnection::dummy(6, a1); + let c1 = NetworkConnection::dummy(6.into(), a1); table.add_connection(c1).unwrap(); - let c2 = NetworkConnection::dummy(7, a2); + let c2 = NetworkConnection::dummy(7.into(), a2); assert_err!(table.add_connection(c2)); table.add_connection(c3).unwrap(); table.add_connection(c4).unwrap(); assert_eq!(table.connection_count(), 3); assert_eq!( table - .remove_connection_by_id(6) + .remove_connection_by_id(6.into()) .map(|c| c.connection_descriptor()) .unwrap(), a2 ); assert_eq!( table - .remove_connection_by_id(3) + .remove_connection_by_id(3.into()) .map(|c| c.connection_descriptor()) .unwrap(), a3 ); assert_eq!( table - .remove_connection_by_id(4) + .remove_connection_by_id(4.into()) .map(|c| c.connection_descriptor()) .unwrap(), a4 diff --git a/veilid-core/src/receipt_manager.rs b/veilid-core/src/receipt_manager.rs index be548ff1..17fcf000 100644 --- a/veilid-core/src/receipt_manager.rs +++ b/veilid-core/src/receipt_manager.rs @@ -70,7 +70,7 @@ impl fmt::Debug for ReceiptRecordCallbackType { } pub struct ReceiptRecord { - expiration_ts: u64, + expiration_ts: Timestamp, receipt: Receipt, expected_returns: u32, returns_so_far: u32, @@ -92,7 +92,7 @@ impl fmt::Debug for ReceiptRecord { impl ReceiptRecord { pub fn new( receipt: Receipt, - expiration_ts: u64, + expiration_ts: Timestamp, expected_returns: u32, receipt_callback: impl ReceiptCallback, ) -> Self { @@ -107,7 +107,7 @@ impl ReceiptRecord { pub fn new_single_shot( receipt: Receipt, - expiration_ts: u64, + expiration_ts: Timestamp, eventual: ReceiptSingleShotType, ) -> Self { Self { @@ -123,7 +123,7 @@ impl ReceiptRecord { /* XXX: may be useful for O(1) timestamp expiration #[derive(Clone, Debug)] struct ReceiptRecordTimestampSort { - expiration_ts: u64, + expiration_ts: Timestamp, record: Arc>, } @@ -150,7 +150,7 @@ impl PartialOrd for ReceiptRecordTimestampSort { pub struct ReceiptManagerInner { network_manager: NetworkManager, records_by_nonce: BTreeMap>>, - next_oldest_ts: Option, + next_oldest_ts: Option, stop_source: Option, timeout_task: MustJoinSingleFuture<()>, } @@ -219,9 +219,9 @@ impl ReceiptManager { } #[instrument(level = "trace", skip(self))] - pub async fn timeout_task_routine(self, now: u64, stop_token: StopToken) { + pub async fn timeout_task_routine(self, now: Timestamp, stop_token: StopToken) { // Go through all receipts and build a list of expired nonces - let mut new_next_oldest_ts: Option = None; + let mut new_next_oldest_ts: Option = None; let mut expired_records = Vec::new(); { let mut inner = self.inner.lock(); @@ -280,7 +280,7 @@ impl ReceiptManager { }; (inner.next_oldest_ts, inner.timeout_task.clone(), stop_token) }; - let now = get_timestamp(); + let now = get_aligned_timestamp(); // If we have at least one timestamp to expire, lets do it if let Some(next_oldest_ts) = next_oldest_ts { if now >= next_oldest_ts { @@ -318,7 +318,7 @@ impl ReceiptManager { pub fn record_receipt( &self, receipt: Receipt, - expiration: u64, + expiration: Timestamp, expected_returns: u32, callback: impl ReceiptCallback, ) { @@ -339,7 +339,7 @@ impl ReceiptManager { pub fn record_single_shot_receipt( &self, receipt: Receipt, - expiration: u64, + expiration: Timestamp, eventual: ReceiptSingleShotType, ) { let receipt_nonce = receipt.get_nonce(); @@ -356,7 +356,7 @@ impl ReceiptManager { fn update_next_oldest_timestamp(inner: &mut ReceiptManagerInner) { // Update the next oldest timestamp - let mut new_next_oldest_ts: Option = None; + let mut new_next_oldest_ts: Option = None; for v in inner.records_by_nonce.values() { let receipt_inner = v.lock(); if new_next_oldest_ts.is_none() diff --git a/veilid-core/src/routing_table/bucket.rs b/veilid-core/src/routing_table/bucket.rs index d1a93d2e..ce0794bf 100644 --- a/veilid-core/src/routing_table/bucket.rs +++ b/veilid-core/src/routing_table/bucket.rs @@ -120,7 +120,7 @@ impl Bucket { .iter() .map(|(k, v)| (k.clone(), v.clone())) .collect(); - let cur_ts = get_timestamp(); + let cur_ts = get_aligned_timestamp(); sorted_entries.sort_by(|a, b| -> core::cmp::Ordering { if a.0 == b.0 { return core::cmp::Ordering::Equal; diff --git a/veilid-core/src/routing_table/bucket_entry.rs b/veilid-core/src/routing_table/bucket_entry.rs index c64cb7ee..ad8b1320 100644 --- a/veilid-core/src/routing_table/bucket_entry.rs +++ b/veilid-core/src/routing_table/bucket_entry.rs @@ -51,7 +51,7 @@ pub struct BucketEntryPublicInternet { /// The PublicInternet node info signed_node_info: Option>, /// The last node info timestamp of ours that this entry has seen - last_seen_our_node_info_ts: u64, + last_seen_our_node_info_ts: Timestamp, /// Last known node status node_status: Option, } @@ -63,7 +63,7 @@ pub struct BucketEntryLocalNetwork { /// The LocalNetwork node info signed_node_info: Option>, /// The last node info timestamp of ours that this entry has seen - last_seen_our_node_info_ts: u64, + last_seen_our_node_info_ts: Timestamp, /// Last known node status node_status: Option, } @@ -93,7 +93,7 @@ pub struct BucketEntryInner { updated_since_last_network_change: bool, /// The last connection descriptors used to contact this node, per protocol type #[with(Skip)] - last_connections: BTreeMap, + last_connections: BTreeMap, /// The node info for this entry on the publicinternet routing domain public_internet: BucketEntryPublicInternet, /// The node info for this entry on the localnetwork routing domain @@ -148,7 +148,7 @@ impl BucketEntryInner { } // Less is more reliable then faster - pub fn cmp_fastest_reliable(cur_ts: u64, e1: &Self, e2: &Self) -> std::cmp::Ordering { + pub fn cmp_fastest_reliable(cur_ts: Timestamp, e1: &Self, e2: &Self) -> std::cmp::Ordering { // Reverse compare so most reliable is at front let ret = e2.state(cur_ts).cmp(&e1.state(cur_ts)); if ret != std::cmp::Ordering::Equal { @@ -170,7 +170,7 @@ impl BucketEntryInner { } // Less is more reliable then older - pub fn cmp_oldest_reliable(cur_ts: u64, e1: &Self, e2: &Self) -> std::cmp::Ordering { + pub fn cmp_oldest_reliable(cur_ts: Timestamp, e1: &Self, e2: &Self) -> std::cmp::Ordering { // Reverse compare so most reliable is at front let ret = e2.state(cur_ts).cmp(&e1.state(cur_ts)); if ret != std::cmp::Ordering::Equal { @@ -191,7 +191,7 @@ impl BucketEntryInner { } } - pub fn sort_fastest_reliable_fn(cur_ts: u64) -> impl FnMut(&Self, &Self) -> std::cmp::Ordering { + pub fn sort_fastest_reliable_fn(cur_ts: Timestamp) -> impl FnMut(&Self, &Self) -> std::cmp::Ordering { move |e1, e2| Self::cmp_fastest_reliable(cur_ts, e1, e2) } @@ -231,7 +231,7 @@ impl BucketEntryInner { // No need to update the signednodeinfo though since the timestamp is the same // Touch the node and let it try to live again self.updated_since_last_network_change = true; - self.touch_last_seen(get_timestamp()); + self.touch_last_seen(get_aligned_timestamp()); } return; } @@ -258,7 +258,7 @@ impl BucketEntryInner { // Update the signed node info *opt_current_sni = Some(Box::new(signed_node_info)); self.updated_since_last_network_change = true; - self.touch_last_seen(get_timestamp()); + self.touch_last_seen(get_aligned_timestamp()); } pub fn has_node_info(&self, routing_domain_set: RoutingDomainSet) -> bool { @@ -367,7 +367,7 @@ impl BucketEntryInner { } // Stores a connection descriptor in this entry's table of last connections - pub fn set_last_connection(&mut self, last_connection: ConnectionDescriptor, timestamp: u64) { + pub fn set_last_connection(&mut self, last_connection: ConnectionDescriptor, timestamp: Timestamp) { let key = self.descriptor_to_key(last_connection); self.last_connections .insert(key, (last_connection, timestamp)); @@ -431,7 +431,7 @@ impl BucketEntryInner { } else { // If this is not connection oriented, then we check our last seen time // to see if this mapping has expired (beyond our timeout) - let cur_ts = get_timestamp(); + let cur_ts = get_aligned_timestamp(); (v.1 + (CONNECTIONLESS_TIMEOUT_SECS as u64 * 1_000_000u64)) >= cur_ts }; @@ -455,7 +455,7 @@ impl BucketEntryInner { self.min_max_version } - pub fn state(&self, cur_ts: u64) -> BucketEntryState { + pub fn state(&self, cur_ts: Timestamp) -> BucketEntryState { if self.check_reliable(cur_ts) { BucketEntryState::Reliable } else if self.check_dead(cur_ts) { @@ -494,7 +494,7 @@ impl BucketEntryInner { } } - pub fn set_our_node_info_ts(&mut self, routing_domain: RoutingDomain, seen_ts: u64) { + pub fn set_our_node_info_ts(&mut self, routing_domain: RoutingDomain, seen_ts: Timestamp) { match routing_domain { RoutingDomain::LocalNetwork => { self.local_network.last_seen_our_node_info_ts = seen_ts; @@ -508,7 +508,7 @@ impl BucketEntryInner { pub fn has_seen_our_node_info_ts( &self, routing_domain: RoutingDomain, - our_node_info_ts: u64, + our_node_info_ts: Timestamp, ) -> bool { match routing_domain { RoutingDomain::LocalNetwork => { @@ -530,7 +530,7 @@ impl BucketEntryInner { ///// stats methods // called every ROLLING_TRANSFERS_INTERVAL_SECS seconds - pub(super) fn roll_transfers(&mut self, last_ts: u64, cur_ts: u64) { + pub(super) fn roll_transfers(&mut self, last_ts: Timestamp, cur_ts: Timestamp) { self.transfer_stats_accounting.roll_transfers( last_ts, cur_ts, @@ -539,12 +539,12 @@ impl BucketEntryInner { } // Called for every round trip packet we receive - fn record_latency(&mut self, latency: u64) { + fn record_latency(&mut self, latency: TimestampDuration) { self.peer_stats.latency = Some(self.latency_stats_accounting.record_latency(latency)); } ///// state machine handling - pub(super) fn check_reliable(&self, cur_ts: u64) -> bool { + pub(super) fn check_reliable(&self, cur_ts: Timestamp) -> bool { // If we have had any failures to send, this is not reliable if self.peer_stats.rpc_stats.failed_to_send > 0 { return false; @@ -558,7 +558,7 @@ impl BucketEntryInner { } } } - pub(super) fn check_dead(&self, cur_ts: u64) -> bool { + pub(super) fn check_dead(&self, cur_ts: Timestamp) -> bool { // If we have failured to send NEVER_REACHED_PING_COUNT times in a row, the node is dead if self.peer_stats.rpc_stats.failed_to_send >= NEVER_REACHED_PING_COUNT { return true; @@ -575,14 +575,14 @@ impl BucketEntryInner { } /// Return the last time we either saw a node, or asked it a question - fn latest_contact_time(&self) -> Option { + fn latest_contact_time(&self) -> Option { self.peer_stats .rpc_stats .last_seen_ts - .max(self.peer_stats.rpc_stats.last_question) + .max(self.peer_stats.rpc_stats.last_question_ts) } - fn needs_constant_ping(&self, cur_ts: u64, interval: u64) -> bool { + fn needs_constant_ping(&self, cur_ts: Timestamp, interval: Timestamp) -> bool { // If we have not either seen the node in the last 'interval' then we should ping it let latest_contact_time = self.latest_contact_time(); @@ -596,7 +596,7 @@ impl BucketEntryInner { } // Check if this node needs a ping right now to validate it is still reachable - pub(super) fn needs_ping(&self, cur_ts: u64, needs_keepalive: bool) -> bool { + pub(super) fn needs_ping(&self, cur_ts: Timestamp, needs_keepalive: bool) -> bool { // See which ping pattern we are to use let state = self.state(cur_ts); @@ -653,7 +653,7 @@ impl BucketEntryInner { } } - pub(super) fn touch_last_seen(&mut self, ts: u64) { + pub(super) fn touch_last_seen(&mut self, ts: Timestamp) { // Mark the node as seen if self .peer_stats @@ -667,7 +667,7 @@ impl BucketEntryInner { self.peer_stats.rpc_stats.last_seen_ts = Some(ts); } - pub(super) fn _state_debug_info(&self, cur_ts: u64) -> String { + pub(super) fn _state_debug_info(&self, cur_ts: Timestamp) -> String { let first_consecutive_seen_ts = if let Some(first_consecutive_seen_ts) = self.peer_stats.rpc_stats.first_consecutive_seen_ts { @@ -698,26 +698,26 @@ impl BucketEntryInner { //////////////////////////////////////////////////////////////// /// Called when rpc processor things happen - pub(super) fn question_sent(&mut self, ts: u64, bytes: u64, expects_answer: bool) { + pub(super) fn question_sent(&mut self, ts: Timestamp, bytes: u64, expects_answer: bool) { self.transfer_stats_accounting.add_up(bytes); self.peer_stats.rpc_stats.messages_sent += 1; self.peer_stats.rpc_stats.failed_to_send = 0; if expects_answer { self.peer_stats.rpc_stats.questions_in_flight += 1; - self.peer_stats.rpc_stats.last_question = Some(ts); + self.peer_stats.rpc_stats.last_question_ts = Some(ts); } } - pub(super) fn question_rcvd(&mut self, ts: u64, bytes: u64) { + pub(super) fn question_rcvd(&mut self, ts: Timestamp, bytes: u64) { self.transfer_stats_accounting.add_down(bytes); self.peer_stats.rpc_stats.messages_rcvd += 1; self.touch_last_seen(ts); } - pub(super) fn answer_sent(&mut self, bytes: u64) { + pub(super) fn answer_sent(&mut self, bytes: ByteCount) { self.transfer_stats_accounting.add_up(bytes); self.peer_stats.rpc_stats.messages_sent += 1; self.peer_stats.rpc_stats.failed_to_send = 0; } - pub(super) fn answer_rcvd(&mut self, send_ts: u64, recv_ts: u64, bytes: u64) { + pub(super) fn answer_rcvd(&mut self, send_ts: Timestamp, recv_ts: Timestamp, bytes: ByteCount) { self.transfer_stats_accounting.add_down(bytes); self.peer_stats.rpc_stats.messages_rcvd += 1; self.peer_stats.rpc_stats.questions_in_flight -= 1; @@ -730,9 +730,9 @@ impl BucketEntryInner { self.peer_stats.rpc_stats.questions_in_flight -= 1; self.peer_stats.rpc_stats.recent_lost_answers += 1; } - pub(super) fn failed_to_send(&mut self, ts: u64, expects_answer: bool) { + pub(super) fn failed_to_send(&mut self, ts: Timestamp, expects_answer: bool) { if expects_answer { - self.peer_stats.rpc_stats.last_question = Some(ts); + self.peer_stats.rpc_stats.last_question_ts = Some(ts); } self.peer_stats.rpc_stats.failed_to_send += 1; self.peer_stats.rpc_stats.first_consecutive_seen_ts = None; @@ -747,7 +747,7 @@ pub struct BucketEntry { impl BucketEntry { pub(super) fn new() -> Self { - let now = get_timestamp(); + let now = get_aligned_timestamp(); Self { ref_count: AtomicU32::new(0), inner: RwLock::new(BucketEntryInner { diff --git a/veilid-core/src/routing_table/debug.rs b/veilid-core/src/routing_table/debug.rs index 45779587..64299c78 100644 --- a/veilid-core/src/routing_table/debug.rs +++ b/veilid-core/src/routing_table/debug.rs @@ -104,7 +104,7 @@ impl RoutingTable { pub(crate) fn debug_info_entries(&self, limit: usize, min_state: BucketEntryState) -> String { let inner = self.inner.read(); let inner = &*inner; - let cur_ts = get_timestamp(); + let cur_ts = get_aligned_timestamp(); let mut out = String::new(); @@ -164,7 +164,7 @@ impl RoutingTable { pub(crate) fn debug_info_buckets(&self, min_state: BucketEntryState) -> String { let inner = self.inner.read(); let inner = &*inner; - let cur_ts = get_timestamp(); + let cur_ts = get_aligned_timestamp(); let mut out = String::new(); const COLS: usize = 16; diff --git a/veilid-core/src/routing_table/mod.rs b/veilid-core/src/routing_table/mod.rs index 32ff20d3..e83fb9eb 100644 --- a/veilid-core/src/routing_table/mod.rs +++ b/veilid-core/src/routing_table/mod.rs @@ -468,14 +468,14 @@ impl RoutingTable { pub fn get_nodes_needing_ping( &self, routing_domain: RoutingDomain, - cur_ts: u64, + cur_ts: Timestamp, ) -> Vec { self.inner .read() .get_nodes_needing_ping(self.clone(), routing_domain, cur_ts) } - pub fn get_all_nodes(&self, cur_ts: u64) -> Vec { + pub fn get_all_nodes(&self, cur_ts: Timestamp) -> Vec { let inner = self.inner.read(); inner.get_all_nodes(self.clone(), cur_ts) } @@ -542,7 +542,7 @@ impl RoutingTable { &self, node_id: DHTKey, descriptor: ConnectionDescriptor, - timestamp: u64, + timestamp: Timestamp, ) -> Option { self.inner.write().register_node_with_existing_connection( self.clone(), @@ -774,7 +774,7 @@ impl RoutingTable { pub fn find_peers_with_sort_and_filter( &self, node_count: usize, - cur_ts: u64, + cur_ts: Timestamp, filters: VecDeque, compare: C, transform: T, @@ -969,7 +969,7 @@ impl RoutingTable { pub fn find_inbound_relay( &self, routing_domain: RoutingDomain, - cur_ts: u64, + cur_ts: Timestamp, ) -> Option { // Get relay filter function let relay_node_filter = match routing_domain { diff --git a/veilid-core/src/routing_table/node_ref.rs b/veilid-core/src/routing_table/node_ref.rs index 8ae8929a..7e53effe 100644 --- a/veilid-core/src/routing_table/node_ref.rs +++ b/veilid-core/src/routing_table/node_ref.rs @@ -119,7 +119,7 @@ pub trait NodeRefBase: Sized { fn set_min_max_version(&self, min_max_version: VersionRange) { self.operate_mut(|_rti, e| e.set_min_max_version(min_max_version)) } - fn state(&self, cur_ts: u64) -> BucketEntryState { + fn state(&self, cur_ts: Timestamp) -> BucketEntryState { self.operate(|_rti, e| e.state(cur_ts)) } fn peer_stats(&self) -> PeerStats { @@ -140,21 +140,21 @@ pub trait NodeRefBase: Sized { .unwrap_or(false) }) } - fn node_info_ts(&self, routing_domain: RoutingDomain) -> u64 { + fn node_info_ts(&self, routing_domain: RoutingDomain) -> Timestamp { self.operate(|_rti, e| { e.signed_node_info(routing_domain) .map(|sni| sni.timestamp()) - .unwrap_or(0u64) + .unwrap_or(0u64.into()) }) } fn has_seen_our_node_info_ts( &self, routing_domain: RoutingDomain, - our_node_info_ts: u64, + our_node_info_ts: Timestamp, ) -> bool { self.operate(|_rti, e| e.has_seen_our_node_info_ts(routing_domain, our_node_info_ts)) } - fn set_our_node_info_ts(&self, routing_domain: RoutingDomain, seen_ts: u64) { + fn set_our_node_info_ts(&self, routing_domain: RoutingDomain, seen_ts: Timestamp) { self.operate_mut(|_rti, e| e.set_our_node_info_ts(routing_domain, seen_ts)); } fn network_class(&self, routing_domain: RoutingDomain) -> Option { @@ -277,7 +277,7 @@ pub trait NodeRefBase: Sized { self.operate_mut(|_rti, e| e.clear_last_connections()) } - fn set_last_connection(&self, connection_descriptor: ConnectionDescriptor, ts: u64) { + fn set_last_connection(&self, connection_descriptor: ConnectionDescriptor, ts: Timestamp) { self.operate_mut(|rti, e| { e.set_last_connection(connection_descriptor, ts); rti.touch_recent_peer(self.common().node_id, connection_descriptor); @@ -297,25 +297,25 @@ pub trait NodeRefBase: Sized { }) } - fn stats_question_sent(&self, ts: u64, bytes: u64, expects_answer: bool) { + fn stats_question_sent(&self, ts: Timestamp, bytes: Timestamp, expects_answer: bool) { self.operate_mut(|rti, e| { rti.transfer_stats_accounting().add_up(bytes); e.question_sent(ts, bytes, expects_answer); }) } - fn stats_question_rcvd(&self, ts: u64, bytes: u64) { + fn stats_question_rcvd(&self, ts: Timestamp, bytes: ByteCount) { self.operate_mut(|rti, e| { rti.transfer_stats_accounting().add_down(bytes); e.question_rcvd(ts, bytes); }) } - fn stats_answer_sent(&self, bytes: u64) { + fn stats_answer_sent(&self, bytes: ByteCount) { self.operate_mut(|rti, e| { rti.transfer_stats_accounting().add_up(bytes); e.answer_sent(bytes); }) } - fn stats_answer_rcvd(&self, send_ts: u64, recv_ts: u64, bytes: u64) { + fn stats_answer_rcvd(&self, send_ts: Timestamp, recv_ts: Timestamp, bytes: ByteCount) { self.operate_mut(|rti, e| { rti.transfer_stats_accounting().add_down(bytes); rti.latency_stats_accounting() @@ -328,7 +328,7 @@ pub trait NodeRefBase: Sized { e.question_lost(); }) } - fn stats_failed_to_send(&self, ts: u64, expects_answer: bool) { + fn stats_failed_to_send(&self, ts: Timestamp, expects_answer: bool) { self.operate_mut(|_rti, e| { e.failed_to_send(ts, expects_answer); }) diff --git a/veilid-core/src/routing_table/route_spec_store.rs b/veilid-core/src/routing_table/route_spec_store.rs index fbf21951..2e6d8fda 100644 --- a/veilid-core/src/routing_table/route_spec_store.rs +++ b/veilid-core/src/routing_table/route_spec_store.rs @@ -7,7 +7,7 @@ use rkyv::{ /// The size of the remote private route cache const REMOTE_PRIVATE_ROUTE_CACHE_SIZE: usize = 1024; /// Remote private route cache entries expire in 5 minutes if they haven't been used -const REMOTE_PRIVATE_ROUTE_CACHE_EXPIRY: u64 = 300_000_000u64; +const REMOTE_PRIVATE_ROUTE_CACHE_EXPIRY: TimestampDuration = 300_000_000u64.into(); /// Amount of time a route can remain idle before it gets tested const ROUTE_MIN_IDLE_TIME_MS: u32 = 30_000; @@ -80,25 +80,25 @@ impl RouteStats { } /// Mark a route as having received something - pub fn record_received(&mut self, cur_ts: u64, bytes: u64) { + pub fn record_received(&mut self, cur_ts: Timestamp, bytes: ByteCount) { self.last_received_ts = Some(cur_ts); self.last_tested_ts = Some(cur_ts); self.transfer_stats_accounting.add_down(bytes); } /// Mark a route as having been sent to - pub fn record_sent(&mut self, cur_ts: u64, bytes: u64) { + pub fn record_sent(&mut self, cur_ts: Timestamp, bytes: ByteCount) { self.last_sent_ts = Some(cur_ts); self.transfer_stats_accounting.add_up(bytes); } /// Mark a route as having been sent to - pub fn record_latency(&mut self, latency: u64) { + pub fn record_latency(&mut self, latency: TimestampDuration) { self.latency_stats = self.latency_stats_accounting.record_latency(latency); } /// Mark a route as having been tested - pub fn record_tested(&mut self, cur_ts: u64) { + pub fn record_tested(&mut self, cur_ts: Timestamp) { self.last_tested_ts = Some(cur_ts); // Reset question_lost and failed_to_send if we test clean @@ -107,7 +107,7 @@ impl RouteStats { } /// Roll transfers for these route stats - pub fn roll_transfers(&mut self, last_ts: u64, cur_ts: u64) { + pub fn roll_transfers(&mut self, last_ts: Timestamp, cur_ts: Timestamp) { self.transfer_stats_accounting.roll_transfers( last_ts, cur_ts, @@ -133,7 +133,7 @@ impl RouteStats { } /// Check if a route needs testing - pub fn needs_testing(&self, cur_ts: u64) -> bool { + pub fn needs_testing(&self, cur_ts: Timestamp) -> bool { // Has the route had any failures lately? if self.questions_lost > 0 || self.failed_to_send > 0 { // If so, always test @@ -634,7 +634,7 @@ impl RouteSpecStore { .map(|nr| nr.node_id()); // Get list of all nodes, and sort them for selection - let cur_ts = get_timestamp(); + let cur_ts = get_aligned_timestamp(); let filter = Box::new( move |rti: &RoutingTableInner, k: DHTKey, v: Option>| -> bool { // Exclude our own node from routes @@ -872,22 +872,25 @@ impl RouteSpecStore { Ok(Some(public_key)) } - #[instrument(level = "trace", skip(self, data), ret, err)] + #[instrument(level = "trace", skip(self, data), ret)] pub fn validate_signatures( &self, public_key: &DHTKey, signatures: &[DHTSignature], data: &[u8], last_hop_id: DHTKey, - ) -> EyreResult> { + ) -> Option<(DHTKeySecret, SafetySpec)> { let inner = &*self.inner.lock(); - let rsd = Self::detail(inner, &public_key).ok_or_else(|| eyre!("route does not exist"))?; + let Some(rsd) = Self::detail(inner, &public_key) else { + log_rpc!(debug "route does not exist: {:?}", public_key); + return None; + }; // Ensure we have the right number of signatures if signatures.len() != rsd.hops.len() - 1 { // Wrong number of signatures log_rpc!(debug "wrong number of signatures ({} should be {}) for routed operation on private route {}", signatures.len(), rsd.hops.len() - 1, public_key); - return Ok(None); + return None; } // Validate signatures to ensure the route was handled by the nodes and not messed with // This is in private route (reverse) order as we are receiving over the route @@ -897,18 +900,18 @@ impl RouteSpecStore { // Verify the node we received the routed operation from is the last hop in our route if *hop_public_key != last_hop_id { log_rpc!(debug "received routed operation from the wrong hop ({} should be {}) on private route {}", hop_public_key.encode(), last_hop_id.encode(), public_key); - return Ok(None); + return None; } } else { // Verify a signature for a hop node along the route if let Err(e) = verify(hop_public_key, data, &signatures[hop_n]) { log_rpc!(debug "failed to verify signature for hop {} at {} on private route {}: {}", hop_n, hop_public_key, public_key, e); - return Ok(None); + return None; } } } // We got the correct signatures, return a key and response safety spec - Ok(Some(( + Some(( rsd.secret_key, SafetySpec { preferred_route: Some(*public_key), @@ -916,7 +919,7 @@ impl RouteSpecStore { stability: rsd.stability, sequencing: rsd.sequencing, }, - ))) + )) } #[instrument(level = "trace", skip(self), ret, err)] @@ -1002,7 +1005,7 @@ impl RouteSpecStore { pub async fn test_route(&self, key: &DHTKey) -> EyreResult { let is_remote = { let inner = &mut *self.inner.lock(); - let cur_ts = get_timestamp(); + let cur_ts = get_aligned_timestamp(); Self::with_peek_remote_private_route(inner, cur_ts, key, |_| {}).is_some() }; if is_remote { @@ -1066,7 +1069,7 @@ impl RouteSpecStore { pub fn release_route(&self, key: &DHTKey) -> bool { let is_remote = { let inner = &mut *self.inner.lock(); - let cur_ts = get_timestamp(); + let cur_ts = get_aligned_timestamp(); Self::with_peek_remote_private_route(inner, cur_ts, key, |_| {}).is_some() }; if is_remote { @@ -1087,7 +1090,7 @@ impl RouteSpecStore { directions: DirectionSet, avoid_node_ids: &[DHTKey], ) -> Option { - let cur_ts = get_timestamp(); + let cur_ts = get_aligned_timestamp(); let mut routes = Vec::new(); @@ -1167,7 +1170,7 @@ impl RouteSpecStore { /// Get the debug description of a route pub fn debug_route(&self, key: &DHTKey) -> Option { let inner = &mut *self.inner.lock(); - let cur_ts = get_timestamp(); + let cur_ts = get_aligned_timestamp(); // If this is a remote route, print it if let Some(s) = Self::with_peek_remote_private_route(inner, cur_ts, key, |rpi| format!("{:#?}", rpi)) @@ -1570,7 +1573,7 @@ impl RouteSpecStore { } // store the private route in our cache - let cur_ts = get_timestamp(); + let cur_ts = get_aligned_timestamp(); let key = Self::with_create_remote_private_route(inner, cur_ts, private_route, |r| { r.private_route.as_ref().unwrap().public_key.clone() }); @@ -1593,7 +1596,7 @@ impl RouteSpecStore { /// Retrieve an imported remote private route by its public key pub fn get_remote_private_route(&self, key: &DHTKey) -> Option { let inner = &mut *self.inner.lock(); - let cur_ts = get_timestamp(); + let cur_ts = get_aligned_timestamp(); Self::with_get_remote_private_route(inner, cur_ts, key, |r| { r.private_route.as_ref().unwrap().clone() }) @@ -1602,7 +1605,7 @@ impl RouteSpecStore { /// Retrieve an imported remote private route by its public key but don't 'touch' it pub fn peek_remote_private_route(&self, key: &DHTKey) -> Option { let inner = &mut *self.inner.lock(); - let cur_ts = get_timestamp(); + let cur_ts = get_aligned_timestamp(); Self::with_peek_remote_private_route(inner, cur_ts, key, |r| { r.private_route.as_ref().unwrap().clone() }) @@ -1611,7 +1614,7 @@ impl RouteSpecStore { // get or create a remote private route cache entry fn with_create_remote_private_route( inner: &mut RouteSpecStoreInner, - cur_ts: u64, + cur_ts: Timestamp, private_route: PrivateRoute, f: F, ) -> R @@ -1660,7 +1663,7 @@ impl RouteSpecStore { // get a remote private route cache entry fn with_get_remote_private_route( inner: &mut RouteSpecStoreInner, - cur_ts: u64, + cur_ts: Timestamp, key: &DHTKey, f: F, ) -> Option @@ -1680,7 +1683,7 @@ impl RouteSpecStore { // peek a remote private route cache entry fn with_peek_remote_private_route( inner: &mut RouteSpecStoreInner, - cur_ts: u64, + cur_ts: Timestamp, key: &DHTKey, f: F, ) -> Option @@ -1714,7 +1717,7 @@ impl RouteSpecStore { let opt_rpr_node_info_ts = { let inner = &mut *self.inner.lock(); - let cur_ts = get_timestamp(); + let cur_ts = get_aligned_timestamp(); Self::with_peek_remote_private_route(inner, cur_ts, key, |rpr| { rpr.last_seen_our_node_info_ts }) @@ -1736,7 +1739,7 @@ impl RouteSpecStore { pub fn mark_remote_private_route_seen_our_node_info( &self, key: &DHTKey, - cur_ts: u64, + cur_ts: Timestamp, ) -> EyreResult<()> { let our_node_info_ts = { let rti = &*self.unlocked_inner.routing_table.inner.read(); @@ -1765,7 +1768,7 @@ impl RouteSpecStore { } /// Get the route statistics for any route we know about, local or remote - pub fn with_route_stats(&self, cur_ts: u64, key: &DHTKey, f: F) -> Option + pub fn with_route_stats(&self, cur_ts: Timestamp, key: &DHTKey, f: F) -> Option where F: FnOnce(&mut RouteStats) -> R, { @@ -1822,7 +1825,7 @@ impl RouteSpecStore { } /// Process transfer statistics to get averages - pub fn roll_transfers(&self, last_ts: u64, cur_ts: u64) { + pub fn roll_transfers(&self, last_ts: Timestamp, cur_ts: Timestamp) { let inner = &mut *self.inner.lock(); // Roll transfers for locally allocated routes diff --git a/veilid-core/src/routing_table/routing_table_inner.rs b/veilid-core/src/routing_table/routing_table_inner.rs index b867bc03..de74e230 100644 --- a/veilid-core/src/routing_table/routing_table_inner.rs +++ b/veilid-core/src/routing_table/routing_table_inner.rs @@ -227,7 +227,7 @@ impl RoutingTableInner { } pub fn reset_all_updated_since_last_network_change(&mut self) { - let cur_ts = get_timestamp(); + let cur_ts = get_aligned_timestamp(); self.with_entries_mut(cur_ts, BucketEntryState::Dead, |rti, _, v| { v.with_mut(rti, |_rti, e| { e.set_updated_since_last_network_change(false) @@ -347,7 +347,7 @@ impl RoutingTableInner { // If the local network topology has changed, nuke the existing local node info and let new local discovery happen if changed { - let cur_ts = get_timestamp(); + let cur_ts = get_aligned_timestamp(); self.with_entries_mut(cur_ts, BucketEntryState::Dead, |rti, _, e| { e.with_mut(rti, |_rti, e| { e.clear_signed_node_info(RoutingDomain::LocalNetwork); @@ -426,7 +426,7 @@ impl RoutingTableInner { min_state: BucketEntryState, ) -> usize { let mut count = 0usize; - let cur_ts = get_timestamp(); + let cur_ts = get_aligned_timestamp(); self.with_entries(cur_ts, min_state, |rti, _, e| { if e.with(rti, |rti, e| e.best_routing_domain(rti, routing_domain_set)) .is_some() @@ -466,7 +466,7 @@ impl RoutingTableInner { F: FnMut(&mut RoutingTableInner, DHTKey, Arc) -> Option, >( &mut self, - cur_ts: u64, + cur_ts: Timestamp, min_state: BucketEntryState, mut f: F, ) -> Option { @@ -491,7 +491,7 @@ impl RoutingTableInner { &self, outer_self: RoutingTable, routing_domain: RoutingDomain, - cur_ts: u64, + cur_ts: Timestamp, ) -> Vec { // Collect relay nodes let opt_relay_id = self.with_routing_domain(routing_domain, |rd| { @@ -531,7 +531,7 @@ impl RoutingTableInner { node_refs } - pub fn get_all_nodes(&self, outer_self: RoutingTable, cur_ts: u64) -> Vec { + pub fn get_all_nodes(&self, outer_self: RoutingTable, cur_ts: Timestamp) -> Vec { let mut node_refs = Vec::::with_capacity(self.bucket_entry_count); self.with_entries(cur_ts, BucketEntryState::Unreliable, |_rti, k, v| { node_refs.push(NodeRef::new(outer_self.clone(), k, v, None)); @@ -700,7 +700,7 @@ impl RoutingTableInner { outer_self: RoutingTable, node_id: DHTKey, descriptor: ConnectionDescriptor, - timestamp: u64, + timestamp: Timestamp, ) -> Option { let out = self.create_node_ref(outer_self, node_id, |_rti, e| { // this node is live because it literally just connected to us @@ -719,7 +719,7 @@ impl RoutingTableInner { pub fn get_routing_table_health(&self) -> RoutingTableHealth { let mut health = RoutingTableHealth::default(); - let cur_ts = get_timestamp(); + let cur_ts = get_aligned_timestamp(); for bucket in &self.buckets { for (_, v) in bucket.entries() { match v.with(self, |_rti, e| e.state(cur_ts)) { @@ -876,7 +876,7 @@ impl RoutingTableInner { where T: for<'r> FnMut(&'r RoutingTableInner, DHTKey, Option>) -> O, { - let cur_ts = get_timestamp(); + let cur_ts = get_aligned_timestamp(); // Add filter to remove dead nodes always let filter_dead = Box::new( @@ -961,7 +961,7 @@ impl RoutingTableInner { where T: for<'r> FnMut(&'r RoutingTableInner, DHTKey, Option>) -> O, { - let cur_ts = get_timestamp(); + let cur_ts = get_aligned_timestamp(); let node_count = { let config = self.config(); let c = config.get(); diff --git a/veilid-core/src/routing_table/stats_accounting.rs b/veilid-core/src/routing_table/stats_accounting.rs index c02ab6ca..2c711493 100644 --- a/veilid-core/src/routing_table/stats_accounting.rs +++ b/veilid-core/src/routing_table/stats_accounting.rs @@ -13,8 +13,8 @@ pub const ROLLING_TRANSFERS_INTERVAL_SECS: u32 = 1; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct TransferCount { - down: u64, - up: u64, + down: ByteCount, + up: ByteCount, } #[derive(Debug, Clone, Default)] @@ -31,18 +31,18 @@ impl TransferStatsAccounting { } } - pub fn add_down(&mut self, bytes: u64) { + pub fn add_down(&mut self, bytes: ByteCount) { self.current_transfer.down += bytes; } - pub fn add_up(&mut self, bytes: u64) { + pub fn add_up(&mut self, bytes: ByteCount) { self.current_transfer.up += bytes; } pub fn roll_transfers( &mut self, - last_ts: u64, - cur_ts: u64, + last_ts: Timestamp, + cur_ts: Timestamp, transfer_stats: &mut TransferStatsDownUp, ) { let dur_ms = cur_ts.saturating_sub(last_ts) / 1000u64; @@ -80,7 +80,7 @@ impl TransferStatsAccounting { #[derive(Debug, Clone, Default)] pub struct LatencyStatsAccounting { - rolling_latencies: VecDeque, + rolling_latencies: VecDeque, } impl LatencyStatsAccounting { @@ -90,7 +90,7 @@ impl LatencyStatsAccounting { } } - pub fn record_latency(&mut self, latency: u64) -> veilid_api::LatencyStats { + pub fn record_latency(&mut self, latency: TimestampDuration) -> veilid_api::LatencyStats { while self.rolling_latencies.len() >= ROLLING_LATENCIES_SIZE { self.rolling_latencies.pop_front(); } diff --git a/veilid-core/src/routing_table/tasks/kick_buckets.rs b/veilid-core/src/routing_table/tasks/kick_buckets.rs index 38eef8af..318f1915 100644 --- a/veilid-core/src/routing_table/tasks/kick_buckets.rs +++ b/veilid-core/src/routing_table/tasks/kick_buckets.rs @@ -7,8 +7,8 @@ impl RoutingTable { pub(crate) async fn kick_buckets_task_routine( self, _stop_token: StopToken, - _last_ts: u64, - cur_ts: u64, + _last_ts: Timestamp, + cur_ts: Timestamp, ) -> EyreResult<()> { let kick_queue: Vec = core::mem::take(&mut *self.unlocked_inner.kick_queue.lock()) .into_iter() diff --git a/veilid-core/src/routing_table/tasks/ping_validator.rs b/veilid-core/src/routing_table/tasks/ping_validator.rs index 2460d29f..46f09949 100644 --- a/veilid-core/src/routing_table/tasks/ping_validator.rs +++ b/veilid-core/src/routing_table/tasks/ping_validator.rs @@ -10,7 +10,7 @@ impl RoutingTable { #[instrument(level = "trace", skip(self), err)] fn ping_validator_public_internet( &self, - cur_ts: u64, + cur_ts: Timestamp, unord: &mut FuturesUnordered< SendPinBoxFuture>>, RPCError>>, >, diff --git a/veilid-core/src/routing_table/tasks/private_route_management.rs b/veilid-core/src/routing_table/tasks/private_route_management.rs index f790afe0..7ae002ef 100644 --- a/veilid-core/src/routing_table/tasks/private_route_management.rs +++ b/veilid-core/src/routing_table/tasks/private_route_management.rs @@ -72,8 +72,8 @@ impl RoutingTable { pub(crate) async fn private_route_management_task_routine( self, stop_token: StopToken, - _last_ts: u64, - cur_ts: u64, + _last_ts: Timestamp, + cur_ts: Timestamp, ) -> EyreResult<()> { // Get our node's current node info and network class and do the right thing let network_class = self diff --git a/veilid-core/src/routing_table/tasks/relay_management.rs b/veilid-core/src/routing_table/tasks/relay_management.rs index 86cfdd3b..4e685c02 100644 --- a/veilid-core/src/routing_table/tasks/relay_management.rs +++ b/veilid-core/src/routing_table/tasks/relay_management.rs @@ -6,8 +6,8 @@ impl RoutingTable { pub(crate) async fn relay_management_task_routine( self, _stop_token: StopToken, - _last_ts: u64, - cur_ts: u64, + _last_ts: Timestamp, + cur_ts: Timestamp, ) -> EyreResult<()> { // Get our node's current node info and network class and do the right thing let Some(own_peer_info) = self.get_own_peer_info(RoutingDomain::PublicInternet) else { diff --git a/veilid-core/src/routing_table/tasks/rolling_transfers.rs b/veilid-core/src/routing_table/tasks/rolling_transfers.rs index 04177c01..436381ec 100644 --- a/veilid-core/src/routing_table/tasks/rolling_transfers.rs +++ b/veilid-core/src/routing_table/tasks/rolling_transfers.rs @@ -6,8 +6,8 @@ impl RoutingTable { pub(crate) async fn rolling_transfers_task_routine( self, _stop_token: StopToken, - last_ts: u64, - cur_ts: u64, + last_ts: Timestamp, + cur_ts: Timestamp, ) -> EyreResult<()> { // log_rtab!("--- rolling_transfers task"); { diff --git a/veilid-core/src/rpc_processor/coders/operations/operation.rs b/veilid-core/src/rpc_processor/coders/operations/operation.rs index 46651748..f23c1df8 100644 --- a/veilid-core/src/rpc_processor/coders/operations/operation.rs +++ b/veilid-core/src/rpc_processor/coders/operations/operation.rs @@ -53,9 +53,9 @@ impl RPCOperationKind { #[derive(Debug, Clone)] pub struct RPCOperation { - op_id: u64, + op_id: OperationId, sender_node_info: Option, - target_node_info_ts: u64, + target_node_info_ts: Timestamp, kind: RPCOperationKind, } @@ -65,7 +65,7 @@ impl RPCOperation { sender_signed_node_info: SenderSignedNodeInfo, ) -> Self { Self { - op_id: get_random_u64(), + op_id: OperationId::new(get_random_u64()), sender_node_info: sender_signed_node_info.signed_node_info, target_node_info_ts: sender_signed_node_info.target_node_info_ts, kind: RPCOperationKind::Question(question), @@ -76,7 +76,7 @@ impl RPCOperation { sender_signed_node_info: SenderSignedNodeInfo, ) -> Self { Self { - op_id: get_random_u64(), + op_id: OperationId::new(get_random_u64()), sender_node_info: sender_signed_node_info.signed_node_info, target_node_info_ts: sender_signed_node_info.target_node_info_ts, kind: RPCOperationKind::Statement(statement), @@ -96,14 +96,14 @@ impl RPCOperation { } } - pub fn op_id(&self) -> u64 { + pub fn op_id(&self) -> OperationId { self.op_id } pub fn sender_node_info(&self) -> Option<&SignedNodeInfo> { self.sender_node_info.as_ref() } - pub fn target_node_info_ts(&self) -> u64 { + pub fn target_node_info_ts(&self) -> Timestamp { self.target_node_info_ts } @@ -119,7 +119,7 @@ impl RPCOperation { operation_reader: &veilid_capnp::operation::Reader, opt_sender_node_id: Option<&DHTKey>, ) -> Result { - let op_id = operation_reader.get_op_id(); + let op_id = OperationId::new(operation_reader.get_op_id()); let sender_node_info = if operation_reader.has_sender_node_info() { if let Some(sender_node_id) = opt_sender_node_id { @@ -135,7 +135,7 @@ impl RPCOperation { None }; - let target_node_info_ts = operation_reader.get_target_node_info_ts(); + let target_node_info_ts = Timestamp::new(operation_reader.get_target_node_info_ts()); let kind_reader = operation_reader.get_kind(); let kind = RPCOperationKind::decode(&kind_reader)?; @@ -149,12 +149,12 @@ impl RPCOperation { } pub fn encode(&self, builder: &mut veilid_capnp::operation::Builder) -> Result<(), RPCError> { - builder.set_op_id(self.op_id); + builder.set_op_id(self.op_id.as_u64()); if let Some(sender_info) = &self.sender_node_info { let mut si_builder = builder.reborrow().init_sender_node_info(); encode_signed_node_info(&sender_info, &mut si_builder)?; } - builder.set_target_node_info_ts(self.target_node_info_ts); + builder.set_target_node_info_ts(self.target_node_info_ts.as_u64()); let mut k_builder = builder.reborrow().init_kind(); self.kind.encode(&mut k_builder)?; Ok(()) diff --git a/veilid-core/src/rpc_processor/coders/signed_direct_node_info.rs b/veilid-core/src/rpc_processor/coders/signed_direct_node_info.rs index 7b583e21..bca21bb5 100644 --- a/veilid-core/src/rpc_processor/coders/signed_direct_node_info.rs +++ b/veilid-core/src/rpc_processor/coders/signed_direct_node_info.rs @@ -10,7 +10,7 @@ pub fn encode_signed_direct_node_info( builder .reborrow() - .set_timestamp(signed_direct_node_info.timestamp); + .set_timestamp(signed_direct_node_info.timestamp.into()); let mut sig_builder = builder.reborrow().init_signature(); let Some(signature) = &signed_direct_node_info.signature else { @@ -36,7 +36,7 @@ pub fn decode_signed_direct_node_info( .get_signature() .map_err(RPCError::protocol)?; - let timestamp = reader.reborrow().get_timestamp(); + let timestamp = reader.reborrow().get_timestamp().into(); let signature = decode_signature(&sig_reader); diff --git a/veilid-core/src/rpc_processor/coders/signed_relayed_node_info.rs b/veilid-core/src/rpc_processor/coders/signed_relayed_node_info.rs index 924a00ad..21d3266b 100644 --- a/veilid-core/src/rpc_processor/coders/signed_relayed_node_info.rs +++ b/veilid-core/src/rpc_processor/coders/signed_relayed_node_info.rs @@ -16,7 +16,7 @@ pub fn encode_signed_relayed_node_info( builder .reborrow() - .set_timestamp(signed_relayed_node_info.timestamp); + .set_timestamp(signed_relayed_node_info.timestamp.into()); let mut sig_builder = builder.reborrow().init_signature(); encode_signature(&signed_relayed_node_info.signature, &mut sig_builder); @@ -50,7 +50,7 @@ pub fn decode_signed_relayed_node_info( .reborrow() .get_signature() .map_err(RPCError::protocol)?; - let timestamp = reader.reborrow().get_timestamp(); + let timestamp = reader.reborrow().get_timestamp().into(); let signature = decode_signature(&sig_reader); diff --git a/veilid-core/src/rpc_processor/mod.rs b/veilid-core/src/rpc_processor/mod.rs index 149c3b8c..01ee69ae 100644 --- a/veilid-core/src/rpc_processor/mod.rs +++ b/veilid-core/src/rpc_processor/mod.rs @@ -37,8 +37,6 @@ use stop_token::future::FutureExt; ///////////////////////////////////////////////////////////////////// -type OperationId = u64; - #[derive(Debug, Clone)] struct RPCMessageHeaderDetailDirect { /// The decoded header of the envelope @@ -82,9 +80,9 @@ enum RPCMessageHeaderDetail { #[derive(Debug, Clone)] struct RPCMessageHeader { /// Time the message was received, not sent - timestamp: u64, + timestamp: Timestamp, /// The length in bytes of the rpc message body - body_len: u64, + body_len: ByteCount, /// The header detail depending on which way the message was received detail: RPCMessageHeaderDetail, } @@ -139,9 +137,9 @@ where #[derive(Debug)] struct WaitableReply { handle: OperationWaitHandle, - timeout: u64, + timeout_us: TimestampDuration, node_ref: NodeRef, - send_ts: u64, + send_ts: Timestamp, send_data_kind: SendDataKind, safety_route: Option, remote_private_route: Option, @@ -152,11 +150,11 @@ struct WaitableReply { #[derive(Clone, Debug, Default)] pub struct Answer { - pub latency: u64, // how long it took to get this answer - pub answer: T, // the answer itself + pub latency: TimestampDuration, // how long it took to get this answer + pub answer: T, // the answer itself } impl Answer { - pub fn new(latency: u64, answer: T) -> Self { + pub fn new(latency: TimestampDuration, answer: T) -> Self { Self { latency, answer } } } @@ -185,16 +183,16 @@ pub struct SenderSignedNodeInfo { /// The current signed node info of the sender if required signed_node_info: Option, /// The last timestamp of the target's node info to assist remote node with sending its latest node info - target_node_info_ts: u64, + target_node_info_ts: Timestamp, } impl SenderSignedNodeInfo { - pub fn new_no_sni(target_node_info_ts: u64) -> Self { + pub fn new_no_sni(target_node_info_ts: Timestamp) -> Self { Self { signed_node_info: None, target_node_info_ts, } } - pub fn new(sender_signed_node_info: SignedNodeInfo, target_node_info_ts: u64) -> Self { + pub fn new(sender_signed_node_info: SignedNodeInfo, target_node_info_ts: Timestamp) -> Self { Self { signed_node_info: Some(sender_signed_node_info), target_node_info_ts, @@ -218,7 +216,7 @@ pub struct RPCProcessorInner { } pub struct RPCProcessorUnlockedInner { - timeout: u64, + timeout_us: TimestampDuration, queue_size: u32, concurrency: u32, max_route_hop_count: usize, @@ -267,7 +265,7 @@ impl RPCProcessor { let validate_dial_info_receipt_time_ms = c.network.dht.validate_dial_info_receipt_time_ms; RPCProcessorUnlockedInner { - timeout, + timeout_us: timeout, queue_size, concurrency, max_route_hop_count, @@ -445,11 +443,11 @@ impl RPCProcessor { async fn wait_for_reply( &self, waitable_reply: WaitableReply, - ) -> Result, RPCError> { + ) -> Result, RPCError> { let out = self .unlocked_inner .waiting_rpc_table - .wait_for_op(waitable_reply.handle, waitable_reply.timeout) + .wait_for_op(waitable_reply.handle, waitable_reply.timeout_us) .await; match &out { Err(_) | Ok(TimeoutOr::Timeout) => { @@ -463,7 +461,7 @@ impl RPCProcessor { } Ok(TimeoutOr::Value((rpcreader, _))) => { // Reply received - let recv_ts = get_timestamp(); + let recv_ts = get_aligned_timestamp(); // Record answer received self.record_answer_received( @@ -759,7 +757,7 @@ impl RPCProcessor { fn record_send_failure( &self, rpc_kind: RPCKind, - send_ts: u64, + send_ts: Timestamp, node_ref: NodeRef, safety_route: Option, remote_private_route: Option, @@ -788,7 +786,7 @@ impl RPCProcessor { /// Record question lost to node or route fn record_question_lost( &self, - send_ts: u64, + send_ts: Timestamp, node_ref: NodeRef, safety_route: Option, remote_private_route: Option, @@ -827,8 +825,8 @@ impl RPCProcessor { fn record_send_success( &self, rpc_kind: RPCKind, - send_ts: u64, - bytes: u64, + send_ts: Timestamp, + bytes: ByteCount, node_ref: NodeRef, safety_route: Option, remote_private_route: Option, @@ -863,9 +861,9 @@ impl RPCProcessor { /// Record answer received from node or route fn record_answer_received( &self, - send_ts: u64, - recv_ts: u64, - bytes: u64, + send_ts: Timestamp, + recv_ts: Timestamp, + bytes: ByteCount, node_ref: NodeRef, safety_route: Option, remote_private_route: Option, @@ -1004,7 +1002,7 @@ impl RPCProcessor { let op_id = operation.op_id(); // Log rpc send - trace!(target: "rpc_message", dir = "send", kind = "question", op_id, desc = operation.kind().desc(), ?dest); + trace!(target: "rpc_message", dir = "send", kind = "question", op_id = op_id.as_u64(), desc = operation.kind().desc(), ?dest); // Produce rendered operation let RenderedOperation { @@ -1019,14 +1017,14 @@ impl RPCProcessor { // Calculate answer timeout // Timeout is number of hops times the timeout per hop - let timeout = self.unlocked_inner.timeout * (hop_count as u64); + let timeout_us = self.unlocked_inner.timeout_us * (hop_count as u64); // Set up op id eventual let handle = self.unlocked_inner.waiting_rpc_table.add_op_waiter(op_id); // Send question - let bytes = message.len() as u64; - let send_ts = get_timestamp(); + let bytes: ByteCount = (message.len() as u64).into(); + let send_ts = get_aligned_timestamp(); let send_data_kind = network_result_try!(self .network_manager() .send_envelope(node_ref.clone(), Some(node_id), message) @@ -1054,7 +1052,7 @@ impl RPCProcessor { // Pass back waitable reply completion Ok(NetworkResult::value(WaitableReply { handle, - timeout, + timeout_us, node_ref, send_ts, send_data_kind, @@ -1078,7 +1076,7 @@ impl RPCProcessor { let operation = RPCOperation::new_statement(statement, ssni); // Log rpc send - trace!(target: "rpc_message", dir = "send", kind = "statement", op_id = operation.op_id(), desc = operation.kind().desc(), ?dest); + trace!(target: "rpc_message", dir = "send", kind = "statement", op_id = operation.op_id().as_u64(), desc = operation.kind().desc(), ?dest); // Produce rendered operation let RenderedOperation { @@ -1092,8 +1090,8 @@ impl RPCProcessor { } = network_result_try!(self.render_operation(dest, &operation)?); // Send statement - let bytes = message.len() as u64; - let send_ts = get_timestamp(); + let bytes: ByteCount = (message.len() as u64).into(); + let send_ts = get_aligned_timestamp(); let _send_data_kind = network_result_try!(self .network_manager() .send_envelope(node_ref.clone(), Some(node_id), message) @@ -1139,7 +1137,7 @@ impl RPCProcessor { let operation = RPCOperation::new_answer(&request.operation, answer, ssni); // Log rpc send - trace!(target: "rpc_message", dir = "send", kind = "answer", op_id = operation.op_id(), desc = operation.kind().desc(), ?dest); + trace!(target: "rpc_message", dir = "send", kind = "answer", op_id = operation.op_id().as_u64(), desc = operation.kind().desc(), ?dest); // Produce rendered operation let RenderedOperation { @@ -1153,8 +1151,8 @@ impl RPCProcessor { } = network_result_try!(self.render_operation(dest, &operation)?); // Send the reply - let bytes = message.len() as u64; - let send_ts = get_timestamp(); + let bytes: ByteCount = (message.len() as u64).into(); + let send_ts = get_aligned_timestamp(); network_result_try!(self.network_manager() .send_envelope(node_ref.clone(), Some(node_id), message) .await @@ -1284,7 +1282,7 @@ impl RPCProcessor { }; // Log rpc receive - trace!(target: "rpc_message", dir = "recv", kind, op_id = msg.operation.op_id(), desc = msg.operation.kind().desc(), header = ?msg.header); + trace!(target: "rpc_message", dir = "recv", kind, op_id = msg.operation.op_id().as_u64(), desc = msg.operation.kind().desc(), header = ?msg.header); // Process specific message kind match msg.operation.kind() { @@ -1366,7 +1364,7 @@ impl RPCProcessor { connection_descriptor, routing_domain, }), - timestamp: get_timestamp(), + timestamp: get_aligned_timestamp(), body_len: body.len() as u64, }, data: RPCMessageData { contents: body }, @@ -1395,8 +1393,8 @@ impl RPCProcessor { remote_safety_route, sequencing, }), - timestamp: get_timestamp(), - body_len: body.len() as u64, + timestamp: get_aligned_timestamp(), + body_len: (body.len() as u64).into(), }, data: RPCMessageData { contents: body }, }; @@ -1428,8 +1426,8 @@ impl RPCProcessor { safety_spec, }, ), - timestamp: get_timestamp(), - body_len: body.len() as u64, + timestamp: get_aligned_timestamp(), + body_len: (body.len() as u64).into(), }, data: RPCMessageData { contents: body }, }; diff --git a/veilid-core/src/rpc_processor/operation_waiter.rs b/veilid-core/src/rpc_processor/operation_waiter.rs index 3f56e339..fb276942 100644 --- a/veilid-core/src/rpc_processor/operation_waiter.rs +++ b/veilid-core/src/rpc_processor/operation_waiter.rs @@ -104,9 +104,9 @@ where pub async fn wait_for_op( &self, mut handle: OperationWaitHandle, - timeout_us: u64, - ) -> Result, RPCError> { - let timeout_ms = u32::try_from(timeout_us / 1000u64) + timeout_us: TimestampDuration, + ) -> Result, RPCError> { + let timeout_ms = u32::try_from(timeout_us.as_u64() / 1000u64) .map_err(|e| RPCError::map_internal("invalid timeout")(e))?; // Take the instance @@ -114,7 +114,7 @@ where let eventual_instance = handle.eventual_instance.take().unwrap(); // wait for eventualvalue - let start_ts = get_timestamp(); + let start_ts = get_aligned_timestamp(); let res = timeout(timeout_ms, eventual_instance) .await .into_timeout_or(); @@ -125,7 +125,7 @@ where }) .map(|res| { let (_span_id, ret) = res.take_value().unwrap(); - let end_ts = get_timestamp(); + let end_ts = get_aligned_timestamp(); //xxx: causes crash (Missing otel data span extensions) // Span::current().follows_from(span_id); diff --git a/veilid-core/src/rpc_processor/rpc_app_call.rs b/veilid-core/src/rpc_processor/rpc_app_call.rs index e068e133..b0506be9 100644 --- a/veilid-core/src/rpc_processor/rpc_app_call.rs +++ b/veilid-core/src/rpc_processor/rpc_app_call.rs @@ -73,7 +73,7 @@ impl RPCProcessor { let res = self .unlocked_inner .waiting_app_call_table - .wait_for_op(handle, self.unlocked_inner.timeout) + .wait_for_op(handle, self.unlocked_inner.timeout_us) .await?; let (message, _latency) = match res { TimeoutOr::Timeout => { @@ -93,7 +93,7 @@ impl RPCProcessor { } /// Exposed to API for apps to return app call answers - pub async fn app_call_reply(&self, id: u64, message: Vec) -> Result<(), RPCError> { + pub async fn app_call_reply(&self, id: OperationId, message: Vec) -> Result<(), RPCError> { self.unlocked_inner .waiting_app_call_table .complete_op_waiter(id, message) diff --git a/veilid-core/src/rpc_processor/rpc_route.rs b/veilid-core/src/rpc_processor/rpc_route.rs index 1602858a..78a12696 100644 --- a/veilid-core/src/rpc_processor/rpc_route.rs +++ b/veilid-core/src/rpc_processor/rpc_route.rs @@ -196,7 +196,6 @@ impl RPCProcessor { &routed_operation.data, sender_id, ) - .map_err(RPCError::protocol)? else { return Ok(NetworkResult::invalid_message("signatures did not validate for private route")); }; diff --git a/veilid-core/src/veilid_api/aligned_u64.rs b/veilid-core/src/veilid_api/aligned_u64.rs new file mode 100644 index 00000000..f805490d --- /dev/null +++ b/veilid-core/src/veilid_api/aligned_u64.rs @@ -0,0 +1,110 @@ +use super::*; + +/// Aligned u64 +/// Required on 32-bit platforms for serialization because Rust aligns u64 on 4 byte boundaries +/// And zero-copy serialization with Rkyv requires 8-byte alignment + +#[derive( + Clone, + Default, + PartialEq, + Eq, + PartialOrd, + Ord, + Copy, + Hash, + Serialize, + Deserialize, + RkyvArchive, + RkyvSerialize, + RkyvDeserialize, +)] +#[repr(C, align(8))] +#[archive_attr(repr(C, align(8)), derive(CheckBytes))] +pub struct AlignedU64(u64); + +impl From for AlignedU64 { + fn from(v: u64) -> Self { + AlignedU64(v) + } +} +impl From for u64 { + fn from(v: AlignedU64) -> Self { + v.0 + } +} + +impl fmt::Display for AlignedU64 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (&self.0 as &dyn fmt::Display).fmt(f) + } +} + +impl fmt::Debug for AlignedU64 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (&self.0 as &dyn fmt::Debug).fmt(f) + } +} + +impl FromStr for AlignedU64 { + type Err = ::Err; + fn from_str(s: &str) -> Result { + Ok(AlignedU64(u64::from_str(s)?)) + } +} + +impl> core::ops::Add for AlignedU64 { + type Output = Self; + + fn add(self, rhs: Rhs) -> Self { + Self(self.0 + rhs.into()) + } +} + +impl> core::ops::AddAssign for AlignedU64 { + fn add_assign(&mut self, rhs: Rhs) { + self.0 += rhs.into(); + } +} + +impl> core::ops::Sub for AlignedU64 { + type Output = Self; + + fn sub(self, rhs: Rhs) -> Self { + Self(self.0 - rhs.into()) + } +} + +impl> core::ops::SubAssign for AlignedU64 { + fn sub_assign(&mut self, rhs: Rhs) { + self.0 -= rhs.into(); + } +} + +impl> core::ops::Mul for AlignedU64 { + type Output = Self; + + fn mul(self, rhs: Rhs) -> Self { + Self(self.0 * rhs.into()) + } +} + +impl> core::ops::Div for AlignedU64 { + type Output = Self; + + fn div(self, rhs: Rhs) -> Self { + Self(self.0 / rhs.into()) + } +} + +impl AlignedU64 { + pub const fn new(v: u64) -> Self { + Self(v) + } + pub fn as_u64(self) -> u64 { + self.0 + } + pub fn saturating_sub(self, rhs: Self) -> Self { + Self(self.0.saturating_sub(rhs.0)) + } +} diff --git a/veilid-core/src/veilid_api/api.rs b/veilid-core/src/veilid_api/api.rs index 4ef5e5e6..88d00f81 100644 --- a/veilid-core/src/veilid_api/api.rs +++ b/veilid-core/src/veilid_api/api.rs @@ -247,7 +247,11 @@ impl VeilidAPI { // App Calls #[instrument(level = "debug", skip(self))] - pub async fn app_call_reply(&self, id: u64, message: Vec) -> Result<(), VeilidAPIError> { + pub async fn app_call_reply( + &self, + id: OperationId, + message: Vec, + ) -> Result<(), VeilidAPIError> { let rpc_processor = self.rpc_processor()?; rpc_processor .app_call_reply(id, message) diff --git a/veilid-core/src/veilid_api/mod.rs b/veilid-core/src/veilid_api/mod.rs index 9dd2cebe..d2d81c45 100644 --- a/veilid-core/src/veilid_api/mod.rs +++ b/veilid-core/src/veilid_api/mod.rs @@ -1,5 +1,6 @@ #![allow(dead_code)] +mod aligned_u64; mod api; mod debug; mod error; @@ -7,6 +8,7 @@ mod routing_context; mod serialize_helpers; mod types; +pub use aligned_u64::*; pub use api::*; pub use debug::*; pub use error::*; diff --git a/veilid-core/src/veilid_api/types.rs b/veilid-core/src/veilid_api/types.rs index baa3c9cb..cc074b97 100644 --- a/veilid-core/src/veilid_api/types.rs +++ b/veilid-core/src/veilid_api/types.rs @@ -2,6 +2,20 @@ use super::*; ///////////////////////////////////////////////////////////////////////////////////////////////////// +/// Microseconds since epoch +pub type Timestamp = AlignedU64; +pub fn get_aligned_timestamp() -> Timestamp { + get_timestamp().into() +} +/// Microseconds duration +pub type TimestampDuration = AlignedU64; +/// Request/Response matching id +pub type OperationId = AlignedU64; +/// Number of bytes +pub type ByteCount = AlignedU64; +/// Tunnel identifier +pub type TunnelId = AlignedU64; + #[derive( Debug, Clone, @@ -113,7 +127,7 @@ pub struct VeilidAppCall { pub message: Vec, /// The id to reply to #[serde(with = "json_as_string")] - pub id: u64, + pub id: OperationId, } #[derive( @@ -141,9 +155,9 @@ pub struct PeerTableData { pub struct VeilidStateNetwork { pub started: bool, #[serde(with = "json_as_string")] - pub bps_down: u64, + pub bps_down: ByteCount, #[serde(with = "json_as_string")] - pub bps_up: u64, + pub bps_up: ByteCount, pub peers: Vec, } @@ -1801,7 +1815,7 @@ impl MatchesDialInfoFilter for DialInfo { #[archive_attr(repr(C), derive(CheckBytes))] pub struct SignedDirectNodeInfo { pub node_info: NodeInfo, - pub timestamp: u64, + pub timestamp: Timestamp, pub signature: Option, } @@ -1809,7 +1823,7 @@ impl SignedDirectNodeInfo { pub fn new( node_id: NodeId, node_info: NodeInfo, - timestamp: u64, + timestamp: Timestamp, signature: DHTSignature, ) -> Result { let node_info_bytes = Self::make_signature_bytes(&node_info, timestamp)?; @@ -1826,7 +1840,7 @@ impl SignedDirectNodeInfo { node_info: NodeInfo, secret: &DHTKeySecret, ) -> Result { - let timestamp = get_timestamp(); + let timestamp = get_aligned_timestamp(); let node_info_bytes = Self::make_signature_bytes(&node_info, timestamp)?; let signature = sign(&node_id.key, secret, &node_info_bytes)?; Ok(Self { @@ -1838,7 +1852,7 @@ impl SignedDirectNodeInfo { fn make_signature_bytes( node_info: &NodeInfo, - timestamp: u64, + timestamp: Timestamp, ) -> Result, VeilidAPIError> { let mut node_info_bytes = Vec::new(); @@ -1849,7 +1863,7 @@ impl SignedDirectNodeInfo { node_info_bytes.append(&mut builder_to_vec(ni_msg).map_err(VeilidAPIError::internal)?); // Add timestamp to signature - node_info_bytes.append(&mut timestamp.to_le_bytes().to_vec()); + node_info_bytes.append(&mut timestamp.as_u64().to_le_bytes().to_vec()); Ok(node_info_bytes) } @@ -1858,7 +1872,7 @@ impl SignedDirectNodeInfo { Self { node_info, signature: None, - timestamp: get_timestamp(), + timestamp: get_aligned_timestamp(), } } @@ -1874,7 +1888,7 @@ pub struct SignedRelayedNodeInfo { pub node_info: NodeInfo, pub relay_id: NodeId, pub relay_info: SignedDirectNodeInfo, - pub timestamp: u64, + pub timestamp: Timestamp, pub signature: DHTSignature, } @@ -1884,7 +1898,7 @@ impl SignedRelayedNodeInfo { node_info: NodeInfo, relay_id: NodeId, relay_info: SignedDirectNodeInfo, - timestamp: u64, + timestamp: Timestamp, signature: DHTSignature, ) -> Result { let node_info_bytes = @@ -1906,7 +1920,7 @@ impl SignedRelayedNodeInfo { relay_info: SignedDirectNodeInfo, secret: &DHTKeySecret, ) -> Result { - let timestamp = get_timestamp(); + let timestamp = get_aligned_timestamp(); let node_info_bytes = Self::make_signature_bytes(&node_info, &relay_id, &relay_info, timestamp)?; let signature = sign(&node_id.key, secret, &node_info_bytes)?; @@ -1923,7 +1937,7 @@ impl SignedRelayedNodeInfo { node_info: &NodeInfo, relay_id: &NodeId, relay_info: &SignedDirectNodeInfo, - timestamp: u64, + timestamp: Timestamp, ) -> Result, VeilidAPIError> { let mut sig_bytes = Vec::new(); @@ -1968,7 +1982,7 @@ impl SignedNodeInfo { } } - pub fn timestamp(&self) -> u64 { + pub fn timestamp(&self) -> Timestamp { match self { SignedNodeInfo::Direct(d) => d.timestamp, SignedNodeInfo::Relayed(r) => r.timestamp, @@ -2201,11 +2215,11 @@ impl MatchesDialInfoFilter for ConnectionDescriptor { #[archive_attr(repr(C), derive(CheckBytes))] pub struct LatencyStats { #[serde(with = "json_as_string")] - pub fastest: u64, // fastest latency in the ROLLING_LATENCIES_SIZE last latencies + pub fastest: TimestampDuration, // 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 + pub average: TimestampDuration, // 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 + pub slowest: TimestampDuration, // slowest latency in the ROLLING_LATENCIES_SIZE last latencies } #[derive( @@ -2223,13 +2237,13 @@ pub struct LatencyStats { #[archive_attr(repr(C), derive(CheckBytes))] pub struct TransferStats { #[serde(with = "json_as_string")] - pub total: u64, // total amount transferred ever + pub total: ByteCount, // total amount transferred ever #[serde(with = "json_as_string")] - pub maximum: u64, // maximum rate over the ROLLING_TRANSFERS_SIZE last amounts + pub maximum: ByteCount, // 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 + pub average: ByteCount, // 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 + pub minimum: ByteCount, // minimum rate over the ROLLING_TRANSFERS_SIZE last amounts } #[derive( @@ -2268,11 +2282,11 @@ pub struct RPCStats { 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 + pub last_question_ts: 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 + 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 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 } @@ -2292,7 +2306,7 @@ pub struct RPCStats { #[archive_attr(repr(C), derive(CheckBytes))] pub struct PeerStats { #[serde(with = "json_as_string")] - pub time_added: u64, // when the peer was added to the routing table + pub time_added: Timestamp, // 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 @@ -2362,8 +2376,6 @@ pub enum TunnelError { NoCapacity, // Endpoint is full } -pub type TunnelId = u64; - #[derive(Clone, Debug, Serialize, Deserialize, RkyvArchive, RkyvSerialize, RkyvDeserialize)] #[archive_attr(repr(C), derive(CheckBytes))] pub struct TunnelEndpoint { @@ -2386,7 +2398,7 @@ impl Default for TunnelEndpoint { #[archive_attr(repr(C), derive(CheckBytes))] pub struct FullTunnel { pub id: TunnelId, - pub timeout: u64, + pub timeout: TimestampDuration, pub local: TunnelEndpoint, pub remote: TunnelEndpoint, } @@ -2397,6 +2409,6 @@ pub struct FullTunnel { #[archive_attr(repr(C), derive(CheckBytes))] pub struct PartialTunnel { pub id: TunnelId, - pub timeout: u64, + pub timeout: TimestampDuration, pub local: TunnelEndpoint, } diff --git a/veilid-flutter/example/web/index.html b/veilid-flutter/example/web/index.html index 128d80a0..373dae36 100644 --- a/veilid-flutter/example/web/index.html +++ b/veilid-flutter/example/web/index.html @@ -47,6 +47,7 @@