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