Skip to content

Commit 89c40ba

Browse files
committed
Optimize the file watcher
1 parent e56ae6d commit 89c40ba

File tree

6 files changed

+91
-108
lines changed

6 files changed

+91
-108
lines changed

Cargo.lock

Lines changed: 1 addition & 27 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ ahash = { version = "0.8.11", default-features = false }
5050
anyhow = "1.0.89"
5151
clap = { version = "4.5.17", features = ["derive"] }
5252
crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] }
53-
notify-debouncer-mini = { version = "0.4.1", default-features = false }
53+
notify = { version = "6.1.1", default-features = false, features = ["macos_fsevent"] }
5454
os_pipe = "1.2.1"
5555
rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" }
5656
serde_json = "1.0.128"

src/watch.rs

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
use anyhow::{Error, Result};
2-
use notify_debouncer_mini::{
3-
new_debouncer,
4-
notify::{self, RecursiveMode},
5-
};
2+
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
63
use std::{
74
io::{self, Write},
85
path::Path,
9-
sync::mpsc::channel,
6+
sync::{
7+
atomic::{AtomicBool, Ordering::Relaxed},
8+
mpsc::channel,
9+
},
1010
time::Duration,
1111
};
1212

@@ -21,6 +21,27 @@ mod notify_event;
2121
mod state;
2222
mod terminal_event;
2323

