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)]
20pub enum ConfigChangeEvent {
21 Reloaded {
23 path: PathBuf,
25 timestamp: SystemTime,
27 },
28 ReloadFailed {
30 path: PathBuf,
32 error: String,
34 timestamp: SystemTime,
36 },
37 FileModified {
39 path: PathBuf,
41 timestamp: SystemTime,
43 },
44 FileDeleted {
46 path: PathBuf,
48 timestamp: SystemTime,
50 },
51}
52
53pub struct HotReloadConfig {
55 current: Arc<RwLock<Config>>,
57 file_path: PathBuf,
59 last_modified: SystemTime,
61 event_sender: Option<Sender<ConfigChangeEvent>>,
63 poll_interval: Duration,
65}
66
67impl HotReloadConfig {
68 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
70 let path = path.as_ref().to_path_buf();
71 let config = Config::from_file(&path)?;
72
73 let last_modified = std::fs::metadata(&path)
74 .map_err(|e| Error::io(path.display().to_string(), e))?
75 .modified()
76 .map_err(|e| Error::io(path.display().to_string(), e))?;
77
78 Ok(Self {
79 current: Arc::new(RwLock::new(config)),
80 file_path: path,
81 last_modified,
82 event_sender: None,
83 poll_interval: Duration::from_millis(1000), })
85 }
86
87 pub fn with_poll_interval(mut self, interval: Duration) -> Self {
89 self.poll_interval = interval;
90 self
91 }
92
93 pub fn with_change_notifications(mut self) -> (Self, Receiver<ConfigChangeEvent>) {
95 let (sender, receiver) = mpsc::channel();
96 self.event_sender = Some(sender);
97 (self, receiver)
98 }
99
100 pub fn config(&self) -> Arc<RwLock<Config>> {
102 Arc::clone(&self.current)
103 }
104
105 pub fn snapshot(&self) -> Result<Config> {
107 let _config = self
108 .current
109 .read()
110 .map_err(|_| Error::concurrency("Failed to acquire read lock".to_string()))?;
111
112 let _content = std::fs::read_to_string(&self.file_path)
115 .map_err(|e| Error::io(self.file_path.display().to_string(), e))?;
116
117 Config::from_file(&self.file_path)
118 }
119
120 pub fn reload(&mut self) -> Result<bool> {
122 let metadata = std::fs::metadata(&self.file_path)
123 .map_err(|e| Error::io(self.file_path.display().to_string(), e))?;
124
125 let modified = metadata
126 .modified()
127 .map_err(|e| Error::io(self.file_path.display().to_string(), e))?;
128
129 if modified <= self.last_modified {
130 return Ok(false); }
132
133 match Config::from_file(&self.file_path) {
134 Ok(new_config) => {
135 {
137 let mut config = self.current.write().map_err(|_| {
138 Error::concurrency("Failed to acquire write lock".to_string())
139 })?;
140 *config = new_config;
141 }
142
143 self.last_modified = modified;
144
145 if let Some(ref sender) = self.event_sender {
147 let _ = sender.send(ConfigChangeEvent::Reloaded {
148 path: self.file_path.clone(),
149 timestamp: SystemTime::now(),
150 });
151 }
152
153 Ok(true)
154 }
155 Err(e) => {
156 if let Some(ref sender) = self.event_sender {
158 let _ = sender.send(ConfigChangeEvent::ReloadFailed {
159 path: self.file_path.clone(),
160 error: e.to_string(),
161 timestamp: SystemTime::now(),
162 });
163 }
164 Err(e)
165 }
166 }
167 }
168
169 pub fn start_watching(self) -> HotReloadHandle {
171 let (stop_sender, stop_receiver) = mpsc::channel();
172 let config_clone = Arc::clone(&self.current);
173 let file_path = self.file_path.clone();
174 let event_sender = self.event_sender.clone();
175 let poll_interval = self.poll_interval;
176 let mut last_modified = self.last_modified;
177
178 let handle = thread::spawn(move || {
179 loop {
180 if stop_receiver.try_recv().is_ok() {
182 break;
183 }
184
185 if let Ok(metadata) = std::fs::metadata(&file_path) {
187 if let Ok(modified) = metadata.modified() {
188 if modified > last_modified {
189 if let Some(ref sender) = event_sender {
191 let _ = sender.send(ConfigChangeEvent::FileModified {
192 path: file_path.clone(),
193 timestamp: SystemTime::now(),
194 });
195 }
196
197 match Config::from_file(&file_path) {
199 Ok(new_config) => {
200 if let Ok(mut config) = config_clone.write() {
202 *config = new_config;
203 last_modified = modified;
204
205 if let Some(ref sender) = event_sender {
207 let _ = sender.send(ConfigChangeEvent::Reloaded {
208 path: file_path.clone(),
209 timestamp: SystemTime::now(),
210 });
211 }
212 }
213 }
214 Err(e) => {
215 if let Some(ref sender) = event_sender {
217 let _ = sender.send(ConfigChangeEvent::ReloadFailed {
218 path: file_path.clone(),
219 error: e.to_string(),
220 timestamp: SystemTime::now(),
221 });
222 }
223 }
224 }
225 }
226 }
227 }
228
229 thread::sleep(poll_interval);
230 }
231 });
232
233 HotReloadHandle {
234 handle: Some(handle),
235 stop_sender,
236 }
237 }
238
239 pub fn file_path(&self) -> &Path {
241 &self.file_path
242 }
243
244 pub fn last_modified(&self) -> SystemTime {
246 self.last_modified
247 }
248}
249
250pub struct HotReloadHandle {
252 handle: Option<thread::JoinHandle<()>>,
253 stop_sender: Sender<()>,
254}
255
256impl HotReloadHandle {
257 pub fn stop(mut self) -> Result<()> {
259 if self.stop_sender.send(()).is_err() {
260 return Err(Error::concurrency("Failed to send stop signal".to_string()));
261 }
262
263 if let Some(handle) = self.handle.take() {
264 handle
265 .join()
266 .map_err(|_| Error::concurrency("Failed to join background thread".to_string()))?;
267 }
268
269 Ok(())
270 }
271}
272
273impl Drop for HotReloadHandle {
274 fn drop(&mut self) {
275 let _ = self.stop_sender.send(());
276 if let Some(handle) = self.handle.take() {
277 let _ = handle.join();
278 }
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use std::fs::File;
286 use std::io::Write;
287 use tempfile::TempDir;
288
289 #[test]
290 fn test_hot_reload_basic() {
291 let temp_dir = TempDir::new().unwrap();
292 let config_path = temp_dir.path().join("test.conf");
293
294 let mut file = File::create(&config_path).unwrap();
296 writeln!(file, "key=value1").unwrap();
297 file.flush().unwrap();
298 drop(file);
299
300 let mut hot_config = HotReloadConfig::from_file(&config_path).unwrap();
302
303 {
305 let config = hot_config.config();
306 let config_read = config.read().unwrap();
307 assert_eq!(
308 config_read.get("key").unwrap().as_string().unwrap(),
309 "value1"
310 );
311 }
312
313 thread::sleep(Duration::from_millis(10));
315
316 let mut file = File::create(&config_path).unwrap();
318 writeln!(file, "key=value2").unwrap();
319 file.flush().unwrap();
320 drop(file);
321
322 let reloaded = hot_config.reload().unwrap();
324 assert!(reloaded);
325
326 {
328 let config = hot_config.config();
329 let config_read = config.read().unwrap();
330 assert_eq!(
331 config_read.get("key").unwrap().as_string().unwrap(),
332 "value2"
333 );
334 }
335 }
336
337 #[test]
338 fn test_hot_reload_notifications() {
339 let temp_dir = TempDir::new().unwrap();
340 let config_path = temp_dir.path().join("test.conf");
341
342 let mut file = File::create(&config_path).unwrap();
344 writeln!(file, "key=value1").unwrap();
345 file.flush().unwrap();
346 drop(file);
347
348 let (mut hot_config, receiver) = HotReloadConfig::from_file(&config_path)
350 .unwrap()
351 .with_change_notifications();
352
353 thread::sleep(Duration::from_millis(10));
355
356 let mut file = File::create(&config_path).unwrap();
358 writeln!(file, "key=value2").unwrap();
359 file.flush().unwrap();
360 drop(file);
361
362 hot_config.reload().unwrap();
364
365 let event = receiver.try_recv().unwrap();
367 match event {
368 ConfigChangeEvent::Reloaded { path, .. } => {
369 assert_eq!(path, config_path);
370 }
371 _ => panic!("Expected Reloaded event"),
372 }
373 }
374
375 #[test]
376 fn test_automatic_watching() {
377 let temp_dir = TempDir::new().unwrap();
378 let config_path = temp_dir.path().join("test.conf");
379
380 let mut file = File::create(&config_path).unwrap();
382 writeln!(file, "key=value1").unwrap();
383 file.flush().unwrap();
384 drop(file);
385
386 let (hot_config, receiver) = HotReloadConfig::from_file(&config_path)
388 .unwrap()
389 .with_poll_interval(Duration::from_millis(50))
390 .with_change_notifications();
391
392 let config_ref = hot_config.config();
393 let handle = hot_config.start_watching();
394
395 thread::sleep(Duration::from_millis(100));
397
398 let mut file = File::create(&config_path).unwrap();
400 writeln!(file, "key=value2").unwrap();
401 file.flush().unwrap();
402 drop(file);
403
404 thread::sleep(Duration::from_millis(200));
406
407 {
409 let config_read = config_ref.read().unwrap();
410 assert_eq!(
411 config_read.get("key").unwrap().as_string().unwrap(),
412 "value2"
413 );
414 }
415
416 let mut received_events = Vec::new();
418 while let Ok(event) = receiver.try_recv() {
419 received_events.push(event);
420 }
421
422 assert!(!received_events.is_empty());
423
424 let has_reloaded = received_events
426 .iter()
427 .any(|event| matches!(event, ConfigChangeEvent::Reloaded { .. }));
428 assert!(has_reloaded);
429
430 handle.stop().unwrap();
432 }
433}