goud_engine/assets/hot_reload/
watcher.rs1use crate::assets::AssetServer;
4use notify::{
5 Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Result as NotifyResult, Watcher,
6};
7use std::collections::{HashMap, HashSet};
8use std::fmt;
9use std::path::{Path, PathBuf};
10use std::sync::mpsc::{channel, Receiver, Sender};
11use std::time::{Duration, Instant};
12
13use super::config::HotReloadConfig;
14use super::events::AssetChangeEvent;
15
16pub struct HotReloadWatcher {
48 _watcher: RecommendedWatcher,
50
51 receiver: Receiver<NotifyResult<Event>>,
53
54 config: HotReloadConfig,
56
57 debounce_map: HashMap<PathBuf, Instant>,
59
60 watched_paths: HashSet<PathBuf>,
62
63 asset_root: PathBuf,
65}
66
67impl HotReloadWatcher {
68 pub fn new(server: &AssetServer) -> NotifyResult<Self> {
85 Self::with_config(server, HotReloadConfig::default())
86 }
87
88 pub fn with_config(server: &AssetServer, config: HotReloadConfig) -> NotifyResult<Self> {
94 let (sender, receiver) = channel();
95 let asset_root = server.asset_root().to_path_buf();
96
97 let watcher = Self::create_watcher(sender, &config)?;
99
100 Ok(Self {
101 _watcher: watcher,
102 receiver,
103 config,
104 debounce_map: HashMap::new(),
105 watched_paths: HashSet::new(),
106 asset_root,
107 })
108 }
109
110 fn create_watcher(
112 sender: Sender<NotifyResult<Event>>,
113 _config: &HotReloadConfig,
114 ) -> NotifyResult<RecommendedWatcher> {
115 let watcher = RecommendedWatcher::new(
116 move |res| {
117 let _ = sender.send(res);
118 },
119 Config::default(),
120 )?;
121
122 Ok(watcher)
123 }
124
125 pub fn watch(&mut self, path: impl AsRef<Path>) -> NotifyResult<()> {
144 let path = path.as_ref();
145 let mode = if self.config.recursive {
146 RecursiveMode::Recursive
147 } else {
148 RecursiveMode::NonRecursive
149 };
150
151 self._watcher.watch(path, mode)?;
152 self.watched_paths.insert(path.to_path_buf());
153
154 Ok(())
155 }
156
157 pub fn unwatch(&mut self, path: impl AsRef<Path>) -> NotifyResult<()> {
163 let path = path.as_ref();
164 self._watcher.unwatch(path)?;
165 self.watched_paths.remove(path);
166
167 Ok(())
168 }
169
170 pub fn is_watching(&self, path: &Path) -> bool {
172 self.watched_paths.contains(path)
173 }
174
175 pub fn watched_paths(&self) -> &HashSet<PathBuf> {
177 &self.watched_paths
178 }
179
180 pub fn config(&self) -> &HotReloadConfig {
182 &self.config
183 }
184
185 pub fn config_mut(&mut self) -> &mut HotReloadConfig {
189 &mut self.config
190 }
191
192 pub fn process_events(&mut self, server: &mut AssetServer) -> usize {
215 if !self.config.enabled {
216 return 0;
217 }
218
219 let now = Instant::now();
220
221 let mut change_events = Vec::new();
223 while let Ok(event_result) = self.receiver.try_recv() {
224 if let Ok(event) = event_result {
225 if let Some(change_event) = self.process_file_event(&event, now) {
226 change_events.push(change_event);
227 }
228 }
229 }
230
231 let mut reload_count = 0;
233 for event in &change_events {
234 let path = event.path();
235 if let Some(relative) = self.relative_path(path) {
236 if server.reload_by_path(&relative) {
238 reload_count += 1;
239 }
240
241 let cascade = server.get_cascade_order(&relative);
243 for dependent_path in &cascade {
244 if server.reload_by_path(dependent_path) {
245 reload_count += 1;
246 }
247 }
248 }
249 }
250
251 if self.debounce_map.len() > 1000 {
253 self.debounce_map
254 .retain(|_, time| now.duration_since(*time) < Duration::from_secs(10));
255 }
256
257 reload_count
258 }
259
260 fn relative_path(&self, path: &Path) -> Option<String> {
264 path.strip_prefix(&self.asset_root)
265 .ok()
266 .and_then(|p| p.to_str())
267 .map(|s| s.to_string())
268 }
269
270 fn process_file_event(&mut self, event: &Event, now: Instant) -> Option<AssetChangeEvent> {
272 let change_event = match &event.kind {
274 EventKind::Modify(_) => {
275 let path = event.paths.first()?.clone();
276 AssetChangeEvent::Modified { path }
277 }
278 EventKind::Create(_) => {
279 let path = event.paths.first()?.clone();
280 AssetChangeEvent::Created { path }
281 }
282 EventKind::Remove(_) => {
283 let path = event.paths.first()?.clone();
284 AssetChangeEvent::Deleted { path }
285 }
286 _ => return None, };
288
289 let path = change_event.path();
290
291 if !self.config.should_watch(path) {
293 return None;
294 }
295
296 if let Some(last_time) = self.debounce_map.get(path) {
298 if now.duration_since(*last_time) < self.config.debounce_duration {
299 return None; }
301 }
302
303 self.debounce_map.insert(path.to_path_buf(), now);
305
306 Some(change_event)
307 }
308
309 pub fn clear_debounce(&mut self) {
313 self.debounce_map.clear();
314 }
315}
316
317impl fmt::Debug for HotReloadWatcher {
318 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319 f.debug_struct("HotReloadWatcher")
320 .field("config", &self.config)
321 .field("watched_paths", &self.watched_paths.len())
322 .field("debounce_entries", &self.debounce_map.len())
323 .finish()
324 }
325}
326
327#[cfg(test)]
332mod tests {
333 use super::*;
334 use std::fs;
335 use std::io::Write;
336 use tempfile::TempDir;
337
338 fn create_test_server() -> (AssetServer, TempDir) {
339 let temp_dir = TempDir::new().unwrap();
340 let server = AssetServer::with_root(temp_dir.path());
341 (server, temp_dir)
342 }
343
344 #[test]
345 fn test_new() {
346 let (server, _temp_dir) = create_test_server();
347 let watcher = HotReloadWatcher::new(&server);
348
349 assert!(watcher.is_ok());
350 }
351
352 #[test]
353 fn test_with_config() {
354 let (server, _temp_dir) = create_test_server();
355 let config = HotReloadConfig::new().with_enabled(false);
356 let watcher = HotReloadWatcher::with_config(&server, config);
357
358 assert!(watcher.is_ok());
359 assert!(!watcher.unwrap().config().enabled);
360 }
361
362 #[test]
363 fn test_watch() {
364 let (server, temp_dir) = create_test_server();
365 let mut watcher = HotReloadWatcher::new(&server).unwrap();
366
367 let result = watcher.watch(temp_dir.path());
368 assert!(result.is_ok());
369 assert!(watcher.is_watching(temp_dir.path()));
370 assert_eq!(watcher.watched_paths().len(), 1);
371 }
372
373 #[test]
374 fn test_unwatch() {
375 let (server, temp_dir) = create_test_server();
376 let mut watcher = HotReloadWatcher::new(&server).unwrap();
377
378 watcher.watch(temp_dir.path()).unwrap();
379 assert!(watcher.is_watching(temp_dir.path()));
380
381 watcher.unwatch(temp_dir.path()).unwrap();
382 assert!(!watcher.is_watching(temp_dir.path()));
383 }
384
385 #[test]
386 fn test_process_events_disabled() {
387 let (mut server, temp_dir) = create_test_server();
388 let config = HotReloadConfig::new().with_enabled(false);
389 let mut watcher = HotReloadWatcher::with_config(&server, config).unwrap();
390
391 watcher.watch(temp_dir.path()).unwrap();
392
393 let test_file = temp_dir.path().join("test.txt");
395 fs::File::create(&test_file)
396 .unwrap()
397 .write_all(b"test")
398 .unwrap();
399
400 std::thread::sleep(Duration::from_millis(50));
402 let count = watcher.process_events(&mut server);
403
404 assert_eq!(count, 0);
405 }
406
407 #[test]
408 fn test_config_mut() {
409 let (server, _temp_dir) = create_test_server();
410 let mut watcher = HotReloadWatcher::new(&server).unwrap();
411
412 watcher.config_mut().enabled = false;
413 assert!(!watcher.config().enabled);
414 }
415
416 #[test]
417 fn test_clear_debounce() {
418 let (server, _temp_dir) = create_test_server();
419 let mut watcher = HotReloadWatcher::new(&server).unwrap();
420
421 watcher.clear_debounce();
422 }
424
425 #[test]
426 fn test_debug() {
427 let (server, _temp_dir) = create_test_server();
428 let watcher = HotReloadWatcher::new(&server).unwrap();
429
430 let debug = format!("{:?}", watcher);
431 assert!(debug.contains("HotReloadWatcher"));
432 }
433
434 #[test]
435 fn test_is_send() {
436 fn requires_send<T: Send>() {}
437 requires_send::<HotReloadWatcher>();
438 }
439}