24+
static EXERCISE_RUNNING: AtomicBool = AtomicBool::new(false);
25+
26+
// Private unit type to force using the constructor function.
27+
#[must_use = "When the guard is dropped, the input is unpaused"]
28+
pub struct InputPauseGuard(());
29+
30+
impl InputPauseGuard {
31+
#[inline]
32+
pub fn scoped_pause() -> Self {
33+
EXERCISE_RUNNING.store(true, Relaxed);
34+
Self(())
35+
}
36+
}
37+
38+
impl Drop for InputPauseGuard {
39+
#[inline]
40+
fn drop(&mut self) {
41+
EXERCISE_RUNNING.store(false, Relaxed);
42+
}
43+
}
44+
2445
enum WatchEvent {
2546
Input(InputEvent),
2647
FileChange { exercise_ind: usize },
@@ -47,21 +68,21 @@ fn run_watch(
4768
let mut manual_run = false;
4869
// Prevent dropping the guard until the end of the function.
4970
// Otherwise, the file watcher exits.
50-
let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names {
51-
let mut debouncer = new_debouncer(
52-
Duration::from_millis(200),
71+
let _watcher_guard = if let Some(exercise_names) = notify_exercise_names {
72+
let mut watcher = RecommendedWatcher::new(
5373
NotifyEventHandler {
5474
sender: watch_event_sender.clone(),
5575
exercise_names,
5676
},
77+
Config::default().with_poll_interval(Duration::from_secs(1)),
5778
)
5879
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
59-
debouncer
60-
.watcher()
80+
81+
watcher
6182
.watch(Path::new("exercises"), RecursiveMode::Recursive)
6283
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
6384

64-
Some(debouncer)
85+
Some(watcher)
6586
} else {
6687
manual_run = true;
6788
None

src/watch/notify_event.rs

Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,67 @@
1-
use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind};
2-
use std::sync::mpsc::Sender;
1+
use notify::{
2+
event::{MetadataKind, ModifyKind},
3+
Event, EventKind,
4+
};
5+
use std::sync::{atomic::Ordering::Relaxed, mpsc::Sender};
36

4-
use super::WatchEvent;
7+
use super::{WatchEvent, EXERCISE_RUNNING};
58

69
pub struct NotifyEventHandler {
710
pub sender: Sender<WatchEvent>,
811
/// Used to report which exercise was modified.
912
pub exercise_names: &'static [&'static [u8]],
1013
}
1114

12-
impl notify_debouncer_mini::DebounceEventHandler for NotifyEventHandler {
13-
fn handle_event(&mut self, input_event: DebounceEventResult) {
14-
let output_event = match input_event {
15-
Ok(input_event) => {
16-
let Some(exercise_ind) = input_event
17-
.iter()
18-
.filter_map(|input_event| {
19-
if input_event.kind != DebouncedEventKind::Any {
20-
return None;
21-
}
22-
23-
let file_name = input_event.path.file_name()?.to_str()?.as_bytes();
24-
25-
if file_name.len() < 4 {
26-
return None;
27-
}
28-
let (file_name_without_ext, ext) = file_name.split_at(file_name.len() - 3);
29-
30-
if ext != b".rs" {
31-
return None;
32-
}
33-
34-
self.exercise_names
35-
.iter()
36-
.position(|exercise_name| *exercise_name == file_name_without_ext)
37-
})
38-
.min()
39-
else {
40-
return;
41-
};
15+
impl notify::EventHandler for NotifyEventHandler {
16+
fn handle_event(&mut self, input_event: notify::Result<Event>) {
17+
if EXERCISE_RUNNING.load(Relaxed) {
18+
return;
19+
}
4220

43-
WatchEvent::FileChange { exercise_ind }
21+
let input_event = match input_event {
22+
Ok(v) => v,
23+
Err(e) => {
24+
// 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));
27+
return;
4428
}
45-
Err(e) => WatchEvent::NotifyErr(e),
4629
};
4730

48-
// An error occurs when the receiver is dropped.
49-
// After dropping the receiver, the debouncer guard should also be dropped.
50-
let _ = self.sender.send(output_event);
31+
match input_event.kind {
32+
EventKind::Any => (),
33+
EventKind::Modify(modify_kind) => match modify_kind {
34+
ModifyKind::Any | ModifyKind::Data(_) => (),
35+
ModifyKind::Metadata(metadata_kind) => match metadata_kind {
36+
MetadataKind::Any | MetadataKind::WriteTime => (),
37+
MetadataKind::AccessTime
38+
| MetadataKind::Permissions
39+
| MetadataKind::Ownership
40+
| MetadataKind::Extended
41+
| MetadataKind::Other => return,
42+
},
43+
ModifyKind::Name(_) | ModifyKind::Other => return,
44+
},
45+
EventKind::Access(_)
46+
| EventKind::Create(_)
47+
| EventKind::Remove(_)
48+
| EventKind::Other => return,
49+
}
50+
51+
let _ = input_event
52+
.paths
53+
.into_iter()
54+
.filter_map(|path| {
55+
let file_name = path.file_name()?.to_str()?.as_bytes();
56+
57+
let [file_name_without_ext @ .., b'.', b'r', b's'] = file_name else {
58+
return None;
59+
};
60+
61+
self.exercise_names
62+
.iter()
63+
.position(|exercise_name| *exercise_name == file_name_without_ext)
64+
})
65+
.try_for_each(|exercise_ind| self.sender.send(WatchEvent::FileChange { exercise_ind }));
5166
}
5267
}

src/watch/state.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,7 @@ use crate::{
1818
term::progress_bar,
1919
};
2020

21-
use super::{
22-
terminal_event::{terminal_event_handler, InputPauseGuard},
23-
WatchEvent,
24-
};
21+
use super::{terminal_event::terminal_event_handler, InputPauseGuard, WatchEvent};
2522

2623
#[derive(PartialEq, Eq)]
2724
enum DoneStatus {

src/watch/terminal_event.rs

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,7 @@
11
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
2-
use std::sync::{
3-
atomic::{AtomicBool, Ordering::Relaxed},
4-
mpsc::Sender,
5-
};
2+
use std::sync::{atomic::Ordering::Relaxed, mpsc::Sender};
63

7-
use super::WatchEvent;
8-
9-
static INPUT_PAUSED: AtomicBool = AtomicBool::new(false);
10-
11-
// Private unit type to force using the constructor function.
12-
#[must_use = "When the guard is dropped, the input is unpaused"]
13-
pub struct InputPauseGuard(());
14-
15-
impl InputPauseGuard {
16-
#[inline]
17-
pub fn scoped_pause() -> Self {
18-
INPUT_PAUSED.store(true, Relaxed);
19-
Self(())
20-
}
21-
}
22-
23-
impl Drop for InputPauseGuard {
24-
#[inline]
25-
fn drop(&mut self) {
26-
INPUT_PAUSED.store(false, Relaxed);
27-
}
28-
}
4+
use super::{WatchEvent, EXERCISE_RUNNING};
295

306
pub enum InputEvent {
317
Run,
@@ -44,7 +20,7 @@ pub fn terminal_event_handler(sender: Sender<WatchEvent>, manual_run: bool) {
4420
KeyEventKind::Press => (),
4521
}
4622

47-
if INPUT_PAUSED.load(Relaxed) {
23+
if EXERCISE_RUNNING.load(Relaxed) {
4824
continue;
4925
}
5026

0 commit comments

Comments
 (0)