diff --git a/packages/app-lib/migrations/20240711194701_init.sql b/packages/app-lib/migrations/20240711194701_init.sql index 281f17c2a..ec45e27b2 100644 --- a/packages/app-lib/migrations/20240711194701_init.sql +++ b/packages/app-lib/migrations/20240711194701_init.sql @@ -105,6 +105,9 @@ CREATE TABLE profiles ( mod_loader TEXT NOT NULL, mod_loader_version TEXT NULL, + -- array of strings + groups JSONB NOT NULL, + linked_project_id TEXT NULL, linked_version_id TEXT NULL, locked INTEGER NULL, diff --git a/packages/app-lib/src/api/pack/install_from.rs b/packages/app-lib/src/api/pack/install_from.rs index 93076c679..b01030f47 100644 --- a/packages/app-lib/src/api/pack/install_from.rs +++ b/packages/app-lib/src/api/pack/install_from.rs @@ -368,7 +368,7 @@ pub async fn set_profile_information( let mod_loader = mod_loader.unwrap_or(ModLoader::Vanilla); let loader_version = if mod_loader != ModLoader::Vanilla { crate::launcher::get_loader_version_from_profile( - &game_version, + game_version, mod_loader, loader_version.cloned().as_deref(), ) diff --git a/packages/app-lib/src/api/pack/install_mrpack.rs b/packages/app-lib/src/api/pack/install_mrpack.rs index cb75a6935..ff3759532 100644 --- a/packages/app-lib/src/api/pack/install_mrpack.rs +++ b/packages/app-lib/src/api/pack/install_mrpack.rs @@ -365,9 +365,8 @@ pub async fn remove_all_related_files( // Iterate over all Modrinth project file paths in the json, and remove them // (There should be few, but this removes any files the .mrpack intended as Modrinth projects but were unrecognized) for file in pack.files { - let path: PathBuf = profile::get_full_path(&profile_path) - .await? - .join(file.path.to_string()); + let path: PathBuf = + profile::get_full_path(&profile_path).await?.join(file.path); if path.exists() { io::remove_file(&path).await?; } diff --git a/packages/app-lib/src/api/profile/create.rs b/packages/app-lib/src/api/profile/create.rs index 5887eaebe..608fc55ec 100644 --- a/packages/app-lib/src/api/profile/create.rs +++ b/packages/app-lib/src/api/profile/create.rs @@ -82,6 +82,7 @@ pub async fn profile_create( game_version, loader: modloader, loader_version: loader.map(|x| x.id), + groups: Vec::new(), linked_data, created: Utc::now(), modified: Utc::now(), @@ -110,13 +111,20 @@ pub async fn profile_create( &state.directories.caches_dir(), &state.io_semaphore, bytes::Bytes::from(bytes), - &icon, + icon, ) .await?; } emit_profile(&profile.path, ProfilePayloadType::Created).await?; + crate::state::fs_watcher::watch_profile( + &profile.path, + &state.file_watcher, + &state.directories, + ) + .await?; + profile.upsert(&state.pool).await?; if !skip_install_profile.unwrap_or(false) { @@ -141,7 +149,7 @@ pub async fn profile_create_from_duplicate( copy_from: &str, ) -> crate::Result { // Original profile - let profile = profile::get(©_from).await?.ok_or_else(|| { + let profile = profile::get(copy_from).await?.ok_or_else(|| { ErrorKind::UnmanagedProfileError(copy_from.to_string()) })?; diff --git a/packages/app-lib/src/api/profile/mod.rs b/packages/app-lib/src/api/profile/mod.rs index f7e6c6289..7c1d017f3 100644 --- a/packages/app-lib/src/api/profile/mod.rs +++ b/packages/app-lib/src/api/profile/mod.rs @@ -250,11 +250,11 @@ pub async fn update_all_projects( .await?; let state = State::get().await?; - let keys = profile.get_projects(&state.pool, &state.fetch_semaphore).await? + let keys = profile + .get_projects(&state.pool, &state.fetch_semaphore) + .await? .into_iter() - .filter(|(_, project)| { - project.update_version_id.is_some() - }) + .filter(|(_, project)| project.update_version_id.is_some()) .map(|x| x.0) .collect::>(); let len = keys.len(); @@ -323,7 +323,7 @@ pub async fn update_project( ) .await?; - if path != project_path.clone() { + if path != project_path { Profile::remove_project(profile_path, project_path).await?; } @@ -441,8 +441,8 @@ pub async fn export_mrpack( _name: Option, ) -> crate::Result<()> { let state = State::get().await?; - let io_semaphore = state.io_semaphore.0.read().await; - let _permit: tokio::sync::SemaphorePermit = io_semaphore.acquire().await?; + let _permit: tokio::sync::SemaphorePermit = + state.io_semaphore.0.acquire().await?; let profile = get(profile_path).await?.ok_or_else(|| { crate::ErrorKind::OtherError(format!( "Tried to export a nonexistent or unloaded profile at path {}!", @@ -476,9 +476,9 @@ pub async fn export_mrpack( create_mrpack_json(&profile, version_id, description).await?; let included_candidates_set = HashSet::<_>::from_iter(included_export_candidates.iter()); - packfile.files.retain(|f| { - included_candidates_set.contains(&f.path) - }); + packfile + .files + .retain(|f| included_candidates_set.contains(&f.path)); // Build vec of all files in the folder let mut path_list = Vec::new(); @@ -503,8 +503,7 @@ pub async fn export_mrpack( let relative_path = pack_get_relative_path(&profile_base_path, &path)?; if packfile.files.iter().any(|f| f.path == relative_path) - || !included_candidates_set - .contains(&relative_path) + || !included_candidates_set.contains(&relative_path) { continue; } @@ -580,7 +579,8 @@ pub async fn get_pack_export_candidates( { let path: PathBuf = entry.path(); - path_list.push(pack_get_relative_path(&profile_base_dir, &path)?); + path_list + .push(pack_get_relative_path(&profile_base_dir, &path)?); } } else { // One layer of files/folders if its a file @@ -590,8 +590,12 @@ pub async fn get_pack_export_candidates( Ok(path_list) } -fn pack_get_relative_path(profile_path: &PathBuf, path: &PathBuf) -> crate::Result { - Ok(path.strip_prefix(profile_path) +fn pack_get_relative_path( + profile_path: &PathBuf, + path: &PathBuf, +) -> crate::Result { + Ok(path + .strip_prefix(profile_path) .map_err(|_| { crate::ErrorKind::FSError(format!( "Path {path:?} does not correspond to a profile", @@ -700,8 +704,8 @@ pub async fn run_credentials( } let mc_process = crate::launcher::launch_minecraft( - &*java_args, - &*env_args, + &java_args, + &env_args, &mc_set_options, &wrapper, &memory, diff --git a/packages/app-lib/src/api/settings.rs b/packages/app-lib/src/api/settings.rs index 6f90595a9..5bba3473f 100644 --- a/packages/app-lib/src/api/settings.rs +++ b/packages/app-lib/src/api/settings.rs @@ -19,18 +19,6 @@ pub async fn set(settings: Settings) -> crate::Result<()> { let state = State::get().await?; let old_settings = Settings::get(&state.pool).await?; - if settings.max_concurrent_writes != old_settings.max_concurrent_writes { - let mut io_semaphore = state.io_semaphore.0.write().await; - *io_semaphore = - tokio::sync::Semaphore::new(settings.max_concurrent_writes); - } - if settings.max_concurrent_downloads - != old_settings.max_concurrent_downloads - { - let mut fetch_semaphore = state.fetch_semaphore.0.write().await; - *fetch_semaphore = - tokio::sync::Semaphore::new(settings.max_concurrent_downloads); - } if settings.discord_rpc != old_settings.discord_rpc { state.discord_rpc.clear_to_default(true).await?; } diff --git a/packages/app-lib/src/event/mod.rs b/packages/app-lib/src/event/mod.rs index 8e77d4f4f..db8f397df 100644 --- a/packages/app-lib/src/event/mod.rs +++ b/packages/app-lib/src/event/mod.rs @@ -255,7 +255,6 @@ pub struct ProfilePayload { #[serde(rename_all = "snake_case")] pub enum ProfilePayloadType { Created, - Added, // also triggered when Created Synced, Edited, Removed, diff --git a/packages/app-lib/src/state/cache.rs b/packages/app-lib/src/state/cache.rs index 872b05a48..d1d41a36b 100644 --- a/packages/app-lib/src/state/cache.rs +++ b/packages/app-lib/src/state/cache.rs @@ -1,17 +1,16 @@ use crate::config::{META_URL, MODRINTH_API_URL, MODRINTH_API_URL_V3}; -use crate::state::ProjectType; use crate::util::fetch::{fetch_json, FetchSemaphore}; use chrono::{DateTime, Utc}; -use dashmap::{DashMap, DashSet}; +use dashmap::DashSet; use reqwest::Method; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fmt::Display; use std::hash::Hash; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; // 1 day -const DEFAULT_ID: &'static str = "0"; +const DEFAULT_ID: &str = "0"; #[derive(Serialize, Deserialize, Copy, Clone, Debug)] #[serde(rename_all = "snake_case")] @@ -611,7 +610,7 @@ impl CachedEntry { .await?; if !values.is_empty() { - Self::upsert_many(&*values, &mut *exec).await?; + Self::upsert_many(&values, &mut *exec).await?; return_vals.append(&mut values); } @@ -620,7 +619,7 @@ impl CachedEntry { if !expired_keys.is_empty() && cache_behaviour == CacheBehaviour::StaleWhileRevalidate { - let _ = tokio::task::spawn(async move { + tokio::task::spawn(async move { // TODO: if possible- find a way to do this without invoking state get let state = crate::state::State::get().await?; @@ -632,7 +631,7 @@ impl CachedEntry { .await?; if !values.is_empty() { - Self::upsert_many(&*values, &state.pool).await?; + Self::upsert_many(&values, &state.pool).await?; } Ok::<(), crate::Error>(()) @@ -644,7 +643,7 @@ impl CachedEntry { async fn fetch_many( type_: CacheValueType, - mut keys: DashSet, + keys: DashSet, fetch_semaphore: &FetchSemaphore, ) -> crate::Result> { macro_rules! fetch_original_values { @@ -745,13 +744,13 @@ impl CachedEntry { CacheValueType::File => { let mut versions = fetch_json::>( Method::POST, - &*format!("{}version_files", MODRINTH_API_URL), + &format!("{}version_files", MODRINTH_API_URL), None, Some(serde_json::json!({ "algorithm": "sha1", "hashes": &keys, })), - &fetch_semaphore, + fetch_semaphore, ) .await?; @@ -877,7 +876,7 @@ impl CachedEntry { let profiles_dir = state.directories.profiles_dir().await; async fn hash_file( - profiles_dir: &PathBuf, + profiles_dir: &Path, path: String, ) -> crate::Result { let path = @@ -930,8 +929,7 @@ impl CachedEntry { .collect::>() .await .into_iter() - .map(|x| x.ok()) - .flatten() + .filter_map(|x| x.ok()) .collect(); results diff --git a/packages/app-lib/src/state/db.rs b/packages/app-lib/src/state/db.rs new file mode 100644 index 000000000..1a84b6b43 --- /dev/null +++ b/packages/app-lib/src/state/db.rs @@ -0,0 +1,23 @@ +use crate::state::DirectoryInfo; +use sqlx::migrate::MigrateDatabase; +use sqlx::sqlite::SqlitePoolOptions; +use sqlx::{Connection, Pool, Sqlite, SqliteConnection}; + +pub(crate) async fn connect() -> crate::Result> { + let uri = + format!("sqlite:{}", DirectoryInfo::get_database_file()?.display()); + + if !Sqlite::database_exists(&uri).await? { + Sqlite::create_database(&uri).await?; + } + + let mut conn: SqliteConnection = SqliteConnection::connect(&uri).await?; + sqlx::migrate!().run(&mut conn).await?; + + let pool = SqlitePoolOptions::new() + .max_connections(100) + .connect(&uri) + .await?; + + Ok(pool) +} diff --git a/packages/app-lib/src/state/db/mod.rs b/packages/app-lib/src/state/db/mod.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/app-lib/src/state/fs_watcher.rs b/packages/app-lib/src/state/fs_watcher.rs new file mode 100644 index 000000000..1d33040c9 --- /dev/null +++ b/packages/app-lib/src/state/fs_watcher.rs @@ -0,0 +1,151 @@ +use crate::event::emit::{emit_profile, emit_warning}; +use crate::event::ProfilePayloadType; +use crate::state::{DirectoryInfo, ProfileInstallStage, ProjectType}; +use futures::{channel::mpsc::channel, SinkExt, StreamExt}; +use notify::{RecommendedWatcher, RecursiveMode}; +use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer}; +use std::time::Duration; +use tokio::sync::RwLock; + +pub type FileWatcher = RwLock>; + +pub async fn init_watcher() -> crate::Result { + let (mut tx, mut rx) = channel(1); + + let file_watcher = new_debouncer( + Duration::from_secs_f32(1.0), + move |res: DebounceEventResult| { + futures::executor::block_on(async { + tx.send(res).await.unwrap(); + }) + }, + )?; + + 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.next().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().to_string_lossy()); + break; + } + + if component.as_os_str() == crate::state::dirs::PROFILES_FOLDER_NAME { + found = true; + } + } + + println!("WATCHER: new_path: {profile_path:?} event path: {:?}", e.path); + if let Some(profile_path) = profile_path { + if e.path + .components() + .any(|x| x.as_os_str() == "crash-reports") + && e.path + .extension() + .map(|x| x == "txt") + .unwrap_or(false) + { + crash_task(profile_path.to_string()); + } else if !visited_profiles.contains(&profile_path) { + let path = profile_path.to_string(); + tokio::spawn(async move { + let _ = emit_profile( + &path, + ProfilePayloadType::Synced, + ) + .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, +) -> crate::Result<()> { + if let Ok(profiles_dir) = std::fs::read_dir(&dirs.profiles_dir().await) { + for profile_dir in profiles_dir { + if let Ok(file_name) = profile_dir.map(|x| x.file_name()) { + if let Some(file_name) = file_name.to_str() { + if file_name.starts_with(".DS_STORE") { + continue; + }; + + watch_profile(file_name, watcher, dirs).await?; + } + } + } + } + + Ok(()) +} + +pub(crate) async fn watch_profile( + profile_path: &str, + watcher: &FileWatcher, + dirs: &DirectoryInfo, +) -> crate::Result<()> { + let profile_path = dirs.profiles_dir().await.join(profile_path); + + for folder in ProjectType::iterator() + .map(|x| x.get_folder()) + .chain(["crash-reports"]) + { + let path = profile_path.join(folder); + + if !path.exists() { + crate::util::io::create_dir_all(&path).await?; + } + + let mut watcher = watcher.write().await; + watcher.watcher().watch(&path, RecursiveMode::Recursive)?; + } + + Ok(()) +} + +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}") + } + }; + }); +} diff --git a/packages/app-lib/src/state/mod.rs b/packages/app-lib/src/state/mod.rs index fe3c39d43..5749614e7 100644 --- a/packages/app-lib/src/state/mod.rs +++ b/packages/app-lib/src/state/mod.rs @@ -9,9 +9,8 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::{OnceCell, RwLock, Semaphore}; -use sqlx::migrate::MigrateDatabase; -use sqlx::sqlite::SqlitePoolOptions; -use sqlx::{Connection, Sqlite, SqliteConnection, SqlitePool}; +use crate::state::fs_watcher::FileWatcher; +use sqlx::SqlitePool; // Submodules mod dirs; @@ -41,6 +40,8 @@ pub use self::minecraft_auth::*; mod cache; pub use self::cache::*; +mod db; +pub mod fs_watcher; mod mr_auth; pub use self::mr_auth::*; @@ -48,6 +49,8 @@ pub use self::mr_auth::*; // TODO: UI: Change so new settings model works // TODO: UI: Change so new java version API works // TODO: pass credentials to modrinth cdn +// TODO: add language requiring restart for discord, concurrent writes/fetches changes +// TODO: get rid of unneccessary locking + atomics in discord // Global state // RwLock on state only has concurrent reads, except for config dir change which takes control of the State @@ -79,6 +82,8 @@ pub struct State { pub discord_rpc: DiscordGuard, pub(crate) pool: SqlitePool, + + pub(crate) file_watcher: FileWatcher, } impl State { @@ -90,13 +95,15 @@ impl State { Ok(()) } - /// Get the current launcher state, initializing it if needed + /// Get the current launcher state, waiting for initialization pub async fn get() -> crate::Result> { if !LAUNCHER_STATE.initialized() { while !LAUNCHER_STATE.initialized() {} } - Ok(Arc::clone(LAUNCHER_STATE.get().unwrap())) + Ok(Arc::clone( + LAUNCHER_STATE.get().expect("State is not initialized!"), + )) } pub fn initialized() -> bool { @@ -115,35 +122,16 @@ impl State { let directories = DirectoryInfo::init()?; - // TODO: move to own file - // db code - let uri = - format!("sqlite:{}", DirectoryInfo::get_database_file()?.display()); - - if !Sqlite::database_exists(&uri).await? { - Sqlite::create_database(&uri).await?; - } - - let mut conn: SqliteConnection = - SqliteConnection::connect(&uri).await?; - sqlx::migrate!().run(&mut conn).await?; - - let pool = SqlitePoolOptions::new() - .max_connections(100) - .connect(&uri) - .await?; - // end db code + let pool = db::connect().await?; let settings = Settings::get(&pool).await?; emit_loading(&loading_bar, 10.0, None).await?; - let fetch_semaphore = FetchSemaphore(RwLock::new(Semaphore::new( - settings.max_concurrent_downloads, - ))); - let io_semaphore = IoSemaphore(RwLock::new(Semaphore::new( - settings.max_concurrent_writes, - ))); + let fetch_semaphore = + FetchSemaphore(Semaphore::new(settings.max_concurrent_downloads)); + let io_semaphore = + IoSemaphore(Semaphore::new(settings.max_concurrent_writes)); emit_loading(&loading_bar, 10.0, None).await?; let is_offline = !fetch::check_internet(3).await; @@ -168,6 +156,9 @@ impl State { let children = Children::new(); + let file_watcher = fs_watcher::init_watcher().await?; + fs_watcher::watch_profiles_init(&file_watcher, &directories).await?; + // Starts a loop of checking if we are online, and updating Self::offine_check_loop(); @@ -185,6 +176,7 @@ impl State { safety_processes: RwLock::new(safety_processes), modrinth_auth_flow: RwLock::new(None), pool, + file_watcher, })) } diff --git a/packages/app-lib/src/state/profiles.rs b/packages/app-lib/src/state/profiles.rs index 2ad6e9bbe..415adc339 100644 --- a/packages/app-lib/src/state/profiles.rs +++ b/packages/app-lib/src/state/profiles.rs @@ -1,5 +1,5 @@ use super::settings::{Hooks, MemorySettings, WindowSize}; -use crate::state::{CacheBehaviour, CachedEntry, CachedFile, FileMetadata}; +use crate::state::{CacheBehaviour, CachedEntry, FileMetadata}; use crate::util; use crate::util::fetch::{write_cached_icon, FetchSemaphore, IoSemaphore}; use crate::util::io::{self}; @@ -22,6 +22,8 @@ pub struct Profile { pub loader: ModLoader, pub loader_version: Option, + pub groups: Vec, + pub linked_data: Option, pub created: DateTime, @@ -217,6 +219,7 @@ impl Profile { SELECT path, install_stage, name, icon_path, game_version, mod_loader, mod_loader_version, + json(groups) as "groups!: serde_json::Value", linked_project_id, linked_version_id, locked, created, modified, last_played, submitted_time_played, recent_time_played, @@ -240,15 +243,28 @@ impl Profile { game_version: x.game_version, loader: ModLoader::from_str(&x.mod_loader), loader_version: x.mod_loader_version, - linked_data: None, + groups: serde_json::from_value(x.groups).unwrap_or_default(), + linked_data: if let Some(project_id) = x.linked_project_id { + if let Some(version_id) = x.linked_version_id { + x.locked.map(|locked| LinkedData { + project_id, + version_id, + locked: locked == 1, + }) + } else { + None + } + } else { + None + }, created: Utc .timestamp_opt(x.created, 0) .single() - .unwrap_or_else(|| Utc::now()), + .unwrap_or_else(Utc::now), modified: Utc .timestamp_opt(x.modified, 0) .single() - .unwrap_or_else(|| Utc::now()), + .unwrap_or_else(Utc::now), last_played: x .last_played .and_then(|x| Utc.timestamp_opt(x, 0).single()), @@ -268,11 +284,8 @@ impl Profile { game_resolution: if let Some(x_res) = x.override_mc_game_resolution_x { - if let Some(y_res) = x.override_mc_game_resolution_y { - Some(WindowSize(x_res as u16, y_res as u16)) - } else { - None - } + x.override_mc_game_resolution_y + .map(|y_res| WindowSize(x_res as u16, y_res as u16)) } else { None }, @@ -293,6 +306,7 @@ impl Profile { SELECT path, install_stage, name, icon_path, game_version, mod_loader, mod_loader_version, + json(groups) as "groups!: serde_json::Value", linked_project_id, linked_version_id, locked, created, modified, last_played, submitted_time_played, recent_time_played, @@ -315,15 +329,17 @@ impl Profile { game_version: x.game_version, loader: ModLoader::from_str(&x.mod_loader), loader_version: x.mod_loader_version, + groups: serde_json::from_value(x.groups) + .unwrap_or_default(), linked_data: if let Some(project_id) = x.linked_project_id { if let Some(version_id) = x.linked_version_id { - if let Some(locked) = x.locked { - Some(LinkedData { - project_id, - version_id, - locked: locked == 1, - }) - } else { None } + x.locked.map(|locked| + LinkedData { + project_id, + version_id, + locked: locked == 1, + } + ) } else { None } } else { None }, created: Utc.timestamp_opt(x.created, 0).single().unwrap_or_else(Utc::now), @@ -340,13 +356,10 @@ impl Profile { }), force_fullscreen: x.override_mc_force_fullscreen.map(|x| x == 1), game_resolution: if let Some(x_res) = x.override_mc_game_resolution_x { - if let Some(y_res) = x.override_mc_game_resolution_y { - Some(WindowSize( - x_res as u16, - y_res as u16, - )) - } else { None } - + x.override_mc_game_resolution_y.map(|y_res| WindowSize( + x_res as u16, + y_res as u16, + )) } else { None }, hooks: Hooks { pre_launch: x.override_hook_pre_launch, @@ -370,6 +383,8 @@ impl Profile { let install_stage = self.install_stage.as_str(); let mod_loader = self.loader.as_str(); + let groups = serde_json::to_string(&self.groups)?; + let linked_data_project_id = self.linked_data.as_ref().map(|x| x.project_id.clone()); let linked_data_version_id = @@ -396,6 +411,7 @@ impl Profile { INSERT INTO profiles ( path, install_stage, name, icon_path, game_version, mod_loader, mod_loader_version, + groups, linked_project_id, linked_version_id, locked, created, modified, last_played, submitted_time_played, recent_time_played, @@ -406,12 +422,13 @@ impl Profile { VALUES ( $1, $2, $3, $4, $5, $6, $7, - $8, $9, $10, - $11, $12, $13, - $14, $15, - $16, $17, $18, - $19, $20, $21, $22, - $23, $24, $25 + jsonb($8), + $9, $10, $11, + $12, $13, $14, + $15, $16, + $17, jsonb($18), jsonb($19), + $20, $21, $22, $23, + $24, $25, $26 ) ON CONFLICT (path) DO UPDATE SET install_stage = $2, @@ -422,28 +439,30 @@ impl Profile { mod_loader = $6, mod_loader_version = $7, - linked_project_id = $8, - linked_version_id = $9, - locked = $10, + groups = jsonb($8), + + linked_project_id = $9, + linked_version_id = $10, + locked = $11, - created = $11, - modified = $12, - last_played = $13, + created = $12, + modified = $13, + last_played = $14, - submitted_time_played = $14, - recent_time_played = $15, + submitted_time_played = $15, + recent_time_played = $16, - override_java_path = $16, - override_extra_launch_args = jsonb($17), - override_custom_env_vars = jsonb($18), - override_mc_memory_max = $19, - override_mc_force_fullscreen = $20, - override_mc_game_resolution_x = $21, - override_mc_game_resolution_y = $22, + override_java_path = $17, + override_extra_launch_args = jsonb($18), + override_custom_env_vars = jsonb($19), + override_mc_memory_max = $20, + override_mc_force_fullscreen = $21, + override_mc_game_resolution_x = $22, + override_mc_game_resolution_y = $23, - override_hook_pre_launch = $23, - override_hook_wrapper = $24, - override_hook_post_exit = $25 + override_hook_pre_launch = $24, + override_hook_wrapper = $25, + override_hook_post_exit = $26 ", self.path, install_stage, @@ -452,6 +471,7 @@ impl Profile { self.game_version, mod_loader, self.loader_version, + groups, linked_data_project_id, linked_data_version_id, linked_data_locked, @@ -571,7 +591,7 @@ impl Profile { &keys.iter().map(|s| &*s.cache_key).collect::>(), None, exec, - &fetch_semaphore, + fetch_semaphore, ) .await?; @@ -579,7 +599,7 @@ impl Profile { &file_hashes.iter().map(|x| &*x.hash).collect::>(), None, exec, - &fetch_semaphore, + fetch_semaphore, ) .await?; @@ -598,7 +618,7 @@ impl Profile { &file_updates.iter().map(|x| &**x).collect::>(), Some(CacheBehaviour::Bypass), exec, - &fetch_semaphore, + fetch_semaphore, ) .await?; @@ -656,72 +676,6 @@ impl Profile { Ok(files) } - // pub fn crash_task(path: ProfilePathId) { - // 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.metadata.name)).await?; - // } - // } - // - // Ok::<(), crate::Error>(()) - // } - // .await; - // - // match res { - // Ok(()) => {} - // Err(err) => { - // tracing::warn!( - // "Unable to send crash report to frontend: {err}" - // ) - // } - // }; - // }); - // } - - // #[tracing::instrument(skip(watcher))] - // #[theseus_macros::debug_pin] - // pub async fn watch_fs( - // profile_path: &Path, - // watcher: &mut Debouncer, - // ) -> crate::Result<()> { - // async fn watch_path( - // profile_path: &Path, - // watcher: &mut Debouncer, - // path: &str, - // ) -> crate::Result<()> { - // let path = profile_path.join(path); - // - // io::create_dir_all(&path).await?; - // - // watcher - // .watcher() - // .watch(&profile_path.join(path), RecursiveMode::Recursive)?; - // - // Ok(()) - // } - // - // watch_path(profile_path, watcher, ProjectType::Mod.get_folder()) - // .await?; - // watch_path(profile_path, watcher, ProjectType::ShaderPack.get_folder()) - // .await?; - // watch_path( - // profile_path, - // watcher, - // ProjectType::ResourcePack.get_folder(), - // ) - // .await?; - // watch_path(profile_path, watcher, ProjectType::DataPack.get_folder()) - // .await?; - // watch_path(profile_path, watcher, "crash-reports").await?; - // - // Ok(()) - // } - #[tracing::instrument(skip(exec))] #[theseus_macros::debug_pin] pub async fn add_project_version<'a, E>( @@ -735,7 +689,7 @@ impl Profile { E: sqlx::Acquire<'a, Database = sqlx::Sqlite>, { let version = - CachedEntry::get_version(&version_id, None, exec, fetch_semaphore) + CachedEntry::get_version(version_id, None, exec, fetch_semaphore) .await? .ok_or_else(|| { crate::ErrorKind::InputError(format!( diff --git a/packages/app-lib/src/util/fetch.rs b/packages/app-lib/src/util/fetch.rs index cc0b2acbb..caadc4fca 100644 --- a/packages/app-lib/src/util/fetch.rs +++ b/packages/app-lib/src/util/fetch.rs @@ -9,15 +9,15 @@ use serde::de::DeserializeOwned; use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::time::{self, Duration}; -use tokio::sync::{RwLock, Semaphore}; +use tokio::sync::Semaphore; use tokio::{fs::File, io::AsyncWriteExt}; use super::io::{self, IOError}; #[derive(Debug)] -pub struct IoSemaphore(pub RwLock); +pub struct IoSemaphore(pub Semaphore); #[derive(Debug)] -pub struct FetchSemaphore(pub RwLock); +pub struct FetchSemaphore(pub Semaphore); lazy_static! { pub static ref REQWEST_CLIENT: reqwest::Client = { @@ -77,8 +77,7 @@ pub async fn fetch_advanced( loading_bar: Option<(&LoadingBarId, f64)>, semaphore: &FetchSemaphore, ) -> crate::Result { - let io_semaphore = semaphore.0.read().await; - let _permit = io_semaphore.acquire().await?; + let _permit = semaphore.0.acquire().await?; for attempt in 1..=(FETCH_ATTEMPTS + 1) { let mut req = REQWEST_CLIENT.request(method.clone(), url); @@ -216,8 +215,7 @@ pub async fn post_json( where T: DeserializeOwned, { - let io_semaphore = semaphore.0.read().await; - let _permit = io_semaphore.acquire().await?; + let _permit = semaphore.0.acquire().await?; let mut req = REQWEST_CLIENT.post(url).json(&json_body); if let Some(creds) = &credentials.0 { @@ -237,8 +235,7 @@ pub async fn read_json( where T: DeserializeOwned, { - let io_semaphore = semaphore.0.read().await; - let _permit = io_semaphore.acquire().await?; + let _permit = semaphore.0.acquire().await?; let json = io::read(path).await?; let json = serde_json::from_slice::(&json)?; @@ -252,8 +249,7 @@ pub async fn write<'a>( bytes: &[u8], semaphore: &IoSemaphore, ) -> crate::Result<()> { - let io_semaphore = semaphore.0.read().await; - let _permit = io_semaphore.acquire().await?; + let _permit = semaphore.0.acquire().await?; if let Some(parent) = path.parent() { io::create_dir_all(parent).await?; @@ -277,8 +273,7 @@ pub async fn copy( let src: &Path = src.as_ref(); let dest = dest.as_ref(); - let io_semaphore = semaphore.0.read().await; - let _permit = io_semaphore.acquire().await?; + let _permit = semaphore.0.acquire().await?; if let Some(parent) = dest.parent() { io::create_dir_all(parent).await?;