tuna_file/
lib.rs

1use std::{
2    collections::HashMap,
3    path::PathBuf,
4    sync::{
5        atomic::{AtomicBool, Ordering},
6        mpsc::{channel, RecvTimeoutError},
7        Arc,
8    },
9    thread::JoinHandle,
10    time::Duration,
11};
12
13use notify::{watcher, RecursiveMode, Watcher};
14use toml::Value;
15use tuna::{Boolean, Float32, Float64, Int32, Int64};
16
17pub struct FileWatcher {
18    shutdown: Arc<AtomicBool>,
19    thread_handle: Option<JoinHandle<()>>,
20    _watcher: notify::RecommendedWatcher,
21}
22
23impl Drop for FileWatcher {
24    fn drop(&mut self) {
25        self.shutdown.store(true, Ordering::Relaxed);
26
27        if let Some(j) = self.thread_handle.take() {
28            match j.join() {
29                Ok(_) => log::debug!("File watcher stopped"),
30                Err(e) => log::error!("Failed joining file watcher: {:?}", e),
31            }
32        }
33    }
34}
35
36type TomlContents = HashMap<String, HashMap<String, Value>>;
37
38fn apply_state(state: TomlContents) {
39    for (category, kvs) in state {
40        for (name, value) in kvs {
41            let success = match value {
42                Value::String(v) => {
43                    log::warn!(
44                        "unsupported string value: `{}/{} = {}`",
45                        &category,
46                        &name,
47                        v,
48                    );
49                    false
50                }
51                Value::Datetime(v) => {
52                    log::warn!(
53                        "unsupported string value: `{}/{} = {}`",
54                        &category,
55                        &name,
56                        v,
57                    );
58                    false
59                }
60                Value::Integer(v) => {
61                    tuna::set::<Int64>(&category, &name, v)
62                        || tuna::set::<Int32>(&category, &name, v as i32)
63                }
64                Value::Float(v) => {
65                    tuna::set::<Float64>(&category, &name, v)
66                        || tuna::set::<Float32>(&category, &name, v as f32)
67                }
68                Value::Boolean(v) => tuna::set::<Boolean>(&category, &name, v),
69
70                Value::Array(_) => false,
71                Value::Table(_) => false,
72            };
73
74            if !success {
75                log::error!("unknown tuneable: `{}/{}`", category, name);
76            }
77        }
78    }
79}
80
81pub fn open(path: PathBuf, period: Duration) -> anyhow::Result<FileWatcher> {
82    let initial_contents = std::fs::read_to_string(&path)?;
83    let initial_state = toml::from_str(&initial_contents)?;
84    apply_state(initial_state);
85
86    let should_exit = Arc::new(AtomicBool::new(false));
87    let shutdown = should_exit.clone();
88
89    let (tx, rx) = channel();
90    let mut watcher = watcher(tx, period).unwrap();
91    watcher
92        .watch(path.clone(), RecursiveMode::Recursive)
93        .unwrap();
94
95    let thread_handle = std::thread::spawn(move || loop {
96        match rx.recv_timeout(Duration::from_millis(10)) {
97            Ok(_event) => {
98                let contents = match std::fs::read_to_string(&path) {
99                    Ok(v) => v,
100                    Err(e) => {
101                        log::error!("failed reading file: {}", e);
102                        continue;
103                    }
104                };
105                let state: TomlContents = match toml::from_str(&contents) {
106                    Ok(v) => v,
107                    Err(e) => {
108                        log::error!("failed parsing file: {}", e);
109                        continue;
110                    }
111                };
112
113                apply_state(state);
114            }
115            Err(e) if e != RecvTimeoutError::Timeout => println!("watch error: {:?}", e),
116            _ => {}
117        }
118
119        if should_exit.load(Ordering::Relaxed) {
120            break;
121        }
122    });
123
124    Ok(FileWatcher {
125        shutdown,
126        thread_handle: Some(thread_handle),
127        _watcher: watcher,
128    })
129}