diff --git a/packages/app-lib/src/state/instances/commands/create_instance.rs b/packages/app-lib/src/state/instances/commands/create_instance.rs index 4f381f2816..7567ec815c 100644 --- a/packages/app-lib/src/state/instances/commands/create_instance.rs +++ b/packages/app-lib/src/state/instances/commands/create_instance.rs @@ -106,6 +106,7 @@ pub(crate) async fn create_instance( tx.commit().await?; crate::state::instances::watcher::watch_instance_folder( + &instance.id, &instance.path, &state.file_watcher, &state.directories, diff --git a/packages/app-lib/src/state/instances/watcher.rs b/packages/app-lib/src/state/instances/watcher.rs index 65e0067cc3..08647a5dc3 100644 --- a/packages/app-lib/src/state/instances/watcher.rs +++ b/packages/app-lib/src/state/instances/watcher.rs @@ -7,15 +7,20 @@ use crate::state::{ use crate::worlds::WorldType; use notify::{RecommendedWatcher, RecursiveMode}; use notify_debouncer_mini::{DebounceEventResult, Debouncer, new_debouncer}; -use std::time::Duration; +use std::{collections::HashMap, sync::Arc, time::Duration}; use tokio::sync::{RwLock, mpsc::channel}; use super::adapters::sqlite::instance_rows; -pub type FileWatcher = RwLock>; +pub struct FileWatcher { + watcher: RwLock>, + instance_ids: Arc>>, +} pub async fn init_watcher() -> crate::Result { let (tx, mut rx) = channel(1); + let instance_ids = Arc::new(RwLock::new(HashMap::new())); + let event_instance_ids = instance_ids.clone(); let file_watcher = new_debouncer( Duration::from_secs_f32(1.0), @@ -32,88 +37,85 @@ pub async fn init_watcher() -> crate::Result { match res { Ok(events) => { + let instance_ids = event_instance_ids.read().await; let mut visited_instances = Vec::new(); - events.iter().for_each(|e| { - let mut instance_path = None; + for e in &events { + let mut instance_path = None; - let mut found = false; - for component in e.path.components() { - if found { - instance_path = Some(component.as_os_str()); - break; - } + let mut found = false; + for component in e.path.components() { + if found { + instance_path = Some(component.as_os_str()); + break; + } - if component.as_os_str() - == crate::state::dirs::INSTANCES_FOLDER_NAME - { - found = true; - } - } + if component.as_os_str() + == crate::state::dirs::INSTANCES_FOLDER_NAME + { + found = true; + } + } - if let Some(instance_path) = instance_path { - let instance_path_str = - instance_path.to_string_lossy().to_string(); - let first_file_name = e - .path - .components() - .skip_while(|x| x.as_os_str() != instance_path) - .nth(1) - .map(|x| x.as_os_str()); - if first_file_name - .as_ref() - .is_some_and(|x| *x == "crash-reports") - && e.path - .extension() - .as_ref() - .is_some_and(|x| *x == "txt") - { - crash_task(instance_path_str); - } else if !visited_instances.contains(&instance_path) - { - let event = if first_file_name - .as_ref() - .is_some_and(|x| *x == "servers.dat") - { - Some(InstancePayloadType::ServersUpdated) - } else if first_file_name.as_ref().is_some_and(|x| { - *x == "saves" - && e.path - .file_name() - .as_ref() - .is_some_and(|x| *x == "level.dat") - }) { - tracing::info!( - "World updated: {}", - e.path.display() - ); - let world = e - .path - .parent() - .unwrap() - .file_name() - .unwrap() - .to_string_lossy() - .to_string(); - if !e.path.is_file() { - let instance_path_str = instance_path_str.clone(); - let world = world.clone(); - tokio::spawn(async move { - if let Ok(state) = State::get().await { - let instance_id = sqlx::query_scalar!( - " - SELECT id - FROM instances - WHERE path = ? - ", - instance_path_str, - ) - .fetch_optional(&state.pool) - .await; - let Ok(Some(instance_id)) = instance_id else { - return; - }; - if let Err(e) = attached_world_data::AttachedWorldData::remove_for_world( + if let Some(instance_path) = instance_path { + let instance_path_str = + instance_path.to_string_lossy().to_string(); + let Some(instance_id) = + instance_ids.get(&instance_path_str).cloned() + else { + continue; + }; + let first_file_name = e + .path + .components() + .skip_while(|x| x.as_os_str() != instance_path) + .nth(1) + .map(|x| x.as_os_str()); + if first_file_name + .as_ref() + .is_some_and(|x| *x == "crash-reports") + && e.path + .extension() + .as_ref() + .is_some_and(|x| *x == "txt") + { + crash_task(instance_id); + } else if !visited_instances.contains(&instance_id) + { + let event = if first_file_name + .as_ref() + .is_some_and(|x| *x == "servers.dat") + { + Some(InstancePayloadType::ServersUpdated) + } else if first_file_name.as_ref().is_some_and( + |x| { + *x == "saves" + && e.path + .file_name() + .as_ref() + .is_some_and(|x| { + *x == "level.dat" + }) + }, + ) { + tracing::info!( + "World updated: {}", + e.path.display() + ); + let world = e + .path + .parent() + .unwrap() + .file_name() + .unwrap() + .to_string_lossy() + .to_string(); + if !e.path.is_file() { + let instance_id = instance_id.clone(); + let world = world.clone(); + tokio::spawn(async move { + if let Ok(state) = State::get().await + && let Err(e) = attached_world_data::AttachedWorldData::remove_for_world( &instance_id, WorldType::Singleplayer, &world, @@ -121,68 +123,69 @@ pub async fn init_watcher() -> crate::Result { ).await { tracing::warn!("Failed to remove AttachedWorldData for '{world}': {e}") } - } - }); - } - Some(InstancePayloadType::WorldUpdated { world }) - } else if first_file_name - .as_ref() - .is_none_or(|x| *x != "saves") - { - Some(InstancePayloadType::Synced) - } else { - None - }; - if let Some(event) = event { - tokio::spawn(async move { - let _ = emit_instance( - &instance_path_str, - event, - ) - .await; - }); - visited_instances.push(instance_path); - } - } - } - }); + }); + } + Some(InstancePayloadType::WorldUpdated { + world, + }) + } else if first_file_name + .as_ref() + .is_none_or(|x| *x != "saves") + { + Some(InstancePayloadType::Synced) + } else { + None + }; + if let Some(event) = event { + let emit_instance_id = instance_id.clone(); + tokio::spawn(async move { + let _ = emit_instance( + &emit_instance_id, + event, + ) + .await; + }); + visited_instances.push(instance_id); + } + } + } + } } Err(error) => tracing::warn!("Unable to watch file: {error}"), } } }); - Ok(RwLock::new(file_watcher)) + Ok(FileWatcher { + watcher: RwLock::new(file_watcher), + instance_ids, + }) } pub(crate) async fn watch_instances_init( watcher: &FileWatcher, dirs: &DirectoryInfo, + pool: &sqlx::SqlitePool, ) { - let Ok(mut instances_dir) = tokio::fs::read_dir(dirs.instances_dir()).await - else { + let Ok(instances) = instance_rows::list_instances(pool).await else { return; }; - while let Ok(Some(instance_dir)) = instances_dir.next_entry().await { - let file_name = instance_dir.file_name(); - let file_name = file_name.to_string_lossy(); - if file_name.starts_with(".DS_Store") { - continue; - } - - watch_instance_folder(&file_name, watcher, dirs).await; + for instance in instances { + watch_instance_folder(&instance.id, &instance.path, watcher, dirs) + .await; } } pub(crate) async fn watch_instance_folder( + instance_id: &str, instance_path: &str, watcher: &FileWatcher, dirs: &DirectoryInfo, ) { - let instance_path = dirs.instances_dir().join(instance_path); + let full_instance_path = dirs.instances_dir().join(instance_path); - let Ok(metadata) = tokio::fs::metadata(&instance_path).await else { + let Ok(metadata) = tokio::fs::metadata(&full_instance_path).await else { return; }; @@ -195,7 +198,7 @@ pub(crate) async fn watch_instance_folder( .map(|x| x.get_folder()) .chain(["crash-reports", "saves"]) { - let full_path = instance_path.join(sub_path); + let full_path = full_instance_path.join(sub_path); let meta = tokio::fs::symlink_metadata(&full_path).await; let exists = meta.is_ok(); @@ -215,10 +218,11 @@ pub(crate) async fn watch_instance_folder( to_watch.push(full_path); } - let mut watcher = watcher.write().await; + let mut debouncer = watcher.watcher.write().await; for full_path in &to_watch { - if let Err(e) = - watcher.watcher().watch(full_path, RecursiveMode::Recursive) + if let Err(e) = debouncer + .watcher() + .watch(full_path, RecursiveMode::Recursive) { tracing::error!( "Failed to watch directory for watcher {full_path:?}: {e}" @@ -227,22 +231,29 @@ pub(crate) async fn watch_instance_folder( } } - if let Err(e) = watcher + if let Err(e) = debouncer .watcher() - .watch(&instance_path, RecursiveMode::NonRecursive) + .watch(&full_instance_path, RecursiveMode::NonRecursive) { tracing::error!( - "Failed to watch root instance directory for watcher {instance_path:?}: {e}" + "Failed to watch root instance directory for watcher {full_instance_path:?}: {e}" ); } + + watcher + .instance_ids + .write() + .await + .insert(instance_path.to_string(), instance_id.to_string()); } -fn crash_task(path: String) { +fn crash_task(instance_id: String) { tokio::task::spawn(async move { let res = async { let state = State::get().await?; let Some(instance) = - instance_rows::get_instance_by_path(&path, &state.pool).await? + instance_rows::get_instance_by_id(&instance_id, &state.pool) + .await? else { return Ok(()); }; diff --git a/packages/app-lib/src/state/mod.rs b/packages/app-lib/src/state/mod.rs index 0a0b496900..0852c31475 100644 --- a/packages/app-lib/src/state/mod.rs +++ b/packages/app-lib/src/state/mod.rs @@ -107,6 +107,7 @@ impl State { instances::watcher::watch_instances_init( &state.file_watcher, &state.directories, + &state.pool, ) .await;