1use std::collections::hash_map::Entry;
7use std::collections::{HashMap, HashSet};
8use std::io;
9use std::marker::PhantomData;
10use std::path::{Path, PathBuf};
11use std::pin::Pin;
12use std::sync::{Arc, Mutex};
13use std::task::{Context, Poll};
14
15use tor_rtcompat::Runtime;
16
17use amplify::Getters;
18use notify::{EventKind, Watcher};
19use postage::watch;
20
21use futures::{Stream, StreamExt as _};
22
23pub type Result<T> = std::result::Result<T, FileWatcherBuildError>;
25
26cfg_if::cfg_if! {
27 if #[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))] {
28 type NotifyWatcher = notify::RecommendedWatcher;
30 } else {
31 type NotifyWatcher = notify::PollWatcher;
33 }
34}
35
36#[derive(Getters)]
59#[must_use = "A dropped FileWatcher exits immediately"]
60pub struct FileWatcher {
61 #[getter(skip)]
64 _watcher: NotifyWatcher,
65 watching_dirs: HashSet<PathBuf>,
67}
68
69impl FileWatcher {
70 pub fn builder<R: Runtime>(runtime: R) -> FileWatcherBuilder<R> {
72 FileWatcherBuilder::new(runtime)
73 }
74}
75
76#[derive(Debug, Clone, PartialEq)]
84#[non_exhaustive]
85pub enum Event {
86 FileChanged,
91 Rescan,
93}
94
95pub struct FileWatcherBuilder<R: Runtime> {
97 #[allow(dead_code)]
102 runtime: PhantomData<R>,
103 watching_dirs: HashMap<PathBuf, HashSet<DirEventFilter>>,
108}
109
110#[derive(Clone, Debug, Hash, PartialEq, Eq)]
116enum DirEventFilter {
117 MatchesExtension(String),
119 MatchesPath(PathBuf),
121}
122
123impl DirEventFilter {
124 fn accepts_path(&self, path: &Path) -> bool {
126 match self {
127 DirEventFilter::MatchesExtension(ext) => path
128 .extension()
129 .and_then(|ext| ext.to_str())
130 .map(|e| e == ext.as_str())
131 .unwrap_or_default(),
132 DirEventFilter::MatchesPath(p) => p == path,
133 }
134 }
135}
136
137impl<R: Runtime> FileWatcherBuilder<R> {
138 pub fn new(_runtime: R) -> Self {
140 FileWatcherBuilder {
141 runtime: PhantomData,
142 watching_dirs: HashMap::new(),
143 }
144 }
145
146 pub fn watch_path<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
155 self.watch_just_parents(path.as_ref())?;
156 Ok(())
157 }
158
159 pub fn watch_dir<P: AsRef<Path>, S: AsRef<str>>(
168 &mut self,
169 path: P,
170 extension: S,
171 ) -> Result<()> {
172 let path = self.watch_just_parents(path.as_ref())?;
173 self.watch_just_abs_dir(
174 &path,
175 DirEventFilter::MatchesExtension(extension.as_ref().into()),
176 );
177 Ok(())
178 }
179
180 fn watch_just_parents(&mut self, path: &Path) -> Result<PathBuf> {
186 let cwd = std::env::current_dir()
193 .map_err(|e| FileWatcherBuildError::CurrentDirectory(Arc::new(e)))?;
194 let path = cwd.join(path);
195 debug_assert!(path.is_absolute());
196
197 let watch_target = match path.parent() {
199 Some(parent) => parent,
201 None => path.as_ref(),
205 };
206
207 self.watch_just_abs_dir(watch_target, DirEventFilter::MatchesPath(path.clone()));
210
211 Ok(path)
212 }
213
214 fn watch_just_abs_dir(&mut self, watch_target: &Path, filter: DirEventFilter) {
220 match self.watching_dirs.entry(watch_target.to_path_buf()) {
221 Entry::Occupied(mut o) => {
222 let _: bool = o.get_mut().insert(filter);
223 }
224 Entry::Vacant(v) => {
225 let _ = v.insert(HashSet::from([filter]));
226 }
227 }
228 }
229
230 pub fn start_watching(self, tx: FileEventSender) -> Result<FileWatcher> {
236 let watching_dirs = self.watching_dirs.clone();
237 let event_sender = move |event: notify::Result<notify::Event>| {
238 let event = handle_event(event, &watching_dirs);
239 if let Some(event) = event {
240 *tx.0.lock().expect("poisoned").borrow_mut() = event;
242 }
243 };
244
245 cfg_if::cfg_if! {
246 if #[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))] {
247 let config = notify::Config::default();
248 } else {
249 #[cfg(not(any(test, feature = "testing")))]
251 const WATCHER_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5);
252
253 #[cfg(any(test, feature = "testing"))]
254 const WATCHER_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(10);
255
256 let config = notify::Config::default()
257 .with_poll_interval(WATCHER_POLL_INTERVAL);
258
259 #[cfg(any(test, feature = "testing"))]
264 let config = config.with_compare_contents(true);
265 }
266 }
267
268 let mut watcher = NotifyWatcher::new(event_sender, config).map_err(Arc::new)?;
269
270 let watching_dirs: HashSet<_> = self.watching_dirs.keys().cloned().collect();
271 for dir in &watching_dirs {
272 watcher
273 .watch(dir, notify::RecursiveMode::NonRecursive)
274 .map_err(Arc::new)?;
275 }
276
277 Ok(FileWatcher {
278 _watcher: watcher,
279 watching_dirs,
280 })
281 }
282}
283
284fn handle_event(
286 event: notify::Result<notify::Event>,
287 watching_dirs: &HashMap<PathBuf, HashSet<DirEventFilter>>,
288) -> Option<Event> {
289 let watching = |f: &PathBuf| {
290 let parent = f.parent().unwrap_or_else(|| f.as_ref());
293
294 match watching_dirs
296 .iter()
297 .find_map(|(dir, filters)| (dir == parent).then_some(filters))
298 {
299 Some(filters) => {
300 filters.iter().any(|filter| filter.accepts_path(f.as_ref()))
302 }
303 None => false,
304 }
305 };
306
307 match event {
309 Ok(event) => {
310 if event.need_rescan() {
311 Some(Event::Rescan)
312 } else if ignore_event_kind(&event.kind) {
313 None
314 } else if event.paths.iter().any(watching) {
315 Some(Event::FileChanged)
316 } else {
317 None
318 }
319 }
320 Err(error) => {
321 if error.paths.iter().any(watching) {
322 Some(Event::FileChanged)
323 } else {
324 None
325 }
326 }
327 }
328}
329
330fn ignore_event_kind(kind: &EventKind) -> bool {
337 use EventKind::*;
338 matches!(kind, Access(_) | Any | Other)
339}
340
341#[derive(Clone)]
350pub struct FileEventSender(Arc<Mutex<watch::Sender<Event>>>);
351
352#[derive(Clone)]
354pub struct FileEventReceiver(watch::Receiver<Event>);
355
356impl Stream for FileEventReceiver {
357 type Item = Event;
358
359 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
360 self.0.poll_next_unpin(cx)
361 }
362}
363
364impl FileEventReceiver {
365 pub fn try_recv(&mut self) -> Option<Event> {
371 use postage::prelude::Stream;
372
373 self.0.try_recv().ok()
374 }
375}
376
377pub fn channel() -> (FileEventSender, FileEventReceiver) {
383 let (tx, rx) = watch::channel_with(Event::Rescan);
384 (
385 FileEventSender(Arc::new(Mutex::new(tx))),
386 FileEventReceiver(rx),
387 )
388}
389
390#[derive(Debug, Clone, thiserror::Error)]
392#[non_exhaustive]
393pub enum FileWatcherBuildError {
394 #[error("Invalid current working directory")]
399 CurrentDirectory(#[source] Arc<io::Error>),
400
401 #[error("Problem creating Watcher")]
403 Notify(#[from] Arc<notify::Error>),
404}
405
406#[cfg(test)]
407mod test {
408 #![allow(clippy::bool_assert_comparison)]
410 #![allow(clippy::clone_on_copy)]
411 #![allow(clippy::dbg_macro)]
412 #![allow(clippy::mixed_attributes_style)]
413 #![allow(clippy::print_stderr)]
414 #![allow(clippy::print_stdout)]
415 #![allow(clippy::single_char_pattern)]
416 #![allow(clippy::unwrap_used)]
417 #![allow(clippy::unchecked_time_subtraction)]
418 #![allow(clippy::useless_vec)]
419 #![allow(clippy::needless_pass_by_value)]
420 #![allow(clippy::string_slice)] use super::*;
424 use notify::event::{AccessKind, ModifyKind};
425 use test_temp_dir::{TestTempDir, test_temp_dir};
426
427 fn write_file(dir: &TestTempDir, name: &str, data: &[u8]) -> PathBuf {
429 let path = dir.as_path_untracked().join(name);
430 std::fs::write(&path, data).unwrap();
431 path
432 }
433
434 fn rescan_event() -> notify::Event {
436 let event = notify::Event::new(notify::EventKind::Any);
437 event.set_flag(notify::event::Flag::Rescan)
438 }
439
440 async fn assert_file_changed(rx: &mut FileEventReceiver) {
442 assert_eq!(rx.next().await, Some(Event::FileChanged));
443
444 while let Some(ev) = rx.try_recv() {
446 assert_eq!(ev, Event::FileChanged);
447 }
448 }
449
450 fn assert_ignored(event: ¬ify::Event, watching: &HashMap<PathBuf, HashSet<DirEventFilter>>) {
453 for kind in [EventKind::Access(AccessKind::Any), EventKind::Other] {
454 let ignored_event = event.clone().set_kind(kind);
455 assert_eq!(handle_event(Ok(ignored_event.clone()), watching), None);
456 let event = ignored_event.set_flag(notify::event::Flag::Rescan);
458 assert_eq!(handle_event(Ok(event), watching), Some(Event::Rescan));
459 }
460 }
461
462 #[test]
463 fn notify_event_handler() {
464 let mut event = notify::Event::new(notify::EventKind::Modify(ModifyKind::Any));
465
466 let mut watching_dirs = Default::default();
467 assert_eq!(handle_event(Ok(event.clone()), &watching_dirs), None);
468 assert_eq!(
469 handle_event(Ok(rescan_event()), &watching_dirs),
470 Some(Event::Rescan)
471 );
472
473 watching_dirs.insert(
475 "/foo/baz".into(),
476 HashSet::from([DirEventFilter::MatchesExtension("auth".into())]),
477 );
478 assert_eq!(handle_event(Ok(event.clone()), &watching_dirs), None);
479 assert_eq!(
480 handle_event(Ok(rescan_event()), &watching_dirs),
481 Some(Event::Rescan)
482 );
483
484 event = event.add_path("/foo/bar/alice.authh".into());
485 assert_eq!(handle_event(Ok(event.clone()), &watching_dirs), None);
486
487 event = event.add_path("/foo/bar/alice.auth".into());
488 assert_eq!(handle_event(Ok(event.clone()), &watching_dirs), None);
489
490 event = event.add_path("/foo/baz/bob.auth".into());
491 assert_eq!(
492 handle_event(Ok(event.clone()), &watching_dirs),
493 Some(Event::FileChanged)
494 );
495
496 assert_ignored(&event, &watching_dirs);
498
499 watching_dirs.insert(
501 "/foo/bar".into(),
502 HashSet::from([DirEventFilter::MatchesPath("/foo/bar/abc".into())]),
503 );
504
505 assert_eq!(
506 handle_event(Ok(event.clone()), &watching_dirs),
507 Some(Event::FileChanged)
508 );
509 assert_eq!(
510 handle_event(Ok(rescan_event()), &watching_dirs),
511 Some(Event::Rescan)
512 );
513
514 assert_ignored(&event, &watching_dirs);
516
517 let event = notify::Event::new(notify::EventKind::Modify(ModifyKind::Any))
519 .add_path("/a/b/c/d".into());
520 let watching_dirs = [(
521 "/a/b/c/".into(),
522 HashSet::from([DirEventFilter::MatchesPath("/a/b/c/d".into())]),
523 )]
524 .into_iter()
525 .collect();
526 assert_eq!(
527 handle_event(Ok(event), &watching_dirs),
528 Some(Event::FileChanged)
529 );
530 assert_eq!(
531 handle_event(Ok(rescan_event()), &watching_dirs),
532 Some(Event::Rescan)
533 );
534
535 let err = notify::Error::path_not_found();
537 assert_eq!(handle_event(Err(err), &watching_dirs), None);
538 let mut err = notify::Error::path_not_found();
539 err = err.add_path("/a/b/c/d".into());
540 assert_eq!(
541 handle_event(Err(err), &watching_dirs),
542 Some(Event::FileChanged)
543 );
544 }
545
546 #[test]
547 fn watch_dirs() {
548 tor_rtcompat::test_with_one_runtime!(|rt| async move {
549 let temp_dir = test_temp_dir!();
550 let (tx, mut rx) = channel();
551 let mut builder = FileWatcher::builder(rt.clone());
553 builder
554 .watch_dir(temp_dir.as_path_untracked(), "foo")
555 .unwrap();
556 let watcher = builder.start_watching(tx).unwrap();
557
558 assert_eq!(rx.try_recv(), Some(Event::Rescan));
562 assert_eq!(rx.try_recv(), None);
563
564 write_file(&temp_dir, "bar.foo", b"hello");
566
567 assert_eq!(rx.next().await, Some(Event::FileChanged));
568
569 drop(watcher);
570 while let Some(ev) = rx.next().await {
572 assert_eq!(ev.clone(), Event::FileChanged);
573 }
574 });
575 }
576
577 #[test]
578 fn watch_file_path() {
579 tor_rtcompat::test_with_one_runtime!(|rt| async move {
580 let temp_dir = test_temp_dir!();
581 let (tx, mut rx) = channel();
582 let path = write_file(&temp_dir, "hello.txt", b"hello");
584 let mut builder = FileWatcher::builder(rt.clone());
585 builder.watch_path(&path).unwrap();
586 let _watcher = builder.start_watching(tx).unwrap();
587
588 assert_eq!(rx.try_recv(), Some(Event::Rescan));
590 assert_eq!(rx.try_recv(), None);
591
592 let _: PathBuf = write_file(&temp_dir, "hello.txt", b"good-bye");
594
595 assert_file_changed(&mut rx).await;
596
597 std::fs::remove_file(&path).unwrap();
599 assert_file_changed(&mut rx).await;
600
601 let tmp_hello = write_file(&temp_dir, "hello.tmp", b"new hello");
603 std::fs::rename(&tmp_hello, &path).unwrap();
605 assert_file_changed(&mut rx).await;
606 });
607 }
608
609 #[test]
610 fn watch_dir_path() {
611 tor_rtcompat::test_with_one_runtime!(|rt| async move {
612 let temp_dir1 = tempfile::TempDir::new().unwrap();
613 let (tx, mut rx) = channel();
614 let mut builder = FileWatcher::builder(rt.clone());
616 builder.watch_path(temp_dir1.path()).unwrap();
617
618 let _watcher = builder.start_watching(tx).unwrap();
619
620 assert_eq!(rx.try_recv(), Some(Event::Rescan));
622 assert_eq!(rx.try_recv(), None);
623
624 std::fs::write(temp_dir1.path().join("hello.txt"), b"hello").unwrap();
626 assert_eq!(rx.try_recv(), None);
627
628 let temp_dir2 = tempfile::TempDir::new().unwrap();
630 std::fs::rename(&temp_dir1, &temp_dir2).unwrap();
631
632 assert_file_changed(&mut rx).await;
634 std::fs::rename(&temp_dir2, &temp_dir1).unwrap();
636 assert_file_changed(&mut rx).await;
637 });
638 }
639}