1
//! Code to watch configuration files for any changes.
2
//!
3
// TODO: perhaps this shouldn't live in tor-config? But it doesn't seem substantial enough to have
4
// its own crate, and it can't live in e.g. tor-basic-utils, because it depends on tor-rtcompat.
5

            
6
use std::collections::hash_map::Entry;
7
use std::collections::{HashMap, HashSet};
8
use std::io;
9
use std::marker::PhantomData;
10
use std::path::{Path, PathBuf};
11
use std::pin::Pin;
12
use std::sync::{Arc, Mutex};
13
use std::task::{Context, Poll};
14

            
15
use tor_rtcompat::Runtime;
16

            
17
use amplify::Getters;
18
use notify::{EventKind, Watcher};
19
use postage::watch;
20

            
21
use futures::{Stream, StreamExt as _};
22

            
23
/// `Result` whose `Err` is [`FileWatcherBuildError`].
24
pub type Result<T> = std::result::Result<T, FileWatcherBuildError>;
25

            
26
cfg_if::cfg_if! {
27
    if #[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))] {
28
        /// The concrete type of the underlying watcher.
29
        type NotifyWatcher = notify::RecommendedWatcher;
30
    } else {
31
        /// The concrete type of the underlying watcher.
32
        type NotifyWatcher = notify::PollWatcher;
33
    }
34
}
35

            
36
/// A wrapper around a `notify::Watcher` to watch a set of parent
37
/// directories in order to learn about changes in some specific files that they
38
/// contain.
39
///
40
/// The `Watcher` implementation in `notify` has a weakness: it gives sensible
41
/// results when you're watching directories, but if you start watching
42
/// non-directory files, it won't notice when those files get replaced.  That's
43
/// a problem for users who want to change their configuration atomically by
44
/// making new files and then moving them into place over the old ones.
45
///
46
/// For more background on the issues with `notify`, see
47
/// <https://github.com/notify-rs/notify/issues/165> and
48
/// <https://github.com/notify-rs/notify/pull/166>.
49
///
50
/// ## Limitations
51
///
52
/// On backends using kqueue, this uses a polling watcher
53
/// to work around a bug in the `notify` crate[^1].
54
/// This introduces a perceivable delay,
55
/// and can be very expensive for large file trees.
56
///
57
/// [^1]: See <https://github.com/notify-rs/notify/issues/644>
58
#[derive(Getters)]
59
#[must_use = "A dropped FileWatcher exits immediately"]
60
pub struct FileWatcher {
61
    /// An underlying `notify` watcher that tells us about directory changes.
62
    // this field is kept only so the watcher is not dropped
63
    #[getter(skip)]
64
    _watcher: NotifyWatcher,
65
    /// The list of directories that we're currently watching.
66
    watching_dirs: HashSet<PathBuf>,
67
}
68

            
69
impl FileWatcher {
70
    /// Create a `FileWatcherBuilder`
71
14
    pub fn builder<R: Runtime>(runtime: R) -> FileWatcherBuilder<R> {
72
14
        FileWatcherBuilder::new(runtime)
73
14
    }
74
}
75

            
76
/// Event possibly triggering a configuration reload
77
//
78
// WARNING!
79
//
80
// Simply adding new, more specific, events, to this struct, would be wrong.
81
// This is because internally, we transmit the events via a postage::watch,
82
// which means that receivers might not receive all events!
83
#[derive(Debug, Clone, PartialEq)]
84
#[non_exhaustive]
85
pub enum Event {
86
    /// Some files may have been modified.
87
    ///
88
    /// This is semantically equivalent to `Rescan`, since in neither case
89
    /// do we say *which* files may have been changed.
90
    FileChanged,
91
    /// Some filesystem events may have been missed.
92
    Rescan,
93
}
94

            
95
/// Builder used to configure a [`FileWatcher`] before it starts watching for changes.
96
pub struct FileWatcherBuilder<R: Runtime> {
97
    /// The runtime.  We used to use this.
98
    ///
99
    /// TODO get rid of this, but after we decide whether to keep using postage::watch.
100
    /// See the Warning note on Event.
101
    #[allow(dead_code)]
102
    runtime: PhantomData<R>,
103
    /// The list of directories that we're currently watching.
104
    ///
105
    /// Each directory has a set of filters that indicates whether a given notify::Event
106
    /// is relevant or not.
107
    watching_dirs: HashMap<PathBuf, HashSet<DirEventFilter>>,
108
}
109

            
110
/// A filter for deciding what to do with a notify::Event pertaining
111
/// to files that are relative to one of the directories we are watching.
112
///
113
// Private, as this is an implementation detail.
114
// If/when we decide to make this public, this might need revisiting.
115
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
116
enum DirEventFilter {
117
    /// Notify the caller about the event, if the file has the specified extension.
118
    MatchesExtension(String),
119
    /// Notify the caller about the event, if the file has the specified path.
120
    MatchesPath(PathBuf),
121
}
122

            
123
impl DirEventFilter {
124
    /// Check whether this filter accepts `path`.
125
554
    fn accepts_path(&self, path: &Path) -> bool {
126
554
        match self {
127
8
            DirEventFilter::MatchesExtension(ext) => path
128
8
                .extension()
129
12
                .and_then(|ext| ext.to_str())
130
12
                .map(|e| e == ext.as_str())
131
8
                .unwrap_or_default(),
132
546
            DirEventFilter::MatchesPath(p) => p == path,
133
        }
134
554
    }
135
}
136

            
137
impl<R: Runtime> FileWatcherBuilder<R> {
138
    /// Create a `FileWatcherBuilder`
139
14
    pub fn new(_runtime: R) -> Self {
140
14
        FileWatcherBuilder {
141
14
            runtime: PhantomData,
142
14
            watching_dirs: HashMap::new(),
143
14
        }
144
14
    }
145

            
146
    /// Add a single path to the list of things to watch.
147
    ///
148
    /// The event receiver will be notified if the path is created, modified, renamed, or removed.
149
    ///
150
    /// If the path is a directory, its contents will **not** be watched.
151
    /// To watch the contents of a directory, use [`watch_dir`](FileWatcherBuilder::watch_dir).
152
    ///
153
    /// Idempotent: does nothing if we're already watching that path.
154
12
    pub fn watch_path<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
155
12
        self.watch_just_parents(path.as_ref())?;
156
12
        Ok(())
157
12
    }
