From 0af394c8ee76280ed1aac555fcaa318b4ecb63cc Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:50:15 -0400 Subject: [PATCH 01/38] refactor(app-lib): use state helpers instead of raw state --- packages/app-lib/src/state/dirs.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/app-lib/src/state/dirs.rs b/packages/app-lib/src/state/dirs.rs index b4fb7e21713..181c89a146b 100644 --- a/packages/app-lib/src/state/dirs.rs +++ b/packages/app-lib/src/state/dirs.rs @@ -2,8 +2,7 @@ pub(crate) mod move_app_directory; use crate::LoadingBarType; use crate::event::emit::{emit_loading, init_loading}; -use crate::state::LAUNCHER_STATE; -use crate::state::Settings; +use crate::state::{Settings, State}; use crate::util::fetch::IoSemaphore; use std::path::PathBuf; use tokio::fs; @@ -23,8 +22,8 @@ pub struct DirectoryInfo { } impl DirectoryInfo { - pub fn global_handle_if_ready() -> Option<&'static Self> { - LAUNCHER_STATE.get().map(|x| &x.directories) + pub fn global_handle_if_ready() -> Option { + State::get_if_initialized().map(|state| state.directories.clone()) } pub fn get_initial_settings_dir(&self) -> Option { From 39530e046b83ac28c16105802dbcad50b4e7dffe Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:44:37 -0400 Subject: [PATCH 02/38] fix(app-lib): await state instead of polling --- packages/app-lib/src/error.rs | 9 +++++ packages/app-lib/src/state/mod.rs | 57 ++++++++++++++++++++++--------- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/packages/app-lib/src/error.rs b/packages/app-lib/src/error.rs index 7f9c718d074..1b957f0a235 100644 --- a/packages/app-lib/src/error.rs +++ b/packages/app-lib/src/error.rs @@ -265,6 +265,15 @@ impl std::fmt::Display for Error { } } +impl Clone for Error { + fn clone(&self) -> Self { + Self { + raw: Arc::clone(&self.raw), + source: Arc::clone(&self.raw).in_current_span(), + } + } +} + impl> From for Error { fn from(source: E) -> Self { let error = Into::::into(source); diff --git a/packages/app-lib/src/state/mod.rs b/packages/app-lib/src/state/mod.rs index f2c6cf6970f..22c4e11e17b 100644 --- a/packages/app-lib/src/state/mod.rs +++ b/packages/app-lib/src/state/mod.rs @@ -1,8 +1,9 @@ //! Theseus state management system use crate::util::fetch::{FetchSemaphore, IoSemaphore}; use dashmap::DashMap; -use std::sync::Arc; use std::sync::atomic::AtomicBool; +use std::sync::{Arc, LazyLock}; +use tokio::sync::watch::Sender; use tokio::sync::{Mutex, MutexGuard, OnceCell, OwnedMutexGuard, Semaphore}; use crate::state::instances::watcher::FileWatcher; @@ -67,9 +68,15 @@ mod legacy_converter; pub mod attached_world_data; pub mod server_join_log; -// Global state -// RwLock on state only has concurrent reads, except for config dir change which takes control of the State +enum StartupPhase { + Pending, + Ready, + Failed(crate::Error), +} + static LAUNCHER_STATE: OnceCell> = OnceCell::const_new(); +static STATE_STARTUP: LazyLock> = + LazyLock::new(|| Sender::new(StartupPhase::Pending)); static STATE_STARTUP_LOCK: Mutex<()> = Mutex::const_new(()); const MAX_CONCURRENT_INSTALL_JOBS: usize = 3; pub struct State { @@ -172,9 +179,17 @@ impl State { pub async fn init(app_identifier: String) -> crate::Result<()> { let _startup = STATE_STARTUP_LOCK.lock().await; - let state = LAUNCHER_STATE - .get_or_try_init(move || Self::initialize_state(app_identifier)) - .await?; + let result = LAUNCHER_STATE + .get_or_try_init(|| async { + STATE_STARTUP.send_replace(StartupPhase::Pending); + Self::initialize_state(app_identifier).await + }) + .await; + STATE_STARTUP.send_replace(match &result { + Ok(_) => StartupPhase::Ready, + Err(error) => StartupPhase::Failed(error.clone()), + }); + let state = result?; if state .startup_complete @@ -297,18 +312,28 @@ impl State { /// Get the current launcher state, waiting for initialization. pub async fn get() -> crate::Result> { - if !LAUNCHER_STATE.initialized() { - tracing::error!( - "Attempted to get state before it is initialized - this should never happen!" - ); - while !LAUNCHER_STATE.initialized() { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - } + if let Some(state) = LAUNCHER_STATE.get() { + return Ok(Arc::clone(state)); } - Ok(Arc::clone( - LAUNCHER_STATE.get().expect("State is not initialized!"), - )) + let mut phases = STATE_STARTUP.subscribe(); + let phase = phases + .wait_for(|phase| !matches!(phase, StartupPhase::Pending)) + .await + .expect("the startup sender is static and never dropped"); + match &*phase { + StartupPhase::Ready => Ok(Arc::clone( + LAUNCHER_STATE + .get() + .expect("state is set before startup is marked ready"), + )), + StartupPhase::Failed(error) => Err(error.clone()), + StartupPhase::Pending => { + unreachable!( + "wait_for returns only once the phase has left Pending" + ) + } + } } pub fn initialized() -> bool { From 41d2007afad155549a0f6ceb059e9482f188d983 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:49:24 -0400 Subject: [PATCH 03/38] refactor(app-lib): get state outside jre helper --- packages/app-lib/src/api/jre.rs | 3 +- packages/app-lib/src/util/jre.rs | 65 +++++++++++++++----------------- 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/packages/app-lib/src/api/jre.rs b/packages/app-lib/src/api/jre.rs index 9dd5e01cb1a..75f8ae96935 100644 --- a/packages/app-lib/src/api/jre.rs +++ b/packages/app-lib/src/api/jre.rs @@ -41,7 +41,8 @@ pub async fn set_java_version(java_version: JavaVersion) -> crate::Result<()> { pub async fn find_filtered_jres( java_version: Option, ) -> crate::Result> { - let jres = jre::get_all_jre().await?; + let state = State::get().await?; + let jres = jre::get_all_jre(&state.directories.java_versions_dir()).await?; // Filter out JREs that are not 1.17 or higher Ok(if let Some(java_version) = java_version { diff --git a/packages/app-lib/src/util/jre.rs b/packages/app-lib/src/util/jre.rs index 4e2c3c4b559..6ad51be1f0f 100644 --- a/packages/app-lib/src/util/jre.rs +++ b/packages/app-lib/src/util/jre.rs @@ -7,7 +7,7 @@ use std::process::Command; use std::{collections::HashSet, path::Path}; use tokio::task::JoinError; -use crate::{State, get_resource_file}; +use crate::get_resource_file; #[cfg(target_os = "windows")] use winreg::{ RegKey, @@ -18,12 +18,14 @@ use winreg::{ // Returns a Vec of unique JavaVersions from the PATH, Windows Registry Keys and common Java locations #[cfg(target_os = "windows")] #[tracing::instrument] -pub async fn get_all_jre() -> Result, JREError> { +pub async fn get_all_jre( + java_versions_dir: &Path, +) -> Result, JREError> { let mut jre_paths = HashSet::new(); // Add JRES directly on PATH jre_paths.extend(get_all_jre_path().await); - jre_paths.extend(get_all_autoinstalled_jre_path().await?); + jre_paths.extend(get_all_autoinstalled_jre_path(java_versions_dir)); if let Ok(java_home) = env::var("JAVA_HOME") { jre_paths.insert(PathBuf::from(java_home)); } @@ -105,13 +107,15 @@ pub fn get_paths_from_jre_winregkey(jre_key: RegKey) -> HashSet { // Returns a Vec of unique JavaVersions from the PATH, and common Java locations #[cfg(target_os = "macos")] #[tracing::instrument] -pub async fn get_all_jre() -> Result, JREError> { +pub async fn get_all_jre( + java_versions_dir: &Path, +) -> Result, JREError> { // Use HashSet to avoid duplicates let mut jre_paths = HashSet::new(); // Add JREs directly on PATH jre_paths.extend(get_all_jre_path().await); - jre_paths.extend(get_all_autoinstalled_jre_path().await?); + jre_paths.extend(get_all_autoinstalled_jre_path(java_versions_dir)); // Hard paths for locations for commonly installed .exes let java_paths = [ @@ -143,13 +147,15 @@ pub async fn get_all_jre() -> Result, JREError> { // Returns a Vec of unique JavaVersions from the PATH, and common Java locations #[cfg(target_os = "linux")] #[tracing::instrument] -pub async fn get_all_jre() -> Result, JREError> { +pub async fn get_all_jre( + java_versions_dir: &Path, +) -> Result, JREError> { // Use HashSet to avoid duplicates let mut jre_paths = HashSet::new(); // Add JREs directly on PATH jre_paths.extend(get_all_jre_path().await); - jre_paths.extend(get_all_autoinstalled_jre_path().await?); + jre_paths.extend(get_all_autoinstalled_jre_path(java_versions_dir)); // Hard paths for locations for commonly installed locations let java_paths = [ @@ -183,37 +189,31 @@ pub async fn get_all_jre() -> Result, JREError> { // Gets all JREs from the PATH env variable #[tracing::instrument] -async fn get_all_autoinstalled_jre_path() -> Result, JREError> -{ - Box::pin(async move { - let state = State::get().await.map_err(|_| JREError::StateError)?; - - let mut jre_paths = HashSet::new(); - let base_path = state.directories.java_versions_dir(); +fn get_all_autoinstalled_jre_path( + base_path: &Path, +) -> HashSet { + let mut jre_paths = HashSet::new(); - if base_path.is_dir() - && let Ok(dir) = std::fs::read_dir(base_path) - { - for entry in dir.flatten() { - let file_path = entry.path().join("bin"); + if base_path.is_dir() + && let Ok(dir) = std::fs::read_dir(base_path) + { + for entry in dir.flatten() { + let file_path = entry.path().join("bin"); - if let Ok(contents) = std::fs::read_to_string(file_path.clone()) + if let Ok(contents) = std::fs::read_to_string(file_path.clone()) { + let entry = entry.path().join(contents); + jre_paths.insert(entry); + } else { + #[cfg(not(target_os = "macos"))] { - let entry = entry.path().join(contents); - jre_paths.insert(entry); - } else { - #[cfg(not(target_os = "macos"))] - { - let file_path = file_path.join(JAVA_BIN); - jre_paths.insert(file_path); - } + let file_path = file_path.join(JAVA_BIN); + jre_paths.insert(file_path); } } } + } - Ok(jre_paths) - }) - .await + jre_paths } // Gets all JREs from the PATH env variable @@ -356,7 +356,4 @@ pub enum JREError { #[error("No stored tag for Minecraft version {0}")] NoMinecraftVersionFound(String), - - #[error("Error getting launcher state")] - StateError, } From a41b4f9c8c27f621e0c4d4d5ad19307f0b9f3c87 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 10:23:42 -0400 Subject: [PATCH 04/38] refactor(app-lib): get state outside file fetching --- .../src/api/instance/shared/publish.rs | 3 +- packages/app-lib/src/launcher/download.rs | 5 ++- packages/app-lib/src/util/fetch.rs | 31 ++----------------- 3 files changed, 9 insertions(+), 30 deletions(-) diff --git a/packages/app-lib/src/api/instance/shared/publish.rs b/packages/app-lib/src/api/instance/shared/publish.rs index 26d4d0d12af..03691368377 100644 --- a/packages/app-lib/src/api/instance/shared/publish.rs +++ b/packages/app-lib/src/api/instance/shared/publish.rs @@ -874,7 +874,7 @@ async fn build_config_bundle_candidate( let previous_bundle = if let Some(previous_bundle) = previous_bundle { Some( - crate::util::fetch::fetch_file_mirrors( + crate::util::fetch::fetch_file_mirrors_in( &[&previous_bundle.url], None, None, @@ -882,6 +882,7 @@ async fn build_config_bundle_candidate( &state.fetch_semaphore, &state.pool, None, + Some(&state.directories.store_staging_dir()), ) .await?, ) diff --git a/packages/app-lib/src/launcher/download.rs b/packages/app-lib/src/launcher/download.rs index f5e14bd1d4d..f8a8ec0b751 100644 --- a/packages/app-lib/src/launcher/download.rs +++ b/packages/app-lib/src/launcher/download.rs @@ -168,6 +168,7 @@ async fn fetch_minecraft_file( &st.fetch_semaphore, &st.pool, None, + &st.directories.store_staging_dir(), ) .await; }; @@ -195,6 +196,7 @@ async fn fetch_minecraft_file( &st.fetch_semaphore, &st.pool, Some(&mut progress_fn as &mut FetchProgressFn<'_>), + &st.directories.store_staging_dir(), ) .await { @@ -992,7 +994,8 @@ pub async fn download_libraries( None, &st.fetch_semaphore, &st.pool, - None, + None, + &st.directories.store_staging_dir(), ) .await { diff --git a/packages/app-lib/src/util/fetch.rs b/packages/app-lib/src/util/fetch.rs index f861f4cb0f6..7fe2b8ff9b8 100644 --- a/packages/app-lib/src/util/fetch.rs +++ b/packages/app-lib/src/util/fetch.rs @@ -498,6 +498,7 @@ pub async fn fetch_file( semaphore: &FetchSemaphore, exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, progress: Option<&mut FetchProgressFn<'_>>, + staging: &Path, ) -> crate::Result { let body = fetch_advanced_with_target( Method::GET, @@ -514,7 +515,7 @@ pub async fn fetch_file( &INSECURE_REQWEST_CLIENT, progress, true, - None, + Some(staging), ) .await?; match body { @@ -523,28 +524,6 @@ pub async fn fetch_file( } } -pub async fn fetch_file_mirrors( - mirrors: &[&str], - sha1: Option<&str>, - download_meta: Option<&DownloadMeta>, - uri_path: Option<&'static str>, - semaphore: &FetchSemaphore, - exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy, - progress: Option<&mut FetchProgressFn<'_>>, -) -> crate::Result { - fetch_file_mirrors_in( - mirrors, - sha1, - download_meta, - uri_path, - semaphore, - exec, - progress, - None, - ) - .await -} - #[allow(clippy::too_many_arguments)] pub(crate) async fn fetch_file_mirrors_in( mirrors: &[&str], @@ -598,11 +577,7 @@ async fn read_file_response( staging: Option<&Path>, ) -> crate::Result { use futures::StreamExt; - let staging = staging.map(Path::to_path_buf).or_else(|| { - crate::State::get_if_initialized() - .map(|state| state.directories.store_staging_dir()) - }); - let (mut file, path) = temporary_file(staging.as_deref()).await?; + let (mut file, path) = temporary_file(staging).await?; let total = response.content_length().unwrap_or(0); let mut stream = response.bytes_stream(); let mut hasher = ContentHasher::default(); From 55d6665c9f7e5cb503b159cb0179076f87f7faf7 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 10:48:12 -0400 Subject: [PATCH 05/38] refactor(app-lib): pull state out of edit_instance me when it asked for a component of state just to get the state again --- packages/app-lib/src/api/instance/icon.rs | 2 +- packages/app-lib/src/api/instance/lifecycle.rs | 2 +- .../app-lib/src/api/instance/shared/publish.rs | 2 +- .../src/state/instances/commands/edit_instance.rs | 14 ++++---------- .../state/instances/commands/refresh_instances.rs | 5 ++--- packages/app-lib/src/state/mod.rs | 2 +- 6 files changed, 10 insertions(+), 17 deletions(-) diff --git a/packages/app-lib/src/api/instance/icon.rs b/packages/app-lib/src/api/instance/icon.rs index e9692257d48..384697e85ca 100644 --- a/packages/app-lib/src/api/instance/icon.rs +++ b/packages/app-lib/src/api/instance/icon.rs @@ -285,7 +285,7 @@ async fn apply_instance_icon( icon_config: Some(icon_config), ..EditInstance::default() }, - &state.pool, + state, ) .await?; diff --git a/packages/app-lib/src/api/instance/lifecycle.rs b/packages/app-lib/src/api/instance/lifecycle.rs index 5fb518bc2e1..e7fc37b318d 100644 --- a/packages/app-lib/src/api/instance/lifecycle.rs +++ b/packages/app-lib/src/api/instance/lifecycle.rs @@ -106,7 +106,7 @@ pub async fn edit( instance.install_stage, )?; } - crate::state::edit_instance(instance_id, patch, &state.pool).await?; + crate::state::edit_instance(instance_id, patch, &state).await?; let instance = crate::state::get_instance(instance_id, &state.pool) .await? diff --git a/packages/app-lib/src/api/instance/shared/publish.rs b/packages/app-lib/src/api/instance/shared/publish.rs index 03691368377..a4b5f9acc68 100644 --- a/packages/app-lib/src/api/instance/shared/publish.rs +++ b/packages/app-lib/src/api/instance/shared/publish.rs @@ -82,7 +82,7 @@ pub(super) async fn detach_local_shared_instance( }), ..Default::default() }, - &state.pool, + state, ) .await?; } diff --git a/packages/app-lib/src/state/instances/commands/edit_instance.rs b/packages/app-lib/src/state/instances/commands/edit_instance.rs index 471d832e203..892c5c6302e 100644 --- a/packages/app-lib/src/state/instances/commands/edit_instance.rs +++ b/packages/app-lib/src/state/instances/commands/edit_instance.rs @@ -5,11 +5,10 @@ use crate::state::instances::{ }; use crate::state::{ Hooks, InstanceInstallStage, LauncherFeatureVersion, MemorySettings, - ModLoader, ReleaseChannel, WindowSize, + ModLoader, ReleaseChannel, State, WindowSize, }; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use sqlx::SqlitePool; #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct EditInstance { @@ -108,18 +107,13 @@ pub struct AppliedContentSetPatch { pub(crate) async fn edit_instance( instance_id: &str, patch: EditInstance, - pool: &SqlitePool, + state: &State, ) -> crate::Result { - let state = crate::State::get_if_initialized(); + let pool = &state.pool; let _runtime_lease = if patch.launch_overrides.is_some() || patch.content_set_patch.is_some() { - match state.as_ref() { - Some(state) => { - Some(state.content_store.runtime_cache_lock.read().await) - } - None => None, - } + Some(state.content_store.runtime_cache_lock.read().await) } else { None }; diff --git a/packages/app-lib/src/state/instances/commands/refresh_instances.rs b/packages/app-lib/src/state/instances/commands/refresh_instances.rs index cfe13be1d28..d99ae9e1c50 100644 --- a/packages/app-lib/src/state/instances/commands/refresh_instances.rs +++ b/packages/app-lib/src/state/instances/commands/refresh_instances.rs @@ -3,8 +3,7 @@ use crate::state::LauncherFeatureVersion; use super::edit_instance::EditInstance; -pub(crate) async fn refresh_all_instances() -> crate::Result<()> { - let state = State::get().await?; +pub(crate) async fn refresh_all_instances(state: &State) -> crate::Result<()> { let instances = crate::state::instances::adapters::sqlite::instance_rows::list_instances( &state.pool, ) @@ -26,7 +25,7 @@ pub(crate) async fn refresh_all_instances() -> crate::Result<()> { launcher_feature_version, ..EditInstance::default() }, - &state.pool, + state, ) .await?; } diff --git a/packages/app-lib/src/state/mod.rs b/packages/app-lib/src/state/mod.rs index 22c4e11e17b..bfb9f4efd51 100644 --- a/packages/app-lib/src/state/mod.rs +++ b/packages/app-lib/src/state/mod.rs @@ -287,7 +287,7 @@ impl State { let res = tokio::try_join!( state.discord_rpc.clear_to_default(true), - instances::refresh_all_instances(), + refresh_all_instances(state), Settings::migrate(&state.pool), ModrinthCredentials::refresh_all(), ); From a7bb3fbf4a8cb74783201f2fbb6ff49f56563a30 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:03:17 -0400 Subject: [PATCH 06/38] refactor(app-lib): pull state out of discord stuff --- packages/app-lib/src/launcher/mod.rs | 2 +- packages/app-lib/src/state/discord.rs | 19 +++++++++++-------- packages/app-lib/src/state/friends.rs | 4 +--- packages/app-lib/src/state/mod.rs | 13 ++++++++++--- packages/app-lib/src/state/mr_auth.rs | 13 ++++++++----- packages/app-lib/src/state/process.rs | 5 ++++- 6 files changed, 35 insertions(+), 21 deletions(-) diff --git a/packages/app-lib/src/launcher/mod.rs b/packages/app-lib/src/launcher/mod.rs index f783b20f0a4..03f67dfbd4a 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -1257,7 +1257,7 @@ pub async fn launch_minecraft( let _ = state .discord_rpc - .set_activity(&format!("Playing {}", instance.name), true) + .set_activity(&format!("Playing {}", instance.name), true, &state.pool) .await; let _ = state diff --git a/packages/app-lib/src/state/discord.rs b/packages/app-lib/src/state/discord.rs index 6fcef793af7..f3f78fb4f9d 100644 --- a/packages/app-lib/src/state/discord.rs +++ b/packages/app-lib/src/state/discord.rs @@ -4,9 +4,10 @@ use discord_rich_presence::{ DiscordIpc, DiscordIpcClient, activity::{Activity, Assets}, }; +use sqlx::SqlitePool; use tokio::sync::RwLock; -use crate::State; +use crate::state::ProcessManager; pub struct DiscordGuard { client: Arc>, @@ -47,10 +48,10 @@ impl DiscordGuard { &self, msg: &str, reconnect_if_fail: bool, + pool: &SqlitePool, ) -> crate::Result<()> { // Check if discord is disabled, and if so, clear the activity instead - let state = State::get().await?; - let settings = crate::state::Settings::get(&state.pool).await?; + let settings = crate::state::Settings::get(pool).await?; if !settings.discord_rpc { Ok(self.clear_activity(true).await?) } else { @@ -124,24 +125,26 @@ impl DiscordGuard { pub async fn clear_to_default( &self, reconnect_if_fail: bool, + pool: &SqlitePool, + process_manager: &ProcessManager, ) -> crate::Result<()> { - let state = State::get().await?; - - let settings = crate::state::Settings::get(&state.pool).await?; + let settings = crate::state::Settings::get(pool).await?; if !settings.discord_rpc { println!("Discord is disabled, clearing activity"); return self.clear_activity(true).await; } - let running_instances = state.process_manager.get_all(); + let running_instances = process_manager.get_all(); if let Some(existing_child) = running_instances.first() { self.set_activity( &format!("Playing {}", existing_child.instance_name), reconnect_if_fail, + pool, ) .await?; } else { - self.set_activity("Idling...", reconnect_if_fail).await?; + self.set_activity("Idling...", reconnect_if_fail, pool) + .await?; } Ok(()) } diff --git a/packages/app-lib/src/state/friends.rs b/packages/app-lib/src/state/friends.rs index ebf250e69fe..bb3995197ba 100644 --- a/packages/app-lib/src/state/friends.rs +++ b/packages/app-lib/src/state/friends.rs @@ -290,9 +290,7 @@ impl FriendsSocket { } #[tracing::instrument(skip_all)] - pub async fn socket_loop() -> crate::Result<()> { - let state = crate::State::get().await?; - + pub async fn socket_loop(state: Arc) -> crate::Result<()> { tokio::task::spawn(async move { let mut last_connection = Utc::now(); let mut last_ping = Utc::now(); diff --git a/packages/app-lib/src/state/mod.rs b/packages/app-lib/src/state/mod.rs index bfb9f4efd51..dbdbb9970d0 100644 --- a/packages/app-lib/src/state/mod.rs +++ b/packages/app-lib/src/state/mod.rs @@ -286,10 +286,17 @@ impl State { } let res = tokio::try_join!( - state.discord_rpc.clear_to_default(true), + state.discord_rpc.clear_to_default( + true, + &state.pool, + &state.process_manager, + ), refresh_all_instances(state), Settings::migrate(&state.pool), - ModrinthCredentials::refresh_all(), + ModrinthCredentials::refresh_all( + &state.pool, + &state.fetch_semaphore, + ), ); if let Err(e) = res { @@ -304,7 +311,7 @@ impl State { &state.process_manager, ) .await; - let _ = FriendsSocket::socket_loop().await; + let _ = FriendsSocket::socket_loop(Arc::clone(state)).await; }); Ok(()) diff --git a/packages/app-lib/src/state/mr_auth.rs b/packages/app-lib/src/state/mr_auth.rs index 86a3bfeca52..9506e7cb4c3 100644 --- a/packages/app-lib/src/state/mr_auth.rs +++ b/packages/app-lib/src/state/mr_auth.rs @@ -3,6 +3,7 @@ use crate::util::fetch::{FetchSemaphore, fetch_advanced}; use chrono::{DateTime, Duration, TimeZone, Utc}; use reqwest::Method; use serde::{Deserialize, Serialize}; +use sqlx::SqlitePool; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ModrinthCredentials { @@ -175,17 +176,19 @@ impl ModrinthCredentials { Ok(()) } - pub(crate) async fn refresh_all() -> crate::Result<()> { - let state = crate::State::get().await?; - let all = Self::get_all(&state.pool).await?; + pub(crate) async fn refresh_all( + pool: &SqlitePool, + fetch_semaphore: &FetchSemaphore, + ) -> crate::Result<()> { + let all = Self::get_all(pool).await?; let user_ids = all.into_iter().map(|x| x.user_id).collect::>(); CachedEntry::get_user_many( &user_ids.iter().map(|x| &**x).collect::>(), Some(CacheBehaviour::Bypass), - &state.pool, - &state.fetch_semaphore, + pool, + fetch_semaphore, ) .await?; diff --git a/packages/app-lib/src/state/process.rs b/packages/app-lib/src/state/process.rs index 45471b9f861..a8fa26dc0a8 100644 --- a/packages/app-lib/src/state/process.rs +++ b/packages/app-lib/src/state/process.rs @@ -984,7 +984,10 @@ impl Process { tracing::warn!("Failed to write exit status to log file: {}", e); } - let _ = state.discord_rpc.clear_to_default(true).await; + let _ = state + .discord_rpc + .clear_to_default(true, &state.pool, &state.process_manager) + .await; let _ = state.friends_socket.update_status(None).await; From 4a1e641263b97f0beb8d1b412d99b90816a936fe Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:34:00 -0400 Subject: [PATCH 07/38] refactor(app-lib): pull state out of file watcher state gets cloned on get anyways so this is effectively the same as before --- .../app-lib/src/state/instances/watcher.rs | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/packages/app-lib/src/state/instances/watcher.rs b/packages/app-lib/src/state/instances/watcher.rs index 4dd9b08e32d..89046788295 100644 --- a/packages/app-lib/src/state/instances/watcher.rs +++ b/packages/app-lib/src/state/instances/watcher.rs @@ -27,7 +27,7 @@ pub struct FileWatcher { static CONTENT_SYNCS: LazyLock> = LazyLock::new(DashMap::new); -fn queue_content_sync(instance_id: String) { +fn queue_content_sync(instance_id: String, state: Arc) { match CONTENT_SYNCS.entry(instance_id.clone()) { Entry::Occupied(mut entry) => { *entry.get_mut() = true; @@ -40,7 +40,6 @@ fn queue_content_sync(instance_id: String) { tokio::spawn(async move { loop { let result: crate::Result = async { - let state = State::get().await?; let Some(instance) = instance_rows::get_instance_by_id( &instance_id, &state.pool, @@ -107,6 +106,16 @@ pub async fn init_watcher() -> crate::Result { tokio::task::spawn(async move { let span = tracing::span!(tracing::Level::INFO, "init_watcher"); tracing::info!(parent: &span, "Initing watcher"); + let state = match State::get().await { + Ok(state) => state, + Err(error) => { + tracing::error!( + parent: &span, + "File watcher stopped, state failed to initialize: {error}" + ); + return; + } + }; while let Some(res) = rx.recv().await { let _span = span.enter(); @@ -187,7 +196,7 @@ pub async fn init_watcher() -> crate::Result { .as_ref() .is_some_and(|x| *x == "txt") { - crash_task(instance_id); + crash_task(instance_id, Arc::clone(&state)); } else if (is_screenshot_event && !visited_screenshot_instances .contains(&instance_id)) @@ -233,16 +242,16 @@ pub async fn init_watcher() -> crate::Result { if !e.path.is_file() { let instance_id = instance_id.clone(); let world = world.clone(); + let state = Arc::clone(&state); 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}") - } + if 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 { @@ -273,7 +282,10 @@ pub async fn init_watcher() -> crate::Result { ) }); if sync_content { - queue_content_sync(emit_instance_id); + queue_content_sync( + emit_instance_id, + Arc::clone(&state), + ); } else { tokio::spawn(async move { if reconcile_screenshots @@ -434,10 +446,9 @@ pub(crate) async fn watch_instance_folder( .insert(instance_path.to_string(), instance_id.to_string()); } -fn crash_task(instance_id: String) { +fn crash_task(instance_id: String, state: Arc) { 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? From 034271c6403c39984a0aa49ad32da913831878b4 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:25:33 -0400 Subject: [PATCH 08/38] refactor(app-lib): pass sqlite pool instead of whole state and retain pool for install progress --- packages/app-lib/src/install/events.rs | 22 ++- packages/app-lib/src/install/recovery.rs | 10 +- packages/app-lib/src/install/runner.rs | 127 ++++++++++++------ .../app-lib/src/install/shared_instance.rs | 6 +- packages/app-lib/src/install/store.rs | 76 +++++------ packages/app-lib/src/launcher/mod.rs | 2 +- .../src/state/content_store/commands/cache.rs | 2 +- 7 files changed, 145 insertions(+), 100 deletions(-) diff --git a/packages/app-lib/src/install/events.rs b/packages/app-lib/src/install/events.rs index ffe9518118d..d49abbd9107 100644 --- a/packages/app-lib/src/install/events.rs +++ b/packages/app-lib/src/install/events.rs @@ -3,6 +3,7 @@ use super::model::{ InstallJobState, InstallPhaseDetails, InstallPhaseId, InstallProgress, }; use super::store; +use sqlx::SqlitePool; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::Mutex; @@ -14,6 +15,7 @@ const CONTENT_PROGRESS_PERSIST_STEPS: u64 = 25; #[derive(Clone, Debug)] pub struct InstallProgressReporter { job_id: Uuid, + pool: SqlitePool, state: Arc>, } @@ -25,9 +27,10 @@ struct InstallProgressReporterState { } impl InstallProgressReporter { - pub fn new(job_id: Uuid, state: InstallJobState) -> Self { + pub fn new(job_id: Uuid, state: InstallJobState, pool: SqlitePool) -> Self { Self { job_id, + pool, state: Arc::new(Mutex::new(InstallProgressReporterState { job: state, last_persisted_at: Instant::now(), @@ -71,30 +74,24 @@ impl InstallProgressReporter { context: Option, persist: bool, ) -> crate::Result<()> { - let app_state = if persist { - Some(crate::State::get().await?) - } else { - None - }; let mut state = self.state.lock().await; state.job.set_context(context); - let Some(app_state) = app_state else { + if !persist { return Ok(()); - }; + } let record = - store::update_state(self.job_id, &state.job, &app_state).await?; + store::update_state(self.job_id, &state.job, &self.pool).await?; state.mark_persisted(); emit_install_job(&record.snapshot()).await } pub async fn persist(&self) -> crate::Result { - let app_state = crate::State::get().await?; let mut state = self.state.lock().await; let record = - store::update_state(self.job_id, &state.job, &app_state).await?; + store::update_state(self.job_id, &state.job, &self.pool).await?; state.mark_persisted(); let snapshot = record.snapshot(); emit_install_job(&snapshot).await?; @@ -131,7 +128,6 @@ impl InstallProgressReporter { events: Vec, ) -> crate::Result<()> { super::control::checkpoint(self.job_id).await?; - let app_state = crate::State::get().await?; let mut state = self.state.lock().await; let phase_started = state.job.progress.phase != phase || matches!( @@ -149,7 +145,7 @@ impl InstallProgressReporter { } let record = - store::update_state(self.job_id, &state.job, &app_state).await?; + store::update_state(self.job_id, &state.job, &self.pool).await?; state.mark_persisted(); emit_install_job(&record.snapshot()).await } diff --git a/packages/app-lib/src/install/recovery.rs b/packages/app-lib/src/install/recovery.rs index f25647eb1cc..7ebe66c5e0a 100644 --- a/packages/app-lib/src/install/recovery.rs +++ b/packages/app-lib/src/install/recovery.rs @@ -483,7 +483,7 @@ async fn copy_symlink(source: &Path, target: &Path) -> crate::Result<()> { } pub async fn recover_interrupted_jobs(state: &State) -> crate::Result<()> { - let jobs = store::list_interrupted_candidates(state).await?; + let jobs = store::list_interrupted_candidates(&state.pool).await?; for job in jobs { let job_id = job.id; @@ -503,7 +503,7 @@ pub async fn recover_interrupted_jobs(state: &State) -> crate::Result<()> { } async fn recover_orphaned_install_stages(state: &State) -> crate::Result<()> { - let jobs = store::list_all(state).await?; + let jobs = store::list_all(&state.pool).await?; for instance in instance_rows::list_instances(&state.pool).await? { let needs_recovery = jobs.iter().any(|job| { job.instance_id.as_deref() == Some(instance.id.as_str()) @@ -578,11 +578,11 @@ async fn recover_interrupted_job_inner( job.id, InstallJobStatus::Canceled, &job.state, - state, + &state.pool, ) .await? { - store::dismiss(job.id, state).await?; + store::dismiss(job.id, &state.pool).await?; clear_staging_dir(&job.state).await; emit_install_job(&record.snapshot()).await?; } @@ -637,7 +637,7 @@ async fn recover_interrupted_job_inner( job.id, InstallJobStatus::Interrupted, &job.state, - state, + &state.pool, ) .await? { diff --git a/packages/app-lib/src/install/runner.rs b/packages/app-lib/src/install/runner.rs index 33a5ca261a2..dc6d6074dfb 100644 --- a/packages/app-lib/src/install/runner.rs +++ b/packages/app-lib/src/install/runner.rs @@ -169,7 +169,7 @@ pub(crate) async fn wait_for_job(job_id: Uuid) -> crate::Result<()> { let notified = completion.notified(); tokio::pin!(notified); notified.as_mut().enable(); - let job = store::get_required(job_id, &state).await?; + let job = store::get_required(job_id, &state.pool).await?; if job.status == InstallJobStatus::Succeeded { return Ok(()); } @@ -206,7 +206,7 @@ pub async fn list_jobs( include_finished: bool, ) -> crate::Result> { let state = State::get().await?; - Ok(store::list(include_finished, &state) + Ok(store::list(include_finished, &state.pool) .await? .into_iter() .map(|job| job.snapshot()) @@ -215,12 +215,12 @@ pub async fn list_jobs( pub async fn get_job(job_id: Uuid) -> crate::Result { let state = State::get().await?; - Ok(store::get_required(job_id, &state).await?.snapshot()) + Ok(store::get_required(job_id, &state.pool).await?.snapshot()) } pub async fn job_support_details(job_id: Uuid) -> crate::Result { let state = State::get().await?; - let job = store::get_required(job_id, &state).await?; + let job = store::get_required(job_id, &state.pool).await?; diagnostics::build_job_support_details(&job, &state).await } @@ -233,7 +233,7 @@ pub fn retry_job( async fn retry_job_inner(job_id: Uuid) -> crate::Result { let _admission = INSTALL_ADMISSION.lock().await; let state = State::get().await?; - let mut job = store::get_required(job_id, &state).await?; + let mut job = store::get_required(job_id, &state.pool).await?; if !matches!( job.status, @@ -247,8 +247,12 @@ async fn retry_job_inner(job_id: Uuid) -> crate::Result { } if let Some(instance_id) = current_instance_id(&job.state) { - store::ensure_no_pending_recovery(&instance_id, Some(job_id), &state) - .await?; + store::ensure_no_pending_recovery( + &instance_id, + Some(job_id), + &state.pool, + ) + .await?; } let cleanup_target_guard = reserve_target(&job.state.target)?; @@ -258,7 +262,8 @@ async fn retry_job_inner(job_id: Uuid) -> crate::Result { let recovered = job.state.clone(); job.state.rollback_error = None; job.state.paths.staging_dir = None; - store::update_status(job_id, job.status, &job.state, &state).await?; + store::update_status(job_id, job.status, &job.state, &state.pool) + .await?; recovery::clear_staging_dir(&recovered).await; } @@ -282,7 +287,7 @@ async fn retry_job_inner(job_id: Uuid) -> crate::Result { job_id, InstallJobStatus::Queued, &job.state, - &state, + &state.pool, ) .await { @@ -315,7 +320,9 @@ async fn retry_job_inner(job_id: Uuid) -> crate::Result { } return Err(error); } - let record = match store::update_state(job_id, &job.state, &state).await { + let record = match store::update_state(job_id, &job.state, &state.pool) + .await + { Ok(record) => record, Err(error) => { let error_view = install_error_view( @@ -372,7 +379,7 @@ async fn set_job_paused( paused: bool, ) -> crate::Result { let state = State::get().await?; - let job = store::get_required(job_id, &state).await?; + let job = store::get_required(job_id, &state.pool).await?; if !job.snapshot().can_pause { return Err(ErrorKind::InputError( "Install job cannot be paused or resumed".to_string(), @@ -383,14 +390,14 @@ async fn set_job_paused( ErrorKind::InputError("Install worker is unavailable".to_string()) })?; control.set_paused(paused)?; - let snapshot = store::get_required(job_id, &state).await?.snapshot(); + let snapshot = store::get_required(job_id, &state.pool).await?.snapshot(); emit_install_job(&snapshot).await?; Ok(snapshot) } pub async fn cancel_job(job_id: Uuid) -> crate::Result { let state = State::get().await?; - let job = store::get_required(job_id, &state).await?; + let job = store::get_required(job_id, &state.pool).await?; if job.snapshot().canceling { return Ok(job.snapshot()); } @@ -404,7 +411,7 @@ pub async fn cancel_job(job_id: Uuid) -> crate::Result { ErrorKind::InputError("Install worker is unavailable".to_string()) })?; control.cancel()?; - let snapshot = store::get_required(job_id, &state).await?.snapshot(); + let snapshot = store::get_required(job_id, &state.pool).await?.snapshot(); emit_install_job(&snapshot).await?; Ok(snapshot) } @@ -416,7 +423,8 @@ pub(crate) async fn cancel_jobs_for_instance_deletion( state: &State, ) -> crate::Result<(MutexGuard<'static, ()>, OwnedMutexGuard<()>)> { let admission = INSTALL_ADMISSION.lock().await; - let jobs = store::list_active_for_instance(instance_id, state).await?; + let jobs = + store::list_active_for_instance(instance_id, &state.pool).await?; for job in &jobs { if let Some(control) = super::control::get(job.id) { // A finishing worker cannot be canceled, but must still finish before deletion. @@ -424,7 +432,9 @@ pub(crate) async fn cancel_jobs_for_instance_deletion( } } let target_guard = target_lock(instance_id).lock_owned().await; - for mut job in store::list_active_for_instance(instance_id, state).await? { + for mut job in + store::list_active_for_instance(instance_id, &state.pool).await? + { let canceled_phase = job.state.progress.phase; job.state.error = Some(InstallErrorView::from_message( "canceled", @@ -439,26 +449,26 @@ pub(crate) async fn cancel_jobs_for_instance_deletion( job.id, InstallJobStatus::Canceled, &job.state, - state, + &state.pool, ) .await? else { continue; }; - store::dismiss(job.id, state).await?; + store::dismiss(job.id, &state.pool).await?; emit_install_job(&record.snapshot()).await?; } for job in jobs { - store::dismiss(job.id, state).await?; + store::dismiss(job.id, &state.pool).await?; } Ok((admission, target_guard)) } pub async fn dismiss_job(job_id: Uuid) -> crate::Result<()> { let state = State::get().await?; - store::dismiss(job_id, &state).await + store::dismiss(job_id, &state.pool).await } fn start( @@ -474,13 +484,15 @@ async fn start_inner( let mut target_guard = reserve_target(&request.target())?; let state = State::get().await?; if let InstallTarget::ExistingInstance { instance_id } = request.target() { - store::ensure_no_pending_recovery(&instance_id, None, &state).await?; + store::ensure_no_pending_recovery(&instance_id, None, &state.pool) + .await?; } let id = Uuid::new_v4(); let mut job_state = InstallJobState::new(request); set_initial_display(&mut job_state); let record = - store::insert(id, &job_state, InstallJobStatus::Queued, &state).await?; + store::insert(id, &job_state, InstallJobStatus::Queued, &state.pool) + .await?; emit_install_job(&record.snapshot()).await?; if let Err(error) = prepare_initial_instance(&mut job_state, &state).await { @@ -498,7 +510,7 @@ async fn start_inner( } return Err(error); } - let record = match store::update_state(id, &job_state, &state).await { + let record = match store::update_state(id, &job_state, &state.pool).await { Ok(record) => record, Err(error) => { let error_view = install_error_view( @@ -765,7 +777,7 @@ async fn run_job_inner( control: &std::sync::Arc, ) -> crate::Result<()> { let state = State::get().await?; - let mut job = store::get_required(job_id, &state).await?; + let mut job = store::get_required(job_id, &state.pool).await?; if job.status != InstallJobStatus::Queued { return Ok(()); @@ -779,7 +791,7 @@ async fn run_job_inner( } else { None }; - job = store::get_required(job_id, &state).await?; + job = store::get_required(job_id, &state.pool).await?; if job.status != InstallJobStatus::Queued { return Ok(()); @@ -792,7 +804,7 @@ async fn run_job_inner( InstallJobStatus::Queued, InstallJobStatus::Running, &job_state, - &state, + &state.pool, ) .await? else { @@ -818,7 +830,7 @@ async fn run_job_inner( } else { control.finish_failed(); } - if let Ok(record) = store::get_required(job_id, &state).await { + if let Ok(record) = store::get_required(job_id, &state.pool).await { let status = record.status; job_state = record.state; if status != InstallJobStatus::Running { @@ -850,7 +862,7 @@ async fn run_job_inner( job_state.rollback_error = None; job_state.context = None; if let Some(record) = - store::complete_success(job_id, &job_state, &state).await? + store::complete_success(job_id, &job_state, &state.pool).await? { if let Err(error) = crate::api::instance::reconcile_instance_synced_options( @@ -910,7 +922,7 @@ async fn terminalize_stranded_job( message: String, ) -> crate::Result<()> { let state = State::get().await?; - let job = store::get_required(job_id, &state).await?; + let job = store::get_required(job_id, &state.pool).await?; if !matches!( job.status, InstallJobStatus::Queued | InstallJobStatus::Running @@ -997,7 +1009,7 @@ async fn terminalize_failed_job_inner( InstallJobStatus::Failed }, &job_state, - state, + &state.pool, ) .await? { @@ -1070,7 +1082,11 @@ async fn run_request_inner( crate::launcher::install_minecraft_with_reporter( &context, false, - Some(InstallProgressReporter::new(job_id, job_state.clone())), + Some(InstallProgressReporter::new( + job_id, + job_state.clone(), + state.pool.clone(), + )), ) .await?; Ok(Some(instance_id)) @@ -1153,7 +1169,11 @@ async fn run_request_inner( launcher_type, base_path, instance_folder, - InstallProgressReporter::new(job_id, job_state.clone()), + InstallProgressReporter::new( + job_id, + job_state.clone(), + state.pool.clone(), + ), ) .await?; Ok(Some(instance_id)) @@ -1179,7 +1199,11 @@ async fn run_request_inner( crate::api::instance::get_full_path(&source_instance_id) .await?, &state.io_semaphore, - InstallProgressReporter::new(job_id, job_state.clone()), + InstallProgressReporter::new( + job_id, + job_state.clone(), + state.pool.clone(), + ), InstallPhaseDetails::Empty, ) .await?; @@ -1195,7 +1219,11 @@ async fn run_request_inner( crate::launcher::install_minecraft_with_reporter( &context, false, - Some(InstallProgressReporter::new(job_id, job_state.clone())), + Some(InstallProgressReporter::new( + job_id, + job_state.clone(), + state.pool.clone(), + )), ) .await?; emit_instance(&instance_id, InstancePayloadType::Edited).await?; @@ -1224,7 +1252,11 @@ async fn run_request_inner( crate::launcher::install_minecraft_with_reporter( &context, force, - Some(InstallProgressReporter::new(job_id, job_state.clone())), + Some(InstallProgressReporter::new( + job_id, + job_state.clone(), + state.pool.clone(), + )), ) .await?; Ok(Some(instance_id)) @@ -1286,7 +1318,11 @@ async fn run_request_inner( crate::state::instances::commands::apply_bulk_update( &instance_id, plan, - InstallProgressReporter::new(job_id, job_state.clone()), + InstallProgressReporter::new( + job_id, + job_state.clone(), + state.pool.clone(), + ), state, ) .await?; @@ -1417,7 +1453,11 @@ async fn remove_existing_pack_content( .into_iter() .filter_map(|file| (!file.enabled).then_some(file.project_id?)) .collect::>(); - let reporter = InstallProgressReporter::new(job_id, job_state.clone()); + let reporter = InstallProgressReporter::new( + job_id, + job_state.clone(), + state.pool.clone(), + ); let old_pack = generate_pack_from_version_id_with_reporter( project_id.clone(), version_id.clone(), @@ -1530,7 +1570,12 @@ async fn install_pack_inner( instance_id: String, reason: DownloadReason, ) -> crate::Result<()> { - let reporter = InstallProgressReporter::new(job_id, job_state.clone()); + let state = State::get().await?; + let reporter = InstallProgressReporter::new( + job_id, + job_state.clone(), + state.pool.clone(), + ); reporter .update( InstallPhaseId::DownloadingPackFile, @@ -1652,7 +1697,7 @@ async fn prepare_update_backup( ) .await?; job_state.paths.staging_dir = Some(staging_dir); - let record = store::update_state(job_id, job_state, state).await?; + let record = store::update_state(job_id, job_state, &state.pool).await?; emit_install_job(&record.snapshot()).await?; super::control::checkpoint(job_id).await } @@ -1701,7 +1746,7 @@ pub(super) async fn update_progress( ) -> crate::Result<()> { super::control::checkpoint(job_id).await?; job_state.set_progress(phase, None, details); - let record = store::update_state(job_id, job_state, state).await?; + let record = store::update_state(job_id, job_state, &state.pool).await?; emit_install_job(&record.snapshot()).await?; Ok(()) } @@ -1721,7 +1766,7 @@ pub(super) async fn update_content_progress( secondary: None, }); job_state.progress.details = InstallPhaseDetails::Empty; - let record = store::update_state(job_id, job_state, state).await?; + let record = store::update_state(job_id, job_state, &state.pool).await?; emit_install_job(&record.snapshot()).await?; Ok(()) } diff --git a/packages/app-lib/src/install/shared_instance.rs b/packages/app-lib/src/install/shared_instance.rs index 248f302b02f..11227becd58 100644 --- a/packages/app-lib/src/install/shared_instance.rs +++ b/packages/app-lib/src/install/shared_instance.rs @@ -714,7 +714,11 @@ async fn apply_shared_instance_content_inner( crate::launcher::install_minecraft_with_reporter( &context, false, - Some(InstallProgressReporter::new(job_id, job_state.clone())), + Some(InstallProgressReporter::new( + job_id, + job_state.clone(), + state.pool.clone(), + )), ) .await?; } diff --git a/packages/app-lib/src/install/store.rs b/packages/app-lib/src/install/store.rs index 62d3dec2a6e..ce5b32436e9 100644 --- a/packages/app-lib/src/install/store.rs +++ b/packages/app-lib/src/install/store.rs @@ -1,8 +1,8 @@ use super::model::{ InstallJobKind, InstallJobSnapshot, InstallJobState, InstallJobStatus, }; -use crate::state::State; use chrono::{DateTime, TimeZone, Utc}; +use sqlx::SqlitePool; use std::collections::HashMap; use std::sync::{Arc, LazyLock, Mutex, Weak}; use tokio::sync::Notify; @@ -110,7 +110,7 @@ pub async fn insert( id: Uuid, state: &InstallJobState, status: InstallJobStatus, - app_state: &State, + pool: &SqlitePool, ) -> crate::Result { let now = Utc::now(); let kind = state.request.kind(); @@ -137,10 +137,10 @@ pub async fn insert( created, modified, ) - .execute(&app_state.pool) + .execute(pool) .await?; - get(id, app_state).await?.ok_or_else(|| { + get(id, pool).await?.ok_or_else(|| { crate::ErrorKind::OtherError(format!( "Install job {id} was not inserted" )) @@ -150,7 +150,7 @@ pub async fn insert( pub async fn get( id: Uuid, - app_state: &State, + pool: &SqlitePool, ) -> crate::Result> { let id = id.to_string(); let row = sqlx::query_as!( @@ -171,7 +171,7 @@ pub async fn get( ", id, ) - .fetch_optional(&app_state.pool) + .fetch_optional(pool) .await?; row.map(row_to_record).transpose() @@ -179,9 +179,9 @@ pub async fn get( pub async fn list( include_finished: bool, - app_state: &State, + pool: &SqlitePool, ) -> crate::Result> { - Ok(deserialize_rows(list_rows(app_state).await?) + Ok(deserialize_rows(list_rows(pool).await?) .into_iter() .filter(|job| { job.needs_recovery() @@ -198,16 +198,16 @@ pub async fn list( } pub(crate) async fn list_all( - app_state: &State, + pool: &SqlitePool, ) -> crate::Result> { - list_rows(app_state) + list_rows(pool) .await? .into_iter() .map(row_to_record) .collect() } -async fn list_rows(app_state: &State) -> crate::Result> { +async fn list_rows(pool: &SqlitePool) -> crate::Result> { let rows = sqlx::query_as!( InstallJobRow, " @@ -225,7 +225,7 @@ async fn list_rows(app_state: &State) -> crate::Result> { ORDER BY created ASC " ) - .fetch_all(&app_state.pool) + .fetch_all(pool) .await?; Ok(rows) @@ -234,9 +234,9 @@ async fn list_rows(app_state: &State) -> crate::Result> { pub(crate) async fn ensure_no_pending_recovery( instance_id: &str, except_job: Option, - app_state: &State, + pool: &SqlitePool, ) -> crate::Result<()> { - if list_all(app_state).await?.iter().any(|job| { + if list_all(pool).await?.iter().any(|job| { job.instance_id.as_deref() == Some(instance_id) && Some(job.id) != except_job && job.needs_recovery() @@ -249,7 +249,7 @@ pub(crate) async fn ensure_no_pending_recovery( } pub async fn list_interrupted_candidates( - app_state: &State, + pool: &SqlitePool, ) -> crate::Result> { let rows = sqlx::query_as!( InstallJobRow, @@ -269,7 +269,7 @@ pub async fn list_interrupted_candidates( ORDER BY created ASC ", ) - .fetch_all(&app_state.pool) + .fetch_all(pool) .await?; Ok(deserialize_rows(rows)) @@ -277,9 +277,9 @@ pub async fn list_interrupted_candidates( pub async fn list_active_for_instance( instance_id: &str, - app_state: &State, + pool: &SqlitePool, ) -> crate::Result> { - Ok(list_interrupted_candidates(app_state) + Ok(list_interrupted_candidates(pool) .await? .into_iter() .filter(|job| job.instance_id.as_deref() == Some(instance_id)) @@ -289,7 +289,7 @@ pub async fn list_active_for_instance( pub async fn update_state( id: Uuid, state: &InstallJobState, - app_state: &State, + pool: &SqlitePool, ) -> crate::Result { let now = Utc::now(); let json = serde_json::to_string(state)?; @@ -311,7 +311,7 @@ pub async fn update_state( .bind(json) .bind(modified) .bind(id_value) - .execute(&app_state.pool) + .execute(pool) .await?; if result.rows_affected() == 0 { @@ -321,14 +321,14 @@ pub async fn update_state( .into()); } - get_required(id, app_state).await + get_required(id, pool).await } pub async fn update_status( id: Uuid, status: InstallJobStatus, state: &InstallJobState, - app_state: &State, + pool: &SqlitePool, ) -> crate::Result { let now = Utc::now(); let finished = status.is_finished().then_some(now.timestamp()); @@ -351,14 +351,14 @@ pub async fn update_status( finished, id_value, ) - .execute(&app_state.pool) + .execute(pool) .await?; if status.is_finished() { notify_completion(id); } - get_required(id, app_state).await + get_required(id, pool).await } pub async fn update_status_if( @@ -366,7 +366,7 @@ pub async fn update_status_if( expected_status: InstallJobStatus, status: InstallJobStatus, state: &InstallJobState, - app_state: &State, + pool: &SqlitePool, ) -> crate::Result> { let now = Utc::now(); let finished = status.is_finished().then_some(now.timestamp()); @@ -391,7 +391,7 @@ pub async fn update_status_if( .bind(finished) .bind(id_value) .bind(expected_status_value) - .execute(&app_state.pool) + .execute(pool) .await?; if result.rows_affected() == 0 { @@ -402,14 +402,14 @@ pub async fn update_status_if( notify_completion(id); } - get_required(id, app_state).await.map(Some) + get_required(id, pool).await.map(Some) } pub async fn finish_active( id: Uuid, status: InstallJobStatus, state: &InstallJobState, - app_state: &State, + pool: &SqlitePool, ) -> crate::Result> { let now = Utc::now(); let finished = now.timestamp(); @@ -437,7 +437,7 @@ pub async fn finish_active( .bind(modified) .bind(finished) .bind(id_value) - .execute(&app_state.pool) + .execute(pool) .await?; if result.rows_affected() == 0 { @@ -448,13 +448,13 @@ pub async fn finish_active( notify_completion(id); } - get_required(id, app_state).await.map(Some) + get_required(id, pool).await.map(Some) } pub async fn complete_success( id: Uuid, state: &InstallJobState, - app_state: &State, + pool: &SqlitePool, ) -> crate::Result> { let Some(instance_id) = instance_id(state) else { return Err(crate::ErrorKind::InputError( @@ -465,7 +465,7 @@ pub async fn complete_success( let now = Utc::now().timestamp(); let json = serde_json::to_string(state)?; let id_value = id.to_string(); - let mut transaction = app_state.pool.begin_with("BEGIN IMMEDIATE").await?; + let mut transaction = pool.begin_with("BEGIN IMMEDIATE").await?; let job_result = sqlx::query( " @@ -515,11 +515,11 @@ pub async fn complete_success( transaction.commit().await?; notify_completion(id); crate::api::instance::queue_game_locale_index(); - get_required(id, app_state).await.map(Some) + get_required(id, pool).await.map(Some) } -pub async fn dismiss(id: Uuid, app_state: &State) -> crate::Result<()> { - let job = get_required(id, app_state).await?; +pub async fn dismiss(id: Uuid, pool: &SqlitePool) -> crate::Result<()> { + let job = get_required(id, pool).await?; if job.instance_id.is_some() && job.needs_recovery() { return Err(crate::state::content_store::input( "Recover or delete this instance before dismissing its failed installation", @@ -536,7 +536,7 @@ pub async fn dismiss(id: Uuid, app_state: &State) -> crate::Result<()> { modified, id, ) - .execute(&app_state.pool) + .execute(pool) .await?; Ok(()) @@ -544,9 +544,9 @@ pub async fn dismiss(id: Uuid, app_state: &State) -> crate::Result<()> { pub async fn get_required( id: Uuid, - app_state: &State, + pool: &SqlitePool, ) -> crate::Result { - get(id, app_state).await?.ok_or_else(|| { + get(id, pool).await?.ok_or_else(|| { crate::ErrorKind::InputError(format!("Unknown install job {id}")).into() }) } diff --git a/packages/app-lib/src/launcher/mod.rs b/packages/app-lib/src/launcher/mod.rs index 03f67dfbd4a..2df5df0819d 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -1219,7 +1219,7 @@ pub async fn launch_minecraft( crate::install::store::ensure_no_pending_recovery( &instance.id, None, - &state, + &state.pool, ) .await?; let _store_lock = state.content_store.files_lock.lock().await; diff --git a/packages/app-lib/src/state/content_store/commands/cache.rs b/packages/app-lib/src/state/content_store/commands/cache.rs index e87ac39dc52..3d9d2330b9c 100644 --- a/packages/app-lib/src/state/content_store/commands/cache.rs +++ b/packages/app-lib/src/state/content_store/commands/cache.rs @@ -215,7 +215,7 @@ impl ContentStore { if self.any_instance_running(state).await? { return Ok(reclaimed); } - for job in crate::install::store::list(false, state).await? { + for job in crate::install::store::list(false, &state.pool).await? { if matches!( job.status, crate::install::InstallJobStatus::Queued From 7d5c866f7db3c9d0b856e93b1f122b2a95ece7f8 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:36:14 -0400 Subject: [PATCH 09/38] refactor(app-lib): pass state directly to install functions --- packages/app-lib/src/install/runner.rs | 6 +++++- packages/app-lib/src/install/shared_instance.rs | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/app-lib/src/install/runner.rs b/packages/app-lib/src/install/runner.rs index dc6d6074dfb..b453ee57792 100644 --- a/packages/app-lib/src/install/runner.rs +++ b/packages/app-lib/src/install/runner.rs @@ -1115,6 +1115,7 @@ async fn run_request_inner( location, instance_id.clone(), DownloadReason::Modpack, + state, ) .await?; apply_post_install_edit(&instance_id, post_install_edit).await?; @@ -1283,6 +1284,7 @@ async fn run_request_inner( location, instance_id.clone(), DownloadReason::Modpack, + state, ) .await?; restore_disabled_projects( @@ -1553,6 +1555,7 @@ pub(super) fn install_pack<'a>( location: CreatePackLocation, instance_id: String, reason: DownloadReason, + state: &'a State, ) -> impl Future> + Send + 'a { Box::pin(install_pack_inner( job_id, @@ -1560,6 +1563,7 @@ pub(super) fn install_pack<'a>( location, instance_id, reason, + state, )) } @@ -1569,8 +1573,8 @@ async fn install_pack_inner( location: CreatePackLocation, instance_id: String, reason: DownloadReason, + state: &State, ) -> crate::Result<()> { - let state = State::get().await?; let reporter = InstallProgressReporter::new( job_id, job_state.clone(), diff --git a/packages/app-lib/src/install/shared_instance.rs b/packages/app-lib/src/install/shared_instance.rs index 11227becd58..d0e19c8e10a 100644 --- a/packages/app-lib/src/install/shared_instance.rs +++ b/packages/app-lib/src/install/shared_instance.rs @@ -674,6 +674,7 @@ async fn apply_shared_instance_content_inner( location, instance_id.to_string(), DownloadReason::Modpack, + state, ) .await?; } else { From 033cb4cc2a7e143fc4f92dd5a6c04354f96893b8 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:37:33 -0400 Subject: [PATCH 10/38] refactor(app-lib): pass state to clear_staging_dir --- packages/app-lib/src/install/recovery.rs | 13 +++++++------ packages/app-lib/src/install/runner.rs | 6 +++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/app-lib/src/install/recovery.rs b/packages/app-lib/src/install/recovery.rs index 7ebe66c5e0a..e70a56cb280 100644 --- a/packages/app-lib/src/install/recovery.rs +++ b/packages/app-lib/src/install/recovery.rs @@ -210,7 +210,10 @@ async fn recover_unrecorded_instance_update_backup( Ok(()) } -pub(super) async fn clear_staging_dir(job_state: &InstallJobState) { +pub(super) async fn clear_staging_dir( + job_state: &InstallJobState, + state: &State, +) { let Some(staging_dir) = &job_state.paths.staging_dir else { return; }; @@ -223,9 +226,7 @@ pub(super) async fn clear_staging_dir(job_state: &InstallJobState) { ); return; } - if let Some(state) = State::get_if_initialized() - && let Some(owner) = - staging_dir.file_name().and_then(|name| name.to_str()) + if let Some(owner) = staging_dir.file_name().and_then(|name| name.to_str()) && let Err(error) = state.content_store.release("rollback", owner).await { tracing::warn!( @@ -583,7 +584,7 @@ async fn recover_interrupted_job_inner( .await? { store::dismiss(job.id, &state.pool).await?; - clear_staging_dir(&job.state).await; + clear_staging_dir(&job.state, state).await; emit_install_job(&record.snapshot()).await?; } @@ -642,7 +643,7 @@ async fn recover_interrupted_job_inner( .await? { if cleanup_succeeded { - clear_staging_dir(&job.state).await; + clear_staging_dir(&job.state, state).await; } emit_install_job(&record.snapshot()).await?; } diff --git a/packages/app-lib/src/install/runner.rs b/packages/app-lib/src/install/runner.rs index b453ee57792..56445729e78 100644 --- a/packages/app-lib/src/install/runner.rs +++ b/packages/app-lib/src/install/runner.rs @@ -264,7 +264,7 @@ async fn retry_job_inner(job_id: Uuid) -> crate::Result { job.state.paths.staging_dir = None; store::update_status(job_id, job.status, &job.state, &state.pool) .await?; - recovery::clear_staging_dir(&recovered).await; + recovery::clear_staging_dir(&recovered, &state).await; } drop(cleanup_target_guard); @@ -882,7 +882,7 @@ async fn run_job_inner( &instance_id, )).await; } - recovery::clear_staging_dir(&job_state).await; + recovery::clear_staging_dir(&job_state, &state).await; if let Err(error) = emit_instance(&instance_id, InstancePayloadType::Edited) .await @@ -1014,7 +1014,7 @@ async fn terminalize_failed_job_inner( .await? { if cleanup_succeeded { - recovery::clear_staging_dir(&job_state).await; + recovery::clear_staging_dir(&job_state, state).await; } emit_install_job(&record.snapshot()).await?; } From c91d9f22c547a2b8d4973197f6dd0ef3deb99277 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:39:55 -0400 Subject: [PATCH 11/38] refactor(app-lib): pass state to runner jobs --- packages/app-lib/src/install/runner.rs | 37 +++++++++++++++----------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/app-lib/src/install/runner.rs b/packages/app-lib/src/install/runner.rs index 56445729e78..1a98c25b5b3 100644 --- a/packages/app-lib/src/install/runner.rs +++ b/packages/app-lib/src/install/runner.rs @@ -361,7 +361,7 @@ async fn retry_job_inner(job_id: Uuid) -> crate::Result { } let registration = super::control::register(job_id); emit_install_job(&record.snapshot()).await?; - spawn_job(job_id, registration, target_guard); + spawn_job(job_id, registration, target_guard, Arc::clone(&state)); Ok(record.snapshot()) } @@ -548,7 +548,7 @@ async fn start_inner( } let registration = super::control::register(id); emit_install_job(&record.snapshot()).await?; - spawn_job(id, registration, target_guard); + spawn_job(id, registration, target_guard, Arc::clone(&state)); Ok(record.snapshot()) } @@ -749,13 +749,16 @@ fn spawn_job( job_id: Uuid, registration: super::control::Registration, target_guard: Option>, + state: Arc, ) { tokio::spawn(async move { let _target_guard = target_guard; - if let Err(error) = run_job(job_id, ®istration.control).await { + if let Err(error) = run_job(job_id, ®istration.control, &state).await + { let failure = error.to_string(); tracing::error!("Install job {job_id} terminated: {failure}"); - if let Err(error) = terminalize_stranded_job(job_id, failure).await + if let Err(error) = + terminalize_stranded_job(job_id, failure, &state).await { tracing::error!( "Failed to terminalize stranded install job {job_id}: {error}" @@ -768,15 +771,16 @@ fn spawn_job( fn run_job<'a>( job_id: Uuid, control: &'a std::sync::Arc, + state: &'a State, ) -> impl Future> + Send + 'a { - Box::pin(run_job_inner(job_id, control)) + Box::pin(run_job_inner(job_id, control, state)) } async fn run_job_inner( job_id: Uuid, control: &std::sync::Arc, + state: &State, ) -> crate::Result<()> { - let state = State::get().await?; let mut job = store::get_required(job_id, &state.pool).await?; if job.status != InstallJobStatus::Queued { @@ -817,7 +821,7 @@ async fn run_job_inner( super::control::CURRENT_INSTALL .scope( control.clone(), - run_request(job_id, &mut job_state, &state), + run_request(job_id, &mut job_state, state), ) .await } @@ -882,7 +886,7 @@ async fn run_job_inner( &instance_id, )).await; } - recovery::clear_staging_dir(&job_state, &state).await; + recovery::clear_staging_dir(&job_state, state).await; if let Err(error) = emit_instance(&instance_id, InstancePayloadType::Edited) .await @@ -909,7 +913,7 @@ async fn run_job_inner( job_state.context.clone(), ) }; - terminalize_failed_job(job_id, job_state, error_view, &state) + terminalize_failed_job(job_id, job_state, error_view, state) .await?; } } @@ -920,8 +924,8 @@ async fn run_job_inner( async fn terminalize_stranded_job( job_id: Uuid, message: String, + state: &State, ) -> crate::Result<()> { - let state = State::get().await?; let job = store::get_required(job_id, &state.pool).await?; if !matches!( job.status, @@ -935,7 +939,7 @@ async fn terminalize_stranded_job( job.state.progress.phase, message, ); - terminalize_failed_job(job_id, job.state, error_view, &state).await + terminalize_failed_job(job_id, job.state, error_view, state).await } fn terminalize_failed_job<'a>( @@ -1118,7 +1122,8 @@ async fn run_request_inner( state, ) .await?; - apply_post_install_edit(&instance_id, post_install_edit).await?; + apply_post_install_edit(&instance_id, post_install_edit, state) + .await?; Ok(Some(instance_id)) } InstallRequest::CreateSharedInstance { data } => { @@ -1194,7 +1199,6 @@ async fn run_request_inner( InstallPhaseDetails::Empty, ) .await?; - let state = State::get().await?; crate::api::pack::import::copy_dotminecraft_with_reporter( &instance_id, crate::api::instance::get_full_path(&source_instance_id) @@ -1293,7 +1297,8 @@ async fn run_request_inner( state, ) .await?; - apply_post_install_edit(&instance_id, post_install_edit).await?; + apply_post_install_edit(&instance_id, post_install_edit, state) + .await?; Ok(Some(instance_id)) } InstallRequest::BulkUpdateContent { @@ -1361,6 +1366,7 @@ async fn run_request_inner( async fn apply_post_install_edit( instance_id: &str, edit: Option, + state: &State, ) -> crate::Result<()> { let Some(mut edit) = edit else { return Ok(()); @@ -1373,8 +1379,7 @@ async fn apply_post_install_edit( if let Some(icon_path) = edit.icon_path.take() { let icon_path = match icon_path { Some(icon_path) => { - let state = State::get().await?; - resolve_icon_path(Some(&icon_path), false, &state).await? + resolve_icon_path(Some(&icon_path), false, state).await? } None => None, }; From 91b12f446e3e8f30c259034d4209410a5c9dee2c Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:52:15 -0400 Subject: [PATCH 12/38] refactor(app-lib): lift state access out of mc install functions --- .../app-lib/src/api/pack/import/atlauncher.rs | 10 ++----- .../app-lib/src/api/pack/import/curseforge.rs | 2 +- .../app-lib/src/api/pack/import/gdlauncher.rs | 2 +- packages/app-lib/src/api/pack/import/mmc.rs | 10 ++----- packages/app-lib/src/api/pack/import/mod.rs | 6 +++-- .../app-lib/src/api/pack/install_mrpack.rs | 1 + packages/app-lib/src/install/runner.rs | 3 +++ .../app-lib/src/install/shared_instance.rs | 1 + packages/app-lib/src/launcher/mod.rs | 27 ++++++++++--------- 9 files changed, 30 insertions(+), 32 deletions(-) diff --git a/packages/app-lib/src/api/pack/import/atlauncher.rs b/packages/app-lib/src/api/pack/import/atlauncher.rs index 1caf64f49ec..792441f503a 100644 --- a/packages/app-lib/src/api/pack/import/atlauncher.rs +++ b/packages/app-lib/src/api/pack/import/atlauncher.rs @@ -263,13 +263,7 @@ async fn import_atlauncher_unmanaged( // Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc) let state = State::get().await?; - finish_import( - instance_id, - minecraft_folder, - &state.io_semaphore, - reporter, - details, - ) - .await?; + finish_import(instance_id, minecraft_folder, &state, reporter, details) + .await?; Ok(()) } diff --git a/packages/app-lib/src/api/pack/import/curseforge.rs b/packages/app-lib/src/api/pack/import/curseforge.rs index e1f46302222..891d7977986 100644 --- a/packages/app-lib/src/api/pack/import/curseforge.rs +++ b/packages/app-lib/src/api/pack/import/curseforge.rs @@ -177,7 +177,7 @@ pub async fn import_curseforge( finish_import( instance_id, curseforge_instance_folder, - &state.io_semaphore, + &state, reporter, details, ) diff --git a/packages/app-lib/src/api/pack/import/gdlauncher.rs b/packages/app-lib/src/api/pack/import/gdlauncher.rs index 7a9f164242d..9e5bf8e2458 100644 --- a/packages/app-lib/src/api/pack/import/gdlauncher.rs +++ b/packages/app-lib/src/api/pack/import/gdlauncher.rs @@ -118,7 +118,7 @@ pub async fn import_gdlauncher( finish_import( instance_id, gdlauncher_instance_folder, - &state.io_semaphore, + &state, reporter, details, ) diff --git a/packages/app-lib/src/api/pack/import/mmc.rs b/packages/app-lib/src/api/pack/import/mmc.rs index 662971c3666..d8ef8b3f07b 100644 --- a/packages/app-lib/src/api/pack/import/mmc.rs +++ b/packages/app-lib/src/api/pack/import/mmc.rs @@ -326,13 +326,7 @@ async fn import_mmc_unmanaged( // Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc) let state = State::get().await?; - finish_import( - instance_id, - minecraft_folder, - &state.io_semaphore, - reporter, - details, - ) - .await?; + finish_import(instance_id, minecraft_folder, &state, reporter, details) + .await?; Ok(()) } diff --git a/packages/app-lib/src/api/pack/import/mod.rs b/packages/app-lib/src/api/pack/import/mod.rs index 357002ab478..19207e6be43 100644 --- a/packages/app-lib/src/api/pack/import/mod.rs +++ b/packages/app-lib/src/api/pack/import/mod.rs @@ -12,6 +12,7 @@ use io::IOError; use serde::{Deserialize, Serialize}; use crate::{ + State, install::{ InstallPhaseDetails, InstallPhaseId, InstallProgress, InstallProgressReporter, @@ -605,14 +606,14 @@ async fn copy_dotminecraft_with_reporter_inner( pub(crate) async fn finish_import( instance_id: &str, dotminecraft: PathBuf, - io_semaphore: &IoSemaphore, + state: &State, reporter: InstallProgressReporter, details: InstallPhaseDetails, ) -> crate::Result<()> { copy_dotminecraft_with_reporter( instance_id, dotminecraft, - io_semaphore, + &state.io_semaphore, reporter.clone(), details, ) @@ -622,6 +623,7 @@ pub(crate) async fn finish_import( instance_id, false, Some(reporter), + state, ) .await?; diff --git a/packages/app-lib/src/api/pack/install_mrpack.rs b/packages/app-lib/src/api/pack/install_mrpack.rs index f9be7f61291..85aef6be6d4 100644 --- a/packages/app-lib/src/api/pack/install_mrpack.rs +++ b/packages/app-lib/src/api/pack/install_mrpack.rs @@ -1286,6 +1286,7 @@ async fn install_zipped_mrpack_files_with_reporter_inner( &instance_id, false, Some(reporter.clone()), + state, ) .await?; reporter.clear_context().await?; diff --git a/packages/app-lib/src/install/runner.rs b/packages/app-lib/src/install/runner.rs index 1a98c25b5b3..87bc9701eed 100644 --- a/packages/app-lib/src/install/runner.rs +++ b/packages/app-lib/src/install/runner.rs @@ -1091,6 +1091,7 @@ async fn run_request_inner( job_state.clone(), state.pool.clone(), )), + state, ) .await?; Ok(Some(instance_id)) @@ -1229,6 +1230,7 @@ async fn run_request_inner( job_state.clone(), state.pool.clone(), )), + state, ) .await?; emit_instance(&instance_id, InstancePayloadType::Edited).await?; @@ -1262,6 +1264,7 @@ async fn run_request_inner( job_state.clone(), state.pool.clone(), )), + state, ) .await?; Ok(Some(instance_id)) diff --git a/packages/app-lib/src/install/shared_instance.rs b/packages/app-lib/src/install/shared_instance.rs index d0e19c8e10a..e2fa1ce44e6 100644 --- a/packages/app-lib/src/install/shared_instance.rs +++ b/packages/app-lib/src/install/shared_instance.rs @@ -720,6 +720,7 @@ async fn apply_shared_instance_content_inner( job_state.clone(), state.pool.clone(), )), + state, ) .await?; } diff --git a/packages/app-lib/src/launcher/mod.rs b/packages/app-lib/src/launcher/mod.rs index 2df5df0819d..cc0e29169c5 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -372,8 +372,10 @@ pub async fn resolve_minecraft_manifest( Ok((refreshed, idx)) } -async fn get_instance_full_path(instance_path: &str) -> crate::Result { - let state = State::get().await?; +fn get_instance_full_path( + instance_path: &str, + state: &State, +) -> crate::Result { let instances_dir = state.directories.instances_dir(); let full_path = io::canonicalize(instances_dir.join(instance_path))?; Ok(full_path) @@ -384,14 +386,16 @@ pub fn install_minecraft_with_reporter( context: &InstanceLaunchContext, repairing: bool, reporter: Option, -) -> impl Future> + Send + '_ { - Box::pin(install_minecraft_inner(context, repairing, reporter)) + state: &State, +) -> impl Future> + Send { + Box::pin(install_minecraft_inner(context, repairing, reporter, state)) } async fn install_minecraft_inner( context: &InstanceLaunchContext, repairing: bool, reporter: Option, + state: &State, ) -> crate::Result<()> { let instance = &context.instance; let content_set = &context.applied_content_set; @@ -416,7 +420,6 @@ async fn install_minecraft_inner( None }; - let state = State::get().await?; let _runtime_lease = state.content_store.runtime_cache_lock.read().await; let previous_install_stage = instance.install_stage; @@ -429,7 +432,7 @@ async fn install_minecraft_inner( emit_instance(&instance.id, InstancePayloadType::Edited).await?; let result = Box::pin(async { - let instance_path = get_instance_full_path(&instance.path).await?; + let instance_path = get_instance_full_path(&instance.path, state)?; if let Some(reporter) = &reporter { reporter .update( @@ -440,7 +443,7 @@ async fn install_minecraft_inner( .await?; } let (minecraft, version_index) = - resolve_minecraft_manifest(&content_set.game_version, &state).await?; + resolve_minecraft_manifest(&content_set.game_version, state).await?; let version = &minecraft.versions[version_index]; let minecraft_updated = version_index <= minecraft @@ -492,7 +495,7 @@ async fn install_minecraft_inner( // Download version info (5) let mut version_info = download::download_version_info( - &state, + state, version, loader_version.as_ref(), Some(repairing), @@ -573,7 +576,7 @@ async fn install_minecraft_inner( .await?; } download::download_minecraft( - &state, + state, &version_info, loading_bar.as_ref(), &java_version.architecture, @@ -795,8 +798,8 @@ pub async fn install_minecraft_for_instance_id_with_reporter( instance_id: &str, repairing: bool, reporter: Option, + state: &State, ) -> crate::Result<()> { - let state = State::get().await?; let context = crate::state::instances::commands::get_instance_launch_context( instance_id, @@ -809,7 +812,7 @@ pub async fn install_minecraft_for_instance_id_with_reporter( )) })?; - install_minecraft_with_reporter(&context, repairing, reporter).await + install_minecraft_with_reporter(&context, repairing, reporter, state).await } pub async fn read_protocol_version_from_jar( @@ -909,7 +912,7 @@ pub async fn launch_minecraft( let state = State::get().await?; let mut runtime_lease = state.content_store.runtime_cache_lock.read().await; - let instance_path = get_instance_full_path(&instance.path).await?; + let instance_path = get_instance_full_path(&instance.path, &state)?; let (minecraft, version_index) = resolve_minecraft_manifest(&content_set.game_version, &state).await?; From 438dac0ef5494e8a845bad589b450eb1dd01fa58 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:16:50 -0400 Subject: [PATCH 13/38] refactor(app-lib): lift state access out of mc launch functions --- packages/app-lib/src/api/instance/install.rs | 1 + packages/app-lib/src/api/instance/run.rs | 2 +- packages/app-lib/src/launcher/mod.rs | 17 +++++++++-------- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/app-lib/src/api/instance/install.rs b/packages/app-lib/src/api/instance/install.rs index ed308c0e9ab..0b67ef45ff3 100644 --- a/packages/app-lib/src/api/instance/install.rs +++ b/packages/app-lib/src/api/instance/install.rs @@ -41,6 +41,7 @@ pub async fn get_optimal_jre_key( crate::launcher::get_java_version_from_launch_context( &context, &version_info, + &state, ) .await } diff --git a/packages/app-lib/src/api/instance/run.rs b/packages/app-lib/src/api/instance/run.rs index 415af27a3d8..d83cb4318f9 100644 --- a/packages/app-lib/src/api/instance/run.rs +++ b/packages/app-lib/src/api/instance/run.rs @@ -162,7 +162,7 @@ async fn run_credentials( .as_ref() .expect("hooked launches always resolve their instance path"); let java_version = - crate::launcher::resolve_java_for_launch(&context).await?; + crate::launcher::resolve_java_for_launch(&context, &state).await?; Some(crate::launcher::hooks::HookEnvironment::from_current_env( &env_args, diff --git a/packages/app-lib/src/launcher/mod.rs b/packages/app-lib/src/launcher/mod.rs index cc0e29169c5..c4552088878 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -133,6 +133,7 @@ macro_rules! processor_rules { pub async fn get_java_version_from_launch_context( context: &InstanceLaunchContext, version_info: &VersionInfo, + state: &State, ) -> crate::Result> { if let Some(java) = context.launch_overrides.java_path.as_ref() { let java = @@ -148,8 +149,6 @@ pub async fn get_java_version_from_launch_context( .as_ref() .map_or(8, |it| it.major_version); - let state = State::get().await?; - let java_version = JavaVersion::get(key, &state.pool).await?; Ok(java_version) @@ -277,12 +276,12 @@ fn loader_versions_for_game_version<'a>( pub(crate) async fn resolve_java_for_launch( context: &InstanceLaunchContext, + state: &State, ) -> crate::Result { - let state = State::get().await?; let _runtime_lease = state.content_store.runtime_cache_lock.read().await; let content_set = &context.applied_content_set; let (minecraft, version_index) = - resolve_minecraft_manifest(&content_set.game_version, &state).await?; + resolve_minecraft_manifest(&content_set.game_version, state).await?; let version = &minecraft.versions[version_index]; let mut loader_version = get_loader_version_from_profile( @@ -302,7 +301,7 @@ pub(crate) async fn resolve_java_for_launch( } let version_info = download::download_version_info( - &state, + state, version, loader_version.as_ref(), None, @@ -316,7 +315,8 @@ pub(crate) async fn resolve_java_for_launch( .as_ref() .map_or(8, |it| it.major_version); let (java_path, set_java) = if let Some(java_version) = - get_java_version_from_launch_context(context, &version_info).await? + get_java_version_from_launch_context(context, &version_info, state) + .await? { (PathBuf::from(java_version.path), false) } else { @@ -525,7 +525,8 @@ async fn install_minecraft_inner( .await?; } let (java_version, set_java) = if let Some(java_version) = - get_java_version_from_launch_context(context, &version_info).await? + get_java_version_from_launch_context(context, &version_info, state) + .await? { (std::path::PathBuf::from(java_version.path), false) } else { @@ -977,7 +978,7 @@ pub async fn launch_minecraft( download_log_config(&state, &version_info, None, false, None).await?; let java_version = - get_java_version_from_launch_context(context, &version_info) + get_java_version_from_launch_context(context, &version_info, &state) .await? .ok_or_else(|| { crate::ErrorKind::LauncherError( From 7119e70f106c2c9834223c777fe7ccd5b8dc9fc5 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:21:22 -0400 Subject: [PATCH 14/38] refactor(app-lib): lift state access out of mc loader functions --- packages/app-lib/src/api/instance/install.rs | 1 + packages/app-lib/src/api/pack/import/atlauncher.rs | 3 ++- packages/app-lib/src/api/pack/import/curseforge.rs | 3 ++- packages/app-lib/src/api/pack/import/gdlauncher.rs | 3 ++- packages/app-lib/src/api/pack/import/mmc.rs | 3 ++- packages/app-lib/src/api/pack/install_from.rs | 2 ++ packages/app-lib/src/api/pack/install_mrpack.rs | 1 + packages/app-lib/src/api/worlds.rs | 1 + packages/app-lib/src/launcher/mod.rs | 7 ++++++- .../src/state/instances/commands/create_instance.rs | 1 + 10 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/app-lib/src/api/instance/install.rs b/packages/app-lib/src/api/instance/install.rs index 0b67ef45ff3..7a192ffc949 100644 --- a/packages/app-lib/src/api/instance/install.rs +++ b/packages/app-lib/src/api/instance/install.rs @@ -26,6 +26,7 @@ pub async fn get_optimal_jre_key( &context.applied_content_set.game_version, context.applied_content_set.loader, context.applied_content_set.loader_version.as_deref(), + &state, ) .await?; let version_info = crate::launcher::download::download_version_info( diff --git a/packages/app-lib/src/api/pack/import/atlauncher.rs b/packages/app-lib/src/api/pack/import/atlauncher.rs index 792441f503a..99d1c8482d3 100644 --- a/packages/app-lib/src/api/pack/import/atlauncher.rs +++ b/packages/app-lib/src/api/pack/import/atlauncher.rs @@ -211,12 +211,14 @@ async fn import_atlauncher_unmanaged( })?; let game_version = atinstance.id; + let state = State::get().await?; let loader_version = if mod_loader != ModLoader::Vanilla { crate::launcher::get_loader_version_from_profile( &game_version, mod_loader, Some(&atinstance.launcher.loader_version.version), + &state, ) .await? } else { @@ -262,7 +264,6 @@ async fn import_atlauncher_unmanaged( .await?; // Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc) - let state = State::get().await?; finish_import(instance_id, minecraft_folder, &state, reporter, details) .await?; Ok(()) diff --git a/packages/app-lib/src/api/pack/import/curseforge.rs b/packages/app-lib/src/api/pack/import/curseforge.rs index 891d7977986..3310af288a5 100644 --- a/packages/app-lib/src/api/pack/import/curseforge.rs +++ b/packages/app-lib/src/api/pack/import/curseforge.rs @@ -92,6 +92,7 @@ pub async fn import_curseforge( } // base mod loader is always None for vanilla + let state = State::get().await?; if let Some(instance_mod_loader) = minecraft_instance.base_mod_loader { let game_version = minecraft_instance.game_version; @@ -118,6 +119,7 @@ pub async fn import_curseforge( &game_version, mod_loader, loader_version.as_deref(), + &state, ) .await? } else { @@ -173,7 +175,6 @@ pub async fn import_curseforge( } // Copy in contained folders as overrides - let state = State::get().await?; finish_import( instance_id, curseforge_instance_folder, diff --git a/packages/app-lib/src/api/pack/import/gdlauncher.rs b/packages/app-lib/src/api/pack/import/gdlauncher.rs index 9e5bf8e2458..172040de229 100644 --- a/packages/app-lib/src/api/pack/import/gdlauncher.rs +++ b/packages/app-lib/src/api/pack/import/gdlauncher.rs @@ -77,12 +77,14 @@ pub async fn import_gdlauncher( let game_version = config.loader.mc_version; let mod_loader = config.loader.loader_type; let loader_version = config.loader.loader_version; + let state = State::get().await?; let loader_version = if mod_loader != ModLoader::Vanilla { crate::launcher::get_loader_version_from_profile( &game_version, mod_loader, loader_version.as_deref(), + &state, ) .await? } else { @@ -114,7 +116,6 @@ pub async fn import_gdlauncher( .await?; // Copy in contained folders as overrides - let state = State::get().await?; finish_import( instance_id, gdlauncher_instance_folder, diff --git a/packages/app-lib/src/api/pack/import/mmc.rs b/packages/app-lib/src/api/pack/import/mmc.rs index d8ef8b3f07b..fdc56379c95 100644 --- a/packages/app-lib/src/api/pack/import/mmc.rs +++ b/packages/app-lib/src/api/pack/import/mmc.rs @@ -314,6 +314,7 @@ async fn import_mmc_unmanaged( }) .collect(); + let state = State::get().await?; install_from::set_instance_information( instance_id.to_string(), &description, @@ -321,11 +322,11 @@ async fn import_mmc_unmanaged( None, &dependencies, false, + &state, ) .await?; // Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc) - let state = State::get().await?; finish_import(instance_id, minecraft_folder, &state, reporter, details) .await?; Ok(()) diff --git a/packages/app-lib/src/api/pack/install_from.rs b/packages/app-lib/src/api/pack/install_from.rs index 0dfa24941b4..647cf419079 100644 --- a/packages/app-lib/src/api/pack/install_from.rs +++ b/packages/app-lib/src/api/pack/install_from.rs @@ -532,6 +532,7 @@ pub async fn set_instance_information( pack_version_id: Option<&str>, dependencies: &HashMap, _ignore_lock: bool, + state: &State, ) -> crate::Result<()> { let mut game_version: Option<&String> = None; let mut mod_loader = None; @@ -572,6 +573,7 @@ pub async fn set_instance_information( game_version, mod_loader, loader_version.cloned().as_deref(), + state, ) .await? } else { diff --git a/packages/app-lib/src/api/pack/install_mrpack.rs b/packages/app-lib/src/api/pack/install_mrpack.rs index 85aef6be6d4..e3d990063f7 100644 --- a/packages/app-lib/src/api/pack/install_mrpack.rs +++ b/packages/app-lib/src/api/pack/install_mrpack.rs @@ -699,6 +699,7 @@ async fn install_zipped_mrpack_files_with_reporter_inner( Some(&pack.version_id), &pack.dependencies, ignore_lock, + state, ) .await?; diff --git a/packages/app-lib/src/api/worlds.rs b/packages/app-lib/src/api/worlds.rs index cdb4687da92..cb50ffb0227 100644 --- a/packages/app-lib/src/api/worlds.rs +++ b/packages/app-lib/src/api/worlds.rs @@ -932,6 +932,7 @@ pub async fn get_instance_protocol_version( &metadata.applied_content_set.game_version, metadata.applied_content_set.loader, metadata.applied_content_set.loader_version.as_deref(), + &state, ) .await?; if metadata.applied_content_set.loader != ModLoader::Vanilla diff --git a/packages/app-lib/src/launcher/mod.rs b/packages/app-lib/src/launcher/mod.rs index c4552088878..d3b7102a7a8 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -158,6 +158,7 @@ pub async fn get_loader_version_from_profile( game_version: &str, loader: ModLoader, loader_version: Option<&str>, + state: &State, ) -> crate::Result> { if loader == ModLoader::Vanilla { return Ok(None); @@ -196,7 +197,6 @@ pub async fn get_loader_version_from_profile( return Ok(None); } - let state = State::get().await?; Ok(installed_loader_version( &state.directories.versions_dir(), game_version, @@ -288,6 +288,7 @@ pub(crate) async fn resolve_java_for_launch( &content_set.game_version, content_set.loader, content_set.loader_version.as_deref(), + state, ) .await?; @@ -296,6 +297,7 @@ pub(crate) async fn resolve_java_for_launch( &content_set.game_version, content_set.loader, Some("stable"), + state, ) .await?; } @@ -468,6 +470,7 @@ async fn install_minecraft_inner( &content_set.game_version, content_set.loader, content_set.loader_version.as_deref(), + state, ) .await?; @@ -477,6 +480,7 @@ async fn install_minecraft_inner( &content_set.game_version, content_set.loader, Some("stable"), + state, ) .await?; @@ -929,6 +933,7 @@ pub async fn launch_minecraft( &content_set.game_version, content_set.loader, content_set.loader_version.as_deref(), + &state, ) .await?; diff --git a/packages/app-lib/src/state/instances/commands/create_instance.rs b/packages/app-lib/src/state/instances/commands/create_instance.rs index f239a1b6a7f..8a4efa62525 100644 --- a/packages/app-lib/src/state/instances/commands/create_instance.rs +++ b/packages/app-lib/src/state/instances/commands/create_instance.rs @@ -49,6 +49,7 @@ pub(crate) async fn create_instance( &input.game_version, input.loader, input.loader_version.as_deref(), + state, ) .await? .map(|value| value.id) From c943f91db92a3fd6b0408fa9731830d19d376f6b Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:26:28 -0400 Subject: [PATCH 15/38] refactor(app-lib): lift state access out of launch_mc --- packages/app-lib/src/api/instance/run.rs | 1 + packages/app-lib/src/launcher/mod.rs | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/app-lib/src/api/instance/run.rs b/packages/app-lib/src/api/instance/run.rs index d83cb4318f9..70cce60f784 100644 --- a/packages/app-lib/src/api/instance/run.rs +++ b/packages/app-lib/src/api/instance/run.rs @@ -303,6 +303,7 @@ async fn run_credentials( post_exit_hook, &context, quick_play_type, + &state, ) .await } diff --git a/packages/app-lib/src/launcher/mod.rs b/packages/app-lib/src/launcher/mod.rs index d3b7102a7a8..ad77bcf0050 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -894,6 +894,7 @@ pub async fn launch_minecraft( post_exit_hook: Option, context: &InstanceLaunchContext, mut quick_play_type: QuickPlayType, + state: &State, ) -> crate::Result { let instance = &context.instance; let content_set = &context.applied_content_set; @@ -914,13 +915,12 @@ pub async fn launch_minecraft( .into()); } - let state = State::get().await?; let mut runtime_lease = state.content_store.runtime_cache_lock.read().await; - let instance_path = get_instance_full_path(&instance.path, &state)?; + let instance_path = get_instance_full_path(&instance.path, state)?; let (minecraft, version_index) = - resolve_minecraft_manifest(&content_set.game_version, &state).await?; + resolve_minecraft_manifest(&content_set.game_version, state).await?; let version = &minecraft.versions[version_index]; let minecraft_updated = version_index <= minecraft @@ -933,7 +933,7 @@ pub async fn launch_minecraft( &content_set.game_version, content_set.loader, content_set.loader_version.as_deref(), - &state, + state, ) .await?; @@ -951,7 +951,7 @@ pub async fn launch_minecraft( }); let mut version_info = download::download_version_info( - &state, + state, version, loader_version.as_ref(), None, @@ -968,7 +968,7 @@ pub async fn launch_minecraft( .unwrap_or(0); if requires_logging_info { version_info = download::download_version_info( - &state, + state, version, loader_version.as_ref(), Some(true), @@ -980,10 +980,10 @@ pub async fn launch_minecraft( } let _ = - download_log_config(&state, &version_info, None, false, None).await?; + download_log_config(state, &version_info, None, false, None).await?; let java_version = - get_java_version_from_launch_context(context, &version_info, &state) + get_java_version_from_launch_context(context, &version_info, state) .await? .ok_or_else(|| { crate::ErrorKind::LauncherError( @@ -1033,7 +1033,7 @@ pub async fn launch_minecraft( )) .as_error()); } - if crate::state::instance_has_running_process(&instance.id, &state).await? { + if crate::state::instance_has_running_process(&instance.id, state).await? { return Err(crate::ErrorKind::LauncherError(format!( "Instance {} is already running", instance.id @@ -1042,7 +1042,7 @@ pub async fn launch_minecraft( } if let Some(path) = download::missing_runtime_file( - &state, + state, &version_info, &java_version.architecture, minecraft_updated, @@ -1209,7 +1209,7 @@ pub async fn launch_minecraft( ) .await?; - crate::state::instances::commands::sync_content_files(&instance.id, &state) + crate::state::instances::commands::sync_content_files(&instance.id, state) .await?; let _instance_content_lock = state.lock_instance_content(&instance.id).await; @@ -1235,7 +1235,7 @@ pub async fn launch_minecraft( let _store_lease = state.content_store.lease().await; state.content_store.recover(Some(&instance.id)).await?; // state.content_store.validate_instance(instance).await?; - if crate::state::instance_has_running_process(&instance.id, &state).await? { + if crate::state::instance_has_running_process(&instance.id, state).await? { return Err(crate::ErrorKind::LauncherError(format!( "Instance {} is already running", instance.id From 4c407214746960e0f13c16456c9c8989158c02ed Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:30:11 -0400 Subject: [PATCH 16/38] refactor(app-lib): lift state access out of wait for job --- packages/app-lib/src/install/runner.rs | 9 ++++++--- packages/app-lib/src/launcher/mod.rs | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/app-lib/src/install/runner.rs b/packages/app-lib/src/install/runner.rs index 87bc9701eed..47ef93d6426 100644 --- a/packages/app-lib/src/install/runner.rs +++ b/packages/app-lib/src/install/runner.rs @@ -28,6 +28,7 @@ use crate::state::{ ModLoader, State, }; use crate::util::fetch::DownloadReason; +use sqlx::SqlitePool; use std::collections::{HashMap, HashSet}; use std::future::Future; use std::path::PathBuf; @@ -161,15 +162,17 @@ pub async fn install_existing_instance( start(InstallRequest::InstallExistingInstance { instance_id, force }).await } -pub(crate) async fn wait_for_job(job_id: Uuid) -> crate::Result<()> { - let state = State::get().await?; +pub(crate) async fn wait_for_job( + job_id: Uuid, + pool: &SqlitePool, +) -> crate::Result<()> { let completion = store::completion_notification(job_id); loop { // Register before reading so completion during the query cannot be missed. let notified = completion.notified(); tokio::pin!(notified); notified.as_mut().enable(); - let job = store::get_required(job_id, &state.pool).await?; + let job = store::get_required(job_id, pool).await?; if job.status == InstallJobStatus::Succeeded { return Ok(()); } diff --git a/packages/app-lib/src/launcher/mod.rs b/packages/app-lib/src/launcher/mod.rs index ad77bcf0050..dfd1b846a73 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -1057,7 +1057,7 @@ pub async fn launch_minecraft( let job_id = uuid::Uuid::parse_str(&job.job_id).map_err(|error| { crate::ErrorKind::LauncherError(error.to_string()) })?; - crate::install::runner::wait_for_job(job_id).await?; + crate::install::runner::wait_for_job(job_id, &state.pool).await?; runtime_lease = state.content_store.runtime_cache_lock.read().await; } let _runtime_lease = runtime_lease; From 13f5efe1497fb7f190b0ab3239a56aff8a65f493 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:46:50 -0400 Subject: [PATCH 17/38] refactor(app-lib): lift state access out of state functions --- packages/app-lib/src/launcher/mod.rs | 4 ++- packages/app-lib/src/state/process.rs | 40 +++++++++++---------------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/packages/app-lib/src/launcher/mod.rs b/packages/app-lib/src/launcher/mod.rs index dfd1b846a73..cff2ced26f2 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -28,6 +28,7 @@ use daedalus::modded::{LoaderVersion, Manifest}; use serde::Deserialize; use std::future::Future; use std::path::{Path, PathBuf}; +use std::sync::Arc; use tokio::process::Command; mod args; @@ -894,7 +895,7 @@ pub async fn launch_minecraft( post_exit_hook: Option, context: &InstanceLaunchContext, mut quick_play_type: QuickPlayType, - state: &State, + state: &Arc, ) -> crate::Result { let instance = &context.instance; let content_set = &context.applied_content_set; @@ -1289,6 +1290,7 @@ pub async fn launch_minecraft( version_info.logging.is_some(), main_class_keep_alive, rpc_server, + state, async |process: &ProcessMetadata, rpc_server| { let process_start_time = process.start_time.to_rfc3339(); let instance_created_time = instance.created.to_rfc3339(); diff --git a/packages/app-lib/src/state/process.rs b/packages/app-lib/src/state/process.rs index a8fa26dc0a8..39a2eb3145d 100644 --- a/packages/app-lib/src/state/process.rs +++ b/packages/app-lib/src/state/process.rs @@ -1,3 +1,4 @@ +use crate::State; use crate::event::emit::{emit_instance, emit_process}; use crate::event::{InstancePayloadType, ProcessPayloadType}; #[cfg(feature = "tauri")] @@ -16,7 +17,7 @@ use std::fs::OpenOptions; use std::io::Write; use std::path::{Path, PathBuf}; use std::process::ExitStatus; -use std::sync::LazyLock; +use std::sync::{Arc, LazyLock}; use std::time::Instant; use tempfile::TempDir; use tokio::io::{AsyncBufReadExt, BufReader}; @@ -178,6 +179,7 @@ impl ProcessManager { xml_logging: bool, main_class_keep_alive: TempDir, rpc_server: RpcServer, + state: &Arc, post_process_init: impl AsyncFnOnce( &ProcessMetadata, &RpcServer, @@ -241,13 +243,6 @@ impl ProcessManager { _main_class_keep_alive: main_class_keep_alive, }; - let state = match crate::State::get().await { - Ok(state) => state, - Err(error) => { - let _ = process.child.kill().await; - return Err(error); - } - }; let persisted_process = child_pid.map(|pid| { (i64::from(pid), process.metadata.start_time.timestamp()) }); @@ -285,7 +280,7 @@ impl ProcessManager { post_process_init(&process.metadata, &process.rpc_server).await { tracing::error!("Failed to run post-process init: {e}"); - clear_persisted_process(&state, persisted_process).await; + clear_persisted_process(state, persisted_process).await; let _ = process.child.kill().await; return Err(e); } @@ -335,6 +330,7 @@ impl ProcessManager { post_exit_env_vars, metadata.uuid, persisted_process, + Arc::clone(state), )); emit_process( @@ -864,28 +860,19 @@ impl Process { post_exit_env_vars: Vec<(String, String)>, uuid: Uuid, persisted_process: Option<(i64, i64)>, + state: Arc, ) -> crate::Result<()> { async fn update_playtime( last_updated_playtime: &mut Instant, instance_id: &str, force_update: bool, + state: &State, ) { let elapsed = last_updated_playtime.elapsed().as_secs(); if elapsed == 0 || (!force_update && elapsed < 60) { return; } - let state = match crate::State::get().await { - Ok(state) => state, - Err(e) => { - tracing::warn!( - "Failed to get state for playtime update on instance {}: {}", - instance_id, - e - ); - return; - } - }; if let Err(e) = crate::state::instances::commands::add_instance_recent_playtime( instance_id, @@ -907,7 +894,6 @@ impl Process { let mc_exit_status; let mut last_updated_playtime = Instant::now(); - let state = crate::State::get().await?; loop { if let Some(process) = state.process_manager.try_wait(uuid)? { if let Some(t) = process { @@ -923,8 +909,13 @@ impl Process { tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; // Auto-update playtime every minute - update_playtime(&mut last_updated_playtime, &instance_id, false) - .await; + update_playtime( + &mut last_updated_playtime, + &instance_id, + false, + &state, + ) + .await; } state.process_manager.remove(uuid); @@ -951,7 +942,8 @@ impl Process { .await?; // Now fully complete- update playtime one last time - update_playtime(&mut last_updated_playtime, &instance_id, true).await; + update_playtime(&mut last_updated_playtime, &instance_id, true, &state) + .await; // Publish play time update // Allow failure, it will be stored locally and sent next time From 46d8d8760490307251a47539a9a7429ddf10d627 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:59:24 -0400 Subject: [PATCH 18/38] refactor(app-lib): lift state access out of logging --- packages/app-lib/src/state/process.rs | 41 +++++++++++++++++++++------ 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/packages/app-lib/src/state/process.rs b/packages/app-lib/src/state/process.rs index 39a2eb3145d..1db658d019e 100644 --- a/packages/app-lib/src/state/process.rs +++ b/packages/app-lib/src/state/process.rs @@ -11,6 +11,7 @@ use quick_xml::Reader; use quick_xml::events::Event; use serde::Deserialize; use serde::Serialize; +use sqlx::SqlitePool; use std::collections::VecDeque; use std::fmt::Debug; use std::fs::OpenOptions; @@ -292,6 +293,7 @@ impl ProcessManager { let instance_id = metadata.instance_id.clone(); let instance_path = metadata.instance_path.clone(); + let pool = state.pool.clone(); tokio::spawn(async move { Process::process_output( &instance_id, @@ -299,6 +301,7 @@ impl ProcessManager { stdout, log_path_clone, xml_logging, + &pool, ) .await; }); @@ -309,6 +312,7 @@ impl ProcessManager { let instance_id = metadata.instance_id.clone(); let instance_path = metadata.instance_path.clone(); + let pool = state.pool.clone(); tokio::spawn(async move { Process::process_output( &instance_id, @@ -316,6 +320,7 @@ impl ProcessManager { stderr, log_path_clone, xml_logging, + &pool, ) .await; }); @@ -428,6 +433,7 @@ impl Process { reader: R, log_path: impl AsRef, xml_logging: bool, + pool: &SqlitePool, ) where R: tokio::io::AsyncRead + Unpin, { @@ -588,6 +594,7 @@ impl Process { instance_id, ×tamp, message, + pool, ).await { tracing::error!("Failed to handle server join logging: {e}"); } @@ -652,6 +659,7 @@ impl Process { if let Err(e) = Self::maybe_handle_old_server_join_logging( instance_id, line.trim_ascii_end(), + pool, ) .await { @@ -765,6 +773,7 @@ impl Process { instance_id: &str, timestamp: &str, message: &str, + pool: &SqlitePool, ) -> crate::Result<()> { let timestamp = timestamp .parse::() @@ -781,13 +790,19 @@ impl Process { ) }) })?; - Self::parse_and_insert_server_join(instance_id, message, timestamp) - .await + Self::parse_and_insert_server_join( + instance_id, + message, + timestamp, + pool, + ) + .await } async fn maybe_handle_old_server_join_logging( instance_id: &str, line: &str, + pool: &SqlitePool, ) -> crate::Result<()> { if let Some((timestamp, message)) = line.split_once(" [CLIENT] [INFO] ") { @@ -797,11 +812,21 @@ impl Process { .map(|x| x.to_utc()) .single() .unwrap_or_else(Utc::now); - Self::parse_and_insert_server_join(instance_id, message, timestamp) - .await + Self::parse_and_insert_server_join( + instance_id, + message, + timestamp, + pool, + ) + .await } else { - Self::parse_and_insert_server_join(instance_id, line, Utc::now()) - .await + Self::parse_and_insert_server_join( + instance_id, + line, + Utc::now(), + pool, + ) + .await } } @@ -809,6 +834,7 @@ impl Process { instance_id: &str, message: &str, timestamp: DateTime, + pool: &SqlitePool, ) -> crate::Result<()> { let Some(host_port_string) = message.strip_prefix("Connecting to ") else { @@ -822,14 +848,13 @@ impl Process { return Ok(()); }; - let state = crate::State::get().await?; crate::state::server_join_log::JoinLogEntry { instance_id: instance_id.to_owned(), host: host.to_string(), port, join_time: timestamp, } - .upsert(&state.pool) + .upsert(pool) .await?; { let instance_id = instance_id.to_owned(); From 4e793921b41bbc3146861d26fa842443d71c9ac0 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:26:13 -0400 Subject: [PATCH 19/38] refactor(app-lib): only pass pool to get java version --- packages/app-lib/src/api/instance/install.rs | 2 +- packages/app-lib/src/launcher/mod.rs | 40 +++++++++++++------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/packages/app-lib/src/api/instance/install.rs b/packages/app-lib/src/api/instance/install.rs index 7a192ffc949..932606781db 100644 --- a/packages/app-lib/src/api/instance/install.rs +++ b/packages/app-lib/src/api/instance/install.rs @@ -42,7 +42,7 @@ pub async fn get_optimal_jre_key( crate::launcher::get_java_version_from_launch_context( &context, &version_info, - &state, + &state.pool, ) .await } diff --git a/packages/app-lib/src/launcher/mod.rs b/packages/app-lib/src/launcher/mod.rs index cff2ced26f2..7321bdcd08a 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -26,6 +26,7 @@ use daedalus as d; use daedalus::minecraft::{LoggingSide, RuleAction, VersionInfo}; use daedalus::modded::{LoaderVersion, Manifest}; use serde::Deserialize; +use sqlx::SqlitePool; use std::future::Future; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -134,7 +135,7 @@ macro_rules! processor_rules { pub async fn get_java_version_from_launch_context( context: &InstanceLaunchContext, version_info: &VersionInfo, - state: &State, + pool: &SqlitePool, ) -> crate::Result> { if let Some(java) = context.launch_overrides.java_path.as_ref() { let java = @@ -150,7 +151,7 @@ pub async fn get_java_version_from_launch_context( .as_ref() .map_or(8, |it| it.major_version); - let java_version = JavaVersion::get(key, &state.pool).await?; + let java_version = JavaVersion::get(key, pool).await?; Ok(java_version) } @@ -318,8 +319,12 @@ pub(crate) async fn resolve_java_for_launch( .as_ref() .map_or(8, |it| it.major_version); let (java_path, set_java) = if let Some(java_version) = - get_java_version_from_launch_context(context, &version_info, state) - .await? + get_java_version_from_launch_context( + context, + &version_info, + &state.pool, + ) + .await? { (PathBuf::from(java_version.path), false) } else { @@ -530,8 +535,12 @@ async fn install_minecraft_inner( .await?; } let (java_version, set_java) = if let Some(java_version) = - get_java_version_from_launch_context(context, &version_info, state) - .await? + get_java_version_from_launch_context( + context, + &version_info, + &state.pool, + ) + .await? { (std::path::PathBuf::from(java_version.path), false) } else { @@ -983,14 +992,17 @@ pub async fn launch_minecraft( let _ = download_log_config(state, &version_info, None, false, None).await?; - let java_version = - get_java_version_from_launch_context(context, &version_info, state) - .await? - .ok_or_else(|| { - crate::ErrorKind::LauncherError( - "Missing correct java installation".to_string(), - ) - })?; + let java_version = get_java_version_from_launch_context( + context, + &version_info, + &state.pool, + ) + .await? + .ok_or_else(|| { + crate::ErrorKind::LauncherError( + "Missing correct java installation".to_string(), + ) + })?; // Test jre version let java_version = From acb28bb7ed4b94be7304f5d21b10e28b26cd5562 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:29:24 -0400 Subject: [PATCH 20/38] refactor(app-lib): only pass directory info to get loader --- packages/app-lib/src/api/instance/install.rs | 2 +- .../app-lib/src/api/pack/import/atlauncher.rs | 2 +- .../app-lib/src/api/pack/import/curseforge.rs | 2 +- .../app-lib/src/api/pack/import/gdlauncher.rs | 2 +- packages/app-lib/src/api/pack/import/mmc.rs | 2 +- packages/app-lib/src/api/pack/install_from.rs | 6 +++--- .../app-lib/src/api/pack/install_mrpack.rs | 2 +- packages/app-lib/src/api/worlds.rs | 2 +- packages/app-lib/src/launcher/mod.rs | 18 +++++++++--------- .../instances/commands/create_instance.rs | 2 +- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/app-lib/src/api/instance/install.rs b/packages/app-lib/src/api/instance/install.rs index 932606781db..9d87fccc217 100644 --- a/packages/app-lib/src/api/instance/install.rs +++ b/packages/app-lib/src/api/instance/install.rs @@ -26,7 +26,7 @@ pub async fn get_optimal_jre_key( &context.applied_content_set.game_version, context.applied_content_set.loader, context.applied_content_set.loader_version.as_deref(), - &state, + &state.directories, ) .await?; let version_info = crate::launcher::download::download_version_info( diff --git a/packages/app-lib/src/api/pack/import/atlauncher.rs b/packages/app-lib/src/api/pack/import/atlauncher.rs index 99d1c8482d3..438a2c10391 100644 --- a/packages/app-lib/src/api/pack/import/atlauncher.rs +++ b/packages/app-lib/src/api/pack/import/atlauncher.rs @@ -218,7 +218,7 @@ async fn import_atlauncher_unmanaged( &game_version, mod_loader, Some(&atinstance.launcher.loader_version.version), - &state, + &state.directories, ) .await? } else { diff --git a/packages/app-lib/src/api/pack/import/curseforge.rs b/packages/app-lib/src/api/pack/import/curseforge.rs index 3310af288a5..b6f055a4021 100644 --- a/packages/app-lib/src/api/pack/import/curseforge.rs +++ b/packages/app-lib/src/api/pack/import/curseforge.rs @@ -119,7 +119,7 @@ pub async fn import_curseforge( &game_version, mod_loader, loader_version.as_deref(), - &state, + &state.directories, ) .await? } else { diff --git a/packages/app-lib/src/api/pack/import/gdlauncher.rs b/packages/app-lib/src/api/pack/import/gdlauncher.rs index 172040de229..11cd23d0a26 100644 --- a/packages/app-lib/src/api/pack/import/gdlauncher.rs +++ b/packages/app-lib/src/api/pack/import/gdlauncher.rs @@ -84,7 +84,7 @@ pub async fn import_gdlauncher( &game_version, mod_loader, loader_version.as_deref(), - &state, + &state.directories, ) .await? } else { diff --git a/packages/app-lib/src/api/pack/import/mmc.rs b/packages/app-lib/src/api/pack/import/mmc.rs index fdc56379c95..6e8194c8df5 100644 --- a/packages/app-lib/src/api/pack/import/mmc.rs +++ b/packages/app-lib/src/api/pack/import/mmc.rs @@ -322,7 +322,7 @@ async fn import_mmc_unmanaged( None, &dependencies, false, - &state, + &state.directories, ) .await?; diff --git a/packages/app-lib/src/api/pack/install_from.rs b/packages/app-lib/src/api/pack/install_from.rs index 647cf419079..fdae5e126b4 100644 --- a/packages/app-lib/src/api/pack/install_from.rs +++ b/packages/app-lib/src/api/pack/install_from.rs @@ -8,7 +8,7 @@ use crate::install::{ }; use crate::state::{ AppliedContentSetPatch, CacheBehaviour, CachedEntry, ContentSourceKind, - EditInstance, InstanceInstallStage, InstanceLink, SideType, + DirectoryInfo, EditInstance, InstanceInstallStage, InstanceLink, SideType, }; use crate::util::fetch::{ DownloadMeta, DownloadReason, FetchProgressFn, fetch, @@ -532,7 +532,7 @@ pub async fn set_instance_information( pack_version_id: Option<&str>, dependencies: &HashMap, _ignore_lock: bool, - state: &State, + directories: &DirectoryInfo, ) -> crate::Result<()> { let mut game_version: Option<&String> = None; let mut mod_loader = None; @@ -573,7 +573,7 @@ pub async fn set_instance_information( game_version, mod_loader, loader_version.cloned().as_deref(), - state, + directories, ) .await? } else { diff --git a/packages/app-lib/src/api/pack/install_mrpack.rs b/packages/app-lib/src/api/pack/install_mrpack.rs index e3d990063f7..61fe0dfbc0e 100644 --- a/packages/app-lib/src/api/pack/install_mrpack.rs +++ b/packages/app-lib/src/api/pack/install_mrpack.rs @@ -699,7 +699,7 @@ async fn install_zipped_mrpack_files_with_reporter_inner( Some(&pack.version_id), &pack.dependencies, ignore_lock, - state, + &state.directories, ) .await?; diff --git a/packages/app-lib/src/api/worlds.rs b/packages/app-lib/src/api/worlds.rs index cb50ffb0227..e6eaaae1db1 100644 --- a/packages/app-lib/src/api/worlds.rs +++ b/packages/app-lib/src/api/worlds.rs @@ -932,7 +932,7 @@ pub async fn get_instance_protocol_version( &metadata.applied_content_set.game_version, metadata.applied_content_set.loader, metadata.applied_content_set.loader_version.as_deref(), - &state, + &state.directories, ) .await?; if metadata.applied_content_set.loader != ModLoader::Vanilla diff --git a/packages/app-lib/src/launcher/mod.rs b/packages/app-lib/src/launcher/mod.rs index 7321bdcd08a..0a13423f25c 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -15,8 +15,8 @@ use crate::launcher::quick_play_version::{ use crate::server_address::{ServerAddress, parse_server_address}; use crate::state::server_join_log::JoinLogEntry; use crate::state::{ - Credentials, InstanceInstallStage, InstanceLaunchContext, InstanceLink, - JavaVersion, MemorySettings, ProcessMetadata, WindowSize, + Credentials, DirectoryInfo, InstanceInstallStage, InstanceLaunchContext, + InstanceLink, JavaVersion, MemorySettings, ProcessMetadata, WindowSize, }; use crate::util::io; use crate::util::rpc::RpcServerBuilder; @@ -160,7 +160,7 @@ pub async fn get_loader_version_from_profile( game_version: &str, loader: ModLoader, loader_version: Option<&str>, - state: &State, + directories: &DirectoryInfo, ) -> crate::Result> { if loader == ModLoader::Vanilla { return Ok(None); @@ -200,7 +200,7 @@ pub async fn get_loader_version_from_profile( } Ok(installed_loader_version( - &state.directories.versions_dir(), + &directories.versions_dir(), game_version, version, )) @@ -290,7 +290,7 @@ pub(crate) async fn resolve_java_for_launch( &content_set.game_version, content_set.loader, content_set.loader_version.as_deref(), - state, + &state.directories, ) .await?; @@ -299,7 +299,7 @@ pub(crate) async fn resolve_java_for_launch( &content_set.game_version, content_set.loader, Some("stable"), - state, + &state.directories, ) .await?; } @@ -476,7 +476,7 @@ async fn install_minecraft_inner( &content_set.game_version, content_set.loader, content_set.loader_version.as_deref(), - state, + &state.directories, ) .await?; @@ -486,7 +486,7 @@ async fn install_minecraft_inner( &content_set.game_version, content_set.loader, Some("stable"), - state, + &state.directories, ) .await?; @@ -943,7 +943,7 @@ pub async fn launch_minecraft( &content_set.game_version, content_set.loader, content_set.loader_version.as_deref(), - state, + &state.directories, ) .await?; diff --git a/packages/app-lib/src/state/instances/commands/create_instance.rs b/packages/app-lib/src/state/instances/commands/create_instance.rs index 8a4efa62525..b95f4f53236 100644 --- a/packages/app-lib/src/state/instances/commands/create_instance.rs +++ b/packages/app-lib/src/state/instances/commands/create_instance.rs @@ -49,7 +49,7 @@ pub(crate) async fn create_instance( &input.game_version, input.loader, input.loader_version.as_deref(), - state, + &state.directories, ) .await? .map(|value| value.id) From b1fbdc5731e22c56806a59bf48be83b1aed13dd8 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:31:09 -0400 Subject: [PATCH 21/38] refactor(app-lib): only pass directory info to get instance full path --- packages/app-lib/src/launcher/mod.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/app-lib/src/launcher/mod.rs b/packages/app-lib/src/launcher/mod.rs index 0a13423f25c..fed2b283831 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -382,9 +382,9 @@ pub async fn resolve_minecraft_manifest( fn get_instance_full_path( instance_path: &str, - state: &State, + directories: &DirectoryInfo, ) -> crate::Result { - let instances_dir = state.directories.instances_dir(); + let instances_dir = directories.instances_dir(); let full_path = io::canonicalize(instances_dir.join(instance_path))?; Ok(full_path) } @@ -440,7 +440,8 @@ async fn install_minecraft_inner( emit_instance(&instance.id, InstancePayloadType::Edited).await?; let result = Box::pin(async { - let instance_path = get_instance_full_path(&instance.path, state)?; + let instance_path = + get_instance_full_path(&instance.path, &state.directories)?; if let Some(reporter) = &reporter { reporter .update( @@ -927,7 +928,8 @@ pub async fn launch_minecraft( let mut runtime_lease = state.content_store.runtime_cache_lock.read().await; - let instance_path = get_instance_full_path(&instance.path, state)?; + let instance_path = + get_instance_full_path(&instance.path, &state.directories)?; let (minecraft, version_index) = resolve_minecraft_manifest(&content_set.game_version, state).await?; From 90ec70cebc387d7df124b2ec82fc636ec59d73d6 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:32:53 -0400 Subject: [PATCH 22/38] refactor(app-lib): only pass pool to install_pack --- packages/app-lib/src/install/runner.rs | 17 +++++++---------- packages/app-lib/src/install/shared_instance.rs | 2 +- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/app-lib/src/install/runner.rs b/packages/app-lib/src/install/runner.rs index 47ef93d6426..8e574e4a3b5 100644 --- a/packages/app-lib/src/install/runner.rs +++ b/packages/app-lib/src/install/runner.rs @@ -1123,7 +1123,7 @@ async fn run_request_inner( location, instance_id.clone(), DownloadReason::Modpack, - state, + &state.pool, ) .await?; apply_post_install_edit(&instance_id, post_install_edit, state) @@ -1294,7 +1294,7 @@ async fn run_request_inner( location, instance_id.clone(), DownloadReason::Modpack, - state, + &state.pool, ) .await?; restore_disabled_projects( @@ -1566,7 +1566,7 @@ pub(super) fn install_pack<'a>( location: CreatePackLocation, instance_id: String, reason: DownloadReason, - state: &'a State, + pool: &'a SqlitePool, ) -> impl Future> + Send + 'a { Box::pin(install_pack_inner( job_id, @@ -1574,7 +1574,7 @@ pub(super) fn install_pack<'a>( location, instance_id, reason, - state, + pool, )) } @@ -1584,13 +1584,10 @@ async fn install_pack_inner( location: CreatePackLocation, instance_id: String, reason: DownloadReason, - state: &State, + pool: &SqlitePool, ) -> crate::Result<()> { - let reporter = InstallProgressReporter::new( - job_id, - job_state.clone(), - state.pool.clone(), - ); + let reporter = + InstallProgressReporter::new(job_id, job_state.clone(), pool.clone()); reporter .update( InstallPhaseId::DownloadingPackFile, diff --git a/packages/app-lib/src/install/shared_instance.rs b/packages/app-lib/src/install/shared_instance.rs index e2fa1ce44e6..1b9574fd4ce 100644 --- a/packages/app-lib/src/install/shared_instance.rs +++ b/packages/app-lib/src/install/shared_instance.rs @@ -674,7 +674,7 @@ async fn apply_shared_instance_content_inner( location, instance_id.to_string(), DownloadReason::Modpack, - state, + &state.pool, ) .await?; } else { From 31375abcc902adb7dda7ab83a99e3546b4012b40 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:35:03 -0400 Subject: [PATCH 23/38] refactor(app-lib): only pass content store to clear_staging_dir --- packages/app-lib/src/install/recovery.rs | 9 +++++---- packages/app-lib/src/install/runner.rs | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/app-lib/src/install/recovery.rs b/packages/app-lib/src/install/recovery.rs index e70a56cb280..6c64efd52ab 100644 --- a/packages/app-lib/src/install/recovery.rs +++ b/packages/app-lib/src/install/recovery.rs @@ -7,6 +7,7 @@ use super::model::{ use super::store; use crate::event::InstancePayloadType; use crate::event::emit::emit_instance; +use crate::state::content_store::ContentStore; use crate::state::instances::adapters::sqlite::{content_rows, instance_rows}; use crate::state::{ ContentEntry, ContentSetRemoteRef, ContentSetRemoteRefType, @@ -212,7 +213,7 @@ async fn recover_unrecorded_instance_update_backup( pub(super) async fn clear_staging_dir( job_state: &InstallJobState, - state: &State, + content_store: &ContentStore, ) { let Some(staging_dir) = &job_state.paths.staging_dir else { return; @@ -227,7 +228,7 @@ pub(super) async fn clear_staging_dir( return; } if let Some(owner) = staging_dir.file_name().and_then(|name| name.to_str()) - && let Err(error) = state.content_store.release("rollback", owner).await + && let Err(error) = content_store.release("rollback", owner).await { tracing::warn!( "Could not release rollback content references: {error}" @@ -584,7 +585,7 @@ async fn recover_interrupted_job_inner( .await? { store::dismiss(job.id, &state.pool).await?; - clear_staging_dir(&job.state, state).await; + clear_staging_dir(&job.state, &state.content_store).await; emit_install_job(&record.snapshot()).await?; } @@ -643,7 +644,7 @@ async fn recover_interrupted_job_inner( .await? { if cleanup_succeeded { - clear_staging_dir(&job.state, state).await; + clear_staging_dir(&job.state, &state.content_store).await; } emit_install_job(&record.snapshot()).await?; } diff --git a/packages/app-lib/src/install/runner.rs b/packages/app-lib/src/install/runner.rs index 8e574e4a3b5..55cb14f4aca 100644 --- a/packages/app-lib/src/install/runner.rs +++ b/packages/app-lib/src/install/runner.rs @@ -267,7 +267,7 @@ async fn retry_job_inner(job_id: Uuid) -> crate::Result { job.state.paths.staging_dir = None; store::update_status(job_id, job.status, &job.state, &state.pool) .await?; - recovery::clear_staging_dir(&recovered, &state).await; + recovery::clear_staging_dir(&recovered, &state.content_store).await; } drop(cleanup_target_guard); @@ -889,7 +889,8 @@ async fn run_job_inner( &instance_id, )).await; } - recovery::clear_staging_dir(&job_state, state).await; + recovery::clear_staging_dir(&job_state, &state.content_store) + .await; if let Err(error) = emit_instance(&instance_id, InstancePayloadType::Edited) .await @@ -1021,7 +1022,7 @@ async fn terminalize_failed_job_inner( .await? { if cleanup_succeeded { - recovery::clear_staging_dir(&job_state, state).await; + recovery::clear_staging_dir(&job_state, &state.content_store).await; } emit_install_job(&record.snapshot()).await?; } From ee39d356aba3dbe8aac4362232878304e8993eff Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:36:39 -0400 Subject: [PATCH 24/38] refactor(app-lib): only pass pool to update_playtime --- packages/app-lib/src/state/process.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/app-lib/src/state/process.rs b/packages/app-lib/src/state/process.rs index 1db658d019e..e872772a42a 100644 --- a/packages/app-lib/src/state/process.rs +++ b/packages/app-lib/src/state/process.rs @@ -891,7 +891,7 @@ impl Process { last_updated_playtime: &mut Instant, instance_id: &str, force_update: bool, - state: &State, + pool: &SqlitePool, ) { let elapsed = last_updated_playtime.elapsed().as_secs(); if elapsed == 0 || (!force_update && elapsed < 60) { @@ -902,7 +902,7 @@ impl Process { crate::state::instances::commands::add_instance_recent_playtime( instance_id, elapsed, - &state.pool, + pool, ) .await { @@ -938,7 +938,7 @@ impl Process { &mut last_updated_playtime, &instance_id, false, - &state, + &state.pool, ) .await; } @@ -967,8 +967,13 @@ impl Process { .await?; // Now fully complete- update playtime one last time - update_playtime(&mut last_updated_playtime, &instance_id, true, &state) - .await; + update_playtime( + &mut last_updated_playtime, + &instance_id, + true, + &state.pool, + ) + .await; // Publish play time update // Allow failure, it will be stored locally and sent next time From f771fb5e03012c3fc93fad59293c9bfe35aa01ab Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:40:07 -0400 Subject: [PATCH 25/38] refactor(app-lib): only pass pool to watcher --- packages/app-lib/src/state/instances/watcher.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/app-lib/src/state/instances/watcher.rs b/packages/app-lib/src/state/instances/watcher.rs index 89046788295..b56c8cfba0b 100644 --- a/packages/app-lib/src/state/instances/watcher.rs +++ b/packages/app-lib/src/state/instances/watcher.rs @@ -9,6 +9,7 @@ use crate::worlds::WorldType; use dashmap::{DashMap, mapref::entry::Entry}; use notify::{RecommendedWatcher, RecursiveMode}; use notify_debouncer_mini::{DebounceEventResult, Debouncer, new_debouncer}; +use sqlx::SqlitePool; use std::sync::LazyLock; use std::{ collections::{HashMap, HashSet}, @@ -196,7 +197,7 @@ pub async fn init_watcher() -> crate::Result { .as_ref() .is_some_and(|x| *x == "txt") { - crash_task(instance_id, Arc::clone(&state)); + crash_task(instance_id, state.pool.clone()); } else if (is_screenshot_event && !visited_screenshot_instances .contains(&instance_id)) @@ -242,13 +243,13 @@ pub async fn init_watcher() -> crate::Result { if !e.path.is_file() { let instance_id = instance_id.clone(); let world = world.clone(); - let state = Arc::clone(&state); + let pool = state.pool.clone(); tokio::spawn(async move { if let Err(e) = attached_world_data::AttachedWorldData::remove_for_world( &instance_id, WorldType::Singleplayer, &world, - &state.pool + &pool ).await { tracing::warn!("Failed to remove AttachedWorldData for '{world}': {e}") } @@ -446,12 +447,11 @@ pub(crate) async fn watch_instance_folder( .insert(instance_path.to_string(), instance_id.to_string()); } -fn crash_task(instance_id: String, state: Arc) { +fn crash_task(instance_id: String, pool: SqlitePool) { tokio::task::spawn(async move { let res = async { let Some(instance) = - instance_rows::get_instance_by_id(&instance_id, &state.pool) - .await? + instance_rows::get_instance_by_id(&instance_id, &pool).await? else { return Ok(()); }; From cf7d1b74c898aa2483fc0f0f167551f0be46958c Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:52:06 -0400 Subject: [PATCH 26/38] style(app-lib): fmt --- packages/app-lib/src/util/jre.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/app-lib/src/util/jre.rs b/packages/app-lib/src/util/jre.rs index 6ad51be1f0f..68a1beca1c0 100644 --- a/packages/app-lib/src/util/jre.rs +++ b/packages/app-lib/src/util/jre.rs @@ -189,9 +189,7 @@ pub async fn get_all_jre( // Gets all JREs from the PATH env variable #[tracing::instrument] -fn get_all_autoinstalled_jre_path( - base_path: &Path, -) -> HashSet { +fn get_all_autoinstalled_jre_path(base_path: &Path) -> HashSet { let mut jre_paths = HashSet::new(); if base_path.is_dir() From affccce5b731dddd93cec0f2f572703b7df895aa Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:25:30 -0400 Subject: [PATCH 27/38] refactor(app-lib): remove extra state get --- packages/app-lib/src/api/pack/import/curseforge.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/app-lib/src/api/pack/import/curseforge.rs b/packages/app-lib/src/api/pack/import/curseforge.rs index b6f055a4021..6c2a54fc2af 100644 --- a/packages/app-lib/src/api/pack/import/curseforge.rs +++ b/packages/app-lib/src/api/pack/import/curseforge.rs @@ -92,7 +92,6 @@ pub async fn import_curseforge( } // base mod loader is always None for vanilla - let state = State::get().await?; if let Some(instance_mod_loader) = minecraft_instance.base_mod_loader { let game_version = minecraft_instance.game_version; From 403355244fd51d196a1444c9165604d8ea8369eb Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:32:56 -0400 Subject: [PATCH 28/38] refactor(app-lib): only pass pool to clear_persisted_process --- packages/app-lib/src/state/process.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/app-lib/src/state/process.rs b/packages/app-lib/src/state/process.rs index e872772a42a..f6bc0df9430 100644 --- a/packages/app-lib/src/state/process.rs +++ b/packages/app-lib/src/state/process.rs @@ -83,7 +83,7 @@ pub fn remove_log_buffer(instance_id: &str) { } async fn clear_persisted_process( - state: &crate::State, + pool: &SqlitePool, process: Option<(i64, i64)>, ) { let Some((pid, start_time)) = process else { @@ -94,7 +94,7 @@ async fn clear_persisted_process( pid, start_time, ) - .execute(&state.pool) + .execute(pool) .await { tracing::warn!("Failed to clear persisted process {pid}: {error}"); @@ -141,7 +141,7 @@ pub(crate) async fn instance_has_running_process( running = true; } else { clear_persisted_process( - state, + &state.pool, Some((process.pid, process.start_time)), ) .await; @@ -281,7 +281,7 @@ impl ProcessManager { post_process_init(&process.metadata, &process.rpc_server).await { tracing::error!("Failed to run post-process init: {e}"); - clear_persisted_process(state, persisted_process).await; + clear_persisted_process(&state.pool, persisted_process).await; let _ = process.child.kill().await; return Err(e); } @@ -944,7 +944,7 @@ impl Process { } state.process_manager.remove(uuid); - clear_persisted_process(&state, persisted_process).await; + clear_persisted_process(&state.pool, persisted_process).await; let sync_instance_id = instance_id.clone(); tokio::spawn(async move { if let Err(error) = From 16f9be08cb9af1317bfa87713706c8d05ed0fe43 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:08:18 -0400 Subject: [PATCH 29/38] refactor(app-lib): pull out install concurrency --- .../app-lib/src/api/pack/install_mrpack.rs | 2 +- packages/app-lib/src/install/runner.rs | 19 +++++++--------- packages/app-lib/src/state/installs.rs | 22 +++++++++++++++++++ packages/app-lib/src/state/mod.rs | 10 ++++----- 4 files changed, 36 insertions(+), 17 deletions(-) create mode 100644 packages/app-lib/src/state/installs.rs diff --git a/packages/app-lib/src/api/pack/install_mrpack.rs b/packages/app-lib/src/api/pack/install_mrpack.rs index 61fe0dfbc0e..1aed0ec9573 100644 --- a/packages/app-lib/src/api/pack/install_mrpack.rs +++ b/packages/app-lib/src/api/pack/install_mrpack.rs @@ -1197,7 +1197,7 @@ async fn install_zipped_mrpack_files_with_reporter_inner( crate::state::file_modified_at_ns(&io::metadata(&path).await?)?; { - let _permit = state.install_db_semaphore.acquire().await?; + let _permit = state.installs.db_semaphore.acquire().await?; let record_context = InstallErrorContext::new("record modpack override") .maybe_project_id(project_id.clone()) diff --git a/packages/app-lib/src/install/runner.rs b/packages/app-lib/src/install/runner.rs index 55cb14f4aca..3504aea3939 100644 --- a/packages/app-lib/src/install/runner.rs +++ b/packages/app-lib/src/install/runner.rs @@ -36,9 +36,6 @@ use std::sync::{Arc, LazyLock, Mutex, Weak}; use tokio::sync::{Mutex as AsyncMutex, MutexGuard, OwnedMutexGuard}; use uuid::Uuid; -/// Admission covers setup and deletion. A target reservation stays with its worker -/// until cleanup finishes, so backups and rollback cannot overlap another install. -static INSTALL_ADMISSION: AsyncMutex<()> = AsyncMutex::const_new(()); static INSTALL_TARGETS: LazyLock>>>> = LazyLock::new(|| Mutex::new(HashMap::new())); @@ -234,8 +231,8 @@ pub fn retry_job( } async fn retry_job_inner(job_id: Uuid) -> crate::Result { - let _admission = INSTALL_ADMISSION.lock().await; let state = State::get().await?; + let _admission = state.installs.admission.lock().await; let mut job = store::get_required(job_id, &state.pool).await?; if !matches!( @@ -421,11 +418,11 @@ pub async fn cancel_job(job_id: Uuid) -> crate::Result { /// The caller must retain both guards until the instance has been removed. /// This prevents a new install from starting after cancellation has finished. -pub(crate) async fn cancel_jobs_for_instance_deletion( +pub(crate) async fn cancel_jobs_for_instance_deletion<'a>( instance_id: &str, - state: &State, -) -> crate::Result<(MutexGuard<'static, ()>, OwnedMutexGuard<()>)> { - let admission = INSTALL_ADMISSION.lock().await; + state: &'a State, +) -> crate::Result<(MutexGuard<'a, ()>, OwnedMutexGuard<()>)> { + let admission = state.installs.admission.lock().await; let jobs = store::list_active_for_instance(instance_id, &state.pool).await?; for job in &jobs { @@ -483,9 +480,9 @@ fn start( async fn start_inner( request: InstallRequest, ) -> crate::Result { - let _admission = INSTALL_ADMISSION.lock().await; - let mut target_guard = reserve_target(&request.target())?; let state = State::get().await?; + let _admission = state.installs.admission.lock().await; + let mut target_guard = reserve_target(&request.target())?; if let InstallTarget::ExistingInstance { instance_id } = request.target() { store::ensure_no_pending_recovery(&instance_id, None, &state.pool) .await?; @@ -792,7 +789,7 @@ async fn run_job_inner( let _install_permit = if control.checkpoint().await.is_ok() { tokio::select! { - permit = state.install_job_semaphore.acquire() => Some(permit?), + permit = state.installs.job_semaphore.acquire() => Some(permit?), () = control.canceled() => None, } } else { diff --git a/packages/app-lib/src/state/installs.rs b/packages/app-lib/src/state/installs.rs new file mode 100644 index 00000000000..349f88fe24f --- /dev/null +++ b/packages/app-lib/src/state/installs.rs @@ -0,0 +1,22 @@ +use tokio::sync::{Mutex, Semaphore}; + +const MAX_CONCURRENT_INSTALL_JOBS: usize = 3; + +/// Limits install job concurrency. +pub(crate) struct Installs { + /// Admission covers setup and deletion. A target reservation stays with its worker + /// until cleanup finishes, so backups and rollback cannot overlap another install. + pub(crate) admission: Mutex<()>, + pub(crate) job_semaphore: Semaphore, + pub(crate) db_semaphore: Semaphore, +} + +impl Installs { + pub(crate) fn new() -> Self { + Self { + admission: Mutex::new(()), + job_semaphore: Semaphore::new(MAX_CONCURRENT_INSTALL_JOBS), + db_semaphore: Semaphore::new(1), + } + } +} diff --git a/packages/app-lib/src/state/mod.rs b/packages/app-lib/src/state/mod.rs index dbdbb9970d0..692398e96af 100644 --- a/packages/app-lib/src/state/mod.rs +++ b/packages/app-lib/src/state/mod.rs @@ -54,6 +54,9 @@ pub(crate) mod runtime_cache; mod friends; pub use self::friends::*; +mod installs; +pub(crate) use self::installs::Installs; + mod tunnel; pub use self::tunnel::*; @@ -78,7 +81,6 @@ static LAUNCHER_STATE: OnceCell> = OnceCell::const_new(); static STATE_STARTUP: LazyLock> = LazyLock::new(|| Sender::new(StartupPhase::Pending)); static STATE_STARTUP_LOCK: Mutex<()> = Mutex::const_new(()); -const MAX_CONCURRENT_INSTALL_JOBS: usize = 3; pub struct State { startup_complete: AtomicBool, /// Information on the location of files used in the launcher @@ -92,8 +94,7 @@ pub struct State { /// Semaphore to limit concurrent API requests. This is separate from the fetch semaphore /// to keep API functionality while the app is performing intensive tasks. pub api_semaphore: FetchSemaphore, - pub(crate) install_job_semaphore: Semaphore, - pub(crate) install_db_semaphore: Semaphore, + pub(crate) installs: Installs, /// Serializes filesystem reconciliation and content mutations per instance. instance_content_locks: DashMap>>, /// Serializes screenshot filesystem reconciliation per instance. @@ -413,8 +414,7 @@ impl State { fetch_semaphore, io_semaphore, api_semaphore, - install_job_semaphore: Semaphore::new(MAX_CONCURRENT_INSTALL_JOBS), - install_db_semaphore: Semaphore::new(1), + installs: Installs::new(), instance_content_locks: DashMap::new(), instance_screenshot_locks: DashMap::new(), shared_instance_locks: DashMap::new(), From 5101d51a4235ad324ada7ebbc6d15381ca4acbed Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Fri, 9 Oct 2026 12:13:36 -0400 Subject: [PATCH 30/38] refactor(app-lib): pull out social stuff --- packages/app-lib/src/api/friends.rs | 2 +- packages/app-lib/src/api/mr_auth.rs | 9 +++++---- packages/app-lib/src/launcher/mod.rs | 2 ++ packages/app-lib/src/state/friends.rs | 7 +++++-- packages/app-lib/src/state/mod.rs | 19 ++++++++----------- packages/app-lib/src/state/presence.rs | 16 ++++++++++++++++ packages/app-lib/src/state/process.rs | 3 ++- 7 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 packages/app-lib/src/state/presence.rs diff --git a/packages/app-lib/src/api/friends.rs b/packages/app-lib/src/api/friends.rs index 0fe883e01dc..cfc70f6e3c5 100644 --- a/packages/app-lib/src/api/friends.rs +++ b/packages/app-lib/src/api/friends.rs @@ -12,7 +12,7 @@ pub async fn friends() -> crate::Result> { pub async fn friend_statuses() -> crate::Result> { let state = crate::State::get().await?; - let statuses = state.friends_socket.friend_statuses(); + let statuses = state.presence.friends_socket.friend_statuses(); Ok(statuses) } diff --git a/packages/app-lib/src/api/mr_auth.rs b/packages/app-lib/src/api/mr_auth.rs index 01d282422e9..4a0eb30f0e5 100644 --- a/packages/app-lib/src/api/mr_auth.rs +++ b/packages/app-lib/src/api/mr_auth.rs @@ -63,7 +63,7 @@ pub async fn authenticate_finish_flow( pub async fn logout() -> crate::Result<()> { let state = crate::State::get().await?; ModrinthCredentials::deactivate_all(&state.pool).await?; - state.friends_socket.disconnect().await?; + state.presence.friends_socket.disconnect().await?; Ok(()) } @@ -101,17 +101,18 @@ pub async fn remove_user(user_id: &str) -> crate::Result<()> { ModrinthCredentials::remove(user_id, &state.pool).await?; if current.is_some_and(|creds| creds.user_id == user_id) { - state.friends_socket.disconnect().await?; + state.presence.friends_socket.disconnect().await?; } Ok(()) } async fn reconnect_friends(state: &crate::State) -> crate::Result<()> { - if let Err(error) = state.friends_socket.disconnect().await { + if let Err(error) = state.presence.friends_socket.disconnect().await { tracing::warn!("Failed to disconnect friends socket: {error}"); } if let Err(error) = state + .presence .friends_socket .connect(&state.pool, &state.api_semaphore, &state.process_manager) .await @@ -129,7 +130,7 @@ pub async fn get_credentials() -> crate::Result> { ModrinthCredentials::get_and_refresh(&state.pool, &state.api_semaphore) .await?; if current.is_none() { - state.friends_socket.disconnect().await?; + state.presence.friends_socket.disconnect().await?; } Ok(current) diff --git a/packages/app-lib/src/launcher/mod.rs b/packages/app-lib/src/launcher/mod.rs index fed2b283831..3bdee22edff 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -1280,11 +1280,13 @@ pub async fn launch_minecraft( } let _ = state + .presence .discord_rpc .set_activity(&format!("Playing {}", instance.name), true, &state.pool) .await; let _ = state + .presence .friends_socket .update_status(Some(instance.name.clone())) .await; diff --git a/packages/app-lib/src/state/friends.rs b/packages/app-lib/src/state/friends.rs index bb3995197ba..93ba2a552b7 100644 --- a/packages/app-lib/src/state/friends.rs +++ b/packages/app-lib/src/state/friends.rs @@ -296,7 +296,8 @@ impl FriendsSocket { let mut last_ping = Utc::now(); loop { - let connected = state.friends_socket.is_connected().await; + let connected = + state.presence.friends_socket.is_connected().await; if !connected && Utc::now().signed_duration_since(last_connection) @@ -305,6 +306,7 @@ impl FriendsSocket { last_connection = Utc::now(); last_ping = Utc::now(); let _ = state + .presence .friends_socket .connect( &state.pool, @@ -317,7 +319,8 @@ impl FriendsSocket { > chrono::Duration::seconds(10) { last_ping = Utc::now(); - let mut write = state.friends_socket.write.write().await; + let mut write = + state.presence.friends_socket.write.write().await; if let Some(write) = write.as_mut() { let _ = write.send(Message::Ping(Bytes::new())).await; } diff --git a/packages/app-lib/src/state/mod.rs b/packages/app-lib/src/state/mod.rs index 692398e96af..431007fe5c8 100644 --- a/packages/app-lib/src/state/mod.rs +++ b/packages/app-lib/src/state/mod.rs @@ -57,6 +57,9 @@ pub use self::friends::*; mod installs; pub(crate) use self::installs::Installs; +mod presence; +pub use self::presence::Presence; + mod tunnel; pub use self::tunnel::*; @@ -106,8 +109,7 @@ pub struct State { pub(crate) game_locale_indexer: crate::api::instance::GameLocaleIndexer, pub(crate) pack_sync_worker: crate::api::instance::PackSyncWorker, - /// Discord RPC - pub discord_rpc: DiscordGuard, + pub presence: Presence, /// Process manager pub process_manager: ProcessManager, @@ -118,9 +120,6 @@ pub struct State { // // /// App identifier string (like com.modrinth.ModrinthApp) // pub app_identifier: String, - /// Friends socket - pub friends_socket: FriendsSocket, - pub restart_after_pending_update: AtomicBool, pub(crate) pool: SqlitePool, @@ -287,7 +286,7 @@ impl State { } let res = tokio::try_join!( - state.discord_rpc.clear_to_default( + state.presence.discord_rpc.clear_to_default( true, &state.pool, &state.process_manager, @@ -305,6 +304,7 @@ impl State { } let _ = state + .presence .friends_socket .connect( &state.pool, @@ -398,15 +398,13 @@ impl State { ) .await?; - let discord_rpc = DiscordGuard::init()?; + let presence = Presence::init()?; tracing::info!("Initializing file watcher"); let file_watcher = instances::watcher::init_watcher().await?; let process_manager = ProcessManager::new(); - let friends_socket = FriendsSocket::new(); - Ok(Arc::new(Self { startup_complete: AtomicBool::new(false), directories, @@ -422,9 +420,8 @@ impl State { game_locale_indexer: crate::api::instance::GameLocaleIndexer::default(), pack_sync_worker: crate::api::instance::PackSyncWorker::default(), - discord_rpc, + presence, process_manager, - friends_socket, restart_after_pending_update: AtomicBool::new(false), pool, file_watcher, diff --git a/packages/app-lib/src/state/presence.rs b/packages/app-lib/src/state/presence.rs new file mode 100644 index 00000000000..0d89483f731 --- /dev/null +++ b/packages/app-lib/src/state/presence.rs @@ -0,0 +1,16 @@ +use super::{DiscordGuard, FriendsSocket}; + +/// Long-running social integrations. +pub struct Presence { + pub discord_rpc: DiscordGuard, + pub friends_socket: FriendsSocket, +} + +impl Presence { + pub(crate) fn init() -> crate::Result { + Ok(Self { + discord_rpc: DiscordGuard::init()?, + friends_socket: FriendsSocket::new(), + }) + } +} diff --git a/packages/app-lib/src/state/process.rs b/packages/app-lib/src/state/process.rs index f6bc0df9430..ada4be655dc 100644 --- a/packages/app-lib/src/state/process.rs +++ b/packages/app-lib/src/state/process.rs @@ -1007,11 +1007,12 @@ impl Process { } let _ = state + .presence .discord_rpc .clear_to_default(true, &state.pool, &state.process_manager) .await; - let _ = state.friends_socket.update_status(None).await; + let _ = state.presence.friends_socket.update_status(None).await; #[cfg(feature = "tauri")] { From 6feda8b400dabc0c51b339cc6cee2d0f291a8d96 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:16:09 -0400 Subject: [PATCH 31/38] refactor(app-lib): pull out per-instance concurrency --- packages/app-lib/src/api/instance/files.rs | 8 +-- .../api/instance/screenshots/operations.rs | 4 +- .../instance/screenshots/reconciliation.rs | 2 +- .../src/api/instance/shared/invites.rs | 12 ++-- packages/app-lib/src/install/recovery.rs | 8 ++- packages/app-lib/src/install/runner.rs | 2 +- packages/app-lib/src/launcher/mod.rs | 2 +- .../state/content_store/commands/migration.rs | 2 +- packages/app-lib/src/state/instance_locks.rs | 54 ++++++++++++++++ .../commands/apply_content_install.rs | 2 +- .../instances/commands/content_mutation.rs | 2 +- .../instances/commands/remove_instance.rs | 4 +- packages/app-lib/src/state/mod.rs | 62 ++----------------- 13 files changed, 87 insertions(+), 77 deletions(-) create mode 100644 packages/app-lib/src/state/instance_locks.rs diff --git a/packages/app-lib/src/api/instance/files.rs b/packages/app-lib/src/api/instance/files.rs index fcd00df79ba..4b7f07c6f04 100644 --- a/packages/app-lib/src/api/instance/files.rs +++ b/packages/app-lib/src/api/instance/files.rs @@ -185,7 +185,7 @@ pub async fn write_instance_file( create_only: bool, ) -> crate::Result<()> { let state = State::get().await?; - let _instance = state.lock_instance_content(instance_id).await; + let _instance = state.instance_locks.lock_content(instance_id).await; let _files = state.content_store.files_lock.lock().await; let destination = resolve(&state, instance_id, path, true) .await? @@ -229,7 +229,7 @@ pub async fn create_instance_directory( path: &str, ) -> crate::Result<()> { let state = State::get().await?; - let _instance = state.lock_instance_content(instance_id).await; + let _instance = state.instance_locks.lock_content(instance_id).await; let _files = state.content_store.files_lock.lock().await; let destination = resolve(&state, instance_id, path, true).await?; fs::create_dir(destination.path()).await?; @@ -242,7 +242,7 @@ pub async fn rename_instance_file( destination: &str, ) -> crate::Result<()> { let state = State::get().await?; - let _instance = state.lock_instance_content(instance_id).await; + let _instance = state.instance_locks.lock_content(instance_id).await; let _files = state.content_store.files_lock.lock().await; let source = resolve(&state, instance_id, source, true).await?; let destination = resolve(&state, instance_id, destination, true).await?; @@ -259,7 +259,7 @@ pub async fn delete_instance_file( recursive: bool, ) -> crate::Result<()> { let state = State::get().await?; - let _instance = state.lock_instance_content(instance_id).await; + let _instance = state.instance_locks.lock_content(instance_id).await; let _files = state.content_store.files_lock.lock().await; let path = resolve(&state, instance_id, path, true).await?; if fs::symlink_metadata(path.path()).await?.is_dir() { diff --git a/packages/app-lib/src/api/instance/screenshots/operations.rs b/packages/app-lib/src/api/instance/screenshots/operations.rs index 3f69c3b64bf..ba68845af71 100644 --- a/packages/app-lib/src/api/instance/screenshots/operations.rs +++ b/packages/app-lib/src/api/instance/screenshots/operations.rs @@ -109,7 +109,7 @@ async fn lock_instance_screenshots<'a>( ) -> Vec> { let mut locks = Vec::new(); for instance_id in instance_ids { - locks.push(state.lock_instance_screenshots(instance_id).await); + locks.push(state.instance_locks.lock_screenshots(instance_id).await); } locks } @@ -344,7 +344,7 @@ pub async fn save_edited_screenshot( .ok_or_else(|| { crate::ErrorKind::InputError("Unknown instance".to_string()) })?; - let _lock = state.lock_instance_screenshots(&source.id).await; + let _lock = state.instance_locks.lock_screenshots(&source.id).await; let scanned = scan_source_screenshots(&state, &source).await?; let current = diff --git a/packages/app-lib/src/api/instance/screenshots/reconciliation.rs b/packages/app-lib/src/api/instance/screenshots/reconciliation.rs index f5511ac6050..1fe19e5ea6c 100644 --- a/packages/app-lib/src/api/instance/screenshots/reconciliation.rs +++ b/packages/app-lib/src/api/instance/screenshots/reconciliation.rs @@ -79,7 +79,7 @@ pub(super) async fn list_source_screenshots( state: &State, source: InstanceScreenshotSource, ) -> crate::Result> { - let _lock = state.lock_instance_screenshots(&source.id).await; + let _lock = state.instance_locks.lock_screenshots(&source.id).await; let scanned = scan_source_screenshots(state, &source).await?; reconcile_source_screenshots(state, &source, scanned).await } diff --git a/packages/app-lib/src/api/instance/shared/invites.rs b/packages/app-lib/src/api/instance/shared/invites.rs index 1e322aa561e..55fde6eed2f 100644 --- a/packages/app-lib/src/api/instance/shared/invites.rs +++ b/packages/app-lib/src/api/instance/shared/invites.rs @@ -25,7 +25,8 @@ pub async fn invite_shared_instance_users( user_ids: Vec, ) -> crate::Result { let state = State::get().await?; - let _shared_instance_lock = state.lock_shared_instance(instance_id).await; + let _shared_instance_lock = + state.instance_locks.lock_shared(instance_id).await; let (metadata, attachment) = shared_instance_for_invites(instance_id, user_ids.len(), &state) .await?; @@ -65,7 +66,8 @@ pub async fn create_shared_instance_invite_link( replace_invite_id: Option, ) -> crate::Result { let state = State::get().await?; - let _shared_instance_lock = state.lock_shared_instance(instance_id).await; + let _shared_instance_lock = + state.instance_locks.lock_shared(instance_id).await; let (metadata, attachment) = shared_instance_for_invites(instance_id, 0, &state).await?; ensure_owner(&attachment)?; @@ -152,7 +154,8 @@ pub async fn revoke_shared_instance_invite( invite_id: String, ) -> crate::Result<()> { let state = State::get().await?; - let _shared_instance_lock = state.lock_shared_instance(instance_id).await; + let _shared_instance_lock = + state.instance_locks.lock_shared(instance_id).await; let Some(attachment) = shared_attachment(instance_id, &state).await? else { return Ok(()); }; @@ -169,7 +172,8 @@ pub async fn remove_shared_instance_users( has_pending_recipients: bool, ) -> crate::Result { let state = State::get().await?; - let _shared_instance_lock = state.lock_shared_instance(instance_id).await; + let _shared_instance_lock = + state.instance_locks.lock_shared(instance_id).await; let Some(attachment) = shared_attachment(instance_id, &state).await? else { return Ok(SharedInstanceUsers::empty()); }; diff --git a/packages/app-lib/src/install/recovery.rs b/packages/app-lib/src/install/recovery.rs index 6c64efd52ab..8e9c309bc6c 100644 --- a/packages/app-lib/src/install/recovery.rs +++ b/packages/app-lib/src/install/recovery.rs @@ -43,8 +43,10 @@ pub(super) async fn prepare_instance_update_backup( state: &State, ) -> crate::Result { let _lease = state.content_store.lease().await; - let _content_lock = - state.lock_instance_content(&metadata.instance.id).await; + let _content_lock = state + .instance_locks + .lock_content(&metadata.instance.id) + .await; let _store_lock = state.content_store.files_lock.lock().await; if crate::state::instance_has_running_process(&metadata.instance.id, state) .await? @@ -242,7 +244,7 @@ async fn restore_instance_update( state: &State, ) -> crate::Result<()> { let instance_id = &rollback.instance.instance.id; - let _content_lock = state.lock_instance_content(instance_id).await; + let _content_lock = state.instance_locks.lock_content(instance_id).await; let _store_lock = state.content_store.files_lock.lock().await; let _lease = state.content_store.lease().await; if crate::state::instance_has_running_process(instance_id, state).await? { diff --git a/packages/app-lib/src/install/runner.rs b/packages/app-lib/src/install/runner.rs index 3504aea3939..13c84719858 100644 --- a/packages/app-lib/src/install/runner.rs +++ b/packages/app-lib/src/install/runner.rs @@ -1730,7 +1730,7 @@ async fn lock_install_target( } async fn lock_instance(instance_id: &str, state: &State) -> crate::Result<()> { - let _content_lock = state.lock_instance_content(instance_id).await; + let _content_lock = state.instance_locks.lock_content(instance_id).await; if crate::state::instance_has_running_process(instance_id, state).await? { return Err(crate::state::content_store::input( "Stop this instance before installing or updating its content", diff --git a/packages/app-lib/src/launcher/mod.rs b/packages/app-lib/src/launcher/mod.rs index 3bdee22edff..c4aca462aa3 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -1227,7 +1227,7 @@ pub async fn launch_minecraft( crate::state::instances::commands::sync_content_files(&instance.id, state) .await?; let _instance_content_lock = - state.lock_instance_content(&instance.id).await; + state.instance_locks.lock_content(&instance.id).await; let current = crate::state::instances::adapters::sqlite::instance_rows::get_instance_by_id( &instance.id, &state.pool, diff --git a/packages/app-lib/src/state/content_store/commands/migration.rs b/packages/app-lib/src/state/content_store/commands/migration.rs index ae8263644f9..dda2dadadbb 100644 --- a/packages/app-lib/src/state/content_store/commands/migration.rs +++ b/packages/app-lib/src/state/content_store/commands/migration.rs @@ -204,7 +204,7 @@ async fn migrate_file_copy( let copy_ms = started.elapsed().as_millis() as u64; let waiting = Instant::now(); let _turn = store.legacy_migration_priority.read().await; - let _instance = state.lock_instance_content(&instance.id).await; + let _instance = state.instance_locks.lock_content(&instance.id).await; let _files = store.files_lock.lock().await; let lock_wait_ms = waiting.elapsed().as_millis() as u64; let Some(instance) = diff --git a/packages/app-lib/src/state/instance_locks.rs b/packages/app-lib/src/state/instance_locks.rs new file mode 100644 index 00000000000..3508f5f8d30 --- /dev/null +++ b/packages/app-lib/src/state/instance_locks.rs @@ -0,0 +1,54 @@ +use dashmap::DashMap; +use std::sync::Arc; +use tokio::sync::{Mutex, OwnedMutexGuard}; + +/// Serializes concurrent changes per instance. +#[derive(Default)] +pub(crate) struct InstanceLocks { + /// Serializes filesystem reconciliation and content mutations per instance. + content: DashMap>>, + screenshots: DashMap>>, + /// Serializes shared instance attachment and recipient mutations per instance. + shared: DashMap>>, +} + +impl InstanceLocks { + pub(crate) async fn lock_content( + &self, + instance_id: &str, + ) -> OwnedMutexGuard<()> { + lock(&self.content, instance_id).await + } + + pub(crate) async fn lock_screenshots( + &self, + instance_id: &str, + ) -> OwnedMutexGuard<()> { + lock(&self.screenshots, instance_id).await + } + + pub(crate) async fn lock_shared( + &self, + instance_id: &str, + ) -> OwnedMutexGuard<()> { + lock(&self.shared, instance_id).await + } + + pub(crate) fn remove(&self, instance_id: &str) { + let _ = self.content.remove(instance_id); + let _ = self.screenshots.remove(instance_id); + let _ = self.shared.remove(instance_id); + } +} + +async fn lock( + locks: &DashMap>>, + instance_id: &str, +) -> OwnedMutexGuard<()> { + let lock = locks + .entry(instance_id.to_string()) + .or_insert_with(|| Arc::new(Mutex::new(()))) + .clone(); + + lock.lock_owned().await +} diff --git a/packages/app-lib/src/state/instances/commands/apply_content_install.rs b/packages/app-lib/src/state/instances/commands/apply_content_install.rs index 76a0ef2f77e..89b21b28071 100644 --- a/packages/app-lib/src/state/instances/commands/apply_content_install.rs +++ b/packages/app-lib/src/state/instances/commands/apply_content_install.rs @@ -681,7 +681,7 @@ pub(crate) async fn set_project_locked( locked: bool, state: &State, ) -> crate::Result<()> { - let _content_lock = state.lock_instance_content(instance_id).await; + let _content_lock = state.instance_locks.lock_content(instance_id).await; let scope = resolve_content_scope(instance_id, None, state).await?; content_rows::set_instance_file_locked( &scope.instance.id, diff --git a/packages/app-lib/src/state/instances/commands/content_mutation.rs b/packages/app-lib/src/state/instances/commands/content_mutation.rs index 76197116675..16b8f9f38d8 100644 --- a/packages/app-lib/src/state/instances/commands/content_mutation.rs +++ b/packages/app-lib/src/state/instances/commands/content_mutation.rs @@ -103,7 +103,7 @@ impl<'a> InstanceContent<'a> { instance_id: &str, state: &'a State, ) -> crate::Result { - let content_lock = state.lock_instance_content(instance_id).await; + let content_lock = state.instance_locks.lock_content(instance_id).await; let store_lock = state.content_store.files_lock.lock().await; let instance = instance_rows::get_instance_by_id(instance_id, &state.pool) diff --git a/packages/app-lib/src/state/instances/commands/remove_instance.rs b/packages/app-lib/src/state/instances/commands/remove_instance.rs index dbc7b7cd7ea..53e78cd3b1c 100644 --- a/packages/app-lib/src/state/instances/commands/remove_instance.rs +++ b/packages/app-lib/src/state/instances/commands/remove_instance.rs @@ -12,7 +12,7 @@ pub(crate) async fn remove_instance( crate::ErrorKind::InputError("Unknown instance".to_string()) })?; let _synced_options_lock = state.lock_synced_options().await; - let _content_lock = state.lock_instance_content(instance_id).await; + let _content_lock = state.instance_locks.lock_content(instance_id).await; let _store_lock = state.content_store.files_lock.lock().await; let _store_lease = state.content_store.lease().await; if crate::state::instance_has_running_process(instance_id, state).await? { @@ -40,7 +40,7 @@ async fn delete_instance_row_and_locks( ) -> crate::Result<()> { // Keep these together so deleted instances cannot leave stale entries in the per-instance lock maps. instance_rows::delete_instance_by_id(instance_id, &state.pool).await?; - state.remove_instance_locks(instance_id); + state.instance_locks.remove(instance_id); Ok(()) } diff --git a/packages/app-lib/src/state/mod.rs b/packages/app-lib/src/state/mod.rs index 431007fe5c8..723cf562da2 100644 --- a/packages/app-lib/src/state/mod.rs +++ b/packages/app-lib/src/state/mod.rs @@ -1,10 +1,9 @@ //! Theseus state management system use crate::util::fetch::{FetchSemaphore, IoSemaphore}; -use dashmap::DashMap; use std::sync::atomic::AtomicBool; use std::sync::{Arc, LazyLock}; use tokio::sync::watch::Sender; -use tokio::sync::{Mutex, MutexGuard, OnceCell, OwnedMutexGuard, Semaphore}; +use tokio::sync::{Mutex, MutexGuard, OnceCell, Semaphore}; use crate::state::instances::watcher::FileWatcher; use sqlx::SqlitePool; @@ -57,6 +56,9 @@ pub use self::friends::*; mod installs; pub(crate) use self::installs::Installs; +mod instance_locks; +pub(crate) use self::instance_locks::InstanceLocks; + mod presence; pub use self::presence::Presence; @@ -98,12 +100,7 @@ pub struct State { /// to keep API functionality while the app is performing intensive tasks. pub api_semaphore: FetchSemaphore, pub(crate) installs: Installs, - /// Serializes filesystem reconciliation and content mutations per instance. - instance_content_locks: DashMap>>, - /// Serializes screenshot filesystem reconciliation per instance. - instance_screenshot_locks: DashMap>>, - /// Serializes shared instance attachment and recipient mutations per instance. - shared_instance_locks: DashMap>>, + pub(crate) instance_locks: InstanceLocks, /// Serializes canonical synced-option mutations and checkpoint updates. synced_options_lock: Mutex<()>, pub(crate) game_locale_indexer: crate::api::instance::GameLocaleIndexer, @@ -132,51 +129,6 @@ impl State { self.synced_options_lock.lock().await } - pub(crate) async fn lock_instance_content( - &self, - instance_id: &str, - ) -> OwnedMutexGuard<()> { - let lock = self - .instance_content_locks - .entry(instance_id.to_string()) - .or_insert_with(|| Arc::new(Mutex::new(()))) - .clone(); - - lock.lock_owned().await - } - - pub(crate) async fn lock_shared_instance( - &self, - instance_id: &str, - ) -> OwnedMutexGuard<()> { - let lock = self - .shared_instance_locks - .entry(instance_id.to_string()) - .or_insert_with(|| Arc::new(Mutex::new(()))) - .clone(); - - lock.lock_owned().await - } - - pub(crate) async fn lock_instance_screenshots( - &self, - instance_id: &str, - ) -> OwnedMutexGuard<()> { - let lock = self - .instance_screenshot_locks - .entry(instance_id.to_string()) - .or_insert_with(|| Arc::new(Mutex::new(()))) - .clone(); - - lock.lock_owned().await - } - - pub(crate) fn remove_instance_locks(&self, instance_id: &str) { - let _ = self.instance_content_locks.remove(instance_id); - let _ = self.instance_screenshot_locks.remove(instance_id); - let _ = self.shared_instance_locks.remove(instance_id); - } - pub async fn init(app_identifier: String) -> crate::Result<()> { let _startup = STATE_STARTUP_LOCK.lock().await; let result = LAUNCHER_STATE @@ -413,9 +365,7 @@ impl State { io_semaphore, api_semaphore, installs: Installs::new(), - instance_content_locks: DashMap::new(), - instance_screenshot_locks: DashMap::new(), - shared_instance_locks: DashMap::new(), + instance_locks: InstanceLocks::default(), synced_options_lock: Mutex::new(()), game_locale_indexer: crate::api::instance::GameLocaleIndexer::default(), From 5fd1322c241158b6172a645a30b368cd24766b9b Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:19:28 -0400 Subject: [PATCH 32/38] refactor(app-lib): pull out options syncing concurrency --- .../synced_options/command_history.rs | 4 +- .../game_options/launch_overrides.rs | 4 +- .../game_options/local_settings_editor.rs | 6 +- .../game_options/locales/mod.rs | 9 +-- .../game_options/pack_updates.rs | 2 +- .../game_options/settings_editor.rs | 6 +- .../game_options/source_selection.rs | 2 +- .../instance/synced_options/orchestration.rs | 18 +++--- .../api/instance/synced_packs/operations.rs | 8 +-- .../src/api/instance/synced_packs/storage.rs | 12 ++-- .../src/api/instance/synced_packs/worker.rs | 61 ++++++++++++------- .../api/instance/synced_servers/modpack.rs | 2 +- .../api/instance/synced_servers/operations.rs | 16 ++--- packages/app-lib/src/api/settings.rs | 4 +- .../instances/commands/remove_instance.rs | 2 +- packages/app-lib/src/state/mod.rs | 19 ++---- packages/app-lib/src/state/synced_options.rs | 17 ++++++ 17 files changed, 111 insertions(+), 81 deletions(-) create mode 100644 packages/app-lib/src/state/synced_options.rs diff --git a/packages/app-lib/src/api/instance/synced_options/command_history.rs b/packages/app-lib/src/api/instance/synced_options/command_history.rs index 59d19237a39..5ee1a4fce52 100644 --- a/packages/app-lib/src/api/instance/synced_options/command_history.rs +++ b/packages/app-lib/src/api/instance/synced_options/command_history.rs @@ -15,7 +15,7 @@ const COMMAND_HISTORY_LIMIT: usize = 50; pub async fn get_command_history() -> crate::Result { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let path = command_history_path(&state); if !path.exists() { return Ok(String::new()); @@ -25,7 +25,7 @@ pub async fn get_command_history() -> crate::Result { pub async fn set_command_history(contents: &str) -> crate::Result { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; create_synced_directories(&state).await?; let normalized = normalize_command_history(contents); io::write(command_history_path(&state), normalized.as_bytes()).await?; diff --git a/packages/app-lib/src/api/instance/synced_options/game_options/launch_overrides.rs b/packages/app-lib/src/api/instance/synced_options/game_options/launch_overrides.rs index 08901884ddc..7e292de0791 100644 --- a/packages/app-lib/src/api/instance/synced_options/game_options/launch_overrides.rs +++ b/packages/app-lib/src/api/instance/synced_options/game_options/launch_overrides.rs @@ -25,7 +25,7 @@ pub(super) fn currently_launcher_owned_keys( /// Writes canonical settings to `options.txt` before launch. pub async fn sync_before_launch(instance_id: &str) -> crate::Result<()> { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let metadata = crate::state::get_instance(instance_id, &state.pool) .await? .ok_or_else(|| input_error("Unknown instance"))?; @@ -47,7 +47,7 @@ pub async fn apply_launcher_overrides( return Ok(()); } let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let metadata = crate::state::get_instance(instance_id, &state.pool) .await? .ok_or_else(|| input_error("Unknown instance"))?; diff --git a/packages/app-lib/src/api/instance/synced_options/game_options/local_settings_editor.rs b/packages/app-lib/src/api/instance/synced_options/game_options/local_settings_editor.rs index a7e71f75aee..5626fd8b3a8 100644 --- a/packages/app-lib/src/api/instance/synced_options/game_options/local_settings_editor.rs +++ b/packages/app-lib/src/api/instance/synced_options/game_options/local_settings_editor.rs @@ -266,7 +266,7 @@ pub async fn get_config( instance_id: &str, ) -> crate::Result { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; load_editor(instance_id, &state).await } @@ -275,7 +275,7 @@ pub async fn preview_changes( request: UpdateGameSettingsRequest, ) -> crate::Result { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let (metadata, document, input_bytes, controlled_keys) = load_instance_document(instance_id, &state).await?; let mut editor = build_editor_state( @@ -335,7 +335,7 @@ pub async fn save_changes( request: UpdateGameSettingsRequest, ) -> crate::Result { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let (metadata, mut document, input_bytes, controlled_keys) = load_instance_document(instance_id, &state).await?; let current_editor = build_editor_state( diff --git a/packages/app-lib/src/api/instance/synced_options/game_options/locales/mod.rs b/packages/app-lib/src/api/instance/synced_options/game_options/locales/mod.rs index f3ea5fc646e..bb8b9efd696 100644 --- a/packages/app-lib/src/api/instance/synced_options/game_options/locales/mod.rs +++ b/packages/app-lib/src/api/instance/synced_options/game_options/locales/mod.rs @@ -80,7 +80,7 @@ struct Candidate { pub(crate) fn queue_game_locale_index() { if let Some(state) = State::get_if_initialized() { - state.game_locale_indexer.notify.notify_one(); + state.synced_options.locales.notify.notify_one(); } else { tracing::warn!( "Game setting locales: cannot queue indexing before state initialization" @@ -90,16 +90,17 @@ pub(crate) fn queue_game_locale_index() { pub(crate) fn start_game_locale_indexer(state: Arc) { if state - .game_locale_indexer + .synced_options + .locales .started .swap(true, Ordering::AcqRel) { return; } - state.game_locale_indexer.notify.notify_one(); + state.synced_options.locales.notify.notify_one(); tokio::spawn(async move { loop { - state.game_locale_indexer.notify.notified().await; + state.synced_options.locales.notify.notified().await; tokio::time::sleep(std::time::Duration::from_millis(300)).await; if let Err(error) = index_installed_sources(&state).await { tracing::warn!(%error, "Game setting locales: indexing failed"); diff --git a/packages/app-lib/src/api/instance/synced_options/game_options/pack_updates.rs b/packages/app-lib/src/api/instance/synced_options/game_options/pack_updates.rs index d125e2a01e5..05039d1a294 100644 --- a/packages/app-lib/src/api/instance/synced_options/game_options/pack_updates.rs +++ b/packages/app-lib/src/api/instance/synced_options/game_options/pack_updates.rs @@ -290,7 +290,7 @@ pub async fn capture_pack_base( source: Option, ) -> crate::Result<()> { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let metadata = crate::state::get_instance(instance_id, &state.pool) .await? .ok_or_else(|| input_error("Unknown instance"))?; diff --git a/packages/app-lib/src/api/instance/synced_options/game_options/settings_editor.rs b/packages/app-lib/src/api/instance/synced_options/game_options/settings_editor.rs index 2512c24b773..05f679bf7ce 100644 --- a/packages/app-lib/src/api/instance/synced_options/game_options/settings_editor.rs +++ b/packages/app-lib/src/api/instance/synced_options/game_options/settings_editor.rs @@ -194,7 +194,7 @@ pub(super) async fn load_settings_editor( pub async fn get_config() -> crate::Result { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; load_settings_editor(&state).await } @@ -203,7 +203,7 @@ pub async fn preview_changes( request: UpdateGameSettingsRequest, ) -> crate::Result { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; if request.expected_catalog_revision != CATALOG_REVISION { return load_settings_editor(&state).await; } @@ -276,7 +276,7 @@ pub async fn save_changes( request: UpdateGameSettingsRequest, ) -> crate::Result { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; if !game_options_sync_is_enabled(&state.pool).await? { return Err(input_error( "Turn on game-settings sync before editing shared values.", diff --git a/packages/app-lib/src/api/instance/synced_options/game_options/source_selection.rs b/packages/app-lib/src/api/instance/synced_options/game_options/source_selection.rs index b4e1c0f0809..931ffd7f191 100644 --- a/packages/app-lib/src/api/instance/synced_options/game_options/source_selection.rs +++ b/packages/app-lib/src/api/instance/synced_options/game_options/source_selection.rs @@ -319,7 +319,7 @@ pub(in crate::api::instance) async fn initialize_from_source_instance( pub async fn list_sync_sources() -> crate::Result> { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let instances = crate::state::list_instances(&state.pool).await?; let mut sources = Vec::with_capacity(instances.len()); for metadata in instances { diff --git a/packages/app-lib/src/api/instance/synced_options/orchestration.rs b/packages/app-lib/src/api/instance/synced_options/orchestration.rs index 4aa7319f1b6..d91fe250e02 100644 --- a/packages/app-lib/src/api/instance/synced_options/orchestration.rs +++ b/packages/app-lib/src/api/instance/synced_options/orchestration.rs @@ -122,7 +122,7 @@ pub async fn get_global_options() -> crate::Result { pub async fn get_initialized_options() -> crate::Result { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let mut options = GlobalSyncedOptions::default(); for option in SyncedOption::ALL { @@ -348,7 +348,7 @@ pub async fn set_global_option( .into()); } let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; pending::cancel(option, None, &state).await?; set_global_option_with_state(option, enabled, base_instance_id, &state) .await @@ -673,7 +673,7 @@ pub async fn set_instance_option( .into()); } let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let metadata = crate::state::get_instance(instance_id, &state.pool) .await? .ok_or_else(|| ErrorKind::InputError("Unknown instance".to_string()))?; @@ -775,7 +775,7 @@ pub async fn get_instance_option_join_preview( option: SyncedOption, ) -> crate::Result { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let metadata = crate::state::get_instance(instance_id, &state.pool) .await? .ok_or_else(|| ErrorKind::InputError("Unknown instance".to_string()))?; @@ -927,7 +927,7 @@ async fn apply_pending_changes(state: &State) -> crate::Result<()> { pub async fn reconcile_all() -> crate::Result<()> { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; create_synced_directories(&state).await?; apply_pending_changes(&state).await?; let instances = crate::state::list_instances(&state.pool).await?; @@ -999,7 +999,7 @@ pub fn reconcile_instance( async fn reconcile_instance_inner(instance_id: &str) -> crate::Result<()> { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; apply_pending_changes(&state).await?; let metadata = crate::state::get_instance(instance_id, &state.pool) .await? @@ -1011,7 +1011,7 @@ pub(crate) async fn reconcile_instance_after_pack_update( instance_id: &str, ) -> crate::Result<()> { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let metadata = crate::state::get_instance(instance_id, &state.pool) .await? .ok_or_else(|| ErrorKind::InputError("Unknown instance".to_string()))?; @@ -1022,7 +1022,7 @@ pub(crate) async fn prepare_instance_update( instance_id: &str, ) -> crate::Result<()> { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let metadata = crate::state::get_instance(instance_id, &state.pool) .await? .ok_or_else(|| ErrorKind::InputError("Unknown instance".to_string()))?; @@ -1139,7 +1139,7 @@ pub async fn reconcile_changed_file( file_name: &str, ) -> crate::Result<()> { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; apply_pending_changes(&state).await?; let metadata = crate::state::get_instance(instance_id, &state.pool) .await? diff --git a/packages/app-lib/src/api/instance/synced_packs/operations.rs b/packages/app-lib/src/api/instance/synced_packs/operations.rs index 33346d78154..0fec5d88f61 100644 --- a/packages/app-lib/src/api/instance/synced_packs/operations.rs +++ b/packages/app-lib/src/api/instance/synced_packs/operations.rs @@ -594,7 +594,7 @@ pub async fn upload_synced_pack( let size = bytes.len() as u64; let sha1 = crate::util::fetch::sha1_async(bytes.clone()).await?; let sha512 = cache_bytes(bytes, &state).await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; if !get_global_options().await?.get(option) { return Err(crate::ErrorKind::InputError( "Pack syncing was disabled while preparing the upload.".to_owned(), @@ -648,7 +648,7 @@ pub async fn set_synced_pack_enabled( enabled: bool, ) -> crate::Result<()> { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let mut library = read_library(&state).await?; let pack = library.packs.get_mut(pack_id).ok_or_else(|| { crate::ErrorKind::InputError("Unknown synced pack.".to_string()) @@ -660,7 +660,7 @@ pub async fn set_synced_pack_enabled( pub async fn remove_synced_pack(pack_id: &str) -> crate::Result<()> { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let mut library = read_library(&state).await?; let pack = library.packs.get(pack_id).ok_or_else(|| { crate::ErrorKind::InputError("Unknown synced pack.".to_string()) @@ -676,7 +676,7 @@ pub async fn desync_pack( mode: DesyncServerMode, ) -> crate::Result<()> { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let metadata = crate::state::get_instance(instance_id, &state.pool) .await? .ok_or_else(|| { diff --git a/packages/app-lib/src/api/instance/synced_packs/storage.rs b/packages/app-lib/src/api/instance/synced_packs/storage.rs index f699c4cb37d..914d7a3ec7e 100644 --- a/packages/app-lib/src/api/instance/synced_packs/storage.rs +++ b/packages/app-lib/src/api/instance/synced_packs/storage.rs @@ -101,7 +101,7 @@ pub(super) async fn write_library( .await?; tokio::fs::rename(&temporary, &destination).await?; renamed = true; - state.pack_sync_worker.revision + state.synced_options.packs.revision .fetch_add(1, std::sync::atomic::Ordering::Release); } crate::state::content_store::sync_directory(&directory(state)).await?; @@ -233,7 +233,7 @@ pub(super) async fn read_stored_file( pub(crate) async fn migrate_store(state: &State) -> crate::Result<()> { let ids = { - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; read_library(state) .await? .packs @@ -244,7 +244,7 @@ pub(crate) async fn migrate_store(state: &State) -> crate::Result<()> { for id in ids { let _turn = state.content_store.legacy_migration_priority.read().await; let snapshot = { - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; read_library(state).await? }; let Some(expected) = snapshot.packs.get(&id) else { @@ -252,7 +252,7 @@ pub(crate) async fn migrate_store(state: &State) -> crate::Result<()> { }; let recovered = recover_legacy_pack(Some(&id), expected, &snapshot, state).await; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let mut library = read_library(state).await?; let Some(previous) = library.packs.get(&id).cloned() else { continue; @@ -292,7 +292,7 @@ pub(crate) async fn migrate_store(state: &State) -> crate::Result<()> { } } let cache_complete = cleanup_legacy_cache(state).await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; if tokio::fs::try_exists(directory(state)).await? { crate::state::content_store::sync_directory(&directory(state)).await?; } @@ -439,7 +439,7 @@ async fn cleanup_legacy_cache(state: &State) -> crate::Result { let mut entries = tokio::fs::read_dir(&legacy).await?; while let Some(entry) = entries.next_entry().await? { let _turn = state.content_store.legacy_migration_priority.read().await; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let library = read_library(state).await?; if !entry.file_type().await?.is_file() { continue; diff --git a/packages/app-lib/src/api/instance/synced_packs/worker.rs b/packages/app-lib/src/api/instance/synced_packs/worker.rs index d376ee86668..9797366439b 100644 --- a/packages/app-lib/src/api/instance/synced_packs/worker.rs +++ b/packages/app-lib/src/api/instance/synced_packs/worker.rs @@ -33,7 +33,8 @@ pub(crate) struct PackSyncWorker { pub(crate) fn queue_reconciliation(instance_id: &str) { if let Some(state) = State::get_if_initialized() { state - .pack_sync_worker + .synced_options + .packs .queue .lock() .instances @@ -44,7 +45,7 @@ pub(crate) fn queue_reconciliation(instance_id: &str) { pub(super) fn queue_all() { if let Some(state) = State::get_if_initialized() { - state.pack_sync_worker.queue.lock().all = true; + state.synced_options.packs.queue.lock().all = true; start(state); } } @@ -53,7 +54,7 @@ pub(crate) async fn flush(instance_id: &str) -> crate::Result<()> { let state = State::get().await?; let (sender, receiver) = oneshot::channel(); { - let mut queue = state.pack_sync_worker.queue.lock(); + let mut queue = state.synced_options.packs.queue.lock(); if let Some(retry_at) = queue.retry_at.get(instance_id) { let remaining = retry_at.saturating_duration_since(Instant::now()); if !remaining.is_zero() { @@ -82,7 +83,7 @@ pub(crate) async fn flush(instance_id: &str) -> crate::Result<()> { fn start(state: Arc) { { - let mut queue = state.pack_sync_worker.queue.lock(); + let mut queue = state.synced_options.packs.queue.lock(); if queue.running { return; } @@ -91,29 +92,36 @@ fn start(state: Arc) { tokio::spawn(async move { loop { tokio::time::sleep(std::time::Duration::from_millis(100)).await; - let all = - std::mem::take(&mut state.pack_sync_worker.queue.lock().all); + let all = std::mem::take( + &mut state.synced_options.packs.queue.lock().all, + ); if all { match crate::state::list_instances(&state.pool).await { Ok(instances) => { - state.pack_sync_worker.queue.lock().instances.extend( - instances - .into_iter() - .map(|metadata| metadata.instance.id), - ); + state + .synced_options + .packs + .queue + .lock() + .instances + .extend( + instances + .into_iter() + .map(|metadata| metadata.instance.id), + ); } Err(error) => { tracing::warn!( "Could not enumerate pack sync targets: {error}" ); - state.pack_sync_worker.queue.lock().all = true; + state.synced_options.packs.queue.lock().all = true; tokio::time::sleep(std::time::Duration::from_secs(1)) .await; } } } let (instance_id, mut pending) = { - let mut queue = state.pack_sync_worker.queue.lock(); + let mut queue = state.synced_options.packs.queue.lock(); let next = queue .waiters .keys() @@ -137,7 +145,7 @@ fn start(state: Arc) { if let Err(error) = &result { match error.raw.as_ref() { crate::ErrorKind::PackSyncChanged => { - let mut queue = state.pack_sync_worker.queue.lock(); + let mut queue = state.synced_options.packs.queue.lock(); queue.instances.insert(instance_id.clone()); if !pending.is_empty() { queue @@ -152,7 +160,8 @@ fn start(state: Arc) { let retry_at = Instant::now() + Duration::from_secs((*retry_in_seconds).max(1)); { - let mut queue = state.pack_sync_worker.queue.lock(); + let mut queue = + state.synced_options.packs.queue.lock(); queue.instances.insert(instance_id.clone()); queue .retry_at @@ -168,7 +177,8 @@ fn start(state: Arc) { tokio::spawn(async move { tokio::time::sleep_until(retry_at).await; state - .pack_sync_worker + .synced_options + .packs .queue .lock() .retry_at @@ -208,7 +218,7 @@ impl<'a> Preparation<'a> { Self { state, metadata, - guard: Some(state.lock_synced_options().await), + guard: Some(state.synced_options.lock().await), } } @@ -218,15 +228,24 @@ impl<'a> Preparation<'a> { work: impl Future>, ) -> crate::Result { write_library(library, self.state).await?; - let revision = - self.state.pack_sync_worker.revision.load(Ordering::Acquire); + let revision = self + .state + .synced_options + .packs + .revision + .load(Ordering::Acquire); self.validate_metadata().await?; let before = fingerprint(self.metadata, self.state).await?; self.guard.take(); let result = work.await; - self.guard = Some(self.state.lock_synced_options().await); + self.guard = Some(self.state.synced_options.lock().await); self.validate_metadata().await?; - if self.state.pack_sync_worker.revision.load(Ordering::Acquire) + if self + .state + .synced_options + .packs + .revision + .load(Ordering::Acquire) != revision || fingerprint(self.metadata, self.state).await? != before { diff --git a/packages/app-lib/src/api/instance/synced_servers/modpack.rs b/packages/app-lib/src/api/instance/synced_servers/modpack.rs index 27858a16cf7..3579480a222 100644 --- a/packages/app-lib/src/api/instance/synced_servers/modpack.rs +++ b/packages/app-lib/src/api/instance/synced_servers/modpack.rs @@ -22,7 +22,7 @@ pub(crate) async fn capture_modpack_server_override( source_path: Option<&str>, ) -> crate::Result<()> { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let metadata = crate::state::get_instance(instance_id, &state.pool) .await? .ok_or_else(|| ErrorKind::InputError("Unknown instance".to_string()))?; diff --git a/packages/app-lib/src/api/instance/synced_servers/operations.rs b/packages/app-lib/src/api/instance/synced_servers/operations.rs index 1a4cbefb93d..e223a6f67d9 100644 --- a/packages/app-lib/src/api/instance/synced_servers/operations.rs +++ b/packages/app-lib/src/api/instance/synced_servers/operations.rs @@ -435,7 +435,7 @@ pub(crate) async fn ensure_managed_server( data: NbtCompound, state: &State, ) -> crate::Result<()> { - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let path = instance_dir(metadata, state).join(SERVERS_FILE); let syncing = participating(metadata, state).await?; let records = list_server_records_locked(metadata, state).await?; @@ -527,7 +527,7 @@ pub(crate) async fn add_user_server( mut data: NbtCompound, state: &State, ) -> crate::Result { - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let id = Uuid::new_v4().to_string(); if participating(metadata, state).await? { let mut canonical = read_canonical(state).await?; @@ -604,7 +604,7 @@ pub(crate) async fn update_server_by_index( accept_textures: Option, state: &State, ) -> crate::Result<()> { - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; if participating(metadata, state).await? { let records = list_server_records_locked(metadata, state).await?; let record = records @@ -668,7 +668,7 @@ pub(crate) async fn remove_server_by_index( index: usize, state: &State, ) -> crate::Result<()> { - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; if participating(metadata, state).await? { let records = list_server_records_locked(metadata, state).await?; let record = records @@ -699,7 +699,7 @@ pub async fn desync_server( mode: DesyncServerMode, ) -> crate::Result<()> { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let metadata = crate::state::get_instance(instance_id, &state.pool) .await? .ok_or_else(|| ErrorKind::InputError("Unknown instance".to_string()))?; @@ -754,7 +754,7 @@ pub async fn desync_server( pub async fn list_synced_servers() -> crate::Result> { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; Ok(read_canonical(&state) .await? .into_iter() @@ -778,7 +778,7 @@ pub async fn list_synced_servers() -> crate::Result> { pub async fn update_synced_server(server: SyncedServer) -> crate::Result<()> { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let mut canonical = read_canonical(&state).await?; let target = canonical .iter_mut() @@ -798,7 +798,7 @@ pub async fn update_synced_server(server: SyncedServer) -> crate::Result<()> { pub async fn remove_synced_server(server_id: &str) -> crate::Result<()> { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let mut canonical = read_canonical(&state).await?; let previous_len = canonical.len(); canonical.retain(|server| server.id != server_id); diff --git a/packages/app-lib/src/api/settings.rs b/packages/app-lib/src/api/settings.rs index e3bd4dd47f6..ba35607008d 100644 --- a/packages/app-lib/src/api/settings.rs +++ b/packages/app-lib/src/api/settings.rs @@ -63,7 +63,7 @@ pub async fn store_verify_with_progress( #[tracing::instrument] pub async fn get() -> crate::Result { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let mut settings = Settings::get(&state.pool).await?; if crate::state::game_options_sync_is_enabled(&state.pool).await? && let Some(fullscreen) = @@ -80,7 +80,7 @@ pub async fn get() -> crate::Result { #[tracing::instrument] pub async fn set(settings: Settings) -> crate::Result<()> { let state = State::get().await?; - let _guard = state.lock_synced_options().await; + let _guard = state.synced_options.lock().await; let current_settings = Settings::get(&state.pool).await?; let game_options_sync_enabled = crate::state::game_options_sync_is_enabled(&state.pool).await?; diff --git a/packages/app-lib/src/state/instances/commands/remove_instance.rs b/packages/app-lib/src/state/instances/commands/remove_instance.rs index 53e78cd3b1c..a597b119745 100644 --- a/packages/app-lib/src/state/instances/commands/remove_instance.rs +++ b/packages/app-lib/src/state/instances/commands/remove_instance.rs @@ -11,7 +11,7 @@ pub(crate) async fn remove_instance( .ok_or_else(|| { crate::ErrorKind::InputError("Unknown instance".to_string()) })?; - let _synced_options_lock = state.lock_synced_options().await; + let _synced_options_lock = state.synced_options.lock().await; let _content_lock = state.instance_locks.lock_content(instance_id).await; let _store_lock = state.content_store.files_lock.lock().await; let _store_lease = state.content_store.lease().await; diff --git a/packages/app-lib/src/state/mod.rs b/packages/app-lib/src/state/mod.rs index 723cf562da2..2d884ff7f77 100644 --- a/packages/app-lib/src/state/mod.rs +++ b/packages/app-lib/src/state/mod.rs @@ -3,7 +3,7 @@ use crate::util::fetch::{FetchSemaphore, IoSemaphore}; use std::sync::atomic::AtomicBool; use std::sync::{Arc, LazyLock}; use tokio::sync::watch::Sender; -use tokio::sync::{Mutex, MutexGuard, OnceCell, Semaphore}; +use tokio::sync::{Mutex, OnceCell, Semaphore}; use crate::state::instances::watcher::FileWatcher; use sqlx::SqlitePool; @@ -62,6 +62,9 @@ pub(crate) use self::instance_locks::InstanceLocks; mod presence; pub use self::presence::Presence; +mod synced_options; +pub(crate) use self::synced_options::SyncedOptions; + mod tunnel; pub use self::tunnel::*; @@ -101,10 +104,7 @@ pub struct State { pub api_semaphore: FetchSemaphore, pub(crate) installs: Installs, pub(crate) instance_locks: InstanceLocks, - /// Serializes canonical synced-option mutations and checkpoint updates. - synced_options_lock: Mutex<()>, - pub(crate) game_locale_indexer: crate::api::instance::GameLocaleIndexer, - pub(crate) pack_sync_worker: crate::api::instance::PackSyncWorker, + pub(crate) synced_options: SyncedOptions, pub presence: Presence, @@ -125,10 +125,6 @@ pub struct State { } impl State { - pub(crate) async fn lock_synced_options(&self) -> MutexGuard<'_, ()> { - self.synced_options_lock.lock().await - } - pub async fn init(app_identifier: String) -> crate::Result<()> { let _startup = STATE_STARTUP_LOCK.lock().await; let result = LAUNCHER_STATE @@ -366,10 +362,7 @@ impl State { api_semaphore, installs: Installs::new(), instance_locks: InstanceLocks::default(), - synced_options_lock: Mutex::new(()), - game_locale_indexer: - crate::api::instance::GameLocaleIndexer::default(), - pack_sync_worker: crate::api::instance::PackSyncWorker::default(), + synced_options: SyncedOptions::default(), presence, process_manager, restart_after_pending_update: AtomicBool::new(false), diff --git a/packages/app-lib/src/state/synced_options.rs b/packages/app-lib/src/state/synced_options.rs new file mode 100644 index 00000000000..c8aa48cf43e --- /dev/null +++ b/packages/app-lib/src/state/synced_options.rs @@ -0,0 +1,17 @@ +use crate::api::instance::{GameLocaleIndexer, PackSyncWorker}; +use tokio::sync::{Mutex, MutexGuard}; + +/// Coordinates options syncing across instances. +#[derive(Default)] +pub(crate) struct SyncedOptions { + /// Serializes canonical synced-option mutations and checkpoint updates. + lock: Mutex<()>, + pub(crate) locales: GameLocaleIndexer, + pub(crate) packs: PackSyncWorker, +} + +impl SyncedOptions { + pub(crate) async fn lock(&self) -> MutexGuard<'_, ()> { + self.lock.lock().await + } +} From 928759cc909c09912427a9300d12d0223fa09a0c Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:57:26 -0400 Subject: [PATCH 33/38] refactor(app-lib): narrow state params to instance locks --- .../src/api/instance/screenshots/operations.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/app-lib/src/api/instance/screenshots/operations.rs b/packages/app-lib/src/api/instance/screenshots/operations.rs index ba68845af71..ec86b16fbc1 100644 --- a/packages/app-lib/src/api/instance/screenshots/operations.rs +++ b/packages/app-lib/src/api/instance/screenshots/operations.rs @@ -15,6 +15,7 @@ use super::reconciliation::{ use crate::State; use crate::event::InstancePayloadType; use crate::event::emit::emit_instance; +use crate::state::InstanceLocks; use crate::state::instances::adapters::sqlite::{ instance_rows::{self, InstanceScreenshotSource}, screenshot_rows, @@ -104,12 +105,12 @@ async fn list_source_screenshot_sets( } async fn lock_instance_screenshots<'a>( - state: &State, + instance_locks: &InstanceLocks, instance_ids: impl IntoIterator, ) -> Vec> { let mut locks = Vec::new(); for instance_id in instance_ids { - locks.push(state.instance_locks.lock_screenshots(instance_id).await); + locks.push(instance_locks.lock_screenshots(instance_id).await); } locks } @@ -123,7 +124,8 @@ pub async fn delete_screenshots(keys: &[ScreenshotKey]) -> crate::Result<()> { .collect::>(); instance_ids.sort_unstable(); instance_ids.dedup(); - let _locks = lock_instance_screenshots(&state, instance_ids).await; + let _locks = + lock_instance_screenshots(&state.instance_locks, instance_ids).await; for key in keys { io::remove_file(get_screenshot_path(key).await?).await?; @@ -228,7 +230,8 @@ pub async fn move_screenshots( .collect::>(); instance_ids.sort_unstable(); instance_ids.dedup(); - let _locks = lock_instance_screenshots(&state, instance_ids).await; + let _locks = + lock_instance_screenshots(&state.instance_locks, instance_ids).await; let target_source = instance_rows::get_instance_screenshot_source( target_instance_id, &state.pool, From dc5b6deace9e2faf69ce85349c007f3efe0ff3e8 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:57:27 -0400 Subject: [PATCH 34/38] refactor(app-lib): narrow state params to directory info --- .../api/instance/screenshots/operations.rs | 19 +++++++++++-------- .../instance/screenshots/reconciliation.rs | 9 +++++---- packages/app-lib/src/install/diagnostics.rs | 12 ++++++------ packages/app-lib/src/install/recovery.rs | 16 ++++++++-------- .../commands/apply_content_install.rs | 10 +++++----- 5 files changed, 35 insertions(+), 31 deletions(-) diff --git a/packages/app-lib/src/api/instance/screenshots/operations.rs b/packages/app-lib/src/api/instance/screenshots/operations.rs index ec86b16fbc1..a5c03cd4583 100644 --- a/packages/app-lib/src/api/instance/screenshots/operations.rs +++ b/packages/app-lib/src/api/instance/screenshots/operations.rs @@ -15,11 +15,11 @@ use super::reconciliation::{ use crate::State; use crate::event::InstancePayloadType; use crate::event::emit::emit_instance; -use crate::state::InstanceLocks; use crate::state::instances::adapters::sqlite::{ instance_rows::{self, InstanceScreenshotSource}, screenshot_rows, }; +use crate::state::{DirectoryInfo, InstanceLocks}; use crate::util::fetch::sha1_file_async; use crate::util::io::{self, IOError}; @@ -240,7 +240,8 @@ pub async fn move_screenshots( .ok_or_else(|| { crate::ErrorKind::InputError("Unknown target instance".to_string()) })?; - let target_dir = source_screenshots_dir(&state, &target_source).await?; + let target_dir = + source_screenshots_dir(&state.directories, &target_source).await?; io::create_dir_all(&target_dir).await?; ensure_directory_is_not_symlink(&target_dir).await?; @@ -301,7 +302,8 @@ pub async fn get_screenshot_path( .ok_or_else(|| { crate::ErrorKind::InputError("Unknown instance".to_string()) })?; - let screenshots_dir = source_screenshots_dir(&state, &source).await?; + let screenshots_dir = + source_screenshots_dir(&state.directories, &source).await?; let canonical_dir = tokio::fs::canonicalize(&screenshots_dir) .await .map_err(|error| IOError::with_path(error, &screenshots_dir))?; @@ -349,7 +351,7 @@ pub async fn save_edited_screenshot( })?; let _lock = state.instance_locks.lock_screenshots(&source.id).await; - let scanned = scan_source_screenshots(&state, &source).await?; + let scanned = scan_source_screenshots(&state.directories, &source).await?; let current = reconcile_source_screenshots(&state, &source, scanned).await?; let source_screenshot = current @@ -401,7 +403,8 @@ pub async fn save_edited_screenshot( .into()); } - let screenshots_dir = source_screenshots_dir(&state, &source).await?; + let screenshots_dir = + source_screenshots_dir(&state.directories, &source).await?; let (target_path, copy_group) = match mode { ScreenshotEditSaveMode::CreateCopy => ( available_target_path(&screenshots_dir, &source_row.file_name) @@ -437,7 +440,7 @@ pub async fn save_edited_screenshot( screenshot_rows::update_screenshot(&source_row, &mut tx).await?; tx.commit().await?; } - let scanned = scan_source_screenshots(&state, &source).await?; + let scanned = scan_source_screenshots(&state.directories, &source).await?; let reconciled = reconcile_source_screenshots(&state, &source, scanned).await?; let mut saved = reconciled @@ -514,10 +517,10 @@ pub async fn save_edited_screenshot( } pub(super) async fn source_screenshots_dir( - state: &State, + directories: &DirectoryInfo, source: &InstanceScreenshotSource, ) -> crate::Result { - let instance_dir = state.directories.instances_dir().join(&source.path); + let instance_dir = directories.instances_dir().join(&source.path); let canonical_instance_dir = tokio::fs::canonicalize(&instance_dir) .await diff --git a/packages/app-lib/src/api/instance/screenshots/reconciliation.rs b/packages/app-lib/src/api/instance/screenshots/reconciliation.rs index 1fe19e5ea6c..434c5bad70b 100644 --- a/packages/app-lib/src/api/instance/screenshots/reconciliation.rs +++ b/packages/app-lib/src/api/instance/screenshots/reconciliation.rs @@ -3,6 +3,7 @@ use super::operations::{ source_screenshots_dir, }; use crate::State; +use crate::state::DirectoryInfo; use crate::state::instances::adapters::sqlite::{ instance_rows::{self, InstanceScreenshotSource}, screenshot_rows::{self, ScreenshotRow}, @@ -80,7 +81,7 @@ pub(super) async fn list_source_screenshots( source: InstanceScreenshotSource, ) -> crate::Result> { let _lock = state.instance_locks.lock_screenshots(&source.id).await; - let scanned = scan_source_screenshots(state, &source).await?; + let scanned = scan_source_screenshots(&state.directories, &source).await?; reconcile_source_screenshots(state, &source, scanned).await } @@ -100,10 +101,10 @@ pub(crate) async fn reconcile_screenshots( } pub(super) async fn scan_source_screenshots( - state: &State, + directories: &DirectoryInfo, source: &InstanceScreenshotSource, ) -> crate::Result> { - let instance_dir = state.directories.instances_dir().join(&source.path); + let instance_dir = directories.instances_dir().join(&source.path); if !tokio::fs::try_exists(&instance_dir) .await .map_err(|error| IOError::with_path(error, &instance_dir))? @@ -111,7 +112,7 @@ pub(super) async fn scan_source_screenshots( return Ok(Vec::new()); } - let screenshots_dir = source_screenshots_dir(state, source).await?; + let screenshots_dir = source_screenshots_dir(directories, source).await?; tokio::task::spawn_blocking(move || scan_screenshots_dir(&screenshots_dir)) .await? } diff --git a/packages/app-lib/src/install/diagnostics.rs b/packages/app-lib/src/install/diagnostics.rs index 776340415f5..d8f79de0c83 100644 --- a/packages/app-lib/src/install/diagnostics.rs +++ b/packages/app-lib/src/install/diagnostics.rs @@ -4,7 +4,7 @@ use super::model::{ InstallPhaseDetails, InstallPhaseId, InstallProgress, }; use super::store; -use crate::state::{ModrinthCredentials, State}; +use crate::state::{DirectoryInfo, ModrinthCredentials, State}; use regex::{Captures, Regex}; use sqlx::Row; use std::fmt::Write as _; @@ -47,7 +47,7 @@ pub async fn build_job_support_details( write_content_summary(&mut details, &job.state.events); write_errors(&mut details, &snapshot); write_raw_snapshot(&mut details, &snapshot); - write_latest_log(&mut details, state).await; + write_latest_log(&mut details, &state.directories).await; censor_support_text(details, state).await } @@ -411,10 +411,10 @@ fn write_raw_snapshot(details: &mut String, snapshot: &InstallJobSnapshot) { } } -async fn write_latest_log(details: &mut String, state: &State) { +async fn write_latest_log(details: &mut String, directories: &DirectoryInfo) { let _ = writeln!(details); let _ = writeln!(details, "Latest launcher log excerpt"); - match latest_launcher_log_tail(state).await { + match latest_launcher_log_tail(directories).await { Ok(Some((path, output))) => { let _ = writeln!(details, "File: {}", path.display()); details.push_str(&output); @@ -540,9 +540,9 @@ fn json_string(value: &T) -> String { } async fn latest_launcher_log_tail( - state: &State, + directories: &DirectoryInfo, ) -> crate::Result> { - let Some(logs_dir) = state.directories.launcher_logs_dir() else { + let Some(logs_dir) = directories.launcher_logs_dir() else { return Ok(None); }; diff --git a/packages/app-lib/src/install/recovery.rs b/packages/app-lib/src/install/recovery.rs index 8e9c309bc6c..848993c47a4 100644 --- a/packages/app-lib/src/install/recovery.rs +++ b/packages/app-lib/src/install/recovery.rs @@ -11,7 +11,7 @@ use crate::state::content_store::ContentStore; use crate::state::instances::adapters::sqlite::{content_rows, instance_rows}; use crate::state::{ ContentEntry, ContentSetRemoteRef, ContentSetRemoteRefType, - ContentSetSyncProvider, ContentSetSyncState, InstanceFile, + ContentSetSyncProvider, ContentSetSyncState, DirectoryInfo, InstanceFile, InstanceMetadata, State, }; use async_walkdir::WalkDir; @@ -56,7 +56,7 @@ pub(super) async fn prepare_instance_update_backup( )); } let owner = job_id.to_string(); - let staging_dir = instance_update_backup_dir(job_id, state); + let staging_dir = instance_update_backup_dir(job_id, &state.directories); if tokio::fs::try_exists(&staging_dir).await? { crate::util::io::remove_dir_all(&staging_dir).await?; } @@ -165,11 +165,11 @@ pub(super) async fn prepare_instance_update_backup( Ok(staging_dir) } -fn instance_update_backup_dir(job_id: Uuid, state: &State) -> PathBuf { - state - .directories - .install_backups_dir() - .join(job_id.to_string()) +fn instance_update_backup_dir( + job_id: Uuid, + directories: &DirectoryInfo, +) -> PathBuf { + directories.install_backups_dir().join(job_id.to_string()) } async fn recover_unrecorded_instance_update_backup( @@ -186,7 +186,7 @@ async fn recover_unrecorded_instance_update_backup( { return Ok(()); } - let staging_dir = instance_update_backup_dir(job.id, state); + let staging_dir = instance_update_backup_dir(job.id, &state.directories); if !tokio::fs::try_exists(&staging_dir).await? { return Ok(()); } diff --git a/packages/app-lib/src/state/instances/commands/apply_content_install.rs b/packages/app-lib/src/state/instances/commands/apply_content_install.rs index 89b21b28071..a628a4e7085 100644 --- a/packages/app-lib/src/state/instances/commands/apply_content_install.rs +++ b/packages/app-lib/src/state/instances/commands/apply_content_install.rs @@ -3,8 +3,8 @@ use crate::state::instances::{ adapters::sqlite::{content_rows, instance_rows}, }; use crate::state::{ - CacheBehaviour, CachedEntry, Dependency, DependencyType, ModLoader, - ProjectType, State, Version, + CacheBehaviour, CachedEntry, Dependency, DependencyType, DirectoryInfo, + ModLoader, ProjectType, State, Version, }; use crate::util::fetch::{self, DownloadMeta, DownloadReason}; use crate::util::io; @@ -701,7 +701,7 @@ pub(crate) async fn rename_project_companion_file( let project_type = ProjectType::get_from_parent_folder(new_project_path); if project_type == Some(ProjectType::ShaderPack) { let scope = resolve_content_scope(instance_id, None, state).await?; - let base = instance_full_path(state, &scope.instance); + let base = instance_full_path(&state.directories, &scope.instance); let old_txt_path = base.join(format!( "{}.txt", @@ -757,10 +757,10 @@ pub(crate) async fn list_project_files( } pub(crate) fn instance_full_path( - state: &State, + directories: &DirectoryInfo, instance: &Instance, ) -> PathBuf { - state.directories.instances_dir().join(&instance.path) + directories.instances_dir().join(&instance.path) } pub(super) async fn upsert_entry_for_file( From 0507142185725e5f51b1a4105d375e4366f30ed4 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:57:35 -0400 Subject: [PATCH 35/38] refactor(app-lib): narrow state params to pool --- packages/app-lib/src/install/diagnostics.rs | 10 ++-- packages/app-lib/src/install/recovery.rs | 7 ++- packages/app-lib/src/install/runner.rs | 57 ++++++++++++------- .../app-lib/src/install/shared_instance.rs | 33 +++++------ .../commands/apply_content_install.rs | 18 +++--- .../commands/apply_content_update.rs | 8 +-- .../instances/commands/create_instance.rs | 9 +-- 7 files changed, 80 insertions(+), 62 deletions(-) diff --git a/packages/app-lib/src/install/diagnostics.rs b/packages/app-lib/src/install/diagnostics.rs index d8f79de0c83..642a6840b82 100644 --- a/packages/app-lib/src/install/diagnostics.rs +++ b/packages/app-lib/src/install/diagnostics.rs @@ -6,7 +6,7 @@ use super::model::{ use super::store; use crate::state::{DirectoryInfo, ModrinthCredentials, State}; use regex::{Captures, Regex}; -use sqlx::Row; +use sqlx::{Row, SqlitePool}; use std::fmt::Write as _; use std::io::{Read, Seek, SeekFrom}; use std::net::{Ipv4Addr, Ipv6Addr}; @@ -49,7 +49,7 @@ pub async fn build_job_support_details( write_raw_snapshot(&mut details, &snapshot); write_latest_log(&mut details, &state.directories).await; - censor_support_text(details, state).await + censor_support_text(details, &state.pool).await } fn result_summary( @@ -600,9 +600,9 @@ fn read_file_tail(path: &Path, max_bytes: u64) -> crate::Result { async fn censor_support_text( mut text: String, - state: &State, + pool: &SqlitePool, ) -> crate::Result { - for credentials in ModrinthCredentials::get_all(&state.pool).await? { + for credentials in ModrinthCredentials::get_all(pool).await? { replace_nonempty( &mut text, &credentials.session, @@ -610,7 +610,7 @@ async fn censor_support_text( ); } - for token in minecraft_tokens(&state.pool).await? { + for token in minecraft_tokens(pool).await? { replace_nonempty(&mut text, &token, "{MINECRAFT_TOKEN}"); } diff --git a/packages/app-lib/src/install/recovery.rs b/packages/app-lib/src/install/recovery.rs index 848993c47a4..979ddb59d61 100644 --- a/packages/app-lib/src/install/recovery.rs +++ b/packages/app-lib/src/install/recovery.rs @@ -18,6 +18,7 @@ use async_walkdir::WalkDir; use chrono::Utc; use futures::StreamExt; use serde::{Deserialize, Serialize}; +use sqlx::SqlitePool; use std::future::Future; use std::path::{Path, PathBuf}; use uuid::Uuid; @@ -330,17 +331,17 @@ async fn restore_instance_update( &snapshot.copied_file_ids, ) .await?; - restore_instance_metadata(&rollback.instance, state).await?; + restore_instance_metadata(&rollback.instance, &state.pool).await?; Ok(()) } async fn restore_instance_metadata( metadata: &InstanceMetadata, - state: &State, + pool: &SqlitePool, ) -> crate::Result<()> { let content_set_id = metadata.applied_content_set.id.as_str(); - let mut tx = state.pool.begin_with("BEGIN IMMEDIATE").await?; + let mut tx = pool.begin_with("BEGIN IMMEDIATE").await?; instance_rows::update_instance(&metadata.instance, &mut tx).await?; content_rows::update_content_set(&metadata.applied_content_set, &mut tx) .await?; diff --git a/packages/app-lib/src/install/runner.rs b/packages/app-lib/src/install/runner.rs index 13c84719858..c26ee7b6777 100644 --- a/packages/app-lib/src/install/runner.rs +++ b/packages/app-lib/src/install/runner.rs @@ -683,7 +683,8 @@ async fn prepare_initial_instance_inner( ); let instance_id = metadata.instance.id; set_instance_id(job_state, instance_id.clone()); - attach_pending_shared_instance(&instance_id, &data, state).await?; + attach_pending_shared_instance(&instance_id, &data, &state.pool) + .await?; emit_instance(&instance_id, InstancePayloadType::Edited).await?; } InstallRequest::ImportInstance { @@ -738,7 +739,8 @@ async fn prepare_initial_instance_inner( } | InstallRequest::BulkUpdateContent { instance_id, .. } | InstallRequest::UpdateSharedInstance { instance_id, .. } => { - prepare_existing_rollback(job_state, state, &instance_id).await?; + prepare_existing_rollback(job_state, &state.pool, &instance_id) + .await?; } } @@ -1059,7 +1061,7 @@ async fn run_request_inner( update_progress( job_id, job_state, - state, + &state.pool, InstallPhaseId::PreparingInstance, InstallPhaseDetails::Instance { name: name.clone() }, ) @@ -1067,7 +1069,7 @@ async fn run_request_inner( update_progress( job_id, job_state, - state, + &state.pool, InstallPhaseId::DownloadingMinecraft, InstallPhaseDetails::Minecraft { game_version, @@ -1110,7 +1112,7 @@ async fn run_request_inner( update_progress( job_id, job_state, - state, + &state.pool, InstallPhaseId::ResolvingPack, modpack_details(&location), ) @@ -1144,8 +1146,12 @@ async fn run_request_inner( ) .await?; - finalize_shared_instance_attachment(&instance_id, &data, state) - .await?; + finalize_shared_instance_attachment( + &instance_id, + &data, + &state.pool, + ) + .await?; emit_instance(&instance_id, InstancePayloadType::Edited).await?; Ok(Some(instance_id)) @@ -1164,7 +1170,7 @@ async fn run_request_inner( update_progress( job_id, job_state, - state, + &state.pool, InstallPhaseId::PreparingInstance, InstallPhaseDetails::Import { launcher_type, @@ -1196,7 +1202,7 @@ async fn run_request_inner( update_progress( job_id, job_state, - state, + &state.pool, InstallPhaseId::PreparingInstance, InstallPhaseDetails::Empty, ) @@ -1238,12 +1244,13 @@ async fn run_request_inner( Ok(Some(instance_id)) } InstallRequest::InstallExistingInstance { instance_id, force } => { - prepare_existing_rollback(job_state, state, &instance_id).await?; + prepare_existing_rollback(job_state, &state.pool, &instance_id) + .await?; lock_instance(&instance_id, state).await?; update_progress( job_id, job_state, - state, + &state.pool, InstallPhaseId::DownloadingMinecraft, InstallPhaseDetails::Empty, ) @@ -1275,7 +1282,8 @@ async fn run_request_inner( location, post_install_edit, } => { - prepare_existing_rollback(job_state, state, &instance_id).await?; + prepare_existing_rollback(job_state, &state.pool, &instance_id) + .await?; lock_instance(&instance_id, state).await?; prepare_update_backup(job_id, job_state, state).await?; crate::api::instance::prepare_instance_update(&instance_id).await?; @@ -1313,7 +1321,7 @@ async fn run_request_inner( update_progress( job_id, job_state, - state, + &state.pool, InstallPhaseId::ResolvingPack, InstallPhaseDetails::Empty, ) @@ -1340,7 +1348,8 @@ async fn run_request_inner( Ok(Some(instance_id)) } InstallRequest::UpdateSharedInstance { instance_id, data } => { - prepare_existing_rollback(job_state, state, &instance_id).await?; + prepare_existing_rollback(job_state, &state.pool, &instance_id) + .await?; lock_instance(&instance_id, state).await?; prepare_update_backup(job_id, job_state, state).await?; let disabled_project_ids = @@ -1359,8 +1368,12 @@ async fn run_request_inner( state, ) .await?; - finalize_shared_instance_attachment(&instance_id, &data, state) - .await?; + finalize_shared_instance_attachment( + &instance_id, + &data, + &state.pool, + ) + .await?; emit_instance(&instance_id, InstancePayloadType::Edited).await?; Ok(Some(instance_id)) } @@ -1645,14 +1658,14 @@ async fn install_pack_inner( async fn prepare_existing_rollback( job_state: &mut InstallJobState, - state: &State, + pool: &SqlitePool, instance_id: &str, ) -> crate::Result<()> { if job_state.rollback.is_some() { return Ok(()); } - let instance = crate::state::get_instance(instance_id, &state.pool) + let instance = crate::state::get_instance(instance_id, pool) .await? .ok_or_else(|| { crate::ErrorKind::InputError(format!( @@ -1750,13 +1763,13 @@ async fn lock_instance(instance_id: &str, state: &State) -> crate::Result<()> { pub(super) async fn update_progress( job_id: Uuid, job_state: &mut InstallJobState, - state: &State, + pool: &SqlitePool, phase: InstallPhaseId, details: InstallPhaseDetails, ) -> crate::Result<()> { super::control::checkpoint(job_id).await?; job_state.set_progress(phase, None, details); - let record = store::update_state(job_id, job_state, &state.pool).await?; + let record = store::update_state(job_id, job_state, pool).await?; emit_install_job(&record.snapshot()).await?; Ok(()) } @@ -1764,7 +1777,7 @@ pub(super) async fn update_progress( pub(super) async fn update_content_progress( job_id: Uuid, job_state: &mut InstallJobState, - state: &State, + pool: &SqlitePool, current: u64, total: u64, ) -> crate::Result<()> { @@ -1776,7 +1789,7 @@ pub(super) async fn update_content_progress( secondary: None, }); job_state.progress.details = InstallPhaseDetails::Empty; - let record = store::update_state(job_id, job_state, &state.pool).await?; + let record = store::update_state(job_id, job_state, pool).await?; emit_install_job(&record.snapshot()).await?; Ok(()) } diff --git a/packages/app-lib/src/install/shared_instance.rs b/packages/app-lib/src/install/shared_instance.rs index 1b9574fd4ce..b7d7f7ae88f 100644 --- a/packages/app-lib/src/install/shared_instance.rs +++ b/packages/app-lib/src/install/shared_instance.rs @@ -21,6 +21,7 @@ use crate::state::{ use crate::util::fetch::{DownloadReason, REQWEST_CLIENT}; use futures::StreamExt; use path_util::SafeRelativeUtf8UnixPathBuf; +use sqlx::SqlitePool; use std::collections::{HashMap, HashSet}; use std::future::Future; use std::path::PathBuf; @@ -32,7 +33,7 @@ const MAX_SHARED_INSTANCE_INITIAL_BUFFER_SIZE: u64 = 8 * 1024 * 1024; pub(super) async fn attach_pending_shared_instance( instance_id: &str, data: &SharedInstanceInstallData, - state: &State, + pool: &SqlitePool, ) -> crate::Result<()> { crate::state::attach_shared_instance( instance_id, @@ -47,7 +48,7 @@ pub(super) async fn attach_pending_shared_instance( applied_version: None, latest_version: Some(data.version), }, - &state.pool, + pool, ) .await } @@ -55,7 +56,7 @@ pub(super) async fn attach_pending_shared_instance( pub(super) async fn finalize_shared_instance_attachment( instance_id: &str, data: &SharedInstanceInstallData, - state: &State, + pool: &SqlitePool, ) -> crate::Result<()> { crate::state::attach_shared_instance( instance_id, @@ -70,7 +71,7 @@ pub(super) async fn finalize_shared_instance_attachment( applied_version: Some(data.version), latest_version: Some(data.version), }, - &state.pool, + pool, ) .await } @@ -296,7 +297,7 @@ async fn apply_shared_instance_update_inner( update_progress( job_id, job_state, - state, + &state.pool, InstallPhaseId::PreparingInstance, InstallPhaseDetails::Instance { name: data.name.clone(), @@ -309,7 +310,7 @@ async fn apply_shared_instance_update_inner( update_content_progress( job_id, job_state, - state, + &state.pool, 0, content_change_count, ) @@ -359,7 +360,7 @@ async fn apply_shared_instance_update_inner( update_content_progress( job_id, job_state, - state, + &state.pool, completed_content_changes, content_change_count, ) @@ -380,7 +381,7 @@ async fn apply_shared_instance_update_inner( update_content_progress( job_id, job_state, - state, + &state.pool, completed_content_changes, content_change_count, ) @@ -398,7 +399,7 @@ async fn apply_shared_instance_update_inner( update_content_progress( job_id, job_state, - state, + &state.pool, completed_content_changes, content_change_count, ) @@ -416,7 +417,7 @@ async fn apply_shared_instance_update_inner( update_content_progress( job_id, job_state, - state, + &state.pool, completed_content_changes, content_change_count, ) @@ -642,7 +643,7 @@ async fn apply_shared_instance_content_inner( update_progress( job_id, job_state, - state, + &state.pool, InstallPhaseId::PreparingInstance, InstallPhaseDetails::Instance { name: data.name.clone(), @@ -663,7 +664,7 @@ async fn apply_shared_instance_content_inner( update_progress( job_id, job_state, - state, + &state.pool, InstallPhaseId::ResolvingPack, modpack_details(&location), ) @@ -695,7 +696,7 @@ async fn apply_shared_instance_content_inner( update_progress( job_id, job_state, - state, + &state.pool, InstallPhaseId::DownloadingMinecraft, InstallPhaseDetails::Minecraft { game_version: data.game_version.clone(), @@ -731,7 +732,7 @@ async fn apply_shared_instance_content_inner( update_content_progress( job_id, job_state, - state, + &state.pool, 0, content_change_count, ) @@ -751,7 +752,7 @@ async fn apply_shared_instance_content_inner( update_content_progress( job_id, job_state, - state, + &state.pool, completed_content_changes, content_change_count, ) @@ -765,7 +766,7 @@ async fn apply_shared_instance_content_inner( update_content_progress( job_id, job_state, - state, + &state.pool, completed_content_changes, content_change_count, ) diff --git a/packages/app-lib/src/state/instances/commands/apply_content_install.rs b/packages/app-lib/src/state/instances/commands/apply_content_install.rs index a628a4e7085..d5e7f068d1e 100644 --- a/packages/app-lib/src/state/instances/commands/apply_content_install.rs +++ b/packages/app-lib/src/state/instances/commands/apply_content_install.rs @@ -15,6 +15,7 @@ use modrinth_content_management::{ ResolutionPreferences, ResolveContentPlan, ResolveContentRequest, ResolvedContent, }; +use sqlx::SqlitePool; use std::path::{Path, PathBuf}; use super::content_mutation::{ @@ -325,9 +326,9 @@ async fn add_resolved_content( pub(crate) async fn resolve_content_scope( instance_id: &str, content_set_id: Option<&str>, - state: &State, + pool: &SqlitePool, ) -> crate::Result { - let instance = instance_rows::get_instance_by_id(instance_id, &state.pool) + let instance = instance_rows::get_instance_by_id(instance_id, pool) .await? .ok_or_else(|| { crate::ErrorKind::InputError("Unknown instance".to_string()) @@ -395,7 +396,7 @@ pub(crate) async fn download_project_version_with_progress( state: &State, progress: Option<&mut fetch::FetchProgressFn<'_>>, ) -> crate::Result { - let scope = resolve_content_scope(instance_id, None, state).await?; + let scope = resolve_content_scope(instance_id, None, &state.pool).await?; let content_set = content_rows::get_content_set(&scope.content_set_id, &state.pool) .await? @@ -641,7 +642,7 @@ pub(crate) async fn content_source_kind_for_project_path( project_path: &str, state: &State, ) -> crate::Result> { - let scope = resolve_content_scope(instance_id, None, state).await?; + let scope = resolve_content_scope(instance_id, None, &state.pool).await?; let Some(file) = content_rows::get_instance_file_by_relative_path( &scope.instance.id, project_path, @@ -666,7 +667,7 @@ pub(crate) async fn is_project_locked( project_path: &str, state: &State, ) -> crate::Result { - let scope = resolve_content_scope(instance_id, None, state).await?; + let scope = resolve_content_scope(instance_id, None, &state.pool).await?; content_rows::is_instance_file_locked( &scope.instance.id, project_path, @@ -682,7 +683,7 @@ pub(crate) async fn set_project_locked( state: &State, ) -> crate::Result<()> { let _content_lock = state.instance_locks.lock_content(instance_id).await; - let scope = resolve_content_scope(instance_id, None, state).await?; + let scope = resolve_content_scope(instance_id, None, &state.pool).await?; content_rows::set_instance_file_locked( &scope.instance.id, project_path, @@ -700,7 +701,8 @@ pub(crate) async fn rename_project_companion_file( ) -> crate::Result<()> { let project_type = ProjectType::get_from_parent_folder(new_project_path); if project_type == Some(ProjectType::ShaderPack) { - let scope = resolve_content_scope(instance_id, None, state).await?; + let scope = + resolve_content_scope(instance_id, None, &state.pool).await?; let base = instance_full_path(&state.directories, &scope.instance); let old_txt_path = base.join(format!( @@ -732,7 +734,7 @@ pub(crate) async fn list_project_files( instance_id: &str, state: &State, ) -> crate::Result> { - let scope = resolve_content_scope(instance_id, None, state).await?; + let scope = resolve_content_scope(instance_id, None, &state.pool).await?; let entries = content_rows::get_content_entries(&scope.content_set_id, &state.pool) .await?; diff --git a/packages/app-lib/src/state/instances/commands/apply_content_update.rs b/packages/app-lib/src/state/instances/commands/apply_content_update.rs index 655de188f4b..1caf950f842 100644 --- a/packages/app-lib/src/state/instances/commands/apply_content_update.rs +++ b/packages/app-lib/src/state/instances/commands/apply_content_update.rs @@ -13,6 +13,7 @@ use crate::state::{ }; use crate::util::fetch::DownloadReason; use futures::stream::{self, StreamExt}; +use sqlx::SqlitePool; use std::cmp::Reverse; use std::collections::{HashMap, HashSet}; use std::future::Future; @@ -375,7 +376,7 @@ pub(crate) async fn plan_bulk_update( state: &State, ) -> crate::Result { let shared_instance_member = - is_shared_instance_member(instance_id, state).await?; + is_shared_instance_member(instance_id, &state.pool).await?; let updateable_paths = bulk_updateable_project_paths( instance_id, shared_instance_member, @@ -700,11 +701,10 @@ fn installed_project_from_row( async fn is_shared_instance_member( instance_id: &str, - state: &State, + pool: &SqlitePool, ) -> crate::Result { let Some(metadata) = - instance_rows::get_instance_metadata_by_id(instance_id, &state.pool) - .await? + instance_rows::get_instance_metadata_by_id(instance_id, pool).await? else { return Ok(false); }; diff --git a/packages/app-lib/src/state/instances/commands/create_instance.rs b/packages/app-lib/src/state/instances/commands/create_instance.rs index b95f4f53236..43c50277b55 100644 --- a/packages/app-lib/src/state/instances/commands/create_instance.rs +++ b/packages/app-lib/src/state/instances/commands/create_instance.rs @@ -12,6 +12,7 @@ use crate::util::fetch; use crate::util::io; use chrono::Utc; use serde::{Deserialize, Serialize}; +use sqlx::SqlitePool; use tracing::{info, trace}; use uuid::Uuid; @@ -166,7 +167,7 @@ async fn resolve_instance_path( let mut path = base_path.clone(); let mut full_path = state.directories.instances_dir().join(&path); - if path_available(&path, &full_path, state).await? { + if path_available(&path, &full_path, &state.pool).await? { return Ok((path, full_path)); } @@ -175,7 +176,7 @@ async fn resolve_instance_path( path = format!("{base_path} ({which})"); full_path = state.directories.instances_dir().join(&path); - if path_available(&path, &full_path, state).await? { + if path_available(&path, &full_path, &state.pool).await? { return Ok((path, full_path)); } @@ -186,13 +187,13 @@ async fn resolve_instance_path( async fn path_available( path: &str, full_path: &std::path::Path, - state: &State, + pool: &SqlitePool, ) -> crate::Result { if full_path.exists() { return Ok(false); } - Ok(instance_rows::get_instance_by_path(path, &state.pool) + Ok(instance_rows::get_instance_by_path(path, pool) .await? .is_none()) } From 4509776e22ff483d4ef936cb3f9170ee58e56a92 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:57:35 -0400 Subject: [PATCH 36/38] refactor(app-lib): narrow state params to api semaphore --- .../app-lib/src/state/content_store/adapters/downloads.rs | 4 ++-- .../app-lib/src/state/content_store/commands/migration.rs | 2 +- .../src/state/content_store/commands/verify_content.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/app-lib/src/state/content_store/adapters/downloads.rs b/packages/app-lib/src/state/content_store/adapters/downloads.rs index 54c30358262..c81b0119b41 100644 --- a/packages/app-lib/src/state/content_store/adapters/downloads.rs +++ b/packages/app-lib/src/state/content_store/adapters/downloads.rs @@ -28,7 +28,7 @@ pub(in crate::state::content_store) async fn download( pub(in crate::state::content_store) async fn repair_sources( pool: &SqlitePool, sha512: &str, - state: &crate::State, + api_semaphore: &FetchSemaphore, ) -> Vec { let version = fetch::fetch_json::( reqwest::Method::GET, @@ -39,7 +39,7 @@ pub(in crate::state::content_store) async fn repair_sources( None, None, Some("/v2/version_file/:hash"), - &state.api_semaphore, + api_semaphore, pool, ) .await; diff --git a/packages/app-lib/src/state/content_store/commands/migration.rs b/packages/app-lib/src/state/content_store/commands/migration.rs index dda2dadadbb..d3c859b93aa 100644 --- a/packages/app-lib/src/state/content_store/commands/migration.rs +++ b/packages/app-lib/src/state/content_store/commands/migration.rs @@ -276,7 +276,7 @@ async fn migrate_file_copy( sources = downloads::repair_sources( &state.pool, &stored.sha512, - state, + &state.api_semaphore, ) .await; } diff --git a/packages/app-lib/src/state/content_store/commands/verify_content.rs b/packages/app-lib/src/state/content_store/commands/verify_content.rs index 021c294c019..8e6c744b7b9 100644 --- a/packages/app-lib/src/state/content_store/commands/verify_content.rs +++ b/packages/app-lib/src/state/content_store/commands/verify_content.rs @@ -191,7 +191,7 @@ impl ContentStore { sources = downloads::repair_sources( &self.pool, &stored_file.sha512, - state, + &state.api_semaphore, ) .await; } From 4d91ac6397100fd7f4bb7986be508a25562b1246 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:57:35 -0400 Subject: [PATCH 37/38] refactor(app-lib): narrow state params to io semaphore --- packages/app-lib/src/install/recovery.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/app-lib/src/install/recovery.rs b/packages/app-lib/src/install/recovery.rs index 979ddb59d61..a62199c27c9 100644 --- a/packages/app-lib/src/install/recovery.rs +++ b/packages/app-lib/src/install/recovery.rs @@ -14,6 +14,7 @@ use crate::state::{ ContentSetSyncProvider, ContentSetSyncState, DirectoryInfo, InstanceFile, InstanceMetadata, State, }; +use crate::util::fetch::IoSemaphore; use async_walkdir::WalkDir; use chrono::Utc; use futures::StreamExt; @@ -141,7 +142,7 @@ pub(super) async fn prepare_instance_update_backup( &instance_path, &staging_dir.join(SHARED_INSTANCE_ROLLBACK_INSTANCE_DIR), &skipped, - state, + &state.io_semaphore, ) .await?; crate::util::io::write( @@ -311,7 +312,7 @@ async fn restore_instance_update( &backup_path, &instance_path, &std::collections::HashSet::new(), - state, + &state.io_semaphore, ) .await?; content_rows::restore_instance_content_snapshot( @@ -412,7 +413,7 @@ async fn copy_directory( source: &Path, target: &Path, skipped: &std::collections::HashSet, - state: &State, + io_semaphore: &IoSemaphore, ) -> crate::Result<()> { crate::util::io::create_dir_all(target).await?; let mut walker = WalkDir::new(source); @@ -437,12 +438,8 @@ async fn copy_directory( if file_type.is_dir() { crate::util::io::create_dir_all(&target_path).await?; } else if file_type.is_file() { - crate::util::fetch::copy( - &entry_path, - &target_path, - &state.io_semaphore, - ) - .await?; + crate::util::fetch::copy(&entry_path, &target_path, io_semaphore) + .await?; } else if file_type.is_symlink() { copy_symlink(&entry_path, &target_path).await?; } From 30a0933564e7dc5f28f37a08c797029b01394cb5 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:57:35 -0400 Subject: [PATCH 38/38] refactor(app-lib): narrow state params to content store --- .../app-lib/src/api/instance/synced_packs/reconciliation.rs | 2 +- packages/app-lib/src/api/pack/install_mrpack.rs | 3 ++- .../src/state/instances/commands/apply_content_install.rs | 2 +- packages/app-lib/src/util/fetch.rs | 5 +++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/app-lib/src/api/instance/synced_packs/reconciliation.rs b/packages/app-lib/src/api/instance/synced_packs/reconciliation.rs index cc3231eb3fe..88eeb4fb114 100644 --- a/packages/app-lib/src/api/instance/synced_packs/reconciliation.rs +++ b/packages/app-lib/src/api/instance/synced_packs/reconciliation.rs @@ -312,7 +312,7 @@ async fn prepare_pack( None, ) .await?; - downloaded.store_file(state).await? + downloaded.store_file(&state.content_store).await? } else { read_stored_file(pack, state).await? }; diff --git a/packages/app-lib/src/api/pack/install_mrpack.rs b/packages/app-lib/src/api/pack/install_mrpack.rs index 1aed0ec9573..0f2af6258bc 100644 --- a/packages/app-lib/src/api/pack/install_mrpack.rs +++ b/packages/app-lib/src/api/pack/install_mrpack.rs @@ -944,7 +944,8 @@ async fn install_zipped_mrpack_files_with_reporter_inner( project.hashes.get(&PackFileHash::Sha1).and_then( |hash| content_context.file_infos_by_hash.get(hash), ); - let stored_file = file.store_file(state).await?; + let stored_file = + file.store_file(&state.content_store).await?; content_context .reporter .preserve_failure_context( diff --git a/packages/app-lib/src/state/instances/commands/apply_content_install.rs b/packages/app-lib/src/state/instances/commands/apply_content_install.rs index d5e7f068d1e..dc1444e33dd 100644 --- a/packages/app-lib/src/state/instances/commands/apply_content_install.rs +++ b/packages/app-lib/src/state/instances/commands/apply_content_install.rs @@ -491,7 +491,7 @@ pub(crate) async fn add_downloaded_project_version_with_enabled( "Invalid project filename", )); } - let stored_file = downloaded.file.store_file(state).await?; + let stored_file = downloaded.file.store_file(&state.content_store).await?; install_stored_file( instance_id, InstallContent { diff --git a/packages/app-lib/src/util/fetch.rs b/packages/app-lib/src/util/fetch.rs index 7fe2b8ff9b8..1821b600bc2 100644 --- a/packages/app-lib/src/util/fetch.rs +++ b/packages/app-lib/src/util/fetch.rs @@ -2,6 +2,7 @@ use super::io::{self, IOError}; use crate::event::LoadingBarId; use crate::event::emit::emit_loading; +use crate::state::content_store::ContentStore; use crate::util::content_hash::{ContentHasher, temporary_file}; use crate::{ErrorKind, LabrinthError}; use bytes::Bytes; @@ -436,12 +437,12 @@ impl DownloadedFile { pub(crate) async fn store_file( &self, - state: &crate::State, + content_store: &ContentStore, ) -> crate::Result { match &self.path { DownloadedFilePath::Stored(stored_file) => Ok(stored_file.clone()), DownloadedFilePath::Temporary(_) => { - state.content_store.store_file(self.path()).await + content_store.store_file(self.path()).await } } }