Path: blob/main/crates/bevy_asset/src/io/file/file_watcher.rs
6601 views
use crate::{1io::{AssetSourceEvent, AssetWatcher},2path::normalize_path,3};4use alloc::borrow::ToOwned;5use core::time::Duration;6use crossbeam_channel::Sender;7use notify_debouncer_full::{8new_debouncer,9notify::{10self,11event::{AccessKind, AccessMode, CreateKind, ModifyKind, RemoveKind, RenameMode},12RecommendedWatcher, RecursiveMode,13},14DebounceEventResult, Debouncer, RecommendedCache,15};16use std::path::{Path, PathBuf};17use tracing::error;1819/// An [`AssetWatcher`] that watches the filesystem for changes to asset files in a given root folder and emits [`AssetSourceEvent`]20/// for each relevant change.21///22/// This uses [`notify_debouncer_full`] to retrieve "debounced" filesystem events.23/// "Debouncing" defines a time window to hold on to events and then removes duplicate events that fall into this window.24/// This introduces a small delay in processing events, but it helps reduce event duplicates. A small delay is also necessary25/// on some systems to avoid processing a change event before it has actually been applied.26pub struct FileWatcher {27_watcher: Debouncer<RecommendedWatcher, RecommendedCache>,28}2930impl FileWatcher {31/// Creates a new [`FileWatcher`] that watches for changes to the asset files in the given `path`.32pub fn new(33path: PathBuf,34sender: Sender<AssetSourceEvent>,35debounce_wait_time: Duration,36) -> Result<Self, notify::Error> {37let root = normalize_path(&path).canonicalize()?;38let watcher = new_asset_event_debouncer(39path.clone(),40debounce_wait_time,41FileEventHandler {42root,43sender,44last_event: None,45},46)?;47Ok(FileWatcher { _watcher: watcher })48}49}5051impl AssetWatcher for FileWatcher {}5253pub(crate) fn get_asset_path(root: &Path, absolute_path: &Path) -> (PathBuf, bool) {54let relative_path = absolute_path.strip_prefix(root).unwrap_or_else(|_| {55panic!(56"FileWatcher::get_asset_path() failed to strip prefix from absolute path: absolute_path={}, root={}",57absolute_path.display(),58root.display()59)60});61let is_meta = relative_path.extension().is_some_and(|e| e == "meta");62let asset_path = if is_meta {63relative_path.with_extension("")64} else {65relative_path.to_owned()66};67(asset_path, is_meta)68}6970/// This is a bit more abstracted than it normally would be because we want to try _very hard_ not to duplicate this71/// event management logic across filesystem-driven [`AssetWatcher`] impls. Each operating system / platform behaves72/// a little differently and this is the result of a delicate balancing act that we should only perform once.73pub(crate) fn new_asset_event_debouncer(74root: PathBuf,75debounce_wait_time: Duration,76mut handler: impl FilesystemEventHandler,77) -> Result<Debouncer<RecommendedWatcher, RecommendedCache>, notify::Error> {78let root = super::get_base_path().join(root);79let mut debouncer = new_debouncer(80debounce_wait_time,81None,82move |result: DebounceEventResult| {83match result {84Ok(events) => {85handler.begin();86for event in events.iter() {87match event.kind {88notify::EventKind::Create(CreateKind::File) => {89if let Some((path, is_meta)) = handler.get_path(&event.paths[0]) {90if is_meta {91handler.handle(92&event.paths,93AssetSourceEvent::AddedMeta(path),94);95} else {96handler.handle(97&event.paths,98AssetSourceEvent::AddedAsset(path),99);100}101}102}103notify::EventKind::Create(CreateKind::Folder) => {104if let Some((path, _)) = handler.get_path(&event.paths[0]) {105handler106.handle(&event.paths, AssetSourceEvent::AddedFolder(path));107}108}109notify::EventKind::Access(AccessKind::Close(AccessMode::Write)) => {110if let Some((path, is_meta)) = handler.get_path(&event.paths[0]) {111if is_meta {112handler.handle(113&event.paths,114AssetSourceEvent::ModifiedMeta(path),115);116} else {117handler.handle(118&event.paths,119AssetSourceEvent::ModifiedAsset(path),120);121}122}123}124// Because this is debounced over a reasonable period of time, Modify(ModifyKind::Name(RenameMode::From)125// events are assumed to be "dangling" without a follow up "To" event. Without debouncing, "From" -> "To" -> "Both"126// events are emitted for renames. If a From is dangling, it is assumed to be "removed" from the context of the asset127// system.128notify::EventKind::Remove(RemoveKind::Any)129| notify::EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {130if let Some((path, is_meta)) = handler.get_path(&event.paths[0]) {131handler.handle(132&event.paths,133AssetSourceEvent::RemovedUnknown { path, is_meta },134);135}136}137notify::EventKind::Create(CreateKind::Any)138| notify::EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {139if let Some((path, is_meta)) = handler.get_path(&event.paths[0]) {140let asset_event = if event.paths[0].is_dir() {141AssetSourceEvent::AddedFolder(path)142} else if is_meta {143AssetSourceEvent::AddedMeta(path)144} else {145AssetSourceEvent::AddedAsset(path)146};147handler.handle(&event.paths, asset_event);148}149}150notify::EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => {151let Some((old_path, old_is_meta)) =152handler.get_path(&event.paths[0])153else {154continue;155};156let Some((new_path, new_is_meta)) =157handler.get_path(&event.paths[1])158else {159continue;160};161// only the new "real" path is considered a directory162if event.paths[1].is_dir() {163handler.handle(164&event.paths,165AssetSourceEvent::RenamedFolder {166old: old_path,167new: new_path,168},169);170} else {171match (old_is_meta, new_is_meta) {172(true, true) => {173handler.handle(174&event.paths,175AssetSourceEvent::RenamedMeta {176old: old_path,177new: new_path,178},179);180}181(false, false) => {182handler.handle(183&event.paths,184AssetSourceEvent::RenamedAsset {185old: old_path,186new: new_path,187},188);189}190(true, false) => {191error!(192"Asset metafile {old_path:?} was changed to asset file {new_path:?}, which is not supported. Try restarting your app to see if configuration is still valid"193);194}195(false, true) => {196error!(197"Asset file {old_path:?} was changed to meta file {new_path:?}, which is not supported. Try restarting your app to see if configuration is still valid"198);199}200}201}202}203notify::EventKind::Modify(_) => {204let Some((path, is_meta)) = handler.get_path(&event.paths[0])205else {206continue;207};208if event.paths[0].is_dir() {209// modified folder means nothing in this case210} else if is_meta {211handler212.handle(&event.paths, AssetSourceEvent::ModifiedMeta(path));213} else {214handler.handle(215&event.paths,216AssetSourceEvent::ModifiedAsset(path),217);218};219}220notify::EventKind::Remove(RemoveKind::File) => {221let Some((path, is_meta)) = handler.get_path(&event.paths[0])222else {223continue;224};225if is_meta {226handler227.handle(&event.paths, AssetSourceEvent::RemovedMeta(path));228} else {229handler230.handle(&event.paths, AssetSourceEvent::RemovedAsset(path));231}232}233notify::EventKind::Remove(RemoveKind::Folder) => {234let Some((path, _)) = handler.get_path(&event.paths[0]) else {235continue;236};237handler.handle(&event.paths, AssetSourceEvent::RemovedFolder(path));238}239_ => {}240}241}242}243Err(errors) => errors.iter().for_each(|error| {244error!("Encountered a filesystem watcher error {error:?}");245}),246}247},248)?;249debouncer.watch(&root, RecursiveMode::Recursive)?;250Ok(debouncer)251}252253pub(crate) struct FileEventHandler {254sender: Sender<AssetSourceEvent>,255root: PathBuf,256last_event: Option<AssetSourceEvent>,257}258259impl FilesystemEventHandler for FileEventHandler {260fn begin(&mut self) {261self.last_event = None;262}263fn get_path(&self, absolute_path: &Path) -> Option<(PathBuf, bool)> {264let absolute_path = absolute_path.canonicalize().ok()?;265Some(get_asset_path(&self.root, &absolute_path))266}267268fn handle(&mut self, _absolute_paths: &[PathBuf], event: AssetSourceEvent) {269if self.last_event.as_ref() != Some(&event) {270self.last_event = Some(event.clone());271self.sender.send(event).unwrap();272}273}274}275276pub(crate) trait FilesystemEventHandler: Send + Sync + 'static {277/// Called each time a set of debounced events is processed278fn begin(&mut self);279/// Returns an actual asset path (if one exists for the given `absolute_path`), as well as a [`bool`] that is280/// true if the `absolute_path` corresponds to a meta file.281fn get_path(&self, absolute_path: &Path) -> Option<(PathBuf, bool)>;282/// Handle the given event283fn handle(&mut self, absolute_paths: &[PathBuf], event: AssetSourceEvent);284}285286287