Compare commits

...
Author SHA1 Message Date
Calum H. (IMB11) f2c25d21c3 fix: fmt 2026-06-29 14:56:16 +01:00
Calum H. (IMB11) d46cdf9b2a fix: change fs watcher to rely more on instance ids than path 2026-06-29 14:54:58 +01:00
3 changed files with 139 additions and 126 deletions
@@ -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,
+137 -126
View File
@@ -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<Debouncer<RecommendedWatcher>>;
pub struct FileWatcher {
watcher: RwLock<Debouncer<RecommendedWatcher>>,
instance_ids: Arc<RwLock<HashMap<String, String>>>,
}
pub async fn init_watcher() -> crate::Result<FileWatcher> {
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<FileWatcher> {
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<FileWatcher> {
).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(());
};
+1
View File
@@ -107,6 +107,7 @@ impl State {
instances::watcher::watch_instances_init(
&state.file_watcher,
&state.directories,
&state.pool,
)
.await;