Skip to content

Commit d4fa61e

Browse files
committed
Debounce file change events
1 parent 554301b commit d4fa61e

File tree

2 files changed

+82
-17
lines changed

2 files changed

+82
-17
lines changed

src/watch.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,11 @@ fn run_watch(
6969
// Prevent dropping the guard until the end of the function.
7070
// Otherwise, the file watcher exits.
7171
let _watcher_guard = if let Some(exercise_names) = notify_exercise_names {
72+
let notify_event_handler =
73+
NotifyEventHandler::build(watch_event_sender.clone(), exercise_names)?;
74+
7275
let mut watcher = RecommendedWatcher::new(
73-
NotifyEventHandler {
74-
sender: watch_event_sender.clone(),
75-
exercise_names,
76-
},
76+
notify_event_handler,
7777
Config::default().with_poll_interval(Duration::from_secs(1)),
7878
)
7979
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;

src/watch/notify_event.rs

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,71 @@
1+
use anyhow::{Context, Result};
12
use notify::{
2-
event::{MetadataKind, ModifyKind},
3+
event::{AccessKind, AccessMode, MetadataKind, ModifyKind, RenameMode},
34
Event, EventKind,
45
};
5-
use std::sync::{atomic::Ordering::Relaxed, mpsc::Sender};
6+
use std::{
7+
sync::{
8+
atomic::Ordering::Relaxed,
9+
mpsc::{sync_channel, RecvTimeoutError, Sender, SyncSender},
10+
},
11+
thread,
12+
time::Duration,
13+
};
614

715
use super::{WatchEvent, EXERCISE_RUNNING};
816

17+
const DEBOUNCE_DURATION: Duration = Duration::from_millis(200);
18+
919
pub struct NotifyEventHandler {
10-
pub sender: Sender<WatchEvent>,
11-
/// Used to report which exercise was modified.
12-
pub exercise_names: &'static [&'static [u8]],
20+
error_sender: Sender<WatchEvent>,
21+
// Sends the index of the updated exercise.
22+
update_sender: SyncSender<usize>,
23+
// Used to report which exercise was modified.
24+
exercise_names: &'static [&'static [u8]],
25+
}
26+
27+
impl NotifyEventHandler {
28+
pub fn build(
29+
watch_event_sender: Sender<WatchEvent>,
30+
exercise_names: &'static [&'static [u8]],
31+
) -> Result<Self> {
32+
let (update_sender, update_receiver) = sync_channel(0);
33+
let error_sender = watch_event_sender.clone();
34+
35+
// Debouncer
36+
thread::Builder::new()
37+
.spawn(move || {
38+
let mut exercise_updated = vec![false; exercise_names.len()];
39+
40+
loop {
41+
match update_receiver.recv_timeout(DEBOUNCE_DURATION) {
42+
Ok(exercise_ind) => exercise_updated[exercise_ind] = true,
43+
Err(RecvTimeoutError::Timeout) => {
44+
for (exercise_ind, updated) in exercise_updated.iter_mut().enumerate() {
45+
if *updated {
46+
if watch_event_sender
47+
.send(WatchEvent::FileChange { exercise_ind })
48+
.is_err()
49+
{
50+
break;
51+
}
52+
53+
*updated = false;
54+
}
55+
}
56+
}
57+
Err(RecvTimeoutError::Disconnected) => break,
58+
}
59+
}
60+
})
61+
.context("Failed to spawn a thread to debounce file changes")?;
62+
63+
Ok(Self {
64+
error_sender,
65+
update_sender,
66+
exercise_names,
67+
})
68+
}
1369
}
1470

1571
impl notify::EventHandler for NotifyEventHandler {
@@ -22,8 +78,8 @@ impl notify::EventHandler for NotifyEventHandler {
2278
Ok(v) => v,
2379
Err(e) => {
2480
// An error occurs when the receiver is dropped.
25-
// After dropping the receiver, the debouncer guard should also be dropped.
26-
let _ = self.sender.send(WatchEvent::NotifyErr(e));
81+
// After dropping the receiver, the watcher guard should also be dropped.
82+
let _ = self.error_sender.send(WatchEvent::NotifyErr(e));
2783
return;
2884
}
2985
};
@@ -32,6 +88,10 @@ impl notify::EventHandler for NotifyEventHandler {
3288
EventKind::Any => (),
3389
EventKind::Modify(modify_kind) => match modify_kind {
3490
ModifyKind::Any | ModifyKind::Data(_) => (),
91+
ModifyKind::Name(rename_mode) => match rename_mode {
92+
RenameMode::Any | RenameMode::To => (),
93+
RenameMode::From | RenameMode::Both | RenameMode::Other => return,
94+
},
3595
ModifyKind::Metadata(metadata_kind) => match metadata_kind {
3696
MetadataKind::Any | MetadataKind::WriteTime => (),
3797
MetadataKind::AccessTime
@@ -40,12 +100,17 @@ impl notify::EventHandler for NotifyEventHandler {
40100
| MetadataKind::Extended
41101
| MetadataKind::Other => return,
42102
},
43-
ModifyKind::Name(_) | ModifyKind::Other => return,
103+
ModifyKind::Other => return,
104+
},
105+
EventKind::Access(access_kind) => match access_kind {
106+
AccessKind::Any => (),
107+
AccessKind::Close(access_mode) => match access_mode {
108+
AccessMode::Any | AccessMode::Write => (),
109+
AccessMode::Execute | AccessMode::Read | AccessMode::Other => return,
110+
},
111+
AccessKind::Read | AccessKind::Open(_) | AccessKind::Other => return,
44112
},
45-
EventKind::Access(_)
46-
| EventKind::Create(_)
47-
| EventKind::Remove(_)
48-
| EventKind::Other => return,
113+
EventKind::Create(_) | EventKind::Remove(_) | EventKind::Other => return,
49114
}
50115

51116
let _ = input_event
@@ -62,6 +127,6 @@ impl notify::EventHandler for NotifyEventHandler {
62127
.iter()
63128
.position(|exercise_name| *exercise_name == file_name_without_ext)
64129
})
65-
.try_for_each(|exercise_ind| self.sender.send(WatchEvent::FileChange { exercise_ind }));
130+
.try_for_each(|exercise_ind| self.update_sender.send(exercise_ind));
66131
}
67132
}

0 commit comments

Comments
 (0)