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
6601 views
1
use crate::{
2
io::{AssetSourceEvent, AssetWatcher},
3
path::normalize_path,
4
};
5
use alloc::borrow::ToOwned;
6
use core::time::Duration;
7
use crossbeam_channel::Sender;
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 = normalize_path(&path).canonicalize()?;
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
pub(crate) fn get_asset_path(root: &Path, absolute_path: &Path) -> (PathBuf, bool) {
55
let relative_path = absolute_path.strip_prefix(root).unwrap_or_else(|_| {
56
panic!(
57
"FileWatcher::get_asset_path() failed to strip prefix from absolute path: absolute_path={}, root={}",
58
absolute_path.display(),
59
root.display()
60
)
61
});
62
let is_meta = relative_path.extension().is_some_and(|e| e == "meta");
63
let asset_path = if is_meta {
64
relative_path.with_extension("")
65
} else {
66
relative_path.to_owned()
67
};
68
(asset_path, is_meta)
69
}
70
71
/// This is a bit more abstracted than it normally would be because we want to try _very hard_ not to duplicate this
72
/// event management logic across filesystem-driven [`AssetWatcher`] impls. Each operating system / platform behaves
73
/// a little differently and this is the result of a delicate balancing act that we should only perform once.
74
pub(crate) fn new_asset_event_debouncer(
75
root: PathBuf,
76
debounce_wait_time: Duration,
77
mut handler: impl FilesystemEventHandler,
78
) -> Result<Debouncer<RecommendedWatcher, RecommendedCache>, notify::Error> {
79
let root = super::get_base_path().join(root);
80
let mut debouncer = new_debouncer(
81
debounce_wait_time,
82
None,
83
move |result: DebounceEventResult| {
84
match result {
85
Ok(events) => {
86
handler.begin();
87
for event in events.iter() {
88
match event.kind {
89
notify::EventKind::Create(CreateKind::File) => {
90
if let Some((path, is_meta)) = handler.get_path(&event.paths[0]) {
91
if is_meta {
92
handler.handle(
93
&event.paths,
94
AssetSourceEvent::AddedMeta(path),
95
);
96
} else {
97
handler.handle(
98
&event.paths,
99
AssetSourceEvent::AddedAsset(path),
100
);
101
}
102
}
103
}
104
notify::EventKind::Create(CreateKind::Folder) => {
105
if let Some((path, _)) = handler.get_path(&event.paths[0]) {
106
handler
107
.handle(&event.paths, AssetSourceEvent::AddedFolder(path));
108
}
109
}
110
notify::EventKind::Access(AccessKind::Close(AccessMode::Write)) => {
111
if let Some((path, is_meta)) = handler.get_path(&event.paths[0]) {
112
if is_meta {
113
handler.handle(
114
&event.paths,
115
AssetSourceEvent::ModifiedMeta(path),
116
);
117
} else {
118
handler.handle(
119
&event.paths,
120
AssetSourceEvent::ModifiedAsset(path),
121
);
122
}
123
}
124
}
125
// Because this is debounced over a reasonable period of time, Modify(ModifyKind::Name(RenameMode::From)
126
// events are assumed to be "dangling" without a follow up "To" event. Without debouncing, "From" -> "To" -> "Both"
127
// events are emitted for renames. If a From is dangling, it is assumed to be "removed" from the context of the asset
128
// system.
129
notify::EventKind::Remove(RemoveKind::Any)
130
| notify::EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {
131
if let Some((path, is_meta)) = handler.get_path(&event.paths[0]) {
132
handler.handle(
133
&event.paths,
134
AssetSourceEvent::RemovedUnknown { path, is_meta },
135
);
136
}
137
}
138
notify::EventKind::Create(CreateKind::Any)
139
| notify::EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {
140
if let Some((path, is_meta)) = handler.get_path(&event.paths[0]) {
141
let asset_event = if event.paths[0].is_dir() {
142
AssetSourceEvent::AddedFolder(path)
143
} else if is_meta {
144
AssetSourceEvent::AddedMeta(path)
145
} else {
146
AssetSourceEvent::AddedAsset(path)
147
};
148
handler.handle(&event.paths, asset_event);
149
}
150
}
151
notify::EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => {
152
let Some((old_path, old_is_meta)) =
153
handler.get_path(&event.paths[0])
154
else {
155
continue;
156
};
157
let Some((new_path, new_is_meta)) =
158
handler.get_path(&event.paths[1])
159
else {
160
continue;
161
};
162
// only the new "real" path is considered a directory
163
if event.paths[1].is_dir() {
164
handler.handle(
165
&event.paths,
166
AssetSourceEvent::RenamedFolder {
167
old: old_path,
168
new: new_path,
169
},
170
);
171
} else {
172
match (old_is_meta, new_is_meta) {
173
(true, true) => {
174
handler.handle(
175
&event.paths,
176
AssetSourceEvent::RenamedMeta {
177
old: old_path,
178
new: new_path,
179
},
180
);
181
}
182
(false, false) => {
183
handler.handle(
184
&event.paths,
185
AssetSourceEvent::RenamedAsset {
186
old: old_path,
187
new: new_path,
188
},
189
);
190
}
191
(true, false) => {
192
error!(
193
"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"
194
);
195
}
196
(false, true) => {
197
error!(
198
"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"
199
);
200
}
201
}
202
}
203
}
204
notify::EventKind::Modify(_) => {
205
let Some((path, is_meta)) = handler.get_path(&event.paths[0])
206
else {
207
continue;
208
};
209
if event.paths[0].is_dir() {
210
// modified folder means nothing in this case
211
} else if is_meta {
212
handler
213
.handle(&event.paths, AssetSourceEvent::ModifiedMeta(path));
214
} else {
215
handler.handle(
216
&event.paths,
217
AssetSourceEvent::ModifiedAsset(path),
218
);
219
};
220
}
221
notify::EventKind::Remove(RemoveKind::File) => {
222
let Some((path, is_meta)) = handler.get_path(&event.paths[0])
223
else {
224
continue;
225
};
226
if is_meta {
227
handler
228
.handle(&event.paths, AssetSourceEvent::RemovedMeta(path));
229
} else {
230
handler
231
.handle(&event.paths, AssetSourceEvent::RemovedAsset(path));
232
}
233
}
234
notify::EventKind::Remove(RemoveKind::Folder) => {
235
let Some((path, _)) = handler.get_path(&event.paths[0]) else {
236
continue;
237
};
238
handler.handle(&event.paths, AssetSourceEvent::RemovedFolder(path));
239
}
240
_ => {}
241
}
242
}
243
}
244
Err(errors) => errors.iter().for_each(|error| {
245
error!("Encountered a filesystem watcher error {error:?}");
246
}),
247
}
248
},
249
)?;
250
debouncer.watch(&root, RecursiveMode::Recursive)?;
251
Ok(debouncer)
252
}
253
254
pub(crate) struct FileEventHandler {
255
sender: Sender<AssetSourceEvent>,
256
root: PathBuf,
257
last_event: Option<AssetSourceEvent>,
258
}
259
260
impl FilesystemEventHandler for FileEventHandler {
261
fn begin(&mut self) {
262
self.last_event = None;
263
}
264
fn get_path(&self, absolute_path: &Path) -> Option<(PathBuf, bool)> {
265
let absolute_path = absolute_path.canonicalize().ok()?;
266
Some(get_asset_path(&self.root, &absolute_path))
267
}
268
269
fn handle(&mut self, _absolute_paths: &[PathBuf], event: AssetSourceEvent) {
270
if self.last_event.as_ref() != Some(&event) {
271
self.last_event = Some(event.clone());
272
self.sender.send(event).unwrap();
273
}
274
}
275
}
276
277
pub(crate) trait FilesystemEventHandler: Send + Sync + 'static {
278
/// Called each time a set of debounced events is processed
279
fn begin(&mut self);
280
/// Returns an actual asset path (if one exists for the given `absolute_path`), as well as a [`bool`] that is
281
/// true if the `absolute_path` corresponds to a meta file.
282
fn get_path(&self, absolute_path: &Path) -> Option<(PathBuf, bool)>;
283
/// Handle the given event
284
fn handle(&mut self, absolute_paths: &[PathBuf], event: AssetSourceEvent);
285
}
286
287