use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use rand::{RngCore, thread_rng};
use std::{
path::Path,
sync::{Arc, Mutex},
};
use async_trait::async_trait;
use tempfile::TempDir;
use tokio::sync::watch;
use tracing::instrument;
use veilid_core::{VeilidConfig, VeilidStateAttachment, VeilidUpdate};
use crate::connection::{
HandlerChain, Result, StateAttachmentWatcher, UpdateDispatch, UpdateHandler,
};
pub struct Connection {
routing_context: veilid_core::RoutingContext,
state_dir: Option<TempDir>,
update_chain: Arc<Mutex<HandlerChain>>,
attachment_state_rx: watch::Receiver<VeilidStateAttachment>,
}
impl Connection {
pub async fn new() -> Result<Connection> {
let state_dir = TempDir::new()?;
let mut conn = Self::new_config(Self::config(state_dir.path(), None)).await?;
conn.state_dir = Some(state_dir);
Ok(conn)
}
#[instrument(skip_all)]
pub async fn new_config(config: VeilidConfig) -> Result<Connection> {
let update_chain = Arc::new(Mutex::new(HandlerChain::new()));
let update_source = Arc::new(UpdateDispatch::new(update_chain.clone()));
let update_cb = Arc::new(move |update: VeilidUpdate| {
update_source.update(update);
});
let api: veilid_core::VeilidAPI = veilid_core::api_startup(update_cb, config).await?;
api.attach().await?;
let (attachment_monitor, attachment_state_rx) = StateAttachmentWatcher::new();
update_chain
.lock()
.unwrap()
.add(Box::new(attachment_monitor));
Ok(Connection {
routing_context: api.routing_context()?,
state_dir: None,
update_chain,
attachment_state_rx,
})
}
pub fn config(state_dir: &Path, program_name: Option<String>) -> VeilidConfig {
VeilidConfig {
program_name: program_name.unwrap_or("veilnet-app".to_string()),
namespace: {
let mut bytes = [0u8; 32];
thread_rng().fill_bytes(&mut bytes);
URL_SAFE_NO_PAD.encode(bytes)
},
network: veilid_core::VeilidConfigNetwork {
..Default::default()
},
table_store: veilid_core::VeilidConfigTableStore {
directory: state_dir.join("table").to_string_lossy().to_string(),
..Default::default()
},
block_store: veilid_core::VeilidConfigBlockStore {
directory: state_dir.join("block").to_string_lossy().to_string(),
..Default::default()
},
protected_store: veilid_core::VeilidConfigProtectedStore {
allow_insecure_fallback: true,
always_use_insecure_storage: false,
directory: state_dir.join("protected").to_string_lossy().to_string(),
..Default::default()
},
..Default::default()
}
}
}
#[async_trait]
impl crate::connection::Connection for Connection {
fn add_update_handler(&self, handler: Box<dyn UpdateHandler + Send + Sync>) {
self.update_chain.lock().unwrap().add(handler);
}
#[instrument(skip_all)]
async fn require_attachment(&mut self) -> Result<()> {
let res = self
.attachment_state_rx
.wait_for(|attachment| attachment.public_internet_ready)
.await;
match res {
Ok(_) => Ok(()),
Err(e) => Err(e.into()),
}
}
fn routing_context(&self) -> impl crate::connection::RoutingContext {
crate::connection::veilid::routing_context::RoutingContext(self.routing_context.clone())
}
#[instrument(skip_all)]
async fn reset(&mut self) -> Result<()> {
self.routing_context.api().detach().await?;
self.routing_context.api().attach().await?;
self.require_attachment().await?;
Ok(())
}
async fn close(self) -> Result<()> {
self.routing_context.api().shutdown().await;
Ok(())
}
}
impl Clone for Connection {
fn clone(&self) -> Self {
Self {
routing_context: self.routing_context.clone(),
state_dir: None,
update_chain: self.update_chain.clone(),
attachment_state_rx: self.attachment_state_rx.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_namespace_generation() {
let config = Connection::config(Path::new("/tmp"), None);
assert!(
!config.namespace.contains('+'),
"Namespace should not contain '+'"
);
assert!(
!config.namespace.contains('/'),
"Namespace should not contain '/'"
);
assert!(
!config.namespace.contains('='),
"Namespace should not contain padding '='"
);
assert_eq!(
config.namespace.len(),
43,
"Namespace should be 43 characters long"
);
let config2 = Connection::config(Path::new("/tmp"), None);
assert_ne!(
config.namespace, config2.namespace,
"Each call should generate a different namespace"
);
}
}