Skip to main content

bevy_asset/processor/
mod.rs

1//! Asset processing in Bevy is a framework for automatically transforming artist-authored assets into the format that best suits the needs of your particular game.
2//!
3//! You can think of the asset processing system as a "build system" for assets.
4//! When an artist adds a new asset to the project or an asset is changed (assuming asset hot reloading is enabled), the asset processing system will automatically perform the specified processing steps on the asset.
5//! This can include things like creating lightmaps for baked lighting, compressing a `.wav` file to an `.ogg`, or generating mipmaps for a texture.
6//!
7//! Its core values are:
8//!
9//! 1. Automatic: new and changed assets should be ready to use in-game without requiring any manual conversion or cleanup steps.
10//! 2. Configurable: every game has its own needs, and a high level of transparency and control is required.
11//! 3. Lossless: the original asset should always be preserved, ensuring artists can make changes later.
12//! 4. Deterministic: performing the same processing steps on the same asset should (generally) produce the exact same result. In cases where this doesn't make sense (steps that involve a degree of randomness or uncertainty), the results across runs should be "acceptably similar", as they will be generated once for a given set of inputs and cached.
13//!
14//! Taken together, this means that the original asset plus the processing steps should be enough to regenerate the final asset.
15//! While it may be possible to manually edit the final asset, this should be discouraged.
16//! Final post-processed assets should generally not be version-controlled, except to save developer time when recomputing heavy asset processing steps.
17//!
18//! # Usage
19//!
20//! Asset processing can be enabled or disabled in [`AssetPlugin`](crate::AssetPlugin) by setting the [`AssetMode`](crate::AssetMode).\
21//! Enable Bevy's `file_watcher` feature to automatically watch for changes to assets and reprocess them.
22//!
23//! To register a new asset processor, use [`AssetProcessor::register_processor`].
24//! To set the default asset processor for a given extension, use [`AssetProcessor::set_default_processor`].
25//! In most cases, these methods will be called directly on [`App`](bevy_app::App) using the [`AssetApp`](crate::AssetApp) extension trait.
26//!
27//! If a default asset processor is set, assets with a matching extension will be processed using that processor before loading.
28//!
29//! For an end-to-end example, check out the examples in the [`examples/asset/processing`](https://github.com/bevyengine/bevy/tree/latest/examples/asset/processing) directory of the Bevy repository.
30//!
31//!  # Defining asset processors
32//!
33//! Bevy provides two different ways to define new asset processors:
34//!
35//! - [`LoadTransformAndSave`] + [`AssetTransformer`](crate::transformer::AssetTransformer): a high-level API for loading, transforming, and saving assets.
36//! - [`Process`]: a flexible low-level API for processing assets in arbitrary ways.
37//!
38//! In most cases, [`LoadTransformAndSave`] should be sufficient.
39
40mod log;
41mod process;
42
43use async_lock::RwLockReadGuardArc;
44pub use log::*;
45pub use process::*;
46
47use crate::{
48    io::{
49        AssetReaderError, AssetSource, AssetSourceBuilders, AssetSourceEvent, AssetSourceId,
50        AssetSources, AssetWriterError, ErasedAssetReader, MissingAssetSourceError,
51    },
52    meta::{
53        get_asset_hash, get_full_asset_hash, AssetAction, AssetActionMinimal, AssetHash, AssetMeta,
54        AssetMetaDyn, AssetMetaMinimal, ProcessedInfo, ProcessedInfoMinimal,
55    },
56    AssetLoadError, AssetMetaCheck, AssetPath, AssetServer, AssetServerMode, DeserializeMetaError,
57    MissingAssetLoaderForExtensionError, UnapprovedPathMode, WriteDefaultMetaError,
58};
59use alloc::{borrow::ToOwned, boxed::Box, string::String, sync::Arc, vec, vec::Vec};
60use bevy_ecs::prelude::*;
61use bevy_platform::{
62    collections::{hash_map::Entry, HashMap, HashSet},
63    sync::{PoisonError, RwLock},
64};
65use bevy_tasks::IoTaskPool;
66use futures_io::ErrorKind;
67use futures_lite::{AsyncWriteExt, StreamExt};
68use futures_util::{select_biased, FutureExt};
69use std::{
70    path::{Path, PathBuf},
71    sync::Mutex,
72};
73use thiserror::Error;
74use tracing::{debug, error, trace, warn};
75
76#[cfg(feature = "trace")]
77use {
78    alloc::string::ToString,
79    tracing::{info_span, instrument::Instrument},
80};
81
82/// A "background" asset processor that reads asset values from a source [`AssetSource`] (which corresponds to an [`AssetReader`](crate::io::AssetReader) / [`AssetWriter`](crate::io::AssetWriter) pair),
83/// processes them in some way, and writes them to a destination [`AssetSource`].
84///
85/// This will create .meta files (a human-editable serialized form of [`AssetMeta`]) in the source [`AssetSource`] for assets
86/// that can be loaded and/or processed. This enables developers to configure how each asset should be loaded and/or processed.
87///
88/// [`AssetProcessor`] can be run in the background while a Bevy App is running. Changes to assets will be automatically detected and hot-reloaded.
89///
90/// Assets will only be re-processed if they have been changed. A hash of each asset source is stored in the metadata of the processed version of the
91/// asset, which is used to determine if the asset source has actually changed.
92///
93/// A [`ProcessorTransactionLog`] is produced, which uses "write-ahead logging" to make the [`AssetProcessor`] crash and failure resistant. If a failed/unfinished
94/// transaction from a previous run is detected, the affected asset(s) will be re-processed.
95///
96/// [`AssetProcessor`] can be cloned. It is backed by an [`Arc`] so clones will share state. Clones can be freely used in parallel.
97#[derive(Resource, Clone)]
98pub struct AssetProcessor {
99    server: AssetServer,
100    pub(crate) data: Arc<AssetProcessorData>,
101}
102
103/// Internal data stored inside an [`AssetProcessor`].
104pub struct AssetProcessorData {
105    /// The state of processing.
106    pub(crate) processing_state: Arc<ProcessingState>,
107    /// The factory that creates the transaction log.
108    ///
109    /// Note: we use a regular Mutex instead of an async mutex since we expect users to only set
110    /// this once, and before the asset processor starts - there is no reason to await (and it
111    /// avoids needing to use [`block_on`](bevy_tasks::block_on) to set the factory).
112    log_factory: Mutex<Option<Box<dyn ProcessorTransactionLogFactory>>>,
113    log: async_lock::RwLock<Option<Box<dyn ProcessorTransactionLog>>>,
114    /// The processors that will be used to process assets.
115    processors: RwLock<Processors>,
116    sources: Arc<AssetSources>,
117}
118
119/// The current state of processing, including the overall state and the state of all assets.
120pub(crate) struct ProcessingState {
121    /// The overall state of processing.
122    state: async_lock::RwLock<ProcessorState>,
123    /// The channel to broadcast when the processor has completed initialization.
124    initialized_sender: async_broadcast::Sender<()>,
125    initialized_receiver: async_broadcast::Receiver<()>,
126    /// The channel to broadcast when the processor has completed processing.
127    finished_sender: async_broadcast::Sender<()>,
128    finished_receiver: async_broadcast::Receiver<()>,
129    /// The current state of the assets.
130    asset_infos: async_lock::RwLock<ProcessorAssetInfos>,
131}
132
133#[derive(Default)]
134struct Processors {
135    /// Maps the type path of the processor to its instance.
136    type_path_to_processor: HashMap<&'static str, Arc<dyn ErasedProcessor>>,
137    /// Maps the short type path of the processor to its instance.
138    short_type_path_to_processor: HashMap<&'static str, ShortTypeProcessorEntry>,
139    /// Maps the file extension of an asset to the type path of the processor we should use to
140    /// process it by default.
141    file_extension_to_default_processor: HashMap<Box<str>, &'static str>,
142}
143
144enum ShortTypeProcessorEntry {
145    /// There is a unique processor with the given short type path.
146    Unique {
147        /// The full type path of the processor.
148        type_path: &'static str,
149        /// The processor itself.
150        processor: Arc<dyn ErasedProcessor>,
151    },
152    /// There are (at least) two processors with the same short type path (storing the full type
153    /// paths of all conflicting processors). Users must fully specify the type path in order to
154    /// disambiguate.
155    Ambiguous(Vec<&'static str>),
156}
157
158impl AssetProcessor {
159    /// Creates a new [`AssetProcessor`] instance.
160    pub fn new(
161        sources: &mut AssetSourceBuilders,
162        watch_processed: bool,
163    ) -> (Self, Arc<AssetSources>) {
164        let state = Arc::new(ProcessingState::new());
165        let mut sources = sources.build_sources(true, watch_processed);
166        sources.gate_on_processor(state.clone());
167        let sources = Arc::new(sources);
168
169        let data = Arc::new(AssetProcessorData::new(sources.clone(), state));
170        // The asset processor uses its own asset server with its own id space
171        let server = AssetServer::new_with_meta_check(
172            sources.clone(),
173            AssetServerMode::Processed,
174            AssetMetaCheck::Always,
175            false,
176            UnapprovedPathMode::default(),
177        );
178        (Self { server, data }, sources)
179    }
180
181    /// Gets a reference to the [`Arc`] containing the [`AssetProcessorData`].
182    pub fn data(&self) -> &Arc<AssetProcessorData> {
183        &self.data
184    }
185
186    /// The "internal" [`AssetServer`] used by the [`AssetProcessor`]. This is _separate_ from the asset processor used by
187    /// the main App. It has different processor-specific configuration and a different ID space.
188    pub fn server(&self) -> &AssetServer {
189        &self.server
190    }
191
192    /// Retrieves the current [`ProcessorState`]
193    pub async fn get_state(&self) -> ProcessorState {
194        self.data.processing_state.get_state().await
195    }
196
197    /// Retrieves the [`AssetSource`] for this processor
198    #[inline]
199    pub fn get_source<'a>(
200        &self,
201        id: impl Into<AssetSourceId<'a>>,
202    ) -> Result<&AssetSource, MissingAssetSourceError> {
203        self.data.sources.get(id.into())
204    }
205
206    /// Retrieves all the [`AssetSource`]s for this processor.
207    #[inline]
208    pub fn sources(&self) -> &AssetSources {
209        &self.data.sources
210    }
211
212    /// Logs an unrecoverable error. On the next run of the processor, all assets will be regenerated. This should only be used as a last resort.
213    /// Every call to this should be considered with scrutiny and ideally replaced with something more granular.
214    async fn log_unrecoverable(&self) {
215        let mut log = self.data.log.write().await;
216        let log = log.as_mut().unwrap();
217        log.unrecoverable()
218            .await
219            .map_err(|error| WriteLogError {
220                log_entry: LogEntry::UnrecoverableError,
221                error,
222            })
223            .unwrap();
224    }
225
226    /// Logs the start of an asset being processed. If this is not followed at some point in the log by a closing [`AssetProcessor::log_end_processing`],
227    /// in the next run of the processor the asset processing will be considered "incomplete" and it will be reprocessed.
228    async fn log_begin_processing(&self, path: &AssetPath<'_>) {
229        let mut log = self.data.log.write().await;
230        let log = log.as_mut().unwrap();
231        log.begin_processing(path)
232            .await
233            .map_err(|error| WriteLogError {
234                log_entry: LogEntry::BeginProcessing(path.clone_owned()),
235                error,
236            })
237            .unwrap();
238    }
239
240    /// Logs the end of an asset being successfully processed. See [`AssetProcessor::log_begin_processing`].
241    async fn log_end_processing(&self, path: &AssetPath<'_>) {
242        let mut log = self.data.log.write().await;
243        let log = log.as_mut().unwrap();
244        log.end_processing(path)
245            .await
246            .map_err(|error| WriteLogError {
247                log_entry: LogEntry::EndProcessing(path.clone_owned()),
248                error,
249            })
250            .unwrap();
251    }
252
253    /// Starts the processor in a background thread.
254    pub fn start(processor: Res<Self>) {
255        let processor = processor.clone();
256        IoTaskPool::get()
257            .spawn(async move {
258                let start_time = std::time::Instant::now();
259                debug!("Processing Assets");
260
261                processor.initialize().await.unwrap();
262
263                let (new_task_sender, new_task_receiver) = async_channel::unbounded();
264                processor
265                    .queue_initial_processing_tasks(&new_task_sender)
266                    .await;
267
268                // Once all the tasks are queued for the initial processing, start actually
269                // executing the tasks.
270                {
271                    let processor = processor.clone();
272                    let new_task_sender = new_task_sender.clone();
273                    IoTaskPool::get()
274                        .spawn(async move {
275                            processor
276                                .execute_processing_tasks(new_task_sender, new_task_receiver)
277                                .await;
278                        })
279                        .detach();
280                }
281
282                processor.data.wait_until_finished().await;
283
284                let end_time = std::time::Instant::now();
285                debug!("Processing finished in {:?}", end_time - start_time);
286
287                debug!("Listening for changes to source assets");
288                processor.spawn_source_change_event_listeners(&new_task_sender);
289            })
290            .detach();
291    }
292
293    /// Sends start task events for all assets in all processed sources into `sender`.
294    async fn queue_initial_processing_tasks(
295        &self,
296        sender: &async_channel::Sender<(AssetSourceId<'static>, PathBuf)>,
297    ) {
298        for source in self.sources().iter_processed() {
299            self.queue_processing_tasks_for_folder(source, PathBuf::from(""), sender)
300                .await
301                .unwrap();
302        }
303    }
304
305    /// Spawns listeners of change events for all asset sources which will start processor tasks in
306    /// response.
307    fn spawn_source_change_event_listeners(
308        &self,
309        sender: &async_channel::Sender<(AssetSourceId<'static>, PathBuf)>,
310    ) {
311        for source in self.data.sources.iter_processed() {
312            let Some(receiver) = source.event_receiver().cloned() else {
313                continue;
314            };
315            let source_id = source.id();
316            let processor = self.clone();
317            let sender = sender.clone();
318            IoTaskPool::get()
319                .spawn(async move {
320                    while let Ok(event) = receiver.recv().await {
321                        let Ok(source) = processor.get_source(source_id.clone()) else {
322                            return;
323                        };
324                        processor
325                            .handle_asset_source_event(source, event, &sender)
326                            .await;
327                    }
328                })
329                .detach();
330        }
331    }
332
333    /// Executes all tasks that come through `receiver`, and updates the processor's overall state
334    /// based on task starts and ends.
335    ///
336    /// This future does not terminate until the channel is closed (not when the channel is empty).
337    /// This means that in [`AssetProcessor::start`], this execution will continue even after all
338    /// the initial tasks are processed.
339    async fn execute_processing_tasks(
340        &self,
341        new_task_sender: async_channel::Sender<(AssetSourceId<'static>, PathBuf)>,
342        new_task_receiver: async_channel::Receiver<(AssetSourceId<'static>, PathBuf)>,
343    ) {
344        // Convert the Sender into a WeakSender so that once all task producers terminate (and drop
345        // their sender), this task doesn't keep itself alive. We still however need a way to get
346        // the sender since processing tasks can start the tasks of dependent assets.
347        let new_task_sender = {
348            let weak_sender = new_task_sender.downgrade();
349            drop(new_task_sender);
350            weak_sender
351        };
352
353        // If there aren't any tasks in the channel the first time around, we should immediately go
354        // to the finished state (otherwise we'd be sitting around stuck in the `Initialized`
355        // state).
356        if new_task_receiver.is_empty() {
357            self.data
358                .processing_state
359                .set_state(ProcessorState::Finished)
360                .await;
361        }
362        enum ProcessorTaskEvent {
363            Start(AssetSourceId<'static>, PathBuf),
364            Finished,
365        }
366        let (task_finished_sender, task_finished_receiver) = async_channel::unbounded::<()>();
367
368        let mut pending_tasks = 0;
369        while let Ok(event) = {
370            // It's ok to use `select_biased` since we prefer to start task rather than finish tasks
371            // anyway - since otherwise we might mark the processor as finished before all queued
372            // tasks are done. `select_biased` also doesn't depend on `std` which is nice!
373            select_biased! {
374                result = new_task_receiver.recv().fuse() => {
375                    result.map(|(source_id, path)| ProcessorTaskEvent::Start(source_id, path))
376                },
377                result = task_finished_receiver.recv().fuse() => {
378                    result.map(|()| ProcessorTaskEvent::Finished)
379                }
380            }
381        } {
382            match event {
383                ProcessorTaskEvent::Start(source_id, path) => {
384                    let Some(new_task_sender) = new_task_sender.upgrade() else {
385                        // If we can't upgrade the task sender, that means all sources of tasks
386                        // (like the source event listeners) have been dropped. That means that the
387                        // sources are no longer in the app, so reading/writing to them will
388                        // probably not work, so ignoring the task is fine. This also likely means
389                        // that the whole app is being dropped, so we can recover on the next
390                        // initialization.
391                        continue;
392                    };
393                    let processor = self.clone();
394                    let task_finished_sender = task_finished_sender.clone();
395                    pending_tasks += 1;
396                    IoTaskPool::get()
397                        .spawn(async move {
398                            let Ok(source) = processor.get_source(source_id) else {
399                                return;
400                            };
401                            processor.process_asset(source, path, new_task_sender).await;
402                            // If the channel gets closed, that's ok. Just ignore it.
403                            let _ = task_finished_sender.send(()).await;
404                        })
405                        .detach();
406                    self.data
407                        .processing_state
408                        .set_state(ProcessorState::Processing)
409                        .await;
410                }
411                ProcessorTaskEvent::Finished => {
412                    pending_tasks -= 1;
413                    if pending_tasks == 0 {
414                        // clean up metadata in asset server
415                        self.server.write_infos().consume_handle_drop_events();
416                        self.data
417                            .processing_state
418                            .set_state(ProcessorState::Finished)
419                            .await;
420                    }
421                }
422            }
423        }
424    }
425
426    /// Writes the default meta file for the provided `path`.
427    ///
428    /// This function generates the appropriate meta file to process `path` with the default
429    /// processor. If there is no default processor, it falls back to the default loader.
430    ///
431    /// Note if there is already a meta file for `path`, this function returns
432    /// `Err(WriteDefaultMetaError::MetaAlreadyExists)`.
433    pub async fn write_default_meta_file_for_path(
434        &self,
435        path: impl Into<AssetPath<'_>>,
436    ) -> Result<(), WriteDefaultMetaError> {
437        let path = path.into();
438        let Some(processor) = path
439            .get_full_extension()
440            .and_then(|extension| self.get_default_processor(extension))
441        else {
442            return self
443                .server
444                .write_default_loader_meta_file_for_path(path)
445                .await;
446        };
447
448        let short_type_path = processor.short_type_path();
449        // Try to get the processor using the short type - if it fails, that must mean that the
450        // short type path is insufficient, so we'll have to fall back to the long path.
451        let processor_path_kind = if self.get_processor(short_type_path).is_ok() {
452            MetaTypePathKind::Short
453        } else {
454            MetaTypePathKind::Long
455        };
456
457        let meta = processor.default_meta(processor_path_kind);
458        let serialized_meta = meta.serialize();
459
460        let source = self.get_source(path.source())?;
461
462        // Note: we get the reader rather than the processed reader, since we want to write the meta
463        // file for the unprocessed version of that asset (so it will be processed by the default
464        // processor).
465        let reader = source.reader();
466        match reader.read_meta_bytes(path.path()).await {
467            Ok(_) => return Err(WriteDefaultMetaError::MetaAlreadyExists),
468            Err(AssetReaderError::NotFound(_)) => {
469                // The meta file couldn't be found so just fall through.
470            }
471            Err(AssetReaderError::Io(err)) => {
472                return Err(WriteDefaultMetaError::IoErrorFromExistingMetaCheck(err))
473            }
474            Err(AssetReaderError::HttpError(err)) => {
475                return Err(WriteDefaultMetaError::HttpErrorFromExistingMetaCheck(err))
476            }
477        }
478
479        let writer = source.writer()?;
480        writer
481            .write_meta_bytes(path.path(), &serialized_meta)
482            .await?;
483
484        Ok(())
485    }
486
487    async fn handle_asset_source_event(
488        &self,
489        source: &AssetSource,
490        event: AssetSourceEvent,
491        new_task_sender: &async_channel::Sender<(AssetSourceId<'static>, PathBuf)>,
492    ) {
493        trace!("{event:?}");
494        match event {
495            AssetSourceEvent::AddedAsset(path)
496            | AssetSourceEvent::AddedMeta(path)
497            | AssetSourceEvent::ModifiedAsset(path)
498            | AssetSourceEvent::ModifiedMeta(path) => {
499                let _ = new_task_sender.send((source.id(), path)).await;
500            }
501            AssetSourceEvent::RemovedAsset(path) => {
502                self.handle_removed_asset(source, path).await;
503            }
504            AssetSourceEvent::RemovedMeta(path) => {
505                self.handle_removed_meta(source, path, new_task_sender)
506                    .await;
507            }
508            AssetSourceEvent::AddedFolder(path) => {
509                self.handle_added_folder(source, path, new_task_sender)
510                    .await;
511            }
512            // NOTE: As a heads up for future devs: this event shouldn't be run in parallel with other events that might
513            // touch this folder (ex: the folder might be re-created with new assets). Clean up the old state first.
514            // Currently this event handler is not parallel, but it could be (and likely should be) in the future.
515            AssetSourceEvent::RemovedFolder(path) => {
516                self.handle_removed_folder(source, &path).await;
517            }
518            AssetSourceEvent::RenamedAsset { old, new } => {
519                // If there was a rename event, but the path hasn't changed, this asset might need reprocessing.
520                // Sometimes this event is returned when an asset is moved "back" into the asset folder
521                if old == new {
522                    let _ = new_task_sender.send((source.id(), new)).await;
523                } else {
524                    self.handle_renamed_asset(source, old, new, new_task_sender)
525                        .await;
526                }
527            }
528            AssetSourceEvent::RenamedMeta { old, new } => {
529                // If there was a rename event, but the path hasn't changed, this asset meta might need reprocessing.
530                // Sometimes this event is returned when an asset meta is moved "back" into the asset folder
531                if old == new {
532                    let _ = new_task_sender.send((source.id(), new)).await;
533                } else {
534                    debug!("Meta renamed from {old:?} to {new:?}");
535                    // Renaming meta should not assume that an asset has also been renamed. Check both old and new assets to see
536                    // if they should be re-imported (and/or have new meta generated)
537                    let _ = new_task_sender.send((source.id(), old)).await;
538                    let _ = new_task_sender.send((source.id(), new)).await;
539                }
540            }
541            AssetSourceEvent::RenamedFolder { old, new } => {
542                // If there was a rename event, but the path hasn't changed, this asset folder might need reprocessing.
543                // Sometimes this event is returned when an asset meta is moved "back" into the asset folder
544                if old == new {
545                    self.handle_added_folder(source, new, new_task_sender).await;
546                } else {
547                    // PERF: this reprocesses everything in the moved folder. this is not necessary in most cases, but
548                    // requires some nuance when it comes to path handling.
549                    self.handle_removed_folder(source, &old).await;
550                    self.handle_added_folder(source, new, new_task_sender).await;
551                }
552            }
553            AssetSourceEvent::RemovedUnknown { path, is_meta } => {
554                let processed_reader = source.ungated_processed_reader().unwrap();
555                match processed_reader.is_directory(&path).await {
556                    Ok(is_directory) => {
557                        if is_directory {
558                            self.handle_removed_folder(source, &path).await;
559                        } else if is_meta {
560                            self.handle_removed_meta(source, path, new_task_sender)
561                                .await;
562                        } else {
563                            self.handle_removed_asset(source, path).await;
564                        }
565                    }
566                    Err(err) => {
567                        match err {
568                            AssetReaderError::NotFound(_) => {
569                                // if the path is not found, a processed version does not exist
570                            }
571                            AssetReaderError::Io(err) => {
572                                error!(
573                                    "Path '{}' was removed, but the destination reader could not determine if it \
574                                    was a folder or a file due to the following error: {err}",
575                                    AssetPath::from_path(&path).with_source(source.id())
576                                );
577                            }
578                            AssetReaderError::HttpError(status) => {
579                                error!(
580                                    "Path '{}' was removed, but the destination reader could not determine if it \
581                                    was a folder or a file due to receiving an unexpected HTTP Status {status}",
582                                    AssetPath::from_path(&path).with_source(source.id())
583                                );
584                            }
585                        }
586                    }
587                }
588            }
589        }
590    }
591
592    async fn handle_added_folder(
593        &self,
594        source: &AssetSource,
595        path: PathBuf,
596        new_task_sender: &async_channel::Sender<(AssetSourceId<'static>, PathBuf)>,
597    ) {
598        debug!(
599            "Folder {} was added. Attempting to re-process",
600            AssetPath::from_path(&path).with_source(source.id())
601        );
602        self.queue_processing_tasks_for_folder(source, path, new_task_sender)
603            .await
604            .unwrap();
605    }
606
607    /// Responds to a removed meta event by reprocessing the asset at the given path.
608    async fn handle_removed_meta(
609        &self,
610        source: &AssetSource,
611        path: PathBuf,
612        new_task_sender: &async_channel::Sender<(AssetSourceId<'static>, PathBuf)>,
613    ) {
614        // If meta was removed, we might need to regenerate it.
615        // Likewise, the user might be manually re-adding the asset.
616        // Therefore, we shouldn't automatically delete the asset ... that is a
617        // user-initiated action.
618        debug!(
619            "Meta for asset {} was removed. Attempting to re-process",
620            AssetPath::from_path(&path).with_source(source.id())
621        );
622        let _ = new_task_sender.send((source.id(), path)).await;
623    }
624
625    /// Removes all processed assets stored at the given path (respecting transactionality), then removes the folder itself.
626    async fn handle_removed_folder(&self, source: &AssetSource, path: &Path) {
627        debug!(
628            "Removing folder {} because source was removed",
629            path.display()
630        );
631        let processed_reader = source.ungated_processed_reader().unwrap();
632        match processed_reader.read_directory(path).await {
633            Ok(mut path_stream) => {
634                while let Some(child_path) = path_stream.next().await {
635                    self.handle_removed_asset(source, child_path).await;
636                }
637            }
638            Err(err) => match err {
639                AssetReaderError::NotFound(_err) => {
640                    // The processed folder does not exist. No need to update anything
641                }
642                AssetReaderError::HttpError(status) => {
643                    self.log_unrecoverable().await;
644                    error!(
645                        "Unrecoverable Error: Failed to read the processed assets at {path:?} in order to remove assets that no longer exist \
646                        in the source directory. Restart the asset processor to fully reprocess assets. HTTP Status Code {status}"
647                    );
648                }
649                AssetReaderError::Io(err) => {
650                    self.log_unrecoverable().await;
651                    error!(
652                        "Unrecoverable Error: Failed to read the processed assets at {path:?} in order to remove assets that no longer exist \
653                        in the source directory. Restart the asset processor to fully reprocess assets. Error: {err}"
654                    );
655                }
656            },
657        }
658        let processed_writer = source.processed_writer().unwrap();
659        if let Err(err) = processed_writer.remove_directory(path).await {
660            match err {
661                AssetWriterError::Io(err) => {
662                    // we can ignore NotFound because if the "final" file in a folder was removed
663                    // then we automatically clean up this folder
664                    if err.kind() != ErrorKind::NotFound {
665                        let asset_path = AssetPath::from_path(path).with_source(source.id());
666                        error!("Failed to remove destination folder that no longer exists in {asset_path}: {err}");
667                    }
668                }
669            }
670        }
671    }
672
673    /// Removes the processed version of an asset and associated in-memory metadata. This will block until all existing reads/writes to the
674    /// asset have finished, thanks to the `file_transaction_lock`.
675    async fn handle_removed_asset(&self, source: &AssetSource, path: PathBuf) {
676        let asset_path = AssetPath::from(path).with_source(source.id());
677        debug!("Removing processed {asset_path} because source was removed");
678        let lock = {
679            // Scope the infos lock so we don't hold up other processing for too long.
680            let mut infos = self.data.processing_state.asset_infos.write().await;
681            infos.remove(&asset_path).await
682        };
683        let Some(lock) = lock else {
684            return;
685        };
686
687        // we must wait for uncontested write access to the asset source to ensure existing
688        // readers/writers can finish their operations
689        let _write_lock = lock.write();
690        self.remove_processed_asset_and_meta(source, asset_path.path())
691            .await;
692    }
693
694    /// Handles a renamed source asset by moving its processed results to the new location and updating in-memory paths + metadata.
695    /// This will cause direct path dependencies to break.
696    async fn handle_renamed_asset(
697        &self,
698        source: &AssetSource,
699        old: PathBuf,
700        new: PathBuf,
701        new_task_sender: &async_channel::Sender<(AssetSourceId<'static>, PathBuf)>,
702    ) {
703        let old = AssetPath::from(old).with_source(source.id());
704        let new = AssetPath::from(new).with_source(source.id());
705        let processed_writer = source.processed_writer().unwrap();
706        let result = {
707            // Scope the infos lock so we don't hold up other processing for too long.
708            let mut infos = self.data.processing_state.asset_infos.write().await;
709            infos.rename(&old, &new, new_task_sender).await
710        };
711        let Some((old_lock, new_lock)) = result else {
712            return;
713        };
714        // we must wait for uncontested write access to both assets to ensure existing
715        // readers/writers can finish their operations
716        let _old_write_lock = old_lock.write();
717        let _new_write_lock = new_lock.write();
718        processed_writer
719            .rename(old.path(), new.path())
720            .await
721            .unwrap();
722        processed_writer
723            .rename_meta(old.path(), new.path())
724            .await
725            .unwrap();
726    }
727
728    async fn queue_processing_tasks_for_folder(
729        &self,
730        source: &AssetSource,
731        path: PathBuf,
732        new_task_sender: &async_channel::Sender<(AssetSourceId<'static>, PathBuf)>,
733    ) -> Result<(), AssetReaderError> {
734        if source.reader().is_directory(&path).await? {
735            let mut path_stream = source.reader().read_directory(&path).await?;
736            while let Some(path) = path_stream.next().await {
737                Box::pin(self.queue_processing_tasks_for_folder(source, path, new_task_sender))
738                    .await?;
739            }
740        } else {
741            let _ = new_task_sender.send((source.id(), path)).await;
742        }
743        Ok(())
744    }
745
746    /// Register a new asset processor.
747    pub fn register_processor<P: Process>(&self, processor: P) {
748        let mut processors = self
749            .data
750            .processors
751            .write()
752            .unwrap_or_else(PoisonError::into_inner);
753        let processor = Arc::new(processor);
754        processors
755            .type_path_to_processor
756            .insert(P::type_path(), processor.clone());
757        match processors
758            .short_type_path_to_processor
759            .entry(P::short_type_path())
760        {
761            Entry::Vacant(entry) => {
762                entry.insert(ShortTypeProcessorEntry::Unique {
763                    type_path: P::type_path(),
764                    processor,
765                });
766            }
767            Entry::Occupied(mut entry) => match entry.get_mut() {
768                ShortTypeProcessorEntry::Unique { type_path, .. } => {
769                    let type_path = *type_path;
770                    *entry.get_mut() =
771                        ShortTypeProcessorEntry::Ambiguous(vec![type_path, P::type_path()]);
772                }
773                ShortTypeProcessorEntry::Ambiguous(type_paths) => {
774                    type_paths.push(P::type_path());
775                }
776            },
777        }
778    }
779
780    /// Set the default processor for the given `extension`. Make sure `P` is registered with [`AssetProcessor::register_processor`].
781    pub fn set_default_processor<P: Process>(&self, extension: &str) {
782        let mut processors = self
783            .data
784            .processors
785            .write()
786            .unwrap_or_else(PoisonError::into_inner);
787        processors
788            .file_extension_to_default_processor
789            .insert(extension.into(), P::type_path());
790    }
791
792    /// Returns the default processor for the given `extension`, if it exists.
793    pub fn get_default_processor(&self, extension: &str) -> Option<Arc<dyn ErasedProcessor>> {
794        let processors = self
795            .data
796            .processors
797            .read()
798            .unwrap_or_else(PoisonError::into_inner);
799        let key = processors
800            .file_extension_to_default_processor
801            .get(extension)?;
802        processors.type_path_to_processor.get(key).cloned()
803    }
804
805    /// Returns the processor with the given `processor_type_name`, if it exists.
806    pub fn get_processor(
807        &self,
808        processor_type_name: &str,
809    ) -> Result<Arc<dyn ErasedProcessor>, GetProcessorError> {
810        let processors = self
811            .data
812            .processors
813            .read()
814            .unwrap_or_else(PoisonError::into_inner);
815        if let Some(short_type_processor) = processors
816            .short_type_path_to_processor
817            .get(processor_type_name)
818        {
819            return match short_type_processor {
820                ShortTypeProcessorEntry::Unique { processor, .. } => Ok(processor.clone()),
821                ShortTypeProcessorEntry::Ambiguous(examples) => Err(GetProcessorError::Ambiguous {
822                    processor_short_name: processor_type_name.to_owned(),
823                    ambiguous_processor_names: examples.clone(),
824                }),
825            };
826        }
827        processors
828            .type_path_to_processor
829            .get(processor_type_name)
830            .cloned()
831            .ok_or_else(|| GetProcessorError::Missing(processor_type_name.to_owned()))
832    }
833
834    /// Populates the initial view of each asset by scanning the unprocessed and processed asset folders.
835    /// This info will later be used to determine whether or not to re-process an asset
836    ///
837    /// This will validate transactions and recover failed transactions when necessary.
838    async fn initialize(&self) -> Result<(), InitializeError> {
839        self.validate_transaction_log_and_recover().await;
840        let mut asset_infos = self.data.processing_state.asset_infos.write().await;
841
842        /// Retrieves asset paths recursively. If `clean_empty_folders_writer` is Some, it will be used to clean up empty
843        /// folders when they are discovered.
844        async fn get_asset_paths(
845            reader: &dyn ErasedAssetReader,
846            path: PathBuf,
847            paths: &mut Vec<PathBuf>,
848            mut empty_dirs: Option<&mut Vec<PathBuf>>,
849        ) -> Result<bool, AssetReaderError> {
850            if reader.is_directory(&path).await? {
851                let mut path_stream = reader.read_directory(&path).await?;
852                let mut contains_files = false;
853
854                while let Some(child_path) = path_stream.next().await {
855                    contains_files |= Box::pin(get_asset_paths(
856                        reader,
857                        child_path,
858                        paths,
859                        empty_dirs.as_deref_mut(),
860                    ))
861                    .await?;
862                }
863                // Add the current directory after all its subdirectories so we delete any empty
864                // subdirectories before the current directory.
865                if !contains_files
866                    && path.parent().is_some()
867                    && let Some(empty_dirs) = empty_dirs
868                {
869                    empty_dirs.push(path);
870                }
871                Ok(contains_files)
872            } else {
873                paths.push(path);
874                Ok(true)
875            }
876        }
877
878        for source in self.sources().iter_processed() {
879            let Some(processed_reader) = source.ungated_processed_reader() else {
880                continue;
881            };
882            let Ok(processed_writer) = source.processed_writer() else {
883                continue;
884            };
885            let mut unprocessed_paths = Vec::new();
886            get_asset_paths(
887                source.reader(),
888                PathBuf::from(""),
889                &mut unprocessed_paths,
890                None,
891            )
892            .await
893            .map_err(InitializeError::FailedToReadSourcePaths)?;
894
895            let mut processed_paths = Vec::new();
896            let mut empty_dirs = Vec::new();
897            get_asset_paths(
898                processed_reader,
899                PathBuf::from(""),
900                &mut processed_paths,
901                Some(&mut empty_dirs),
902            )
903            .await
904            .map_err(InitializeError::FailedToReadDestinationPaths)?;
905
906            // Remove any empty directories from the processed path. Note: this has to happen after
907            // we fetch all the paths, otherwise the path stream can skip over paths
908            // (we're modifying a collection while iterating through it).
909            for empty_dir in empty_dirs {
910                // We don't care if this succeeds, since it's just a cleanup task. It is best-effort
911                let _ = processed_writer.remove_empty_directory(&empty_dir).await;
912            }
913
914            for path in unprocessed_paths {
915                asset_infos.get_or_insert(AssetPath::from(path).with_source(source.id()));
916            }
917
918            for path in processed_paths {
919                let mut dependencies = Vec::new();
920                let asset_path = AssetPath::from(path).with_source(source.id());
921                if let Some(info) = asset_infos.get_mut(&asset_path) {
922                    match processed_reader.read_meta_bytes(asset_path.path()).await {
923                        Ok(meta_bytes) => {
924                            match ron::de::from_bytes::<ProcessedInfoMinimal>(&meta_bytes) {
925                                Ok(minimal) => {
926                                    trace!(
927                                        "Populated processed info for asset {asset_path} {:?}",
928                                        minimal.processed_info
929                                    );
930
931                                    if let Some(processed_info) = &minimal.processed_info {
932                                        for process_dependency_info in
933                                            &processed_info.process_dependencies
934                                        {
935                                            dependencies.push(process_dependency_info.path.clone());
936                                        }
937                                    }
938                                    info.processed_info = minimal.processed_info;
939                                }
940                                Err(err) => {
941                                    trace!("Removing processed data for {asset_path} because meta could not be parsed: {err}");
942                                    self.remove_processed_asset_and_meta(source, asset_path.path())
943                                        .await;
944                                }
945                            }
946                        }
947                        Err(err) => {
948                            trace!("Removing processed data for {asset_path} because meta failed to load: {err}");
949                            self.remove_processed_asset_and_meta(source, asset_path.path())
950                                .await;
951                        }
952                    }
953                } else {
954                    trace!("Removing processed data for non-existent asset {asset_path}");
955                    self.remove_processed_asset_and_meta(source, asset_path.path())
956                        .await;
957                }
958
959                for dependency in dependencies {
960                    asset_infos.add_dependent(&dependency, asset_path.clone());
961                }
962            }
963        }
964
965        self.data
966            .processing_state
967            .set_state(ProcessorState::Processing)
968            .await;
969
970        Ok(())
971    }
972
973    /// Removes the processed version of an asset and its metadata, if it exists. This _is not_ transactional like `remove_processed_asset_transactional`, nor
974    /// does it remove existing in-memory metadata.
975    async fn remove_processed_asset_and_meta(&self, source: &AssetSource, path: &Path) {
976        if let Err(err) = source.processed_writer().unwrap().remove(path).await {
977            warn!("Failed to remove non-existent asset {path:?}: {err}");
978        }
979
980        if let Err(err) = source.processed_writer().unwrap().remove_meta(path).await {
981            warn!("Failed to remove non-existent meta {path:?}: {err}");
982        }
983
984        self.clean_empty_processed_ancestor_folders(source, path)
985            .await;
986    }
987
988    async fn clean_empty_processed_ancestor_folders(&self, source: &AssetSource, mut path: &Path) {
989        // As a safety precaution don't delete absolute paths to avoid deleting folders outside of the destination folder
990        if path.is_absolute() {
991            error!("Attempted to clean up ancestor folders of an absolute path. This is unsafe so the operation was skipped.");
992            return;
993        }
994        while let Some(parent) = path.parent() {
995            path = parent;
996            if parent == Path::new("") {
997                break;
998            }
999            if source
1000                .processed_writer()
1001                .unwrap()
1002                .remove_empty_directory(parent)
1003                .await
1004                .is_err()
1005            {
1006                // if we fail to delete a folder, stop walking up the tree
1007                break;
1008            }
1009        }
1010    }
1011
1012    /// Processes the asset (if it has not already been processed or the asset source has changed).
1013    /// If the asset has "process dependencies" (relies on the values of other assets), it will asynchronously await until
1014    /// the dependencies have been processed (See [`ProcessorGatedReader`], which is used in the [`AssetProcessor`]'s [`AssetServer`]
1015    /// to block reads until the asset is processed).
1016    ///
1017    /// [`LoadContext`]: crate::loader::LoadContext
1018    /// [`ProcessorGatedReader`]: crate::io::processor_gated::ProcessorGatedReader
1019    async fn process_asset(
1020        &self,
1021        source: &AssetSource,
1022        path: PathBuf,
1023        processor_task_event: async_channel::Sender<(AssetSourceId<'static>, PathBuf)>,
1024    ) {
1025        let asset_path = AssetPath::from(path).with_source(source.id());
1026        let result = self.process_asset_internal(source, &asset_path).await;
1027        let mut infos = self.data.processing_state.asset_infos.write().await;
1028        infos
1029            .finish_processing(asset_path, result, processor_task_event)
1030            .await;
1031    }
1032
1033    async fn process_asset_internal(
1034        &self,
1035        source: &AssetSource,
1036        asset_path: &AssetPath<'static>,
1037    ) -> Result<ProcessResult, ProcessError> {
1038        // TODO: check if already processing to protect against duplicate hot-reload events
1039        debug!("Processing {}", asset_path);
1040        let server = &self.server;
1041        let path = asset_path.path();
1042        let reader = source.reader();
1043
1044        let reader_err = |err| ProcessError::AssetReaderError {
1045            path: asset_path.clone(),
1046            err,
1047        };
1048        let writer_err = |err| ProcessError::AssetWriterError {
1049            path: asset_path.clone(),
1050            err,
1051        };
1052
1053        let (mut source_meta, meta_bytes, processor) = match reader.read_meta_bytes(path).await {
1054            Ok(meta_bytes) => {
1055                let minimal: AssetMetaMinimal = ron::de::from_bytes(&meta_bytes).map_err(|e| {
1056                    ProcessError::DeserializeMetaError(DeserializeMetaError::DeserializeMinimal(e))
1057                })?;
1058                let (meta, processor) = match minimal.asset {
1059                    AssetActionMinimal::Load { loader } => {
1060                        let loader = server.get_asset_loader_with_type_name(&loader).await?;
1061                        let meta = loader.deserialize_meta(&meta_bytes)?;
1062                        (meta, None)
1063                    }
1064                    AssetActionMinimal::Process { processor } => {
1065                        let processor = self.get_processor(&processor)?;
1066                        let meta = processor.deserialize_meta(&meta_bytes)?;
1067                        (meta, Some(processor))
1068                    }
1069                    AssetActionMinimal::Ignore => {
1070                        return Ok(ProcessResult::Ignored);
1071                    }
1072                };
1073                (meta, meta_bytes, processor)
1074            }
1075            Err(AssetReaderError::NotFound(_path)) => {
1076                let (meta, processor) = if let Some(processor) = asset_path
1077                    .get_full_extension()
1078                    .and_then(|ext| self.get_default_processor(ext))
1079                {
1080                    // Note: It doesn't matter whether we use the Long or Short kind, since we're
1081                    // returning the processor here anyway, and we're only using this meta to pass
1082                    // along the processor settings.
1083                    let meta = processor.default_meta(MetaTypePathKind::Long);
1084                    (meta, Some(processor))
1085                } else {
1086                    match server.get_path_asset_loader(asset_path.clone()).await {
1087                        Ok(loader) => (loader.default_meta(), None),
1088                        Err(MissingAssetLoaderForExtensionError { .. }) => {
1089                            let meta: Box<dyn AssetMetaDyn> =
1090                                Box::new(AssetMeta::<(), ()>::new(AssetAction::Ignore));
1091                            (meta, None)
1092                        }
1093                    }
1094                };
1095                let meta_bytes = meta.serialize();
1096                (meta, meta_bytes, processor)
1097            }
1098            Err(err) => {
1099                return Err(ProcessError::ReadAssetMetaError {
1100                    path: asset_path.clone(),
1101                    err,
1102                })
1103            }
1104        };
1105
1106        let processed_writer = source.processed_writer()?;
1107
1108        let new_hash = {
1109            // Create a reader just for computing the hash. Keep this scoped here so that we drop it
1110            // as soon as the hash is computed.
1111            let mut reader_for_hash = reader.read(path).await.map_err(reader_err)?;
1112
1113            get_asset_hash(&meta_bytes, &mut reader_for_hash)
1114                .await
1115                .map_err(reader_err)?
1116        };
1117        let mut new_processed_info = ProcessedInfo {
1118            hash: new_hash,
1119            full_hash: new_hash,
1120            process_dependencies: Vec::new(),
1121        };
1122
1123        {
1124            let infos = self.data.processing_state.asset_infos.read().await;
1125            if let Some(current_processed_info) = infos
1126                .get(asset_path)
1127                .and_then(|i| i.processed_info.as_ref())
1128                && current_processed_info.hash == new_hash
1129            {
1130                let mut dependency_changed = false;
1131                for current_dep_info in &current_processed_info.process_dependencies {
1132                    let live_hash = infos
1133                        .get(&current_dep_info.path)
1134                        .and_then(|i| i.processed_info.as_ref())
1135                        .map(|i| i.full_hash);
1136                    if live_hash != Some(current_dep_info.full_hash) {
1137                        dependency_changed = true;
1138                        break;
1139                    }
1140                }
1141                if !dependency_changed {
1142                    return Ok(ProcessResult::SkippedNotChanged);
1143                }
1144            }
1145        }
1146
1147        // Note: this lock must remain alive until all processed asset and meta writes have finished (or failed)
1148        // See ProcessedAssetInfo::file_transaction_lock docs for more info
1149        let _transaction_lock = {
1150            let lock = {
1151                let mut infos = self.data.processing_state.asset_infos.write().await;
1152                let info = infos.get_or_insert(asset_path.clone());
1153                // Clone out the transaction lock first and then lock after we've dropped the
1154                // asset_infos. Otherwise, trying to lock a single path can block all other paths to
1155                // (leading to deadlocks).
1156                info.file_transaction_lock.clone()
1157            };
1158            lock.write_arc().await
1159        };
1160
1161        // NOTE: if processing the asset fails this will produce an "unfinished" log entry, forcing a rebuild on next run.
1162        // Directly writing to the asset destination in the processor necessitates this behavior
1163        // TODO: this class of failure can be recovered via re-processing + smarter log validation that allows for duplicate transactions in the event of failures
1164        self.log_begin_processing(asset_path).await;
1165        if let Some(processor) = processor {
1166            // Unwrap is ok since we have a processor, so the `AssetAction` must have been
1167            // `AssetAction::Process` (which includes its settings).
1168            let settings = source_meta.process_settings().unwrap();
1169
1170            // Create a reader just for the actual process. Note: this means that we're performing
1171            // two reads for the same file (but we avoid having to load the whole file into memory).
1172            // For some sources (like local file systems), this is not a big deal, but for other
1173            // sources like an HTTP asset sources, this could be an entire additional download (if
1174            // the asset source doesn't do any caching). In practice, most sources being processed
1175            // are likely to be local, and processing in general is a publish-time operation, so
1176            // it's not likely to be too big a deal. If in the future, we decide we want to avoid
1177            // this repeated read, we could "ask" the asset source if it prefers avoiding repeated
1178            // reads or not.
1179            let reader_for_process = reader.read(path).await.map_err(reader_err)?;
1180
1181            let mut writer = processed_writer.write(path).await.map_err(writer_err)?;
1182            let mut processed_meta = {
1183                let mut context = ProcessContext::new(
1184                    self,
1185                    asset_path,
1186                    reader_for_process,
1187                    &mut new_processed_info,
1188                );
1189                let process = processor.process(&mut context, settings, &mut *writer);
1190                #[cfg(feature = "trace")]
1191                let process = {
1192                    let span = info_span!(
1193                        "asset processing",
1194                        processor = processor.type_path(),
1195                        asset = asset_path.to_string(),
1196                    );
1197                    process.instrument(span)
1198                };
1199                process.await?
1200            };
1201
1202            writer
1203                .flush()
1204                .await
1205                .map_err(|e| ProcessError::AssetWriterError {
1206                    path: asset_path.clone(),
1207                    err: AssetWriterError::Io(e),
1208                })?;
1209
1210            let full_hash = get_full_asset_hash(
1211                new_hash,
1212                new_processed_info
1213                    .process_dependencies
1214                    .iter()
1215                    .map(|i| i.full_hash),
1216            );
1217            new_processed_info.full_hash = full_hash;
1218            *processed_meta.processed_info_mut() = Some(new_processed_info.clone());
1219            let meta_bytes = processed_meta.serialize();
1220
1221            processed_writer
1222                .write_meta_bytes(path, &meta_bytes)
1223                .await
1224                .map_err(writer_err)?;
1225        } else {
1226            // See the reasoning for processing why it's ok to do a second read here.
1227            let mut reader_for_copy = reader.read(path).await.map_err(reader_err)?;
1228            let mut writer = processed_writer.write(path).await.map_err(writer_err)?;
1229            futures_lite::io::copy(&mut reader_for_copy, &mut writer)
1230                .await
1231                .map_err(|err| ProcessError::AssetWriterError {
1232                    path: asset_path.clone_owned(),
1233                    err: err.into(),
1234                })?;
1235            *source_meta.processed_info_mut() = Some(new_processed_info.clone());
1236            let meta_bytes = source_meta.serialize();
1237            processed_writer
1238                .write_meta_bytes(path, &meta_bytes)
1239                .await
1240                .map_err(writer_err)?;
1241        }
1242        self.log_end_processing(asset_path).await;
1243
1244        Ok(ProcessResult::Processed(new_processed_info))
1245    }
1246
1247    async fn validate_transaction_log_and_recover(&self) {
1248        let log_factory = self
1249            .data
1250            .log_factory
1251            .lock()
1252            .unwrap_or_else(PoisonError::into_inner)
1253            // Take the log factory to indicate we've started and this should disable setting a new
1254            // log factory.
1255            .take()
1256            .expect("the asset processor only starts once");
1257        if let Err(err) = validate_transaction_log(log_factory.as_ref()).await {
1258            let state_is_valid = match err {
1259                ValidateLogError::ReadLogError(err) => {
1260                    error!("Failed to read processor log file. Processed assets cannot be validated so they must be re-generated {err}");
1261                    false
1262                }
1263                ValidateLogError::UnrecoverableError => {
1264                    error!("Encountered an unrecoverable error in the last run. Processed assets cannot be validated so they must be re-generated");
1265                    false
1266                }
1267                ValidateLogError::EntryErrors(entry_errors) => {
1268                    let mut state_is_valid = true;
1269                    for entry_error in entry_errors {
1270                        match entry_error {
1271                            LogEntryError::DuplicateTransaction(_)
1272                            | LogEntryError::EndedMissingTransaction(_) => {
1273                                error!("{}", entry_error);
1274                                state_is_valid = false;
1275                                break;
1276                            }
1277                            LogEntryError::UnfinishedTransaction(path) => {
1278                                debug!("Asset {path:?} did not finish processing. Clearing state for that asset");
1279                                let mut unrecoverable_err = |message: &dyn core::fmt::Display| {
1280                                    error!("Failed to remove asset {path:?}: {message}");
1281                                    state_is_valid = false;
1282                                };
1283                                let Ok(source) = self.get_source(path.source()) else {
1284                                    unrecoverable_err(&"AssetSource does not exist");
1285                                    continue;
1286                                };
1287                                let Ok(processed_writer) = source.processed_writer() else {
1288                                    unrecoverable_err(&"AssetSource does not have a processed AssetWriter registered");
1289                                    continue;
1290                                };
1291
1292                                if let Err(err) = processed_writer.remove(path.path()).await {
1293                                    match err {
1294                                        AssetWriterError::Io(err) => {
1295                                            // any error but NotFound means we could be in a bad state
1296                                            if err.kind() != ErrorKind::NotFound {
1297                                                unrecoverable_err(&err);
1298                                            }
1299                                        }
1300                                    }
1301                                }
1302                                if let Err(err) = processed_writer.remove_meta(path.path()).await {
1303                                    match err {
1304                                        AssetWriterError::Io(err) => {
1305                                            // any error but NotFound means we could be in a bad state
1306                                            if err.kind() != ErrorKind::NotFound {
1307                                                unrecoverable_err(&err);
1308                                            }
1309                                        }
1310                                    }
1311                                }
1312                            }
1313                        }
1314                    }
1315                    state_is_valid
1316                }
1317            };
1318
1319            if !state_is_valid {
1320                error!("Processed asset transaction log state was invalid and unrecoverable for some reason (see previous logs). Removing processed assets and starting fresh.");
1321                for source in self.sources().iter_processed() {
1322                    let Ok(processed_writer) = source.processed_writer() else {
1323                        continue;
1324                    };
1325                    if let Err(err) = processed_writer
1326                        .remove_assets_in_directory(Path::new(""))
1327                        .await
1328                    {
1329                        panic!("Processed assets were in a bad state. To correct this, the asset processor attempted to remove all processed assets and start from scratch. This failed. There is no way to continue. Try restarting, or deleting imported asset folder manually. {err}");
1330                    }
1331                }
1332            }
1333        }
1334        let mut log = self.data.log.write().await;
1335        *log = match log_factory.create_new_log().await {
1336            Ok(log) => Some(log),
1337            Err(err) => panic!("Failed to initialize asset processor log. This cannot be recovered. Try restarting. If that doesn't work, try deleting processed asset folder. {}", err),
1338        };
1339    }
1340}
1341
1342impl AssetProcessorData {
1343    /// Initializes a new [`AssetProcessorData`] using the given [`AssetSources`].
1344    pub(crate) fn new(sources: Arc<AssetSources>, processing_state: Arc<ProcessingState>) -> Self {
1345        AssetProcessorData {
1346            processing_state,
1347            sources,
1348            log_factory: Mutex::new(Some(Box::new(FileTransactionLogFactory::default()))),
1349            log: Default::default(),
1350            processors: Default::default(),
1351        }
1352    }
1353
1354    /// Sets the transaction log factory for the processor.
1355    ///
1356    /// If this is called after asset processing has begun (in the `Startup` schedule), it will
1357    /// return an error. If not called, the default transaction log will be used.
1358    pub fn set_log_factory(
1359        &self,
1360        factory: Box<dyn ProcessorTransactionLogFactory>,
1361    ) -> Result<(), SetTransactionLogFactoryError> {
1362        let mut log_factory = self
1363            .log_factory
1364            .lock()
1365            .unwrap_or_else(PoisonError::into_inner);
1366        if log_factory.is_none() {
1367            // This indicates the asset processor has already started, so setting the factory does
1368            // nothing here.
1369            return Err(SetTransactionLogFactoryError::AlreadyInUse);
1370        }
1371
1372        *log_factory = Some(factory);
1373        Ok(())
1374    }
1375
1376    /// Returns a future that will not finish until the path has been processed.
1377    pub async fn wait_until_processed(&self, path: AssetPath<'static>) -> ProcessStatus {
1378        self.processing_state.wait_until_processed(path).await
1379    }
1380
1381    /// Returns a future that will not finish until the processor has been initialized.
1382    pub async fn wait_until_initialized(&self) {
1383        self.processing_state.wait_until_initialized().await;
1384    }
1385
1386    /// Returns a future that will not finish until processing has finished.
1387    pub async fn wait_until_finished(&self) {
1388        self.processing_state.wait_until_finished().await;
1389    }
1390}
1391
1392impl ProcessingState {
1393    /// Creates a new empty processing state.
1394    fn new() -> Self {
1395        let (mut initialized_sender, initialized_receiver) = async_broadcast::broadcast(1);
1396        let (mut finished_sender, finished_receiver) = async_broadcast::broadcast(1);
1397        // allow overflow on these "one slot" channels to allow receivers to retrieve the "latest" state, and to allow senders to
1398        // not block if there was older state present.
1399        initialized_sender.set_overflow(true);
1400        finished_sender.set_overflow(true);
1401
1402        Self {
1403            state: async_lock::RwLock::new(ProcessorState::Initializing),
1404            initialized_sender,
1405            initialized_receiver,
1406            finished_sender,
1407            finished_receiver,
1408            asset_infos: Default::default(),
1409        }
1410    }
1411
1412    /// Sets the overall state of processing and broadcasts appropriate events.
1413    async fn set_state(&self, state: ProcessorState) {
1414        let mut state_guard = self.state.write().await;
1415        let last_state = *state_guard;
1416        *state_guard = state;
1417        if last_state != ProcessorState::Finished && state == ProcessorState::Finished {
1418            self.finished_sender.broadcast(()).await.unwrap();
1419        } else if last_state != ProcessorState::Processing && state == ProcessorState::Processing {
1420            self.initialized_sender.broadcast(()).await.unwrap();
1421        }
1422    }
1423
1424    /// Retrieves the current [`ProcessorState`]
1425    pub(crate) async fn get_state(&self) -> ProcessorState {
1426        *self.state.read().await
1427    }
1428
1429    /// Gets a "transaction lock" that can be used to ensure no writes to asset or asset meta occur
1430    /// while it is held.
1431    pub(crate) async fn get_transaction_lock(
1432        &self,
1433        path: &AssetPath<'static>,
1434    ) -> Result<RwLockReadGuardArc<()>, AssetReaderError> {
1435        let lock = {
1436            let infos = self.asset_infos.read().await;
1437            let info = infos
1438                .get(path)
1439                .ok_or_else(|| AssetReaderError::NotFound(path.path().to_owned()))?;
1440            // Clone out the transaction lock first and then lock after we've dropped the
1441            // asset_infos. Otherwise, trying to lock a single path can block all other paths to
1442            // (leading to deadlocks).
1443            info.file_transaction_lock.clone()
1444        };
1445        Ok(lock.read_arc().await)
1446    }
1447
1448    /// Returns a future that will not finish until the path has been processed.
1449    pub(crate) async fn wait_until_processed(&self, path: AssetPath<'static>) -> ProcessStatus {
1450        self.wait_until_initialized().await;
1451        let mut receiver = {
1452            let infos = self.asset_infos.write().await;
1453            let info = infos.get(&path);
1454            match info {
1455                Some(info) => match info.status {
1456                    Some(result) => return result,
1457                    // This receiver must be created prior to losing the read lock to ensure this is transactional
1458                    None => info.status_receiver.clone(),
1459                },
1460                None => return ProcessStatus::NonExistent,
1461            }
1462        };
1463        receiver.recv().await.unwrap()
1464    }
1465
1466    /// Returns a future that will not finish until the processor has been initialized.
1467    pub(crate) async fn wait_until_initialized(&self) {
1468        let receiver = {
1469            let state = self.state.read().await;
1470            match *state {
1471                ProcessorState::Initializing => {
1472                    // This receiver must be created prior to losing the read lock to ensure this is transactional
1473                    Some(self.initialized_receiver.clone())
1474                }
1475                _ => None,
1476            }
1477        };
1478
1479        if let Some(mut receiver) = receiver {
1480            receiver.recv().await.unwrap();
1481        }
1482    }
1483
1484    /// Returns a future that will not finish until processing has finished.
1485    pub(crate) async fn wait_until_finished(&self) {
1486        let receiver = {
1487            let state = self.state.read().await;
1488            match *state {
1489                ProcessorState::Initializing | ProcessorState::Processing => {
1490                    // This receiver must be created prior to losing the read lock to ensure this is transactional
1491                    Some(self.finished_receiver.clone())
1492                }
1493                ProcessorState::Finished => None,
1494            }
1495        };
1496
1497        if let Some(mut receiver) = receiver {
1498            receiver.recv().await.unwrap();
1499        }
1500    }
1501}
1502
1503/// The (successful) result of processing an asset
1504#[derive(Debug, Clone)]
1505pub enum ProcessResult {
1506    Processed(ProcessedInfo),
1507    SkippedNotChanged,
1508    Ignored,
1509}
1510
1511/// The final status of processing an asset
1512#[derive(Debug, PartialEq, Eq, Copy, Clone)]
1513pub enum ProcessStatus {
1514    Processed,
1515    Failed,
1516    NonExistent,
1517}
1518
1519// NOTE: if you add new fields to this struct, make sure they are propagated (when relevant) in ProcessorAssetInfos::rename
1520#[derive(Debug)]
1521pub(crate) struct ProcessorAssetInfo {
1522    processed_info: Option<ProcessedInfo>,
1523    /// Paths of assets that depend on this asset when they are being processed.
1524    dependents: HashSet<AssetPath<'static>>,
1525    status: Option<ProcessStatus>,
1526    /// A lock that controls read/write access to processed asset files. The lock is shared for both the asset bytes and the meta bytes.
1527    /// _This lock must be locked whenever a read or write to processed assets occurs_
1528    /// There are scenarios where processed assets (and their metadata) are being read and written in multiple places at once:
1529    /// * when the processor is running in parallel with an app
1530    /// * when processing assets in parallel, the processor might read an asset's `process_dependencies` when processing new versions of those dependencies
1531    ///     * this second scenario almost certainly isn't possible with the current implementation, but its worth protecting against
1532    ///
1533    /// This lock defends against those scenarios by ensuring readers don't read while processed files are being written. And it ensures
1534    /// Because this lock is shared across meta and asset bytes, readers can ensure they don't read "old" versions of metadata with "new" asset data.
1535    pub(crate) file_transaction_lock: Arc<async_lock::RwLock<()>>,
1536    status_sender: async_broadcast::Sender<ProcessStatus>,
1537    status_receiver: async_broadcast::Receiver<ProcessStatus>,
1538}
1539
1540impl Default for ProcessorAssetInfo {
1541    fn default() -> Self {
1542        let (mut status_sender, status_receiver) = async_broadcast::broadcast(1);
1543        // allow overflow on these "one slot" channels to allow receivers to retrieve the "latest" state, and to allow senders to
1544        // not block if there was older state present.
1545        status_sender.set_overflow(true);
1546        Self {
1547            processed_info: Default::default(),
1548            dependents: Default::default(),
1549            file_transaction_lock: Default::default(),
1550            status: None,
1551            status_sender,
1552            status_receiver,
1553        }
1554    }
1555}
1556
1557impl ProcessorAssetInfo {
1558    async fn update_status(&mut self, status: ProcessStatus) {
1559        if self.status != Some(status) {
1560            self.status = Some(status);
1561            self.status_sender.broadcast(status).await.unwrap();
1562        }
1563    }
1564}
1565
1566/// The "current" in memory view of the asset space. This is "eventually consistent". It does not directly
1567/// represent the state of assets in storage, but rather a valid historical view that will gradually become more
1568/// consistent as events are processed.
1569#[derive(Default, Debug)]
1570pub struct ProcessorAssetInfos {
1571    /// The "current" in memory view of the asset space. During processing, if path does not exist in this, it should
1572    /// be considered non-existent.
1573    /// NOTE: YOU MUST USE `Self::get_or_insert` or `Self::insert` TO ADD ITEMS TO THIS COLLECTION TO ENSURE
1574    /// `non_existent_dependents` DATA IS CONSUMED
1575    infos: HashMap<AssetPath<'static>, ProcessorAssetInfo>,
1576    /// Dependents for assets that don't exist. This exists to track "dangling" asset references due to deleted / missing files.
1577    /// If the dependent asset is added, it can "resolve" these dependencies and re-compute those assets.
1578    /// Therefore this _must_ always be consistent with the `infos` data. If a new asset is added to `infos`, it should
1579    /// check this maps for dependencies and add them. If an asset is removed, it should update the dependents here.
1580    non_existent_dependents: HashMap<AssetPath<'static>, HashSet<AssetPath<'static>>>,
1581}
1582
1583impl ProcessorAssetInfos {
1584    fn get_or_insert(&mut self, asset_path: AssetPath<'static>) -> &mut ProcessorAssetInfo {
1585        self.infos.entry(asset_path.clone()).or_insert_with(|| {
1586            let mut info = ProcessorAssetInfo::default();
1587            // track existing dependents by resolving existing "hanging" dependents.
1588            if let Some(dependents) = self.non_existent_dependents.remove(&asset_path) {
1589                info.dependents = dependents;
1590            }
1591            info
1592        })
1593    }
1594
1595    pub(crate) fn get(&self, asset_path: &AssetPath<'static>) -> Option<&ProcessorAssetInfo> {
1596        self.infos.get(asset_path)
1597    }
1598
1599    fn get_mut(&mut self, asset_path: &AssetPath<'static>) -> Option<&mut ProcessorAssetInfo> {
1600        self.infos.get_mut(asset_path)
1601    }
1602
1603    fn add_dependent(&mut self, asset_path: &AssetPath<'static>, dependent: AssetPath<'static>) {
1604        if let Some(info) = self.get_mut(asset_path) {
1605            info.dependents.insert(dependent);
1606        } else {
1607            let dependents = self
1608                .non_existent_dependents
1609                .entry(asset_path.clone())
1610                .or_default();
1611            dependents.insert(dependent);
1612        }
1613    }
1614
1615    /// Finalize processing the asset, which will incorporate the result of the processed asset into the in-memory view the processed assets.
1616    async fn finish_processing(
1617        &mut self,
1618        asset_path: AssetPath<'static>,
1619        result: Result<ProcessResult, ProcessError>,
1620        reprocess_sender: async_channel::Sender<(AssetSourceId<'static>, PathBuf)>,
1621    ) {
1622        match result {
1623            Ok(ProcessResult::Processed(processed_info)) => {
1624                debug!("Finished processing \"{}\"", asset_path);
1625                // clean up old dependents
1626                let old_processed_info = self
1627                    .infos
1628                    .get_mut(&asset_path)
1629                    .and_then(|i| i.processed_info.take());
1630                if let Some(old_processed_info) = old_processed_info {
1631                    self.clear_dependencies(&asset_path, old_processed_info);
1632                }
1633
1634                // populate new dependents
1635                for process_dependency_info in &processed_info.process_dependencies {
1636                    self.add_dependent(&process_dependency_info.path, asset_path.to_owned());
1637                }
1638                let info = self.get_or_insert(asset_path);
1639                info.processed_info = Some(processed_info);
1640                info.update_status(ProcessStatus::Processed).await;
1641                let dependents = info.dependents.iter().cloned().collect::<Vec<_>>();
1642                for path in dependents {
1643                    let _ = reprocess_sender
1644                        .send((path.source().clone_owned(), path.path().to_owned()))
1645                        .await;
1646                }
1647            }
1648            Ok(ProcessResult::SkippedNotChanged) => {
1649                debug!("Skipping processing (unchanged) \"{}\"", asset_path);
1650                let info = self.get_mut(&asset_path).expect("info should exist");
1651                // NOTE: skipping an asset on a given pass doesn't mean it won't change in the future as a result
1652                // of a dependency being re-processed. This means apps might receive an "old" (but valid) asset first.
1653                // This is in the interest of fast startup times that don't block for all assets being checked + reprocessed
1654                // Therefore this relies on hot-reloading in the app to pickup the "latest" version of the asset
1655                // If "block until latest state is reflected" is required, we can easily add a less granular
1656                // "block until first pass finished" mode
1657                info.update_status(ProcessStatus::Processed).await;
1658            }
1659            Ok(ProcessResult::Ignored) => {
1660                debug!("Skipping processing (ignored) \"{}\"", asset_path);
1661            }
1662            Err(ProcessError::ExtensionRequired) => {
1663                // Skip assets without extensions
1664            }
1665            Err(ProcessError::MissingAssetLoaderForExtension(_)) => {
1666                trace!("No loader found for {asset_path}");
1667            }
1668            Err(ProcessError::AssetReaderError {
1669                err: AssetReaderError::NotFound(_),
1670                ..
1671            }) => {
1672                // if there is no asset source, no processing can be done
1673                trace!("No need to process asset {asset_path} because it does not exist");
1674            }
1675            Err(err) => {
1676                error!("Failed to process asset {asset_path}: {err}");
1677                // if this failed because a dependency could not be loaded, make sure it is reprocessed if that dependency is reprocessed
1678                if let ProcessError::AssetLoadError(AssetLoadError::AssetLoaderError(dependency)) =
1679                    err
1680                {
1681                    let info = self.get_mut(&asset_path).expect("info should exist");
1682                    info.processed_info = Some(ProcessedInfo {
1683                        hash: AssetHash::default(),
1684                        full_hash: AssetHash::default(),
1685                        process_dependencies: vec![],
1686                    });
1687                    self.add_dependent(dependency.path(), asset_path.to_owned());
1688                }
1689
1690                let info = self.get_mut(&asset_path).expect("info should exist");
1691                info.update_status(ProcessStatus::Failed).await;
1692            }
1693        }
1694    }
1695
1696    /// Remove the info for the given path. This should only happen if an asset's source is
1697    /// removed/non-existent. Returns the transaction lock for the asset, or [`None`] if the asset
1698    /// path does not exist.
1699    async fn remove(
1700        &mut self,
1701        asset_path: &AssetPath<'static>,
1702    ) -> Option<Arc<async_lock::RwLock<()>>> {
1703        let info = self.infos.remove(asset_path)?;
1704        if let Some(processed_info) = info.processed_info {
1705            self.clear_dependencies(asset_path, processed_info);
1706        }
1707        // Tell all listeners this asset does not exist
1708        info.status_sender
1709            .broadcast(ProcessStatus::NonExistent)
1710            .await
1711            .unwrap();
1712        if !info.dependents.is_empty() {
1713            error!(
1714                    "The asset at {asset_path} was removed, but it had assets that depend on it to be processed. Consider updating the path in the following assets: {:?}",
1715                    info.dependents
1716                );
1717            self.non_existent_dependents
1718                .insert(asset_path.clone(), info.dependents);
1719        }
1720
1721        Some(info.file_transaction_lock)
1722    }
1723
1724    /// Remove the info for the old path, and move over its info to the new path. This should only
1725    /// happen if an asset's source is removed/non-existent. Returns the transaction locks for the
1726    /// old and new assets respectively, or [`None`] if the old path does not exist.
1727    async fn rename(
1728        &mut self,
1729        old: &AssetPath<'static>,
1730        new: &AssetPath<'static>,
1731        new_task_sender: &async_channel::Sender<(AssetSourceId<'static>, PathBuf)>,
1732    ) -> Option<(Arc<async_lock::RwLock<()>>, Arc<async_lock::RwLock<()>>)> {
1733        let mut info = self.infos.remove(old)?;
1734        if !info.dependents.is_empty() {
1735            // TODO: We can't currently ensure "moved" folders with relative paths aren't broken because AssetPath
1736            // doesn't distinguish between absolute and relative paths. We have "erased" relativeness. In the short term,
1737            // we could do "remove everything in a folder and re-add", but that requires full rebuilds / destroying the cache.
1738            // If processors / loaders could enumerate dependencies, we could check if the new deps line up with a rename.
1739            // If deps encoded "relativeness" as part of loading, that would also work (this seems like the right call).
1740            // TODO: it would be nice to log an error here for dependents that aren't also being moved + fixed.
1741            // (see the remove impl).
1742            error!(
1743                    "The asset at {old} was removed, but it had assets that depend on it to be processed. Consider updating the path in the following assets: {:?}",
1744                    info.dependents
1745                );
1746            self.non_existent_dependents
1747                .insert(old.clone(), core::mem::take(&mut info.dependents));
1748        }
1749        if let Some(processed_info) = &info.processed_info {
1750            // Update "dependent" lists for this asset's "process dependencies" to use new path.
1751            for dep in &processed_info.process_dependencies {
1752                if let Some(info) = self.infos.get_mut(&dep.path) {
1753                    info.dependents.remove(old);
1754                    info.dependents.insert(new.clone());
1755                } else if let Some(dependents) = self.non_existent_dependents.get_mut(&dep.path) {
1756                    dependents.remove(old);
1757                    dependents.insert(new.clone());
1758                }
1759            }
1760        }
1761        // Tell all listeners this asset no longer exists
1762        info.status_sender
1763            .broadcast(ProcessStatus::NonExistent)
1764            .await
1765            .unwrap();
1766        let new_info = self.get_or_insert(new.clone());
1767        new_info.processed_info = info.processed_info;
1768        new_info.status = info.status;
1769        // Ensure things waiting on the new path are informed of the status of this asset
1770        if let Some(status) = new_info.status {
1771            new_info.status_sender.broadcast(status).await.unwrap();
1772        }
1773        let dependents = new_info.dependents.iter().cloned().collect::<Vec<_>>();
1774        // Queue the asset for a reprocess check, in case it needs new meta.
1775        let _ = new_task_sender
1776            .send((new.source().clone_owned(), new.path().to_owned()))
1777            .await;
1778        for dependent in dependents {
1779            // Queue dependents for reprocessing because they might have been waiting for this asset.
1780            let _ = new_task_sender
1781                .send((
1782                    dependent.source().clone_owned(),
1783                    dependent.path().to_owned(),
1784                ))
1785                .await;
1786        }
1787
1788        Some((
1789            info.file_transaction_lock,
1790            new_info.file_transaction_lock.clone(),
1791        ))
1792    }
1793
1794    fn clear_dependencies(&mut self, asset_path: &AssetPath<'static>, removed_info: ProcessedInfo) {
1795        for old_load_dep in removed_info.process_dependencies {
1796            if let Some(info) = self.infos.get_mut(&old_load_dep.path) {
1797                info.dependents.remove(asset_path);
1798            } else if let Some(dependents) =
1799                self.non_existent_dependents.get_mut(&old_load_dep.path)
1800            {
1801                dependents.remove(asset_path);
1802            }
1803        }
1804    }
1805}
1806
1807/// The current state of the [`AssetProcessor`].
1808#[derive(Copy, Clone, PartialEq, Eq)]
1809pub enum ProcessorState {
1810    /// The processor is still initializing, which involves scanning the current asset folders,
1811    /// constructing an in-memory view of the asset space, recovering from previous errors / crashes,
1812    /// and cleaning up old / unused assets.
1813    Initializing,
1814    /// The processor is currently processing assets.
1815    Processing,
1816    /// The processor has finished processing all valid assets and reporting invalid assets.
1817    Finished,
1818}
1819
1820/// An error that occurs when initializing the [`AssetProcessor`].
1821#[derive(Error, Debug)]
1822pub enum InitializeError {
1823    #[error(transparent)]
1824    FailedToReadSourcePaths(AssetReaderError),
1825    #[error(transparent)]
1826    FailedToReadDestinationPaths(AssetReaderError),
1827    #[error("Failed to validate asset log: {0}")]
1828    ValidateLogError(#[from] ValidateLogError),
1829}
1830
1831/// An error when attempting to set the transaction log factory.
1832#[derive(Error, Debug)]
1833pub enum SetTransactionLogFactoryError {
1834    /// The transaction log is already in use, so setting the factory does nothing.
1835    #[error("Transaction log is already in use so setting the factory does nothing")]
1836    AlreadyInUse,
1837}
1838
1839/// An error when retrieving an asset processor.
1840#[derive(Error, Debug, PartialEq, Eq)]
1841pub enum GetProcessorError {
1842    /// The processor of that name does not exist.
1843    #[error("The processor '{0}' does not exist")]
1844    Missing(String),
1845    /// The given short name is ambiguous between several processors.
1846    #[error("The processor '{processor_short_name}' is ambiguous between several processors: {ambiguous_processor_names:?}")]
1847    Ambiguous {
1848        /// The given string for the processor name.
1849        processor_short_name: String,
1850        /// The list of processors that might match it.
1851        ambiguous_processor_names: Vec<&'static str>,
1852    },
1853}
1854
1855impl From<GetProcessorError> for ProcessError {
1856    fn from(err: GetProcessorError) -> Self {
1857        match err {
1858            GetProcessorError::Missing(name) => Self::MissingProcessor(name),
1859            GetProcessorError::Ambiguous {
1860                processor_short_name,
1861                ambiguous_processor_names,
1862            } => Self::AmbiguousProcessor {
1863                processor_short_name,
1864                ambiguous_processor_names,
1865            },
1866        }
1867    }
1868}
1869
1870#[cfg(test)]
1871mod tests;