1use crate::config::Config;
11use crate::error::{Error, Result};
12use std::path::{Path, PathBuf};
13use std::sync::mpsc::{self, Receiver, Sender};
14use std::sync::{Arc, RwLock};
15use std::thread;
16use std::time::{Duration, SystemTime};
17
18#[derive(Debug, Clone)]
25#[non_exhaustive]
26pub enum ConfigChangeEvent {
27 Reloaded {
29 path: PathBuf,
31 timestamp: SystemTime,
33 },
34 ReloadFailed {
36 path: PathBuf,
38 error: String,
40 timestamp: SystemTime,
42 },
43 FileModified {
45 path: PathBuf,
47 timestamp: SystemTime,
49 },
50 FileDeleted {
52 path: PathBuf,
54 timestamp: SystemTime,
56 },
57}
58
59pub struct HotReloadConfig {
61 current: Arc<RwLock<Config>>,
63 file_path: PathBuf,
65 last_modified: SystemTime,
67 event_sender: Option<Sender<ConfigChangeEvent>>,
69 poll_interval: Duration,
71}
72
73impl HotReloadConfig {
74 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
76 let path = path.as_ref().to_path_buf();
77 let config = Config::from_file(&path)?;
78
79 let last_modified = std::fs::metadata(&path)
80 .map_err(|e| Error::io(path.display().to_string(), e))?
81 .modified()
82 .map_err(|e| Error::io(path.display().to_string(), e))?;
83
84 Ok(Self {
85 current: Arc::new(RwLock::new(config)),
86 file_path: path,
87 last_modified,
88 event_sender: None,
89 poll_interval: Duration::from_secs(1), })
91 }
92
93 pub fn with_poll_interval(mut self, interval: Duration) -> Self {
95 self.poll_interval = interval;
96 self
97 }
98
99 pub fn with_change_notifications(mut self) -> (Self, Receiver<ConfigChangeEvent>) {
101 let (sender, receiver) = mpsc::channel();
102 self.event_sender = Some(sender);
103 (self, receiver)
104 }
105
106 pub fn config(&self) -> Arc<RwLock<Config>> {
108 Arc::clone(&self.current)
109 }
110
111 pub fn snapshot(&self) -> Result<Config> {
113 let _config = self
114 .current
115 .read()
116 .map_err(|_| Error::concurrency("Failed to acquire read lock".to_string()))?;
117
118 let _content = std::fs::read_to_string(&self.file_path)
121 .map_err(|e| Error::io(self.file_path.display().to_string(), e))?;
122
123 Config::from_file(&self.file_path)
124 }
125
126 pub fn reload(&mut self) -> Result<bool> {
128 let metadata = std::fs::metadata(&self.file_path)
129 .map_err(|e| Error::io(self.file_path.display().to_string(), e))?;
130
131 let modified = metadata
132 .modified()
133 .map_err(|e| Error::io(self.file_path.display().to_string(), e))?;
134
135 if modified <= self.last_modified {
136 return Ok(false); }
138
139 match Config::from_file(&self.file_path) {
140 Ok(new_config) => {
141 {
143 let mut config = self.current.write().map_err(|_| {
144 Error::concurrency("Failed to acquire write lock".to_string())
145 })?;
146 *config = new_config;
147 }
148
149 self.last_modified = modified;
150
151 if let Some(ref sender) = self.event_sender {
153 let _ = sender.send(ConfigChangeEvent::Reloaded {
154 path: self.file_path.clone(),
155 timestamp: SystemTime::now(),
156 });
157 }
158
159 Ok(true)
160 }
161 Err(e) => {
162 if let Some(ref sender) = self.event_sender {
164 let _ = sender.send(ConfigChangeEvent::ReloadFailed {
165 path: self.file_path.clone(),
166 error: e.to_string(),
167 timestamp: SystemTime::now(),
168 });
169 }
170 Err(e)
171 }
172 }
173 }
174
175 pub fn start_watching(self) -> HotReloadHandle {
177 let (stop_sender, stop_receiver) = mpsc::channel();
178 let config_clone = Arc::clone(&self.current);
179 let file_path = self.file_path.clone();
180 let event_sender = self.event_sender.clone();
181 let poll_interval = self.poll_interval;
182 let mut last_modified = self.last_modified;
183
184 let handle = thread::spawn(move || {
185 loop {
186 if stop_receiver.try_recv().is_ok() {
188 break;
189 }
190
191 if let Ok(metadata) = std::fs::metadata(&file_path) {
193 if let Ok(modified) = metadata.modified() {
194 if modified > last_modified {
195 if let Some(ref sender) = event_sender {
197 let _ = sender.send(ConfigChangeEvent::FileModified {
198 path: file_path.clone(),
199 timestamp: SystemTime::now(),
200 });
201 }
202
203 match Config::from_file(&file_path) {
205 Ok(new_config) => {
206 if let Ok(mut config) = config_clone.write() {
208 *config = new_config;
209 last_modified = modified;
210
211 if let Some(ref sender) = event_sender {
213 let _ = sender.send(ConfigChangeEvent::Reloaded {
214 path: file_path.clone(),
215 timestamp: SystemTime::now(),
216 });
217 }
218 }
219 }
220 Err(e) => {
221 if let Some(ref sender) = event_sender {
223 let _ = sender.send(ConfigChangeEvent::ReloadFailed {
224 path: file_path.clone(),
225 error: e.to_string(),
226 timestamp: SystemTime::now(),
227 });
228 }
229 }
230 }
231 }
232 }
233 }
234
235 thread::sleep(poll_interval);
236 }
237 });
238
239 HotReloadHandle {
240 handle: Some(handle),
241 stop_sender,
242 }
243 }
244
245 pub fn file_path(&self) -> &Path {
247 &self.file_path
248 }
249
250 pub fn last_modified(&self) -> SystemTime {
252 self.last_modified
253 }
254}
255
256pub struct HotReloadHandle {
258 handle: Option<thread::JoinHandle<()>>,
259 stop_sender: Sender<()>,
260}
261
262impl HotReloadHandle {
263 pub fn stop(mut self) -> Result<()> {
265 if self.stop_sender.send(()).is_err() {
266 return Err(Error::concurrency("Failed to send stop signal".to_string()));
267 }
268
269 if let Some(handle) = self.handle.take() {
270 handle
271 .join()
272 .map_err(|_| Error::concurrency("Failed to join background thread".to_string()))?;
273 }
274
275 Ok(())
276 }
277}
278
279impl Drop for HotReloadHandle {
280 fn drop(&mut self) {
281 let _ = self.stop_sender.send(());
282 if let Some(handle) = self.handle.take() {
283 let _ = handle.join();
284 }
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use std::fs::File;
292 use std::io::Write;
293 use tempfile::TempDir;
294
295 #[test]
296 fn test_hot_reload_basic() {
297 let temp_dir = TempDir::new().unwrap();
298 let config_path = temp_dir.path().join("test.conf");
299
300 let mut file = File::create(&config_path).unwrap();
302 writeln!(file, "key=value1").unwrap();
303 file.flush().unwrap();
304 drop(file);
305
306 let mut hot_config = HotReloadConfig::from_file(&config_path).unwrap();
308
309 {
311 let config = hot_config.config();
312 let config_read = config.read().unwrap();
313 assert_eq!(
314 config_read.get("key").unwrap().as_string().unwrap(),
315 "value1"
316 );
317 }
318
319 thread::sleep(Duration::from_millis(10));
321
322 let mut file = File::create(&config_path).unwrap();
324 writeln!(file, "key=value2").unwrap();
325 file.flush().unwrap();
326 drop(file);
327
328 let reloaded = hot_config.reload().unwrap();
330 assert!(reloaded);
331
332 {
334 let config = hot_config.config();
335 let config_read = config.read().unwrap();
336 assert_eq!(
337 config_read.get("key").unwrap().as_string().unwrap(),
338 "value2"
339 );
340 }
341 }
342
343 #[test]
344 fn test_hot_reload_notifications() {
345 let temp_dir = TempDir::new().unwrap();
346 let config_path = temp_dir.path().join("test.conf");
347
348 let mut file = File::create(&config_path).unwrap();
350 writeln!(file, "key=value1").unwrap();
351 file.flush().unwrap();
352 drop(file);
353
354 let (mut hot_config, receiver) = HotReloadConfig::from_file(&config_path)
356 .unwrap()
357 .with_change_notifications();
358
359 thread::sleep(Duration::from_millis(10));
361
362 let mut file = File::create(&config_path).unwrap();
364 writeln!(file, "key=value2").unwrap();
365 file.flush().unwrap();
366 drop(file);
367
368 hot_config.reload().unwrap();
370
371 let event = receiver.try_recv().unwrap();
373 match event {
374 ConfigChangeEvent::Reloaded { path, .. } => {
375 assert_eq!(path, config_path);
376 }
377 _ => panic!("Expected Reloaded event"),
378 }
379 }
380
381 #[test]
382 fn test_automatic_watching() {
383 let temp_dir = TempDir::new().unwrap();
384 let config_path = temp_dir.path().join("test.conf");
385
386 let mut file = File::create(&config_path).unwrap();
388 writeln!(file, "key=value1").unwrap();
389 file.flush().unwrap();
390 drop(file);
391
392 let (hot_config, receiver) = HotReloadConfig::from_file(&config_path)
394 .unwrap()
395 .with_poll_interval(Duration::from_millis(50))
396 .with_change_notifications();
397
398 let config_ref = hot_config.config();
399 let handle = hot_config.start_watching();
400
401 thread::sleep(Duration::from_millis(100));
403
404 let mut file = File::create(&config_path).unwrap();
406 writeln!(file, "key=value2").unwrap();
407 file.flush().unwrap();
408 drop(file);
409
410 thread::sleep(Duration::from_millis(200));
412
413 {
415 let config_read = config_ref.read().unwrap();
416 assert_eq!(
417 config_read.get("key").unwrap().as_string().unwrap(),
418 "value2"
419 );
420 }
421
422 let mut received_events = Vec::new();
424 while let Ok(event) = receiver.try_recv() {
425 received_events.push(event);
426 }
427
428 assert!(!received_events.is_empty());
429
430 let has_reloaded = received_events
432 .iter()
433 .any(|event| matches!(event, ConfigChangeEvent::Reloaded { .. }));
434 assert!(has_reloaded);
435
436 handle.stop().unwrap();
438 }
439}