158

            
159
    /// Add a directory (but not any subdirs) to the list of things to watch.
160
    ///
161
    /// The event receiver will be notified whenever a file with the specified `extension`
162
    /// is created within this directory, or if an existing file with this extension
163
    /// is modified, renamed, or removed.
164
    /// Changes to files that have a different extension are ignored.
165
    ///
166
    /// Idempotent.
167
2
    pub fn watch_dir<P: AsRef<Path>, S: AsRef<str>>(
168
2
        &mut self,
169
2
        path: P,
170
2
        extension: S,
171
2
    ) -> Result<()> {
172
2
        let path = self.watch_just_parents(path.as_ref())?;
173
2
        self.watch_just_abs_dir(
174
2
            &path,
175
2
            DirEventFilter::MatchesExtension(extension.as_ref().into()),
176
        );
177
2
        Ok(())
178
2
    }
179

            
180
    /// Add the parents of `path` to the list of things to watch.
181
    ///
182
    /// Returns the absolute path of `path`.
183
    ///
184
    /// Idempotent.
185
14
    fn watch_just_parents(&mut self, path: &Path) -> Result<PathBuf> {
186
        // Make the path absolute (without necessarily making it canonical).
187
        //
188
        // We do this because `notify` reports all of its events in terms of
189
        // absolute paths, so if we were to tell it to watch a directory by its
190
        // relative path, we'd get reports about the absolute paths of the files
191
        // in that directory.
192
14
        let cwd = std::env::current_dir()
193
14
            .map_err(|e| FileWatcherBuildError::CurrentDirectory(Arc::new(e)))?;
194
14
        let path = cwd.join(path);
195
14
        debug_assert!(path.is_absolute());
196

            
197
        // See what directory we should watch in order to watch this file.
198
14
        let watch_target = match path.parent() {
199
            // The file has a parent, so watch that.
200
14
            Some(parent) => parent,
201
            // The file has no parent.  Given that it's absolute, that means
202
            // that we're looking at the root directory.  There's nowhere to go
203
            // "up" from there.
204
            None => path.as_ref(),
205
        };
206

            
207
        // Note this file as one that we're watching, so that we can see changes
208
        // to it later on.
209
14
        self.watch_just_abs_dir(watch_target, DirEventFilter::MatchesPath(path.clone()));
210

            
211
14
        Ok(path)
212
14
    }
