mirror of
https://github.com/modrinth/code.git
synced 2026-08-27 01:54:47 +00:00
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:
@@ -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,
|
||||||
|
|||||||
@@ -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(());
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user