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}