use crate::client::{Error, Result};
use qp2p::Config as QuicP2pConfig;
use serde::{Deserialize, Serialize};
use std::{
net::{Ipv4Addr, SocketAddr},
path::{Path, PathBuf},
time::Duration,
};
use tokio::{
fs::File,
io::{self, AsyncReadExt},
};
use tracing::{debug, warn};
const DEFAULT_LOCAL_ADDR: (Ipv4Addr, u16) = (Ipv4Addr::UNSPECIFIED, 0);
pub const DEFAULT_OPERATION_TIMEOUT: Duration = Duration::from_secs(120);
pub const DEFAULT_ACK_WAIT: Duration = Duration::from_secs(10);
const DEFAULT_ROOT_DIR_NAME: &str = "root_dir";
const SN_QUERY_TIMEOUT: &str = "SN_QUERY_TIMEOUT";
const SN_CMD_TIMEOUT: &str = "SN_CMD_TIMEOUT";
const SN_AE_WAIT: &str = "SN_AE_WAIT";
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ClientConfig {
pub local_addr: SocketAddr,
pub root_dir: PathBuf,
pub genesis_key: bls::PublicKey,
pub qp2p: QuicP2pConfig,
pub query_timeout: Duration,
pub cmd_timeout: Duration,
pub cmd_ack_wait: Duration,
}
impl ClientConfig {
pub async fn new(
root_dir: Option<&Path>,
local_addr: Option<SocketAddr>,
genesis_key: bls::PublicKey,
config_file_path: Option<&Path>,
query_timeout: Option<Duration>,
cmd_timeout: Option<Duration>,
cmd_ack_wait: Option<Duration>,
) -> Self {
let root_dir = root_dir
.map(|p| p.to_path_buf())
.unwrap_or_else(default_dir);
let qp2p = match &config_file_path {
None => QuicP2pConfig::default(),
Some(path) => read_config_file(path).await.unwrap_or_default(),
};
let query_timeout = query_timeout.unwrap_or(DEFAULT_OPERATION_TIMEOUT);
let cmd_timeout = cmd_timeout.unwrap_or(DEFAULT_OPERATION_TIMEOUT);
let cmd_ack_wait = cmd_ack_wait.unwrap_or(DEFAULT_ACK_WAIT);
let query_timeout = match std::env::var(SN_QUERY_TIMEOUT) {
Ok(timeout) => match timeout.parse() {
Ok(time) => {
warn!(
"Query timeout set from env var {}: {}s",
SN_QUERY_TIMEOUT, time
);
Duration::from_secs(time)
}
Err(error) => {
warn!("There was an error parsing {} env var value: '{}'. Default or client configured query timeout will be used: {:?}", SN_QUERY_TIMEOUT, timeout, error);
query_timeout
}
},
Err(_) => query_timeout,
};
let cmd_timeout = match std::env::var(SN_CMD_TIMEOUT) {
Ok(timeout) => match timeout.parse() {
Ok(time) => {
warn!(
"Query timeout set from env var {}: {}s",
SN_CMD_TIMEOUT, time
);
Duration::from_secs(time)
}
Err(error) => {
warn!("There was an error parsing {} env var value: '{}'. Default or client configured cmd timeout will be used: {:?}", SN_CMD_TIMEOUT, timeout, error);
cmd_timeout
}
},
Err(_) => cmd_timeout,
};
let cmd_ack_wait = match std::env::var(SN_AE_WAIT) {
Ok(timeout) => match timeout.parse() {
Ok(time) => {
warn!(
"Client AE wait post-put set from env var {}: {}s",
SN_AE_WAIT, time
);
Duration::from_secs(time)
}
Err(error) => {
warn!("There was an error parsing {} env var value: '{}'. Default or client configured query timeout will be used: {:?}", SN_AE_WAIT, timeout, error);
cmd_ack_wait
}
},
Err(_) => cmd_ack_wait,
};
info!(
"Client set to use a query timeout of {:?}, and AE await post-put for {:?}",
query_timeout, cmd_ack_wait
);
Self {
local_addr: local_addr.unwrap_or_else(|| SocketAddr::from(DEFAULT_LOCAL_ADDR)),
root_dir: root_dir.clone(),
genesis_key,
qp2p,
query_timeout,
cmd_timeout,
cmd_ack_wait,
}
}
}
async fn read_config_file(filepath: &Path) -> Result<QuicP2pConfig, Error> {
debug!("Reading config file '{}' ...", filepath.display());
let mut file = File::open(filepath).await?;
let mut contents = vec![];
let _size = file.read_to_end(&mut contents).await?;
serde_json::from_slice(&contents).map_err(|err| {
warn!(
"Could not parse content of config file '{}': {}",
filepath.display(),
err
);
err.into()
})
}
fn default_dir() -> PathBuf {
project_dirs()
.unwrap_or_default()
.join(DEFAULT_ROOT_DIR_NAME)
}
fn project_dirs() -> Result<PathBuf> {
let mut home_dir = dirs_next::home_dir()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Home directory not found"))?;
home_dir.push(".safe");
home_dir.push("client");
Ok(home_dir)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::utils::test_utils::init_test_logger;
use bincode::serialize;
use eyre::Result;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use std::fs::File;
use tempfile::tempdir;
use tokio::fs::create_dir_all;
#[tokio::test(flavor = "multi_thread")]
async fn custom_config_path() -> Result<()> {
init_test_logger();
let temp_dir = tempdir()?;
let root_dir = temp_dir.path().to_path_buf();
let cfg_filename: String = thread_rng().sample_iter(&Alphanumeric).take(15).collect();
let config_filepath = root_dir.join(&cfg_filename);
let genesis_key = bls::SecretKey::random().public_key();
let config = ClientConfig::new(
Some(&root_dir),
None,
genesis_key,
Some(&config_filepath),
None,
None,
None,
)
.await;
let mut str_path = root_dir
.to_str()
.ok_or(eyre::eyre!("No path for to_str".to_string()))?
.to_string();
if str_path.ends_with('/') {
let _some_last_char = str_path.pop();
}
let expected_query_timeout = std::env::var(SN_QUERY_TIMEOUT)
.map(|v| {
v.parse()
.map(Duration::from_secs)
.unwrap_or(DEFAULT_OPERATION_TIMEOUT)
})
.unwrap_or(DEFAULT_OPERATION_TIMEOUT);
let expected_cmd_timeout = std::env::var(SN_CMD_TIMEOUT)
.map(|v| {
v.parse()
.map(Duration::from_secs)
.unwrap_or(DEFAULT_OPERATION_TIMEOUT)
})
.unwrap_or(DEFAULT_OPERATION_TIMEOUT);
let expected_cmd_ack_wait = std::env::var(SN_AE_WAIT)
.map(|v| {
v.parse()
.map(Duration::from_secs)
.unwrap_or(DEFAULT_ACK_WAIT)
})
.unwrap_or(DEFAULT_ACK_WAIT);
let expected_config = ClientConfig {
local_addr: (Ipv4Addr::UNSPECIFIED, 0).into(),
root_dir: root_dir.clone(),
genesis_key,
qp2p: QuicP2pConfig {
..Default::default()
},
query_timeout: expected_query_timeout,
cmd_timeout: expected_cmd_timeout,
cmd_ack_wait: expected_cmd_ack_wait,
};
assert_eq!(format!("{:?}", config), format!("{:?}", expected_config));
assert_eq!(serialize(&config)?, serialize(&expected_config)?);
create_dir_all(&root_dir).await?;
let mut file = File::create(&config_filepath)?;
let config_on_disk = ClientConfig::new(
None,
None,
genesis_key,
Some(&config_filepath),
None,
None,
None,
)
.await;
serde_json::to_writer_pretty(&mut file, &config_on_disk)?;
file.sync_all()?;
let read_cfg = ClientConfig::new(None, None, genesis_key, None, None, None, None).await;
assert_eq!(serialize(&config_on_disk)?, serialize(&read_cfg)?);
Ok(())
}
}