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