#[cfg(not(target_arch = "wasm32"))]
use config::File;
#[cfg(not(target_arch = "wasm32"))]
use config::Source;
use config::Value;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Mutex;
use std::sync::MutexGuard;
use tracing::level_filters::LevelFilter;
pub fn config() -> Config {
get_config().clone()
}
fn get_config() -> MutexGuard<'static, Config> {
CONFIG.lock().unwrap_or_else(|poison| {
warn!("config poisoned, restoring initial config");
let mut c = poison.into_inner();
*c = init_config();
CONFIG.clear_poison();
c
})
}
pub fn set<V: Into<config::Value>>(name: impl Into<String>, value: V) {
get_config().set_value(name, value);
}
pub fn get_value(name: &str) -> Option<Value> {
get_config().misc.get(name).cloned()
}
pub fn get<T: FromStr>(name: &str) -> Option<T> {
get_config()
.misc
.get(name)
.and_then(|v| v.clone().into_string().ok())
.and_then(|v| v.parse::<T>().ok())
}
#[cfg(not(target_arch = "wasm32"))]
fn init_config() -> Config {
let mut settings = ::config::Config::builder();
if let Some(mut path) = dirs::config_dir() {
path.push("futuresdr");
path.push("config.toml");
settings = settings.add_source(File::from(path.clone()).required(false));
}
settings =
settings.add_source(File::new("config.toml", config::FileFormat::Toml).required(false));
settings = settings.add_source(config::Environment::with_prefix("futuresdr"));
let mut c = Config::default();
match settings.build() {
Ok(settings) => match settings.collect() {
Ok(config) => {
for (k, v) in config.iter() {
c.set_value(k.clone(), v.clone());
}
}
Err(e) => warn!("error parsing config {e:?}"),
},
Err(e) => warn!("error reading config {e:?}"),
}
c
}
#[cfg(target_arch = "wasm32")]
fn init_config() -> Config {
Config::default()
}
static CONFIG: Lazy<Mutex<Config>> = Lazy::new(|| Mutex::new(init_config()));
#[derive(Debug, Clone)]
pub struct Config {
pub queue_size: usize,
pub buffer_size: usize,
pub stack_size: usize,
pub log_level: LevelFilter,
pub ctrlport_enable: bool,
pub ctrlport_bind: SocketAddr,
pub frontend_path: Option<PathBuf>,
misc: HashMap<String, Value>,
}
impl Config {
fn set_value<V: Into<config::Value>>(&mut self, name: impl Into<String>, value: V) {
let name = name.into();
let value = value.into();
match name.as_str() {
"queue_size" => {
if let Some(value) = config_parse::<usize>(&name, &value) {
self.queue_size = value.max(1);
}
}
"buffer_size" => {
if let Some(value) = config_parse::<usize>(&name, &value) {
self.buffer_size = value;
}
}
"stack_size" => {
if let Some(value) = config_parse::<usize>(&name, &value) {
self.stack_size = value;
}
}
"log_level" => {
if let Some(value) = config_parse::<LevelFilter>(&name, &value) {
self.log_level = value;
}
}
"ctrlport_enable" => {
if let Some(value) = config_parse::<bool>(&name, &value) {
self.ctrlport_enable = value;
}
}
"ctrlport_bind" => {
if let Some(value) = config_parse::<SocketAddr>(&name, &value) {
self.ctrlport_bind = value;
}
}
"frontend_path" => {
if let Some(value) = config_parse::<PathBuf>(&name, &value) {
self.frontend_path = Some(value);
}
}
"slab_reserved" => {
warn!("config key {name} is no longer supported and will be ignored");
}
_ => {
self.misc.insert(name, value);
}
}
}
}
impl Default for Config {
#[cfg(debug_assertions)]
fn default() -> Self {
Config {
queue_size: 128,
buffer_size: 32768,
stack_size: 16 * 1024 * 1024,
log_level: LevelFilter::DEBUG,
ctrlport_enable: true,
ctrlport_bind: SocketAddr::from(([127, 0, 0, 1], 1337)),
frontend_path: None,
misc: HashMap::new(),
}
}
#[cfg(not(debug_assertions))]
fn default() -> Self {
Config {
queue_size: 128,
buffer_size: 32768,
stack_size: 16 * 1024 * 1024,
log_level: LevelFilter::INFO,
ctrlport_enable: true,
ctrlport_bind: SocketAddr::from(([127, 0, 0, 1], 1337)),
frontend_path: None,
misc: HashMap::new(),
}
}
}
fn config_parse<T: FromStr>(name: &str, v: &Value) -> Option<T> {
if let Ok(v) = v.clone().into_string()
&& let Ok(v) = v.parse::<T>()
{
return Some(v);
}
warn!("invalid config value for {name}: {v:?}");
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invalid_bool_config_value_is_ignored() {
let mut config = Config {
ctrlport_enable: true,
..Config::default()
};
config.set_value("ctrlport_enable", "maybe");
assert!(config.ctrlport_enable);
}
#[test]
fn invalid_numeric_config_value_is_ignored() {
let mut config = Config {
queue_size: 256,
..Config::default()
};
config.set_value("queue_size", "not-a-number");
assert_eq!(config.queue_size, 256);
}
#[test]
fn zero_queue_size_is_clamped_to_one() {
let mut config = Config::default();
config.set_value("queue_size", 0_u64);
assert_eq!(config.queue_size, 1);
}
#[test]
fn zero_buffer_size_is_valid() {
let mut config = Config::default();
config.set_value("buffer_size", 0_u64);
assert_eq!(config.buffer_size, 0);
}
#[test]
fn ctrlport_bind_string_parses_to_socket_addr() {
let mut config = Config::default();
config.set_value("ctrlport_bind", "0.0.0.0:4242");
assert_eq!(config.ctrlport_bind, SocketAddr::from(([0, 0, 0, 0], 4242)));
}
#[test]
fn invalid_ctrlport_bind_value_is_ignored() {
let mut config = Config {
ctrlport_bind: SocketAddr::from(([0, 0, 0, 0], 4242)),
..Config::default()
};
config.set_value("ctrlport_bind", "not-a-socket-addr");
assert_eq!(config.ctrlport_bind, SocketAddr::from(([0, 0, 0, 0], 4242)));
}
#[test]
fn ipv6_ctrlport_bind_string_parses_to_socket_addr() {
let mut config = Config::default();
config.set_value("ctrlport_bind", "[::1]:1337");
assert_eq!(
config.ctrlport_bind,
SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 1], 1337))
);
}
#[test]
fn slab_reserved_is_ignored() {
let mut config = Config::default();
config.set_value("slab_reserved", 16_u64);
assert!(!config.misc.contains_key("slab_reserved"));
}
}