fix: change fs watcher to rely more on instance ids than path (#6547)

* fix: change fs watcher to rely more on instance ids than path

* fix: fmt
This commit is contained in:
Calum H.
2026-06-29 14:38:44 +00:00
committed by GitHub
parent 4a972bca73
commit f6e6ac28d4
3 changed files with 139 additions and 126 deletions
@@ -106,6 +106,7 @@ pub(crate) async fn create_instance(
tx.commit().await?; tx.commit().await?;
crate::state::instances::watcher::watch_instance_folder( crate::state::instances::watcher::watch_instance_folder(
&instance.id,
&instance.path, &instance.path,
&state.file_watcher, &state.file_watcher,
&state.directories, &state.directories,
+137 -126
View File
@@ -7,15 +7,20 @@ use crate::state::{
use crate::worlds::WorldType; use crate::worlds::WorldType;
use notify::{RecommendedWatcher, RecursiveMode}; use notify::{RecommendedWatcher, RecursiveMode};
use notify_debouncer_mini::{DebounceEventResult, Debouncer, new_debouncer}; 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 tokio::sync::{RwLock, mpsc::channel};
use super::adapters::sqlite::instance_rows; use super::adapters::sqlite::instance_rows;
pub type FileWatcher = RwLock<Debouncer<RecommendedWatcher>>; pub struct FileWatcher {
watcher: RwLock<Debouncer<RecommendedWatcher>>,
instance_ids: Arc<RwLock<HashMap<String, String>>>,
}
pub async fn init_watcher() -> crate::Result<FileWatcher> { pub async fn init_watcher() -> crate::Result<FileWatcher> {
let (tx, mut rx) = channel(1); 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( let file_watcher = new_debouncer(
Duration::from_secs_f32(1.0), Duration::from_secs_f32(1.0),
@@ -32,88 +37,85 @@ pub async fn init_watcher() -> crate::Result<FileWatcher> {
match res { match res {
Ok(events) => { Ok(events) => {
let instance_ids = event_instance_ids.read().await;
let mut visited_instances = Vec::new(); let mut visited_instances = Vec::new();
events.iter().for_each(|e| { for e in &events {
let mut instance_path = None; let mut instance_path = None;
let mut found = false; let mut found = false;
for component in e.path.components() { for component in e.path.components() {
if found { if found {
instance_path = Some(component.as_os_str()); instance_path = Some(component.as_os_str());
break; break;
} }
if component.as_os_str() if component.as_os_str()
== crate::state::dirs::INSTANCES_FOLDER_NAME == crate::state::dirs::INSTANCES_FOLDER_NAME
{ {
found = true; found = true;
} }
} }
if let Some(instance_path) = instance_path { if let Some(instance_path) = instance_path {
let instance_path_str = let instance_path_str =
instance_path.to_string_lossy().to_string(); instance_path.to_string_lossy().to_string();
let first_file_name = e let Some(instance_id) =
.path instance_ids.get(&instance_path_str).cloned()
.components() else {
.skip_while(|x| x.as_os_str() != instance_path) continue;
.nth(1) };
.map(|x| x.as_os_str()); let first_file_name = e
if first_file_name .path
.as_ref() .components()
.is_some_and(|x| *x == "crash-reports") .skip_while(|x| x.as_os_str() != instance_path)
&& e.path .nth(1)
.extension() .map(|x| x.as_os_str());
.as_ref() if first_file_name
.is_some_and(|x| *x == "txt") .as_ref()
{ .is_some_and(|x| *x == "crash-reports")
crash_task(instance_path_str); && e.path
} else if !visited_instances.contains(&instance_path) .extension()
{ .as_ref()
let event = if first_file_name .is_some_and(|x| *x == "txt")
.as_ref() {
.is_some_and(|x| *x == "servers.dat") crash_task(instance_id);
{ } else if !visited_instances.contains(&instance_id)
Some(InstancePayloadType::ServersUpdated) {
} else if first_file_name.as_ref().is_some_and(|x| { let event = if first_file_name
*x == "saves" .as_ref()
&& e.path .is_some_and(|x| *x == "servers.dat")
.file_name() {
.as_ref() Some(InstancePayloadType::ServersUpdated)
.is_some_and(|x| *x == "level.dat") } else if first_file_name.as_ref().is_some_and(
}) { |x| {
tracing::info!( *x == "saves"
"World updated: {}", && e.path
e.path.display() .file_name()
); .as_ref()
let world = e .is_some_and(|x| {
.path *x == "level.dat"
.parent() })
.unwrap() },
.file_name() ) {
.unwrap() tracing::info!(
.to_string_lossy() "World updated: {}",
.to_string(); e.path.display()
if !e.path.is_file() { );
let instance_path_str = instance_path_str.clone(); let world = e
let world = world.clone(); .path
tokio::spawn(async move { .parent()
if let Ok(state) = State::get().await { .unwrap()
let instance_id = sqlx::query_scalar!( .file_name()
" .unwrap()
SELECT id .to_string_lossy()
FROM instances .to_string();
WHERE path = ? if !e.path.is_file() {
", let instance_id = instance_id.clone();
instance_path_str, let world = world.clone();
) tokio::spawn(async move {
.fetch_optional(&state.pool) if let Ok(state) = State::get().await
.await; && let Err(e) = attached_world_data::AttachedWorldData::remove_for_world(
let Ok(Some(instance_id)) = instance_id else {
return;
};
if let Err(e) = attached_world_data::AttachedWorldData::remove_for_world(
&instance_id, &instance_id,
WorldType::Singleplayer, WorldType::Singleplayer,
&world, &world,
@@ -121,68 +123,69 @@ pub async fn init_watcher() -> crate::Result<FileWatcher> {
).await { ).await {
tracing::warn!("Failed to remove AttachedWorldData for '{world}': {e}") tracing::warn!("Failed to remove AttachedWorldData for '{world}': {e}")
} }
} });
}); }
} Some(InstancePayloadType::WorldUpdated {
Some(InstancePayloadType::WorldUpdated { world }) world,
} else if first_file_name })
.as_ref() } else if first_file_name
.is_none_or(|x| *x != "saves") .as_ref()
{ .is_none_or(|x| *x != "saves")
Some(InstancePayloadType::Synced) {
} else { Some(InstancePayloadType::Synced)
None } else {
}; None
if let Some(event) = event { };
tokio::spawn(async move { if let Some(event) = event {
let _ = emit_instance( let emit_instance_id = instance_id.clone();
&instance_path_str, tokio::spawn(async move {
event, let _ = emit_instance(
) &emit_instance_id,
.await; event,
}); )
visited_instances.push(instance_path); .await;
} });
} visited_instances.push(instance_id);
} }
}); }
}
}
} }
Err(error) => tracing::warn!("Unable to watch file: {error}"), 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( pub(crate) async fn watch_instances_init(
watcher: &FileWatcher, watcher: &FileWatcher,
dirs: &DirectoryInfo, dirs: &DirectoryInfo,
pool: &sqlx::SqlitePool,
) { ) {
let Ok(mut instances_dir) = tokio::fs::read_dir(dirs.instances_dir()).await let Ok(instances) = instance_rows::list_instances(pool).await else {
else {
return; return;
}; };
while let Ok(Some(instance_dir)) = instances_dir.next_entry().await { for instance in instances {
let file_name = instance_dir.file_name(); watch_instance_folder(&instance.id, &instance.path, watcher, dirs)
let file_name = file_name.to_string_lossy(); .await;
if file_name.starts_with(".DS_Store") {
continue;
}
watch_instance_folder(&file_name, watcher, dirs).await;
} }
} }
pub(crate) async fn watch_instance_folder( pub(crate) async fn watch_instance_folder(
instance_id: &str,
instance_path: &str, instance_path: &str,
watcher: &FileWatcher, watcher: &FileWatcher,
dirs: &DirectoryInfo, 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; return;
}; };
@@ -195,7 +198,7 @@ pub(crate) async fn watch_instance_folder(
.map(|x| x.get_folder()) .map(|x| x.get_folder())
.chain(["crash-reports", "saves"]) .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 meta = tokio::fs::symlink_metadata(&full_path).await;
let exists = meta.is_ok(); let exists = meta.is_ok();
@@ -215,10 +218,11 @@ pub(crate) async fn watch_instance_folder(
to_watch.push(full_path); to_watch.push(full_path);
} }
let mut watcher = watcher.write().await; let mut debouncer = watcher.watcher.write().await;
for full_path in &to_watch { for full_path in &to_watch {
if let Err(e) = if let Err(e) = debouncer
watcher.watcher().watch(full_path, RecursiveMode::Recursive) .watcher()
.watch(full_path, RecursiveMode::Recursive)
{ {
tracing::error!( tracing::error!(
"Failed to watch directory for watcher {full_path:?}: {e}" "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() .watcher()
.watch(&instance_path, RecursiveMode::NonRecursive) .watch(&full_instance_path, RecursiveMode::NonRecursive)
{ {
tracing::error!( 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 { tokio::task::spawn(async move {
let res = async { let res = async {
let state = State::get().await?; let state = State::get().await?;
let Some(instance) = 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 { else {
return Ok(()); return Ok(());
}; };
+1
View File
@@ -107,6 +107,7 @@ impl State {
instances::watcher::watch_instances_init( instances::watcher::watch_instances_init(
&state.file_watcher, &state.file_watcher,
&state.directories, &state.directories,
&state.pool,
) )
.await; .await;