1use crate::NativeRootIdentity;
10use crate::cancellation::{CancellationError, CancellationToken};
11use crate::kernel::{LogicalName, NamespacePath, NamespacePathError};
12use crate::model::{FilesystemProfile, VolumeLimits};
13use crate::performance::{
14 MeasuredResult, OperationFailure, OperationReceipt, WorkBudget, WorkCounters, WorkError,
15};
16use notify::event::{ModifyKind, RenameMode};
17use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
18use std::collections::BTreeSet;
19use std::mem::size_of;
20use std::path::{Component, Path, PathBuf};
21use std::sync::atomic::{AtomicU32, Ordering};
22use std::sync::mpsc::{Receiver, SyncSender, TryRecvError, TrySendError, sync_channel};
23use std::sync::{Arc, Condvar, Mutex};
24use std::time::{Duration, Instant};
25use thiserror::Error;
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub enum NativeWatchBackend {
30 LinuxInotify,
32 MacosFsevents,
34 WindowsReadDirectoryChanges,
36 Unsupported,
38}
39
40impl NativeWatchBackend {
41 #[must_use]
43 pub const fn as_str(self) -> &'static str {
44 match self {
45 Self::LinuxInotify => "linux-inotify",
46 Self::MacosFsevents => "macos-fsevents",
47 Self::WindowsReadDirectoryChanges => "windows-read-directory-changes",
48 Self::Unsupported => "unsupported",
49 }
50 }
51}
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
55pub struct NativeWatchCapabilities {
56 pub backend: NativeWatchBackend,
58 pub recursive: bool,
60 pub persistent_restart: bool,
65 pub root_identity_fencing: bool,
67}
68
69#[must_use]
71pub const fn native_watch_capabilities() -> NativeWatchCapabilities {
72 #[cfg(target_os = "linux")]
73 let backend = NativeWatchBackend::LinuxInotify;
74 #[cfg(target_os = "macos")]
75 let backend = NativeWatchBackend::MacosFsevents;
76 #[cfg(target_os = "windows")]
77 let backend = NativeWatchBackend::WindowsReadDirectoryChanges;
78 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
79 let backend = NativeWatchBackend::Unsupported;
80
81 NativeWatchCapabilities {
82 backend,
83 recursive: !matches!(backend, NativeWatchBackend::Unsupported),
84 persistent_restart: false,
85 root_identity_fencing: cfg!(any(unix, windows)),
86 }
87}
88
89#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
91pub struct WatchEpoch(u64);
92
93impl WatchEpoch {
94 #[must_use]
96 pub const fn from_u64(value: u64) -> Self {
97 Self(value)
98 }
99
100 #[must_use]
102 pub const fn get(self) -> u64 {
103 self.0
104 }
105}
106
107#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
109pub struct WatchSequence(u64);
110
111impl WatchSequence {
112 #[must_use]
114 pub const fn from_u64(value: u64) -> Self {
115 Self(value)
116 }
117
118 #[must_use]
120 pub const fn get(self) -> u64 {
121 self.0
122 }
123}
124
125#[derive(Clone, Debug, Eq, PartialEq)]
127pub enum WatchChange {
128 Created(NamespacePath),
130 Modified(NamespacePath),
132 MetadataChanged(NamespacePath),
134 Removed(NamespacePath),
136 Renamed {
138 from: NamespacePath,
140 to: NamespacePath,
142 },
143}
144
145#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
147pub enum WatchInvalidationReason {
148 #[error("an initial authenticated baseline is required")]
150 InitialSnapshotRequired,
151 #[error("the native notification queue overflowed")]
153 QueueOverflow,
154 #[error("the native backend requires a rescan")]
156 NativeRescanRequired,
157 #[error("the native backend reported an asynchronous error")]
159 BackendError,
160 #[error("a native path cannot be represented by volume semantics")]
162 UnrepresentablePath,
163 #[error("a native rename could not be paired exactly")]
165 AmbiguousRename,
166 #[error("the watched root changed identity")]
168 RootChanged,
169}
170
171#[derive(Clone, Debug, Eq, PartialEq)]
173pub enum WatchBatch {
174 Changes {
176 epoch: WatchEpoch,
178 first_sequence: WatchSequence,
180 next_sequence: WatchSequence,
182 changes: Vec<WatchChange>,
184 },
185 RescanRequired {
187 epoch: WatchEpoch,
189 reason: WatchInvalidationReason,
191 },
192}
193
194#[derive(Clone, Copy, Debug, Eq, PartialEq)]
196pub struct NativeWatchOptions {
197 pub limits: VolumeLimits,
199 pub maximum_queued_changes: u32,
201 pub recursive: bool,
203}
204
205#[cfg(unix)]
206fn open_native_root(path: &Path) -> std::io::Result<std::fs::File> {
207 use std::os::unix::fs::OpenOptionsExt;
208 let mut options = std::fs::OpenOptions::new();
209 options
210 .read(true)
211 .custom_flags(libc::O_DIRECTORY | libc::O_NOFOLLOW);
212 options.open(path)
213}
214
215#[cfg(windows)]
216fn open_native_root(path: &Path) -> std::io::Result<std::fs::File> {
217 use std::os::windows::fs::MetadataExt;
218 use std::os::windows::fs::OpenOptionsExt;
219 use windows::Win32::Storage::FileSystem::{
220 FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT,
221 FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
222 };
223 let mut options = std::fs::OpenOptions::new();
224 options
225 .read(true)
226 .share_mode(FILE_SHARE_READ.0 | FILE_SHARE_WRITE.0 | FILE_SHARE_DELETE.0)
227 .custom_flags(FILE_FLAG_BACKUP_SEMANTICS.0 | FILE_FLAG_OPEN_REPARSE_POINT.0);
228 let file = options.open(path)?;
229 if file.metadata()?.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0 != 0 {
230 return Err(std::io::Error::new(
231 std::io::ErrorKind::InvalidInput,
232 "watch root is a reparse point",
233 ));
234 }
235 Ok(file)
236}
237
238#[cfg(not(any(unix, windows)))]
239fn open_native_root(path: &Path) -> std::io::Result<std::fs::File> {
240 std::fs::File::open(path)
241}
242
243const fn native_filesystem_profile() -> FilesystemProfile {
244 #[cfg(unix)]
245 {
246 FilesystemProfile::Posix
247 }
248 #[cfg(windows)]
249 {
250 FilesystemProfile::Windows
251 }
252 #[cfg(not(any(unix, windows)))]
253 {
254 FilesystemProfile::Portable
255 }
256}
257
258impl NativeWatchOptions {
259 #[must_use]
261 pub const fn new(limits: VolumeLimits) -> Self {
262 Self {
263 limits,
264 maximum_queued_changes: 16_384,
265 recursive: true,
266 }
267 }
268
269 fn validate(self) -> Result<Self, NativeWatchError> {
270 if self.maximum_queued_changes == 0 {
271 return Err(NativeWatchError::InvalidOptions);
272 }
273 Ok(self)
274 }
275}
276
277#[derive(Debug)]
278struct SharedState {
279 invalidation: Option<WatchInvalidationReason>,
280 #[cfg(target_os = "linux")]
281 pending_rename: Option<PendingRename>,
282 fence: Option<PathBuf>,
284 excluded: Vec<PathBuf>,
286}
287
288#[cfg(target_os = "linux")]
289#[derive(Debug)]
290struct PendingRename {
291 tracker: usize,
292 from: PathBuf,
293 to: Option<PathBuf>,
294}
295
296impl SharedState {
297 const fn new(invalidation: Option<WatchInvalidationReason>) -> Self {
298 Self {
299 invalidation,
300 #[cfg(target_os = "linux")]
301 pending_rename: None,
302 fence: None,
303 excluded: Vec::new(),
304 }
305 }
306
307 fn seal_invalidation(&mut self) -> Option<WatchInvalidationReason> {
308 #[cfg(target_os = "linux")]
309 if self.invalidation.is_none() && self.pending_rename.is_some() {
310 self.invalidation = Some(WatchInvalidationReason::AmbiguousRename);
311 }
312 self.invalidation
313 }
314}
315
316struct NativeEventContext {
317 root: PathBuf,
318 root_identity: NativeRootIdentity,
319 profile: FilesystemProfile,
320 limits: VolumeLimits,
321 #[cfg(target_os = "linux")]
322 maximum_queued_changes: u32,
323}
324
325pub struct NativeWatch {
332 watcher: RecommendedWatcher,
333 root: PathBuf,
334 recursive: bool,
335 watched_directories: BTreeSet<PathBuf>,
336 receiver: Receiver<WatchChange>,
337 queued: Arc<AtomicU32>,
338 shared: Arc<Mutex<SharedState>>,
339 fenced: Arc<Condvar>,
340 fences: u64,
341 excluded: Vec<(NamespacePath, PathBuf)>,
342 epoch: WatchEpoch,
343 next_sequence: WatchSequence,
344 rescan_in_progress: bool,
345 root_identity: NativeRootIdentity,
346}
347
348impl NativeWatch {
349 #[must_use]
351 pub const fn capabilities(&self) -> NativeWatchCapabilities {
352 native_watch_capabilities()
353 }
354
355 pub fn open(
366 root: impl AsRef<Path>,
367 options: NativeWatchOptions,
368 ) -> Result<Self, NativeWatchError> {
369 Self::open_with_profile(root, native_filesystem_profile(), options)
370 }
371
372 pub fn open_with_profile(
382 root: impl AsRef<Path>,
383 profile: FilesystemProfile,
384 options: NativeWatchOptions,
385 ) -> Result<Self, NativeWatchError> {
386 let options = options.validate()?;
387 let requested_root = root.as_ref();
388 let admission = requested_root
389 .symlink_metadata()
390 .map_err(|error| NativeWatchError::Io(error.to_string()))?;
391 if admission.file_type().is_symlink() {
392 return Err(NativeWatchError::RootIsNotDirectory);
393 }
394 let root = requested_root
395 .canonicalize()
396 .map_err(|error| NativeWatchError::Io(error.to_string()))?;
397 let root_file =
398 open_native_root(&root).map_err(|error| NativeWatchError::Io(error.to_string()))?;
399 let metadata = root_file
400 .metadata()
401 .map_err(|error| NativeWatchError::Io(error.to_string()))?;
402 if !metadata.is_dir() {
403 return Err(NativeWatchError::RootIsNotDirectory);
404 }
405 let root_identity = NativeRootIdentity::from_file(&root_file)
406 .map_err(|error| NativeWatchError::Io(error.to_string()))?;
407 let capacity = usize::try_from(options.maximum_queued_changes)
408 .map_err(|_| NativeWatchError::InvalidOptions)?;
409 let (sender, receiver) = sync_channel(capacity);
410 let queued = Arc::new(AtomicU32::new(0));
411 let shared = Arc::new(Mutex::new(SharedState::new(Some(
412 WatchInvalidationReason::InitialSnapshotRequired,
413 ))));
414 let fenced = Arc::new(Condvar::new());
415 let callback_shared = Arc::clone(&shared);
416 let callback_fenced = Arc::clone(&fenced);
417 let callback_queued = Arc::clone(&queued);
418 let callback_context = NativeEventContext {
419 root: root.clone(),
420 root_identity,
421 profile,
422 limits: options.limits,
423 #[cfg(target_os = "linux")]
424 maximum_queued_changes: options.maximum_queued_changes,
425 };
426 let mut watcher = notify::recommended_watcher(move |event: notify::Result<Event>| {
427 accept_native_event(
428 &event,
429 &callback_context,
430 &sender,
431 &callback_shared,
432 &callback_queued,
433 );
434 if let (Ok(event), Ok(mut state)) = (&event, callback_shared.lock())
437 && state
438 .fence
439 .as_ref()
440 .is_some_and(|fence| event.paths.contains(fence))
441 {
442 state.fence = None;
443 callback_fenced.notify_all();
444 }
445 })
446 .map_err(|error| NativeWatchError::Backend(error.to_string()))?;
447 watcher
448 .watch(
449 &root,
450 if options.recursive {
451 RecursiveMode::Recursive
452 } else {
453 RecursiveMode::NonRecursive
454 },
455 )
456 .map_err(|error| NativeWatchError::Backend(error.to_string()))?;
457 Ok(Self {
458 watcher,
459 watched_directories: BTreeSet::from([root.clone()]),
460 root,
461 recursive: options.recursive,
462 receiver,
463 queued,
464 shared,
465 fenced,
466 fences: 0,
467 excluded: Vec::new(),
468 epoch: WatchEpoch(0),
469 next_sequence: WatchSequence(0),
470 rescan_in_progress: false,
471 root_identity,
472 })
473 }
474
475 pub fn watch_directory(&mut self, directory: &NamespacePath) -> Result<(), NativeWatchError> {
488 if self.recursive || directory.is_root() {
489 return Ok(());
490 }
491 let relative = crate::native_capture::namespace_to_host_path(directory)
492 .map_err(|_| NativeWatchError::UnrepresentablePath)?;
493 let mut candidate = self.root.clone();
494 for component in relative.components() {
495 let Component::Normal(component) = component else {
496 return Err(NativeWatchError::UnrepresentablePath);
497 };
498 candidate.push(component);
499 let metadata = match candidate.symlink_metadata() {
500 Ok(metadata) => metadata,
501 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
502 candidate.pop();
503 break;
504 }
505 Err(error) => return Err(NativeWatchError::Io(error.to_string())),
506 };
507 if metadata.file_type().is_symlink() {
508 return Err(NativeWatchError::SymbolicLinkDirectory);
509 }
510 if !metadata.is_dir() {
511 return Err(NativeWatchError::ObservedPathNotDirectory);
512 }
513 }
514 if self.watched_directories.contains(&candidate) {
515 return Ok(());
516 }
517 self.watcher
518 .watch(&candidate, RecursiveMode::NonRecursive)
519 .map_err(|error| NativeWatchError::Backend(error.to_string()))?;
520 self.watched_directories.insert(candidate);
521 Ok(())
522 }
523
524 fn forget_removed_directories(&mut self, changes: &[WatchChange]) {
525 if self.recursive {
526 return;
527 }
528 let mut removed = Vec::new();
529 for change in changes {
530 match change {
531 WatchChange::Removed(path) => removed.push(path),
532 WatchChange::Renamed { from, to } => {
533 removed.push(from);
534 removed.push(to);
535 }
536 WatchChange::Created(_)
537 | WatchChange::Modified(_)
538 | WatchChange::MetadataChanged(_) => {}
539 }
540 }
541 for path in removed {
542 let Ok(relative) = crate::native_capture::namespace_to_host_path(path) else {
543 continue;
544 };
545 let removed = self.root.join(relative);
546 let stale = self
547 .watched_directories
548 .iter()
549 .filter(|watched| **watched != self.root && watched.starts_with(&removed))
550 .cloned()
551 .collect::<Vec<_>>();
552 for watched in stale {
553 let _ = self.watcher.unwatch(&watched);
554 self.watched_directories.remove(&watched);
555 }
556 }
557 }
558
559 fn reset_demand_watches(&mut self) {
560 if self.recursive {
561 return;
562 }
563 let stale = self
564 .watched_directories
565 .iter()
566 .filter(|path| **path != self.root)
567 .cloned()
568 .collect::<Vec<_>>();
569 for path in stale {
570 let _ = self.watcher.unwatch(&path);
571 self.watched_directories.remove(&path);
572 }
573 }
574
575 pub fn accept_lazy_baseline(&mut self) -> Result<WatchBatch, NativeWatchError> {
587 self.begin_rescan()?;
588 self.finish_rescan()
589 }
590
591 #[must_use]
593 pub const fn root_identity(&self) -> NativeRootIdentity {
594 self.root_identity
595 }
596
597 pub fn verify_root_identity(
604 &self,
605 observed: NativeRootIdentity,
606 ) -> Result<(), NativeWatchError> {
607 if observed == self.root_identity {
608 return Ok(());
609 }
610 self.shared
611 .lock()
612 .map_err(|_| NativeWatchError::StatePoisoned)?
613 .invalidation = Some(WatchInvalidationReason::RootChanged);
614 Err(NativeWatchError::RootChanged)
615 }
616
617 pub fn begin_rescan(&mut self) -> Result<WatchEpoch, NativeWatchError> {
626 if self.rescan_in_progress {
627 return Err(NativeWatchError::RescanAlreadyInProgress);
628 }
629 let mut shared = self
630 .shared
631 .lock()
632 .map_err(|_| NativeWatchError::StatePoisoned)?;
633 while self.receiver.try_recv().is_ok() {
634 self.queued.fetch_sub(1, Ordering::AcqRel);
635 }
636 shared.invalidation = None;
637 #[cfg(target_os = "linux")]
638 {
639 shared.pending_rename = None;
640 }
641 self.epoch = WatchEpoch(
642 self.epoch
643 .0
644 .checked_add(1)
645 .ok_or(NativeWatchError::SequenceExhausted)?,
646 );
647 self.next_sequence = WatchSequence(0);
648 drop(shared);
649 self.reset_demand_watches();
650 self.rescan_in_progress = true;
651 Ok(self.epoch)
652 }
653
654 #[cfg(target_os = "windows")]
655 fn accept_verified_baseline<T, E>(
656 &mut self,
657 verify: impl FnOnce() -> Result<Option<T>, E>,
658 ) -> Result<Result<Option<T>, E>, NativeWatchError> {
659 if self.rescan_in_progress {
660 return Err(NativeWatchError::RescanAlreadyInProgress);
661 }
662 let mut shared = self
663 .shared
664 .lock()
665 .map_err(|_| NativeWatchError::StatePoisoned)?;
666 if shared.invalidation != Some(WatchInvalidationReason::InitialSnapshotRequired)
667 || self.epoch.0 != 0
668 {
669 return Err(NativeWatchError::RescanAlreadyInProgress);
670 }
671 let verified = match verify() {
672 Ok(Some(verified)) => verified,
673 Ok(None) => return Ok(Ok(None)),
674 Err(error) => return Ok(Err(error)),
675 };
676 self.epoch = WatchEpoch(
680 self.epoch
681 .0
682 .checked_add(1)
683 .ok_or(NativeWatchError::SequenceExhausted)?,
684 );
685 self.next_sequence = WatchSequence(0);
686 shared.invalidation = None;
687 Ok(Ok(Some(verified)))
688 }
689
690 #[cfg(target_os = "windows")]
698 pub fn accept_windows_usn_baseline(
699 &mut self,
700 checkpoint: crate::WindowsUsnCheckpoint,
701 ) -> Result<Result<crate::WindowsUsnContinuity, crate::WindowsUsnError>, NativeWatchError> {
702 let root = self.root.clone();
703 self.accept_verified_baseline(|| {
704 crate::validate_windows_usn_checkpoint(&root, checkpoint).map(|continuity| {
705 (continuity == crate::WindowsUsnContinuity::Unchanged).then_some(continuity)
706 })
707 })
708 .map(|result| {
709 result.map(|admitted| {
710 admitted.unwrap_or(crate::WindowsUsnContinuity::BaselineRequired(
711 crate::WindowsUsnDiscontinuity::VolumeAdvanced,
712 ))
713 })
714 })
715 }
716
717 pub fn finish_rescan(&mut self) -> Result<WatchBatch, NativeWatchError> {
727 if !self.rescan_in_progress {
728 return Err(NativeWatchError::NoRescanInProgress);
729 }
730 let observed = open_native_root(&self.root)
731 .and_then(|root| NativeRootIdentity::from_file(&root))
732 .ok();
733 if observed != Some(self.root_identity) {
734 self.shared
735 .lock()
736 .map_err(|_| NativeWatchError::StatePoisoned)?
737 .invalidation = Some(WatchInvalidationReason::RootChanged);
738 }
739 self.rescan_in_progress = false;
740 let mut shared = self
741 .shared
742 .lock()
743 .map_err(|_| NativeWatchError::StatePoisoned)?;
744 Ok(shared.seal_invalidation().map_or(
745 WatchBatch::Changes {
746 epoch: self.epoch,
747 first_sequence: self.next_sequence,
748 next_sequence: self.next_sequence,
749 changes: Vec::new(),
750 },
751 |reason| WatchBatch::RescanRequired {
752 epoch: self.epoch,
753 reason,
754 },
755 ))
756 }
757
758 pub fn abort_rescan(
769 &mut self,
770 reason: WatchInvalidationReason,
771 ) -> Result<(), NativeWatchError> {
772 if !self.rescan_in_progress {
773 return Err(NativeWatchError::NoRescanInProgress);
774 }
775 self.rescan_in_progress = false;
776 self.shared
777 .lock()
778 .map_err(|_| NativeWatchError::StatePoisoned)?
779 .invalidation = Some(reason);
780 Ok(())
781 }
782
783 pub fn exclude_subtrees(&mut self, subtrees: &[NamespacePath]) -> Result<(), NativeWatchError> {
793 let excluded = subtrees
794 .iter()
795 .map(|subtree| {
796 if subtree.is_root() {
797 return Err(NativeWatchError::UnrepresentablePath);
798 }
799 crate::native_capture::namespace_to_host_path(subtree)
800 .map(|relative| self.root.join(relative))
801 .map_err(|_| NativeWatchError::UnrepresentablePath)
802 })
803 .collect::<Result<Vec<_>, _>>()?;
804 self.shared
805 .lock()
806 .map_err(|_| NativeWatchError::StatePoisoned)?
807 .excluded
808 .clone_from(&excluded);
809 self.excluded = subtrees.iter().cloned().zip(excluded).collect();
810 Ok(())
811 }
812
813 pub fn fence(&mut self, timeout: Duration) -> Result<(), NativeWatchError> {
829 let delivered = self.fence_cookie().is_ok_and(|cookie| {
830 self.await_cookie(&cookie, timeout)
831 .is_ok_and(|delivered| delivered)
832 });
833 let mut state = self
834 .shared
835 .lock()
836 .map_err(|_| NativeWatchError::StatePoisoned)?;
837 state.fence = None;
838 if !delivered && state.invalidation.is_none() {
839 state.invalidation = Some(WatchInvalidationReason::NativeRescanRequired);
840 }
841 Ok(())
842 }
843
844 fn fence_cookie(&mut self) -> Result<PathBuf, NativeWatchError> {
845 let (directory, host_directory) = self.fence_directory()?;
846 self.watch_directory(&directory)?;
847 self.fences = self.fences.wrapping_add(1);
848 Ok(host_directory.join(format!(
849 ".acyclic-fence-{}-{}",
850 std::process::id(),
851 self.fences
852 )))
853 }
854
855 fn await_cookie(&self, cookie: &Path, timeout: Duration) -> Result<bool, NativeWatchError> {
857 let mut state = self
858 .shared
859 .lock()
860 .map_err(|_| NativeWatchError::StatePoisoned)?;
861 if state.invalidation.is_some() {
862 return Ok(false);
863 }
864 state.fence = Some(cookie.to_path_buf());
865 drop(state);
866 std::fs::File::create_new(cookie)
867 .and_then(|_| std::fs::remove_file(cookie))
868 .map_err(|error| NativeWatchError::Io(error.to_string()))?;
869 let deadline = Instant::now() + timeout;
870 let mut state = self
871 .shared
872 .lock()
873 .map_err(|_| NativeWatchError::StatePoisoned)?;
874 while state.fence.is_some() && state.invalidation.is_none() {
875 let remaining = deadline.saturating_duration_since(Instant::now());
876 if remaining.is_zero() {
877 return Ok(false);
878 }
879 state = self
880 .fenced
881 .wait_timeout(state, remaining)
882 .map_err(|_| NativeWatchError::StatePoisoned)?
883 .0;
884 }
885 Ok(state.invalidation.is_none())
886 }
887
888 fn fence_directory(&self) -> Result<(NamespacePath, PathBuf), NativeWatchError> {
889 for (subtree, host) in &self.excluded {
890 match host.symlink_metadata() {
891 Ok(metadata) if metadata.is_dir() => return Ok((subtree.clone(), host.clone())),
892 Ok(_) => {}
893 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
894 Err(error) => return Err(NativeWatchError::Io(error.to_string())),
895 }
896 }
897 let (subtree, host) = self
898 .excluded
899 .first()
900 .cloned()
901 .ok_or(NativeWatchError::InvalidOptions)?;
902 std::fs::create_dir(&host).map_err(|error| NativeWatchError::Io(error.to_string()))?;
903 Ok((subtree, host))
904 }
905
906 pub fn poll(
914 &mut self,
915 maximum_changes: u32,
916 budget: WorkBudget,
917 cancellation: &CancellationToken,
918 ) -> MeasuredResult<OperationReceipt<WatchBatch>, NativeWatchError> {
919 cancellation
920 .check()
921 .map_err(|error| OperationFailure::before_work(error.into()))?;
922 if maximum_changes == 0 {
923 return Err(OperationFailure::before_work(
924 NativeWatchError::ZeroPollLimit,
925 ));
926 }
927 if self.rescan_in_progress {
928 return Err(OperationFailure::before_work(
929 NativeWatchError::RescanInProgress,
930 ));
931 }
932 if let Some(reason) = self.invalidation()? {
933 return Ok(OperationReceipt {
934 value: WatchBatch::RescanRequired {
935 epoch: self.epoch,
936 reason,
937 },
938 work: WorkCounters::default(),
939 });
940 }
941 let root_work = WorkCounters {
942 backend_read_operations: 1,
943 ..WorkCounters::default()
944 };
945 root_work
946 .verify(budget)
947 .map_err(|error| OperationFailure::before_work(error.into()))?;
948 self.verify_current_root()?;
949 if let Some(reason) = self.invalidation()? {
950 return Ok(OperationReceipt {
951 value: WatchBatch::RescanRequired {
952 epoch: self.epoch,
953 reason,
954 },
955 work: root_work,
956 });
957 }
958 self.poll_contiguous(maximum_changes, budget, root_work)
959 }
960
961 fn poll_contiguous(
962 &mut self,
963 maximum_changes: u32,
964 budget: WorkBudget,
965 initial_work: WorkCounters,
966 ) -> MeasuredResult<OperationReceipt<WatchBatch>, NativeWatchError> {
967 let queued = self.queued.load(Ordering::Acquire).min(maximum_changes);
968 if queued == 0 {
969 return Ok(OperationReceipt {
970 value: WatchBatch::Changes {
971 epoch: self.epoch,
972 first_sequence: self.next_sequence,
973 next_sequence: self.next_sequence,
974 changes: Vec::new(),
975 },
976 work: initial_work,
977 });
978 }
979 let capacity = usize::try_from(queued)
980 .map_err(|_| OperationFailure::before_work(NativeWatchError::InvalidOptions))?;
981 let allocation_bytes = capacity
982 .checked_mul(size_of::<WatchChange>())
983 .map(crate::foundation::usize_to_u64)
984 .ok_or_else(|| {
985 OperationFailure::before_work(NativeWatchError::Work(WorkError::Overflow))
986 })?;
987 let mut work = initial_work
988 .checked_add(WorkCounters {
989 allocation_operations: 1,
990 peak_allocation_bytes: allocation_bytes,
991 ..WorkCounters::default()
992 })
993 .map_err(|error| OperationFailure::before_work(error.into()))?;
994 work.verify(budget)
995 .map_err(|error| OperationFailure::before_work(error.into()))?;
996 let mut changes = Vec::new();
997 changes
998 .try_reserve_exact(capacity)
999 .map_err(|_| OperationFailure::new(NativeWatchError::AllocationFailed, work))?;
1000 while changes.len() < capacity {
1001 match self.receiver.try_recv() {
1002 Ok(change) => {
1003 self.queued.fetch_sub(1, Ordering::AcqRel);
1004 changes.push(change);
1005 }
1006 Err(TryRecvError::Empty) => break,
1007 Err(TryRecvError::Disconnected) => {
1008 self.invalidate(WatchInvalidationReason::BackendError)?;
1009 break;
1010 }
1011 }
1012 }
1013 if let Some(reason) = self.invalidation()? {
1014 return Ok(OperationReceipt {
1015 value: WatchBatch::RescanRequired {
1016 epoch: self.epoch,
1017 reason,
1018 },
1019 work,
1020 });
1021 }
1022 let count = crate::foundation::usize_to_u64(changes.len());
1023 let next = self
1024 .next_sequence
1025 .0
1026 .checked_add(count)
1027 .ok_or_else(|| OperationFailure::new(NativeWatchError::SequenceExhausted, work))?;
1028 work = work
1029 .checked_add(WorkCounters {
1030 items_examined: count,
1031 items_returned: count,
1032 ..WorkCounters::default()
1033 })
1034 .map_err(|error| OperationFailure::new(error.into(), work))?;
1035 work.verify(budget)
1036 .map_err(|error| OperationFailure::new(error.into(), work))?;
1037 let first_sequence = self.next_sequence;
1038 self.next_sequence = WatchSequence(next);
1039 self.forget_removed_directories(&changes);
1040 Ok(OperationReceipt {
1041 value: WatchBatch::Changes {
1042 epoch: self.epoch,
1043 first_sequence,
1044 next_sequence: self.next_sequence,
1045 changes,
1046 },
1047 work,
1048 })
1049 }
1050
1051 fn verify_current_root(&self) -> Result<(), OperationFailure<NativeWatchError>> {
1052 let observed = open_native_root(&self.root)
1053 .and_then(|root| NativeRootIdentity::from_file(&root))
1054 .ok();
1055 if observed == Some(self.root_identity) {
1056 return Ok(());
1057 }
1058 self.shared
1059 .lock()
1060 .map_err(|_| OperationFailure::before_work(NativeWatchError::StatePoisoned))?
1061 .invalidation = Some(WatchInvalidationReason::RootChanged);
1062 Ok(())
1063 }
1064
1065 fn invalidation(
1066 &self,
1067 ) -> Result<Option<WatchInvalidationReason>, OperationFailure<NativeWatchError>> {
1068 self.shared
1069 .lock()
1070 .map(|mut state| state.seal_invalidation())
1071 .map_err(|_| OperationFailure::before_work(NativeWatchError::StatePoisoned))
1072 }
1073
1074 fn invalidate(
1075 &self,
1076 reason: WatchInvalidationReason,
1077 ) -> Result<(), OperationFailure<NativeWatchError>> {
1078 let mut shared = self
1079 .shared
1080 .lock()
1081 .map_err(|_| OperationFailure::before_work(NativeWatchError::StatePoisoned))?;
1082 shared.invalidation.get_or_insert(reason);
1083 Ok(())
1084 }
1085}
1086
1087impl crate::demand::DemandDirectoryObserver for Mutex<NativeWatch> {
1088 fn observe_directory(
1089 &self,
1090 directory: &NamespacePath,
1091 ) -> Result<(), crate::demand::DemandError> {
1092 self.lock()
1093 .map_err(|_| NativeWatchError::StatePoisoned)?
1094 .watch_directory(directory)?;
1095 Ok(())
1096 }
1097}
1098
1099fn accept_native_event(
1100 event: ¬ify::Result<Event>,
1101 context: &NativeEventContext,
1102 sender: &SyncSender<WatchChange>,
1103 shared: &Arc<Mutex<SharedState>>,
1104 queued: &Arc<AtomicU32>,
1105) {
1106 let Ok(mut state) = shared.lock() else {
1107 return;
1108 };
1109 if state.invalidation.is_some() {
1110 return;
1111 }
1112 if let Ok(event) = event
1113 && !event.paths.is_empty()
1114 && event.paths.iter().all(|path| {
1115 state
1116 .excluded
1117 .iter()
1118 .any(|excluded| path.starts_with(excluded))
1119 })
1120 {
1121 return;
1122 }
1123 let mapped = event
1124 .as_ref()
1125 .map_err(|_| WatchInvalidationReason::BackendError)
1126 .and_then(|event| map_native_event(event, context));
1127 let changes = match mapped {
1128 Ok(MappedNativeEvent::Changes(changes)) => changes,
1129 #[cfg(target_os = "linux")]
1130 Ok(MappedNativeEvent::RenameHalf {
1131 tracker,
1132 mode,
1133 path,
1134 }) => {
1135 match mode {
1136 RenameMode::From if state.pending_rename.is_none() => {
1137 if queued.load(Ordering::Acquire) >= context.maximum_queued_changes {
1138 state.invalidation = Some(WatchInvalidationReason::QueueOverflow);
1139 return;
1140 }
1141 state.pending_rename = Some(PendingRename {
1142 tracker,
1143 from: path,
1144 to: None,
1145 });
1146 }
1147 RenameMode::To => {
1148 let Some(pending) = state.pending_rename.as_mut() else {
1149 state.invalidation = Some(WatchInvalidationReason::AmbiguousRename);
1150 return;
1151 };
1152 if pending.tracker != tracker || pending.to.is_some() {
1153 state.invalidation = Some(WatchInvalidationReason::AmbiguousRename);
1154 } else {
1155 pending.to = Some(path);
1156 }
1157 }
1158 _ => state.invalidation = Some(WatchInvalidationReason::AmbiguousRename),
1159 }
1160 return;
1161 }
1162 #[cfg(target_os = "linux")]
1163 Ok(MappedNativeEvent::RenameBoth {
1164 tracker,
1165 from,
1166 to,
1167 changes,
1168 }) => {
1169 let matched = state.pending_rename.as_ref().is_some_and(|pending| {
1170 pending.tracker == tracker
1171 && pending.from == from
1172 && pending.to.as_ref() == Some(&to)
1173 });
1174 if !matched {
1175 state.invalidation = Some(WatchInvalidationReason::AmbiguousRename);
1176 return;
1177 }
1178 state.pending_rename = None;
1179 changes
1180 }
1181 Err(reason) => {
1182 state.invalidation = Some(reason);
1183 return;
1184 }
1185 };
1186 for change in changes {
1187 #[cfg(target_os = "linux")]
1188 if queued
1189 .load(Ordering::Acquire)
1190 .saturating_add(u32::from(state.pending_rename.is_some()))
1191 >= context.maximum_queued_changes
1192 {
1193 state.invalidation = Some(WatchInvalidationReason::QueueOverflow);
1194 break;
1195 }
1196 match sender.try_send(change) {
1197 Ok(()) => {
1198 queued.fetch_add(1, Ordering::Release);
1199 }
1200 Err(TrySendError::Full(_) | TrySendError::Disconnected(_)) => {
1201 state.invalidation = Some(WatchInvalidationReason::QueueOverflow);
1202 break;
1203 }
1204 }
1205 }
1206}
1207
1208enum MappedNativeEvent {
1209 Changes(Vec<WatchChange>),
1210 #[cfg(target_os = "linux")]
1211 RenameHalf {
1212 tracker: usize,
1213 mode: RenameMode,
1214 path: PathBuf,
1215 },
1216 #[cfg(target_os = "linux")]
1217 RenameBoth {
1218 tracker: usize,
1219 from: PathBuf,
1220 to: PathBuf,
1221 changes: Vec<WatchChange>,
1222 },
1223}
1224
1225fn map_native_event(
1226 event: &Event,
1227 context: &NativeEventContext,
1228) -> Result<MappedNativeEvent, WatchInvalidationReason> {
1229 validate_replayed_root_creation(event, &context.root, context.root_identity)?;
1230 #[cfg(target_os = "linux")]
1231 if let EventKind::Modify(ModifyKind::Name(mode)) = event.kind {
1232 if event.need_rescan() {
1233 return Err(WatchInvalidationReason::NativeRescanRequired);
1234 }
1235 let tracker = event
1236 .attrs
1237 .tracker()
1238 .ok_or(WatchInvalidationReason::AmbiguousRename)?;
1239 if matches!(mode, RenameMode::From | RenameMode::To) {
1240 let [path] = event.paths.as_slice() else {
1241 return Err(WatchInvalidationReason::AmbiguousRename);
1242 };
1243 let relative =
1244 relative_namespace_path(&context.root, path, context.profile, context.limits)?;
1245 if relative.is_root() {
1246 return Err(WatchInvalidationReason::RootChanged);
1247 }
1248 return Ok(MappedNativeEvent::RenameHalf {
1249 tracker,
1250 mode,
1251 path: path.clone(),
1252 });
1253 }
1254 if mode == RenameMode::Both {
1255 let changes = map_event(event, &context.root, context.profile, context.limits)?;
1256 let [from, to] = event.paths.as_slice() else {
1257 return Err(WatchInvalidationReason::AmbiguousRename);
1258 };
1259 return Ok(MappedNativeEvent::RenameBoth {
1260 tracker,
1261 from: from.clone(),
1262 to: to.clone(),
1263 changes,
1264 });
1265 }
1266 }
1267 map_event(event, &context.root, context.profile, context.limits).map(MappedNativeEvent::Changes)
1268}
1269
1270fn validate_replayed_root_creation(
1271 event: &Event,
1272 root: &Path,
1273 admitted_root_identity: NativeRootIdentity,
1274) -> Result<(), WatchInvalidationReason> {
1275 #[cfg(target_os = "macos")]
1276 if event.paths.iter().any(|path| path == root) && matches!(event.kind, EventKind::Create(_)) {
1277 let observed = open_native_root(root)
1278 .and_then(|file| NativeRootIdentity::from_file(&file))
1279 .ok();
1280 if observed != Some(admitted_root_identity) {
1281 return Err(WatchInvalidationReason::RootChanged);
1282 }
1283 }
1284 #[cfg(not(target_os = "macos"))]
1285 let _ = (event, root, admitted_root_identity);
1286 Ok(())
1287}
1288
1289fn map_event(
1290 event: &Event,
1291 root: &Path,
1292 profile: FilesystemProfile,
1293 limits: VolumeLimits,
1294) -> Result<Vec<WatchChange>, WatchInvalidationReason> {
1295 if event.need_rescan() {
1296 return Err(WatchInvalidationReason::NativeRescanRequired);
1297 }
1298 if matches!(event.kind, EventKind::Access(_)) {
1299 return Ok(Vec::new());
1300 }
1301 if event.paths.is_empty() || event.paths.len() > 2 {
1302 return Err(WatchInvalidationReason::UnrepresentablePath);
1303 }
1304 let mut paths = Vec::new();
1305 paths
1306 .try_reserve_exact(event.paths.len())
1307 .map_err(|_| WatchInvalidationReason::UnrepresentablePath)?;
1308 for path in &event.paths {
1309 #[cfg(target_os = "macos")]
1314 if path == root && matches!(event.kind, EventKind::Create(_)) {
1315 continue;
1316 }
1317 paths.push(relative_namespace_path(root, path, profile, limits)?);
1318 }
1319 if paths.iter().any(NamespacePath::is_root)
1320 && matches!(
1321 event.kind,
1322 EventKind::Remove(_) | EventKind::Modify(ModifyKind::Name(_))
1323 )
1324 {
1325 return Err(WatchInvalidationReason::RootChanged);
1326 }
1327 #[cfg(target_os = "macos")]
1332 if paths.iter().any(NamespacePath::is_root) {
1333 return Err(WatchInvalidationReason::NativeRescanRequired);
1334 }
1335 if let EventKind::Modify(ModifyKind::Name(mode)) = event.kind {
1336 return match (mode, paths.as_slice()) {
1337 (RenameMode::Both, [from, to]) => Ok(vec![WatchChange::Renamed {
1338 from: from.clone(),
1339 to: to.clone(),
1340 }]),
1341 #[cfg(target_os = "linux")]
1345 _ => Err(WatchInvalidationReason::AmbiguousRename),
1346 #[cfg(not(target_os = "linux"))]
1355 (_, paths) => Ok(paths.iter().cloned().map(WatchChange::Modified).collect()),
1356 };
1357 }
1358 let mut changes = Vec::new();
1359 changes
1360 .try_reserve_exact(paths.len())
1361 .map_err(|_| WatchInvalidationReason::UnrepresentablePath)?;
1362 for path in paths {
1363 changes.push(match event.kind {
1364 #[cfg(target_os = "macos")]
1370 EventKind::Create(_) => WatchChange::Modified(path),
1371 #[cfg(not(target_os = "macos"))]
1372 EventKind::Create(_) => WatchChange::Created(path),
1373 EventKind::Remove(_) => WatchChange::Removed(path),
1374 #[cfg(target_os = "macos")]
1379 EventKind::Modify(ModifyKind::Metadata(_)) => WatchChange::Modified(path),
1380 #[cfg(not(target_os = "macos"))]
1381 EventKind::Modify(ModifyKind::Metadata(_)) => WatchChange::MetadataChanged(path),
1382 EventKind::Modify(_) | EventKind::Any | EventKind::Other => WatchChange::Modified(path),
1383 EventKind::Access(_) => continue,
1384 });
1385 }
1386 Ok(changes)
1387}
1388
1389fn relative_namespace_path(
1390 root: &Path,
1391 path: &Path,
1392 profile: FilesystemProfile,
1393 limits: VolumeLimits,
1394) -> Result<NamespacePath, WatchInvalidationReason> {
1395 let relative = path
1396 .strip_prefix(root)
1397 .map_err(|_| WatchInvalidationReason::UnrepresentablePath)?;
1398 let mut components = Vec::new();
1399 for component in relative.components() {
1400 let Component::Normal(name) = component else {
1401 return Err(WatchInvalidationReason::UnrepresentablePath);
1402 };
1403 let (encoding, bytes) =
1404 crate::native_name::host_name_bytes(name, profile, limits.maximum_component_bytes)
1405 .map_err(|_| WatchInvalidationReason::UnrepresentablePath)?;
1406 let logical = LogicalName::new(encoding, bytes, limits.maximum_component_bytes)
1407 .map_err(|_| WatchInvalidationReason::UnrepresentablePath)?;
1408 components.push(logical);
1409 }
1410 NamespacePath::new(components, limits)
1411 .map_err(|_error: NamespacePathError| WatchInvalidationReason::UnrepresentablePath)
1412}
1413
1414#[derive(Debug, Error)]
1416pub enum NativeWatchError {
1417 #[error("native watcher options are invalid")]
1419 InvalidOptions,
1420 #[error("native watcher root is not a directory")]
1422 RootIsNotDirectory,
1423 #[error("native watcher directory traverses a symbolic link")]
1426 SymbolicLinkDirectory,
1427 #[error("native watcher observed path is not a directory")]
1429 ObservedPathNotDirectory,
1430 #[error("native watcher directory is not representable on this host")]
1432 UnrepresentablePath,
1433 #[error("native watcher root changed identity")]
1435 RootChanged,
1436 #[error("native watcher I/O failed: {0}")]
1438 Io(String),
1439 #[error("native watcher backend failed: {0}")]
1441 Backend(String),
1442 #[error("native watcher state is poisoned")]
1444 StatePoisoned,
1445 #[error("native watcher baseline scan is already active")]
1447 RescanAlreadyInProgress,
1448 #[error("native watcher baseline scan is not active")]
1450 NoRescanInProgress,
1451 #[error("native watcher baseline scan is in progress")]
1453 RescanInProgress,
1454 #[error("native watcher poll limit must be non-zero")]
1456 ZeroPollLimit,
1457 #[error("native watcher sequence is exhausted")]
1459 SequenceExhausted,
1460 #[error("native watcher output allocation failed")]
1462 AllocationFailed,
1463 #[error(transparent)]
1465 Cancelled(#[from] CancellationError),
1466 #[error(transparent)]
1468 Work(#[from] WorkError),
1469}
1470
1471#[cfg(test)]
1472#[path = "tests/watch.rs"]
1473mod tests;