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,
+62 -51
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,9 +37,10 @@ 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;
@@ -54,6 +60,11 @@ pub async fn init_watcher() -> crate::Result<FileWatcher> {
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 Some(instance_id) =
instance_ids.get(&instance_path_str).cloned()
else {
continue;
};
let first_file_name = e let first_file_name = e
.path .path
.components() .components()
@@ -68,21 +79,25 @@ pub async fn init_watcher() -> crate::Result<FileWatcher> {
.as_ref() .as_ref()
.is_some_and(|x| *x == "txt") .is_some_and(|x| *x == "txt")
{ {
crash_task(instance_path_str); crash_task(instance_id);
} else if !visited_instances.contains(&instance_path) } else if !visited_instances.contains(&instance_id)
{ {
let event = if first_file_name let event = if first_file_name
.as_ref() .as_ref()
.is_some_and(|x| *x == "servers.dat") .is_some_and(|x| *x == "servers.dat")
{ {
Some(InstancePayloadType::ServersUpdated) Some(InstancePayloadType::ServersUpdated)
} else if first_file_name.as_ref().is_some_and(|x| { } else if first_file_name.as_ref().is_some_and(
|x| {
*x == "saves" *x == "saves"
&& e.path && e.path
.file_name() .file_name()
.as_ref() .as_ref()
.is_some_and(|x| *x == "level.dat") .is_some_and(|x| {
}) { *x == "level.dat"
})
},
) {
tracing::info!( tracing::info!(
"World updated: {}", "World updated: {}",
e.path.display() e.path.display()
@@ -96,24 +111,11 @@ pub async fn init_watcher() -> crate::Result<FileWatcher> {
.to_string_lossy() .to_string_lossy()
.to_string(); .to_string();
if !e.path.is_file() { if !e.path.is_file() {
let instance_path_str = instance_path_str.clone(); let instance_id = instance_id.clone();
let world = world.clone(); let world = world.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Ok(state) = State::get().await { if let Ok(state) = State::get().await
let instance_id = sqlx::query_scalar!( && let Err(e) = attached_world_data::AttachedWorldData::remove_for_world(
"
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(
&instance_id, &instance_id,
WorldType::Singleplayer, WorldType::Singleplayer,
&world, &world,
@@ -121,10 +123,11 @@ 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 { world }) Some(InstancePayloadType::WorldUpdated {
world,
})
} else if first_file_name } else if first_file_name
.as_ref() .as_ref()
.is_none_or(|x| *x != "saves") .is_none_or(|x| *x != "saves")
@@ -134,55 +137,55 @@ pub async fn init_watcher() -> crate::Result<FileWatcher> {
None None
}; };
if let Some(event) = event { if let Some(event) = event {
let emit_instance_id = instance_id.clone();
tokio::spawn(async move { tokio::spawn(async move {
let _ = emit_instance( let _ = emit_instance(
&instance_path_str, &emit_instance_id,
event, event,
) )
.await; .await;
}); });
visited_instances.push(instance_path); 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;