use serde_json::Value;
use std::path::Path;
use std::sync::Mutex;
pub fn open_file<P: AsRef<Path>>(path: P) -> std::io::Result<String> {
std::fs::read_to_string(path)
}
pub fn load_json_config<P: AsRef<Path>>(config_file_path: P) -> Result<Value, String> {
let contents = open_file(config_file_path).map_err(|e| format!("open_file: {}", e))?;
serde_json::from_str(&contents).map_err(|e| format!("json parse: {}", e))
}
pub struct DummyWatcher;
impl DummyWatcher {
pub fn check<P: AsRef<Path>>(&self, _path: P) -> bool {
false
}
pub fn watch<P: AsRef<Path>>(&self, _path: P) {
}
}
#[derive(Debug, Clone)]
pub struct DeferredCall {
pub method: String,
pub path: std::path::PathBuf,
}
pub struct DeferredWatcher {
pub calls: Mutex<Vec<DeferredCall>>,
}
impl Default for DeferredWatcher {
fn default() -> Self {
Self::new()
}
}
impl DeferredWatcher {
pub fn new() -> Self {
Self {
calls: Mutex::new(Vec::new()),
}
}
pub fn check<P: AsRef<Path>>(&self, path: P) {
self.calls.lock().unwrap().push(DeferredCall {
method: "__call__".into(),
path: path.as_ref().to_path_buf(),
});
}
pub fn watch<P: AsRef<Path>>(&self, path: P) {
self.calls.lock().unwrap().push(DeferredCall {
method: "watch".into(),
path: path.as_ref().to_path_buf(),
});
}
pub fn unwatch<P: AsRef<Path>>(&self, path: P) {
self.calls.lock().unwrap().push(DeferredCall {
method: "unwatch".into(),
path: path.as_ref().to_path_buf(),
});
}
pub fn transfer_calls(&self) -> Vec<DeferredCall> {
let mut calls = self.calls.lock().unwrap();
std::mem::take(&mut *calls)
}
}
pub struct ConfigLoader {
pub watcher_type: String,
pub pl: Option<()>,
pub interval: Option<u64>,
pub watched: std::collections::HashMap<std::path::PathBuf, std::collections::HashSet<u64>>,
pub missing: std::collections::HashMap<String, std::collections::HashSet<(u64, u64)>>,
pub loaded: std::collections::HashMap<std::path::PathBuf, Value>,
pub lock: Mutex<()>,
}
impl Default for ConfigLoader {
fn default() -> Self {
Self::new(false)
}
}
impl ConfigLoader {
pub fn new(run_once: bool) -> Self {
let watcher_type = if run_once {
"dummy".to_string()
} else {
"deferred".to_string()
};
Self {
watcher_type,
pl: None,
interval: None,
watched: std::collections::HashMap::new(),
missing: std::collections::HashMap::new(),
loaded: std::collections::HashMap::new(),
lock: Mutex::new(()),
}
}
pub fn set_pl(&mut self, _pl: ()) {
self.pl = Some(());
}
pub fn set_interval(&mut self, interval: u64) {
self.interval = Some(interval);
}
pub fn register<P: AsRef<Path>>(&mut self, function_id: u64, path: P) {
let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
self.watched
.entry(path.as_ref().to_path_buf())
.or_default()
.insert(function_id);
}
pub fn register_missing(
&mut self,
condition_function_id: u64,
function_id: u64,
key: impl Into<String>,
) {
let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
self.missing
.entry(key.into())
.or_default()
.insert((condition_function_id, function_id));
}
pub fn unregister_functions(&mut self, removed_functions: &std::collections::HashSet<u64>) {
let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
let paths: Vec<std::path::PathBuf> = self.watched.keys().cloned().collect();
for path in paths {
if let Some(functions) = self.watched.get_mut(&path) {
for id in removed_functions {
functions.remove(id);
}
if functions.is_empty() {
self.watched.remove(&path);
self.loaded.remove(&path);
}
}
}
}
pub fn unregister_missing(
&mut self,
removed_functions: &std::collections::HashSet<(u64, u64)>,
) {
let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
let keys: Vec<String> = self.missing.keys().cloned().collect();
for key in keys {
if let Some(functions) = self.missing.get_mut(&key) {
for pair in removed_functions {
functions.remove(pair);
}
if functions.is_empty() {
self.missing.remove(&key);
}
}
}
}
pub fn load<P, F>(&mut self, path: P, load_fn: F) -> Result<Value, String>
where
P: AsRef<Path>,
F: FnOnce(&Path) -> Result<Value, String>,
{
let path = path.as_ref().to_path_buf();
if let Some(cached) = self.loaded.get(&path) {
return Ok(cached.clone());
}
let r = load_fn(&path)?;
self.loaded.insert(path, r.clone());
Ok(r)
}
}
impl ConfigLoader {
pub fn set_watcher(&mut self, watcher_type: &str, _force: bool) -> bool {
if watcher_type == self.watcher_type {
return false;
}
let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
self.watcher_type = watcher_type.to_string();
true
}
pub fn update<W, D, C, DM, L>(
&mut self,
watcher: W,
dispatch_fn: D,
condition_fn: C,
dispatch_missing_fn: DM,
load_fn: L,
) -> Vec<(std::path::PathBuf, String)>
where
W: Fn(&std::path::Path) -> Result<bool, String>,
D: Fn(u64, &std::path::Path),
C: Fn(u64, &str) -> Option<std::path::PathBuf>,
DM: Fn(u64, &std::path::Path),
L: Fn(&std::path::Path) -> Result<Value, String>,
{
let mut toload: Vec<std::path::PathBuf> = Vec::new();
let mut errors: Vec<(std::path::PathBuf, String)> = Vec::new();
let watched_snapshot: Vec<(std::path::PathBuf, std::collections::HashSet<u64>)> = {
let _g = self.lock.lock().unwrap();
self.watched
.iter()
.map(|(p, fs)| (p.clone(), fs.clone()))
.collect()
};
for (path, functions) in &watched_snapshot {
for function_id in functions {
let modified = watcher(path).unwrap_or(true);
if modified && !toload.contains(path) {
toload.push(path.clone());
}
if modified {
dispatch_fn(*function_id, path);
}
}
}
let missing_snapshot: Vec<(String, std::collections::HashSet<(u64, u64)>)> = {
let _g = self.lock.lock().unwrap();
self.missing
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
};
for (key, functions) in &missing_snapshot {
let mut remaining: std::collections::HashSet<(u64, u64)> = functions.clone();
for &(cond_id, func_id) in functions {
if let Some(path) = condition_fn(cond_id, key) {
if !toload.contains(&path) {
toload.push(path.clone());
}
dispatch_missing_fn(func_id, &path);
remaining.remove(&(cond_id, func_id));
}
}
let _g = self.lock.lock().unwrap();
if remaining.is_empty() {
self.missing.remove(key);
} else {
self.missing.insert(key.clone(), remaining);
}
}
for path in &toload {
match load_fn(path) {
Ok(v) => {
self.loaded.insert(path.clone(), v);
}
Err(e) => {
self.loaded.remove(path);
errors.push((path.clone(), e));
}
}
}
errors
}
pub fn run<W, D, C, DM, L>(
&mut self,
shutdown_event: &std::sync::Arc<std::sync::atomic::AtomicBool>,
watcher: W,
dispatch_fn: D,
condition_fn: C,
dispatch_missing_fn: DM,
load_fn: L,
) where
W: Fn(&std::path::Path) -> Result<bool, String>,
D: Fn(u64, &std::path::Path),
C: Fn(u64, &str) -> Option<std::path::PathBuf>,
DM: Fn(u64, &std::path::Path),
L: Fn(&std::path::Path) -> Result<Value, String>,
{
use std::sync::atomic::Ordering;
while self.interval.is_some() && !shutdown_event.load(Ordering::Relaxed) {
let _ = self.update(
&watcher,
&dispatch_fn,
&condition_fn,
&dispatch_missing_fn,
&load_fn,
);
let interval = self.interval.unwrap();
let slice = std::time::Duration::from_millis(100);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(interval);
while std::time::Instant::now() < deadline {
if shutdown_event.load(Ordering::Relaxed) {
return;
}
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
std::thread::sleep(slice.min(remaining));
}
}
}
pub fn exception(&self, msg: &str, args: &[&str]) -> Result<String, String> {
let mut rendered = msg.to_string();
for (i, arg) in args.iter().enumerate() {
rendered = rendered.replace(&format!("{{{}}}", i), arg);
}
if self.pl.is_some() {
Ok(format!("config_loader: {}", rendered))
} else {
Err(rendered)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn tmp_json(content: &str) -> std::path::PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut p = std::env::temp_dir();
p.push(format!(
"powerliners-config-test-{}-{}-{}.json",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos(),
COUNTER.fetch_add(1, Ordering::SeqCst)
));
let mut f = std::fs::File::create(&p).unwrap();
f.write_all(content.as_bytes()).unwrap();
p
}
#[test]
fn open_file_reads_utf8_contents() {
let p = tmp_json("héllo, world");
let r = open_file(&p).unwrap();
assert_eq!(r, "héllo, world");
std::fs::remove_file(&p).ok();
}
#[test]
fn load_json_config_parses_basic() {
let p = tmp_json(r#"{"name": "powerline", "version": 1}"#);
let v = load_json_config(&p).unwrap();
assert_eq!(v["name"], "powerline");
assert_eq!(v["version"], 1);
std::fs::remove_file(&p).ok();
}
#[test]
fn load_json_config_returns_err_on_bad_json() {
let p = tmp_json("{ this isn't json }");
assert!(load_json_config(&p).is_err());
std::fs::remove_file(&p).ok();
}
#[test]
fn dummy_watcher_always_returns_false() {
let w = DummyWatcher;
assert!(!w.check("/etc/passwd"));
w.watch("/etc/passwd");
}
#[test]
fn deferred_watcher_queues_calls() {
let w = DeferredWatcher::new();
w.watch("/etc/config1");
w.check("/etc/config2");
w.unwatch("/etc/config1");
let calls = w.transfer_calls();
assert_eq!(calls.len(), 3);
assert_eq!(calls[0].method, "watch");
assert_eq!(calls[1].method, "__call__");
assert_eq!(calls[2].method, "unwatch");
let calls2 = w.transfer_calls();
assert!(calls2.is_empty());
}
#[test]
fn config_loader_run_once_uses_dummy_watcher_type() {
let cl = ConfigLoader::new(true);
assert_eq!(cl.watcher_type, "dummy");
}
#[test]
fn config_loader_default_uses_deferred_watcher_type() {
let cl = ConfigLoader::new(false);
assert_eq!(cl.watcher_type, "deferred");
}
#[test]
fn config_loader_set_pl_records_value() {
let mut cl = ConfigLoader::new(false);
cl.set_pl(());
assert!(cl.pl.is_some());
}
#[test]
fn config_loader_set_interval_records_value() {
let mut cl = ConfigLoader::new(false);
cl.set_interval(5);
assert_eq!(cl.interval, Some(5));
}
#[test]
fn config_loader_register_adds_function_to_watched_path() {
let mut cl = ConfigLoader::new(false);
cl.register(42, "/etc/powerline/config.json");
assert!(cl
.watched
.contains_key(std::path::Path::new("/etc/powerline/config.json")));
assert!(cl.watched[std::path::Path::new("/etc/powerline/config.json")].contains(&42));
}
#[test]
fn config_loader_register_dedupes_function_id() {
let mut cl = ConfigLoader::new(false);
cl.register(42, "/x");
cl.register(42, "/x");
assert_eq!(cl.watched[std::path::Path::new("/x")].len(), 1);
}
#[test]
fn config_loader_register_missing_adds_pair_to_key() {
let mut cl = ConfigLoader::new(false);
cl.register_missing(100, 200, "key1");
assert!(cl.missing.contains_key("key1"));
assert!(cl.missing["key1"].contains(&(100, 200)));
}
#[test]
fn config_loader_unregister_functions_drops_path_when_empty() {
let mut cl = ConfigLoader::new(false);
cl.register(42, "/x");
let mut removed = std::collections::HashSet::new();
removed.insert(42);
cl.unregister_functions(&removed);
assert!(!cl.watched.contains_key(std::path::Path::new("/x")));
}
#[test]
fn config_loader_unregister_functions_keeps_path_with_remaining_functions() {
let mut cl = ConfigLoader::new(false);
cl.register(42, "/x");
cl.register(99, "/x");
let mut removed = std::collections::HashSet::new();
removed.insert(42);
cl.unregister_functions(&removed);
assert!(cl.watched.contains_key(std::path::Path::new("/x")));
assert_eq!(cl.watched[std::path::Path::new("/x")].len(), 1);
}
#[test]
fn config_loader_unregister_functions_clears_loaded_entry() {
let mut cl = ConfigLoader::new(false);
cl.register(42, "/x");
cl.loaded
.insert(std::path::PathBuf::from("/x"), serde_json::json!({"a": 1}));
let mut removed = std::collections::HashSet::new();
removed.insert(42);
cl.unregister_functions(&removed);
assert!(!cl.loaded.contains_key(std::path::Path::new("/x")));
}
#[test]
fn config_loader_unregister_missing_drops_key_when_empty() {
let mut cl = ConfigLoader::new(false);
cl.register_missing(100, 200, "key1");
let mut removed = std::collections::HashSet::new();
removed.insert((100, 200));
cl.unregister_missing(&removed);
assert!(!cl.missing.contains_key("key1"));
}
#[test]
fn config_loader_load_calls_load_fn_on_cache_miss() {
let mut cl = ConfigLoader::new(false);
let p = std::path::PathBuf::from("/test/config.json");
let r = cl
.load(&p, |_| Ok(serde_json::json!({"loaded": true})))
.unwrap();
assert_eq!(r["loaded"], true);
assert!(cl.loaded.contains_key(&p));
}
#[test]
fn config_loader_load_returns_cached_value_on_hit() {
let mut cl = ConfigLoader::new(false);
let p = std::path::PathBuf::from("/test/config.json");
cl.loaded
.insert(p.clone(), serde_json::json!({"cached": true}));
let r = cl
.load(&p, |_| {
panic!("load_fn should not be called on cache hit");
})
.unwrap();
assert_eq!(r["cached"], true);
}
#[test]
fn config_loader_load_propagates_load_fn_errors() {
let mut cl = ConfigLoader::new(false);
let p = std::path::PathBuf::from("/test/missing.json");
let r = cl.load(&p, |_| Err("read fail".to_string()));
assert!(r.is_err());
assert!(!cl.loaded.contains_key(&p));
}
#[test]
fn config_loader_set_watcher_same_type_is_noop() {
let mut cl = ConfigLoader::new(false);
assert!(!cl.set_watcher("deferred", false));
assert_eq!(cl.watcher_type, "deferred");
}
#[test]
fn config_loader_set_watcher_different_type_swaps() {
let mut cl = ConfigLoader::new(false);
assert!(cl.set_watcher("inotify", false));
assert_eq!(cl.watcher_type, "inotify");
}
#[test]
fn config_loader_set_watcher_dummy_to_inotify() {
let mut cl = ConfigLoader::new(true);
assert_eq!(cl.watcher_type, "dummy");
assert!(cl.set_watcher("inotify", false));
assert_eq!(cl.watcher_type, "inotify");
}
#[test]
fn config_loader_exception_no_pl_returns_err() {
let cl = ConfigLoader::new(false);
let r = cl.exception("Error: {0} not found", &["foo"]);
assert!(r.is_err());
assert_eq!(r.unwrap_err(), "Error: foo not found");
}
#[test]
fn config_loader_exception_with_pl_returns_formatted() {
let mut cl = ConfigLoader::new(false);
cl.set_pl(());
let r = cl.exception("Error: {0} broken", &["xyz"]).unwrap();
assert_eq!(r, "config_loader: Error: xyz broken");
}
#[test]
fn config_loader_exception_substitutes_multiple_args() {
let cl = ConfigLoader::new(false);
let r = cl.exception("a={0} b={1}", &["X", "Y"]).unwrap_err();
assert_eq!(r, "a=X b=Y");
}
#[test]
fn config_loader_update_dispatches_modified_paths() {
let mut cl = ConfigLoader::new(false);
let p = std::path::PathBuf::from("/tmp/zz_pwl_test_a.json");
cl.register(42, &p);
let called = std::sync::Arc::new(std::sync::Mutex::new(
Vec::<(u64, std::path::PathBuf)>::new(),
));
let loaded = std::sync::Arc::new(std::sync::Mutex::new(Vec::<std::path::PathBuf>::new()));
let called_c = called.clone();
let loaded_c = loaded.clone();
let errors = cl.update(
|_p| Ok(true),
move |id, path| called_c.lock().unwrap().push((id, path.to_path_buf())),
|_id, _key| None,
|_id, _path| {},
move |path| {
loaded_c.lock().unwrap().push(path.to_path_buf());
Ok(Value::Object(serde_json::Map::new()))
},
);
assert!(errors.is_empty());
assert_eq!(*called.lock().unwrap(), vec![(42, p.clone())]);
assert_eq!(*loaded.lock().unwrap(), vec![p.clone()]);
assert!(cl.loaded.contains_key(&p));
}
#[test]
fn config_loader_update_skips_unmodified_paths() {
let mut cl = ConfigLoader::new(false);
let p = std::path::PathBuf::from("/tmp/zz_pwl_test_b.json");
cl.register(99, &p);
let called = std::sync::Arc::new(std::sync::Mutex::new(0u32));
let called_c = called.clone();
let errors = cl.update(
|_p| Ok(false),
move |_id, _path| {
*called_c.lock().unwrap() += 1;
},
|_id, _key| None,
|_id, _path| {},
|_path| Ok(Value::Null),
);
assert!(errors.is_empty());
assert_eq!(*called.lock().unwrap(), 0);
assert!(!cl.loaded.contains_key(&p));
}
#[test]
fn config_loader_update_load_error_surfaces() {
let mut cl = ConfigLoader::new(false);
let p = std::path::PathBuf::from("/tmp/zz_pwl_test_c.json");
cl.register(1, &p);
let errors = cl.update(
|_p| Ok(true),
|_id, _path| {},
|_id, _key| None,
|_id, _path| {},
|_path| Err("disk full".to_string()),
);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].0, p);
assert!(errors[0].1.contains("disk full"));
assert!(!cl.loaded.contains_key(&p));
}
#[test]
fn config_loader_update_resolves_missing_via_condition() {
let mut cl = ConfigLoader::new(false);
let resolved = std::path::PathBuf::from("/tmp/zz_pwl_test_d.json");
let key = "ext.shell.theme".to_string();
cl.register_missing(7, 13, &key);
let resolved_c = resolved.clone();
let dispatched = std::sync::Arc::new(std::sync::Mutex::new(Vec::<u64>::new()));
let dispatched_c = dispatched.clone();
let errors = cl.update(
|_p| Ok(false),
|_id, _path| {},
move |_cond_id, _k| Some(resolved_c.clone()),
move |id, _path| dispatched_c.lock().unwrap().push(id),
|_path| Ok(Value::Bool(true)),
);
assert!(errors.is_empty());
assert_eq!(*dispatched.lock().unwrap(), vec![13]);
assert!(!cl.missing.contains_key(&key));
}
#[test]
fn config_loader_run_exits_immediately_when_shutdown_event_set() {
use std::sync::atomic::AtomicBool;
let mut cl = ConfigLoader::new(false);
cl.set_interval(60);
let event = std::sync::Arc::new(AtomicBool::new(true));
let invoked = std::sync::Arc::new(std::sync::Mutex::new(0u32));
let invoked_c = invoked.clone();
cl.run(
&event,
|_p| Ok(false),
move |_id, _path| {
*invoked_c.lock().unwrap() += 1;
},
|_id, _key| None,
|_id, _path| {},
|_path| Ok(Value::Null),
);
assert_eq!(*invoked.lock().unwrap(), 0);
}
#[test]
fn config_loader_run_exits_when_interval_is_none() {
use std::sync::atomic::AtomicBool;
let mut cl = ConfigLoader::new(false);
let event = std::sync::Arc::new(AtomicBool::new(false));
cl.run(
&event,
|_p| Ok(false),
|_id, _path| {},
|_id, _key| None,
|_id, _path| {},
|_path| Ok(Value::Null),
);
}
}