feat:移除了弹窗,服务器添加sls
This commit is contained in:
commit
6a295f9a7a
4082 files changed
+1322534
No files matched your search
@@ -0,0 +1,744 @@
|
||||
use crate::State;
|
||||
use crate::event::InstancePayloadType;
|
||||
use crate::event::emit::{emit_instance, emit_minecraft_crash_warning};
|
||||
use crate::state::{
|
||||
DirectoryInfo, InstanceInstallStage, ProjectType, attached_world_data,
|
||||
};
|
||||
use crate::worlds::WorldType;
|
||||
use notify::{RecommendedWatcher, RecursiveMode};
|
||||
use notify_debouncer_mini::{DebounceEventResult, Debouncer, new_debouncer};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::sync::{RwLock, mpsc::channel};
|
||||
|
||||
use super::adapters::sqlite::instance_rows;
|
||||
use super::config_sync::{CONFIG_FILE_NAME, CONFIG_FILE_TEMP_NAME};
|
||||
|
||||
pub struct FileWatcher {
|
||||
watcher: RwLock<Debouncer<RecommendedWatcher>>,
|
||||
instance_ids: Arc<RwLock<HashMap<String, String>>>,
|
||||
content_changes: Arc<RwLock<HashMap<String, InstanceContentChangeState>>>,
|
||||
manual_import_directory: Arc<RwLock<Option<PathBuf>>>,
|
||||
manual_import_generation: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct InstanceContentWatchSnapshot {
|
||||
pub epoch: u64,
|
||||
pub generation: u64,
|
||||
pub dirty_paths: HashSet<String>,
|
||||
pub directory_dirty: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct InstanceContentChangeState {
|
||||
epoch: u64,
|
||||
generation: u64,
|
||||
dirty_paths: HashSet<String>,
|
||||
directory_dirty: bool,
|
||||
tracked_paths: HashSet<String>,
|
||||
}
|
||||
|
||||
static NEXT_CONTENT_EPOCH: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
pub async fn init_watcher() -> crate::Result<FileWatcher> {
|
||||
let (tx, mut rx) = channel(1);
|
||||
let instance_ids = Arc::new(RwLock::new(HashMap::<String, String>::new()));
|
||||
let event_instance_ids = instance_ids.clone();
|
||||
let content_changes = Arc::new(RwLock::new(HashMap::<
|
||||
String,
|
||||
InstanceContentChangeState,
|
||||
>::new()));
|
||||
let event_content_changes = content_changes.clone();
|
||||
let manual_import_directory = Arc::new(RwLock::new(None::<PathBuf>));
|
||||
let event_manual_import_directory = manual_import_directory.clone();
|
||||
let manual_import_generation = Arc::new(AtomicU64::new(0));
|
||||
|
||||
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, "Initing watcher");
|
||||
while let Some(res) = rx.recv().await {
|
||||
let _span = span.enter();
|
||||
|
||||
match res {
|
||||
Ok(events) => {
|
||||
let instance_ids = event_instance_ids.read().await;
|
||||
let manual_import_directory =
|
||||
event_manual_import_directory.read().await.clone();
|
||||
let mut visited_instances = Vec::new();
|
||||
let mut scan_manual_downloads = false;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 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());
|
||||
let relative_path = e
|
||||
.path
|
||||
.components()
|
||||
.skip_while(|x| x.as_os_str() != instance_path)
|
||||
.skip(1)
|
||||
.map(|component| {
|
||||
component.as_os_str().to_string_lossy()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("/");
|
||||
if !relative_path.is_empty() {
|
||||
record_upgrade_content_change(
|
||||
&event_content_changes,
|
||||
&instance_id,
|
||||
&relative_path,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
if first_file_name
|
||||
.is_some_and(is_config_sync_file_name)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let is_crash_report = first_file_name
|
||||
.as_ref()
|
||||
.is_some_and(|x| *x == "crash-reports")
|
||||
&& e.path
|
||||
.extension()
|
||||
.as_ref()
|
||||
.is_some_and(|x| *x == "txt");
|
||||
let is_jvm_crash =
|
||||
first_file_name.as_ref().is_some_and(|x| {
|
||||
x.to_string_lossy()
|
||||
.starts_with("hs_err_pid")
|
||||
}) && e
|
||||
.path
|
||||
.extension()
|
||||
.as_ref()
|
||||
.is_some_and(|x| *x == "log");
|
||||
if is_crash_report || is_jvm_crash {
|
||||
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,
|
||||
&state.pool
|
||||
).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 {
|
||||
let emit_instance_id = instance_id.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = emit_instance(
|
||||
&emit_instance_id,
|
||||
event,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
visited_instances.push(instance_id);
|
||||
}
|
||||
}
|
||||
} else if manual_import_directory.as_ref().is_some_and(
|
||||
|directory| e.path.starts_with(directory),
|
||||
) {
|
||||
scan_manual_downloads = true;
|
||||
}
|
||||
}
|
||||
if scan_manual_downloads
|
||||
&& let Some(directory) = manual_import_directory
|
||||
{
|
||||
tokio::spawn(async move {
|
||||
if let Err(error) =
|
||||
crate::api::curseforge::scan_pending_manual_downloads_in(
|
||||
&directory,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
"Unable to scan pending manual downloads: {error}"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(error) => tracing::warn!("Unable to watch file: {error}"),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(FileWatcher {
|
||||
watcher: RwLock::new(file_watcher),
|
||||
instance_ids,
|
||||
content_changes,
|
||||
manual_import_directory,
|
||||
manual_import_generation,
|
||||
})
|
||||
}
|
||||
|
||||
impl FileWatcher {
|
||||
pub(crate) async fn track_upgrade_source(
|
||||
&self,
|
||||
instance_id: &str,
|
||||
paths: impl IntoIterator<Item = String>,
|
||||
) -> Option<InstanceContentWatchSnapshot> {
|
||||
let mut changes = self.content_changes.write().await;
|
||||
let change = changes.get_mut(instance_id)?;
|
||||
change.tracked_paths.extend(paths);
|
||||
Some(change.snapshot())
|
||||
}
|
||||
|
||||
pub(crate) async fn content_watch_snapshot(
|
||||
&self,
|
||||
instance_id: &str,
|
||||
) -> Option<InstanceContentWatchSnapshot> {
|
||||
self.content_changes
|
||||
.read()
|
||||
.await
|
||||
.get(instance_id)
|
||||
.map(InstanceContentChangeState::snapshot)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn record_upgrade_content_change(
|
||||
&self,
|
||||
instance_id: &str,
|
||||
relative_path: &str,
|
||||
) {
|
||||
record_upgrade_content_change(
|
||||
&self.content_changes,
|
||||
instance_id,
|
||||
relative_path,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn configure_manual_import_directory(
|
||||
&self,
|
||||
directory: Option<PathBuf>,
|
||||
) -> crate::Result<()> {
|
||||
let current = self.manual_import_directory.read().await.clone();
|
||||
if current == directory {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut debouncer = self.watcher.write().await;
|
||||
if let Some(directory) = directory.as_ref() {
|
||||
debouncer
|
||||
.watcher()
|
||||
.watch(directory, RecursiveMode::NonRecursive)?;
|
||||
}
|
||||
if let Some(current) = current.as_ref() {
|
||||
let _ = debouncer.watcher().unwatch(current);
|
||||
}
|
||||
*self.manual_import_directory.write().await = directory;
|
||||
let generation = self
|
||||
.manual_import_generation
|
||||
.fetch_add(1, Ordering::Relaxed)
|
||||
+ 1;
|
||||
let Some(directory) = self.manual_import_directory.read().await.clone()
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let active_generation = self.manual_import_generation.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(3));
|
||||
interval.set_missed_tick_behavior(
|
||||
tokio::time::MissedTickBehavior::Skip,
|
||||
);
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if active_generation.load(Ordering::Relaxed) != generation {
|
||||
break;
|
||||
}
|
||||
if let Err(error) =
|
||||
crate::api::curseforge::scan_pending_manual_downloads_in(
|
||||
&directory,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
"Unable to poll pending manual downloads: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn watch_instances_init(
|
||||
watcher: &FileWatcher,
|
||||
dirs: &DirectoryInfo,
|
||||
pool: &sqlx::SqlitePool,
|
||||
) {
|
||||
let Ok(instances) = instance_rows::list_instances(pool).await else {
|
||||
return;
|
||||
};
|
||||
|
||||
for instance in instances {
|
||||
watch_instance_folder(
|
||||
&instance.id,
|
||||
&instance.path,
|
||||
&dirs.instance_game_dir(&instance),
|
||||
watcher,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn watch_instance_folder(
|
||||
instance_id: &str,
|
||||
instance_path: &str,
|
||||
full_instance_path: &Path,
|
||||
watcher: &FileWatcher,
|
||||
) {
|
||||
let Ok(metadata) = tokio::fs::metadata(full_instance_path).await else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !metadata.is_dir() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut to_watch = Vec::new();
|
||||
for full_path in instance_watch_paths(full_instance_path) {
|
||||
if &full_path == full_instance_path {
|
||||
// The root is watched non-recursively after the subfolders.
|
||||
continue;
|
||||
}
|
||||
let meta = tokio::fs::symlink_metadata(&full_path).await;
|
||||
let exists = meta.is_ok();
|
||||
let is_symlink = meta.ok().is_some_and(|m| m.file_type().is_symlink());
|
||||
let sub_path = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
if !exists
|
||||
&& !is_symlink
|
||||
&& !sub_path.contains('.')
|
||||
&& let Err(e) = crate::util::io::create_dir_all(&full_path).await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to create directory for watcher {full_path:?}: {e}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
to_watch.push(full_path);
|
||||
}
|
||||
|
||||
let mut debouncer = watcher.watcher.write().await;
|
||||
for full_path in &to_watch {
|
||||
if let Err(e) = debouncer
|
||||
.watcher()
|
||||
.watch(full_path, RecursiveMode::Recursive)
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to watch directory for watcher {full_path:?}: {e}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = debouncer
|
||||
.watcher()
|
||||
.watch(full_instance_path, RecursiveMode::NonRecursive)
|
||||
{
|
||||
tracing::error!(
|
||||
"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());
|
||||
watcher
|
||||
.content_changes
|
||||
.write()
|
||||
.await
|
||||
.insert(instance_id.to_string(), new_instance_content_change_state());
|
||||
}
|
||||
|
||||
/// Stops watching an instance folder and forgets its instance-id mapping.
|
||||
///
|
||||
/// Used when the instance folder is about to be renamed or replaced. On
|
||||
/// Windows an active watch keeps an open directory handle, which blocks
|
||||
/// renaming the folder with `ERROR_ACCESS_DENIED`; the folder must be
|
||||
/// unwatched first and re-registered afterwards.
|
||||
pub(crate) async fn unwatch_instance_folder(
|
||||
instance_path: &str,
|
||||
full_instance_path: &Path,
|
||||
watcher: &FileWatcher,
|
||||
) {
|
||||
let mut debouncer = watcher.watcher.write().await;
|
||||
for full_path in instance_watch_paths(full_instance_path) {
|
||||
let _ = debouncer.watcher().unwatch(&full_path);
|
||||
}
|
||||
|
||||
let instance_id = watcher.instance_ids.write().await.remove(instance_path);
|
||||
if let Some(instance_id) = instance_id {
|
||||
watcher.content_changes.write().await.remove(&instance_id);
|
||||
}
|
||||
}
|
||||
|
||||
impl InstanceContentChangeState {
|
||||
fn snapshot(&self) -> InstanceContentWatchSnapshot {
|
||||
InstanceContentWatchSnapshot {
|
||||
epoch: self.epoch,
|
||||
generation: self.generation,
|
||||
dirty_paths: self.dirty_paths.clone(),
|
||||
directory_dirty: self.directory_dirty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn new_instance_content_change_state() -> InstanceContentChangeState {
|
||||
InstanceContentChangeState {
|
||||
epoch: NEXT_CONTENT_EPOCH.fetch_add(1, Ordering::Relaxed),
|
||||
..InstanceContentChangeState::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_upgrade_content_change(
|
||||
relative_path: &str,
|
||||
tracked_paths: &HashSet<String>,
|
||||
) -> bool {
|
||||
let normalized = relative_path.replace('\\', "/");
|
||||
let top_level = normalized.split('/').next().unwrap_or_default();
|
||||
matches!(
|
||||
top_level,
|
||||
"mods" | "resourcepacks" | "shaderpacks" | "datapacks"
|
||||
) || tracked_paths.contains(&normalized)
|
||||
}
|
||||
|
||||
async fn record_upgrade_content_change(
|
||||
content_changes: &RwLock<HashMap<String, InstanceContentChangeState>>,
|
||||
instance_id: &str,
|
||||
relative_path: &str,
|
||||
) {
|
||||
let mut content_changes = content_changes.write().await;
|
||||
if let Some(change) = content_changes.get_mut(instance_id)
|
||||
&& is_upgrade_content_change(relative_path, &change.tracked_paths)
|
||||
{
|
||||
change.generation = change.generation.wrapping_add(1);
|
||||
change.dirty_paths.insert(relative_path.replace('\\', "/"));
|
||||
change.directory_dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// All paths `watch_instance_folder` registers for a single instance,
|
||||
/// including the root, so `unwatch_instance_folder` can release them again.
|
||||
fn instance_watch_paths(full_instance_path: &Path) -> Vec<PathBuf> {
|
||||
// `saves` is both a ProjectType folder and part of the crash-report
|
||||
// extras; deduplicate so watch/unwatch stay symmetric (a leftover watch
|
||||
// handle on a subfolder keeps Windows from renaming the instance root).
|
||||
let mut seen = HashSet::new();
|
||||
let mut paths = Vec::new();
|
||||
for sub in ProjectType::iterator()
|
||||
.map(|x| x.get_folder())
|
||||
.chain(["crash-reports", "saves"])
|
||||
{
|
||||
let full_path = full_instance_path.join(sub);
|
||||
if seen.insert(full_path.clone()) {
|
||||
paths.push(full_path);
|
||||
}
|
||||
}
|
||||
paths.push(full_instance_path.to_path_buf());
|
||||
paths
|
||||
}
|
||||
|
||||
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_id(&instance_id, &state.pool)
|
||||
.await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if instance.install_stage == InstanceInstallStage::Installed {
|
||||
emit_minecraft_crash_warning(&instance_id, &instance.name)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok::<(), crate::Error>(())
|
||||
}
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
tracing::warn!("Unable to send crash report to frontend: {err}")
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
fn is_config_sync_file_name(name: &std::ffi::OsStr) -> bool {
|
||||
let name = name.to_string_lossy();
|
||||
name == CONFIG_FILE_NAME || name == CONFIG_FILE_TEMP_NAME
|
||||
}
|
||||
|
||||
#[cfg(all(test, windows))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn watched_instance_folder_cannot_be_renamed_on_windows() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let dirs = DirectoryInfo {
|
||||
settings_dir: temp.path().to_path_buf(),
|
||||
config_dir: temp.path().to_path_buf(),
|
||||
app_identifier: "test".to_string(),
|
||||
};
|
||||
let watcher = init_watcher().await.unwrap();
|
||||
let instance_path = "watched-instance";
|
||||
let full_path = dirs.instances_dir().join(instance_path);
|
||||
std::fs::create_dir_all(&full_path).unwrap();
|
||||
|
||||
watch_instance_folder(
|
||||
"instance-1",
|
||||
instance_path,
|
||||
&full_path,
|
||||
&watcher,
|
||||
)
|
||||
.await;
|
||||
|
||||
// On Windows, an active watch keeps a directory handle open and blocks
|
||||
// renaming the instance folder (ERROR_ACCESS_DENIED). This is the
|
||||
// failure the symlink import used to hit.
|
||||
let rename_result = std::fs::rename(
|
||||
&full_path,
|
||||
temp.path().join("watched-instance.bak"),
|
||||
);
|
||||
assert!(
|
||||
rename_result.is_err(),
|
||||
"a watched folder must not be renameable on Windows"
|
||||
);
|
||||
|
||||
// The import flow unwatches the folder first; after that the rename
|
||||
// must succeed (the watcher closes its handles asynchronously, so a
|
||||
// short retry window is needed).
|
||||
unwatch_instance_folder(instance_path, &full_path, &watcher).await;
|
||||
|
||||
let mut renamed = false;
|
||||
for _ in 0..20 {
|
||||
match std::fs::rename(
|
||||
&full_path,
|
||||
temp.path().join("watched-instance.bak"),
|
||||
) {
|
||||
Ok(()) => {
|
||||
renamed = true;
|
||||
break;
|
||||
}
|
||||
Err(error)
|
||||
if error.kind() == std::io::ErrorKind::PermissionDenied =>
|
||||
{
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
Err(error) => panic!("unexpected rename error: {error:?}"),
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
renamed,
|
||||
"rename should succeed after the folder is unwatched"
|
||||
);
|
||||
|
||||
drop(watcher);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod config_file_name_tests {
|
||||
use super::*;
|
||||
use std::ffi::OsStr;
|
||||
|
||||
#[test]
|
||||
fn recognizes_sync_config_files_but_not_other_instance_events() {
|
||||
assert!(is_config_sync_file_name(OsStr::new("axolotl_config.json")));
|
||||
assert!(is_config_sync_file_name(OsStr::new(
|
||||
"axolotl_config.json.tmp"
|
||||
)));
|
||||
assert!(!is_config_sync_file_name(OsStr::new("mods")));
|
||||
assert!(!is_config_sync_file_name(OsStr::new("servers.dat")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn content_change_tracking_ignores_unrelated_paths() {
|
||||
let watcher = init_watcher().await.unwrap();
|
||||
watcher.content_changes.write().await.insert(
|
||||
"instance".to_string(),
|
||||
new_instance_content_change_state(),
|
||||
);
|
||||
watcher
|
||||
.track_upgrade_source(
|
||||
"instance",
|
||||
["schematics/existing.schem".to_string()],
|
||||
)
|
||||
.await;
|
||||
|
||||
watcher
|
||||
.record_upgrade_content_change("instance", "config/options.txt")
|
||||
.await;
|
||||
watcher
|
||||
.record_upgrade_content_change(
|
||||
"instance",
|
||||
"schematics/existing.schem",
|
||||
)
|
||||
.await;
|
||||
watcher
|
||||
.record_upgrade_content_change("instance", "mods/new.jar")
|
||||
.await;
|
||||
|
||||
let snapshot =
|
||||
watcher.content_watch_snapshot("instance").await.unwrap();
|
||||
assert_eq!(snapshot.generation, 2);
|
||||
assert!(snapshot.dirty_paths.contains("mods/new.jar"));
|
||||
assert!(snapshot.dirty_paths.contains("schematics/existing.schem"));
|
||||
assert!(!snapshot.dirty_paths.contains("config/options.txt"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn concurrent_content_notifications_do_not_lose_generation() {
|
||||
let watcher = Arc::new(init_watcher().await.unwrap());
|
||||
watcher.content_changes.write().await.insert(
|
||||
"instance".to_string(),
|
||||
new_instance_content_change_state(),
|
||||
);
|
||||
let mut tasks = Vec::new();
|
||||
for index in 0..32 {
|
||||
let watcher = Arc::clone(&watcher);
|
||||
tasks.push(tokio::spawn(async move {
|
||||
watcher
|
||||
.record_upgrade_content_change(
|
||||
"instance",
|
||||
&format!("mods/{index}.jar"),
|
||||
)
|
||||
.await;
|
||||
}));
|
||||
}
|
||||
for task in tasks {
|
||||
task.await.unwrap();
|
||||
}
|
||||
|
||||
let snapshot =
|
||||
watcher.content_watch_snapshot("instance").await.unwrap();
|
||||
assert_eq!(snapshot.generation, 32);
|
||||
assert_eq!(snapshot.dirty_paths.len(), 32);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn watcher_reinitialization_changes_content_epoch() {
|
||||
let first = init_watcher().await.unwrap();
|
||||
let second = init_watcher().await.unwrap();
|
||||
first.content_changes.write().await.insert(
|
||||
"instance".to_string(),
|
||||
new_instance_content_change_state(),
|
||||
);
|
||||
second.content_changes.write().await.insert(
|
||||
"instance".to_string(),
|
||||
new_instance_content_change_state(),
|
||||
);
|
||||
assert_ne!(
|
||||
first
|
||||
.content_watch_snapshot("instance")
|
||||
.await
|
||||
.unwrap()
|
||||
.epoch,
|
||||
second
|
||||
.content_watch_snapshot("instance")
|
||||
.await
|
||||
.unwrap()
|
||||
.epoch
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user