Path: blob/main/crates/bevy_asset/src/io/file/file_watcher.rs
9462 views
use crate::{1io::{AssetSourceEvent, AssetWatcher},2path::normalize_path,3};4use alloc::{borrow::ToOwned, vec::Vec};5use async_channel::Sender;6use core::time::Duration;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 = make_absolute_path(&path)?;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 {}5253/// Converts the provided path into an absolute one.54fn make_absolute_path(path: &Path) -> Result<PathBuf, std::io::Error> {55// We use `normalize` + `absolute` instead of `canonicalize` to avoid reading the filesystem to56// resolve the path. This also means that paths that no longer exist can still become absolute57// (e.g., a file that was renamed will have the "old" path no longer exist).58Ok(normalize_path(&std::path::absolute(path)?))59}6061pub(crate) fn get_asset_path(root: &Path, absolute_path: &Path) -> (PathBuf, bool) {62let relative_path = absolute_path.strip_prefix(root).unwrap_or_else(|_| {63panic!(64"FileWatcher::get_asset_path() failed to strip prefix from absolute path: absolute_path={}, root={}",65absolute_path.display(),66root.display()67)68});69let is_meta = relative_path.extension().is_some_and(|e| e == "meta");70let asset_path = if is_meta {71relative_path.with_extension("")72} else {73relative_path.to_owned()74};75(asset_path, is_meta)76}7778/// This is a bit more abstracted than it normally would be because we want to try _very hard_ not to duplicate this79/// event management logic across filesystem-driven [`AssetWatcher`] impls. Each operating system / platform behaves80/// a little differently and this is the result of a delicate balancing act that we should only perform once.81pub(crate) fn new_asset_event_debouncer(82root: PathBuf,83debounce_wait_time: Duration,84mut handler: impl FilesystemEventHandler,85) -> Result<Debouncer<RecommendedWatcher, RecommendedCache>, notify::Error> {86let root = super::get_base_path().join(root);87let mut debouncer = new_debouncer(88debounce_wait_time,89None,90move |result: DebounceEventResult| {91match result {92Ok(events) => {93handler.begin();94for event in events.iter() {95// Make all the paths absolute here so we don't need to do it in each96// handler.97let paths = event98.paths99.iter()100.map(PathBuf::as_path)101.map(|p| {102make_absolute_path(p).expect("paths from the debouncer are valid")103})104.collect::<Vec<_>>();105106match event.kind {107notify::EventKind::Create(CreateKind::File) => {108if let Some((path, is_meta)) = handler.get_path(&paths[0]) {109if is_meta {110handler.handle(&paths, AssetSourceEvent::AddedMeta(path));111} else {112handler.handle(&paths, AssetSourceEvent::AddedAsset(path));113}114}115}116notify::EventKind::Create(CreateKind::Folder) => {117if let Some((path, _)) = handler.get_path(&paths[0]) {118handler.handle(&paths, AssetSourceEvent::AddedFolder(path));119}120}121notify::EventKind::Access(AccessKind::Close(AccessMode::Write)) => {122if let Some((path, is_meta)) = handler.get_path(&paths[0]) {123if is_meta {124handler125.handle(&paths, AssetSourceEvent::ModifiedMeta(path));126} else {127handler128.handle(&paths, AssetSourceEvent::ModifiedAsset(path));129}130}131}132// Because this is debounced over a reasonable period of time, Modify(ModifyKind::Name(RenameMode::From)133// events are assumed to be "dangling" without a follow up "To" event. Without debouncing, "From" -> "To" -> "Both"134// events are emitted for renames. If a From is dangling, it is assumed to be "removed" from the context of the asset135// system.136notify::EventKind::Remove(RemoveKind::Any)137| notify::EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {138if let Some((path, is_meta)) = handler.get_path(&paths[0]) {139handler.handle(140&paths,141AssetSourceEvent::RemovedUnknown { path, is_meta },142);143}144}145notify::EventKind::Create(CreateKind::Any)146| notify::EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {147if let Some((path, is_meta)) = handler.get_path(&paths[0]) {148let asset_event = if paths[0].is_dir() {149AssetSourceEvent::AddedFolder(path)150} else if is_meta {151AssetSourceEvent::AddedMeta(path)152} else {153AssetSourceEvent::AddedAsset(path)154};155handler.handle(&paths, asset_event);156}157}158notify::EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => {159let Some((old_path, old_is_meta)) = handler.get_path(&paths[0])160else {161continue;162};163let Some((new_path, new_is_meta)) = handler.get_path(&paths[1])164else {165continue;166};167// only the new "real" path is considered a directory168if paths[1].is_dir() {169handler.handle(170&paths,171AssetSourceEvent::RenamedFolder {172old: old_path,173new: new_path,174},175);176} else {177match (old_is_meta, new_is_meta) {178(true, true) => {179handler.handle(180&paths,181AssetSourceEvent::RenamedMeta {182old: old_path,183new: new_path,184},185);186}187(false, false) => {188handler.handle(189&paths,190AssetSourceEvent::RenamedAsset {191old: old_path,192new: new_path,193},194);195}196(true, false) => {197error!(198"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"199);200}201(false, true) => {202error!(203"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"204);205}206}207}208}209notify::EventKind::Modify(_) => {210let Some((path, is_meta)) = handler.get_path(&paths[0]) else {211continue;212};213if paths[0].is_dir() {214// modified folder means nothing in this case215} else if is_meta {216handler.handle(&paths, AssetSourceEvent::ModifiedMeta(path));217} else {218handler.handle(&paths, AssetSourceEvent::ModifiedAsset(path));219};220}221notify::EventKind::Remove(RemoveKind::File) => {222let Some((path, is_meta)) = handler.get_path(&paths[0]) else {223continue;224};225if is_meta {226handler.handle(&paths, AssetSourceEvent::RemovedMeta(path));227} else {228handler.handle(&paths, AssetSourceEvent::RemovedAsset(path));229}230}231notify::EventKind::Remove(RemoveKind::Folder) => {232let Some((path, _)) = handler.get_path(&paths[0]) else {233continue;234};235handler.handle(&paths, AssetSourceEvent::RemovedFolder(path));236}237_ => {}238}239}240}241Err(errors) => errors.iter().for_each(|error| {242error!("Encountered a filesystem watcher error {error:?}");243}),244}245},246)?;247debouncer.watch(&root, RecursiveMode::Recursive)?;248Ok(debouncer)249}250251pub(crate) struct FileEventHandler {252sender: Sender<AssetSourceEvent>,253root: PathBuf,254last_event: Option<AssetSourceEvent>,255}256257impl FilesystemEventHandler for FileEventHandler {258fn begin(&mut self) {259self.last_event = None;260}261fn get_path(&self, absolute_path: &Path) -> Option<(PathBuf, bool)> {262Some(get_asset_path(&self.root, absolute_path))263}264265fn handle(&mut self, _absolute_paths: &[PathBuf], event: AssetSourceEvent) {266if self.last_event.as_ref() != Some(&event) {267self.last_event = Some(event.clone());268self.sender.send_blocking(event).unwrap();269}270}271}272273pub(crate) trait FilesystemEventHandler: Send + Sync + 'static {274/// Called each time a set of debounced events is processed275fn begin(&mut self);276/// Returns an actual asset path (if one exists for the given `absolute_path`), as well as a [`bool`] that is277/// true if the `absolute_path` corresponds to a meta file.278fn get_path(&self, absolute_path: &Path) -> Option<(PathBuf, bool)>;279/// Handle the given event280fn handle(&mut self, absolute_paths: &[PathBuf], event: AssetSourceEvent);281}282283284