fix: move (almost) all IO off the UI thread (#886)

This commit is contained in:
LynithDev 2026-09-29 19:03:25 +02:00 • committed by GitHub
parent 1574d1afb1
commit bdb9780794
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 521 additions and 339 deletions

View file

@ -340,7 +340,9 @@ pub async fn start_launcher(
station: RadioStation<AppState, AppChannel>, station: RadioStation<AppState, AppChannel>,
events: oneclient_events::EventBus, events: oneclient_events::EventBus,
) -> Result<(), anyhow::Error> { ) -> 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_net::status::start(state.services.requester.clone());
oneclient_polyplus::start(std::sync::Arc::clone(&state.auth)); oneclient_polyplus::start(std::sync::Arc::clone(&state.auth));

View file

@ -26,6 +26,7 @@ use oneclient_events::{Answer, Level};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::components::IconType; use crate::components::IconType;
use crate::launcher::off_ui;
use crate::notifications::{ use crate::notifications::{
ClusterUpdateSummary, NotificationAction, NotificationSpec, OptionalModRef, OptionalModsGroup, ClusterUpdateSummary, NotificationAction, NotificationSpec, OptionalModRef, OptionalModsGroup,
OptionalModsOutcome, PackageUpdateGroup, PendingPrompt, OptionalModsOutcome, PackageUpdateGroup, PendingPrompt,
@ -548,8 +549,14 @@ impl Actions {
spawn_forever(async move { spawn_forever(async move {
let Ok(state) = launcher::state() else { return }; let Ok(state) = launcher::state() else { return };
let events = state.services.events.clone(); let events = state.services.events.clone();
match oneclient_core::import_migration_game_dir(&state, source, &folder_name, target) match off_ui({
.await let state = state.clone();
async move {
oneclient_core::import_migration_game_dir(&state, source, &folder_name, target)
.await
}
})
.await
{ {
Ok(()) => { Ok(()) => {
events events
@ -571,7 +578,12 @@ impl Actions {
spawn_forever(async move { spawn_forever(async move {
let Ok(state) = launcher::state() else { return }; let Ok(state) = launcher::state() else { return };
let events = state.services.events.clone(); 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(_) => { Ok(_) => {
events.signal(oneclient_events::Signal::JavaChanged); events.signal(oneclient_events::Signal::JavaChanged);
invalidate_java_queries().await invalidate_java_queries().await
@ -788,87 +800,95 @@ impl Actions {
actions.close_optional_mods(OptionalModsOutcome::Launch); actions.close_optional_mods(OptionalModsOutcome::Launch);
return; return;
}; };
let content = state.services.content();
let events = state.services.events.clone(); let events = state.services.events.clone();
let session = let (installed, failed) = off_ui(async move {
oneclient_events::GroupedProgressSession::start(&events, "Adding optional mods"); let content = state.services.content();
let opt_in = session.child( let session = oneclient_events::GroupedProgressSession::start(
"Enabling mods", &state.services.events,
mods.len() as u64, "Adding optional mods",
oneclient_events::TaskCategory::Packages, );
); let opt_in = session.child(
opt_in.set_phase(oneclient_events::TaskPhase::Installing); "Enabling mods",
mods.len() as u64,
oneclient_events::TaskCategory::Packages,
);
opt_in.set_phase(oneclient_events::TaskPhase::Installing);
let mut clusters: Vec<ClusterId> = Vec::new(); let mut clusters: Vec<ClusterId> = Vec::new();
let mut enabled: Vec<(ClusterId, OptionalModRef)> = Vec::new(); let mut enabled: Vec<(ClusterId, OptionalModRef)> = Vec::new();
let mut failed = 0usize; let mut failed = 0usize;
for (cluster_id, (bundle_name, package_id)) in &mods { for (cluster_id, (bundle_name, package_id)) in &mods {
match oneclient_core::set_bundle_package_enabled( match oneclient_core::set_bundle_package_enabled(
*cluster_id, *cluster_id,
bundle_name, bundle_name,
package_id, package_id,
true, true,
false, false,
&content, &content,
) )
.await .await
{ {
Ok(()) => { Ok(()) => {
if !clusters.contains(cluster_id) { if !clusters.contains(cluster_id) {
clusters.push(*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 installed = 0usize;
let mut applied: Vec<ClusterId> = Vec::new(); let mut applied: Vec<ClusterId> = Vec::new();
for cluster_id in &clusters { for cluster_id in &clusters {
match oneclient_core::apply_bundle_updates_with( match oneclient_core::apply_bundle_updates_with(
*cluster_id, *cluster_id,
state.bundles.as_ref(), state.bundles.as_ref(),
&content, &content,
Some(&session), Some(&session),
None, None,
) )
.await .await
{ {
Ok(result) => { Ok(result) => {
installed += result.additions_applied.len(); installed += result.additions_applied.len();
failed += result.additions_failed.len(); failed += result.additions_failed.len();
applied.push(*cluster_id); applied.push(*cluster_id);
} }
Err(err) => { Err(err) => {
tracing::warn!(cluster_id, error = %err, "failed to install optional mods"); tracing::warn!(cluster_id, error = %err, "failed to install optional mods");
}
} }
} }
}
// An offer only counts as answered once its mod really was enabled // An offer only counts as answered once its mod really was enabled
// and installed otherwise the click is lost and never comes back // and installed otherwise the click is lost and never comes back
enabled.retain(|(cluster_id, _)| applied.contains(cluster_id)); enabled.retain(|(cluster_id, _)| applied.contains(cluster_id));
for (cluster_id, package_ids) in group_by_cluster(&enabled) { for (cluster_id, package_ids) in group_by_cluster(&enabled) {
if let Err(err) = if let Err(err) =
oneclient_core::resolve_optional_mods(cluster_id, &package_ids, &content).await oneclient_core::resolve_optional_mods(cluster_id, &package_ids, &content)
{ .await
tracing::warn!(cluster_id, error = %err, "failed to clear answered offers"); {
tracing::warn!(cluster_id, error = %err, "failed to clear answered offers");
}
} }
}
session.finish(); session.finish();
(installed, failed)
})
.await;
super::invalidate_cluster_queries().await; super::invalidate_cluster_queries().await;
if installed > 0 { if installed > 0 {
@ -1030,14 +1050,16 @@ impl Actions {
spawn_forever(async move { spawn_forever(async move {
let Ok(state) = launcher::state() else { return }; let Ok(state) = launcher::state() else { return };
let events = state.services.events.clone(); let events = state.services.events.clone();
match oneclient_content::packages::PackageStore::import_local_files( let imported = off_ui({
&files, let state = state.clone();
cluster_id, async move {
&state.services.content(), let report = oneclient_content::packages::PackageStore::import_local_files(
) &files,
.await cluster_id,
{ &state.services.content(),
Ok(report) => { )
.await?;
let mut deferred = false; let mut deferred = false;
for row in &report.imported { for row in &report.imported {
let live = oneclient_content::packages::PackageStore::sync_live_content( let live = oneclient_content::packages::PackageStore::sync_live_content(
@ -1050,7 +1072,13 @@ impl Actions {
deferred |= live == LiveSync::Deferred; deferred |= live == LiveSync::Deferred;
} }
Ok::<_, oneclient_content::ContentError>((report, deferred))
}
})
.await;
match imported {
Ok((report, deferred)) => {
notify_import( notify_import(
&events, &events,
&report, &report,
@ -1137,15 +1165,24 @@ impl Actions {
return; return;
}; };
let install = match crate::install::install_package( let install = match off_ui({
&state, let state = state.clone();
provider, let project_id = project_id.clone();
&project_id, let version_id = version_id.clone();
&version_id, let world = world.clone();
cluster_id, async move {
world.clone(), crate::install::install_package(
allow_flagged, &state,
) provider,
&project_id,
&version_id,
cluster_id,
world,
allow_flagged,
)
.await
}
})
.await .await
{ {
Ok(install) => install, Ok(install) => install,
@ -1299,13 +1336,20 @@ impl Actions {
spawn_forever(async move { spawn_forever(async move {
let Ok(state) = launcher::state() else { return }; let Ok(state) = launcher::state() else { return };
let events = state.services.events.clone(); let events = state.services.events.clone();
match oneclient_core::install_bundle( match off_ui({
cluster_id, let state = state.clone();
&bundle_name, let bundle_name = bundle_name.clone();
skip_compatibility, async move {
state.bundles.as_ref(), oneclient_core::install_bundle(
&state.services.content(), cluster_id,
) &bundle_name,
skip_compatibility,
state.bundles.as_ref(),
&state.services.content(),
)
.await
}
})
.await .await
{ {
Ok(_) => { Ok(_) => {
@ -1328,12 +1372,18 @@ impl Actions {
let actions = self.clone(); let actions = self.clone();
spawn_forever(async move { spawn_forever(async move {
let Ok(state) = launcher::state() else { return }; let Ok(state) = launcher::state() else { return };
let result = match oneclient_core::apply_bundle_updates( let result = match off_ui({
cluster_id, let state = state.clone();
state.bundles.as_ref(), async move {
&state.services.content(), oneclient_core::apply_bundle_updates(
None, cluster_id,
) state.bundles.as_ref(),
&state.services.content(),
None,
)
.await
}
})
.await .await
{ {
Ok(result) => result, Ok(result) => result,
@ -1388,14 +1438,26 @@ impl Actions {
let mut station = self.station; let mut station = self.station;
spawn_forever(async move { spawn_forever(async move {
// The copy is on this thread's executor, so it can write the count let (progress, mut seen) = tokio::sync::watch::channel((0, 0));
// straight into the channel the move screen reads let mut copy = std::pin::pin!(off_ui(async move {
let result = oneclient_core::relocate::relocate(&state, &plan, |copied, total| { 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); let mut guard = station.write_channel(AppChannel::Relocation);
guard.relocation.copied = copied; guard.relocation.copied = copied;
guard.relocation.total = total; 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 { if let Err(message) = &result {
tracing::error!("moving the data folder failed: {message}"); tracing::error!("moving the data folder failed: {message}");
@ -1445,15 +1507,18 @@ impl Actions {
.syncing_bundles = true; .syncing_bundles = true;
spawn_forever(async move { spawn_forever(async move {
if let Err(err) = state.bundles.sync(&state.services.content()).await { off_ui(async move {
tracing::error!("bundle catalog sync failed: {err:#}"); 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::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:#}"); if let Err(err) = oneclient_core::clusters::ensure_from_bundles(&state).await {
} tracing::error!("bundle cluster provisioning failed: {err:#}");
}
})
.await;
actions actions
.station .station
@ -1488,22 +1553,28 @@ impl Actions {
.launcher .launcher
.syncing_bundles = true; .syncing_bundles = true;
let synced = let sync = {
match tokio::time::timeout(BUNDLE_SYNC_BUDGET, state.bundles.sync(&content)).await { let bundles = state.bundles.clone();
Ok(Ok(_)) => true, let content = content.clone();
Ok(Err(err)) => { off_ui(
tracing::warn!( async move { tokio::time::timeout(BUNDLE_SYNC_BUDGET, bundles.sync(&content)).await },
cluster_id, )
error = %err, };
"bundle catalog sync failed, launching against the cached catalog" let synced = match sync.await {
); Ok(Ok(_)) => true,
false Ok(Err(err)) => {
} tracing::warn!(
Err(_elapsed) => { cluster_id,
tracing::debug!(cluster_id, "bundle catalog sync exceeded its launch budget"); error = %err,
false "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 self.station
.clone() .clone()
@ -1513,12 +1584,19 @@ impl Actions {
// Bundle content is forced, so the update mode never gates it and the // Bundle content is forced, so the update mode never gates it and the
// modal never lists it // modal never lists it
let result = match oneclient_core::apply_bundle_updates( let deadline = Instant::now() + BUNDLE_APPLY_BUDGET;
cluster_id, let result = match off_ui({
state.bundles.as_ref(), let state = state.clone();
&content, async move {
Some(Instant::now() + BUNDLE_APPLY_BUDGET), oneclient_core::apply_bundle_updates(
) cluster_id,
state.bundles.as_ref(),
&state.services.content(),
Some(deadline),
)
.await
}
})
.await .await
{ {
Ok(result) => result, Ok(result) => result,
@ -1567,10 +1645,13 @@ impl Actions {
// Bounded a hanging network would otherwise hold the launch for // Bounded a hanging network would otherwise hold the launch for
// reqwest's much longer timeouts // reqwest's much longer timeouts
let check = match tokio::time::timeout( let check = match off_ui(async move {
UPDATE_CHECK_BUDGET, tokio::time::timeout(
oneclient_core::refresh_browser_package_updates(cluster_id, &content), UPDATE_CHECK_BUDGET,
) oneclient_core::refresh_browser_package_updates(cluster_id, &content),
)
.await
})
.await .await
{ {
Ok(Ok(check)) => check, Ok(Ok(check)) => check,
@ -1642,39 +1723,45 @@ impl Actions {
state: &Arc<oneclient_core::LauncherState>, state: &Arc<oneclient_core::LauncherState>,
updates: &[oneclient_core::BrowserPackageUpdate], updates: &[oneclient_core::BrowserPackageUpdate],
) { ) {
let content = state.services.content(); let state = state.clone();
let session = oneclient_events::GroupedProgressSession::start( let updates = updates.to_vec();
&state.services.events, let (applied, session_id) = off_ui(async move {
"Updating packages", let content = state.services.content();
); let session = oneclient_events::GroupedProgressSession::start(
session.expect( &state.services.events,
oneclient_events::TaskCategory::Packages, "Updating packages",
updates.len() as u64, );
updates.len() as u64, session.expect(
); oneclient_events::TaskCategory::Packages,
updates.len() as u64,
let mut applied = 0usize; updates.len() as u64,
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();
}
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 { let spec = (applied > 0).then(|| NotificationSpec {
title: "Packages updated".to_string(), title: "Packages updated".to_string(),
body: format!( body: format!(
@ -1705,11 +1792,17 @@ impl Actions {
state: &Arc<oneclient_core::LauncherState>, state: &Arc<oneclient_core::LauncherState>,
cluster_id: ClusterId, cluster_id: ClusterId,
) -> OptionalModsOutcome { ) -> OptionalModsOutcome {
let pending = match oneclient_core::pending_optional_mods( let pending = match off_ui({
cluster_id, let state = state.clone();
state.bundles.as_ref(), async move {
&state.services.content(), oneclient_core::pending_optional_mods(
) cluster_id,
state.bundles.as_ref(),
&state.services.content(),
)
.await
}
})
.await .await
{ {
Ok(pending) => pending, Ok(pending) => pending,
@ -1762,28 +1855,33 @@ impl Actions {
let actions = self.clone(); let actions = self.clone();
spawn_forever(async move { spawn_forever(async move {
let Ok(state) = launcher::state() else { return }; let Ok(state) = launcher::state() else { return };
let events = state.services.events.clone();
let session = oneclient_events::GroupedProgressSession::start( let (result, session_id) = off_ui({
&events, let update = update.clone();
format!("Updating {}", update.display_name), async move {
); let session = oneclient_events::GroupedProgressSession::start(
let child = session.child( &state.services.events,
update.display_name.clone(), format!("Updating {}", update.display_name),
1, );
oneclient_events::TaskCategory::Packages, let child = session.child(
); update.display_name.clone(),
1,
oneclient_events::TaskCategory::Packages,
);
let result = oneclient_core::apply_browser_package_update( let result = oneclient_core::apply_browser_package_update(
&update, &update,
Some(&child), Some(&child),
&state.services.content(), &state.services.content(),
) )
.await;
child.finish();
(result, session.detach())
}
})
.await; .await;
child.finish();
let session_id = session.detach();
let spec = match &result { let spec = match &result {
Ok(_) => NotificationSpec { Ok(_) => NotificationSpec {
title: "Updated".to_string(), title: "Updated".to_string(),
@ -1912,16 +2010,19 @@ async fn launch(actions: &Actions, cluster_id: ClusterId) {
return; return;
} }
if let Err(err) = oneclient_core::launch_cluster(&state, cluster_id, &account, true).await { off_ui(async move {
// A missing file is the one failure the launcher can fix itself and a if let Err(err) = oneclient_core::launch_cluster(&state, cluster_id, &account, true).await {
// path inside our metadata folder gives the user nothing to act on // A missing file is the one failure the launcher can fix itself and a
if err.indicates_missing_files() { // path inside our metadata folder gives the user nothing to act on
repair_and_relaunch(&state, cluster_id, &account, err).await; if err.indicates_missing_files() {
return; 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 /// Only ever one retry if the game still will not start after a full rehash and

View file

@ -8,6 +8,8 @@ use oneclient_core::{
}; };
use oneclient_db::models::ClusterId; use oneclient_db::models::ClusterId;
use crate::launcher::off_ui;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ClusterBundles { pub struct ClusterBundles {
pub cluster: Cluster, pub cluster: Cluster,
@ -27,22 +29,25 @@ impl QueryCapability for OnboardingBundlesQuery {
async fn run(&self, _keys: &Self::Keys) -> Result<Self::Ok, Self::Err> { async fn run(&self, _keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
let state = crate::launcher::state()?; 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()); let mut out = Vec::with_capacity(clusters.len());
for cluster in clusters { for cluster in clusters {
let archives = state let archives = state
.bundles .bundles
.archives_for( .archives_for(
&state.services.content(), &state.services.content(),
&cluster.mc_version, &cluster.mc_version,
cluster.mc_loader, cluster.mc_loader,
) )
.await .await
.unwrap_or_default(); .unwrap_or_default();
out.push(ClusterBundles { cluster, archives }); out.push(ClusterBundles { cluster, archives });
} }
Ok(out) Ok(out)
})
.await
} }
} }
@ -74,11 +79,15 @@ impl QueryCapability for BundlesWithStatusQuery {
async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> { async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
let _ = keys; let _ = keys;
let state = crate::launcher::state()?; let state = crate::launcher::state()?;
Ok(get_bundles_with_update_status( let cluster_id = self.cluster_id;
self.cluster_id, Ok(off_ui(async move {
state.bundles.as_ref(), get_bundles_with_update_status(
&state.services.content(), cluster_id,
) state.bundles.as_ref(),
&state.services.content(),
)
.await
})
.await?) .await?)
} }
} }
@ -154,11 +163,15 @@ impl QueryCapability for BundleUpdatesQuery {
async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> { async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
let _ = keys; let _ = keys;
let state = crate::launcher::state()?; let state = crate::launcher::state()?;
Ok(oneclient_core::check_bundle_updates( let cluster_id = self.cluster_id;
self.cluster_id, Ok(off_ui(async move {
state.bundles.as_ref(), oneclient_core::check_bundle_updates(
&state.services.content(), cluster_id,
) state.bundles.as_ref(),
&state.services.content(),
)
.await
})
.await?) .await?)
} }
} }

View file

@ -8,6 +8,8 @@ use oneclient_core::{
LauncherError, LogFileInfo, LogLevel, LogLine, MclogsUploadResponse, ReadOptions, LauncherError, LogFileInfo, LogLevel, LogLine, MclogsUploadResponse, ReadOptions,
}; };
use crate::launcher::{off_ui, off_ui_blocking};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ClusterLogsKeys { pub struct ClusterLogsKeys {
pub cluster_id: i64, pub cluster_id: i64,
@ -24,7 +26,7 @@ impl QueryCapability for ClusterLogsQuery {
async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> { async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
let state = crate::launcher::state()?; let state = crate::launcher::state()?;
let cluster = state.clusters.get(keys.cluster_id).await?; 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() { if keys.path.as_os_str().is_empty() {
return Ok(Vec::new()); return Ok(Vec::new());
} }
Ok(oneclient_core::read_log_at( let path = keys.path.clone();
&keys.path, let options = ReadOptions {
&ReadOptions { level_filter: keys.level,
level_filter: keys.level, search: keys.search.clone(),
search: keys.search.clone(), max_lines: keys.max_lines,
max_lines: keys.max_lines, };
}, Ok(off_ui(async move { oneclient_core::read_log_at(&path, &options).await }).await?)
)
.await?)
} }
} }
@ -109,7 +109,11 @@ impl MutationCapability for UploadLogMutation {
async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> { async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
let state = crate::launcher::state()?; 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?)
} }
} }

View file

@ -205,7 +205,12 @@ impl MutationCapability for ClusterMutation {
ClusterAction::VerifyFiles { cluster_id } => { ClusterAction::VerifyFiles { cluster_id } => {
// Reports its own outcome not the generic failure toast a // Reports its own outcome not the generic failure toast a
// verify that finds nothing wrong is still a useful result // 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) => { Ok(report) => {
let notify = services.events.notify("Verification complete"); let notify = services.events.notify("Verification complete");
let notify = notify.body(report.summary()); let notify = notify.body(report.summary());

View file

@ -217,17 +217,23 @@ impl QueryCapability for PackageVersionsQuery {
async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> { async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
let state = crate::launcher::state()?; let state = crate::launcher::state()?;
let provider = state.services.packages.get(keys.provider)?; let keys = keys.clone();
Ok(provider crate::launcher::off_ui(async move {
.list_versions( Ok(state
&keys.project_id, .services
keys.game_version.as_deref(), .packages
keys.loader, .get(keys.provider)?
keys.page * VERSIONS_PAGE_SIZE, .list_versions(
VERSIONS_PAGE_SIZE, &keys.project_id,
&state.services.content(), keys.game_version.as_deref(),
) keys.loader,
.await?) keys.page * VERSIONS_PAGE_SIZE,
VERSIONS_PAGE_SIZE,
&state.services.content(),
)
.await?)
})
.await
} }
} }

View file

@ -11,6 +11,8 @@ use notify::{Event, EventKind};
use oneclient_core::{LauncherError, ScreenshotInfo}; use oneclient_core::{LauncherError, ScreenshotInfo};
use tokio::sync::Semaphore; use tokio::sync::Semaphore;
use crate::launcher::off_ui_blocking;
static LOCAL_IMAGE_SEMAPHORE: OnceLock<Arc<Semaphore>> = OnceLock::new(); static LOCAL_IMAGE_SEMAPHORE: OnceLock<Arc<Semaphore>> = OnceLock::new();
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
@ -29,7 +31,7 @@ impl QueryCapability for ClusterScreenshotsQuery {
async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> { async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
let state = crate::launcher::state()?; let state = crate::launcher::state()?;
let cluster = state.clusters.get(keys.cluster_id).await?; 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<Self::Ok, Self::Err> { async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
match keys { 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?)
}
} }
} }

View file

@ -5,6 +5,8 @@ use freya::query::{
use_mutation, use_query, use_mutation, use_query,
}; };
use oneclient_core::LauncherError; use oneclient_core::LauncherError;
use crate::launcher::off_ui;
use oneclient_core::relocate::{Leftovers, discard_leftovers, leftovers}; use oneclient_core::relocate::{Leftovers, discard_leftovers, leftovers};
use oneclient_core::storage::{StorageReport, storage_report}; use oneclient_core::storage::{StorageReport, storage_report};
@ -21,7 +23,7 @@ impl QueryCapability for StorageReportQuery {
async fn run(&self, _keys: &Self::Keys) -> Result<Self::Ok, Self::Err> { async fn run(&self, _keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
let state = crate::launcher::state()?; 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<Self::Ok, Self::Err> { async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
let state = crate::launcher::state().map_err(|e| e.to_string())?; let state = crate::launcher::state().map_err(|e| e.to_string())?;
let keys = *keys;
match keys { off_ui(async move {
StorageAction::CleanUnreferencedCache => { match keys {
oneclient_core::storage::clean_unreferenced_cache(&state) StorageAction::CleanUnreferencedCache => {
.await oneclient_core::storage::clean_unreferenced_cache(&state)
.map(|_| ()) .await
.map(|_| ())
}
StorageAction::CleanLegacyClusterContent => {
oneclient_core::storage::clean_cluster_leftovers(&state)
.await
.map(|_| ())
}
} }
StorageAction::CleanLegacyClusterContent => { })
oneclient_core::storage::clean_cluster_leftovers(&state) .await
.await
.map(|_| ())
}
}
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
@ -97,7 +103,7 @@ impl QueryCapability for LeftoversQuery {
async fn run(&self, _keys: &Self::Keys) -> Result<Self::Ok, Self::Err> { async fn run(&self, _keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
let state = crate::launcher::state()?; 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<Self::Ok, Self::Err> { async fn run(&self, _keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
let state = crate::launcher::state().map_err(|err| err.to_string())?; 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<Self::Ok, Self::Err>) { async fn on_settled(&self, _keys: &Self::Keys, result: &Result<Self::Ok, Self::Err>) {

View file

@ -2,6 +2,8 @@ use freya::query::{Query, QueryCapability, QueryStateData, UseQuery, use_query};
use oneclient_common::domain::GameLoader; use oneclient_common::domain::GameLoader;
use oneclient_core::{LauncherError, VersionMetadata}; use oneclient_core::{LauncherError, VersionMetadata};
use crate::launcher::off_ui;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct VersionsMetadataQuery; pub struct VersionsMetadataQuery;
@ -15,18 +17,21 @@ impl QueryCapability for VersionsMetadataQuery {
async fn run(&self, _keys: &Self::Keys) -> Result<Self::Ok, Self::Err> { async fn run(&self, _keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
let state = crate::launcher::state()?; let state = crate::launcher::state()?;
let metadata = state off_ui(async move {
.versions let metadata = state
.metadata(&state.services.requester.config().meta_url_base) .versions
.await; .metadata(&state.services.requester.config().meta_url_base)
if !metadata.is_empty() { .await;
return Ok(metadata); if !metadata.is_empty() {
} return Ok(metadata);
state.versions.sync(&state.services).await?; }
Ok(state state.versions.sync(&state.services).await?;
.versions Ok(state
.metadata(&state.services.requester.config().meta_url_base) .versions
.await) .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<Self::Ok, Self::Err> { async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
let state = crate::launcher::state()?; let state = crate::launcher::state()?;
let mut metadata = state.metadata.lock().await; let keys = keys.clone();
Ok(oneclient_core::get_loader_versions( Ok(off_ui(async move {
&mut metadata, let mut metadata = state.metadata.lock().await;
&state.services.mc(), oneclient_core::get_loader_versions(
&keys.mc_version, &mut metadata,
keys.loader, &state.services.mc(),
) &keys.mc_version,
keys.loader,
)
.await
})
.await?) .await?)
} }
} }

View file

@ -20,3 +20,17 @@ pub fn install(state: Arc<LauncherState>) -> Arc<LauncherState> {
pub fn state() -> LauncherResult<Arc<LauncherState>> { pub fn state() -> LauncherResult<Arc<LauncherState>> {
LAUNCHER.get().cloned().ok_or(LauncherError::NotInitialized) LAUNCHER.get().cloned().ok_or(LauncherError::NotInitialized)
} }
pub async fn off_ui<T: Send + 'static>(work: impl Future<Output = T> + 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<T: Send + 'static>(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()),
}
}

View file

@ -12,6 +12,7 @@ use tokio::sync::mpsc::UnboundedSender;
use crate::hooks::Actions; use crate::hooks::Actions;
use crate::launcher; use crate::launcher;
use crate::launcher::off_ui;
/// Front-ends match on these /// Front-ends match on these
pub const MICROSOFT_JAVA_CHOICE_INSTALL: &str = "java.microsoft.install"; 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; return;
}; };
if opted_out(&state) { let offer = off_ui({
return; 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; 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) { if !claim_ask(cluster_id) {
return; return;
} }

View file

@ -3,6 +3,7 @@ use std::path::PathBuf;
use freya::prelude::spawn_forever; use freya::prelude::spawn_forever;
use crate::hooks::Actions; use crate::hooks::Actions;
use crate::launcher::off_ui_blocking;
pub fn database_path() -> Option<PathBuf> { pub fn database_path() -> Option<PathBuf> {
oneclient_common::paths::database_file().ok() oneclient_common::paths::database_file().ok()
@ -29,19 +30,25 @@ pub fn restore_latest(actions: &Actions) {
return; return;
}; };
match oneclient_db::backup::restore(&path, &snapshot) { let actions = actions.clone();
Ok(_) => retry(actions), spawn_forever(async move {
Err(err) => set_error(actions, format!("Couldn't restore the snapshot: {err}")), 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) { pub fn reset(actions: &Actions) {
let Some(path) = database_path() else { return }; let Some(path) = database_path() else { return };
match oneclient_db::backup::reset(&path) { let actions = actions.clone();
Ok(_) => retry(actions), spawn_forever(async move {
Err(err) => set_error(actions, format!("Couldn't reset the database: {err}")), 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) { fn retry(actions: &Actions) {

View file

@ -319,7 +319,7 @@ fn plan(
checking.set(true); checking.set(true);
error.set(None); 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)), Ok(planned) => pending.set(Some(planned)),
Err(message) => error.set(Some(message)), Err(message) => error.set(Some(message)),
} }

View file

@ -898,8 +898,8 @@ async fn addition_eligible_bundles(
candidate_keys_by_bundle: &HashMap<String, HashSet<String>>, candidate_keys_by_bundle: &HashMap<String, HashSet<String>>,
overrides: &[ClusterBundleOverrideRow], overrides: &[ClusterBundleOverrideRow],
) -> ContentResult<HashSet<String>> { ) -> ContentResult<HashSet<String>> {
let (live_managed_keys, _) = let live: Vec<_> = all_linked.iter().filter(|item| item.enabled).collect();
installed_bundle_keys(ctx, all_linked.iter().filter(|item| item.enabled)).await?; let (live_managed_keys, _) = installed_bundle_keys(ctx, live).await?;
let mut eligible: HashSet<String> = bundle_packages let mut eligible: HashSet<String> = bundle_packages
.iter() .iter()