213

            
214
    /// Add just this (absolute) directory to the list of things to watch.
215
    ///
216
    /// Does not watch any of the parents.
217
    ///
218
    /// Idempotent.
219
16
    fn watch_just_abs_dir(&mut self, watch_target: &Path, filter: DirEventFilter) {
220
16
        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
16
            Entry::Vacant(v) => {
225
16
                let _ = v.insert(HashSet::from([filter]));
226
16
            }
227
        }
228
16
    }
229

            
230
    /// Build a `FileWatcher` and start sending events to `tx`.
231
    ///
232
    /// On startup, the watcher sends a [`Rescan`](Event::Rescan) event.
233
    /// This helps mitigate the event loss that occurs if the watched files are modified between
234
    /// the time they are initially loaded and the time when the watcher is set up.
235
14
    pub fn start_watching(self, tx: FileEventSender) -> Result<FileWatcher> {
236
14
        let watching_dirs = self.watching_dirs.clone();
237
74
        let event_sender = move |event: notify::Result<notify::Event>| {
238
74
            let event = handle_event(event, &watching_dirs);
239
74
            if let Some(event) = event {
240
26
                // NB!  This can lose events!  See the internal warning comment on `Event`
241
26
                *tx.0.lock().expect("poisoned").borrow_mut() = event;
242
48
            }
243
74
        };
244

            
245
        cfg_if::cfg_if! {
246
            if #[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))] {
247
14
                let config = notify::Config::default();
248
            } else {
249
                /// The polling frequency, for use with the `PollWatcher`.
250
                #[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
                // When testing, compare the contents of the files too, not just their mtime
260
                // Otherwise, because the polling backend detects changes based on mtime,
261
                // if the test creates/writes files too fast,
262
                // it will fail to notice changes (this can happen, for example, on a tmpfs).
263
                #[cfg(any(test, feature = "testing"))]
264
                let config = config.with_compare_contents(true);
265
            }
266
        }
267

            
268
14
        let mut watcher = NotifyWatcher::new(event_sender, config).map_err(Arc::new)?;
269

            
270
14
        let watching_dirs: HashSet<_> = self.watching_dirs.keys().cloned().collect();
271
16
        for dir in &watching_dirs {
272
16
            watcher
273
16
                .watch(dir, notify::RecursiveMode::NonRecursive)
274
16
                .map_err(Arc::new)?;
275
        }
276

            
277
14
        Ok(FileWatcher {
278
14
            _watcher: watcher,
279
14
            watching_dirs,
280
14
        })
281
14
    }
