1use std::path::{Path, PathBuf};
25use std::sync::{Arc, Mutex};
26use std::time::{Duration, Instant};
27
28use notify::{Config, Event, EventKind, PollWatcher, RecursiveMode, Watcher};
29
30#[derive(Debug, Clone)]
32pub struct WatchEvent {
33 pub paths: Vec<PathBuf>,
37 pub kind: WatchEventKind,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum WatchEventKind {
46 Create,
48 Write,
50 Remove,
52 Other,
54}
55
56const MAX_PENDING_WATCH_PATHS: usize = 128;
57const MAX_PENDING_WATCH_ERRORS: usize = 16;
58const MAX_WATCH_ERROR_BYTES: usize = 512;
59
60impl From<&EventKind> for WatchEventKind {
61 fn from(k: &EventKind) -> Self {
62 match k {
63 EventKind::Create(_) => Self::Create,
64 EventKind::Modify(_) => Self::Write,
65 EventKind::Remove(_) => Self::Remove,
66 _ => Self::Other,
67 }
68 }
69}
70
71pub struct PolicyWatcher {
77 _inner: PollWatcher,
78 pending: Arc<Mutex<PendingSignals>>,
79 debounce: Duration,
80 last_emit: Option<Instant>,
81}
82
83struct PendingSignals {
84 changed: bool,
85 paths: Vec<PathBuf>,
86 kind: WatchEventKind,
87 pending_errors: Vec<String>,
88 last_error: Option<String>,
89}
90
91impl Default for PendingSignals {
92 fn default() -> Self {
93 Self {
94 changed: false,
95 paths: Vec::new(),
96 kind: WatchEventKind::Other,
97 pending_errors: Vec::new(),
98 last_error: None,
99 }
100 }
101}
102
103fn queue_new_error(errors: &mut Vec<String>, last_error: &mut Option<String>, mut error: String) {
104 if error.len() > MAX_WATCH_ERROR_BYTES {
105 let mut end = MAX_WATCH_ERROR_BYTES;
106 while !error.is_char_boundary(end) {
107 end -= 1;
108 }
109 error.truncate(end);
110 }
111 if last_error.as_ref() != Some(&error) {
112 if errors.len() < MAX_PENDING_WATCH_ERRORS {
113 errors.push(error.clone());
114 }
115 *last_error = Some(error);
116 }
117}
118
119fn promote_kind(current: WatchEventKind, incoming: WatchEventKind) -> WatchEventKind {
120 match (current, incoming) {
121 (WatchEventKind::Create, _) | (_, WatchEventKind::Create) => WatchEventKind::Create,
122 (WatchEventKind::Write, _) | (_, WatchEventKind::Write) => WatchEventKind::Write,
123 (WatchEventKind::Remove, _) | (_, WatchEventKind::Remove) => WatchEventKind::Remove,
124 _ => WatchEventKind::Other,
125 }
126}
127
128impl PendingSignals {
129 fn record_event(&mut self, paths: impl IntoIterator<Item = PathBuf>, kind: WatchEventKind) {
130 self.changed = true;
131 self.last_error = None;
132 self.kind = promote_kind(self.kind, kind);
133 for path in paths {
134 if self.paths.len() == MAX_PENDING_WATCH_PATHS {
135 break;
136 }
137 self.paths.push(path);
138 }
139 }
140
141 fn record_error(&mut self, error: String) {
142 queue_new_error(&mut self.pending_errors, &mut self.last_error, error);
143 }
144}
145
146impl PolicyWatcher {
147 pub fn events(&mut self) -> Vec<WatchEvent> {
151 let now = Instant::now();
152 let ready = match self.last_emit {
153 None => true,
154 Some(t) => now.duration_since(t) >= self.debounce,
155 };
156 if !ready {
157 return Vec::new();
158 }
159 let mut pending = self.pending.lock().unwrap_or_else(|e| e.into_inner());
160 if !pending.changed {
161 return Vec::new();
162 }
163 self.last_emit = Some(now);
164 pending.changed = false;
165 let paths = std::mem::take(&mut pending.paths);
166 let kind = std::mem::replace(&mut pending.kind, WatchEventKind::Other);
167 vec![WatchEvent { paths, kind }]
168 }
169
170 pub fn take_errors(&mut self) -> Vec<String> {
173 let mut pending = self.pending.lock().unwrap_or_else(|e| e.into_inner());
174 std::mem::take(&mut pending.pending_errors)
175 }
176
177 pub fn stop(self) {
180 drop(self);
182 }
183}
184
185pub fn watch<P: AsRef<Path>>(dir: P, debounce: Duration) -> std::io::Result<PolicyWatcher> {
189 let dir = std::fs::canonicalize(dir.as_ref())?;
190 let policies_dir = dir.join("policies");
191 let schema_path = dir.join("schema.cedarschema");
192 let pending = Arc::new(Mutex::new(PendingSignals::default()));
193 let callback_pending = Arc::clone(&pending);
194 let mut inner = PollWatcher::new(
195 move |res: notify::Result<Event>| {
196 match res {
200 Ok(ev) => {
201 let kind = WatchEventKind::from(&ev.kind);
202 let paths = ev.paths.into_iter().filter(|path| {
205 path == &schema_path
206 || (path
207 .parent()
208 .is_some_and(|parent| parent.starts_with(&policies_dir))
209 && path.extension().is_some_and(|ext| ext == "cedar"))
210 });
211 let mut bounded_paths = Vec::new();
212 for path in paths {
213 if bounded_paths.len() == MAX_PENDING_WATCH_PATHS {
214 break;
215 }
216 bounded_paths.push(path);
217 }
218 if !bounded_paths.is_empty() {
219 callback_pending
220 .lock()
221 .unwrap_or_else(|e| e.into_inner())
222 .record_event(bounded_paths, kind);
223 }
224 }
225 Err(error) => {
226 callback_pending
227 .lock()
228 .unwrap_or_else(|e| e.into_inner())
229 .record_error(error.to_string());
230 }
231 }
232 },
233 Config::default()
234 .with_poll_interval(Duration::from_millis(100))
235 .with_compare_contents(true),
236 )
237 .map_err(|e| std::io::Error::other(format!("notify watcher: {e}")))?;
238 inner
239 .watch(&dir, RecursiveMode::Recursive)
240 .map_err(|e| std::io::Error::other(format!("notify watch: {e}")))?;
241 Ok(PolicyWatcher {
242 _inner: inner,
243 pending,
244 debounce,
245 last_emit: None,
246 })
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use std::fs;
253 use std::io::Write;
254 use tempfile::tempdir;
255
256 #[test]
259 fn watcher_emits_event_for_nested_policy_create() {
260 let dir = tempdir().unwrap();
261 fs::create_dir(dir.path().join("policies")).unwrap();
262 let mut w = watch(dir.path(), Duration::from_millis(50)).unwrap();
263 std::thread::sleep(Duration::from_millis(50));
265 let path = dir.path().join("policies/test.cedar");
266 {
267 let mut f = fs::File::create(&path).unwrap();
268 f.write_all(b"permit (principal, action, resource);\n")
269 .unwrap();
270 }
271 let deadline = std::time::Instant::now() + Duration::from_secs(2);
275 let events = loop {
276 let events = w.events();
277 if !events.is_empty() || std::time::Instant::now() >= deadline {
278 break events;
279 }
280 std::thread::sleep(Duration::from_millis(25));
281 };
282 assert!(!events.is_empty(), "expected at least one event");
283 let paths: Vec<_> = events.iter().flat_map(|e| e.paths.iter()).collect();
284 assert!(
285 paths.iter().any(|p| p.ends_with("policies/test.cedar")),
286 "expected policies/test.cedar in events, got: {paths:?}"
287 );
288 }
289
290 #[test]
291 fn watcher_emits_event_for_policy_in_nested_subdirectory() {
292 let dir = tempdir().unwrap();
293 let policy_dir = dir.path().join("policies/team");
294 fs::create_dir_all(&policy_dir).unwrap();
295 let mut watcher = watch(dir.path(), Duration::from_millis(50)).unwrap();
296 std::thread::sleep(Duration::from_millis(50));
297 let path = policy_dir.join("read.cedar");
298 fs::write(&path, "permit(principal, action, resource);").unwrap();
299
300 let deadline = std::time::Instant::now() + Duration::from_secs(2);
301 let events = loop {
302 let events = watcher.events();
303 if !events.is_empty() || std::time::Instant::now() >= deadline {
304 break events;
305 }
306 std::thread::sleep(Duration::from_millis(25));
307 };
308 assert!(
309 events
310 .iter()
311 .flat_map(|event| &event.paths)
312 .any(|changed| changed.ends_with("policies/team/read.cedar")),
313 "expected nested policy path {path:?} in events: {events:?}"
314 );
315 }
316
317 #[test]
318 fn watcher_emits_event_for_schema_change() {
319 let dir = tempdir().unwrap();
320 fs::create_dir(dir.path().join("policies")).unwrap();
321 let mut watcher = watch(dir.path(), Duration::from_millis(50)).unwrap();
322 std::thread::sleep(Duration::from_millis(50));
323 fs::write(dir.path().join("schema.cedarschema"), "namespace test {}").unwrap();
324
325 let deadline = std::time::Instant::now() + Duration::from_secs(2);
326 let events = loop {
327 let events = watcher.events();
328 if !events.is_empty() || std::time::Instant::now() >= deadline {
329 break events;
330 }
331 std::thread::sleep(Duration::from_millis(25));
332 };
333 assert!(
334 events
335 .iter()
336 .flat_map(|event| &event.paths)
337 .any(|path| path.ends_with("schema.cedarschema")),
338 "expected schema.cedarschema in events: {events:?}"
339 );
340 }
341
342 #[cfg(unix)]
343 #[test]
344 fn watcher_detects_atomic_projected_volume_update() {
345 use std::os::unix::fs::symlink;
346
347 let dir = tempdir().unwrap();
348 let first = dir.path().join("..data-1");
349 let second = dir.path().join("..data-2");
350 fs::create_dir_all(first.join("policies")).unwrap();
351 fs::create_dir_all(second.join("policies")).unwrap();
352 fs::write(
353 first.join("policies/allow.cedar"),
354 "permit(principal, action, resource);",
355 )
356 .unwrap();
357 fs::write(
358 second.join("policies/allow.cedar"),
359 "forbid(principal, action, resource);",
360 )
361 .unwrap();
362 fs::create_dir(dir.path().join("policies")).unwrap();
363 symlink(&first, dir.path().join("..data")).unwrap();
364 symlink(
365 "../..data/policies/allow.cedar",
366 dir.path().join("policies/allow.cedar"),
367 )
368 .unwrap();
369
370 let mut watcher = watch(dir.path(), Duration::from_millis(50)).unwrap();
371 std::thread::sleep(Duration::from_millis(150));
372 let next_link = dir.path().join("..data-next");
373 symlink(&second, &next_link).unwrap();
374 fs::rename(&next_link, dir.path().join("..data")).unwrap();
375
376 let deadline = std::time::Instant::now() + Duration::from_secs(2);
377 let events = loop {
378 let events = watcher.events();
379 if !events.is_empty() || std::time::Instant::now() >= deadline {
380 break events;
381 }
382 std::thread::sleep(Duration::from_millis(25));
383 };
384 assert!(
385 events
386 .iter()
387 .flat_map(|event| &event.paths)
388 .any(|path| { path.ends_with("policies/allow.cedar") }),
389 "expected atomic projection update to report policies/allow.cedar: {events:?}"
390 );
391 }
392
393 #[test]
394 fn repeated_runtime_watcher_errors_are_deduplicated_until_recovery() {
395 let mut errors = Vec::new();
396 let mut last_error = None;
397 queue_new_error(&mut errors, &mut last_error, "watch failed".to_owned());
398 queue_new_error(&mut errors, &mut last_error, "watch failed".to_owned());
399 assert_eq!(errors, ["watch failed"]);
400
401 last_error = None; queue_new_error(&mut errors, &mut last_error, "watch failed".to_owned());
403 assert_eq!(errors, ["watch failed", "watch failed"]);
404 }
405
406 #[test]
407 fn pending_watcher_signals_coalesce_and_stay_bounded() {
408 let mut pending = PendingSignals::default();
409 let paths = (0..MAX_PENDING_WATCH_PATHS + 50)
410 .map(|index| PathBuf::from(format!("policies/{index}.cedar")));
411 pending.record_event(paths, WatchEventKind::Write);
412 pending.record_event(
413 [PathBuf::from("policies/important.cedar")],
414 WatchEventKind::Create,
415 );
416
417 assert!(pending.changed);
418 assert_eq!(pending.paths.len(), MAX_PENDING_WATCH_PATHS);
419 assert_eq!(pending.kind, WatchEventKind::Create);
420
421 for index in 0..MAX_PENDING_WATCH_ERRORS + 10 {
422 pending.record_error(format!("watch error {index}: {}", "x".repeat(1024)));
423 }
424 assert_eq!(pending.pending_errors.len(), MAX_PENDING_WATCH_ERRORS);
425 assert!(pending
426 .pending_errors
427 .iter()
428 .all(|error| error.len() <= MAX_WATCH_ERROR_BYTES));
429 }
430
431 #[test]
434 fn watcher_stop_is_idempotent() {
435 let dir = tempdir().unwrap();
436 let w = watch(dir.path(), Duration::from_millis(10)).unwrap();
437 w.stop();
438 }
439}