libsurfer/
file_watcher.rs
1#[cfg(not(target_arch = "wasm32"))]
2use camino::Utf8Path;
3#[cfg(all(not(windows), not(target_arch = "wasm32")))]
4use log::{error, info};
5#[cfg(all(not(windows), not(target_arch = "wasm32")))]
6use notify::Error;
7#[cfg(all(not(windows), not(target_arch = "wasm32")))]
8use notify::{event::ModifyKind, Config, Event, EventKind, RecursiveMode, Watcher};
9#[cfg(all(not(windows), not(target_arch = "wasm32")))]
10use std::time::Duration;
11
12pub struct FileWatcher {
15 #[cfg(all(not(windows), not(target_arch = "wasm32")))]
16 _inner: notify::RecommendedWatcher,
17}
18
19#[allow(dead_code)] fn is_same_file(p1: impl AsRef<std::path::Path>, p2: impl AsRef<std::path::Path>) -> bool {
24 match (
25 std::fs::canonicalize(p1.as_ref()),
26 std::fs::canonicalize(p2.as_ref()),
27 ) {
28 (Ok(p1_canon), Ok(p2_canon)) => p1_canon == p2_canon,
29 _ => false,
30 }
31}
32
33#[cfg(all(not(windows), not(target_arch = "wasm32")))]
34impl FileWatcher {
35 pub fn new<F>(path: &Utf8Path, on_change: F) -> Result<FileWatcher, Error>
39 where
40 F: Fn() + Send + Sync + 'static,
41 {
42 let std_path = path.as_std_path().to_owned();
43 let binding = std_path.clone();
44 let Some(parent) = binding.parent() else {
45 return Err(Error::new(notify::ErrorKind::PathNotFound).add_path(std_path));
46 };
47 let mut watcher = notify::RecommendedWatcher::new(
48 move |res| match res {
49 Ok(Event {
50 kind: EventKind::Modify(ModifyKind::Data(_)),
51 paths,
52 ..
53 }) => {
54 if paths.iter().any(|path| is_same_file(path, &std_path)) {
55 info!("Observed file {} was changed on disk", std_path.display());
56 on_change();
57 }
58 }
59 Ok(_) => {}
60 Err(e) => error!("Error while watching fil\n{}", e),
61 },
62 Config::default().with_poll_interval(Duration::from_secs(1)),
63 )?;
64
65 watcher.watch(parent, RecursiveMode::NonRecursive)?;
66 info!("Watching file {} for changes", binding.display());
67
68 Ok(FileWatcher { _inner: watcher })
69 }
70}
71
72#[cfg(windows)]
79impl FileWatcher {
80 pub fn new<F>(_path: &Utf8Path, _on_change: F) -> color_eyre::Result<FileWatcher>
81 where
82 F: Fn() + Send + Sync + 'static,
83 {
84 Ok(FileWatcher {})
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use crate::file_watcher::{is_same_file, FileWatcher};
92 use camino::Utf8Path;
93 use std::fs;
94 use std::fs::File;
95 #[cfg(not(windows))]
96 use std::fs::OpenOptions;
97 #[cfg(not(windows))]
98 use std::io::Write;
99 use std::path::{Path, PathBuf};
100 use std::sync::{Arc, Condvar, Mutex};
101 use std::time::Duration;
102
103 struct TempDir {
104 inner: tempfile::TempDir,
105 }
106
107 impl TempDir {
108 pub fn new() -> TempDir {
109 TempDir {
110 inner: tempfile::TempDir::new().unwrap(),
111 }
112 }
113
114 pub fn create(&self, file: &str) -> PathBuf {
115 let file_path = self.path().join(file);
116 File::create(&file_path).unwrap();
117 file_path
118 }
119
120 pub fn mkdir(&self, name: &str) -> PathBuf {
121 let file_path = self.path().join(name);
122 fs::create_dir(&file_path).unwrap();
123 file_path
124 }
125
126 pub fn path(&self) -> &Path {
127 self.inner.path()
128 }
129 }
130
131 pub struct CallbackGuard {
133 called: Mutex<bool>,
134 lock: Condvar,
135 }
136
137 impl CallbackGuard {
138 pub fn new() -> Arc<Self> {
139 Arc::new(CallbackGuard {
140 called: Mutex::new(false),
141 lock: Condvar::new(),
142 })
143 }
144
145 pub fn signal(&self) {
146 let mut guard = self.called.lock().unwrap();
147 *guard = true;
148 self.lock.notify_all();
149 }
150
151 #[cfg(not(windows))]
154 pub fn assert_called(&self) {
155 let mut started = self.called.lock().unwrap();
156 let result = self
157 .lock
158 .wait_timeout(started, Duration::from_secs(10))
159 .unwrap();
160 started = result.0;
161 if *started == true {
162 return;
164 }
165 panic!("Timeout while waiting for callback")
166 }
167
168 pub fn assert_not_called(&self) {
171 let mut started = self.called.lock().unwrap();
172 let result = self
173 .lock
174 .wait_timeout(started, Duration::from_secs(10))
175 .unwrap();
176 started = result.0;
177 if *started == true {
178 panic!("Callback was called");
179 }
180 }
181 }
182
183 #[test]
184 #[cfg(not(windows))]
185 pub fn notifies_on_change() -> Result<(), Box<dyn std::error::Error>> {
186 let tmp_dir = TempDir::new();
187 let path = tmp_dir.create("test");
188
189 let barrier = CallbackGuard::new();
190 let barrier_clone = barrier.clone();
191 let _watcher = FileWatcher::new(Utf8Path::from_path(path.as_ref()).unwrap(), move || {
192 barrier_clone.signal();
193 });
194 {
195 let mut file = OpenOptions::new().write(true).open(&path)?;
197 writeln!(file, "Changes")?;
198 }
199 barrier.assert_called();
200 Ok(())
201 }
202
203 #[test]
204 pub fn does_not_notify_on_create_and_delete() -> Result<(), Box<dyn std::error::Error>> {
205 let tmp_dir = TempDir::new();
206 let path = tmp_dir.path().join("test");
207
208 let barrier = CallbackGuard::new();
209 let barrier_clone = barrier.clone();
210
211 let _watcher = FileWatcher::new(Utf8Path::from_path(path.as_ref()).unwrap(), move || {
212 barrier_clone.signal();
213 });
214 {
215 File::create(&path)?;
217 }
218 {
219 fs::remove_file(path)?;
221 }
222 barrier.assert_not_called();
223 Ok(())
224 }
225
226 #[test]
227 #[cfg(not(windows))]
228 pub fn resolves_files_that_are_named_differently() -> Result<(), Box<dyn std::error::Error>> {
229 let tmp_dir = TempDir::new();
230 let mut path = tmp_dir.mkdir("test");
231 path.push("test_file");
232 File::create(&path).unwrap();
233
234 let barrier = CallbackGuard::new();
235 let barrier_clone = barrier.clone();
236
237 let _watcher = FileWatcher::new(
238 &Utf8Path::from_path(tmp_dir.path())
239 .unwrap()
240 .join("test/test_file"),
241 move || {
242 barrier_clone.signal();
243 },
244 );
245 {
246 let mut file = OpenOptions::new().write(true).open(&path)?;
248 writeln!(file, "Changes")?;
249 }
250 barrier.assert_called();
251 Ok(())
252 }
253
254 #[test]
255 pub fn check_file_for_difference() {
256 let tmp_dir = TempDir::new();
257 let file1 = tmp_dir.create("file1");
258 let dir = tmp_dir.mkdir("dir");
259 let mut file2 = dir.clone();
260 file2.push("file2");
261 File::create(&file2).unwrap();
262 assert!(is_same_file(&file1, &file1));
263 assert!(!is_same_file(&file1, &file2));
264 let mut complicated_file2 = dir.clone();
265 complicated_file2.push("../dir/file2");
266 assert!(is_same_file(&complicated_file2, &file2))
267 }
268}