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::path::{Path, PathBuf};
10
use std::pin::Pin;
11
use std::sync::Arc;
12
use std::task::{Context, Poll};
13

            
14
use tor_rtcompat::Runtime;
15

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

            
21
use futures::{SinkExt as _, 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
pub struct FileWatcher {
60
    /// An underlying `notify` watcher that tells us about directory changes.
61
    // this field is kept only so the watcher is not dropped
62
    #[getter(skip)]
63
    _watcher: NotifyWatcher,
64
    /// The list of directories that we're currently watching.
65
    watching_dirs: HashSet<PathBuf>,
66
}
67

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

            
75
/// Event possibly triggering a configuration reload
76
#[derive(Debug, Clone, PartialEq)]
77
#[non_exhaustive]
78
pub enum Event {
79
    /// Some files may have been modified.
80
    FileChanged,
81
    /// Some filesystem events may have been missed.
82
    Rescan,
83
}
84

            
85
/// Builder used to configure a [`FileWatcher`] before it starts watching for changes.
86
pub struct FileWatcherBuilder<R: Runtime> {
87
    /// The runtime.
88
    runtime: R,
89
    /// The list of directories that we're currently watching.
90
    ///
91
    /// Each directory has a set of filters that indicates whether a given notify::Event
92
    /// is relevant or not.
93
    watching_dirs: HashMap<PathBuf, HashSet<DirEventFilter>>,
94
}
95

            
96
/// A filter for deciding what to do with a notify::Event pertaining
97
/// to files that are relative to one of the directories we are watching.
98
///
99
// Private, as this is an implementation detail.
100
// If/when we decide to make this public, this might need revisiting.
101
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
102
enum DirEventFilter {
103
    /// Notify the caller about the event, if the file has the specified extension.
104
    MatchesExtension(String),
105
    /// Notify the caller about the event, if the file has the specified path.
106
    MatchesPath(PathBuf),
107
}
108

            
109
impl DirEventFilter {
110
    /// Check whether this filter accepts `path`.
111
2838
    fn accepts_path(&self, path: &Path) -> bool {
112
2838
        match self {
113
1232
            DirEventFilter::MatchesExtension(ext) => path
114
1232
                .extension()
115
1243
                .and_then(|ext| ext.to_str())
116
1243
                .map(|e| e == ext.as_str())
117
1232
                .unwrap_or_default(),
118
1606
            DirEventFilter::MatchesPath(p) => p == path,
119
        }
120
2838
    }
121
}
122

            
123
impl<R: Runtime> FileWatcherBuilder<R> {
124
    /// Create a `FileWatcherBuilder`
125
18
    pub fn new(runtime: R) -> Self {
126
18
        FileWatcherBuilder {
127
18
            runtime,
128
18
            watching_dirs: HashMap::new(),
129
18
        }
130
18
    }
131

            
132
    /// Add a single path to the list of things to watch.
133
    ///
134
    /// The event receiver will be notified if the path is created, modified, renamed, or removed.
135
    ///
136
    /// If the path is a directory, its contents will **not** be watched.
137
    /// To watch the contents of a directory, use [`watch_dir`](FileWatcherBuilder::watch_dir).
138
    ///
139
    /// Idempotent: does nothing if we're already watching that path.
140
24
    pub fn watch_path<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
141
24
        self.watch_just_parents(path.as_ref())?;
142
24
        Ok(())
143
24
    }
144

            
145
    /// Add a directory (but not any subdirs) to the list of things to watch.
146
    ///
147
    /// The event receiver will be notified whenever a file with the specified `extension`
148
    /// is created within this directory, or if an existing file with this extension
149
    /// is modified, renamed, or removed.
150
    /// Changes to files that have a different extension are ignored.
151
    ///
152
    /// Idempotent.
153
14
    pub fn watch_dir<P: AsRef<Path>, S: AsRef<str>>(
154
14
        &mut self,
155
14
        path: P,
156
14
        extension: S,
157
14
    ) -> Result<()> {
158
14
        let path = self.watch_just_parents(path.as_ref())?;
159
14
        self.watch_just_abs_dir(
160
14
            &path,
161
14
            DirEventFilter::MatchesExtension(extension.as_ref().into()),
162
14
        );
163
14
        Ok(())
164
14
    }
165

            
166
    /// Add the parents of `path` to the list of things to watch.
167
    ///
168
    /// Returns the absolute path of `path`.
169
    ///
170
    /// Idempotent.
171
38
    fn watch_just_parents(&mut self, path: &Path) -> Result<PathBuf> {
172
        // Make the path absolute (without necessarily making it canonical).
173
        //
174
        // We do this because `notify` reports all of its events in terms of
175
        // absolute paths, so if we were to tell it to watch a directory by its
176
        // relative path, we'd get reports about the absolute paths of the files
177
        // in that directory.
178
38
        let cwd = std::env::current_dir()
179
38
            .map_err(|e| FileWatcherBuildError::CurrentDirectory(Arc::new(e)))?;
180
38
        let path = cwd.join(path);
181
38
        debug_assert!(path.is_absolute());
182

            
183
        // See what directory we should watch in order to watch this file.
184
38
        let watch_target = match path.parent() {
185
            // The file has a parent, so watch that.
186
38
            Some(parent) => parent,
187
            // The file has no parent.  Given that it's absolute, that means
188
            // that we're looking at the root directory.  There's nowhere to go
189
            // "up" from there.
190
            None => path.as_ref(),
191
        };
192

            
193
        // Note this file as one that we're watching, so that we can see changes
194
        // to it later on.
195
38
        self.watch_just_abs_dir(watch_target, DirEventFilter::MatchesPath(path.clone()));
196
38

            
197
38
        Ok(path)
198
38
    }
199

            
200
    /// Add just this (absolute) directory to the list of things to watch.
201
    ///
202
    /// Does not watch any of the parents.
203
    ///
204
    /// Idempotent.
205
52
    fn watch_just_abs_dir(&mut self, watch_target: &Path, filter: DirEventFilter) {
206
52
        match self.watching_dirs.entry(watch_target.to_path_buf()) {
207
20
            Entry::Occupied(mut o) => {
208
20
                let _: bool = o.get_mut().insert(filter);
209
20
            }
210
32
            Entry::Vacant(v) => {
211
32
                let _ = v.insert(HashSet::from([filter]));
212
32
            }
213
        }
214
52
    }
215

            
216
    /// Build a `FileWatcher` and start sending events to `tx`.
217
    ///
218
    /// On startup, the watcher sends a [`Rescan`](Event::Rescan) event.
219
    /// This helps mitigate the event loss that occurs if the watched files are modified between
220
    /// the time they are initially loaded and the time when the watcher is set up.
221
18
    pub fn start_watching(self, tx: FileEventSender) -> Result<FileWatcher> {
222
18
        let runtime = self.runtime;
223
18
        let watching_dirs = self.watching_dirs.clone();
224
130
        let event_sender = move |event: notify::Result<notify::Event>| {
225
130
            let event = handle_event(event, &watching_dirs);
226
130
            if let Some(event) = event {
227
36
                // It *should* be alright to block_on():
228
36
                //   * on all platforms, the `RecommendedWatcher`'s event_handler is called from a
229
36
                //     separate thread
230
36
                //   * notify's own async_monitor example uses block_on() to run async code in the
231
36
                //     event handler
232
36
                runtime.block_on(async {
233
36
                    let _ = tx.0.lock().await.send(event).await;
234
36
                });
235
94
            }
236
130
        };
237

            
238
        cfg_if::cfg_if! {
239
            if #[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))] {
240
18
                let config = notify::Config::default();
241
            } else {
242
                /// The polling frequency, for use with the `PollWatcher`.
243
                #[cfg(not(any(test, feature = "testing")))]
244
                const WATCHER_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5);
245

            
246
                #[cfg(any(test, feature = "testing"))]
247
                const WATCHER_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(10);
248

            
249
                let config = notify::Config::default()
250
                    .with_poll_interval(WATCHER_POLL_INTERVAL);
251

            
252
                // When testing, compare the contents of the files too, not just their mtime
253
                // Otherwise, because the polling backend detects changes based on mtime,
254
                // if the test creates/writes files too fast,
255
                // it will fail to notice changes (this can happen, for example, on a tmpfs).
256
                #[cfg(any(test, feature = "testing"))]
257
                let config = config.with_compare_contents(true);
258
            }
259
        }
