Compare commits

...
64 changed files with 1554 additions and 287 deletions
+4
View File
@@ -101,6 +101,10 @@ jobs:
~/.cargo/bin
key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Ensure sccache directory exists
if: needs.skip-if-clean.outputs.internal == 'true'
run: sudo mkdir -p /mnt/sccache && sudo chown $(id -u):$(id -g) /mnt/sccache
- name: Mount sccache disk cache
if: needs.skip-if-clean.outputs.internal == 'true'
uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0
+4
View File
@@ -109,6 +109,10 @@ jobs:
~/.cargo/bin
key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Ensure sccache directory exists
if: needs.skip-if-clean.outputs.internal == 'true'
run: sudo mkdir -p /mnt/sccache && sudo chown $(id -u):$(id -g) /mnt/sccache
- name: Mount sccache disk cache
if: needs.skip-if-clean.outputs.internal == 'true'
uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0
Generated
+70
View File
@@ -3856,6 +3856,12 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-timer"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968"
[[package]]
name = "futures-util"
version = "0.3.31"
@@ -4167,6 +4173,29 @@ dependencies = [
"system-deps",
]
[[package]]
name = "governor"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8"
dependencies = [
"cfg-if",
"dashmap",
"futures-sink",
"futures-timer",
"futures-util",
"getrandom 0.3.3",
"hashbrown 0.16.0",
"nonzero_ext",
"parking_lot",
"portable-atomic",
"quanta",
"rand 0.9.2",
"smallvec",
"spinning_top",
"web-time",
]
[[package]]
name = "group"
version = "0.13.0"
@@ -6300,6 +6329,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "nonzero_ext"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
[[package]]
name = "noop_proc_macro"
version = "0.3.0"
@@ -7900,6 +7935,21 @@ dependencies = [
"bytemuck",
]
[[package]]
name = "quanta"
version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7"
dependencies = [
"crossbeam-utils",
"libc",
"once_cell",
"raw-cpuid",
"wasi 0.11.1+wasi-snapshot-preview1",
"web-sys",
"winapi",
]
[[package]]
name = "quartz_nbt"
version = "0.2.9"
@@ -8227,6 +8277,15 @@ dependencies = [
"rgb",
]
[[package]]
name = "raw-cpuid"
version = "11.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
dependencies = [
"bitflags 2.9.4",
]
[[package]]
name = "raw-window-handle"
version = "0.6.2"
@@ -9838,6 +9897,15 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
[[package]]
name = "spinning_top"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300"
dependencies = [
"lock_api",
]
[[package]]
name = "spki"
version = "0.7.3"
@@ -10940,8 +11008,10 @@ dependencies = [
"fs4",
"futures",
"futures-lite 2.6.1",
"governor",
"heck 0.5.0",
"hickory-resolver 0.25.2",
"httpdate",
"indicatif",
"itertools 0.14.0",
"modrinth-content-management",
+2
View File
@@ -89,10 +89,12 @@ fs4 = { version = "0.13.1", default-features = false }
futures = "0.3.31"
futures-lite = "2.6.1"
futures-util = "0.3.31"
governor = "0.10.4"
heck = "0.5.0"
hex = "0.4.3"
hickory-resolver = "0.25.2"
hmac = "0.12.1"
httpdate = "1.0.3"
hyper = "1.7.0"
hyper-rustls = { version = "0.27.7", default-features = false, features = [
"aws-lc-rs",
+1 -1
View File
@@ -1,4 +1,4 @@
import { invoke } from '@tauri-apps/api/core'
import { invoke } from '@/helpers/invoke'
export async function get_project(id, cacheBehaviour) {
return await invoke('plugin:cache|get_project', { id, cacheBehaviour })
+1 -1
View File
@@ -1,5 +1,5 @@
import type { User } from '@modrinth/utils'
import { invoke } from '@tauri-apps/api/core'
import { invoke } from '@/helpers/invoke'
import type { Dayjs } from 'dayjs'
import dayjs from 'dayjs'
+1 -1
View File
@@ -1,4 +1,4 @@
import { invoke } from '@tauri-apps/api/core'
import { invoke } from '@/helpers/invoke'
import { install_job_listener } from './events'
import type { InstanceLink, InstanceLoader } from './types'
+1 -1
View File
@@ -5,7 +5,7 @@
*/
import type { Labrinth } from '@modrinth/api-client'
import type { ContentItem, ContentOwner } from '@modrinth/ui'
import { invoke } from '@tauri-apps/api/core'
import { invoke } from '@/helpers/invoke'
import type { InstallJobSnapshot } from './install'
import type {
+83
View File
@@ -0,0 +1,83 @@
import { invoke as tauriInvoke } from '@tauri-apps/api/core'
export type OperationCause =
| 'app/startup'
| 'navigation/home'
| 'navigation/library'
| 'navigation/browse'
| 'navigation/project'
| 'navigation/instance/overview'
| 'navigation/instance/content'
| 'navigation/instance/logs'
| 'navigation/servers'
| 'navigation/server/manage'
| 'navigation/server/content'
| 'instance/refresh/user'
| 'instance/refresh/filesystem_watch'
| 'instance/update/all'
| 'instance/update/single'
| 'instance/install'
| 'cache/revalidate'
| 'auth/session_refresh'
| 'background/friends'
| 'minecraft/launch'
| 'app/update_check'
| 'unattributed'
function navigationCause(pathname: string): OperationCause {
if (pathname === '/hosting/manage' || pathname === '/hosting/manage/') {
return 'navigation/servers'
}
if (pathname.startsWith('/hosting/manage/')) {
return pathname.split('/').includes('content')
? 'navigation/server/content'
: 'navigation/server/manage'
}
if (pathname.startsWith('/browse/')) return 'navigation/browse'
if (pathname.startsWith('/project/')) return 'navigation/project'
if (pathname.startsWith('/library')) return 'navigation/library'
if (pathname.startsWith('/instance/')) {
return pathname.split('/').includes('logs')
? 'navigation/instance/logs'
: 'navigation/instance/content'
}
return 'navigation/home'
}
function commandCause(command: string): OperationCause {
if (command === 'plugin:instance|instance_update_all') return 'instance/update/all'
if (
command === 'plugin:instance|instance_update_project' ||
command === 'plugin:instance|instance_switch_project_version_with_dependencies' ||
command === 'plugin:instance|instance_update_managed_modrinth_version'
) {
return 'instance/update/single'
}
if (
command.startsWith('plugin:install|install_') ||
command === 'plugin:jre|jre_auto_install_java' ||
command === 'plugin:instance|instance_add_project_from_version' ||
command === 'plugin:instance|instance_install_project_with_dependencies' ||
command === 'plugin:instance|instance_repair_managed_modrinth'
) {
return 'instance/install'
}
if (
command === 'plugin:instance|instance_run' ||
command === 'plugin:worlds|start_join_singleplayer_world' ||
command === 'plugin:worlds|start_join_server'
) {
return 'minecraft/launch'
}
if (command === 'plugin:mr-auth|get') return 'auth/session_refresh'
return navigationCause(window.location.pathname)
}
export function invoke<T>(command: string, args: Record<string, unknown> = {}): Promise<T> {
return tauriInvoke<T>(command, {
...args,
invocationContext: { cause: commandCause(command) },
})
}
+1 -1
View File
@@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import { invoke } from '@tauri-apps/api/core'
import { invoke } from '@/helpers/invoke'
/*
+1 -1
View File
@@ -1,4 +1,4 @@
import { invoke } from '@tauri-apps/api/core'
import { invoke } from '@/helpers/invoke'
/// Gets the game versions from daedalus
// Returns a VersionManifest
+1 -1
View File
@@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import { invoke } from '@tauri-apps/api/core'
import { invoke } from '@/helpers/invoke'
export type ModrinthCredentials = {
session: string
+1 -1
View File
@@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import { invoke } from '@tauri-apps/api/core'
import { invoke } from '@/helpers/invoke'
// Gets cached category tags
export async function get_categories() {
+1 -1
View File
@@ -1,6 +1,6 @@
import type { GameVersion } from '@modrinth/ui'
import { autoToHTML } from '@sfirew/minecraft-motd-parser'
import { invoke } from '@tauri-apps/api/core'
import { invoke } from '@/helpers/invoke'
import dayjs from 'dayjs'
import { get_full_path } from '@/helpers/instance'
+17 -6
View File
@@ -6,20 +6,27 @@ macro_rules! impl_cache_methods {
$(
paste::paste! {
#[tauri::command]
pub async fn [<get_ $variant:snake>](id: &str, cache_behaviour: Option<CacheBehaviour>) -> Result<Option<$type>>
pub async fn [<get_ $variant:snake>](
id: &str,
cache_behaviour: Option<CacheBehaviour>,
invocation_context: theseus::InvocationContext,
) -> Result<Option<$type>>
{
Ok(theseus::cache::[<get_ $variant:snake>](id, cache_behaviour).await?)
let context = invocation_context;
Ok(theseus::cache::[<get_ $variant:snake>](&context, id, cache_behaviour).await?)
}
#[tauri::command]
pub async fn [<get_ $variant:snake _many>](
ids: Vec<String>,
cache_behaviour: Option<CacheBehaviour>,
invocation_context: theseus::InvocationContext,
) -> Result<Vec<$type>>
{
let context = invocation_context;
let ids = ids.iter().map(|x| &**x).collect::<Vec<&str>>();
let entries =
theseus::cache::[<get_ $variant:snake _many>](&*ids, cache_behaviour).await?;
theseus::cache::[<get_ $variant:snake _many>](&context, &*ids, cache_behaviour).await?;
Ok(entries)
}
@@ -73,9 +80,13 @@ pub async fn purge_cache_types(cache_types: Vec<CacheValueType>) -> Result<()> {
pub async fn get_project_versions(
project_id: &str,
cache_behaviour: Option<CacheBehaviour>,
invocation_context: theseus::InvocationContext,
) -> Result<Option<Vec<Version>>> {
Ok(
theseus::cache::get_project_versions(project_id, cache_behaviour)
.await?,
let context = invocation_context;
Ok(theseus::cache::get_project_versions(
&context,
project_id,
cache_behaviour,
)
.await?)
}
+17 -6
View File
@@ -13,8 +13,11 @@ pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
}
#[tauri::command]
pub async fn friends() -> crate::api::Result<Vec<UserFriend>> {
Ok(theseus::friends::friends().await?)
pub async fn friends(
invocation_context: theseus::InvocationContext,
) -> crate::api::Result<Vec<UserFriend>> {
let context = invocation_context;
Ok(theseus::friends::friends(&context).await?)
}
#[tauri::command]
@@ -23,11 +26,19 @@ pub async fn friend_statuses() -> crate::api::Result<Vec<UserStatus>> {
}
#[tauri::command]
pub async fn add_friend(user_id: &str) -> crate::api::Result<()> {
Ok(theseus::friends::add_friend(user_id).await?)
pub async fn add_friend(
user_id: &str,
invocation_context: theseus::InvocationContext,
) -> crate::api::Result<()> {
let context = invocation_context;
Ok(theseus::friends::add_friend(&context, user_id).await?)
}
#[tauri::command]
pub async fn remove_friend(user_id: &str) -> crate::api::Result<()> {
Ok(theseus::friends::remove_friend(user_id).await?)
pub async fn remove_friend(
user_id: &str,
invocation_context: theseus::InvocationContext,
) -> crate::api::Result<()> {
let context = invocation_context;
Ok(theseus::friends::remove_friend(&context, user_id).await?)
}
+38 -5
View File
@@ -67,15 +67,23 @@ impl InstallPostInstallEditRequest {
#[tauri::command]
pub async fn install_get_modpack_preview(
location: CreatePackLocation,
invocation_context: theseus::InvocationContext,
) -> Result<InstallModpackPreview> {
Ok(theseus::pack::install_from::get_instance_from_pack(location).await?)
let context = invocation_context;
Ok(
theseus::pack::install_from::get_instance_from_pack(&context, location)
.await?,
)
}
#[tauri::command]
pub async fn install_create_instance(
request: InstallCreateInstanceRequest,
invocation_context: theseus::InvocationContext,
) -> Result<InstallJobSnapshot> {
let context = invocation_context;
Ok(theseus::install::create_instance(
&context,
request.name.trim().to_string(),
request.game_version,
request.loader,
@@ -93,8 +101,11 @@ pub async fn install_create_instance(
pub async fn install_create_modpack_instance(
location: CreatePackLocation,
post_install_edit: Option<InstallPostInstallEditRequest>,
invocation_context: theseus::InvocationContext,
) -> Result<InstallJobSnapshot> {
let context = invocation_context;
Ok(theseus::install::create_modpack_instance(
&context,
location,
post_install_edit.map(|edit| edit.into_core()).transpose()?,
)
@@ -106,8 +117,11 @@ pub async fn install_import_instance(
launcher_type: ImportLauncherType,
base_path: PathBuf,
instance_folder: String,
invocation_context: theseus::InvocationContext,
) -> Result<InstallJobSnapshot> {
let context = invocation_context;
Ok(theseus::install::import_instance(
&context,
launcher_type,
base_path,
instance_folder,
@@ -118,16 +132,28 @@ pub async fn install_import_instance(
#[tauri::command]
pub async fn install_duplicate_instance(
source_instance_id: String,
invocation_context: theseus::InvocationContext,
) -> Result<InstallJobSnapshot> {
Ok(theseus::install::duplicate_instance(source_instance_id).await?)
let context = invocation_context;
Ok(
theseus::install::duplicate_instance(&context, source_instance_id)
.await?,
)
}
#[tauri::command]
pub async fn install_existing_instance(
instance_id: String,
force: bool,
invocation_context: theseus::InvocationContext,
) -> Result<InstallJobSnapshot> {
Ok(theseus::install::install_existing_instance(instance_id, force).await?)
let context = invocation_context;
Ok(theseus::install::install_existing_instance(
&context,
instance_id,
force,
)
.await?)
}
#[tauri::command]
@@ -135,8 +161,11 @@ pub async fn install_pack_to_existing_instance(
instance_id: String,
location: CreatePackLocation,
post_install_edit: Option<InstallPostInstallEditRequest>,
invocation_context: theseus::InvocationContext,
) -> Result<InstallJobSnapshot> {
let context = invocation_context;
Ok(theseus::install::install_pack_to_existing_instance(
&context,
instance_id,
location,
post_install_edit.map(|edit| edit.into_core()).transpose()?,
@@ -157,8 +186,12 @@ pub async fn install_job_get(job_id: Uuid) -> Result<InstallJobSnapshot> {
}
#[tauri::command]
pub async fn install_job_retry(job_id: Uuid) -> Result<InstallJobSnapshot> {
Ok(theseus::install::retry_job(job_id).await?)
pub async fn install_job_retry(
job_id: Uuid,
invocation_context: theseus::InvocationContext,
) -> Result<InstallJobSnapshot> {
let context = invocation_context;
Ok(theseus::install::retry_job(&context, job_id).await?)
}
#[tauri::command]
+82 -22
View File
@@ -437,15 +437,25 @@ pub async fn instance_list() -> Result<Vec<Instance>> {
pub async fn instance_get_projects(
instance_id: &str,
cache_behaviour: Option<CacheBehaviour>,
invocation_context: theseus::InvocationContext,
) -> Result<DashMap<String, ContentFile>> {
Ok(theseus::instance::get_projects(instance_id, cache_behaviour).await?)
let context = invocation_context;
Ok(
theseus::instance::get_projects(&context, instance_id, cache_behaviour)
.await?,
)
}
#[tauri::command]
pub async fn instance_get_installed_project_ids(
instance_id: &str,
invocation_context: theseus::InvocationContext,
) -> Result<Vec<String>> {
Ok(theseus::instance::get_installed_project_ids(instance_id).await?)
let context = invocation_context;
Ok(
theseus::instance::get_installed_project_ids(&context, instance_id)
.await?,
)
}
#[tauri::command]
@@ -466,27 +476,36 @@ pub async fn instance_get_install_candidates(
pub async fn instance_content(
instance_id: &str,
cache_behaviour: Option<CacheBehaviour>,
invocation_context: theseus::InvocationContext,
) -> Result<Vec<ContentItem>> {
instance_get_content_items(instance_id, cache_behaviour).await
instance_get_content_items(instance_id, cache_behaviour, invocation_context)
.await
}
#[tauri::command]
pub async fn instance_get_content_items(
instance_id: &str,
cache_behaviour: Option<CacheBehaviour>,
invocation_context: theseus::InvocationContext,
) -> Result<Vec<ContentItem>> {
Ok(
theseus::instance::get_content_items(instance_id, cache_behaviour)
.await?,
let context = invocation_context;
Ok(theseus::instance::get_content_items(
&context,
instance_id,
cache_behaviour,
)
.await?)
}
#[tauri::command]
pub async fn instance_get_dependencies_as_content_items(
dependencies: Vec<Dependency>,
cache_behaviour: Option<CacheBehaviour>,
invocation_context: theseus::InvocationContext,
) -> Result<Vec<ContentItem>> {
let context = invocation_context;
Ok(theseus::instance::get_dependencies_as_content_items(
&context,
dependencies,
cache_behaviour,
)
@@ -497,22 +516,26 @@ pub async fn instance_get_dependencies_as_content_items(
pub async fn instance_get_linked_modpack_info(
instance_id: &str,
cache_behaviour: Option<CacheBehaviour>,
invocation_context: theseus::InvocationContext,
) -> Result<Option<LinkedModpackInfo>> {
Ok(
theseus::instance::get_linked_modpack_info(
instance_id,
cache_behaviour,
)
.await?,
let context = invocation_context;
Ok(theseus::instance::get_linked_modpack_info(
&context,
instance_id,
cache_behaviour,
)
.await?)
}
#[tauri::command]
pub async fn instance_get_linked_modpack_content(
instance_id: &str,
cache_behaviour: Option<CacheBehaviour>,
invocation_context: theseus::InvocationContext,
) -> Result<Vec<ContentItem>> {
let context = invocation_context;
Ok(theseus::instance::get_linked_modpack_content(
&context,
instance_id,
cache_behaviour,
)
@@ -535,19 +558,23 @@ pub async fn instance_get_mod_full_path(
#[tauri::command]
pub async fn instance_get_optimal_jre_key(
instance_id: &str,
invocation_context: theseus::InvocationContext,
) -> Result<Option<JavaVersion>> {
Ok(theseus::instance::get_optimal_jre_key(instance_id).await?)
let context = invocation_context;
Ok(theseus::instance::get_optimal_jre_key(&context, instance_id).await?)
}
#[tauri::command]
pub async fn instance_check_installed(
instance_id: &str,
project_id: &str,
invocation_context: theseus::InvocationContext,
) -> Result<bool> {
let context = invocation_context;
let check_project_id = project_id;
if let Ok(projects) =
theseus::instance::get_projects(instance_id, None).await
theseus::instance::get_projects(&context, instance_id, None).await
{
Ok(projects.into_iter().any(|(_, project)| {
project
@@ -563,19 +590,26 @@ pub async fn instance_check_installed(
#[tauri::command]
pub async fn instance_update_all(
instance_id: &str,
invocation_context: theseus::InvocationContext,
) -> Result<HashMap<String, String>> {
Ok(theseus::instance::update_all_projects(instance_id).await?)
let context = invocation_context;
Ok(theseus::instance::update_all_projects(&context, instance_id).await?)
}
#[tauri::command]
pub async fn instance_update_project(
instance_id: &str,
project_path: &str,
invocation_context: theseus::InvocationContext,
) -> Result<String> {
Ok(
theseus::instance::update_project(instance_id, project_path, None)
.await?,
let context = invocation_context;
Ok(theseus::instance::update_project(
&context,
instance_id,
project_path,
None,
)
.await?)
}
#[tauri::command]
@@ -584,8 +618,11 @@ pub async fn instance_add_project_from_version(
version_id: &str,
reason: DownloadReason,
dependent_on_version_id: Option<String>,
invocation_context: theseus::InvocationContext,
) -> Result<String> {
let context = invocation_context;
Ok(theseus::instance::add_project_from_version(
&context,
instance_id,
version_id,
reason,
@@ -598,8 +635,11 @@ pub async fn instance_add_project_from_version(
pub async fn instance_install_project_with_dependencies(
instance_id: &str,
request: InstallProjectWithDependenciesRequest,
invocation_context: theseus::InvocationContext,
) -> Result<ResolveContentPlan> {
let context = invocation_context;
Ok(theseus::instance::install_project_with_dependencies(
&context,
instance_id,
request,
)
@@ -611,8 +651,11 @@ pub async fn instance_switch_project_version_with_dependencies(
instance_id: &str,
project_path: &str,
version_id: &str,
invocation_context: theseus::InvocationContext,
) -> Result<String> {
let context = invocation_context;
Ok(theseus::instance::switch_project_version_with_dependencies(
&context,
instance_id,
project_path,
version_id,
@@ -635,8 +678,12 @@ pub async fn instance_add_project_from_path(
}
#[tauri::command]
pub async fn instance_is_file_on_modrinth(project_path: &Path) -> Result<bool> {
Ok(theseus::instance::is_file_on_modrinth(project_path).await?)
pub async fn instance_is_file_on_modrinth(
project_path: &Path,
invocation_context: theseus::InvocationContext,
) -> Result<bool> {
let context = invocation_context;
Ok(theseus::instance::is_file_on_modrinth(&context, project_path).await?)
}
#[tauri::command]
@@ -666,8 +713,11 @@ pub async fn instance_remove_project(
pub async fn instance_update_managed_modrinth_version(
instance_id: String,
version_id: String,
invocation_context: theseus::InvocationContext,
) -> Result<theseus::install::InstallJobSnapshot> {
let context = invocation_context;
Ok(theseus::instance::update_managed_modrinth_version(
&context,
&instance_id,
&version_id,
)
@@ -677,8 +727,13 @@ pub async fn instance_update_managed_modrinth_version(
#[tauri::command]
pub async fn instance_repair_managed_modrinth(
instance_id: &str,
invocation_context: theseus::InvocationContext,
) -> Result<theseus::install::InstallJobSnapshot> {
Ok(theseus::instance::repair_managed_modrinth(instance_id).await?)
let context = invocation_context;
Ok(
theseus::instance::repair_managed_modrinth(&context, instance_id)
.await?,
)
}
#[tauri::command]
@@ -689,8 +744,11 @@ pub async fn instance_export_mrpack(
version_id: Option<String>,
description: Option<String>,
name: Option<String>,
invocation_context: theseus::InvocationContext,
) -> Result<()> {
let context = invocation_context;
theseus::instance::export_mrpack(
&context,
instance_id,
export_location,
included_overrides,
@@ -713,12 +771,14 @@ pub async fn instance_get_pack_export_candidates(
pub async fn instance_run(
instance_id: &str,
server_address: Option<String>,
invocation_context: theseus::InvocationContext,
) -> Result<ProcessMetadata> {
let context = invocation_context;
let quick_play = match server_address {
Some(addr) => QuickPlayType::Server(ServerAddress::Unresolved(addr)),
None => QuickPlayType::None,
};
Ok(theseus::instance::run(instance_id, quick_play).await?)
Ok(theseus::instance::run(&context, instance_id, quick_play).await?)
}
#[tauri::command]
+6 -2
View File
@@ -53,8 +53,12 @@ pub async fn jre_test_jre(path: PathBuf, major_version: u32) -> Result<bool> {
// Auto installs java for the given java version
#[tauri::command]
pub async fn jre_auto_install_java(java_version: u32) -> Result<PathBuf> {
Ok(jre::auto_install_java(java_version).await?)
pub async fn jre_auto_install_java(
java_version: u32,
invocation_context: theseus::InvocationContext,
) -> Result<PathBuf> {
let context = invocation_context;
Ok(jre::auto_install_java(&context, java_version).await?)
}
// Gets the maximum memory a system has available.
+11 -4
View File
@@ -13,12 +13,19 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
/// Gets the game versions from daedalus
#[tauri::command]
pub async fn metadata_get_game_versions() -> Result<VersionManifest> {
Ok(theseus::metadata::get_minecraft_versions().await?)
pub async fn metadata_get_game_versions(
invocation_context: theseus::InvocationContext,
) -> Result<VersionManifest> {
let context = invocation_context;
Ok(theseus::metadata::get_minecraft_versions(&context).await?)
}
/// Gets the fabric versions from daedalus
#[tauri::command]
pub async fn metadata_get_loader_versions(loader: &str) -> Result<Manifest> {
Ok(theseus::metadata::get_loader_versions(loader).await?)
pub async fn metadata_get_loader_versions(
loader: &str,
invocation_context: theseus::InvocationContext,
) -> Result<Manifest> {
let context = invocation_context;
Ok(theseus::metadata::get_loader_versions(&context, loader).await?)
}
+9 -3
View File
@@ -22,7 +22,9 @@ pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
#[tauri::command]
pub async fn modrinth_login<R: Runtime>(
app: tauri::AppHandle<R>,
invocation_context: theseus::InvocationContext,
) -> Result<ModrinthCredentials> {
let context = invocation_context;
let (auth_code_recv_socket_tx, auth_code_recv_socket) = oneshot::channel();
let auth_code = tokio::spawn(oauth_utils::auth_code_reply::listen(
auth_code_recv_socket_tx,
@@ -58,7 +60,8 @@ pub async fn modrinth_login<R: Runtime>(
));
};
let credentials = mr_auth::authenticate_finish_flow(&auth_code).await?;
let credentials =
mr_auth::authenticate_finish_flow(&context, &auth_code).await?;
if let Some(main_window) = app.get_window("main") {
main_window.set_focus().ok();
@@ -73,8 +76,11 @@ pub async fn logout() -> Result<()> {
}
#[tauri::command]
pub async fn get() -> Result<Option<ModrinthCredentials>> {
Ok(theseus::mr_auth::get_credentials().await?)
pub async fn get(
invocation_context: theseus::InvocationContext,
) -> Result<Option<ModrinthCredentials>> {
let context = invocation_context;
Ok(theseus::mr_auth::get_credentials(&context).await?)
}
#[tauri::command]
+25 -10
View File
@@ -15,30 +15,45 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
/// Gets cached category tags from the database
#[tauri::command]
pub async fn tags_get_categories() -> Result<Vec<Category>> {
Ok(theseus::tags::get_category_tags().await?)
pub async fn tags_get_categories(
invocation_context: theseus::InvocationContext,
) -> Result<Vec<Category>> {
let context = invocation_context;
Ok(theseus::tags::get_category_tags(&context).await?)
}
/// Gets cached report type tags from the database
#[tauri::command]
pub async fn tags_get_report_types() -> Result<Vec<String>> {
Ok(theseus::tags::get_report_type_tags().await?)
pub async fn tags_get_report_types(
invocation_context: theseus::InvocationContext,
) -> Result<Vec<String>> {
let context = invocation_context;
Ok(theseus::tags::get_report_type_tags(&context).await?)
}
/// Gets cached loader tags from the database
#[tauri::command]
pub async fn tags_get_loaders() -> Result<Vec<Loader>> {
Ok(theseus::tags::get_loader_tags().await?)
pub async fn tags_get_loaders(
invocation_context: theseus::InvocationContext,
) -> Result<Vec<Loader>> {
let context = invocation_context;
Ok(theseus::tags::get_loader_tags(&context).await?)
}
/// Gets cached game version tags from the database
#[tauri::command]
pub async fn tags_get_game_versions() -> Result<Vec<GameVersion>> {
Ok(theseus::tags::get_game_version_tags().await?)
pub async fn tags_get_game_versions(
invocation_context: theseus::InvocationContext,
) -> Result<Vec<GameVersion>> {
let context = invocation_context;
Ok(theseus::tags::get_game_version_tags(&context).await?)
}
/// Gets cached donation platform tags from the database
#[tauri::command]
pub async fn tags_get_donation_platforms() -> Result<Vec<DonationPlatform>> {
Ok(theseus::tags::get_donation_platform_tags().await?)
pub async fn tags_get_donation_platforms(
invocation_context: theseus::InvocationContext,
) -> Result<Vec<DonationPlatform>> {
let context = invocation_context;
Ok(theseus::tags::get_donation_platform_tags(&context).await?)
}
+14 -3
View File
@@ -196,8 +196,10 @@ pub async fn remove_server_from_instance(
#[tauri::command]
pub async fn get_instance_protocol_version(
instance_id: &str,
invocation_context: theseus::InvocationContext,
) -> Result<Option<ProtocolVersion>> {
Ok(worlds::get_instance_protocol_version(instance_id).await?)
let context = invocation_context;
Ok(worlds::get_instance_protocol_version(&context, instance_id).await?)
}
#[tauri::command]
@@ -212,9 +214,15 @@ pub async fn get_server_status(
pub async fn start_join_singleplayer_world(
instance_id: &str,
world: String,
invocation_context: theseus::InvocationContext,
) -> Result<ProcessMetadata> {
let process =
instance::run(instance_id, QuickPlayType::Singleplayer(world)).await?;
let context = invocation_context;
let process = instance::run(
&context,
instance_id,
QuickPlayType::Singleplayer(world),
)
.await?;
Ok(process)
}
@@ -223,8 +231,11 @@ pub async fn start_join_singleplayer_world(
pub async fn start_join_server(
instance_id: &str,
address: &str,
invocation_context: theseus::InvocationContext,
) -> Result<ProcessMetadata> {
let context = invocation_context;
let process = instance::run(
&context,
instance_id,
QuickPlayType::Server(ServerAddress::Unresolved(address.to_owned())),
)
+2
View File
@@ -43,8 +43,10 @@ flate2 = { workspace = true }
fs4 = { workspace = true, features = ["tokio"] }
futures = { workspace = true, features = ["alloc", "async-await"] }
futures-lite = { workspace = true }
governor = { workspace = true }
heck = { workspace = true }
hickory-resolver = { workspace = true }
httpdate = { workspace = true }
indicatif = { workspace = true, optional = true }
itertools = { workspace = true }
modrinth-content-management = { workspace = true }
+7 -2
View File
@@ -1,3 +1,4 @@
use crate::InvocationContext;
use crate::state::{
CacheBehaviour, CacheValueType, CachedEntry, Organization, Project,
ProjectV3, SearchResults, SearchResultsV3, TeamMember, User, Version,
@@ -9,23 +10,25 @@ macro_rules! impl_cache_methods {
paste::paste! {
#[tracing::instrument]
pub async fn [<get_ $variant:snake>](
context: &InvocationContext,
id: &str,
cache_behaviour: Option<CacheBehaviour>,
) -> crate::Result<Option<$type>>
{
let state = crate::State::get().await?;
Ok(CachedEntry::[<get_ $variant:snake _many>](&[id], cache_behaviour, &state.pool, &state.api_semaphore).await?.into_iter().next())
Ok(CachedEntry::[<get_ $variant:snake _many>](context, &[id], cache_behaviour, &state.pool, &state.api_semaphore).await?.into_iter().next())
}
#[tracing::instrument]
pub async fn [<get_ $variant:snake _many>](
context: &InvocationContext,
ids: &[&str],
cache_behaviour: Option<CacheBehaviour>,
) -> crate::Result<Vec<$type>>
{
let state = crate::State::get().await?;
let entries =
CachedEntry::[<get_ $variant:snake _many>](ids, None, &state.pool, &state.api_semaphore).await?;
CachedEntry::[<get_ $variant:snake _many>](context, ids, None, &state.pool, &state.api_semaphore).await?;
Ok(entries)
}
@@ -58,11 +61,13 @@ pub async fn purge_cache_types(
/// Uses the cache system with the ProjectVersions cache type.
#[tracing::instrument]
pub async fn get_project_versions(
context: &InvocationContext,
project_id: &str,
cache_behaviour: Option<CacheBehaviour>,
) -> crate::Result<Option<Vec<Version>>> {
let state = crate::State::get().await?;
CachedEntry::get_project_versions(
context,
project_id,
cache_behaviour,
&state.pool,
+28 -8
View File
@@ -1,11 +1,15 @@
use crate::InvocationContext;
use crate::state::{FriendsSocket, UserFriend};
use ariadne::users::UserStatus;
#[tracing::instrument]
pub async fn friends() -> crate::Result<Vec<UserFriend>> {
pub async fn friends(
context: &InvocationContext,
) -> crate::Result<Vec<UserFriend>> {
let state = crate::State::get().await?;
let friends =
FriendsSocket::friends(&state.pool, &state.api_semaphore).await?;
FriendsSocket::friends(context, &state.pool, &state.api_semaphore)
.await?;
Ok(friends)
}
@@ -18,19 +22,35 @@ pub async fn friend_statuses() -> crate::Result<Vec<UserStatus>> {
}
#[tracing::instrument]
pub async fn add_friend(user_id: &str) -> crate::Result<()> {
pub async fn add_friend(
context: &InvocationContext,
user_id: &str,
) -> crate::Result<()> {
let state = crate::State::get().await?;
FriendsSocket::add_friend(user_id, &state.pool, &state.api_semaphore)
.await?;
FriendsSocket::add_friend(
context,
user_id,
&state.pool,
&state.api_semaphore,
)
.await?;
Ok(())
}
#[tracing::instrument]
pub async fn remove_friend(user_id: &str) -> crate::Result<()> {
pub async fn remove_friend(
context: &InvocationContext,
user_id: &str,
) -> crate::Result<()> {
let state = crate::State::get().await?;
FriendsSocket::remove_friend(user_id, &state.pool, &state.api_semaphore)
.await?;
FriendsSocket::remove_friend(
context,
user_id,
&state.pool,
&state.api_semaphore,
)
.await?;
Ok(())
}
+22 -2
View File
@@ -1,3 +1,4 @@
use crate::InvocationContext;
use crate::state::{
CacheBehaviour, ContentFile, ContentItem, ContentSet, Dependency,
InstanceInstallCandidate, InstanceInstallTarget, LinkedModpackInfo,
@@ -7,10 +8,11 @@ use dashmap::DashMap;
#[tracing::instrument]
pub async fn sync_content_files(
context: &InvocationContext,
instance_id: &str,
) -> crate::Result<Vec<crate::state::instances::InstanceFile>> {
let state = State::get().await?;
crate::state::sync_content_files(instance_id, &state).await
crate::state::sync_content_files(context, instance_id, &state).await
}
#[tracing::instrument]
@@ -23,11 +25,13 @@ pub async fn list_content_sets(
#[tracing::instrument]
pub async fn get_projects(
context: &InvocationContext,
instance_id: &str,
cache_behaviour: Option<CacheBehaviour>,
) -> crate::Result<DashMap<String, ContentFile>> {
let state = State::get().await?;
crate::state::get_content_projects(
context,
instance_id,
None,
cache_behaviour,
@@ -38,10 +42,12 @@ pub async fn get_projects(
#[tracing::instrument]
pub async fn get_installed_project_ids(
context: &InvocationContext,
instance_id: &str,
) -> crate::Result<Vec<String>> {
let state = State::get().await?;
crate::state::get_installed_project_ids_for_instance(
context,
instance_id,
None,
&state,
@@ -67,20 +73,30 @@ pub async fn get_install_candidates(
#[tracing::instrument]
pub async fn get_content_items(
context: &InvocationContext,
instance_id: &str,
cache_behaviour: Option<CacheBehaviour>,
) -> crate::Result<Vec<ContentItem>> {
let state = State::get().await?;
crate::state::list_content(instance_id, None, cache_behaviour, &state).await
crate::state::list_content(
context,
instance_id,
None,
cache_behaviour,
&state,
)
.await
}
#[tracing::instrument]
pub async fn get_linked_modpack_content(
context: &InvocationContext,
instance_id: &str,
cache_behaviour: Option<CacheBehaviour>,
) -> crate::Result<Vec<ContentItem>> {
let state = State::get().await?;
crate::state::list_linked_modpack_content(
context,
instance_id,
None,
cache_behaviour,
@@ -91,11 +107,13 @@ pub async fn get_linked_modpack_content(
#[tracing::instrument]
pub async fn get_dependencies_as_content_items(
context: &InvocationContext,
dependencies: Vec<Dependency>,
cache_behaviour: Option<CacheBehaviour>,
) -> crate::Result<Vec<ContentItem>> {
let state = State::get().await?;
crate::state::dependencies_to_content_items(
context,
&dependencies,
cache_behaviour,
&state.pool,
@@ -106,11 +124,13 @@ pub async fn get_dependencies_as_content_items(
#[tracing::instrument]
pub async fn get_linked_modpack_info(
context: &InvocationContext,
instance_id: &str,
cache_behaviour: Option<CacheBehaviour>,
) -> crate::Result<Option<LinkedModpackInfo>> {
let state = State::get().await?;
crate::state::get_linked_modpack_info(
context,
instance_id,
None,
cache_behaviour,
@@ -1,6 +1,7 @@
use super::content::get_projects;
use super::get::get;
use super::paths::get_full_path;
use crate::InvocationContext;
use crate::event::LoadingBarType;
use crate::event::emit::{emit_loading, init_loading};
use crate::pack::install_from::{
@@ -20,6 +21,7 @@ use tokio::io::AsyncReadExt;
#[tracing::instrument(skip_all)]
pub async fn export_mrpack(
context: &InvocationContext,
instance_id: &str,
export_path: PathBuf,
included_export_candidates: Vec<String>,
@@ -54,7 +56,7 @@ pub async fn export_mrpack(
let mut writer = ZipFileWriter::with_tokio(&mut file);
let version_id = version_id.unwrap_or("1.0.0".to_string());
let mut packfile =
create_mrpack_json(&metadata, version_id, description).await?;
create_mrpack_json(context, &metadata, version_id, description).await?;
packfile.files.retain(|f| {
is_export_candidate_included(
f.path.as_str(),
@@ -176,6 +178,7 @@ fn pack_get_relative_path(
#[tracing::instrument(skip_all)]
pub async fn create_mrpack_json(
context: &InvocationContext,
metadata: &InstanceMetadata,
version_id: String,
description: Option<String>,
@@ -212,6 +215,7 @@ pub async fn create_mrpack_json(
let state = State::get().await?;
let projects = get_projects(
context,
&metadata.instance.id,
Some(CacheBehaviour::MustRevalidate),
)
@@ -223,6 +227,7 @@ pub async fn create_mrpack_json(
})
.collect::<Vec<_>>();
let versions = CachedEntry::get_version_many(
context,
&projects.iter().map(|x| &*x.1).collect::<Vec<_>>(),
None,
&state.pool,
+8 -1
View File
@@ -1,6 +1,10 @@
use crate::state::{JavaVersion, State};
use crate::{
InvocationContext,
state::{JavaVersion, State},
};
pub async fn get_optimal_jre_key(
invocation_context: &InvocationContext,
instance_id: &str,
) -> crate::Result<Option<JavaVersion>> {
let state = State::get().await?;
@@ -17,18 +21,21 @@ pub async fn get_optimal_jre_key(
})?;
let (minecraft, version_index) =
crate::launcher::resolve_minecraft_manifest(
invocation_context,
&context.applied_content_set.game_version,
&state,
)
.await?;
let version = &minecraft.versions[version_index];
let loader_version = crate::launcher::get_loader_version_from_profile(
invocation_context,
&context.applied_content_set.game_version,
context.applied_content_set.loader,
context.applied_content_set.loader_version.as_deref(),
)
.await?;
let version_info = crate::launcher::download::download_version_info(
invocation_context,
&state,
version,
loader_version.as_ref(),
@@ -1,3 +1,4 @@
use crate::InvocationContext;
use crate::event::InstancePayloadType;
use crate::event::emit::emit_instance;
use crate::state::instances::adapters::sqlite::instance_rows;
@@ -11,6 +12,7 @@ use std::path::Path;
#[tracing::instrument]
#[allow(clippy::too_many_arguments)]
pub(crate) async fn create(
context: &InvocationContext,
name: String,
game_version: String,
modloader: ModLoader,
@@ -20,6 +22,7 @@ pub(crate) async fn create(
) -> crate::Result<InstanceMetadata> {
let state = State::get().await?;
let instance = crate::state::create_instance(
context,
CreateInstance {
name,
path: None,
+22 -1
View File
@@ -1,3 +1,4 @@
use crate::InvocationContext;
use crate::event::emit::{emit_instance, emit_loading, init_loading};
use crate::event::{InstancePayloadType, LoadingBarType};
use crate::state::instances::adapters::sqlite::instance_rows;
@@ -20,6 +21,7 @@ pub struct InstallProjectWithDependenciesRequest {
#[tracing::instrument]
pub async fn update_all_projects(
context: &InvocationContext,
instance_id: &str,
) -> crate::Result<HashMap<String, String>> {
let state = State::get().await?;
@@ -34,6 +36,7 @@ pub async fn update_all_projects(
)
.await?;
let map = crate::state::instances::commands::update_all_projects(
context,
instance_id,
&state,
)
@@ -46,12 +49,14 @@ pub async fn update_all_projects(
#[tracing::instrument]
pub async fn update_project(
context: &InvocationContext,
instance_id: &str,
project_path: &str,
skip_send_event: Option<bool>,
) -> crate::Result<String> {
let state = State::get().await?;
let path = crate::state::instances::commands::update_project(
context,
instance_id,
project_path,
&state,
@@ -67,6 +72,7 @@ pub async fn update_project(
#[tracing::instrument]
pub async fn add_project_from_version(
context: &InvocationContext,
instance_id: &str,
version_id: &str,
reason: fetch::DownloadReason,
@@ -75,6 +81,7 @@ pub async fn add_project_from_version(
let state = State::get().await?;
let project_path =
crate::state::instances::commands::add_project_from_version(
context,
instance_id,
version_id,
reason,
@@ -90,6 +97,7 @@ pub async fn add_project_from_version(
#[tracing::instrument]
pub async fn install_project_with_dependencies(
context: &InvocationContext,
instance_id: &str,
request: InstallProjectWithDependenciesRequest,
) -> crate::Result<ResolveContentPlan> {
@@ -98,6 +106,7 @@ pub async fn install_project_with_dependencies(
crate::ErrorKind::InputError("Unknown instance".to_string())
})?;
let plan = crate::state::instances::commands::resolve_install_plan(
context,
instance_id,
crate::state::instances::commands::InstanceInstallProjectRequest {
project_id: request.project_id,
@@ -112,8 +121,10 @@ pub async fn install_project_with_dependencies(
let instance_id = metadata.instance.id;
let project_ids = plan_project_ids(&plan);
let install_plan = plan.clone();
let context = context.clone();
tokio::spawn(async move {
match crate::state::instances::commands::install_resolved_content_plan(
&context,
&instance_id,
&install_plan,
&state,
@@ -176,6 +187,7 @@ fn plan_project_ids(plan: &ResolveContentPlan) -> Vec<String> {
#[tracing::instrument]
pub async fn switch_project_version_with_dependencies(
context: &InvocationContext,
instance_id: &str,
project_path: &str,
version_id: &str,
@@ -186,6 +198,7 @@ pub async fn switch_project_version_with_dependencies(
})?;
let path =
crate::state::instances::commands::switch_project_version_with_dependencies(
context,
instance_id,
project_path,
version_id,
@@ -214,10 +227,14 @@ pub async fn add_project_from_path(
}
#[tracing::instrument]
pub async fn is_file_on_modrinth(path: &Path) -> crate::Result<bool> {
pub async fn is_file_on_modrinth(
context: &InvocationContext,
path: &Path,
) -> crate::Result<bool> {
let state = State::get().await?;
let (_, hash) = fetch::sha1_file_async(path).await?;
let files = CachedEntry::get_file_many(
context,
&[&hash],
Some(CacheBehaviour::Bypass),
&state.pool,
@@ -266,6 +283,7 @@ pub async fn remove_project(
#[tracing::instrument]
pub async fn update_managed_modrinth_version(
context: &InvocationContext,
instance_id: &str,
version_id: &str,
) -> crate::Result<crate::install::InstallJobSnapshot> {
@@ -310,6 +328,7 @@ pub async fn update_managed_modrinth_version(
};
crate::install::install_pack_to_existing_instance(
context,
metadata.instance.id,
crate::api::pack::install_from::CreatePackLocation::FromVersionId {
project_id,
@@ -324,6 +343,7 @@ pub async fn update_managed_modrinth_version(
#[tracing::instrument]
pub async fn repair_managed_modrinth(
context: &InvocationContext,
instance_id: &str,
) -> crate::Result<crate::install::InstallJobSnapshot> {
let state = State::get().await?;
@@ -363,6 +383,7 @@ pub async fn repair_managed_modrinth(
};
crate::install::install_pack_to_existing_instance(
context,
metadata.instance.id,
crate::api::pack::install_from::CreatePackLocation::FromVersionId {
project_id,
+38 -20
View File
@@ -1,4 +1,5 @@
use super::content::get_projects;
use crate::InvocationContext;
use crate::server_address::ServerAddress;
use crate::state::{
Credentials, InstanceLink, ProcessMetadata, Settings, State,
@@ -20,6 +21,7 @@ pub enum QuickPlayType {
#[tracing::instrument]
pub async fn run(
invocation_context: &InvocationContext,
instance_id: &str,
quick_play_type: QuickPlayType,
) -> crate::Result<ProcessMetadata> {
@@ -28,18 +30,25 @@ pub async fn run(
.await?
.ok_or_else(|| crate::ErrorKind::NoCredentialsError.as_error())?;
run_credentials(instance_id, &default_account, quick_play_type).await
run_credentials(
invocation_context,
instance_id,
&default_account,
quick_play_type,
)
.await
}
#[tracing::instrument(skip(credentials))]
async fn run_credentials(
invocation_context: &InvocationContext,
instance_id: &str,
credentials: &Credentials,
quick_play_type: QuickPlayType,
) -> crate::Result<ProcessMetadata> {
let state = State::get().await?;
let settings = Settings::get(&state.pool).await?;
let context =
let launch_context =
crate::state::instances::commands::get_instance_launch_context(
instance_id,
&state.pool,
@@ -51,7 +60,7 @@ async fn run_credentials(
))
})?;
let pre_launch_hooks = context
let pre_launch_hooks = launch_context
.launch_overrides
.hooks
.pre_launch
@@ -72,7 +81,7 @@ async fn run_credentials(
state
.directories
.instances_dir()
.join(&context.instance.path),
.join(&launch_context.instance.path),
)?;
let result = Command::new(command)
.args(cmd)
@@ -93,29 +102,32 @@ async fn run_credentials(
}
}
let java_args = context
let java_args = launch_context
.launch_overrides
.extra_launch_args
.clone()
.unwrap_or(settings.extra_launch_args);
let wrapper = context
let wrapper = launch_context
.launch_overrides
.hooks
.wrapper
.clone()
.or(settings.hooks.wrapper)
.filter(|hook_command| !hook_command.is_empty());
let memory = context.launch_overrides.memory.unwrap_or(settings.memory);
let resolution = context
let memory = launch_context
.launch_overrides
.memory
.unwrap_or(settings.memory);
let resolution = launch_context
.launch_overrides
.game_resolution
.unwrap_or(settings.game_resolution);
let env_args = context
let env_args = launch_context
.launch_overrides
.custom_env_vars
.clone()
.unwrap_or(settings.custom_env_vars);
let post_exit_hook = context
let post_exit_hook = launch_context
.launch_overrides
.hooks
.post_exit
@@ -124,13 +136,13 @@ async fn run_credentials(
.filter(|hook_command| !hook_command.is_empty());
let mut mc_set_options: Vec<(String, String)> = vec![];
if let Some(fullscreen) = context.launch_overrides.force_fullscreen {
if let Some(fullscreen) = launch_context.launch_overrides.force_fullscreen {
mc_set_options.push(("fullscreen".to_string(), fullscreen.to_string()));
} else if settings.force_fullscreen {
mc_set_options.push(("fullscreen".to_string(), "true".to_string()));
}
if let Some(project_id) = server_play_project_id(&context.link)
if let Some(project_id) = server_play_project_id(&launch_context.link)
&& !project_id.trim().is_empty()
{
let server_id = uuid::Uuid::new_v4().to_string();
@@ -148,6 +160,7 @@ async fn run_credentials(
match join_result {
Ok(resp) if resp.status().is_success() => {
let result = fetch::post_json(
invocation_context,
concat!(
env!("MODRINTH_API_BASE_URL"),
"analytics/minecraft-server-play"
@@ -181,6 +194,7 @@ async fn run_credentials(
crate::minecraft_skins::flush_pending_skin_change().await?;
crate::launcher::launch_minecraft(
invocation_context,
&java_args,
&env_args,
&mc_set_options,
@@ -189,7 +203,7 @@ async fn run_credentials(
&resolution,
credentials,
post_exit_hook,
&context,
&launch_context,
quick_play_type,
)
.await
@@ -224,10 +238,11 @@ pub async fn kill(instance_id: &str) -> crate::Result<()> {
#[tracing::instrument]
pub async fn try_update_playtime_by_instance_id(
invocation_context: &InvocationContext,
instance_id: &str,
) -> crate::Result<()> {
let state = State::get().await?;
let context =
let launch_context =
crate::state::instances::commands::get_instance_launch_context(
instance_id,
&state.pool,
@@ -238,9 +253,9 @@ pub async fn try_update_playtime_by_instance_id(
"Tried to update playtime for nonexistent instance {instance_id}!"
))
})?;
let updated_recent_playtime = context.instance.recent_time_played;
let updated_recent_playtime = launch_context.instance.recent_time_played;
let res = if updated_recent_playtime > 0 {
let modrinth_pack_version_id = match &context.link {
let modrinth_pack_version_id = match &launch_context.link {
InstanceLink::ModrinthModpack { version_id, .. }
| InstanceLink::ServerProjectModpack {
content_version_id: version_id,
@@ -258,13 +273,15 @@ pub async fn try_update_playtime_by_instance_id(
};
let playtime_update_json = json!({
"seconds": updated_recent_playtime,
"loader": context.applied_content_set.loader.as_str(),
"game_version": &context.applied_content_set.game_version,
"loader": launch_context.applied_content_set.loader.as_str(),
"game_version": &launch_context.applied_content_set.game_version,
"parent": modrinth_pack_version_id,
});
let mut hashmap: HashMap<String, serde_json::Value> = HashMap::new();
for (_, project) in get_projects(instance_id, None).await? {
for (_, project) in
get_projects(invocation_context, instance_id, None).await?
{
if let Some(metadata) = project.metadata {
hashmap
.insert(metadata.version_id, playtime_update_json.clone());
@@ -272,6 +289,7 @@ pub async fn try_update_playtime_by_instance_id(
}
fetch::post_json(
invocation_context,
concat!(env!("MODRINTH_API_BASE_URL"), "analytics/playtime"),
serde_json::to_value(hashmap)?,
&state.api_semaphore,
@@ -284,7 +302,7 @@ pub async fn try_update_playtime_by_instance_id(
if res.is_ok() {
crate::state::instances::commands::mark_instance_playtime_submitted(
&context.instance.id,
&launch_context.instance.id,
updated_recent_playtime,
&state.pool,
)
+14 -5
View File
@@ -19,7 +19,7 @@ use sysinfo::{MemoryRefreshKind, RefreshKind};
use crate::util::io;
use crate::util::jre::extract_java_version;
use crate::{
LoadingBarType, State,
InvocationContext, LoadingBarType, State,
util::jre::{self},
};
@@ -59,22 +59,27 @@ pub async fn find_filtered_jres(
})
}
pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
auto_install_java_with_loading(java_version, true).await
pub async fn auto_install_java(
context: &InvocationContext,
java_version: u32,
) -> crate::Result<PathBuf> {
auto_install_java_with_loading(context, java_version, true).await
}
pub async fn auto_install_java_with_loading(
context: &InvocationContext,
java_version: u32,
show_loading: bool,
) -> crate::Result<PathBuf> {
auto_install_java_inner(java_version, show_loading, None).await
auto_install_java_inner(context, java_version, show_loading, None).await
}
pub async fn auto_install_java_with_reporter(
context: &InvocationContext,
java_version: u32,
reporter: InstallProgressReporter,
) -> crate::Result<PathBuf> {
auto_install_java_inner(java_version, false, Some(reporter)).await
auto_install_java_inner(context, java_version, false, Some(reporter)).await
}
const JAVA_INSTALL_STEPS: u64 = 4;
@@ -111,6 +116,7 @@ fn java_step_progress(current: u64) -> InstallProgress {
}
async fn auto_install_java_inner(
context: &InvocationContext,
java_version: u32,
show_loading: bool,
reporter: Option<InstallProgressReporter>,
@@ -167,6 +173,7 @@ async fn auto_install_java_inner(
.await?;
}
let packages = fetch_json::<Vec<Package>>(
context,
Method::GET,
&metadata_url,
None,
@@ -235,6 +242,7 @@ async fn auto_install_java_inner(
};
fetch_advanced_with_progress(
context,
Method::GET,
&download.download_url,
None,
@@ -250,6 +258,7 @@ async fn auto_install_java_inner(
.await?
} else {
fetch_advanced(
context,
Method::GET,
&download.download_url,
None,
+10 -3
View File
@@ -1,12 +1,15 @@
use crate::State;
use crate::state::CachedEntry;
use crate::{InvocationContext, State};
pub use daedalus::minecraft::VersionManifest;
pub use daedalus::modded::Manifest;
#[tracing::instrument]
pub async fn get_minecraft_versions() -> crate::Result<VersionManifest> {
pub async fn get_minecraft_versions(
context: &InvocationContext,
) -> crate::Result<VersionManifest> {
let state = State::get().await?;
let minecraft_versions = CachedEntry::get_minecraft_manifest(
context,
None,
&state.pool,
&state.api_semaphore,
@@ -20,11 +23,15 @@ pub async fn get_minecraft_versions() -> crate::Result<VersionManifest> {
}
// #[tracing::instrument]
pub async fn get_loader_versions(loader: &str) -> crate::Result<Manifest> {
pub async fn get_loader_versions(
context: &InvocationContext,
loader: &str,
) -> crate::Result<Manifest> {
let state = State::get().await?;
let cache_key =
daedalus::modded::loader_manifest_metadata(loader).cache_key;
let loaders = CachedEntry::get_loader_manifest(
context,
&cache_key,
None,
&state.pool,
+1 -1
View File
@@ -37,7 +37,7 @@ pub mod data {
pub mod prelude {
pub use crate::{
State,
InvocationContext, State,
data::*,
event::CommandPayload,
install, instance, jre, metadata, minecraft_auth, mr_auth, pack,
+18 -5
View File
@@ -1,3 +1,4 @@
use crate::InvocationContext;
use crate::state::ModrinthCredentials;
#[tracing::instrument]
@@ -7,11 +8,13 @@ pub fn authenticate_begin_flow() -> &'static str {
#[tracing::instrument]
pub async fn authenticate_finish_flow(
context: &InvocationContext,
code: &str,
) -> crate::Result<ModrinthCredentials> {
let state = crate::State::get().await?;
let creds = crate::state::finish_login_flow(
context,
code,
&state.api_semaphore,
&state.pool,
@@ -21,7 +24,12 @@ pub async fn authenticate_finish_flow(
creds.upsert(&state.pool).await?;
state
.friends_socket
.connect(&state.pool, &state.api_semaphore, &state.process_manager)
.connect(
context,
&state.pool,
&state.api_semaphore,
&state.process_manager,
)
.await?;
Ok(creds)
@@ -41,11 +49,16 @@ pub async fn logout() -> crate::Result<()> {
}
#[tracing::instrument]
pub async fn get_credentials() -> crate::Result<Option<ModrinthCredentials>> {
pub async fn get_credentials(
context: &InvocationContext,
) -> crate::Result<Option<ModrinthCredentials>> {
let state = crate::State::get().await?;
let current =
ModrinthCredentials::get_and_refresh(&state.pool, &state.api_semaphore)
.await?;
let current = ModrinthCredentials::get_and_refresh(
context,
&state.pool,
&state.api_semaphore,
)
.await?;
Ok(current)
}
@@ -125,6 +125,7 @@ pub async fn is_valid_atlauncher(instance_folder: PathBuf) -> bool {
#[tracing::instrument]
pub async fn import_atlauncher(
context: &crate::InvocationContext,
atlauncher_base_path: PathBuf, // path to base atlauncher folder
instance_folder: String, // instance folder in atlauncher_base_path
instance_id: &str,
@@ -178,6 +179,7 @@ pub async fn import_atlauncher(
let minecraft_folder = atlauncher_instance_path;
import_atlauncher_unmanaged(
context,
instance_id,
minecraft_folder,
backup_name,
@@ -191,6 +193,7 @@ pub async fn import_atlauncher(
}
async fn import_atlauncher_unmanaged(
context: &crate::InvocationContext,
instance_id: &str,
minecraft_folder: PathBuf,
backup_name: String,
@@ -214,6 +217,7 @@ async fn import_atlauncher_unmanaged(
let loader_version = if mod_loader != ModLoader::Vanilla {
crate::launcher::get_loader_version_from_profile(
context,
&game_version,
mod_loader,
Some(&atinstance.launcher.loader_version.version),
@@ -264,6 +268,7 @@ async fn import_atlauncher_unmanaged(
// Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
let state = State::get().await?;
finish_import(
context,
instance_id,
minecraft_folder,
&state.io_semaphore,
@@ -49,6 +49,7 @@ pub async fn is_valid_curseforge(instance_folder: PathBuf) -> bool {
}
pub async fn import_curseforge(
context: &crate::InvocationContext,
curseforge_instance_folder: PathBuf, // instance's folder
instance_id: &str,
reporter: InstallProgressReporter,
@@ -82,6 +83,7 @@ pub async fn import_curseforge(
}) = minecraft_instance.installed_modpack.clone()
{
let icon_bytes = fetch(
context,
&thumbnail_url,
None,
None,
@@ -128,6 +130,7 @@ pub async fn import_curseforge(
let loader_version = if mod_loader != ModLoader::Vanilla {
crate::launcher::get_loader_version_from_profile(
context,
&game_version,
mod_loader,
loader_version.as_deref(),
@@ -188,6 +191,7 @@ pub async fn import_curseforge(
// Copy in contained folders as overrides
let state = State::get().await?;
finish_import(
context,
instance_id,
curseforge_instance_folder,
&state.io_semaphore,
@@ -41,6 +41,7 @@ pub async fn is_valid_gdlauncher(instance_folder: PathBuf) -> bool {
}
pub async fn import_gdlauncher(
context: &crate::InvocationContext,
gdlauncher_instance_folder: PathBuf, // instance's folder
instance_id: &str,
reporter: InstallProgressReporter,
@@ -80,6 +81,7 @@ pub async fn import_gdlauncher(
let loader_version = if mod_loader != ModLoader::Vanilla {
crate::launcher::get_loader_version_from_profile(
context,
&game_version,
mod_loader,
loader_version.as_deref(),
@@ -116,6 +118,7 @@ pub async fn import_gdlauncher(
// Copy in contained folders as overrides
let state = State::get().await?;
finish_import(
context,
instance_id,
gdlauncher_instance_folder,
&state.io_semaphore,
+8 -3
View File
@@ -177,6 +177,7 @@ async fn load_instance_cfg(file_path: &Path) -> crate::Result<MMCInstance> {
// #[tracing::instrument]
pub async fn import_mmc(
context: &crate::InvocationContext,
mmc_base_path: PathBuf, // path to base mmc folder
instance_folder: String, // instance folder in mmc_base_path
instance_id: &str,
@@ -235,23 +236,24 @@ pub async fn import_mmc(
// Modrinth Managed Pack
// Kept separate as we may in the future want to add special handling for modrinth managed packs
import_mmc_unmanaged(instance_id, minecraft_folder, "Imported Modrinth Modpack".to_string(), description, mmc_pack, reporter, details).await?;
import_mmc_unmanaged(context, instance_id, minecraft_folder, "Imported Modrinth Modpack".to_string(), description, mmc_pack, reporter, details).await?;
}
Some(MMCManagedPackType::Flame | MMCManagedPackType::ATLauncher) => {
// For flame/atlauncher managed packs
// Treat as unmanaged, but with 'minecraft' folder instead of '.minecraft'
import_mmc_unmanaged(instance_id, minecraft_folder, "Imported Modpack".to_string(), description, mmc_pack, reporter, details).await?;
import_mmc_unmanaged(context, instance_id, minecraft_folder, "Imported Modpack".to_string(), description, mmc_pack, reporter, details).await?;
},
Some(_) => {
// For managed packs that aren't modrinth, flame, atlauncher
// Treat as unmanaged
import_mmc_unmanaged(instance_id, minecraft_folder, "ImportedModpack".to_string(), description, mmc_pack, reporter, details).await?;
import_mmc_unmanaged(context, instance_id, minecraft_folder, "ImportedModpack".to_string(), description, mmc_pack, reporter, details).await?;
},
_ => return Err(crate::ErrorKind::InputError("Instance is managed, but managed pack type not specified in instance.cfg".to_string()).into())
}
} else {
// Directly import unmanaged pack
import_mmc_unmanaged(
context,
instance_id,
minecraft_folder,
"Imported Modpack".to_string(),
@@ -266,6 +268,7 @@ pub async fn import_mmc(
}
async fn import_mmc_unmanaged(
context: &crate::InvocationContext,
instance_id: &str,
minecraft_folder: PathBuf,
backup_name: String,
@@ -315,6 +318,7 @@ async fn import_mmc_unmanaged(
.collect();
install_from::set_instance_information(
context,
instance_id.to_string(),
&description,
&backup_name,
@@ -327,6 +331,7 @@ async fn import_mmc_unmanaged(
// Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
let state = State::get().await?;
finish_import(
context,
instance_id,
minecraft_folder,
&state.io_semaphore,
@@ -115,6 +115,7 @@ pub async fn get_importable_instances(
}
pub(crate) async fn import_instance_with_reporter(
context: &crate::InvocationContext,
instance_id: &str,
launcher_type: ImportLauncherType,
base_path: PathBuf,
@@ -122,6 +123,7 @@ pub(crate) async fn import_instance_with_reporter(
reporter: InstallProgressReporter,
) -> crate::Result<()> {
import_instance_inner(
context,
instance_id,
launcher_type,
base_path,
@@ -132,6 +134,7 @@ pub(crate) async fn import_instance_with_reporter(
}
async fn import_instance_inner(
context: &crate::InvocationContext,
instance_id: &str,
launcher_type: ImportLauncherType,
base_path: PathBuf,
@@ -146,6 +149,7 @@ async fn import_instance_inner(
let res = match launcher_type {
ImportLauncherType::MultiMC | ImportLauncherType::PrismLauncher => {
mmc::import_mmc(
context,
base_path, // path to base mmc folder
instance_folder, // instance folder in mmc_base_path
instance_id,
@@ -156,6 +160,7 @@ async fn import_instance_inner(
}
ImportLauncherType::ATLauncher => {
atlauncher::import_atlauncher(
context,
base_path, // path to atlauncher folder
instance_folder, // instance folder in atlauncher
instance_id,
@@ -166,6 +171,7 @@ async fn import_instance_inner(
}
ImportLauncherType::GDLauncher => {
gdlauncher::import_gdlauncher(
context,
base_path.join("instances").join(instance_folder), // path to gdlauncher folder
instance_id,
reporter.clone(),
@@ -175,6 +181,7 @@ async fn import_instance_inner(
}
ImportLauncherType::Curseforge => {
curseforge::import_curseforge(
context,
base_path.join("Instances").join(instance_folder), // path to curseforge folder
instance_id,
reporter.clone(),
@@ -199,6 +206,7 @@ async fn import_instance_inner(
{
matched = true;
Box::pin(import_instance_inner(
context,
instance_id,
lt,
base_path,
@@ -408,6 +416,7 @@ pub(crate) async fn copy_dotminecraft_with_reporter(
}
pub(crate) async fn finish_import(
context: &crate::InvocationContext,
instance_id: &str,
dotminecraft: PathBuf,
io_semaphore: &IoSemaphore,
@@ -424,6 +433,7 @@ pub(crate) async fn finish_import(
.await?;
crate::launcher::install_minecraft_for_instance_id_with_reporter(
context,
instance_id,
false,
Some(reporter),
+12 -2
View File
@@ -164,6 +164,7 @@ pub struct CreatePackDescription {
}
pub async fn get_instance_from_pack(
context: &crate::InvocationContext,
location: CreatePackLocation,
) -> crate::Result<CreatePackInstance> {
match location {
@@ -194,6 +195,7 @@ pub async fn get_instance_from_pack(
let state = State::get().await?;
let (_, hash) = sha1_file_async(&path).await?;
match CachedEntry::get_file_many(
context,
&[&hash],
Some(CacheBehaviour::StaleWhileRevalidateSkipOffline),
&state.pool,
@@ -217,6 +219,7 @@ pub async fn get_instance_from_pack(
let external_files_in_modpack =
super::install_mrpack::get_external_files_from_mrpack(
context,
&CreatePackFile::Path(path),
)
.await?;
@@ -234,6 +237,7 @@ pub async fn get_instance_from_pack(
#[tracing::instrument(skip(reporter))]
#[allow(clippy::too_many_arguments)]
pub(crate) async fn generate_pack_from_version_id_with_reporter(
context: &crate::InvocationContext,
project_id: String,
version_id: String,
title: String,
@@ -246,6 +250,7 @@ pub(crate) async fn generate_pack_from_version_id_with_reporter(
let has_icon_url = icon_url.is_some();
let version = CachedEntry::get_version(
context,
&version_id,
Some(CacheBehaviour::Bypass),
&state.pool,
@@ -351,14 +356,15 @@ pub(crate) async fn generate_pack_from_version_id_with_reporter(
};
let progress = Some(&mut progress as &mut FetchProgressFn<'_>);
let context = InstallErrorContext::new("download modpack file")
let error_context = InstallErrorContext::new("download modpack file")
.urls(vec![url.clone()])
.maybe_expected_hash(hash.cloned())
.project_id(project_id.clone())
.version_id(version_id.clone())
.build();
reporter.set_context(context).await?;
reporter.set_context(error_context).await?;
let file = fetch_advanced_with_progress(
context,
Method::GET,
&url,
hash.map(|x| &**x),
@@ -378,6 +384,7 @@ pub(crate) async fn generate_pack_from_version_id_with_reporter(
.await?;
let project = CachedEntry::get_project(
context,
&version.project_id,
None,
&state.pool,
@@ -406,6 +413,7 @@ pub(crate) async fn generate_pack_from_version_id_with_reporter(
)
.await?;
let icon_bytes = fetch(
context,
&icon_url,
None,
None,
@@ -484,6 +492,7 @@ pub async fn generate_pack_from_file(
/// Sets generated instance attributes to the pack ones.
/// This includes the pack name, icon, game version, loader version, and loader
pub async fn set_instance_information(
context: &crate::InvocationContext,
instance_id: String,
description: &CreatePackDescription,
backup_name: &str,
@@ -527,6 +536,7 @@ pub async fn set_instance_information(
let mod_loader = mod_loader.unwrap_or(ModLoader::Vanilla);
let loader_version = if mod_loader != ModLoader::Vanilla {
crate::launcher::get_loader_version_from_profile(
context,
game_version,
mod_loader,
loader_version.cloned().as_deref(),
@@ -238,6 +238,7 @@ where
}
pub(crate) async fn get_external_files_from_mrpack(
invocation_context: &crate::InvocationContext,
file: &CreatePackFile,
) -> crate::Result<Vec<String>> {
let mut zip_reader = MrpackZipReader::new(file).await?;
@@ -299,6 +300,7 @@ pub(crate) async fn get_external_files_from_mrpack(
.map(|(_, hash)| hash.as_str())
.collect::<Vec<_>>();
let recognized_hashes = match CachedEntry::get_file_many(
invocation_context,
&hashes,
None,
&state.pool,
@@ -396,6 +398,7 @@ where
}
pub(crate) async fn install_zipped_mrpack_files_with_reporter(
invocation_context: &crate::InvocationContext,
create_pack: CreatePack,
ignore_lock: bool,
reason: DownloadReason,
@@ -565,6 +568,7 @@ pub(crate) async fn install_zipped_mrpack_files_with_reporter(
}
set_instance_information(
invocation_context,
instance_id.clone(),
&description,
&pack.name,
@@ -629,6 +633,7 @@ pub(crate) async fn install_zipped_mrpack_files_with_reporter(
.collect::<Vec<_>>();
let file_infos_by_hash = Arc::new(
CachedEntry::get_file_many(
invocation_context,
&file_info_hashes,
None,
&state.pool,
@@ -764,6 +769,7 @@ pub(crate) async fn install_zipped_mrpack_files_with_reporter(
let progress =
&mut report_download_progress as &mut FetchProgressFn<'_>;
let file = match fetch_mirrors_with_progress(
invocation_context,
&project
.downloads
.iter()
@@ -1060,6 +1066,7 @@ pub(crate) async fn install_zipped_mrpack_files_with_reporter(
}
crate::launcher::install_minecraft_for_instance_id_with_reporter(
invocation_context,
&instance_id,
false,
Some(reporter.clone()),
@@ -1088,6 +1095,7 @@ fn modpack_source_kind(version_id: Option<&str>) -> ContentSourceKind {
#[tracing::instrument(skip(mrpack_file))]
pub async fn remove_all_related_files(
invocation_context: &crate::InvocationContext,
instance_id: String,
mrpack_file: CreatePackFile,
) -> crate::Result<()> {
@@ -1145,6 +1153,7 @@ pub async fn remove_all_related_files(
// First, get project info by hash
let file_infos = CachedEntry::get_file_many(
invocation_context,
&all_hashes.iter().map(|x| &**x).collect::<Vec<_>>(),
None,
&state.pool,
+53 -31
View File
@@ -1,72 +1,94 @@
//! Theseus tag management interface
use crate::state::CachedEntry;
pub use crate::{
State,
InvocationContext, State,
state::{Category, DonationPlatform, GameVersion, Loader},
};
/// Get category tags
#[tracing::instrument]
pub async fn get_category_tags() -> crate::Result<Vec<Category>> {
pub async fn get_category_tags(
context: &InvocationContext,
) -> crate::Result<Vec<Category>> {
let state = State::get().await?;
let categories =
CachedEntry::get_categories(None, &state.pool, &state.api_semaphore)
.await?
.ok_or_else(|| {
crate::ErrorKind::NoValueFor("category tags".to_string())
})?;
let categories = CachedEntry::get_categories(
context,
None,
&state.pool,
&state.api_semaphore,
)
.await?
.ok_or_else(|| crate::ErrorKind::NoValueFor("category tags".to_string()))?;
Ok(categories)
}
/// Get report type tags
#[tracing::instrument]
pub async fn get_report_type_tags() -> crate::Result<Vec<String>> {
pub async fn get_report_type_tags(
context: &InvocationContext,
) -> crate::Result<Vec<String>> {
let state = State::get().await?;
let report_types =
CachedEntry::get_report_types(None, &state.pool, &state.api_semaphore)
.await?
.ok_or_else(|| {
crate::ErrorKind::NoValueFor("report type tags".to_string())
})?;
let report_types = CachedEntry::get_report_types(
context,
None,
&state.pool,
&state.api_semaphore,
)
.await?
.ok_or_else(|| {
crate::ErrorKind::NoValueFor("report type tags".to_string())
})?;
Ok(report_types)
}
/// Get loader tags
#[tracing::instrument]
pub async fn get_loader_tags() -> crate::Result<Vec<Loader>> {
pub async fn get_loader_tags(
context: &InvocationContext,
) -> crate::Result<Vec<Loader>> {
let state = State::get().await?;
let loaders =
CachedEntry::get_loaders(None, &state.pool, &state.api_semaphore)
.await?
.ok_or_else(|| {
crate::ErrorKind::NoValueFor("loader tags".to_string())
})?;
let loaders = CachedEntry::get_loaders(
context,
None,
&state.pool,
&state.api_semaphore,
)
.await?
.ok_or_else(|| crate::ErrorKind::NoValueFor("loader tags".to_string()))?;
Ok(loaders)
}
/// Get game version tags
#[tracing::instrument]
pub async fn get_game_version_tags() -> crate::Result<Vec<GameVersion>> {
pub async fn get_game_version_tags(
context: &InvocationContext,
) -> crate::Result<Vec<GameVersion>> {
let state = State::get().await?;
let game_versions =
CachedEntry::get_game_versions(None, &state.pool, &state.api_semaphore)
.await?
.ok_or_else(|| {
crate::ErrorKind::NoValueFor("game version tags".to_string())
})?;
let game_versions = CachedEntry::get_game_versions(
context,
None,
&state.pool,
&state.api_semaphore,
)
.await?
.ok_or_else(|| {
crate::ErrorKind::NoValueFor("game version tags".to_string())
})?;
Ok(game_versions)
}
/// Get donation platform tags
#[tracing::instrument]
pub async fn get_donation_platform_tags() -> crate::Result<Vec<DonationPlatform>>
{
pub async fn get_donation_platform_tags(
context: &InvocationContext,
) -> crate::Result<Vec<DonationPlatform>> {
let state = State::get().await?;
let donation_platforms = CachedEntry::get_donation_platforms(
context,
None,
&state.pool,
&state.api_semaphore,
+3
View File
@@ -923,6 +923,7 @@ mod servers_data {
}
pub async fn get_instance_protocol_version(
invocation_context: &crate::InvocationContext,
instance_id: &str,
) -> Result<Option<ProtocolVersion>> {
let metadata =
@@ -951,6 +952,7 @@ pub async fn get_instance_protocol_version(
let state = State::get().await?;
let (minecraft, version_index) =
crate::launcher::resolve_minecraft_manifest(
invocation_context,
&metadata.applied_content_set.game_version,
&state,
)
@@ -958,6 +960,7 @@ pub async fn get_instance_protocol_version(
let version = &minecraft.versions[version_index];
let loader_version = get_loader_version_from_profile(
invocation_context,
&metadata.applied_content_set.game_version,
metadata.applied_content_set.loader,
metadata.applied_content_set.loader_version.as_deref(),
+24
View File
@@ -79,6 +79,9 @@ pub enum ErrorKind {
#[error("Too many API errors, try again in {0} minutes")]
ApiIsDownError(u32),
#[error("Too many requests, retry in {}", format_seconds(*.retry_in_seconds))]
Ratelimited { retry_in_seconds: u64 },
#[error("{0}")]
LabrinthError(LabrinthError),
@@ -192,6 +195,27 @@ pub enum ErrorKind {
DiscordRichPresenceError(#[from] discord_rich_presence::error::Error),
}
fn format_seconds(seconds: u64) -> String {
let plural = |unit: u64| if unit == 1 { "" } else { "s" };
if seconds <= 59 {
return format!("{seconds} second{}", plural(seconds));
}
let minutes = seconds / 60;
let rem_seconds = seconds % 60;
if rem_seconds == 0 {
return format!("{minutes} minutes");
}
format!(
"{minutes} minute{} and {rem_seconds} second{}",
plural(minutes),
plural(rem_seconds)
)
}
#[derive(Debug)]
pub struct Error {
pub raw: Arc<ErrorKind>,
+99 -45
View File
@@ -6,7 +6,6 @@ use super::model::{
InstallRequest, InstallRollbackState, InstallTarget,
};
use super::{diagnostics, recovery, store};
use crate::ErrorKind;
use crate::api::pack::install_from::{
CreatePackLocation, generate_pack_from_file,
generate_pack_from_version_id_with_reporter, get_instance_from_pack,
@@ -19,11 +18,13 @@ use crate::state::{
ContentSourceKind, InstanceInstallStage, InstanceLink, ModLoader, State,
};
use crate::util::fetch::DownloadReason;
use crate::{ErrorKind, InvocationContext};
use std::collections::HashSet;
use std::path::PathBuf;
use uuid::Uuid;
pub async fn create_instance(
context: &InvocationContext,
name: String,
game_version: String,
loader: ModLoader,
@@ -31,64 +32,89 @@ pub async fn create_instance(
icon_path: Option<String>,
link: InstanceLink,
) -> crate::Result<InstallJobSnapshot> {
start(InstallRequest::CreateInstance {
name,
game_version,
loader,
loader_version,
icon_path,
link,
})
start(
context,
InstallRequest::CreateInstance {
name,
game_version,
loader,
loader_version,
icon_path,
link,
},
)
.await
}
pub async fn create_modpack_instance(
context: &InvocationContext,
location: CreatePackLocation,
post_install_edit: Option<InstallPostInstallEdit>,
) -> crate::Result<InstallJobSnapshot> {
start(InstallRequest::CreateModpackInstance {
location,
post_install_edit,
})
start(
context,
InstallRequest::CreateModpackInstance {
location,
post_install_edit,
},
)
.await
}
pub async fn import_instance(
context: &InvocationContext,
launcher_type: crate::api::pack::import::ImportLauncherType,
base_path: PathBuf,
instance_folder: String,
) -> crate::Result<InstallJobSnapshot> {
start(InstallRequest::ImportInstance {
launcher_type,
base_path,
instance_folder,
})
start(
context,
InstallRequest::ImportInstance {
launcher_type,
base_path,
instance_folder,
},
)
.await
}
pub async fn duplicate_instance(
context: &InvocationContext,
source_instance_id: String,
) -> crate::Result<InstallJobSnapshot> {
start(InstallRequest::DuplicateInstance { source_instance_id }).await
start(
context,
InstallRequest::DuplicateInstance { source_instance_id },
)
.await
}
pub async fn install_existing_instance(
context: &InvocationContext,
instance_id: String,
force: bool,
) -> crate::Result<InstallJobSnapshot> {
start(InstallRequest::InstallExistingInstance { instance_id, force }).await
start(
context,
InstallRequest::InstallExistingInstance { instance_id, force },
)
.await
}
pub async fn install_pack_to_existing_instance(
context: &InvocationContext,
instance_id: String,
location: CreatePackLocation,
post_install_edit: Option<InstallPostInstallEdit>,
) -> crate::Result<InstallJobSnapshot> {
start(InstallRequest::InstallPackToExistingInstance {
instance_id,
location,
post_install_edit,
})
start(
context,
InstallRequest::InstallPackToExistingInstance {
instance_id,
location,
post_install_edit,
},
)
.await
}
@@ -114,7 +140,10 @@ pub async fn job_support_details(job_id: Uuid) -> crate::Result<String> {
diagnostics::build_job_support_details(&job, &state).await
}
pub async fn retry_job(job_id: Uuid) -> crate::Result<InstallJobSnapshot> {
pub async fn retry_job(
context: &InvocationContext,
job_id: Uuid,
) -> crate::Result<InstallJobSnapshot> {
let state = State::get().await?;
let mut job = store::get_required(job_id, &state).await?;
@@ -138,7 +167,7 @@ pub async fn retry_job(job_id: Uuid) -> crate::Result<InstallJobSnapshot> {
job.state.progress.phase = InstallPhaseId::PreparingInstance;
job.state.progress.progress = None;
job.state.progress.details = InstallPhaseDetails::Empty;
prepare_initial_instance(&mut job.state, &state).await?;
prepare_initial_instance(context, &mut job.state, &state).await?;
job.state.record_event(InstallJobEventKind::JobQueued {
kind: job.state.request.kind(),
});
@@ -151,7 +180,7 @@ pub async fn retry_job(job_id: Uuid) -> crate::Result<InstallJobSnapshot> {
)
.await?;
emit_install_job(&record.snapshot()).await?;
spawn_job(job_id);
spawn_job(job_id, context.clone());
Ok(record.snapshot())
}
@@ -214,19 +243,23 @@ pub async fn dismiss_job(job_id: Uuid) -> crate::Result<()> {
store::dismiss(job_id, &state).await
}
async fn start(request: InstallRequest) -> crate::Result<InstallJobSnapshot> {
async fn start(
context: &InvocationContext,
request: InstallRequest,
) -> crate::Result<InstallJobSnapshot> {
let state = State::get().await?;
let id = Uuid::new_v4();
let mut job_state = InstallJobState::new(request);
prepare_initial_instance(&mut job_state, &state).await?;
prepare_initial_instance(context, &mut job_state, &state).await?;
let record =
store::insert(id, &job_state, InstallJobStatus::Queued, &state).await?;
emit_install_job(&record.snapshot()).await?;
spawn_job(id);
spawn_job(id, context.clone());
Ok(record.snapshot())
}
async fn prepare_initial_instance(
context: &InvocationContext,
job_state: &mut InstallJobState,
state: &State,
) -> crate::Result<()> {
@@ -240,6 +273,7 @@ async fn prepare_initial_instance(
link,
} => {
let metadata = crate::api::instance::create(
context,
name,
game_version,
loader,
@@ -259,7 +293,7 @@ async fn prepare_initial_instance(
location,
post_install_edit,
} => {
let preview = get_instance_from_pack(location).await?;
let preview = get_instance_from_pack(context, location).await?;
let name = post_install_edit
.as_ref()
.and_then(|edit| edit.name.clone())
@@ -281,6 +315,7 @@ async fn prepare_initial_instance(
.or_else(|| preview.link.clone())
.unwrap_or(InstanceLink::Unmanaged);
let metadata = crate::api::instance::create(
context,
name,
preview.game_version,
preview.modloader,
@@ -300,6 +335,7 @@ async fn prepare_initial_instance(
instance_folder, ..
} => {
let metadata = crate::api::instance::create(
context,
instance_folder,
"1.19.4".to_string(),
ModLoader::Vanilla,
@@ -325,6 +361,7 @@ async fn prepare_initial_instance(
)
})?;
let created = crate::api::instance::create(
context,
metadata.instance.name,
metadata.applied_content_set.game_version,
metadata.applied_content_set.loader,
@@ -351,9 +388,9 @@ async fn prepare_initial_instance(
Ok(())
}
fn spawn_job(job_id: Uuid) {
fn spawn_job(job_id: Uuid, context: InvocationContext) {
tokio::spawn(async move {
if let Err(error) = run_job(job_id).await {
if let Err(error) = run_job(&context, job_id).await {
tracing::error!(
"Install job {job_id} failed to update state: {error}"
);
@@ -361,7 +398,10 @@ fn spawn_job(job_id: Uuid) {
});
}
async fn run_job(job_id: Uuid) -> crate::Result<()> {
async fn run_job(
context: &InvocationContext,
job_id: Uuid,
) -> crate::Result<()> {
let state = State::get().await?;
let mut job = store::get_required(job_id, &state).await?;
@@ -387,7 +427,7 @@ async fn run_job(job_id: Uuid) -> crate::Result<()> {
.await?;
emit_install_job(&record.snapshot()).await?;
let result = run_request(job_id, &mut job_state, &state).await;
let result = run_request(context, job_id, &mut job_state, &state).await;
if let Ok(record) = store::get_required(job_id, &state).await {
job_state = record.state;
}
@@ -468,6 +508,7 @@ async fn run_job(job_id: Uuid) -> crate::Result<()> {
}
async fn run_request(
context: &InvocationContext,
job_id: Uuid,
job_state: &mut InstallJobState,
state: &State,
@@ -506,7 +547,7 @@ async fn run_request(
},
)
.await?;
let context =
let launch_context =
crate::state::instances::commands::get_instance_launch_context(
&instance_id,
&state.pool,
@@ -516,7 +557,8 @@ async fn run_request(
crate::ErrorKind::InputError("Unknown instance".to_string())
})?;
crate::launcher::install_minecraft_with_reporter(
&context,
context,
&launch_context,
false,
Some(InstallProgressReporter::new(job_id, job_state.clone())),
)
@@ -542,6 +584,7 @@ async fn run_request(
)
.await?;
install_pack(
context,
job_id,
job_state,
location,
@@ -575,6 +618,7 @@ async fn run_request(
)
.await?;
crate::api::pack::import::import_instance_with_reporter(
context,
&instance_id,
launcher_type,
base_path,
@@ -609,7 +653,7 @@ async fn run_request(
InstallPhaseDetails::Empty,
)
.await?;
let context =
let launch_context =
crate::state::instances::commands::get_instance_launch_context(
&instance_id,
&state.pool,
@@ -619,7 +663,8 @@ async fn run_request(
crate::ErrorKind::InputError("Unknown instance".to_string())
})?;
crate::launcher::install_minecraft_with_reporter(
&context,
context,
&launch_context,
false,
Some(InstallProgressReporter::new(job_id, job_state.clone())),
)
@@ -637,7 +682,7 @@ async fn run_request(
InstallPhaseDetails::Empty,
)
.await?;
let context =
let launch_context =
crate::state::instances::commands::get_instance_launch_context(
&instance_id,
&state.pool,
@@ -647,7 +692,8 @@ async fn run_request(
crate::ErrorKind::InputError("Unknown instance".to_string())
})?;
crate::launcher::install_minecraft_with_reporter(
&context,
context,
&launch_context,
force,
Some(InstallProgressReporter::new(job_id, job_state.clone())),
)
@@ -661,6 +707,7 @@ async fn run_request(
} => {
prepare_existing_rollback(job_state, state, &instance_id).await?;
let disabled_project_ids = remove_existing_pack_content(
context,
job_id,
job_state,
state,
@@ -668,6 +715,7 @@ async fn run_request(
)
.await?;
install_pack(
context,
job_id,
job_state,
location,
@@ -715,6 +763,7 @@ async fn apply_post_install_edit(
}
async fn remove_existing_pack_content(
context: &InvocationContext,
job_id: Uuid,
job_state: &InstallJobState,
state: &State,
@@ -761,6 +810,7 @@ async fn remove_existing_pack_content(
.collect::<HashSet<_>>();
let reporter = InstallProgressReporter::new(job_id, job_state.clone());
let old_pack = generate_pack_from_version_id_with_reporter(
context,
project_id.clone(),
version_id.clone(),
metadata.instance.name.clone(),
@@ -772,6 +822,7 @@ async fn remove_existing_pack_content(
.await?;
crate::api::pack::install_mrpack::remove_all_related_files(
context,
instance_id.to_string(),
old_pack.file,
)
@@ -871,6 +922,7 @@ async fn restore_disabled_projects(
}
async fn install_pack(
context: &InvocationContext,
job_id: Uuid,
job_state: &mut InstallJobState,
location: CreatePackLocation,
@@ -902,6 +954,7 @@ async fn install_pack(
)
.await?;
generate_pack_from_version_id_with_reporter(
context,
project_id,
version_id,
title,
@@ -925,6 +978,7 @@ async fn install_pack(
};
install_zipped_mrpack_files_with_reporter(
context,
create_pack,
false,
reason,
@@ -1074,9 +1128,9 @@ fn install_error_code(
PreparingJava => "java_error",
_ => "metadata_error",
},
ErrorKind::FetchError(_) | ErrorKind::ApiIsDownError(_) => {
"network_error"
}
ErrorKind::FetchError(_)
| ErrorKind::ApiIsDownError(_)
| ErrorKind::Ratelimited { .. } => "network_error",
ErrorKind::Any(_)
if matches!(
phase,
+42 -13
View File
@@ -7,6 +7,7 @@ use crate::install::{
use crate::instance::QuickPlayType;
use crate::launcher::parse_rules;
use crate::{
InvocationContext,
event::{
LoadingBarId,
emit::{emit_loading, loading_try_for_each_concurrent},
@@ -142,24 +143,33 @@ impl MinecraftDownloadProgress {
}
async fn fetch_minecraft_file(
invocation_context: &InvocationContext,
st: &State,
url: &str,
sha1: Option<&str>,
expected_size: Option<u64>,
progress: Option<MinecraftDownloadProgress>,
context: InstallErrorContext,
error_context: InstallErrorContext,
) -> crate::Result<bytes::Bytes> {
let mut context = context;
context.urls.push(url.to_string());
context.expected_hash = sha1.map(str::to_string);
context.expected_size = expected_size;
let mut error_context = error_context;
error_context.urls.push(url.to_string());
error_context.expected_hash = sha1.map(str::to_string);
error_context.expected_size = expected_size;
if let Some(progress) = &progress {
progress.set_context(context.clone()).await?;
progress.set_context(error_context.clone()).await?;
}
let Some(progress) = progress else {
return fetch(url, sha1, None, None, &st.fetch_semaphore, &st.pool)
.await;
return fetch(
invocation_context,
url,
sha1,
None,
None,
&st.fetch_semaphore,
&st.pool,
)
.await;
};
let last_downloaded = Arc::new(AtomicU64::new(0));
@@ -178,6 +188,7 @@ async fn fetch_minecraft_file(
};
let bytes = match fetch_advanced_with_progress(
invocation_context,
Method::GET,
url,
sha1,
@@ -194,7 +205,7 @@ async fn fetch_minecraft_file(
{
Ok(bytes) => bytes,
Err(error) => {
progress.persist_failure_context(context).await;
progress.persist_failure_context(error_context).await;
return Err(error);
}
};
@@ -384,6 +395,7 @@ fn missing_initial_minecraft_bytes(
#[tracing::instrument(skip(st, version))]
pub async fn download_minecraft(
context: &InvocationContext,
st: &State,
version: &GameVersionInfo,
loading_bar: Option<&LoadingBarId>,
@@ -415,6 +427,7 @@ pub async fn download_minecraft(
// 5
let assets_index = download_assets_index(
context,
st,
version,
loading_bar,
@@ -441,10 +454,10 @@ pub async fn download_minecraft(
tokio::try_join! {
// Total loading sums to 90/60
download_client(st, version, loading_bar, force, progress.clone()), // 9
download_log_config(st, version, loading_bar, force, progress.clone()),
download_assets(st, version.assets == "legacy", &assets_index, loading_bar, amount, force, progress.clone()), // 40
download_libraries(st, version.libraries.as_slice(), &version.id, loading_bar, amount, java_arch, force, minecraft_updated, progress.clone()) // 40
download_client(context, st, version, loading_bar, force, progress.clone()), // 9
download_log_config(context, st, version, loading_bar, force, progress.clone()),
download_assets(context, st, version.assets == "legacy", &assets_index, loading_bar, amount, force, progress.clone()), // 40
download_libraries(context, st, version.libraries.as_slice(), &version.id, loading_bar, amount, java_arch, force, minecraft_updated, progress.clone()) // 40
}?;
tracing::info!("Done downloading Minecraft!");
@@ -454,6 +467,7 @@ pub async fn download_minecraft(
#[tracing::instrument(skip_all, fields(version = version.id.as_str(), loader = ?loader))]
pub async fn download_version_info(
context: &InvocationContext,
st: &State,
version: &GameVersion,
loader: Option<&LoaderVersion>,
@@ -494,6 +508,7 @@ pub async fn download_version_info(
.await?;
}
let mut info = fetch_json(
context,
Method::GET,
&version.url,
None,
@@ -519,6 +534,7 @@ pub async fn download_version_info(
.await?;
}
let partial: d::modded::PartialVersionInfo = fetch_json(
context,
Method::GET,
&loader.url,
None,
@@ -548,6 +564,7 @@ pub async fn download_version_info(
#[tracing::instrument(skip_all)]
pub async fn download_client(
context: &InvocationContext,
st: &State,
version_info: &GameVersionInfo,
loading_bar: Option<&LoadingBarId>,
@@ -572,6 +589,7 @@ pub async fn download_client(
if !path.exists() || force {
let bytes = fetch_minecraft_file(
context,
st,
&client_download.url,
Some(&client_download.sha1),
@@ -598,6 +616,7 @@ pub async fn download_client(
#[tracing::instrument(skip_all)]
pub async fn download_assets_index(
context: &InvocationContext,
st: &State,
version: &GameVersionInfo,
loading_bar: Option<&LoadingBarId>,
@@ -617,6 +636,7 @@ pub async fn download_assets_index(
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
} else {
let index = fetch_minecraft_file(
context,
st,
&version.asset_index.url,
None,
@@ -645,6 +665,7 @@ pub async fn download_assets_index(
#[tracing::instrument(skip(st, index))]
pub async fn download_assets(
context: &InvocationContext,
st: &State,
with_legacy: bool,
index: &AssetsIndex,
@@ -691,6 +712,7 @@ pub async fn download_assets(
if should_fetch_object {
let resource = fetch_cell
.get_or_try_init(|| fetch_minecraft_file(
context,
st,
&url,
Some(hash),
@@ -711,6 +733,7 @@ pub async fn download_assets(
if should_fetch_legacy {
let resource = fetch_cell
.get_or_try_init(|| fetch_minecraft_file(
context,
st,
&url,
Some(hash),
@@ -740,6 +763,7 @@ pub async fn download_assets(
#[tracing::instrument(skip(st, libraries))]
#[allow(clippy::too_many_arguments)]
pub async fn download_libraries(
context: &InvocationContext,
st: &State,
libraries: &[Library],
version: &str,
@@ -796,6 +820,7 @@ pub async fn download_libraries(
if let Some(native) = classifiers.get(&parsed_key) {
let data = fetch_minecraft_file(
context,
st,
&native.url,
Some(&native.sha1),
@@ -851,6 +876,7 @@ pub async fn download_libraries(
&& !artifact.url.is_empty()
{
let bytes = fetch_minecraft_file(
context,
st,
&artifact.url,
Some(&artifact.sha1),
@@ -896,6 +922,7 @@ pub async fn download_libraries(
//
// See DEV-479.
match fetch(
context,
&url,
None,
None,
@@ -938,6 +965,7 @@ pub async fn download_libraries(
#[tracing::instrument(skip_all)]
pub async fn download_log_config(
context: &InvocationContext,
st: &State,
version_info: &GameVersionInfo,
loading_bar: Option<&LoadingBarId>,
@@ -962,6 +990,7 @@ pub async fn download_log_config(
if !path.exists() || force {
let bytes = fetch_minecraft_file(
context,
st,
&log_download.url,
Some(&log_download.sha1),
+68 -21
View File
@@ -156,6 +156,7 @@ pub async fn get_java_version_from_launch_context(
}
pub async fn get_loader_version_from_profile(
invocation_context: &crate::InvocationContext,
game_version: &str,
loader: ModLoader,
loader_version: Option<&str>,
@@ -172,8 +173,11 @@ pub async fn get_loader_version_from_profile(
id => it.id == *id,
};
let versions =
crate::api::metadata::get_loader_versions(loader.as_meta_str()).await?;
let versions = crate::api::metadata::get_loader_versions(
invocation_context,
loader.as_meta_str(),
)
.await?;
if let Some(loaders) =
loader_versions_for_game_version(&versions, game_version)
@@ -218,10 +222,13 @@ fn loader_versions_for_game_version<'a>(
/// game version. If the version isn't found in the cache, forces a manifest
/// refresh to pick up newly-released versions.
pub async fn resolve_minecraft_manifest(
invocation_context: &crate::InvocationContext,
game_version: &str,
state: &State,
) -> crate::Result<(d::minecraft::VersionManifest, usize)> {
let minecraft = crate::api::metadata::get_minecraft_versions().await?;
let minecraft =
crate::api::metadata::get_minecraft_versions(invocation_context)
.await?;
if let Some(idx) = minecraft
.versions
@@ -234,6 +241,7 @@ pub async fn resolve_minecraft_manifest(
// Version not found in cache — force a manifest refresh in case it was
// released after the cache was populated.
let refreshed = crate::state::CachedEntry::get_minecraft_manifest(
invocation_context,
Some(crate::state::CacheBehaviour::MustRevalidate),
&state.pool,
&state.api_semaphore,
@@ -262,12 +270,13 @@ async fn get_instance_full_path(instance_path: &str) -> crate::Result<PathBuf> {
}
pub async fn install_minecraft_with_reporter(
context: &InstanceLaunchContext,
invocation_context: &crate::InvocationContext,
launch_context: &InstanceLaunchContext,
repairing: bool,
reporter: Option<InstallProgressReporter>,
) -> crate::Result<()> {
let instance = &context.instance;
let content_set = &context.applied_content_set;
let instance = &launch_context.instance;
let content_set = &launch_context.applied_content_set;
let phase_details = InstallPhaseDetails::Minecraft {
game_version: content_set.game_version.clone(),
loader: content_set.loader,
@@ -309,8 +318,12 @@ pub async fn install_minecraft_with_reporter(
)
.await?;
}
let (minecraft, version_index) =
resolve_minecraft_manifest(&content_set.game_version, &state).await?;
let (minecraft, version_index) = resolve_minecraft_manifest(
invocation_context,
&content_set.game_version,
&state,
)
.await?;
let version = &minecraft.versions[version_index];
let minecraft_updated = version_index
<= minecraft
@@ -332,6 +345,7 @@ pub async fn install_minecraft_with_reporter(
}
let mut loader_version = get_loader_version_from_profile(
invocation_context,
&content_set.game_version,
content_set.loader,
content_set.loader_version.as_deref(),
@@ -341,6 +355,7 @@ pub async fn install_minecraft_with_reporter(
// If no loader version is selected, try to select the stable version!
if content_set.loader != ModLoader::Vanilla && loader_version.is_none() {
loader_version = get_loader_version_from_profile(
invocation_context,
&content_set.game_version,
content_set.loader,
Some("stable"),
@@ -362,6 +377,7 @@ pub async fn install_minecraft_with_reporter(
// Download version info (5)
let mut version_info = download::download_version_info(
invocation_context,
&state,
version,
loader_version.as_ref(),
@@ -392,18 +408,25 @@ pub async fn install_minecraft_with_reporter(
.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(launch_context, &version_info)
.await?
{
(std::path::PathBuf::from(java_version.path), false)
} else {
let path = if let Some(reporter) = &reporter {
crate::api::jre::auto_install_java_with_reporter(
invocation_context,
key,
reporter.clone(),
)
.await?
} else {
crate::api::jre::auto_install_java_with_loading(key, true).await?
crate::api::jre::auto_install_java_with_loading(
invocation_context,
key,
true,
)
.await?
};
(path, true)
@@ -443,6 +466,7 @@ pub async fn install_minecraft_with_reporter(
.await?;
}
download::download_minecraft(
invocation_context,
&state,
&version_info,
loading_bar.as_ref(),
@@ -622,12 +646,13 @@ pub async fn install_minecraft_with_reporter(
}
pub async fn install_minecraft_for_instance_id_with_reporter(
invocation_context: &crate::InvocationContext,
instance_id: &str,
repairing: bool,
reporter: Option<InstallProgressReporter>,
) -> crate::Result<()> {
let state = State::get().await?;
let context =
let launch_context =
crate::state::instances::commands::get_instance_launch_context(
instance_id,
&state.pool,
@@ -639,7 +664,13 @@ pub async fn install_minecraft_for_instance_id_with_reporter(
))
})?;
install_minecraft_with_reporter(&context, repairing, reporter).await
install_minecraft_with_reporter(
invocation_context,
&launch_context,
repairing,
reporter,
)
.await
}
pub async fn read_protocol_version_from_jar(
@@ -698,6 +729,7 @@ fn link_project_and_version(
#[tracing::instrument(skip_all)]
#[allow(clippy::too_many_arguments)]
pub async fn launch_minecraft(
invocation_context: &crate::InvocationContext,
java_args: &[String],
env_args: &[(String, String)],
mc_set_options: &[(String, String)],
@@ -706,11 +738,11 @@ pub async fn launch_minecraft(
resolution: &WindowSize,
credentials: &Credentials,
post_exit_hook: Option<String>,
context: &InstanceLaunchContext,
launch_context: &InstanceLaunchContext,
mut quick_play_type: QuickPlayType,
) -> crate::Result<ProcessMetadata> {
let instance = &context.instance;
let content_set = &context.applied_content_set;
let instance = &launch_context.instance;
let content_set = &launch_context.applied_content_set;
if instance.install_stage == InstanceInstallStage::PackInstalling
|| instance.install_stage == InstanceInstallStage::MinecraftInstalling
@@ -732,8 +764,12 @@ pub async fn launch_minecraft(
let instance_path = get_instance_full_path(&instance.path).await?;
let (minecraft, version_index) =
resolve_minecraft_manifest(&content_set.game_version, &state).await?;
let (minecraft, version_index) = resolve_minecraft_manifest(
invocation_context,
&content_set.game_version,
&state,
)
.await?;
let version = &minecraft.versions[version_index];
let minecraft_updated = version_index
<= minecraft
@@ -743,6 +779,7 @@ pub async fn launch_minecraft(
.unwrap_or(0);
let loader_version = get_loader_version_from_profile(
invocation_context,
&content_set.game_version,
content_set.loader,
content_set.loader_version.as_deref(),
@@ -763,6 +800,7 @@ pub async fn launch_minecraft(
});
let mut version_info = download::download_version_info(
invocation_context,
&state,
version,
loader_version.as_ref(),
@@ -780,6 +818,7 @@ pub async fn launch_minecraft(
.unwrap_or(0);
if requires_logging_info {
version_info = download::download_version_info(
invocation_context,
&state,
version,
loader_version.as_ref(),
@@ -791,11 +830,18 @@ pub async fn launch_minecraft(
}
}
let _ =
download_log_config(&state, &version_info, None, false, None).await?;
let _ = download_log_config(
invocation_context,
&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(launch_context, &version_info)
.await?
.ok_or_else(|| {
crate::ErrorKind::LauncherError(
@@ -1048,6 +1094,7 @@ pub async fn launch_minecraft(
state
.process_manager
.insert_new_process(
invocation_context,
&instance.id,
&instance.path,
&instance.name,
@@ -1062,7 +1109,7 @@ pub async fn launch_minecraft(
let instance_created_time = instance.created.to_rfc3339();
let instance_modified_time = instance.modified.to_rfc3339();
let (link_project_id, link_version_id) =
link_project_and_version(&context.link);
link_project_and_version(&launch_context.link);
let system_properties = [
("modrinth.process.startTime", Some(&process_start_time)),
("modrinth.profile.created", Some(&instance_created_time)),
+2
View File
@@ -16,6 +16,7 @@ mod event;
pub mod install;
mod launcher;
mod logger;
mod operation;
mod state;
pub use api::*;
@@ -25,6 +26,7 @@ pub use event::{
emit::init_loading,
};
pub use logger::start_logger;
pub use operation::{InvocationContext, REQUEST_CONTEXT_HEADER};
pub use state::State;
pub use util::fetch::DownloadReason;
+66
View File
@@ -0,0 +1,66 @@
use serde::Deserialize;
pub const REQUEST_CONTEXT_HEADER: &str = "Modrinth-App-Request-Context";
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct InvocationContext {
pub cause: String,
}
impl InvocationContext {
pub fn new(cause: impl Into<String>) -> Self {
Self {
cause: cause.into(),
}
}
pub fn cause(&self) -> &str {
&self.cause
}
pub fn request_context_header(&self) -> &str {
&self.cause
}
pub fn referer(&self) -> String {
format!("https://tauri.modrinth.app/_rc/{}", self.cause)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_frontend_defined_causes() {
let context: InvocationContext =
serde_json::from_str(r#"{"cause":"new/frontend/cause"}"#).unwrap();
assert_eq!(context.cause(), "new/frontend/cause");
}
#[test]
fn invocation_context_contains_only_a_cause() {
assert!(
serde_json::from_str::<InvocationContext>(
r#"{"cause":"navigation/home","id":"frontend-id"}"#
)
.is_err()
);
}
#[test]
fn headers_use_the_frontend_cause_unchanged() {
let context = InvocationContext::new("navigation/instance/content");
assert_eq!(
context.referer(),
"https://tauri.modrinth.app/_rc/navigation/instance/content"
);
assert_eq!(
context.request_context_header(),
"navigation/instance/content"
);
}
}
+30 -3
View File
@@ -1,3 +1,4 @@
use crate::InvocationContext;
use crate::state::ProjectType;
use crate::util::fetch::{FetchSemaphore, fetch_json, sha1_async};
use chrono::{DateTime, Utc};
@@ -804,17 +805,19 @@ macro_rules! impl_cache_methods {
paste::paste! {
#[tracing::instrument(skip(pool, fetch_semaphore))]
pub async fn [<get_ $variant:snake>](
context: &InvocationContext,
id: &str,
cache_behaviour: Option<CacheBehaviour>,
pool: &SqlitePool,
fetch_semaphore: &FetchSemaphore,
) -> crate::Result<Option<$type>>
{
Ok(Self::[<get_ $variant:snake _many>](&[id], cache_behaviour, pool, fetch_semaphore).await?.into_iter().next())
Ok(Self::[<get_ $variant:snake _many>](context, &[id], cache_behaviour, pool, fetch_semaphore).await?.into_iter().next())
}
#[tracing::instrument(skip(pool, fetch_semaphore))]
pub async fn [<get_ $variant:snake _many>](
context: &InvocationContext,
ids: &[&str],
cache_behaviour: Option<CacheBehaviour>,
pool: &SqlitePool,
@@ -822,7 +825,7 @@ macro_rules! impl_cache_methods {
) -> crate::Result<Vec<$type>>
{
let entry =
CachedEntry::get_many(CacheValueType::$variant, ids, cache_behaviour, pool, fetch_semaphore).await?;
CachedEntry::get_many(context, CacheValueType::$variant, ids, cache_behaviour, pool, fetch_semaphore).await?;
Ok(entry.into_iter().filter_map(|x| if let Some(CacheValue::$variant(value)) = x.data {
Some(value)
@@ -843,13 +846,14 @@ macro_rules! impl_cache_method_singular {
paste::paste! {
#[tracing::instrument(skip(pool, fetch_semaphore))]
pub async fn [<get_ $variant:snake>] (
context: &InvocationContext,
cache_behaviour: Option<CacheBehaviour>,
pool: &SqlitePool,
fetch_semaphore: &FetchSemaphore,
) -> crate::Result<Option<$type>>
{
let entry =
CachedEntry::get(CacheValueType::$variant, DEFAULT_ID, cache_behaviour, pool, fetch_semaphore).await?;
CachedEntry::get(context, CacheValueType::$variant, DEFAULT_ID, cache_behaviour, pool, fetch_semaphore).await?;
if let Some(CacheValue::$variant(value)) = entry.map(|x| x.data).flatten() {
Ok(Some(value))
@@ -890,6 +894,7 @@ impl_cache_method_singular!(
impl CachedEntry {
#[tracing::instrument(skip(pool, fetch_semaphore))]
pub async fn get(
context: &InvocationContext,
type_: CacheValueType,
key: &str,
cache_behaviour: Option<CacheBehaviour>,
@@ -897,6 +902,7 @@ impl CachedEntry {
fetch_semaphore: &FetchSemaphore,
) -> crate::Result<Option<Self>> {
Ok(Self::get_many(
context,
type_,
&[key],
cache_behaviour,
@@ -910,6 +916,7 @@ impl CachedEntry {
#[tracing::instrument(skip(pool, fetch_semaphore))]
pub async fn get_many(
context: &InvocationContext,
type_: CacheValueType,
keys: &[&str],
cache_behaviour: Option<CacheBehaviour>,
@@ -1017,6 +1024,7 @@ impl CachedEntry {
if !remaining_keys.is_empty() {
let res = Self::fetch_many(
context,
type_,
remaining_keys.clone(),
fetch_semaphore,
@@ -1057,11 +1065,13 @@ impl CachedEntry {
|| cache_behaviour
== CacheBehaviour::StaleWhileRevalidateSkipOffline)
{
let context = context.clone();
tokio::task::spawn(async move {
// TODO: if possible- find a way to do this without invoking state get
let state = crate::state::State::get().await?;
let values = Self::fetch_many(
&context,
type_,
expired_keys,
&state.api_semaphore,
@@ -1084,12 +1094,14 @@ impl CachedEntry {
}
async fn fetch_many(
context: &InvocationContext,
type_: CacheValueType,
keys: DashSet<impl Display + Eq + Hash + Serialize>,
fetch_semaphore: &FetchSemaphore,
pool: &SqlitePool,
) -> crate::Result<Vec<(Self, bool)>> {
async fn fetch_many_batched<T: DeserializeOwned>(
context: &InvocationContext,
method: Method,
api_url: &str,
url: &str,
@@ -1112,6 +1124,7 @@ impl CachedEntry {
let res = futures::future::try_join_all(urls.iter().map(|url| {
fetch_json::<Vec<_>>(
context,
method.clone(),
url,
None,
@@ -1129,6 +1142,7 @@ impl CachedEntry {
macro_rules! fetch_original_values {
($type:ident, $api_url:expr, $url_suffix:expr, $uri_path:expr, $cache_variant:path) => {{
let mut results = fetch_many_batched(
context,
Method::GET,
$api_url,
&format!("{}?ids=", $url_suffix),
@@ -1192,6 +1206,7 @@ impl CachedEntry {
vec![(
$cache_variant(
fetch_json(
context,
Method::GET,
&*format!("{}{}", $api_url, $url_suffix),
None,
@@ -1247,6 +1262,7 @@ impl CachedEntry {
}
CacheValueType::Team => {
let mut teams = fetch_many_batched::<Vec<TeamMember>>(
context,
Method::GET,
env!("MODRINTH_API_URL_V3"),
"teams?ids=",
@@ -1287,6 +1303,7 @@ impl CachedEntry {
}
CacheValueType::Organization => {
let mut orgs = fetch_many_batched::<Organization>(
context,
Method::GET,
env!("MODRINTH_API_URL_V3"),
"organizations?ids=",
@@ -1343,6 +1360,7 @@ impl CachedEntry {
}
CacheValueType::File => {
let mut versions = fetch_json::<HashMap<String, Version>>(
context,
Method::POST,
concat!(env!("MODRINTH_API_URL"), "version_files"),
None,
@@ -1419,6 +1437,7 @@ impl CachedEntry {
futures::future::try_join_all(fetch_urls.iter().map(
|(_, _, url)| {
fetch_json(
context,
Method::GET,
url,
None,
@@ -1644,6 +1663,7 @@ impl CachedEntry {
let variation = fetch_json::<
HashMap<String, Vec<Version>>,
>(
context,
Method::POST,
concat!(
env!("MODRINTH_API_URL"),
@@ -1779,6 +1799,7 @@ impl CachedEntry {
futures::future::try_join_all(fetch_urls.iter().map(
|(_, url)| {
fetch_json(
context,
Method::GET,
url,
None,
@@ -1821,6 +1842,7 @@ impl CachedEntry {
);
match fetch_json::<Vec<Version>>(
context,
Method::GET,
&url,
None,
@@ -1873,6 +1895,7 @@ impl CachedEntry {
futures::future::try_join_all(fetch_urls.iter().map(
|(_, url)| {
fetch_json(
context,
Method::GET,
url,
None,
@@ -2075,11 +2098,13 @@ impl CachedEntry {
/// Get modpack file hashes from cache
pub async fn get_modpack_files(
context: &InvocationContext,
version_id: &str,
pool: &SqlitePool,
fetch_semaphore: &FetchSemaphore,
) -> crate::Result<Option<CachedModpackFiles>> {
let entry = Self::get(
context,
CacheValueType::ModpackFiles,
version_id,
None,
@@ -2102,12 +2127,14 @@ impl CachedEntry {
/// Get versions for a project (without changelogs for fast loading)
#[tracing::instrument(skip(pool, fetch_semaphore))]
pub async fn get_project_versions(
context: &InvocationContext,
project_id: &str,
cache_behaviour: Option<CacheBehaviour>,
pool: &SqlitePool,
fetch_semaphore: &FetchSemaphore,
) -> crate::Result<Option<Vec<Version>>> {
let entry = Self::get(
context,
CacheValueType::ProjectVersions,
project_id,
cache_behaviour,
+12 -1
View File
@@ -1,4 +1,5 @@
use crate::ErrorKind;
use crate::InvocationContext;
use crate::data::ModrinthCredentials;
use crate::event::FriendPayload;
use crate::event::emit::{emit_friend, emit_notification};
@@ -68,12 +69,14 @@ impl FriendsSocket {
#[tracing::instrument(skip_all)]
pub async fn connect(
&self,
context: &InvocationContext,
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
semaphore: &FetchSemaphore,
process_manager: &ProcessManager,
) -> crate::Result<()> {
let credentials =
ModrinthCredentials::get_and_refresh(exec, semaphore).await?;
ModrinthCredentials::get_and_refresh(context, exec, semaphore)
.await?;
if let Some(credentials) = credentials {
let mut request = format!(
@@ -255,6 +258,7 @@ impl FriendsSocket {
#[tracing::instrument(skip_all)]
pub async fn socket_loop() -> crate::Result<()> {
let state = crate::State::get().await?;
let context = InvocationContext::new("background/friends");
tokio::task::spawn(async move {
let mut last_connection = Utc::now();
@@ -272,6 +276,7 @@ impl FriendsSocket {
let _ = state
.friends_socket
.connect(
&context,
&state.pool,
&state.api_semaphore,
&state.process_manager,
@@ -321,10 +326,12 @@ impl FriendsSocket {
#[tracing::instrument(skip_all)]
pub async fn friends(
context: &InvocationContext,
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
semaphore: &FetchSemaphore,
) -> crate::Result<Vec<UserFriend>> {
fetch_json(
context,
Method::GET,
concat!(env!("MODRINTH_API_URL_V3"), "friends"),
None,
@@ -346,11 +353,13 @@ impl FriendsSocket {
#[tracing::instrument(skip(exec, semaphore))]
pub async fn add_friend(
context: &InvocationContext,
user_id: &str,
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
semaphore: &FetchSemaphore,
) -> crate::Result<()> {
let result = fetch_advanced(
context,
Method::POST,
&format!("{}friend/{user_id}", env!("MODRINTH_API_URL_V3")),
None,
@@ -380,11 +389,13 @@ impl FriendsSocket {
#[tracing::instrument(skip(exec, semaphore))]
pub async fn remove_friend(
context: &InvocationContext,
user_id: &str,
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
semaphore: &FetchSemaphore,
) -> crate::Result<()> {
fetch_advanced(
context,
Method::DELETE,
&format!("{}friend/{user_id}", env!("MODRINTH_API_URL_V3")),
None,
@@ -1,3 +1,4 @@
use crate::InvocationContext;
use crate::state::instances::{
ContentRequirement, ContentSourceKind, Instance, InstanceFile,
adapters::sqlite::{content_rows, instance_rows},
@@ -45,6 +46,7 @@ pub(crate) struct InstanceInstallProjectRequest {
}
struct CachedEntryContentProvider<'a> {
context: &'a InvocationContext,
state: &'a State,
cache_behaviour: Option<CacheBehaviour>,
}
@@ -57,6 +59,7 @@ impl ContentMetadataProvider for CachedEntryContentProvider<'_> {
) -> Result<Option<modrinth_content_management::Version>, ResolveError>
{
let version = CachedEntry::get_version(
self.context,
version_id,
self.cache_behaviour,
&self.state.pool,
@@ -73,6 +76,7 @@ impl ContentMetadataProvider for CachedEntryContentProvider<'_> {
project_id: &str,
) -> Result<Vec<modrinth_content_management::Version>, ResolveError> {
let versions = CachedEntry::get_project_versions(
self.context,
project_id,
self.cache_behaviour,
&self.state.pool,
@@ -157,6 +161,7 @@ fn target_preferences(
}
pub(crate) async fn resolve_install_plan(
context: &InvocationContext,
instance_id: &str,
request: InstanceInstallProjectRequest,
state: &State,
@@ -171,12 +176,14 @@ pub(crate) async fn resolve_install_plan(
})?;
let existing_project_ids =
crate::state::get_installed_project_ids_for_instance(
context,
instance_id,
None,
state,
)
.await?;
let provider = CachedEntryContentProvider {
context,
state,
cache_behaviour: Some(CacheBehaviour::MustRevalidate),
};
@@ -200,11 +207,13 @@ pub(crate) async fn resolve_install_plan(
}
pub(crate) async fn install_resolved_content_plan(
context: &InvocationContext,
instance_id: &str,
plan: &ResolveContentPlan,
state: &State,
) -> crate::Result<()> {
add_resolved_content(
context,
instance_id,
&plan.primary,
DownloadReason::Standalone,
@@ -213,6 +222,7 @@ pub(crate) async fn install_resolved_content_plan(
.await?;
for dependency in &plan.dependencies {
add_resolved_content(
context,
instance_id,
dependency,
DownloadReason::Dependency,
@@ -225,12 +235,14 @@ pub(crate) async fn install_resolved_content_plan(
}
pub(crate) async fn switch_project_version_with_dependencies(
context: &InvocationContext,
instance_id: &str,
project_path: &str,
version_id: &str,
state: &State,
) -> crate::Result<String> {
let version = CachedEntry::get_version(
context,
version_id,
Some(CacheBehaviour::MustRevalidate),
&state.pool,
@@ -246,6 +258,7 @@ pub(crate) async fn switch_project_version_with_dependencies(
.map(ContentType::from)
.unwrap_or(ContentType::Mod);
let plan = resolve_install_plan(
context,
instance_id,
InstanceInstallProjectRequest {
project_id: version.project_id,
@@ -259,6 +272,7 @@ pub(crate) async fn switch_project_version_with_dependencies(
let was_disabled = project_path.ends_with(".disabled");
let mut new_path = add_project_from_version(
context,
instance_id,
&plan.primary.version_id,
DownloadReason::Update,
@@ -276,6 +290,7 @@ pub(crate) async fn switch_project_version_with_dependencies(
for dependency in &plan.dependencies {
add_resolved_content(
context,
instance_id,
dependency,
DownloadReason::Dependency,
@@ -299,12 +314,14 @@ pub(crate) async fn switch_project_version_with_dependencies(
}
async fn add_resolved_content(
context: &InvocationContext,
instance_id: &str,
content: &ResolvedContent,
reason: DownloadReason,
state: &State,
) -> crate::Result<String> {
add_project_from_version(
context,
instance_id,
&content.version_id,
reason,
@@ -342,6 +359,7 @@ pub(crate) async fn resolve_content_scope(
}
pub(crate) async fn add_project_from_version(
context: &InvocationContext,
instance_id: &str,
version_id: &str,
reason: DownloadReason,
@@ -350,6 +368,7 @@ pub(crate) async fn add_project_from_version(
state: &State,
) -> crate::Result<String> {
let downloaded = download_project_version(
context,
instance_id,
version_id,
reason,
@@ -363,6 +382,7 @@ pub(crate) async fn add_project_from_version(
}
pub(crate) async fn download_project_version(
context: &InvocationContext,
instance_id: &str,
version_id: &str,
reason: DownloadReason,
@@ -380,6 +400,7 @@ pub(crate) async fn download_project_version(
))
})?;
let version = CachedEntry::get_version(
context,
version_id,
None,
&state.pool,
@@ -408,6 +429,7 @@ pub(crate) async fn download_project_version(
dependent_on: dependent_on_version_id,
};
let bytes = fetch::fetch(
context,
&file.url,
file.hashes.get("sha1").map(|hash| hash.as_str()),
Some(&download_meta),
@@ -1,3 +1,4 @@
use crate::InvocationContext;
use crate::state::instances::{
ContentEntry, ContentSet, ContentSourceKind, InstanceFile,
adapters::sqlite::{content_rows, instance_rows},
@@ -63,11 +64,13 @@ struct ResolvedDependency {
}
pub(crate) async fn update_project(
context: &InvocationContext,
instance_id: &str,
project_path: &str,
state: &State,
) -> crate::Result<String> {
let updates = check_content_updates(
context,
instance_id,
Some(CacheBehaviour::MustRevalidate),
state,
@@ -82,16 +85,19 @@ pub(crate) async fn update_project(
)
})?;
apply_content_update(instance_id, project_path, &update, state).await
apply_content_update(context, instance_id, project_path, &update, state)
.await
}
async fn apply_content_update(
context: &InvocationContext,
instance_id: &str,
project_path: &str,
update: &ContentUpdate,
state: &State,
) -> crate::Result<String> {
let mut new_path = add_project_from_version(
context,
instance_id,
&update.update_version_id,
DownloadReason::Update,
@@ -122,6 +128,7 @@ async fn apply_content_update(
}
pub(crate) async fn update_all_projects(
context: &InvocationContext,
instance_id: &str,
state: &State,
) -> crate::Result<HashMap<String, String>> {
@@ -132,12 +139,17 @@ pub(crate) async fn update_all_projects(
0,
)
.await?;
let plan = plan_bulk_update(instance_id, state).await?;
let plan = plan_bulk_update(context, instance_id, state).await?;
let download_total =
plan.project_updates.len() + plan.dependency_additions.len();
let downloads =
download_planned_projects(instance_id, &plan, download_total, state)
.await?;
let downloads = download_planned_projects(
context,
instance_id,
&plan,
download_total,
state,
)
.await?;
let mut changed = HashMap::new();
emit_bulk_update_progress(
@@ -198,6 +210,7 @@ pub(crate) async fn update_all_projects(
}
async fn download_planned_projects(
context: &InvocationContext,
instance_id: &str,
plan: &BulkUpdatePlan,
total: usize,
@@ -226,6 +239,7 @@ async fn download_planned_projects(
match download {
PlannedDownload::ProjectUpdate(update) => {
let downloaded = download_project_version(
context,
instance_id,
&update.update_version_id,
DownloadReason::Update,
@@ -240,6 +254,7 @@ async fn download_planned_projects(
}
PlannedDownload::DependencyAddition(dependency) => {
let downloaded = download_project_version(
context,
instance_id,
&dependency.version_id,
DownloadReason::Dependency,
@@ -292,11 +307,12 @@ async fn emit_bulk_update_progress(
}
async fn plan_bulk_update(
context: &InvocationContext,
instance_id: &str,
state: &State,
) -> crate::Result<BulkUpdatePlan> {
let updateable_paths =
bulk_updateable_project_paths(instance_id, state).await?;
bulk_updateable_project_paths(context, instance_id, state).await?;
if updateable_paths.is_empty() {
return Ok(BulkUpdatePlan {
project_updates: Vec::new(),
@@ -305,6 +321,7 @@ async fn plan_bulk_update(
}
let updates = check_content_updates(
context,
instance_id,
Some(CacheBehaviour::MustRevalidate),
state,
@@ -364,6 +381,7 @@ async fn plan_bulk_update(
let version_id_refs =
version_ids.iter().map(|id| id.as_str()).collect::<Vec<_>>();
let versions = CachedEntry::get_version_many(
context,
&version_id_refs,
Some(CacheBehaviour::MustRevalidate),
&state.pool,
@@ -388,7 +406,8 @@ async fn plan_bulk_update(
})
.collect::<Vec<_>>();
let planned_dependencies =
dependency_closure(planned_versions, &content_set, state).await?;
dependency_closure(context, planned_versions, &content_set, state)
.await?;
let dependency_additions = planned_dependencies
.values()
.filter(|dependency| {
@@ -415,10 +434,12 @@ async fn plan_bulk_update(
}
async fn bulk_updateable_project_paths(
context: &InvocationContext,
instance_id: &str,
state: &State,
) -> crate::Result<HashSet<String>> {
let items = super::list_content::list_content(
context,
instance_id,
None,
Some(CacheBehaviour::MustRevalidate),
@@ -476,6 +497,7 @@ fn installed_project_from_row(
}
async fn dependency_closure(
context: &InvocationContext,
root_versions: Vec<Version>,
content_set: &ContentSet,
state: &State,
@@ -497,6 +519,7 @@ async fn dependency_closure(
}
let Some(dependency_version) = resolve_dependency_version(
context,
dependency,
content_set,
state,
@@ -536,6 +559,7 @@ fn is_required_dependency(
}
async fn resolve_dependency_version(
context: &InvocationContext,
dependency: &Dependency,
content_set: &ContentSet,
state: &State,
@@ -543,15 +567,19 @@ async fn resolve_dependency_version(
project_versions_cache: &mut HashMap<String, Option<Vec<Version>>>,
) -> crate::Result<Option<Version>> {
if let Some(version_id) = &dependency.version_id {
return cached_version(version_id, version_cache, state).await;
return cached_version(context, version_id, version_cache, state).await;
}
let Some(project_id) = &dependency.project_id else {
return Ok(None);
};
let Some(mut versions) =
cached_project_versions(project_id, project_versions_cache, state)
.await?
let Some(mut versions) = cached_project_versions(
context,
project_id,
project_versions_cache,
state,
)
.await?
else {
return Ok(None);
};
@@ -562,12 +590,14 @@ async fn resolve_dependency_version(
}
async fn cached_version(
context: &InvocationContext,
version_id: &str,
version_cache: &mut HashMap<String, Option<Version>>,
state: &State,
) -> crate::Result<Option<Version>> {
if !version_cache.contains_key(version_id) {
let version = CachedEntry::get_version(
context,
version_id,
Some(CacheBehaviour::MustRevalidate),
&state.pool,
@@ -581,12 +611,14 @@ async fn cached_version(
}
async fn cached_project_versions(
context: &InvocationContext,
project_id: &str,
project_versions_cache: &mut HashMap<String, Option<Vec<Version>>>,
state: &State,
) -> crate::Result<Option<Vec<Version>>> {
if !project_versions_cache.contains_key(project_id) {
let versions = CachedEntry::get_project_versions(
context,
project_id,
Some(CacheBehaviour::MustRevalidate),
&state.pool,
@@ -1,3 +1,4 @@
use crate::InvocationContext;
use crate::state::instances::{
ContentEntry, InstanceFile,
adapters::sqlite::{content_rows, instance_rows},
@@ -27,6 +28,7 @@ struct UpdateCandidate {
}
pub(crate) async fn check_content_updates(
context: &InvocationContext,
instance_id: &str,
cache_behaviour: Option<CacheBehaviour>,
state: &State,
@@ -53,12 +55,13 @@ pub(crate) async fn check_content_updates(
entry.file_id.as_deref().map(|file_id| (file_id, entry))
})
.collect::<HashMap<_, _>>();
let files = sync_instance_content_files(&instance, state).await?;
let files = sync_instance_content_files(context, &instance, state).await?;
let hashes = files
.iter()
.map(|file| file.sha1.as_str())
.collect::<Vec<_>>();
let file_info = CachedEntry::get_file_many(
context,
&hashes,
cache_behaviour,
&state.pool,
@@ -91,7 +94,8 @@ pub(crate) async fn check_content_updates(
}
let installed_channels =
installed_update_channels(&candidates, cache_behaviour, state).await?;
installed_update_channels(context, &candidates, cache_behaviour, state)
.await?;
let update_keys = candidates
.iter()
.map(|candidate| {
@@ -112,6 +116,7 @@ pub(crate) async fn check_content_updates(
.map(|key| key.as_str())
.collect::<Vec<_>>();
let updates = CachedEntry::get_file_update_many(
context,
&update_key_refs,
cache_behaviour,
&state.pool,
@@ -159,6 +164,7 @@ pub(crate) async fn check_content_updates(
}
async fn installed_update_channels(
context: &InvocationContext,
candidates: &[UpdateCandidate],
cache_behaviour: Option<CacheBehaviour>,
state: &State,
@@ -168,6 +174,7 @@ async fn installed_update_channels(
.map(|candidate| candidate.current_version_id.as_str())
.collect::<Vec<_>>();
let versions = CachedEntry::get_version_many(
context,
&version_ids,
cache_behaviour,
&state.pool,
@@ -1,3 +1,4 @@
use crate::InvocationContext;
use crate::launcher::get_loader_version_from_profile;
use crate::state::instances::{
ContentSet, ContentSetStatus, ContentSourceKind, Instance,
@@ -27,6 +28,7 @@ pub struct CreateInstance {
}
pub(crate) async fn create_instance(
context: &InvocationContext,
input: CreateInstance,
state: &State,
) -> crate::Result<Instance> {
@@ -45,6 +47,7 @@ pub(crate) async fn create_instance(
let loader_version = if input.loader != ModLoader::Vanilla {
get_loader_version_from_profile(
context,
&input.game_version,
input.loader,
input.loader_version.as_deref(),
@@ -56,7 +59,8 @@ pub(crate) async fn create_instance(
};
let icon_path =
resolve_icon_path(input.icon_path.as_deref(), state).await?;
resolve_icon_path(context, input.icon_path.as_deref(), state)
.await?;
let now = Utc::now();
let instance_id = format!("local:{}", Uuid::new_v4());
let content_set_id = format!("content-set:{}", Uuid::new_v4());
@@ -167,6 +171,7 @@ async fn path_available(
}
async fn resolve_icon_path(
context: &InvocationContext,
icon_path: Option<&str>,
state: &State,
) -> crate::Result<Option<String>> {
@@ -178,6 +183,7 @@ async fn resolve_icon_path(
|| icon.starts_with("http://")
{
let fetched = fetch::fetch(
context,
icon,
None,
None,
@@ -1,7 +1,6 @@
use super::sync_content_files::{
project_type_for_file, sync_instance_content_files,
};
use crate::State;
use crate::pack::install_from::{PackFileHash, PackFormat};
use crate::state::instances::adapters::sqlite;
use crate::state::instances::{
@@ -17,6 +16,7 @@ use crate::state::{
use crate::util::fetch::{
DownloadMeta, DownloadReason, FetchSemaphore, fetch_mirrors, sha1_async,
};
use crate::{InvocationContext, State};
use async_zip::base::read::seek::ZipFileReader;
use dashmap::DashMap;
use sqlx::SqlitePool;
@@ -59,6 +59,7 @@ pub(crate) async fn list_content_sets(
}
pub(crate) async fn get_content_projects(
context: &InvocationContext,
instance_id: &str,
content_set_id: Option<&str>,
cache_behaviour: Option<CacheBehaviour>,
@@ -72,6 +73,7 @@ pub(crate) async fn get_content_projects(
.await?;
content_projects_for_scope(
context,
&resolved,
cache_behaviour,
state,
@@ -81,12 +83,14 @@ pub(crate) async fn get_content_projects(
}
pub(crate) async fn get_installed_project_ids_for_instance(
context: &InvocationContext,
instance_id: &str,
content_set_id: Option<&str>,
state: &State,
) -> crate::Result<Vec<String>> {
let projects =
get_content_projects(instance_id, content_set_id, None, state).await?;
get_content_projects(context, instance_id, content_set_id, None, state)
.await?;
Ok(projects
.into_iter()
@@ -191,6 +195,7 @@ fn instance_matches_targets(
}
pub(crate) async fn list_content(
context: &InvocationContext,
instance_id: &str,
content_set_id: Option<&str>,
cache_behaviour: Option<CacheBehaviour>,
@@ -215,6 +220,7 @@ pub(crate) async fn list_content(
match linked_modpack_ids(&link) {
Some((_, version_id)) => {
get_cached_modpack_identifiers(
context,
&version_id,
&state.pool,
&state.api_semaphore,
@@ -240,12 +246,18 @@ pub(crate) async fn list_content(
} else {
ContentFilter::All
};
let files =
content_projects_for_scope(&resolved, cache_behaviour, state, filter)
.await?;
let files = content_projects_for_scope(
context,
&resolved,
cache_behaviour,
state,
filter,
)
.await?;
let files = files.into_iter().collect::<Vec<_>>();
content_files_to_content_items(
context,
&resolved.instance,
&files,
cache_behaviour,
@@ -255,6 +267,7 @@ pub(crate) async fn list_content(
}
pub(crate) async fn list_linked_modpack_content(
context: &InvocationContext,
instance_id: &str,
content_set_id: Option<&str>,
cache_behaviour: Option<CacheBehaviour>,
@@ -273,6 +286,7 @@ pub(crate) async fn list_linked_modpack_content(
.await?;
if is_imported_modpack_scope(&link) {
let files = content_projects_for_scope(
context,
&resolved,
cache_behaviour,
state,
@@ -286,6 +300,7 @@ pub(crate) async fn list_linked_modpack_content(
let files = files.into_iter().collect::<Vec<_>>();
return content_files_to_content_items(
context,
&resolved.instance,
&files,
cache_behaviour,
@@ -298,6 +313,7 @@ pub(crate) async fn list_linked_modpack_content(
return Ok(Vec::new());
};
let ids = match get_modpack_identifiers(
context,
&version_id,
&resolved.content_set,
&state.pool,
@@ -312,6 +328,7 @@ pub(crate) async fn list_linked_modpack_content(
}
};
let files = content_projects_for_scope(
context,
&resolved,
cache_behaviour,
state,
@@ -321,6 +338,7 @@ pub(crate) async fn list_linked_modpack_content(
let files = files.into_iter().collect::<Vec<_>>();
content_files_to_content_items(
context,
&resolved.instance,
&files,
cache_behaviour,
@@ -330,6 +348,7 @@ pub(crate) async fn list_linked_modpack_content(
}
pub(crate) async fn get_linked_modpack_info(
context: &InvocationContext,
instance_id: &str,
content_set_id: Option<&str>,
cache_behaviour: Option<CacheBehaviour>,
@@ -349,18 +368,21 @@ pub(crate) async fn get_linked_modpack_info(
};
let (project, version, all_versions) = tokio::try_join!(
CachedEntry::get_project(
context,
&project_id,
cache_behaviour,
&state.pool,
&state.api_semaphore,
),
CachedEntry::get_version(
context,
&version_id,
cache_behaviour,
&state.pool,
&state.api_semaphore,
),
CachedEntry::get_project_versions(
context,
&project_id,
cache_behaviour,
&state.pool,
@@ -375,12 +397,14 @@ pub(crate) async fn get_linked_modpack_info(
let (project, all_versions) = if version.project_id != project_id {
let (modpack_project, modpack_versions) = tokio::try_join!(
CachedEntry::get_project(
context,
&version.project_id,
cache_behaviour,
&state.pool,
&state.api_semaphore,
),
CachedEntry::get_project_versions(
context,
&version.project_id,
cache_behaviour,
&state.pool,
@@ -398,6 +422,7 @@ pub(crate) async fn get_linked_modpack_info(
})?;
let owner = if let Some(org_id) = &project.organization {
let org = CachedEntry::get_organization(
context,
org_id,
cache_behaviour,
&state.pool,
@@ -412,6 +437,7 @@ pub(crate) async fn get_linked_modpack_info(
})
} else {
let team = CachedEntry::get_team(
context,
&project.team,
cache_behaviour,
&state.pool,
@@ -447,6 +473,7 @@ pub(crate) async fn get_linked_modpack_info(
}
pub(crate) async fn dependencies_to_content_items(
context: &InvocationContext,
dependencies: &[Dependency],
cache_behaviour: Option<CacheBehaviour>,
pool: &SqlitePool,
@@ -464,6 +491,7 @@ pub(crate) async fn dependencies_to_content_items(
.filter_map(|dependency| dependency.version_id.clone())
.collect::<HashSet<_>>();
let meta = resolve_metadata(
context,
&project_ids,
&version_ids,
cache_behaviour,
@@ -586,12 +614,14 @@ async fn resolve_content_scope_with_instance(
}
async fn content_projects_for_scope(
context: &InvocationContext,
resolved: &ResolvedContentScope,
cache_behaviour: Option<CacheBehaviour>,
state: &State,
filter: ContentFilter<'_>,
) -> crate::Result<DashMap<String, ContentFile>> {
let files = sync_instance_content_files(&resolved.instance, state).await?;
let files =
sync_instance_content_files(context, &resolved.instance, state).await?;
let entries = sqlite::content_rows::get_content_entries(
&resolved.content_set.id,
&state.pool,
@@ -608,6 +638,7 @@ async fn content_projects_for_scope(
.map(|file| file.sha1.as_str())
.collect::<Vec<_>>();
let file_info = CachedEntry::get_file_many(
context,
&hashes,
cache_behaviour,
&state.pool,
@@ -619,6 +650,7 @@ async fn content_projects_for_scope(
.map(|file| (file.hash.clone(), file))
.collect::<HashMap<_, _>>();
let installed_channels = get_installed_update_channels(
context,
&file_info_by_hash,
cache_behaviour,
&state.pool,
@@ -647,6 +679,7 @@ async fn content_projects_for_scope(
let update_key_refs =
update_keys.iter().map(String::as_str).collect::<Vec<_>>();
let file_updates = CachedEntry::get_file_update_many(
context,
&update_key_refs,
cache_behaviour,
&state.pool,
@@ -746,6 +779,7 @@ async fn content_projects_for_scope(
}
async fn get_installed_update_channels(
context: &InvocationContext,
file_info_by_hash: &HashMap<String, CachedFile>,
cache_behaviour: Option<CacheBehaviour>,
pool: &SqlitePool,
@@ -760,6 +794,7 @@ async fn get_installed_update_channels(
}
let version_id_refs = version_ids.iter().copied().collect::<Vec<_>>();
let versions = CachedEntry::get_version_many(
context,
&version_id_refs,
cache_behaviour,
pool,
@@ -809,6 +844,7 @@ fn file_update_cache_key(
}
async fn content_files_to_content_items(
context: &InvocationContext,
instance: &Instance,
files: &[(String, ContentFile)],
cache_behaviour: Option<CacheBehaviour>,
@@ -831,6 +867,7 @@ async fn content_files_to_content_items(
})
.collect::<HashSet<_>>();
let meta = resolve_metadata(
context,
&project_ids,
&version_ids,
cache_behaviour,
@@ -916,6 +953,7 @@ struct ResolvedMetadata {
}
async fn resolve_metadata(
context: &InvocationContext,
project_ids: &HashSet<String>,
version_ids: &HashSet<String>,
cache_behaviour: Option<CacheBehaviour>,
@@ -934,6 +972,7 @@ async fn resolve_metadata(
Ok(Vec::new())
} else {
CachedEntry::get_project_many(
context,
&project_id_refs,
cache_behaviour,
pool,
@@ -947,6 +986,7 @@ async fn resolve_metadata(
Ok(Vec::new())
} else {
CachedEntry::get_version_many(
context,
&version_id_refs,
cache_behaviour,
pool,
@@ -977,6 +1017,7 @@ async fn resolve_metadata(
Ok(Vec::new())
} else {
CachedEntry::get_team_many(
context,
&team_id_refs,
cache_behaviour,
pool,
@@ -990,6 +1031,7 @@ async fn resolve_metadata(
Ok(Vec::new())
} else {
CachedEntry::get_organization_many(
context,
&org_id_refs,
cache_behaviour,
pool,
@@ -1171,13 +1213,18 @@ impl ModpackIdentifiers {
}
async fn get_cached_modpack_identifiers(
context: &InvocationContext,
version_id: &str,
pool: &SqlitePool,
fetch_semaphore: &FetchSemaphore,
) -> crate::Result<Option<ModpackIdentifiers>> {
let Some(cached) =
CachedEntry::get_modpack_files(version_id, pool, fetch_semaphore)
.await?
let Some(cached) = CachedEntry::get_modpack_files(
context,
version_id,
pool,
fetch_semaphore,
)
.await?
else {
return Ok(None);
};
@@ -1193,14 +1240,19 @@ async fn get_cached_modpack_identifiers(
}
async fn get_modpack_identifiers(
context: &InvocationContext,
version_id: &str,
content_set: &ContentSet,
pool: &SqlitePool,
fetch_semaphore: &FetchSemaphore,
) -> crate::Result<ModpackIdentifiers> {
if let Some(cached) =
CachedEntry::get_modpack_files(version_id, pool, fetch_semaphore)
.await?
if let Some(cached) = CachedEntry::get_modpack_files(
context,
version_id,
pool,
fetch_semaphore,
)
.await?
{
if !cached.project_ids.is_empty() {
return Ok(ModpackIdentifiers {
@@ -1214,9 +1266,14 @@ async fn get_modpack_identifiers(
.iter()
.map(String::as_str)
.collect::<Vec<_>>();
let files =
CachedEntry::get_file_many(&hash_refs, None, pool, fetch_semaphore)
.await?;
let files = CachedEntry::get_file_many(
context,
&hash_refs,
None,
pool,
fetch_semaphore,
)
.await?;
let project_ids = files
.iter()
.map(|file| file.project_id.clone())
@@ -1237,14 +1294,19 @@ async fn get_modpack_identifiers(
});
}
let version =
CachedEntry::get_version(version_id, None, pool, fetch_semaphore)
.await?
.ok_or_else(|| {
crate::ErrorKind::InputError(format!(
"Modpack version {version_id} not found"
))
})?;
let version = CachedEntry::get_version(
context,
version_id,
None,
pool,
fetch_semaphore,
)
.await?
.ok_or_else(|| {
crate::ErrorKind::InputError(format!(
"Modpack version {version_id} not found"
))
})?;
let primary_file = version
.files
.iter()
@@ -1262,6 +1324,7 @@ async fn get_modpack_identifiers(
dependent_on: Some(version_id.to_string()),
};
let mrpack_bytes = fetch_mirrors(
context,
&[&primary_file.url],
primary_file.hashes.get("sha1").map(String::as_str),
Some(&download_meta),
@@ -1,12 +1,13 @@
use crate::State;
use crate::state::instances::adapters::{filesystem, sqlite};
use crate::state::instances::{Instance, InstanceFile};
use crate::state::{CachedEntry, ProjectType};
use crate::{InvocationContext, State};
use chrono::Utc;
use std::collections::HashMap;
use uuid::Uuid;
pub(crate) async fn sync_content_files(
context: &InvocationContext,
instance_id: &str,
state: &State,
) -> crate::Result<Vec<InstanceFile>> {
@@ -17,10 +18,11 @@ pub(crate) async fn sync_content_files(
crate::ErrorKind::InputError("Unknown instance".to_string())
})?;
sync_instance_content_files(&instance, state).await
sync_instance_content_files(context, &instance, state).await
}
pub(crate) async fn sync_instance_content_files(
context: &InvocationContext,
instance: &Instance,
state: &State,
) -> crate::Result<Vec<InstanceFile>> {
@@ -33,6 +35,7 @@ pub(crate) async fn sync_instance_content_files(
.map(|file| file.hash_cache_key.as_str())
.collect::<Vec<_>>();
let hashes = CachedEntry::get_file_hash_many(
context,
&cache_keys,
None,
&state.pool,
@@ -1,10 +1,10 @@
use crate::State;
use crate::event::InstancePayloadType;
use crate::event::emit::{emit_instance, emit_warning};
use crate::state::{
DirectoryInfo, InstanceInstallStage, ProjectType, attached_world_data,
};
use crate::worlds::WorldType;
use crate::{InvocationContext, State};
use notify::{RecommendedWatcher, RecursiveMode};
use notify_debouncer_mini::{DebounceEventResult, Debouncer, new_debouncer};
use std::{collections::HashMap, sync::Arc, time::Duration};
@@ -34,6 +34,12 @@ pub async fn init_watcher() -> crate::Result<FileWatcher> {
tracing::info!(parent: &span, "Initing watcher");
while let Some(res) = rx.recv().await {
let _span = span.enter();
let context =
InvocationContext::new("instance/refresh/filesystem_watch");
tracing::debug!(
invocation_cause = %context.cause(),
"Processing filesystem watcher refresh"
);
match res {
Ok(events) => {
+5 -1
View File
@@ -1,4 +1,5 @@
//! Theseus state management system
use crate::InvocationContext;
use crate::util::fetch::{FetchSemaphore, IoSemaphore};
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
@@ -114,20 +115,23 @@ impl State {
)
.await;
let auth_context = InvocationContext::new("auth/session_refresh");
let res = tokio::try_join!(
state.discord_rpc.clear_to_default(true),
instances::refresh_all_instances(),
Settings::migrate(&state.pool),
ModrinthCredentials::refresh_all(),
ModrinthCredentials::refresh_all(&auth_context),
);
if let Err(e) = res {
tracing::error!("Error running discord RPC: {e}");
}
let friends_context = InvocationContext::new("background/friends");
let _ = state
.friends_socket
.connect(
&friends_context,
&state.pool,
&state.api_semaphore,
&state.process_manager,
+11 -2
View File
@@ -1,3 +1,4 @@
use crate::InvocationContext;
use crate::state::{CacheBehaviour, CachedEntry};
use crate::util::fetch::{FetchSemaphore, fetch_advanced};
use chrono::{DateTime, Duration, TimeZone, Utc};
@@ -16,6 +17,7 @@ pub struct ModrinthCredentials {
impl ModrinthCredentials {
pub async fn get_and_refresh(
context: &InvocationContext,
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
semaphore: &FetchSemaphore,
) -> crate::Result<Option<Self>> {
@@ -29,6 +31,7 @@ impl ModrinthCredentials {
}
let resp = fetch_advanced(
context,
Method::POST,
concat!(env!("MODRINTH_API_URL"), "session/refresh"),
None,
@@ -173,13 +176,16 @@ impl ModrinthCredentials {
Ok(())
}
pub(crate) async fn refresh_all() -> crate::Result<()> {
pub(crate) async fn refresh_all(
context: &InvocationContext,
) -> crate::Result<()> {
let state = crate::State::get().await?;
let all = Self::get_all(&state.pool).await?;
let user_ids = all.into_iter().map(|x| x.0).collect::<Vec<_>>();
CachedEntry::get_user_many(
context,
&user_ids.iter().map(|x| &**x).collect::<Vec<_>>(),
Some(CacheBehaviour::Bypass),
&state.pool,
@@ -196,6 +202,7 @@ pub const fn get_login_url() -> &'static str {
}
pub async fn finish_login_flow(
context: &InvocationContext,
code: &str,
semaphore: &FetchSemaphore,
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
@@ -206,7 +213,7 @@ pub async fn finish_login_flow(
// needed. TODO not do this for the reasons outlined at
// https://oauth.net/2/grant-types/implicit/
let info = fetch_info(code, semaphore, exec).await?;
let info = fetch_info(context, code, semaphore, exec).await?;
Ok(ModrinthCredentials {
session: code.to_string(),
@@ -217,11 +224,13 @@ pub async fn finish_login_flow(
}
async fn fetch_info(
context: &InvocationContext,
token: &str,
semaphore: &FetchSemaphore,
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
) -> crate::Result<crate::state::cache::User> {
let result = fetch_advanced(
context,
Method::GET,
concat!(env!("MODRINTH_API_URL"), "user"),
None,
+4
View File
@@ -102,6 +102,7 @@ impl ProcessManager {
#[allow(clippy::too_many_arguments)]
pub async fn insert_new_process(
&self,
invocation_context: &crate::InvocationContext,
instance_id: &str,
instance_path: &str,
instance_name: &str,
@@ -214,6 +215,7 @@ impl ProcessManager {
}
tokio::spawn(Process::sequential_process_manager(
invocation_context.clone(),
instance_id.to_string(),
instance_path.to_string(),
post_exit_command,
@@ -747,6 +749,7 @@ impl Process {
// Also, as the process ends, it spawns the follow-up process if it exists
// By convention, ExitStatus is last command's exit status, and we exit on the first non-zero exit status
async fn sequential_process_manager(
invocation_context: crate::InvocationContext,
instance_id: String,
instance_path: String,
post_exit_command: Option<String>,
@@ -833,6 +836,7 @@ impl Process {
tokio::spawn(async move {
if let Err(e) =
crate::api::instance::try_update_playtime_by_instance_id(
&invocation_context,
&playtime_instance_id,
)
.await
+335 -6
View File
@@ -2,10 +2,13 @@
use super::io::{self, IOError};
use crate::event::LoadingBarId;
use crate::event::emit::emit_loading;
use crate::operation::{InvocationContext, REQUEST_CONTEXT_HEADER};
use crate::{ErrorKind, LabrinthError};
use bytes::Bytes;
use chrono::{DateTime, TimeDelta, Utc};
use eyre::{Context, eyre};
use governor::clock::{Clock, DefaultClock};
use governor::{DefaultDirectRateLimiter, Quota, RateLimiter};
use parking_lot::Mutex;
use rand::Rng;
use reqwest::Method;
@@ -14,12 +17,14 @@ use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::ffi::OsStr;
use std::future::Future;
use std::num::NonZeroU32;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::LazyLock;
use std::time::{self};
use std::sync::{Arc, LazyLock};
use std::time::{self, Duration, Instant, SystemTime};
use tokio::sync::Semaphore;
use tokio::{fs::File, io::AsyncReadExt, io::AsyncWriteExt};
use tracing::{debug, info};
pub const DOWNLOAD_META_HEADER: &str = "modrinth-download-meta";
@@ -188,8 +193,148 @@ static GLOBAL_FETCH_FENCE: LazyLock<FetchFence> =
inner: Mutex::new(HashMap::new()),
});
const API_RETRY_AFTER_FALLBACK: Duration = Duration::from_secs(60);
// This means the unit recovery time will be:
// replenish one unit time in seconds = (60 / (units recovered per minute))
// smooth recovery time = replenish one unit time in seconds * API_RATE_LIMIT_RECOVERY_SIZE.
//
// At 20 it means (60 / 120) * 20 = 10 seconds.
const API_RATE_LIMIT_RECOVERY_SIZE: u32 = 20;
/// Implements request-rate-limit handling as well as a local rate limiter of 120 RPM + 50 burst.
struct ApiRateLimit {
block_until: Mutex<Option<Instant>>,
check_lock: Mutex<()>,
local: Arc<DefaultDirectRateLimiter>,
recovery_padding: Duration,
}
impl ApiRateLimit {
fn new() -> Self {
let quota = Quota::per_minute(NonZeroU32::new(120).unwrap())
.allow_burst(NonZeroU32::new(50).unwrap());
let recovery_size =
API_RATE_LIMIT_RECOVERY_SIZE.min(quota.burst_size().get());
Self {
block_until: Mutex::new(None),
check_lock: Mutex::new(()),
local: Arc::new(RateLimiter::direct(quota)),
recovery_padding: quota
.replenish_interval()
.saturating_mul(recovery_size.saturating_sub(1)),
}
}
fn check(&self) -> crate::Result<()> {
let _check_guard = self.check_lock.lock();
self.ensure_not_blocked()?;
if let Err(not_until) = self.local.check() {
// Adds hysteresis to the rate limiting system, ensuring recovery happens
// for longer but for more units, avoiding the "flapping" effect when running
// out of units.
let retry_after = not_until
.wait_time_from(DefaultClock::default().now())
.saturating_add(self.recovery_padding);
info!(
?retry_after,
"Hit builtin rate limiter; waiting for recovery"
);
self.block_for(retry_after);
let retry_in_seconds = self
.retry_in_seconds()
.unwrap_or_else(|| duration_seconds_ceil(retry_after));
return Err(ErrorKind::Ratelimited { retry_in_seconds }.into());
}
self.ensure_not_blocked()
}
fn handle_response(
&self,
response: &reqwest::Response,
) -> Option<ErrorKind> {
if response.status() != reqwest::StatusCode::TOO_MANY_REQUESTS {
return None;
}
debug!("Received 429 response; blocking");
let retry_after = response
.headers()
.get(reqwest::header::RETRY_AFTER)
.and_then(|value| value.to_str().ok())
.and_then(|value| parse_retry_after(value, SystemTime::now()))
.unwrap_or(API_RETRY_AFTER_FALLBACK);
self.block_for(retry_after);
Some(ErrorKind::Ratelimited {
retry_in_seconds: self.retry_in_seconds().unwrap_or(0),
})
}
fn ensure_not_blocked(&self) -> crate::Result<()> {
if let Some(retry_in_seconds) = self.retry_in_seconds() {
debug!("Hit builtin rate limiter; blocking");
return Err(ErrorKind::Ratelimited { retry_in_seconds }.into());
}
Ok(())
}
fn block_for(&self, duration: Duration) {
let Some(block_until) = Instant::now().checked_add(duration) else {
return;
};
let mut current_block = self.block_until.lock();
if current_block.is_none_or(|current| current < block_until) {
*current_block = Some(block_until);
}
}
fn retry_in_seconds(&self) -> Option<u64> {
let mut block_until = self.block_until.lock();
let remaining = (*block_until)?.checked_duration_since(Instant::now());
if let Some(remaining) = remaining
&& !remaining.is_zero()
{
return Some(duration_seconds_ceil(remaining));
}
*block_until = None;
None
}
}
static GLOBAL_API_RATE_LIMIT: LazyLock<ApiRateLimit> =
LazyLock::new(ApiRateLimit::new);
fn parse_retry_after(value: &str, now: SystemTime) -> Option<Duration> {
if let Ok(seconds) = value.parse::<u64>() {
return Some(Duration::from_secs(seconds));
}
let retry_at = httpdate::parse_http_date(value).ok()?;
Some(retry_at.duration_since(now).unwrap_or(Duration::ZERO))
}
fn duration_seconds_ceil(duration: Duration) -> u64 {
duration
.as_secs()
.saturating_add(u64::from(duration.subsec_nanos() > 0))
}
fn reqwest_client_builder() -> reqwest::ClientBuilder {
reqwest::Client::builder()
.referer(false)
.connect_timeout(time::Duration::from_secs(15))
.read_timeout(time::Duration::from_secs(30))
.tcp_keepalive(Some(time::Duration::from_secs(10)))
@@ -210,6 +355,41 @@ pub static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
.expect("client configuration should be valid")
});
fn is_approved_modrinth_service_url(url: &str) -> bool {
let Ok(url) = url::Url::parse(url) else {
return false;
};
[
env!("MODRINTH_API_BASE_URL"),
env!("MODRINTH_API_URL"),
env!("MODRINTH_API_URL_V3"),
]
.into_iter()
.filter_map(|approved| url::Url::parse(approved).ok())
.any(|approved| url.origin() == approved.origin())
}
fn attach_invocation_headers(
request: reqwest::RequestBuilder,
url: &str,
context: &InvocationContext,
) -> reqwest::RequestBuilder {
if !is_approved_modrinth_service_url(url) {
return request;
}
if cfg!(debug_assertions) && context.cause() == "unattributed" {
tracing::warn!(
"Unattributed invocation context reached the Modrinth HTTP boundary"
);
}
request
.header(reqwest::header::REFERER, context.referer())
.header(REQUEST_CONTEXT_HEADER, context.request_context_header())
}
const FETCH_ATTEMPTS: usize = 2;
pub type FetchProgressFn<'a> = dyn FnMut(
@@ -221,6 +401,7 @@ pub type FetchProgressFn<'a> = dyn FnMut(
#[tracing::instrument(skip(semaphore))]
pub async fn fetch(
context: &InvocationContext,
url: &str,
sha1: Option<&str>,
download_meta: Option<&DownloadMeta>,
@@ -229,6 +410,7 @@ pub async fn fetch(
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
) -> crate::Result<Bytes> {
fetch_advanced(
context,
Method::GET,
url,
sha1,
@@ -245,6 +427,7 @@ pub async fn fetch(
#[tracing::instrument(skip(semaphore))]
pub async fn fetch_with_client(
context: &InvocationContext,
url: &str,
sha1: Option<&str>,
download_meta: Option<&DownloadMeta>,
@@ -254,6 +437,7 @@ pub async fn fetch_with_client(
client: &reqwest::Client,
) -> crate::Result<Bytes> {
fetch_advanced_with_client(
context,
Method::GET,
url,
sha1,
@@ -271,6 +455,7 @@ pub async fn fetch_with_client(
#[tracing::instrument(skip(semaphore, progress))]
pub async fn fetch_with_client_progress(
context: &InvocationContext,
url: &str,
sha1: Option<&str>,
download_meta: Option<&DownloadMeta>,
@@ -281,6 +466,7 @@ pub async fn fetch_with_client_progress(
progress: Option<&mut FetchProgressFn<'_>>,
) -> crate::Result<Bytes> {
fetch_advanced_with_client_and_progress(
context,
Method::GET,
url,
sha1,
@@ -299,6 +485,7 @@ pub async fn fetch_with_client_progress(
#[tracing::instrument(skip(json_body, semaphore))]
pub async fn fetch_json<T>(
context: &InvocationContext,
method: Method,
url: &str,
sha1: Option<&str>,
@@ -311,8 +498,8 @@ where
T: DeserializeOwned,
{
let result = fetch_advanced(
method, url, sha1, json_body, None, None, None, uri_path, semaphore,
exec,
context, method, url, sha1, json_body, None, None, None, uri_path,
semaphore, exec,
)
.await?;
let value = serde_json::from_slice(&result)?;
@@ -324,6 +511,7 @@ where
#[tracing::instrument(skip(json_body, semaphore))]
#[allow(clippy::too_many_arguments)]
pub async fn fetch_advanced(
context: &InvocationContext,
method: Method,
url: &str,
sha1: Option<&str>,
@@ -336,6 +524,7 @@ pub async fn fetch_advanced(
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
) -> crate::Result<Bytes> {
fetch_advanced_with_client(
context,
method,
url,
sha1,
@@ -354,6 +543,7 @@ pub async fn fetch_advanced(
#[tracing::instrument(skip(json_body, semaphore, progress))]
#[allow(clippy::too_many_arguments)]
pub async fn fetch_advanced_with_progress(
context: &InvocationContext,
method: Method,
url: &str,
sha1: Option<&str>,
@@ -367,6 +557,7 @@ pub async fn fetch_advanced_with_progress(
progress: Option<&mut FetchProgressFn<'_>>,
) -> crate::Result<Bytes> {
fetch_advanced_with_client_and_progress(
context,
method,
url,
sha1,
@@ -387,6 +578,7 @@ pub async fn fetch_advanced_with_progress(
#[tracing::instrument(skip(json_body, semaphore))]
#[allow(clippy::too_many_arguments)]
pub async fn fetch_advanced_with_client(
context: &InvocationContext,
method: Method,
url: &str,
sha1: Option<&str>,
@@ -400,6 +592,7 @@ pub async fn fetch_advanced_with_client(
client: &reqwest::Client,
) -> crate::Result<Bytes> {
fetch_advanced_with_client_and_progress(
context,
method,
url,
sha1,
@@ -419,6 +612,7 @@ pub async fn fetch_advanced_with_client(
#[tracing::instrument(skip(json_body, semaphore, client, progress))]
#[allow(clippy::too_many_arguments)]
async fn fetch_advanced_with_client_and_progress(
context: &InvocationContext,
method: Method,
url: &str,
sha1: Option<&str>,
@@ -452,6 +646,10 @@ async fn fetch_advanced_with_client_and_progress(
.map(|m| (DOWNLOAD_META_HEADER.to_string(), m.to_header_value()));
for attempt in 1..=(FETCH_ATTEMPTS + 1) {
if is_api_url {
GLOBAL_API_RATE_LIMIT.check()?;
}
if let Some(fence_key) = fence_key
&& GLOBAL_FETCH_FENCE.is_blocked(fence_key)
{
@@ -461,7 +659,11 @@ async fn fetch_advanced_with_client_and_progress(
.into());
}
let mut req = client.request(method.clone(), url);
let mut req = attach_invocation_headers(
client.request(method.clone(), url),
url,
context,
);
if let Some(body) = json_body.clone() {
req = req.json(&body);
@@ -483,6 +685,13 @@ async fn fetch_advanced_with_client_and_progress(
let result = req.send().await;
match result {
Ok(resp) => {
if is_api_url
&& let Some(error) =
GLOBAL_API_RATE_LIMIT.handle_response(&resp)
{
return Err(error.into());
}
if resp.status().is_server_error() {
if let Some(fence_key) = fence_key {
GLOBAL_FETCH_FENCE.record_fail(fence_key);
@@ -601,6 +810,7 @@ async fn fetch_advanced_with_client_and_progress(
/// Downloads a file from specified mirrors
#[tracing::instrument(skip(semaphore))]
pub async fn fetch_mirrors(
context: &InvocationContext,
mirrors: &[&str],
sha1: Option<&str>,
download_meta: Option<&DownloadMeta>,
@@ -616,6 +826,7 @@ pub async fn fetch_mirrors(
for (index, mirror) in mirrors.iter().enumerate() {
let result = fetch_with_client(
context,
mirror,
sha1,
download_meta,
@@ -636,6 +847,7 @@ pub async fn fetch_mirrors(
#[tracing::instrument(skip(semaphore, progress))]
pub async fn fetch_mirrors_with_progress(
context: &InvocationContext,
mirrors: &[&str],
sha1: Option<&str>,
download_meta: Option<&DownloadMeta>,
@@ -652,6 +864,7 @@ pub async fn fetch_mirrors_with_progress(
for (index, mirror) in mirrors.iter().enumerate() {
let result = fetch_with_client_progress(
context,
mirror,
sha1,
download_meta,
@@ -674,6 +887,7 @@ pub async fn fetch_mirrors_with_progress(
/// Posts a JSON to a URL
#[tracing::instrument(skip(json_body, semaphore))]
pub async fn post_json(
context: &InvocationContext,
url: &str,
json_body: serde_json::Value,
semaphore: &FetchSemaphore,
@@ -681,7 +895,12 @@ pub async fn post_json(
) -> crate::Result<()> {
let _permit = semaphore.0.acquire().await?;
let mut req = INSECURE_REQWEST_CLIENT.post(url).json(&json_body);
let mut req = attach_invocation_headers(
INSECURE_REQWEST_CLIENT.post(url),
url,
context,
)
.json(&json_body);
if let Some(creds) =
crate::state::ModrinthCredentials::get_active(exec).await?
@@ -784,6 +1003,116 @@ pub async fn sha1_async(bytes: Bytes) -> crate::Result<String> {
Ok(hash)
}
#[cfg(test)]
mod invocation_context_tests {
use super::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
fn built_request(
url: &str,
context: &InvocationContext,
) -> reqwest::Request {
attach_invocation_headers(
INSECURE_REQWEST_CLIENT.get(url),
url,
context,
)
.build()
.unwrap()
}
#[test]
fn approved_origins_receive_invocation_headers() {
let context = InvocationContext::new("navigation/instance/content");
let request = built_request(
concat!(env!("MODRINTH_API_URL"), "project/example"),
&context,
);
assert_eq!(
request.headers().get(reqwest::header::REFERER).unwrap(),
"https://tauri.modrinth.app/_rc/navigation/instance/content"
);
assert_eq!(
request.headers().get(REQUEST_CONTEXT_HEADER).unwrap(),
"navigation/instance/content"
);
}
#[test]
fn third_party_origins_do_not_receive_invocation_headers() {
let context = InvocationContext::new("instance/install");
let request = built_request("https://example.com/file.jar", &context);
assert!(!request.headers().contains_key(reqwest::header::REFERER));
assert!(!request.headers().contains_key(REQUEST_CONTEXT_HEADER));
}
#[test]
fn similarly_prefixed_hosts_are_not_approved() {
let context = InvocationContext::new("navigation/browse");
let approved = url::Url::parse(env!("MODRINTH_API_URL")).unwrap();
let url = format!(
"{}://{}.example.com/project",
approved.scheme(),
approved.host_str().unwrap()
);
let request = built_request(&url, &context);
assert!(!request.headers().contains_key(reqwest::header::REFERER));
assert!(!request.headers().contains_key(REQUEST_CONTEXT_HEADER));
}
#[test]
fn every_retry_attempt_retains_the_same_context() {
let context = InvocationContext::new("new/frontend/cause");
let url = concat!(env!("MODRINTH_API_URL_V3"), "projects");
for _ in 0..=FETCH_ATTEMPTS {
let request = built_request(url, &context);
assert_eq!(
request.headers().get(REQUEST_CONTEXT_HEADER).unwrap(),
context.request_context_header()
);
assert_eq!(
request.headers().get(reqwest::header::REFERER).unwrap(),
context.referer().as_str()
);
}
}
#[tokio::test]
async fn disabling_automatic_referer_keeps_redirects_enabled() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
for response in [
"HTTP/1.1 302 Found\r\nLocation: /final\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok",
] {
let (mut stream, _) = listener.accept().await.unwrap();
let mut request = vec![0; 1024];
stream.read(&mut request).await.unwrap();
stream.write_all(response.as_bytes()).await.unwrap();
}
});
let response = reqwest_client_builder()
.build()
.unwrap()
.get(format!("http://{address}/redirect"))
.send()
.await
.unwrap();
assert_eq!(response.url().path(), "/final");
assert_eq!(response.bytes().await.unwrap(), "ok");
server.await.unwrap();
}
}
pub async fn sha1_file_async(
path: impl AsRef<Path>,
) -> crate::Result<(u64, String)> {