Files
AstralRinth/packages/app-lib/src/state/fs_watcher.rs
Josiah Glosson cf190d86d5 Update Rust dependencies (#4139)
* Update Rust version

* Update async-compression 0.4.25 -> 0.4.27

* Update async-tungstenite 0.29.1 -> 0.30.0

* Update bytemuck 1.23.0 -> 1.23.1

* Update clap 4.5.40 -> 4.5.43

* Update deadpool-redis 0.21.1 -> 0.22.0 and redis 0.31.0 -> 0.32.4

* Update enumset 1.1.6 -> 1.1.7

* Update hyper-util 0.1.14 -> 0.1.16

* Update indexmap 2.9.0 -> 2.10.0

* Update indicatif 0.17.11 -> 0.18.0

* Update jemalloc_pprof 0.7.0 -> 0.8.1

* Update lettre 0.11.17 -> 0.11.18

* Update meilisearch-sdk 0.28.0 -> 0.29.1

* Update notify 8.0.0 -> 8.2.0 and notify-debouncer-mini 0.6.0 -> 0.7.0

* Update quick-xml 0.37.5 -> 0.38.1

* Fix theseus lint

* Update reqwest 0.12.20 -> 0.12.22

* Cargo fmt in theseus

* Update rgb 0.8.50 -> 0.8.52

* Update sentry 0.41.0 -> 0.42.0 and sentry-actix 0.41.0 -> 0.42.0

* Update serde_json 1.0.140 -> 1.0.142

* Update serde_with 3.13.0 -> 3.14.0

* Update spdx 0.10.8 -> 0.10.9

* Update sysinfo 0.35.2 -> 0.36.1

* Update tauri suite

* Fix build by updating mappings

* Update tokio 1.45.1 -> 1.47.1 and tokio-util 0.7.15 -> 0.7.16

* Update tracing-actix-web 0.7.18 -> 0.7.19

* Update zip 4.2.0 -> 4.3.0

* Misc Cargo.lock updates

* Update Dockerfiles
2025-08-08 22:50:44 +00:00

245 lines
9.7 KiB
Rust

use crate::State;
use crate::event::ProfilePayloadType;
use crate::event::emit::{emit_profile, emit_warning};
use crate::state::{
DirectoryInfo, ProfileInstallStage, ProjectType, attached_world_data,
};
use crate::worlds::WorldType;
use notify::{RecommendedWatcher, RecursiveMode};
use notify_debouncer_mini::{DebounceEventResult, Debouncer, new_debouncer};
use std::time::Duration;
use tokio::sync::{RwLock, mpsc::channel};
pub type FileWatcher = RwLock<Debouncer<RecommendedWatcher>>;
pub async fn init_watcher() -> crate::Result<FileWatcher> {
let (tx, mut rx) = channel(1);
let file_watcher = new_debouncer(
Duration::from_secs_f32(1.0),
move |res: DebounceEventResult| {
tx.blocking_send(res).ok();
},
)?;
tokio::task::spawn(async move {
let span = tracing::span!(tracing::Level::INFO, "init_watcher");
tracing::info!(parent: &span, "Initting watcher");
while let Some(res) = rx.recv().await {
let _span = span.enter();
match res {
Ok(events) => {
let mut visited_profiles = Vec::new();
events.iter().for_each(|e| {
let mut profile_path = None;
let mut found = false;
for component in e.path.components() {
if found {
profile_path = Some(component.as_os_str());
break;
}
if component.as_os_str()
== crate::state::dirs::PROFILES_FOLDER_NAME
{
found = true;
}
}
if let Some(profile_path) = profile_path {
let profile_path_str =
profile_path.to_string_lossy().to_string();
let first_file_name = e
.path
.components()
.skip_while(|x| x.as_os_str() != profile_path)
.nth(1)
.map(|x| x.as_os_str());
if first_file_name
.filter(|x| *x == "crash-reports")
.is_some()
&& e.path
.extension()
.filter(|x| *x == "txt")
.is_some()
{
crash_task(profile_path_str);
} else if !visited_profiles.contains(&profile_path)
{
let event = if first_file_name
.filter(|x| *x == "servers.dat")
.is_some()
{
Some(ProfilePayloadType::ServersUpdated)
} else if first_file_name
.filter(|x| {
*x == "saves"
&& e.path
.file_name()
.filter(|x| *x == "level.dat")
.is_some()
})
.is_some()
{
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 profile_path_str = profile_path_str.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(
&profile_path_str,
WorldType::Singleplayer,
&world,
&state.pool
).await {
tracing::warn!("Failed to remove AttachedWorldData for '{world}': {e}")
}
});
}
Some(ProfilePayloadType::WorldUpdated { world })
} else if first_file_name
.filter(|x| *x == "saves")
.is_none()
{
Some(ProfilePayloadType::Synced)
} else {
None
};
if let Some(event) = event {
tokio::spawn(async move {
let _ = emit_profile(
&profile_path_str,
event,
)
.await;
});
visited_profiles.push(profile_path);
}
}
}
});
}
Err(error) => tracing::warn!("Unable to watch file: {error}"),
}
}
});
Ok(RwLock::new(file_watcher))
}
/// Watches all existing profiles
pub(crate) async fn watch_profiles_init(
watcher: &FileWatcher,
dirs: &DirectoryInfo,
) {
if let Ok(profiles_dir) = std::fs::read_dir(dirs.profiles_dir()) {
for profile_dir in profiles_dir {
if let Ok(file_name) = profile_dir.map(|x| x.file_name())
&& let Some(file_name) = file_name.to_str()
{
if file_name.starts_with(".DS_Store") {
continue;
};
watch_profile(file_name, watcher, dirs).await;
}
}
}
}
pub(crate) async fn watch_profile(
profile_path: &str,
watcher: &FileWatcher,
dirs: &DirectoryInfo,
) {
let profile_path = dirs.profiles_dir().join(profile_path);
if profile_path.exists() && profile_path.is_dir() {
for sub_path in ProjectType::iterator().map(|x| x.get_folder()).chain([
"crash-reports",
"saves",
"servers.dat",
]) {
let full_path = profile_path.join(sub_path);
if !full_path.exists() && !full_path.is_symlink() {
if !sub_path.contains(".") {
if let Err(e) =
crate::util::io::create_dir_all(&full_path).await
{
tracing::error!(
"Failed to create directory for watcher {full_path:?}: {e}"
);
return;
}
} else if sub_path == "servers.dat" {
const EMPTY_NBT: &[u8] = &[
10, // Compound tag
0, 0, // Empty name
0, // End of compound tag
];
if let Err(e) =
crate::util::io::write(&full_path, EMPTY_NBT).await
{
tracing::error!(
"Failed to create file for watcher {full_path:?}: {e}"
);
return;
}
}
}
let mut watcher = watcher.write().await;
if let Err(e) = watcher
.watcher()
.watch(&full_path, RecursiveMode::Recursive)
{
tracing::error!(
"Failed to watch directory for watcher {full_path:?}: {e}"
);
return;
}
}
}
}
fn crash_task(path: String) {
tokio::task::spawn(async move {
let res = async {
let profile = crate::api::profile::get(&path).await?;
if let Some(profile) = profile {
// Hide warning if profile is not yet installed
if profile.install_stage == ProfileInstallStage::Installed {
emit_warning(&format!("Profile {} has crashed! Visit the logs page to see a crash report.", profile.name)).await?;
}
}
Ok::<(), crate::Error>(())
}
.await;
match res {
Ok(()) => {}
Err(err) => {
tracing::warn!("Unable to send crash report to frontend: {err}")
}
};
});
}