260

            
261
18
        let mut watcher = NotifyWatcher::new(event_sender, config).map_err(Arc::new)?;
262

            
263
18
        let watching_dirs: HashSet<_> = self.watching_dirs.keys().cloned().collect();
264
50
        for dir in &watching_dirs {
265
32
            watcher
266
32
                .watch(dir, notify::RecursiveMode::NonRecursive)
267
32
                .map_err(Arc::new)?;
268
        }
269

            
270
18
        Ok(FileWatcher {
271
18
            _watcher: watcher,
272
18
            watching_dirs,
273
18
        })
274
18
    }
275
}
276

            
277
/// Map a `notify` event to the [`Event`] type returned by [`FileWatcher`].
278
2878
fn handle_event(
279
2878
    event: notify::Result<notify::Event>,
280
2878
    watching_dirs: &HashMap<PathBuf, HashSet<DirEventFilter>>,
281
2878
) -> Option<Event> {
282
2926
    let watching = |f: &PathBuf| {
283
1284
        // For paths with no parent (i.e. root), the watcher is added for the path itself,
284
1284
        // so we do the same here.
285
1284
        let parent = f.parent().unwrap_or_else(|| f.as_ref());
286
1284

            
287
1284
        // Find the filters that apply to this directory
288
1284
        match watching_dirs
289
1284
            .iter()
290
1292
            .find_map(|(dir, filters)| (dir == parent).then_some(filters))
291
        {
292
1274
            Some(filters) => {
293
1274
                // This event is interesting, if any of the filters apply.
294
2838
                filters.iter().any(|filter| filter.accepts_path(f.as_ref()))
295
            }
296
10
            None => false,
297
        }
298
1284
    };
299

            
300
    // filter events we don't want and map to event code
301
2878
    match event {
302
2874
        Ok(event) => {
303
2874
            if event.need_rescan() {
304
16
                Some(Event::Rescan)
305
2858
            } else if ignore_event_kind(&event.kind) {
306
1790
                None
307
1068
            } else if event.paths.iter().any(watching) {
308
504
                Some(Event::FileChanged)
309
            } else {
310
564
                None
311
            }
312
        }
313
4
        Err(error) => {
314
4
            if error.paths.iter().any(watching) {
315
2
                Some(Event::FileChanged)
316
            } else {
317
2
                None
318
            }
319
        }
320
    }
321
2878
}
322

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

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

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

            
349
impl Stream for FileEventReceiver {
350
    type Item = Event;
351

            
352
3766
    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
353
3766
        self.0.poll_next_unpin(cx)
354
3766
    }