282
}
283

            
284
/// Map a `notify` event to the [`Event`] type returned by [`FileWatcher`].
285
970
fn handle_event(
286
970
    event: notify::Result<notify::Event>,
287
970
    watching_dirs: &HashMap<PathBuf, HashSet<DirEventFilter>>,
288
970
) -> Option<Event> {
289
1008
    let watching = |f: &PathBuf| {
290
        // For paths with no parent (i.e. root), the watcher is added for the path itself,
291
        // so we do the same here.
292
564
        let parent = f.parent().unwrap_or_else(|| f.as_ref());
293

            
294
        // Find the filters that apply to this directory
295
564
        match watching_dirs
296
564
            .iter()
297
572
            .find_map(|(dir, filters)| (dir == parent).then_some(filters))
298
        {
299
554
            Some(filters) => {
300
                // This event is interesting, if any of the filters apply.
301
554
                filters.iter().any(|filter| filter.accepts_path(f.as_ref()))
302
            }
303
10
            None => false,
304
        }
305
564
    };
306

            
307
    // filter events we don't want and map to event code
308
970
    match event {
309
966
        Ok(event) => {
310
966
            if event.need_rescan() {
311
16
                Some(Event::Rescan)
312
950
            } else if ignore_event_kind(&event.kind) {
313
461
                None
314
489
            } else if event.paths.iter().any(watching) {
315
154
                Some(Event::FileChanged)
316
            } else {
317
335
                None
318
            }
319
        }
320
4
        Err(error) => {
321
4
            if error.paths.iter().any(watching) {
322
2
                Some(Event::FileChanged)
323
            } else {
324
2
                None
325
            }
326
        }
327
    }
328
970
}
329

            
330
/// Check whether this is a kind of [`notify::Event`] that we want to ignore.
331
///
332
/// Returns `true` for
333
///   * events that trigger on non-mutating file accesses
334
///   * catch-all events (used by `notify` for unsupported/unknown events)
335
///   * "other" meta-events
336
950
fn ignore_event_kind(kind: &EventKind) -> bool {
337
    use EventKind::*;
338
950
    matches!(kind, Access(_) | Any | Other)
339
950
}
340

            
341
/// The sender half of a watch channel used by a [`FileWatcher`] for sending [`Event`]s.
342
///
343
/// For use with [`FileWatcherBuilder::start_watching`].
344
///
345
/// **Important**: to avoid contention, avoid sharing clones of the same `FileEventSender`
346
/// with multiple [`FileWatcherBuilder`]s. This type is [`Clone`] to support creating new
347
/// [`FileWatcher`]s from an existing [`channel`], which enables existing receivers to receive
348
/// events from new `FileWatcher`s (any old `FileWatcher`s are supposed to be discarded).
349
#[derive(Clone)]
350
pub struct FileEventSender(Arc<Mutex<watch::Sender<Event>>>);
351

            
352
/// The receiver half of a watch channel used for receiving [`Event`]s sent by a [`FileWatcher`].
353
#[derive(Clone)]
354
pub struct FileEventReceiver(watch::Receiver<Event>);
355

            
356
impl Stream for FileEventReceiver {
357
    type Item = Event;
358

            
359
2983
    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
360
2983
        self.0.poll_next_unpin(cx)
361
2983
    }
