diff --git a/packages/oneclient_app/src/events.rs b/packages/oneclient_app/src/events.rs index 1565cb25..777435f7 100644 --- a/packages/oneclient_app/src/events.rs +++ b/packages/oneclient_app/src/events.rs @@ -340,7 +340,9 @@ pub async fn start_launcher( station: RadioStation, events: oneclient_events::EventBus, ) -> Result<(), anyhow::Error> { - let state = crate::launcher::install(oneclient_core::LauncherState::new(events).await?); + let state = crate::launcher::install( + crate::launcher::off_ui(oneclient_core::LauncherState::new(events)).await?, + ); oneclient_net::status::start(state.services.requester.clone()); oneclient_polyplus::start(std::sync::Arc::clone(&state.auth)); diff --git a/packages/oneclient_app/src/hooks/actions.rs b/packages/oneclient_app/src/hooks/actions.rs index b50351ae..7bf05724 100644 --- a/packages/oneclient_app/src/hooks/actions.rs +++ b/packages/oneclient_app/src/hooks/actions.rs @@ -26,6 +26,7 @@ use oneclient_events::{Answer, Level}; use tokio::sync::mpsc; use crate::components::IconType; +use crate::launcher::off_ui; use crate::notifications::{ ClusterUpdateSummary, NotificationAction, NotificationSpec, OptionalModRef, OptionalModsGroup, OptionalModsOutcome, PackageUpdateGroup, PendingPrompt, @@ -548,8 +549,14 @@ impl Actions { spawn_forever(async move { let Ok(state) = launcher::state() else { return }; let events = state.services.events.clone(); - match oneclient_core::import_migration_game_dir(&state, source, &folder_name, target) - .await + match off_ui({ + let state = state.clone(); + async move { + oneclient_core::import_migration_game_dir(&state, source, &folder_name, target) + .await + } + }) + .await { Ok(()) => { events @@ -571,7 +578,12 @@ impl Actions { spawn_forever(async move { let Ok(state) = launcher::state() else { return }; let events = state.services.events.clone(); - match state.java.install_runtime_from(&vendor, major).await { + match off_ui({ + let state = state.clone(); + async move { state.java.install_runtime_from(&vendor, major).await } + }) + .await + { Ok(_) => { events.signal(oneclient_events::Signal::JavaChanged); invalidate_java_queries().await @@ -788,87 +800,95 @@ impl Actions { actions.close_optional_mods(OptionalModsOutcome::Launch); return; }; - let content = state.services.content(); let events = state.services.events.clone(); - let session = - oneclient_events::GroupedProgressSession::start(&events, "Adding optional mods"); - let opt_in = session.child( - "Enabling mods", - mods.len() as u64, - oneclient_events::TaskCategory::Packages, - ); - opt_in.set_phase(oneclient_events::TaskPhase::Installing); + let (installed, failed) = off_ui(async move { + let content = state.services.content(); + let session = oneclient_events::GroupedProgressSession::start( + &state.services.events, + "Adding optional mods", + ); + let opt_in = session.child( + "Enabling mods", + mods.len() as u64, + oneclient_events::TaskCategory::Packages, + ); + opt_in.set_phase(oneclient_events::TaskPhase::Installing); - let mut clusters: Vec = Vec::new(); - let mut enabled: Vec<(ClusterId, OptionalModRef)> = Vec::new(); - let mut failed = 0usize; - for (cluster_id, (bundle_name, package_id)) in &mods { - match oneclient_core::set_bundle_package_enabled( - *cluster_id, - bundle_name, - package_id, - true, - false, - &content, - ) - .await - { - Ok(()) => { - if !clusters.contains(cluster_id) { - clusters.push(*cluster_id); + let mut clusters: Vec = Vec::new(); + let mut enabled: Vec<(ClusterId, OptionalModRef)> = Vec::new(); + let mut failed = 0usize; + for (cluster_id, (bundle_name, package_id)) in &mods { + match oneclient_core::set_bundle_package_enabled( + *cluster_id, + bundle_name, + package_id, + true, + false, + &content, + ) + .await + { + Ok(()) => { + if !clusters.contains(cluster_id) { + clusters.push(*cluster_id); + } + enabled.push((*cluster_id, (bundle_name.clone(), package_id.clone()))); + } + Err(err) => { + failed += 1; + tracing::warn!( + cluster_id, + %bundle_name, + %package_id, + error = %err, + "failed to opt in to an optional mod" + ); } - enabled.push((*cluster_id, (bundle_name.clone(), package_id.clone()))); - } - Err(err) => { - failed += 1; - tracing::warn!( - cluster_id, - %bundle_name, - %package_id, - error = %err, - "failed to opt in to an optional mod" - ); } } - } - opt_in.finish(); + opt_in.finish(); - let mut installed = 0usize; - let mut applied: Vec = Vec::new(); - for cluster_id in &clusters { - match oneclient_core::apply_bundle_updates_with( - *cluster_id, - state.bundles.as_ref(), - &content, - Some(&session), - None, - ) - .await - { - Ok(result) => { - installed += result.additions_applied.len(); - failed += result.additions_failed.len(); - applied.push(*cluster_id); - } - Err(err) => { - tracing::warn!(cluster_id, error = %err, "failed to install optional mods"); + let mut installed = 0usize; + let mut applied: Vec = Vec::new(); + for cluster_id in &clusters { + match oneclient_core::apply_bundle_updates_with( + *cluster_id, + state.bundles.as_ref(), + &content, + Some(&session), + None, + ) + .await + { + Ok(result) => { + installed += result.additions_applied.len(); + failed += result.additions_failed.len(); + applied.push(*cluster_id); + } + Err(err) => { + tracing::warn!(cluster_id, error = %err, "failed to install optional mods"); + } } } - } - // An offer only counts as answered once its mod really was enabled - // and installed otherwise the click is lost and never comes back - enabled.retain(|(cluster_id, _)| applied.contains(cluster_id)); - for (cluster_id, package_ids) in group_by_cluster(&enabled) { - if let Err(err) = - oneclient_core::resolve_optional_mods(cluster_id, &package_ids, &content).await - { - tracing::warn!(cluster_id, error = %err, "failed to clear answered offers"); + // An offer only counts as answered once its mod really was enabled + // and installed otherwise the click is lost and never comes back + enabled.retain(|(cluster_id, _)| applied.contains(cluster_id)); + for (cluster_id, package_ids) in group_by_cluster(&enabled) { + if let Err(err) = + oneclient_core::resolve_optional_mods(cluster_id, &package_ids, &content) + .await + { + tracing::warn!(cluster_id, error = %err, "failed to clear answered offers"); + } } - } - session.finish(); + session.finish(); + (installed, failed) + }) + .await; + super::invalidate_cluster_queries().await; if installed > 0 { @@ -1030,14 +1050,16 @@ impl Actions { spawn_forever(async move { let Ok(state) = launcher::state() else { return }; let events = state.services.events.clone(); - match oneclient_content::packages::PackageStore::import_local_files( - &files, - cluster_id, - &state.services.content(), - ) - .await - { - Ok(report) => { + let imported = off_ui({ + let state = state.clone(); + async move { + let report = oneclient_content::packages::PackageStore::import_local_files( + &files, + cluster_id, + &state.services.content(), + ) + .await?; + let mut deferred = false; for row in &report.imported { let live = oneclient_content::packages::PackageStore::sync_live_content( @@ -1050,7 +1072,13 @@ impl Actions { deferred |= live == LiveSync::Deferred; } + Ok::<_, oneclient_content::ContentError>((report, deferred)) + } + }) + .await; + match imported { + Ok((report, deferred)) => { notify_import( &events, &report, @@ -1137,15 +1165,24 @@ impl Actions { return; }; - let install = match crate::install::install_package( - &state, - provider, - &project_id, - &version_id, - cluster_id, - world.clone(), - allow_flagged, - ) + let install = match off_ui({ + let state = state.clone(); + let project_id = project_id.clone(); + let version_id = version_id.clone(); + let world = world.clone(); + async move { + crate::install::install_package( + &state, + provider, + &project_id, + &version_id, + cluster_id, + world, + allow_flagged, + ) + .await + } + }) .await { Ok(install) => install, @@ -1299,13 +1336,20 @@ impl Actions { spawn_forever(async move { let Ok(state) = launcher::state() else { return }; let events = state.services.events.clone(); - match oneclient_core::install_bundle( - cluster_id, - &bundle_name, - skip_compatibility, - state.bundles.as_ref(), - &state.services.content(), - ) + match off_ui({ + let state = state.clone(); + let bundle_name = bundle_name.clone(); + async move { + oneclient_core::install_bundle( + cluster_id, + &bundle_name, + skip_compatibility, + state.bundles.as_ref(), + &state.services.content(), + ) + .await + } + }) .await { Ok(_) => { @@ -1328,12 +1372,18 @@ impl Actions { let actions = self.clone(); spawn_forever(async move { let Ok(state) = launcher::state() else { return }; - let result = match oneclient_core::apply_bundle_updates( - cluster_id, - state.bundles.as_ref(), - &state.services.content(), - None, - ) + let result = match off_ui({ + let state = state.clone(); + async move { + oneclient_core::apply_bundle_updates( + cluster_id, + state.bundles.as_ref(), + &state.services.content(), + None, + ) + .await + } + }) .await { Ok(result) => result, @@ -1388,14 +1438,26 @@ impl Actions { let mut station = self.station; spawn_forever(async move { - // The copy is on this thread's executor, so it can write the count - // straight into the channel the move screen reads - let result = oneclient_core::relocate::relocate(&state, &plan, |copied, total| { + let (progress, mut seen) = tokio::sync::watch::channel((0, 0)); + let mut copy = std::pin::pin!(off_ui(async move { + oneclient_core::relocate::relocate(&state, &plan, |copied, total| { + let _ = progress.send((copied, total)); + }) + .await + })); + + let mut show_progress = |(copied, total)| { let mut guard = station.write_channel(AppChannel::Relocation); guard.relocation.copied = copied; guard.relocation.total = total; - }) - .await; + }; + let result = loop { + tokio::select! { + result = &mut copy => break result, + Ok(()) = seen.changed() => show_progress(*seen.borrow_and_update()), + } + }; + show_progress(*seen.borrow()); if let Err(message) = &result { tracing::error!("moving the data folder failed: {message}"); @@ -1445,15 +1507,18 @@ impl Actions { .syncing_bundles = true; spawn_forever(async move { - if let Err(err) = state.bundles.sync(&state.services.content()).await { - tracing::error!("bundle catalog sync failed: {err:#}"); - } - if let Err(err) = oneclient_core::clusters::apply_remote_migrations(&state).await { - tracing::error!("cluster migrations failed: {err:#}"); - } - if let Err(err) = oneclient_core::clusters::ensure_from_bundles(&state).await { - tracing::error!("bundle cluster provisioning failed: {err:#}"); - } + off_ui(async move { + if let Err(err) = state.bundles.sync(&state.services.content()).await { + tracing::error!("bundle catalog sync failed: {err:#}"); + } + if let Err(err) = oneclient_core::clusters::apply_remote_migrations(&state).await { + tracing::error!("cluster migrations failed: {err:#}"); + } + if let Err(err) = oneclient_core::clusters::ensure_from_bundles(&state).await { + tracing::error!("bundle cluster provisioning failed: {err:#}"); + } + }) + .await; actions .station @@ -1488,22 +1553,28 @@ impl Actions { .launcher .syncing_bundles = true; - let synced = - match tokio::time::timeout(BUNDLE_SYNC_BUDGET, state.bundles.sync(&content)).await { - Ok(Ok(_)) => true, - Ok(Err(err)) => { - tracing::warn!( - cluster_id, - error = %err, - "bundle catalog sync failed, launching against the cached catalog" - ); - false - } - Err(_elapsed) => { - tracing::debug!(cluster_id, "bundle catalog sync exceeded its launch budget"); - false - } - }; + let sync = { + let bundles = state.bundles.clone(); + let content = content.clone(); + off_ui( + async move { tokio::time::timeout(BUNDLE_SYNC_BUDGET, bundles.sync(&content)).await }, + ) + }; + let synced = match sync.await { + Ok(Ok(_)) => true, + Ok(Err(err)) => { + tracing::warn!( + cluster_id, + error = %err, + "bundle catalog sync failed, launching against the cached catalog" + ); + false + } + Err(_elapsed) => { + tracing::debug!(cluster_id, "bundle catalog sync exceeded its launch budget"); + false + } + }; self.station .clone() @@ -1513,12 +1584,19 @@ impl Actions { // Bundle content is forced, so the update mode never gates it and the // modal never lists it - let result = match oneclient_core::apply_bundle_updates( - cluster_id, - state.bundles.as_ref(), - &content, - Some(Instant::now() + BUNDLE_APPLY_BUDGET), - ) + let deadline = Instant::now() + BUNDLE_APPLY_BUDGET; + let result = match off_ui({ + let state = state.clone(); + async move { + oneclient_core::apply_bundle_updates( + cluster_id, + state.bundles.as_ref(), + &state.services.content(), + Some(deadline), + ) + .await + } + }) .await { Ok(result) => result, @@ -1567,10 +1645,13 @@ impl Actions { // Bounded a hanging network would otherwise hold the launch for // reqwest's much longer timeouts - let check = match tokio::time::timeout( - UPDATE_CHECK_BUDGET, - oneclient_core::refresh_browser_package_updates(cluster_id, &content), - ) + let check = match off_ui(async move { + tokio::time::timeout( + UPDATE_CHECK_BUDGET, + oneclient_core::refresh_browser_package_updates(cluster_id, &content), + ) + .await + }) .await { Ok(Ok(check)) => check, @@ -1642,39 +1723,45 @@ impl Actions { state: &Arc, updates: &[oneclient_core::BrowserPackageUpdate], ) { - let content = state.services.content(); - let session = oneclient_events::GroupedProgressSession::start( - &state.services.events, - "Updating packages", - ); - session.expect( - oneclient_events::TaskCategory::Packages, - updates.len() as u64, - updates.len() as u64, - ); - - let mut applied = 0usize; - for update in updates { - let child = session.child( - update.display_name.clone(), - 1, - oneclient_events::TaskCategory::Packages, + let state = state.clone(); + let updates = updates.to_vec(); + let (applied, session_id) = off_ui(async move { + let content = state.services.content(); + let session = oneclient_events::GroupedProgressSession::start( + &state.services.events, + "Updating packages", + ); + session.expect( + oneclient_events::TaskCategory::Packages, + updates.len() as u64, + updates.len() as u64, ); - match oneclient_core::apply_browser_package_update(update, Some(&child), &content).await - { - Ok(_) => applied += 1, - // Not a reason to hold the game back the cache row survives - // so the next launch offers it again - Err(err) => tracing::warn!( - package = %update.display_name, - error = %err, - "automatic package update failed" - ), - } - child.finish(); - } - let session_id = session.detach(); + let mut applied = 0usize; + for update in &updates { + let child = session.child( + update.display_name.clone(), + 1, + oneclient_events::TaskCategory::Packages, + ); + match oneclient_core::apply_browser_package_update(update, Some(&child), &content) + .await + { + Ok(_) => applied += 1, + // Not a reason to hold the game back the cache row survives + // so the next launch offers it again + Err(err) => tracing::warn!( + package = %update.display_name, + error = %err, + "automatic package update failed" + ), + } + child.finish(); + } + + (applied, session.detach()) + }) + .await; let spec = (applied > 0).then(|| NotificationSpec { title: "Packages updated".to_string(), body: format!( @@ -1705,11 +1792,17 @@ impl Actions { state: &Arc, cluster_id: ClusterId, ) -> OptionalModsOutcome { - let pending = match oneclient_core::pending_optional_mods( - cluster_id, - state.bundles.as_ref(), - &state.services.content(), - ) + let pending = match off_ui({ + let state = state.clone(); + async move { + oneclient_core::pending_optional_mods( + cluster_id, + state.bundles.as_ref(), + &state.services.content(), + ) + .await + } + }) .await { Ok(pending) => pending, @@ -1762,28 +1855,33 @@ impl Actions { let actions = self.clone(); spawn_forever(async move { let Ok(state) = launcher::state() else { return }; - let events = state.services.events.clone(); - let session = oneclient_events::GroupedProgressSession::start( - &events, - format!("Updating {}", update.display_name), - ); - let child = session.child( - update.display_name.clone(), - 1, - oneclient_events::TaskCategory::Packages, - ); + let (result, session_id) = off_ui({ + let update = update.clone(); + async move { + let session = oneclient_events::GroupedProgressSession::start( + &state.services.events, + format!("Updating {}", update.display_name), + ); + let child = session.child( + update.display_name.clone(), + 1, + oneclient_events::TaskCategory::Packages, + ); - let result = oneclient_core::apply_browser_package_update( - &update, - Some(&child), - &state.services.content(), - ) + let result = oneclient_core::apply_browser_package_update( + &update, + Some(&child), + &state.services.content(), + ) + .await; + + child.finish(); + (result, session.detach()) + } + }) .await; - child.finish(); - let session_id = session.detach(); - let spec = match &result { Ok(_) => NotificationSpec { title: "Updated".to_string(), @@ -1912,16 +2010,19 @@ async fn launch(actions: &Actions, cluster_id: ClusterId) { return; } - if let Err(err) = oneclient_core::launch_cluster(&state, cluster_id, &account, true).await { - // A missing file is the one failure the launcher can fix itself and a - // path inside our metadata folder gives the user nothing to act on - if err.indicates_missing_files() { - repair_and_relaunch(&state, cluster_id, &account, err).await; - return; - } + off_ui(async move { + if let Err(err) = oneclient_core::launch_cluster(&state, cluster_id, &account, true).await { + // A missing file is the one failure the launcher can fix itself and a + // path inside our metadata folder gives the user nothing to act on + if err.indicates_missing_files() { + repair_and_relaunch(&state, cluster_id, &account, err).await; + return; + } - events.game_failed(cluster_id, format!("{err:#}")); - } + events.game_failed(cluster_id, format!("{err:#}")); + } + }) + .await; } /// Only ever one retry if the game still will not start after a full rehash and diff --git a/packages/oneclient_app/src/hooks/queries/bundles.rs b/packages/oneclient_app/src/hooks/queries/bundles.rs index 7e22aa88..911dc881 100644 --- a/packages/oneclient_app/src/hooks/queries/bundles.rs +++ b/packages/oneclient_app/src/hooks/queries/bundles.rs @@ -8,6 +8,8 @@ use oneclient_core::{ }; use oneclient_db::models::ClusterId; +use crate::launcher::off_ui; + #[derive(Clone, Debug)] pub struct ClusterBundles { pub cluster: Cluster, @@ -27,22 +29,25 @@ impl QueryCapability for OnboardingBundlesQuery { async fn run(&self, _keys: &Self::Keys) -> Result { let state = crate::launcher::state()?; - let clusters = state.clusters.list().await?; + off_ui(async move { + let clusters = state.clusters.list().await?; - let mut out = Vec::with_capacity(clusters.len()); - for cluster in clusters { - let archives = state - .bundles - .archives_for( - &state.services.content(), - &cluster.mc_version, - cluster.mc_loader, - ) - .await - .unwrap_or_default(); - out.push(ClusterBundles { cluster, archives }); - } - Ok(out) + let mut out = Vec::with_capacity(clusters.len()); + for cluster in clusters { + let archives = state + .bundles + .archives_for( + &state.services.content(), + &cluster.mc_version, + cluster.mc_loader, + ) + .await + .unwrap_or_default(); + out.push(ClusterBundles { cluster, archives }); + } + Ok(out) + }) + .await } } @@ -74,11 +79,15 @@ impl QueryCapability for BundlesWithStatusQuery { async fn run(&self, keys: &Self::Keys) -> Result { let _ = keys; let state = crate::launcher::state()?; - Ok(get_bundles_with_update_status( - self.cluster_id, - state.bundles.as_ref(), - &state.services.content(), - ) + let cluster_id = self.cluster_id; + Ok(off_ui(async move { + get_bundles_with_update_status( + cluster_id, + state.bundles.as_ref(), + &state.services.content(), + ) + .await + }) .await?) } } @@ -154,11 +163,15 @@ impl QueryCapability for BundleUpdatesQuery { async fn run(&self, keys: &Self::Keys) -> Result { let _ = keys; let state = crate::launcher::state()?; - Ok(oneclient_core::check_bundle_updates( - self.cluster_id, - state.bundles.as_ref(), - &state.services.content(), - ) + let cluster_id = self.cluster_id; + Ok(off_ui(async move { + oneclient_core::check_bundle_updates( + cluster_id, + state.bundles.as_ref(), + &state.services.content(), + ) + .await + }) .await?) } } diff --git a/packages/oneclient_app/src/hooks/queries/logs.rs b/packages/oneclient_app/src/hooks/queries/logs.rs index c1718ffe..75d29069 100644 --- a/packages/oneclient_app/src/hooks/queries/logs.rs +++ b/packages/oneclient_app/src/hooks/queries/logs.rs @@ -8,6 +8,8 @@ use oneclient_core::{ LauncherError, LogFileInfo, LogLevel, LogLine, MclogsUploadResponse, ReadOptions, }; +use crate::launcher::{off_ui, off_ui_blocking}; + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct ClusterLogsKeys { pub cluster_id: i64, @@ -24,7 +26,7 @@ impl QueryCapability for ClusterLogsQuery { async fn run(&self, keys: &Self::Keys) -> Result { let state = crate::launcher::state()?; let cluster = state.clusters.get(keys.cluster_id).await?; - Ok(oneclient_core::list_cluster_logs(&cluster)?) + Ok(off_ui_blocking(move || oneclient_core::list_cluster_logs(&cluster)).await?) } } @@ -48,15 +50,13 @@ impl QueryCapability for LogContentQuery { if keys.path.as_os_str().is_empty() { return Ok(Vec::new()); } - Ok(oneclient_core::read_log_at( - &keys.path, - &ReadOptions { - level_filter: keys.level, - search: keys.search.clone(), - max_lines: keys.max_lines, - }, - ) - .await?) + let path = keys.path.clone(); + let options = ReadOptions { + level_filter: keys.level, + search: keys.search.clone(), + max_lines: keys.max_lines, + }; + Ok(off_ui(async move { oneclient_core::read_log_at(&path, &options).await }).await?) } } @@ -109,7 +109,11 @@ impl MutationCapability for UploadLogMutation { async fn run(&self, keys: &Self::Keys) -> Result { let state = crate::launcher::state()?; - Ok(oneclient_core::upload_log_at(&state.services.requester, &keys.path).await?) + let path = keys.path.clone(); + Ok(off_ui( + async move { oneclient_core::upload_log_at(&state.services.requester, &path).await }, + ) + .await?) } } diff --git a/packages/oneclient_app/src/hooks/queries/mutations.rs b/packages/oneclient_app/src/hooks/queries/mutations.rs index 124e7f0a..cfdaca1b 100644 --- a/packages/oneclient_app/src/hooks/queries/mutations.rs +++ b/packages/oneclient_app/src/hooks/queries/mutations.rs @@ -205,7 +205,12 @@ impl MutationCapability for ClusterMutation { ClusterAction::VerifyFiles { cluster_id } => { // Reports its own outcome not the generic failure toast a // verify that finds nothing wrong is still a useful result - match oneclient_core::verify_cluster_files(&state, *cluster_id).await { + let (state, cluster_id) = (state.clone(), *cluster_id); + match crate::launcher::off_ui(async move { + oneclient_core::verify_cluster_files(&state, cluster_id).await + }) + .await + { Ok(report) => { let notify = services.events.notify("Verification complete"); let notify = notify.body(report.summary()); diff --git a/packages/oneclient_app/src/hooks/queries/packages.rs b/packages/oneclient_app/src/hooks/queries/packages.rs index b1c23cbd..37e22f96 100644 --- a/packages/oneclient_app/src/hooks/queries/packages.rs +++ b/packages/oneclient_app/src/hooks/queries/packages.rs @@ -217,17 +217,23 @@ impl QueryCapability for PackageVersionsQuery { async fn run(&self, keys: &Self::Keys) -> Result { let state = crate::launcher::state()?; - let provider = state.services.packages.get(keys.provider)?; - Ok(provider - .list_versions( - &keys.project_id, - keys.game_version.as_deref(), - keys.loader, - keys.page * VERSIONS_PAGE_SIZE, - VERSIONS_PAGE_SIZE, - &state.services.content(), - ) - .await?) + let keys = keys.clone(); + crate::launcher::off_ui(async move { + Ok(state + .services + .packages + .get(keys.provider)? + .list_versions( + &keys.project_id, + keys.game_version.as_deref(), + keys.loader, + keys.page * VERSIONS_PAGE_SIZE, + VERSIONS_PAGE_SIZE, + &state.services.content(), + ) + .await?) + }) + .await } } diff --git a/packages/oneclient_app/src/hooks/queries/screenshots.rs b/packages/oneclient_app/src/hooks/queries/screenshots.rs index 6a599a08..15830fe7 100644 --- a/packages/oneclient_app/src/hooks/queries/screenshots.rs +++ b/packages/oneclient_app/src/hooks/queries/screenshots.rs @@ -11,6 +11,8 @@ use notify::{Event, EventKind}; use oneclient_core::{LauncherError, ScreenshotInfo}; use tokio::sync::Semaphore; +use crate::launcher::off_ui_blocking; + static LOCAL_IMAGE_SEMAPHORE: OnceLock> = OnceLock::new(); #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -29,7 +31,7 @@ impl QueryCapability for ClusterScreenshotsQuery { async fn run(&self, keys: &Self::Keys) -> Result { let state = crate::launcher::state()?; let cluster = state.clusters.get(keys.cluster_id).await?; - Ok(oneclient_core::list_cluster_screenshots(&cluster)?) + Ok(off_ui_blocking(move || oneclient_core::list_cluster_screenshots(&cluster)).await?) } } @@ -133,7 +135,10 @@ impl MutationCapability for ScreenshotActionMutation { async fn run(&self, keys: &Self::Keys) -> Result { match keys { - ScreenshotAction::Delete { path } => Ok(oneclient_core::delete_screenshot(path)?), + ScreenshotAction::Delete { path } => { + let path = path.clone(); + Ok(off_ui_blocking(move || oneclient_core::delete_screenshot(&path)).await?) + } } } diff --git a/packages/oneclient_app/src/hooks/queries/storage.rs b/packages/oneclient_app/src/hooks/queries/storage.rs index 658294dd..4dad13c6 100644 --- a/packages/oneclient_app/src/hooks/queries/storage.rs +++ b/packages/oneclient_app/src/hooks/queries/storage.rs @@ -5,6 +5,8 @@ use freya::query::{ use_mutation, use_query, }; use oneclient_core::LauncherError; + +use crate::launcher::off_ui; use oneclient_core::relocate::{Leftovers, discard_leftovers, leftovers}; use oneclient_core::storage::{StorageReport, storage_report}; @@ -21,7 +23,7 @@ impl QueryCapability for StorageReportQuery { async fn run(&self, _keys: &Self::Keys) -> Result { let state = crate::launcher::state()?; - storage_report(&state).await + off_ui(async move { storage_report(&state).await }).await } } @@ -55,19 +57,23 @@ impl MutationCapability for StorageActionMutation { async fn run(&self, keys: &Self::Keys) -> Result { let state = crate::launcher::state().map_err(|e| e.to_string())?; + let keys = *keys; - match keys { - StorageAction::CleanUnreferencedCache => { - oneclient_core::storage::clean_unreferenced_cache(&state) - .await - .map(|_| ()) + off_ui(async move { + match keys { + StorageAction::CleanUnreferencedCache => { + oneclient_core::storage::clean_unreferenced_cache(&state) + .await + .map(|_| ()) + } + StorageAction::CleanLegacyClusterContent => { + oneclient_core::storage::clean_cluster_leftovers(&state) + .await + .map(|_| ()) + } } - StorageAction::CleanLegacyClusterContent => { - oneclient_core::storage::clean_cluster_leftovers(&state) - .await - .map(|_| ()) - } - } + }) + .await .map_err(|e| e.to_string()) } @@ -97,7 +103,7 @@ impl QueryCapability for LeftoversQuery { async fn run(&self, _keys: &Self::Keys) -> Result { let state = crate::launcher::state()?; - Ok(leftovers(&state).await) + Ok(off_ui(async move { leftovers(&state).await }).await) } } @@ -126,7 +132,7 @@ impl MutationCapability for DiscardLeftoversMutation { async fn run(&self, _keys: &Self::Keys) -> Result { let state = crate::launcher::state().map_err(|err| err.to_string())?; - discard_leftovers(&state).await + off_ui(async move { discard_leftovers(&state).await }).await } async fn on_settled(&self, _keys: &Self::Keys, result: &Result) { diff --git a/packages/oneclient_app/src/hooks/queries/versions.rs b/packages/oneclient_app/src/hooks/queries/versions.rs index d4ff81cf..31d6bd4c 100644 --- a/packages/oneclient_app/src/hooks/queries/versions.rs +++ b/packages/oneclient_app/src/hooks/queries/versions.rs @@ -2,6 +2,8 @@ use freya::query::{Query, QueryCapability, QueryStateData, UseQuery, use_query}; use oneclient_common::domain::GameLoader; use oneclient_core::{LauncherError, VersionMetadata}; +use crate::launcher::off_ui; + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct VersionsMetadataQuery; @@ -15,18 +17,21 @@ impl QueryCapability for VersionsMetadataQuery { async fn run(&self, _keys: &Self::Keys) -> Result { let state = crate::launcher::state()?; - let metadata = state - .versions - .metadata(&state.services.requester.config().meta_url_base) - .await; - if !metadata.is_empty() { - return Ok(metadata); - } - state.versions.sync(&state.services).await?; - Ok(state - .versions - .metadata(&state.services.requester.config().meta_url_base) - .await) + off_ui(async move { + let metadata = state + .versions + .metadata(&state.services.requester.config().meta_url_base) + .await; + if !metadata.is_empty() { + return Ok(metadata); + } + state.versions.sync(&state.services).await?; + Ok(state + .versions + .metadata(&state.services.requester.config().meta_url_base) + .await) + }) + .await } } @@ -61,13 +66,17 @@ impl QueryCapability for LoaderVersionsQuery { async fn run(&self, keys: &Self::Keys) -> Result { let state = crate::launcher::state()?; - let mut metadata = state.metadata.lock().await; - Ok(oneclient_core::get_loader_versions( - &mut metadata, - &state.services.mc(), - &keys.mc_version, - keys.loader, - ) + let keys = keys.clone(); + Ok(off_ui(async move { + let mut metadata = state.metadata.lock().await; + oneclient_core::get_loader_versions( + &mut metadata, + &state.services.mc(), + &keys.mc_version, + keys.loader, + ) + .await + }) .await?) } } diff --git a/packages/oneclient_app/src/launcher.rs b/packages/oneclient_app/src/launcher.rs index 8b7bcb4e..98cd8915 100644 --- a/packages/oneclient_app/src/launcher.rs +++ b/packages/oneclient_app/src/launcher.rs @@ -20,3 +20,17 @@ pub fn install(state: Arc) -> Arc { pub fn state() -> LauncherResult> { LAUNCHER.get().cloned().ok_or(LauncherError::NotInitialized) } + +pub async fn off_ui(work: impl Future + Send + 'static) -> T { + match tokio::spawn(work).await { + Ok(value) => value, + Err(err) => std::panic::resume_unwind(err.into_panic()), + } +} + +pub async fn off_ui_blocking(work: impl FnOnce() -> T + Send + 'static) -> T { + match tokio::task::spawn_blocking(work).await { + Ok(value) => value, + Err(err) => std::panic::resume_unwind(err.into_panic()), + } +} diff --git a/packages/oneclient_app/src/microsoft_java.rs b/packages/oneclient_app/src/microsoft_java.rs index 1545ac8c..2b0142c2 100644 --- a/packages/oneclient_app/src/microsoft_java.rs +++ b/packages/oneclient_app/src/microsoft_java.rs @@ -12,6 +12,7 @@ use tokio::sync::mpsc::UnboundedSender; use crate::hooks::Actions; use crate::launcher; +use crate::launcher::off_ui; /// Front-ends match on these pub const MICROSOFT_JAVA_CHOICE_INSTALL: &str = "java.microsoft.install"; @@ -97,49 +98,58 @@ pub async fn offer_for_pinned_cluster(actions: &Actions, cluster_id: ClusterId) return; }; - if opted_out(&state) { - return; - } + let offer = off_ui({ + let state = state.clone(); + async move { + if opted_out(&state) { + return None; + } - let Some(pinned) = pinned_runtime(&state, cluster_id).await else { + let pinned = pinned_runtime(&state, cluster_id).await?; + + if pinned.vendor == JavaVendor::Microsoft { + return None; + } + + let major = match oneclient_core::required_java_major(&state, cluster_id).await { + Ok(Some(major)) => major, + Ok(None) => { + tracing::info!(cluster_id, "the cluster's manifest names no Java version"); + return None; + } + Err(err) => { + tracing::warn!( + cluster_id, + "could not read the cluster's Java version: {err:#}" + ); + return None; + } + }; + + let installed = match state + .java + .has_vendor_runtime(&JavaVendor::Microsoft, Some(major)) + .await + { + Ok(installed) => installed, + Err(err) => { + tracing::warn!("could not check for a Microsoft Java runtime: {err:#}"); + false + } + }; + + if !installed && !publishes(&state, major).await { + return None; + } + + Some((major, installed)) + } + }) + .await; + let Some((major, installed)) = offer else { return; }; - if pinned.vendor == JavaVendor::Microsoft { - return; - } - - let major = match oneclient_core::required_java_major(&state, cluster_id).await { - Ok(Some(major)) => major, - Ok(None) => { - tracing::info!(cluster_id, "the cluster's manifest names no Java version"); - return; - } - Err(err) => { - tracing::warn!( - cluster_id, - "could not read the cluster's Java version: {err:#}" - ); - return; - } - }; - - let installed = match state - .java - .has_vendor_runtime(&JavaVendor::Microsoft, Some(major)) - .await - { - Ok(installed) => installed, - Err(err) => { - tracing::warn!("could not check for a Microsoft Java runtime: {err:#}"); - false - } - }; - - if !installed && !publishes(&state, major).await { - return; - } - if !claim_ask(cluster_id) { return; } diff --git a/packages/oneclient_app/src/recovery.rs b/packages/oneclient_app/src/recovery.rs index 04b4c844..b977dcdf 100644 --- a/packages/oneclient_app/src/recovery.rs +++ b/packages/oneclient_app/src/recovery.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use freya::prelude::spawn_forever; use crate::hooks::Actions; +use crate::launcher::off_ui_blocking; pub fn database_path() -> Option { oneclient_common::paths::database_file().ok() @@ -29,19 +30,25 @@ pub fn restore_latest(actions: &Actions) { return; }; - match oneclient_db::backup::restore(&path, &snapshot) { - Ok(_) => retry(actions), - Err(err) => set_error(actions, format!("Couldn't restore the snapshot: {err}")), - } + let actions = actions.clone(); + spawn_forever(async move { + match off_ui_blocking(move || oneclient_db::backup::restore(&path, &snapshot)).await { + Ok(_) => retry(&actions), + Err(err) => set_error(&actions, format!("Couldn't restore the snapshot: {err}")), + } + }); } pub fn reset(actions: &Actions) { let Some(path) = database_path() else { return }; - match oneclient_db::backup::reset(&path) { - Ok(_) => retry(actions), - Err(err) => set_error(actions, format!("Couldn't reset the database: {err}")), - } + let actions = actions.clone(); + spawn_forever(async move { + match off_ui_blocking(move || oneclient_db::backup::reset(&path)).await { + Ok(_) => retry(&actions), + Err(err) => set_error(&actions, format!("Couldn't reset the database: {err}")), + } + }); } fn retry(actions: &Actions) { diff --git a/packages/oneclient_app/src/view/app/settings/launcher.rs b/packages/oneclient_app/src/view/app/settings/launcher.rs index ec689bfd..d6bca2ba 100644 --- a/packages/oneclient_app/src/view/app/settings/launcher.rs +++ b/packages/oneclient_app/src/view/app/settings/launcher.rs @@ -319,7 +319,7 @@ fn plan( checking.set(true); error.set(None); - match plan_move(picked.as_deref()).await { + match crate::launcher::off_ui(async move { plan_move(picked.as_deref()).await }).await { Ok(planned) => pending.set(Some(planned)), Err(message) => error.set(Some(message)), } diff --git a/packages/oneclient_content/src/bundles/updates.rs b/packages/oneclient_content/src/bundles/updates.rs index dab3a56c..a99c437d 100644 --- a/packages/oneclient_content/src/bundles/updates.rs +++ b/packages/oneclient_content/src/bundles/updates.rs @@ -898,8 +898,8 @@ async fn addition_eligible_bundles( candidate_keys_by_bundle: &HashMap>, overrides: &[ClusterBundleOverrideRow], ) -> ContentResult> { - let (live_managed_keys, _) = - installed_bundle_keys(ctx, all_linked.iter().filter(|item| item.enabled)).await?; + let live: Vec<_> = all_linked.iter().filter(|item| item.enabled).collect(); + let (live_managed_keys, _) = installed_bundle_keys(ctx, live).await?; let mut eligible: HashSet = bundle_packages .iter()