355
}
356

            
357
impl FileEventReceiver {
358
    /// Try to read a message from the stream, without blocking.
359
    ///
360
    /// Returns `Some` if a message is ready.
361
    /// Returns `None` if the stream is open, but no messages are available,
362
    /// or if the stream is closed.
363
568
    pub fn try_recv(&mut self) -> Option<Event> {
364
        use postage::prelude::Stream;
365

            
366
568
        self.0.try_recv().ok()
367
568
    }
368
}
369

            
370
/// Create a new channel for use with a [`FileWatcher`].
371
//
372
// Note: the [`FileEventSender`] and [`FileEventReceiver`]  wrappers exist
373
// so we don't expose the channel's underlying type
374
// in our public API.
375
346
pub fn channel() -> (FileEventSender, FileEventReceiver) {
376
346
    let (tx, rx) = watch::channel_with(Event::Rescan);
377
346
    (
378
346
        FileEventSender(Arc::new(Mutex::new(tx))),
379
346
        FileEventReceiver(rx),
380
346
    )
381
346
}
382

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

            
394
    /// Encountered a problem while creating a `Watcher`.
395
    #[error("Problem creating Watcher")]
396
    Notify(#[from] Arc<notify::Error>),
397
}
398

            
399
#[cfg(test)]
400
mod test {
401
    // @@ begin test lint list maintained by maint/add_warning @@
402
    #![allow(clippy::bool_assert_comparison)]
403
    #![allow(clippy::clone_on_copy)]
404
    #![allow(clippy::dbg_macro)]
405
    #![allow(clippy::mixed_attributes_style)]
406
    #![allow(clippy::print_stderr)]
407
    #![allow(clippy::print_stdout)]
408
    #![allow(clippy::single_char_pattern)]
409
    #![allow(clippy::unwrap_used)]
410
    #![allow(clippy::unchecked_duration_subtraction)]
411
    #![allow(clippy::useless_vec)]
412
    #![allow(clippy::needless_pass_by_value)]
413
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
414

            
415
    use super::*;
416
    use notify::event::{AccessKind, ModifyKind};
417
    use test_temp_dir::{test_temp_dir, TestTempDir};
418

            
419
    /// Write `data` to file `name` within `dir`.
420
    fn write_file(dir: &TestTempDir, name: &str, data: &[u8]) -> PathBuf {
421
        let path = dir.as_path_untracked().join(name);
422
        std::fs::write(&path, data).unwrap();
423
        path
424
    }
425

            
426
    /// Return an event that has the Rescan flag set
427
    fn rescan_event() -> notify::Event {
428
        let event = notify::Event::new(notify::EventKind::Any);
429
        event.set_flag(notify::event::Flag::Rescan)
430
    }
431

            
432
    /// Assert that at least one FileChanged event is received.
433
    async fn assert_file_changed(rx: &mut FileEventReceiver) {
434
        assert_eq!(rx.next().await, Some(Event::FileChanged));
435

            
436
        // The write might trigger more than one event
437
        while let Some(ev) = rx.try_recv() {
438
            assert_eq!(ev, Event::FileChanged);
439
        }
440
    }
441

            
442
    /// Set the `EventKind` of `event` to an uninteresting `EventKind`
443
    /// and assert that it is ignored by `handle_event`.
444
    fn assert_ignored(event: &notify::Event, watching: &HashMap<PathBuf, HashSet<DirEventFilter>>) {
445
        for kind in [EventKind::Access(AccessKind::Any), EventKind::Other] {
446
            let ignored_event = event.clone().set_kind(kind);
447
            assert_eq!(handle_event(Ok(ignored_event.clone()), watching), None);
448
            // ...but if the rescan flag is set, the event is *not* ignored
449
            let event = ignored_event.set_flag(notify::event::Flag::Rescan);
450
            assert_eq!(handle_event(Ok(event), watching), Some(Event::Rescan));
451
        }
452
    }
453

            
454
    #[test]
455
    fn notify_event_handler() {
456
        let mut event = notify::Event::new(notify::EventKind::Modify(ModifyKind::Any));
457

            
458
        let mut watching_dirs = Default::default();
459
        assert_eq!(handle_event(Ok(event.clone()), &watching_dirs), None);
460
        assert_eq!(
461
            handle_event(Ok(rescan_event()), &watching_dirs),
462
            Some(Event::Rescan)
463
        );
464

            
465
        // Watch some directories
466
        watching_dirs.insert(
467
            "/foo/baz".into(),
468
            HashSet::from([DirEventFilter::MatchesExtension("auth".into())]),
469
        );
470
        assert_eq!(handle_event(Ok(event.clone()), &watching_dirs), None);
471
        assert_eq!(
472
            handle_event(Ok(rescan_event()), &watching_dirs),
473
            Some(Event::Rescan)
474
        );
475

            
476
        event = event.add_path("/foo/bar/alice.authh".into());
477
        assert_eq!(handle_event(Ok(event.clone()), &watching_dirs), None);
478

            
479
        event = event.add_path("/foo/bar/alice.auth".into());
480
        assert_eq!(handle_event(Ok(event.clone()), &watching_dirs), None);
481

            
482
        event = event.add_path("/foo/baz/bob.auth".into());
483
        assert_eq!(
484
            handle_event(Ok(event.clone()), &watching_dirs),
485
            Some(Event::FileChanged)
486
        );
487

            
488
        // The same event, but with an irrelevant kind, gets ignored:
489
        assert_ignored(&event, &watching_dirs);
490

            
491
        // Watch some files within /foo/bar
492
        watching_dirs.insert(
493
            "/foo/bar".into(),
494
            HashSet::from([DirEventFilter::MatchesPath("/foo/bar/abc".into())]),
495
        );
496

            
497
        assert_eq!(
498
            handle_event(Ok(event.clone()), &watching_dirs),
499
            Some(Event::FileChanged)
500
        );
501
        assert_eq!(
502
            handle_event(Ok(rescan_event()), &watching_dirs),
503
            Some(Event::Rescan)
504
        );
505

            
506
        // The same event, but with an irrelevant kind, gets ignored:
507
        assert_ignored(&event, &watching_dirs);
508

            
509
        // Watch some other files
510
        let event = notify::Event::new(notify::EventKind::Modify(ModifyKind::Any))
511
            .add_path("/a/b/c/d".into());
512
        let watching_dirs = [(
513
            "/a/b/c/".into(),
514
            HashSet::from([DirEventFilter::MatchesPath("/a/b/c/d".into())]),
515
        )]
516
        .into_iter()
517
        .collect();
518
        assert_eq!(
519
            handle_event(Ok(event), &watching_dirs),
520
            Some(Event::FileChanged)
521
        );
522
        assert_eq!(
523
            handle_event(Ok(rescan_event()), &watching_dirs),
524
            Some(Event::Rescan)
525
        );
526

            
527
        // Errors can also trigger an event
528
        let err = notify::Error::path_not_found();
529
        assert_eq!(handle_event(Err(err), &watching_dirs), None);
530
        let mut err = notify::Error::path_not_found();
531
        err = err.add_path("/a/b/c/d".into());
532
        assert_eq!(
533
            handle_event(Err(err), &watching_dirs),
534
            Some(Event::FileChanged)
535
        );
536
    }
537

            
538
    #[test]
539
    fn watch_dirs() {
540
        tor_rtcompat::test_with_one_runtime!(|rt| async move {
541
            let temp_dir = test_temp_dir!();
542
            let (tx, mut rx) = channel();
543
            // Watch for changes in .foo files from temp_dir
544
            let mut builder = FileWatcher::builder(rt.clone());
545
            builder
546
                .watch_dir(temp_dir.as_path_untracked(), "foo")
547
                .unwrap();
548
            let watcher = builder.start_watching(tx).unwrap();
549

            
550
            // On startup, the watcher sends a Event::Rescan event.
551
            // This is because the watcher is often set up after loading
552
            // the files or directories it is watching.
553
            assert_eq!(rx.try_recv(), Some(Event::Rescan));
554
            assert_eq!(rx.try_recv(), None);
555

            
556
            // Write a file with extension "foo".
557
            write_file(&temp_dir, "bar.foo", b"hello");
558

            
559
            assert_eq!(rx.next().await, Some(Event::FileChanged));
560

            
561
            drop(watcher);
562
            // The write might trigger more than one event
563
            while let Some(ev) = rx.next().await {
564
                assert_eq!(ev.clone(), Event::FileChanged);
565
            }
566
        });
567
    }
568

            
569
    #[test]
570
    fn watch_file_path() {
571
        tor_rtcompat::test_with_one_runtime!(|rt| async move {
572
            let temp_dir = test_temp_dir!();
573
            let (tx, mut rx) = channel();
574
            // Watch for changes to hello.txt
575
            let path = write_file(&temp_dir, "hello.txt", b"hello");
576
            let mut builder = FileWatcher::builder(rt.clone());
577
            builder.watch_path(&path).unwrap();
578
            let _watcher = builder.start_watching(tx).unwrap();
579

            
580
            // On startup, the watcher sends a Event::Rescan event.
581
            assert_eq!(rx.try_recv(), Some(Event::Rescan));
582
            assert_eq!(rx.try_recv(), None);
583

            
584
            // Write to hello.txt
585
            let _: PathBuf = write_file(&temp_dir, "hello.txt", b"good-bye");
586

            
587
            assert_file_changed(&mut rx).await;
588

            
589
            // Remove hello.txt
590
            std::fs::remove_file(&path).unwrap();
591
            assert_file_changed(&mut rx).await;
592

            
593
            // Create a new file
594
            let tmp_hello = write_file(&temp_dir, "hello.tmp", b"new hello");
595
            // Copy it over to the watched hello.txt location
596
            std::fs::rename(&tmp_hello, &path).unwrap();
597
            assert_file_changed(&mut rx).await;
598
        });
599
    }
600

            
601
    #[test]
602
    fn watch_dir_path() {
603
        tor_rtcompat::test_with_one_runtime!(|rt| async move {
604
            let temp_dir1 = tempfile::TempDir::new().unwrap();
605
            let (tx, mut rx) = channel();
606
            // Watch temp_dir for changes
607
            let mut builder = FileWatcher::builder(rt.clone());
608
            builder.watch_path(temp_dir1.path()).unwrap();
609

            
610
            let _watcher = builder.start_watching(tx).unwrap();
611

            
612
            // On startup, the watcher sends a Event::Rescan event.
613
            assert_eq!(rx.try_recv(), Some(Event::Rescan));
614
            assert_eq!(rx.try_recv(), None);
615

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

            
620
            // Move temp_dir1 to temp_dir2
621
            let temp_dir2 = tempfile::TempDir::new().unwrap();
622
            std::fs::rename(&temp_dir1, &temp_dir2).unwrap();
623

            
624
            // Moving the directory triggers an event...
625
            assert_file_changed(&mut rx).await;
626
            // ...and so does moving it back to its original location
627
            std::fs::rename(&temp_dir2, &temp_dir1).unwrap();
628
            assert_file_changed(&mut rx).await;
629
        });
630
    }
631
}