362
}
363

            
364
impl FileEventReceiver {
365
    /// Try to read a message from the stream, without blocking.
366
    ///
367
    /// Returns `Some` if a message is ready.
368
    /// Returns `None` if the stream is open, but no messages are available,
369
    /// or if the stream is closed.
370
402
    pub fn try_recv(&mut self) -> Option<Event> {
371
        use postage::prelude::Stream;
372

            
373
402
        self.0.try_recv().ok()
374
402
    }
375
}
376

            
377
/// Create a new channel for use with a [`FileWatcher`].
378
//
379
// Note: the [`FileEventSender`] and [`FileEventReceiver`]  wrappers exist
380
// so we don't expose the channel's underlying type
381
// in our public API.
382
321
pub fn channel() -> (FileEventSender, FileEventReceiver) {
383
321
    let (tx, rx) = watch::channel_with(Event::Rescan);
384
321
    (
385
321
        FileEventSender(Arc::new(Mutex::new(tx))),
386
321
        FileEventReceiver(rx),
387
321
    )
388
321
}
389

            
390
/// An error coming from a [`FileWatcherBuilder`].
391
#[derive(Debug, Clone, thiserror::Error)]
392
#[non_exhaustive]
393
pub enum FileWatcherBuildError {
394
    /// Invalid current working directory.
395
    ///
396
    /// This error can happen if the current directory does not exist,
397
    /// or if we don't have the necessary permissions to access it.
398
    #[error("Invalid current working directory")]
399
    CurrentDirectory(#[source] Arc<io::Error>),
400

            
401
    /// Encountered a problem while creating a `Watcher`.
402
    #[error("Problem creating Watcher")]
403
    Notify(#[from] Arc<notify::Error>),
404
}
405

            
406
#[cfg(test)]
407
mod test {
408
    // @@ begin test lint list maintained by maint/add_warning @@
409
    #![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)] // See arti#2571
421
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
422

            
423
    use super::*;
424
    use notify::event::{AccessKind, ModifyKind};
425
    use test_temp_dir::{TestTempDir, test_temp_dir};
426

            
427
    /// Write `data` to file `name` within `dir`.
428
    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
    /// Return an event that has the Rescan flag set
435
    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
    /// Assert that at least one FileChanged event is received.
441
    async fn assert_file_changed(rx: &mut FileEventReceiver) {
442
        assert_eq!(rx.next().await, Some(Event::FileChanged));
443

            
444
        // The write might trigger more than one event
445
        while let Some(ev) = rx.try_recv() {
446
            assert_eq!(ev, Event::FileChanged);
447
        }
448
    }
449

            
450
    /// Set the `EventKind` of `event` to an uninteresting `EventKind`
451
    /// and assert that it is ignored by `handle_event`.
452
    fn assert_ignored(event: &notify::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
            // ...but if the rescan flag is set, the event is *not* ignored
457
            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
        // Watch some directories
474
        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
        // The same event, but with an irrelevant kind, gets ignored:
497
        assert_ignored(&event, &watching_dirs);
498

            
499
        // Watch some files within /foo/bar
500
        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
        // The same event, but with an irrelevant kind, gets ignored:
515
        assert_ignored(&event, &watching_dirs);
516

            
517
        // Watch some other files
518
        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
        // Errors can also trigger an event
536
        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
            // Watch for changes in .foo files from temp_dir
552
            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
            // On startup, the watcher sends a Event::Rescan event.
559
            // This is because the watcher is often set up after loading
560
            // the files or directories it is watching.
561
            assert_eq!(rx.try_recv(), Some(Event::Rescan));
562
            assert_eq!(rx.try_recv(), None);
563

            
564
            // Write a file with extension "foo".
565
            write_file(&temp_dir, "bar.foo", b"hello");
566

            
567
            assert_eq!(rx.next().await, Some(Event::FileChanged));
568

            
569
            drop(watcher);
570
            // The write might trigger more than one event
571
            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
            // Watch for changes to hello.txt
583
            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
            // On startup, the watcher sends a Event::Rescan event.
589
            assert_eq!(rx.try_recv(), Some(Event::Rescan));
590
            assert_eq!(rx.try_recv(), None);
591

            
592
            // Write to hello.txt
593
            let _: PathBuf = write_file(&temp_dir, "hello.txt", b"good-bye");
594

            
595
            assert_file_changed(&mut rx).await;
596

            
597
            // Remove hello.txt
598
            std::fs::remove_file(&path).unwrap();
599
            assert_file_changed(&mut rx).await;
600

            
601
            // Create a new file
602
            let tmp_hello = write_file(&temp_dir, "hello.tmp", b"new hello");
603
            // Copy it over to the watched hello.txt location
604
            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
            // Watch temp_dir for changes
615
            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
            // On startup, the watcher sends a Event::Rescan event.
621
            assert_eq!(rx.try_recv(), Some(Event::Rescan));
622
            assert_eq!(rx.try_recv(), None);
623

            
624
            // Writing a file to this directory shouldn't trigger an event
625
            std::fs::write(temp_dir1.path().join("hello.txt"), b"hello").unwrap();
626
            assert_eq!(rx.try_recv(), None);
627

            
628
            // Move temp_dir1 to temp_dir2
629
            let temp_dir2 = tempfile::TempDir::new().unwrap();
630
            std::fs::rename(&temp_dir1, &temp_dir2).unwrap();
631

            
632
            // Moving the directory triggers an event...
633
            assert_file_changed(&mut rx).await;
634
            // ...and so does moving it back to its original location
635
            std::fs::rename(&temp_dir2, &temp_dir1).unwrap();
636
            assert_file_changed(&mut rx).await;
637
        });
638
    }
639
}