diff --git a/apps/app-frontend/src/helpers/cache.js b/apps/app-frontend/src/helpers/cache.js index ab10222526..641e069550 100644 --- a/apps/app-frontend/src/helpers/cache.js +++ b/apps/app-frontend/src/helpers/cache.js @@ -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 }) diff --git a/apps/app-frontend/src/helpers/friends.ts b/apps/app-frontend/src/helpers/friends.ts index 7fd27b5a88..e480ee31fd 100644 --- a/apps/app-frontend/src/helpers/friends.ts +++ b/apps/app-frontend/src/helpers/friends.ts @@ -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' diff --git a/apps/app-frontend/src/helpers/install.ts b/apps/app-frontend/src/helpers/install.ts index ee0f3f2d83..ceaaee83d2 100644 --- a/apps/app-frontend/src/helpers/install.ts +++ b/apps/app-frontend/src/helpers/install.ts @@ -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' diff --git a/apps/app-frontend/src/helpers/instance.ts b/apps/app-frontend/src/helpers/instance.ts index 3a7a1b567e..5a1eb3235b 100644 --- a/apps/app-frontend/src/helpers/instance.ts +++ b/apps/app-frontend/src/helpers/instance.ts @@ -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 { diff --git a/apps/app-frontend/src/helpers/invoke.ts b/apps/app-frontend/src/helpers/invoke.ts new file mode 100644 index 0000000000..427fe74f04 --- /dev/null +++ b/apps/app-frontend/src/helpers/invoke.ts @@ -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(command: string, args: Record = {}): Promise { + return tauriInvoke(command, { + ...args, + invocationContext: { cause: commandCause(command) }, + }) +} diff --git a/apps/app-frontend/src/helpers/jre.js b/apps/app-frontend/src/helpers/jre.js index 21af5847e1..9e1ddf1c5d 100644 --- a/apps/app-frontend/src/helpers/jre.js +++ b/apps/app-frontend/src/helpers/jre.js @@ -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' /* diff --git a/apps/app-frontend/src/helpers/metadata.js b/apps/app-frontend/src/helpers/metadata.js index 51898dd53e..1553318572 100644 --- a/apps/app-frontend/src/helpers/metadata.js +++ b/apps/app-frontend/src/helpers/metadata.js @@ -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 diff --git a/apps/app-frontend/src/helpers/mr_auth.ts b/apps/app-frontend/src/helpers/mr_auth.ts index d28ad63db4..e44bf2ba57 100644 --- a/apps/app-frontend/src/helpers/mr_auth.ts +++ b/apps/app-frontend/src/helpers/mr_auth.ts @@ -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 diff --git a/apps/app-frontend/src/helpers/tags.js b/apps/app-frontend/src/helpers/tags.js index fdfdb04d07..3e93b26685 100644 --- a/apps/app-frontend/src/helpers/tags.js +++ b/apps/app-frontend/src/helpers/tags.js @@ -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() { diff --git a/apps/app-frontend/src/helpers/worlds.ts b/apps/app-frontend/src/helpers/worlds.ts index 3b5285f51b..09f64a8f65 100644 --- a/apps/app-frontend/src/helpers/worlds.ts +++ b/apps/app-frontend/src/helpers/worlds.ts @@ -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' diff --git a/apps/app/src/api/cache.rs b/apps/app/src/api/cache.rs index f9f7b7452a..29aa970551 100644 --- a/apps/app/src/api/cache.rs +++ b/apps/app/src/api/cache.rs @@ -6,20 +6,27 @@ macro_rules! impl_cache_methods { $( paste::paste! { #[tauri::command] - pub async fn [](id: &str, cache_behaviour: Option) -> Result> + pub async fn []( + id: &str, + cache_behaviour: Option, + invocation_context: theseus::InvocationContext, + ) -> Result> { - Ok(theseus::cache::[](id, cache_behaviour).await?) + let context = invocation_context; + Ok(theseus::cache::[](&context, id, cache_behaviour).await?) } #[tauri::command] pub async fn []( ids: Vec, cache_behaviour: Option, + invocation_context: theseus::InvocationContext, ) -> Result> { + let context = invocation_context; let ids = ids.iter().map(|x| &**x).collect::>(); let entries = - theseus::cache::[](&*ids, cache_behaviour).await?; + theseus::cache::[](&context, &*ids, cache_behaviour).await?; Ok(entries) } @@ -73,9 +80,13 @@ pub async fn purge_cache_types(cache_types: Vec) -> Result<()> { pub async fn get_project_versions( project_id: &str, cache_behaviour: Option, + invocation_context: theseus::InvocationContext, ) -> Result>> { - 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?) } diff --git a/apps/app/src/api/friends.rs b/apps/app/src/api/friends.rs index 82d1d37f69..7b9ad48ebb 100644 --- a/apps/app/src/api/friends.rs +++ b/apps/app/src/api/friends.rs @@ -13,8 +13,11 @@ pub fn init() -> TauriPlugin { } #[tauri::command] -pub async fn friends() -> crate::api::Result> { - Ok(theseus::friends::friends().await?) +pub async fn friends( + invocation_context: theseus::InvocationContext, +) -> crate::api::Result> { + let context = invocation_context; + Ok(theseus::friends::friends(&context).await?) } #[tauri::command] @@ -23,11 +26,19 @@ pub async fn friend_statuses() -> crate::api::Result> { } #[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?) } diff --git a/apps/app/src/api/install.rs b/apps/app/src/api/install.rs index 96d2f0481b..a9a671dfee 100644 --- a/apps/app/src/api/install.rs +++ b/apps/app/src/api/install.rs @@ -67,15 +67,23 @@ impl InstallPostInstallEditRequest { #[tauri::command] pub async fn install_get_modpack_preview( location: CreatePackLocation, + invocation_context: theseus::InvocationContext, ) -> Result { - 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 { + 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, + invocation_context: theseus::InvocationContext, ) -> Result { + 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 { + 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 { - 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 { - 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, + invocation_context: theseus::InvocationContext, ) -> Result { + 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 { } #[tauri::command] -pub async fn install_job_retry(job_id: Uuid) -> Result { - Ok(theseus::install::retry_job(job_id).await?) +pub async fn install_job_retry( + job_id: Uuid, + invocation_context: theseus::InvocationContext, +) -> Result { + let context = invocation_context; + Ok(theseus::install::retry_job(&context, job_id).await?) } #[tauri::command] diff --git a/apps/app/src/api/instance.rs b/apps/app/src/api/instance.rs index 1b33adf62c..75f93d8bb1 100644 --- a/apps/app/src/api/instance.rs +++ b/apps/app/src/api/instance.rs @@ -437,15 +437,25 @@ pub async fn instance_list() -> Result> { pub async fn instance_get_projects( instance_id: &str, cache_behaviour: Option, + invocation_context: theseus::InvocationContext, ) -> Result> { - 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> { - 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, + invocation_context: theseus::InvocationContext, ) -> Result> { - 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, + invocation_context: theseus::InvocationContext, ) -> Result> { - 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, cache_behaviour: Option, + invocation_context: theseus::InvocationContext, ) -> Result> { + 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, + invocation_context: theseus::InvocationContext, ) -> Result> { - 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, + invocation_context: theseus::InvocationContext, ) -> Result> { + 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> { - 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 { + 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> { - 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 { - 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, + invocation_context: theseus::InvocationContext, ) -> Result { + 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 { + 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 { + 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 { - 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 { + 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 { + 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 { - 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, description: Option, name: Option, + 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, + invocation_context: theseus::InvocationContext, ) -> Result { + 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] diff --git a/apps/app/src/api/jre.rs b/apps/app/src/api/jre.rs index 71c72257cf..cdcc560a57 100644 --- a/apps/app/src/api/jre.rs +++ b/apps/app/src/api/jre.rs @@ -53,8 +53,12 @@ pub async fn jre_test_jre(path: PathBuf, major_version: u32) -> Result { // Auto installs java for the given java version #[tauri::command] -pub async fn jre_auto_install_java(java_version: u32) -> Result { - Ok(jre::auto_install_java(java_version).await?) +pub async fn jre_auto_install_java( + java_version: u32, + invocation_context: theseus::InvocationContext, +) -> Result { + let context = invocation_context; + Ok(jre::auto_install_java(&context, java_version).await?) } // Gets the maximum memory a system has available. diff --git a/apps/app/src/api/metadata.rs b/apps/app/src/api/metadata.rs index 9364924e50..fb4f13c32c 100644 --- a/apps/app/src/api/metadata.rs +++ b/apps/app/src/api/metadata.rs @@ -13,12 +13,19 @@ pub fn init() -> tauri::plugin::TauriPlugin { /// Gets the game versions from daedalus #[tauri::command] -pub async fn metadata_get_game_versions() -> Result { - Ok(theseus::metadata::get_minecraft_versions().await?) +pub async fn metadata_get_game_versions( + invocation_context: theseus::InvocationContext, +) -> Result { + 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 { - Ok(theseus::metadata::get_loader_versions(loader).await?) +pub async fn metadata_get_loader_versions( + loader: &str, + invocation_context: theseus::InvocationContext, +) -> Result { + let context = invocation_context; + Ok(theseus::metadata::get_loader_versions(&context, loader).await?) } diff --git a/apps/app/src/api/mr_auth.rs b/apps/app/src/api/mr_auth.rs index 2143d20c5e..93e6d7bccc 100644 --- a/apps/app/src/api/mr_auth.rs +++ b/apps/app/src/api/mr_auth.rs @@ -22,7 +22,9 @@ pub fn init() -> TauriPlugin { #[tauri::command] pub async fn modrinth_login( app: tauri::AppHandle, + invocation_context: theseus::InvocationContext, ) -> Result { + 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( )); }; - 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> { - Ok(theseus::mr_auth::get_credentials().await?) +pub async fn get( + invocation_context: theseus::InvocationContext, +) -> Result> { + let context = invocation_context; + Ok(theseus::mr_auth::get_credentials(&context).await?) } #[tauri::command] diff --git a/apps/app/src/api/tags.rs b/apps/app/src/api/tags.rs index b69d221d71..61cadf6a94 100644 --- a/apps/app/src/api/tags.rs +++ b/apps/app/src/api/tags.rs @@ -15,30 +15,45 @@ pub fn init() -> tauri::plugin::TauriPlugin { /// Gets cached category tags from the database #[tauri::command] -pub async fn tags_get_categories() -> Result> { - Ok(theseus::tags::get_category_tags().await?) +pub async fn tags_get_categories( + invocation_context: theseus::InvocationContext, +) -> Result> { + 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> { - Ok(theseus::tags::get_report_type_tags().await?) +pub async fn tags_get_report_types( + invocation_context: theseus::InvocationContext, +) -> Result> { + 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> { - Ok(theseus::tags::get_loader_tags().await?) +pub async fn tags_get_loaders( + invocation_context: theseus::InvocationContext, +) -> Result> { + 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> { - Ok(theseus::tags::get_game_version_tags().await?) +pub async fn tags_get_game_versions( + invocation_context: theseus::InvocationContext, +) -> Result> { + 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> { - Ok(theseus::tags::get_donation_platform_tags().await?) +pub async fn tags_get_donation_platforms( + invocation_context: theseus::InvocationContext, +) -> Result> { + let context = invocation_context; + Ok(theseus::tags::get_donation_platform_tags(&context).await?) } diff --git a/apps/app/src/api/worlds.rs b/apps/app/src/api/worlds.rs index afa743e0eb..0f5a1d3771 100644 --- a/apps/app/src/api/worlds.rs +++ b/apps/app/src/api/worlds.rs @@ -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> { - 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 { - 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 { + let context = invocation_context; let process = instance::run( + &context, instance_id, QuickPlayType::Server(ServerAddress::Unresolved(address.to_owned())), ) diff --git a/packages/app-lib/src/api/cache.rs b/packages/app-lib/src/api/cache.rs index 62dc68a24d..80c2de7956 100644 --- a/packages/app-lib/src/api/cache.rs +++ b/packages/app-lib/src/api/cache.rs @@ -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 []( + context: &InvocationContext, id: &str, cache_behaviour: Option, ) -> crate::Result> { let state = crate::State::get().await?; - Ok(CachedEntry::[](&[id], cache_behaviour, &state.pool, &state.api_semaphore).await?.into_iter().next()) + Ok(CachedEntry::[](context, &[id], cache_behaviour, &state.pool, &state.api_semaphore).await?.into_iter().next()) } #[tracing::instrument] pub async fn []( + context: &InvocationContext, ids: &[&str], cache_behaviour: Option, ) -> crate::Result> { let state = crate::State::get().await?; let entries = - CachedEntry::[](ids, None, &state.pool, &state.api_semaphore).await?; + CachedEntry::[](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, ) -> crate::Result>> { let state = crate::State::get().await?; CachedEntry::get_project_versions( + context, project_id, cache_behaviour, &state.pool, diff --git a/packages/app-lib/src/api/friends.rs b/packages/app-lib/src/api/friends.rs index 0fe883e01d..1028561ff2 100644 --- a/packages/app-lib/src/api/friends.rs +++ b/packages/app-lib/src/api/friends.rs @@ -1,11 +1,15 @@ +use crate::InvocationContext; use crate::state::{FriendsSocket, UserFriend}; use ariadne::users::UserStatus; #[tracing::instrument] -pub async fn friends() -> crate::Result> { +pub async fn friends( + context: &InvocationContext, +) -> crate::Result> { 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> { } #[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(()) } diff --git a/packages/app-lib/src/api/instance/content.rs b/packages/app-lib/src/api/instance/content.rs index 9adb2734cb..8e140632c9 100644 --- a/packages/app-lib/src/api/instance/content.rs +++ b/packages/app-lib/src/api/instance/content.rs @@ -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> { 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, ) -> crate::Result> { 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> { 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, ) -> crate::Result> { 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, ) -> crate::Result> { 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, cache_behaviour: Option, ) -> crate::Result> { 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, ) -> crate::Result> { let state = State::get().await?; crate::state::get_linked_modpack_info( + context, instance_id, None, cache_behaviour, diff --git a/packages/app-lib/src/api/instance/export_mrpack.rs b/packages/app-lib/src/api/instance/export_mrpack.rs index 6faada6f25..5f0a53b208 100644 --- a/packages/app-lib/src/api/instance/export_mrpack.rs +++ b/packages/app-lib/src/api/instance/export_mrpack.rs @@ -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, @@ -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, @@ -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::>(); let versions = CachedEntry::get_version_many( + context, &projects.iter().map(|x| &*x.1).collect::>(), None, &state.pool, diff --git a/packages/app-lib/src/api/instance/install.rs b/packages/app-lib/src/api/instance/install.rs index ed308c0e9a..36ed097b92 100644 --- a/packages/app-lib/src/api/instance/install.rs +++ b/packages/app-lib/src/api/instance/install.rs @@ -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> { 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(), diff --git a/packages/app-lib/src/api/instance/lifecycle.rs b/packages/app-lib/src/api/instance/lifecycle.rs index 0139b6b5f7..ffdace8ae6 100644 --- a/packages/app-lib/src/api/instance/lifecycle.rs +++ b/packages/app-lib/src/api/instance/lifecycle.rs @@ -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 { let state = State::get().await?; let instance = crate::state::create_instance( + context, CreateInstance { name, path: None, diff --git a/packages/app-lib/src/api/instance/projects.rs b/packages/app-lib/src/api/instance/projects.rs index 18c3c98e86..285849ab05 100644 --- a/packages/app-lib/src/api/instance/projects.rs +++ b/packages/app-lib/src/api/instance/projects.rs @@ -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> { 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, ) -> crate::Result { 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 { @@ -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 { #[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 { +pub async fn is_file_on_modrinth( + context: &InvocationContext, + path: &Path, +) -> crate::Result { 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 { @@ -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 { 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, diff --git a/packages/app-lib/src/api/instance/run.rs b/packages/app-lib/src/api/instance/run.rs index e4d1706de3..a9f1a2d92c 100644 --- a/packages/app-lib/src/api/instance/run.rs +++ b/packages/app-lib/src/api/instance/run.rs @@ -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 { @@ -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 { 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 = 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, ) diff --git a/packages/app-lib/src/api/jre.rs b/packages/app-lib/src/api/jre.rs index 4944512591..16236e9205 100644 --- a/packages/app-lib/src/api/jre.rs +++ b/packages/app-lib/src/api/jre.rs @@ -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 { - auto_install_java_with_loading(java_version, true).await +pub async fn auto_install_java( + context: &InvocationContext, + java_version: u32, +) -> crate::Result { + 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 { - 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 { - 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, @@ -167,6 +173,7 @@ async fn auto_install_java_inner( .await?; } let packages = fetch_json::>( + 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, diff --git a/packages/app-lib/src/api/metadata.rs b/packages/app-lib/src/api/metadata.rs index befad43b9f..1754a2c625 100644 --- a/packages/app-lib/src/api/metadata.rs +++ b/packages/app-lib/src/api/metadata.rs @@ -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 { +pub async fn get_minecraft_versions( + context: &InvocationContext, +) -> crate::Result { 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 { } // #[tracing::instrument] -pub async fn get_loader_versions(loader: &str) -> crate::Result { +pub async fn get_loader_versions( + context: &InvocationContext, + loader: &str, +) -> crate::Result { 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, diff --git a/packages/app-lib/src/api/mod.rs b/packages/app-lib/src/api/mod.rs index 927a666aaf..9e5f326ef8 100644 --- a/packages/app-lib/src/api/mod.rs +++ b/packages/app-lib/src/api/mod.rs @@ -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, diff --git a/packages/app-lib/src/api/mr_auth.rs b/packages/app-lib/src/api/mr_auth.rs index 42ce41fe5e..56d27efa26 100644 --- a/packages/app-lib/src/api/mr_auth.rs +++ b/packages/app-lib/src/api/mr_auth.rs @@ -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 { 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> { +pub async fn get_credentials( + context: &InvocationContext, +) -> crate::Result> { 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) } diff --git a/packages/app-lib/src/api/pack/import/atlauncher.rs b/packages/app-lib/src/api/pack/import/atlauncher.rs index 1caf64f49e..89b8961842 100644 --- a/packages/app-lib/src/api/pack/import/atlauncher.rs +++ b/packages/app-lib/src/api/pack/import/atlauncher.rs @@ -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, diff --git a/packages/app-lib/src/api/pack/import/curseforge.rs b/packages/app-lib/src/api/pack/import/curseforge.rs index becc532407..234f64228f 100644 --- a/packages/app-lib/src/api/pack/import/curseforge.rs +++ b/packages/app-lib/src/api/pack/import/curseforge.rs @@ -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, diff --git a/packages/app-lib/src/api/pack/import/gdlauncher.rs b/packages/app-lib/src/api/pack/import/gdlauncher.rs index 7a9f164242..6cccb665de 100644 --- a/packages/app-lib/src/api/pack/import/gdlauncher.rs +++ b/packages/app-lib/src/api/pack/import/gdlauncher.rs @@ -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, diff --git a/packages/app-lib/src/api/pack/import/mmc.rs b/packages/app-lib/src/api/pack/import/mmc.rs index 662971c366..60ef0e56a2 100644 --- a/packages/app-lib/src/api/pack/import/mmc.rs +++ b/packages/app-lib/src/api/pack/import/mmc.rs @@ -177,6 +177,7 @@ async fn load_instance_cfg(file_path: &Path) -> crate::Result { // #[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, diff --git a/packages/app-lib/src/api/pack/import/mod.rs b/packages/app-lib/src/api/pack/import/mod.rs index 4a58cfcbf3..b96375d44f 100644 --- a/packages/app-lib/src/api/pack/import/mod.rs +++ b/packages/app-lib/src/api/pack/import/mod.rs @@ -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), diff --git a/packages/app-lib/src/api/pack/install_from.rs b/packages/app-lib/src/api/pack/install_from.rs index e4a86d79f1..5d65af2341 100644 --- a/packages/app-lib/src/api/pack/install_from.rs +++ b/packages/app-lib/src/api/pack/install_from.rs @@ -164,6 +164,7 @@ pub struct CreatePackDescription { } pub async fn get_instance_from_pack( + context: &crate::InvocationContext, location: CreatePackLocation, ) -> crate::Result { 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(), diff --git a/packages/app-lib/src/api/pack/install_mrpack.rs b/packages/app-lib/src/api/pack/install_mrpack.rs index 9eea91b162..b866075d18 100644 --- a/packages/app-lib/src/api/pack/install_mrpack.rs +++ b/packages/app-lib/src/api/pack/install_mrpack.rs @@ -238,6 +238,7 @@ where } pub(crate) async fn get_external_files_from_mrpack( + invocation_context: &crate::InvocationContext, file: &CreatePackFile, ) -> crate::Result> { 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::>(); 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::>(); 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::>(), None, &state.pool, diff --git a/packages/app-lib/src/api/tags.rs b/packages/app-lib/src/api/tags.rs index f43ad0a0b0..3d36e35351 100644 --- a/packages/app-lib/src/api/tags.rs +++ b/packages/app-lib/src/api/tags.rs @@ -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> { +pub async fn get_category_tags( + context: &InvocationContext, +) -> crate::Result> { 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> { +pub async fn get_report_type_tags( + context: &InvocationContext, +) -> crate::Result> { 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> { +pub async fn get_loader_tags( + context: &InvocationContext, +) -> crate::Result> { 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> { +pub async fn get_game_version_tags( + context: &InvocationContext, +) -> crate::Result> { 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> -{ +pub async fn get_donation_platform_tags( + context: &InvocationContext, +) -> crate::Result> { let state = State::get().await?; let donation_platforms = CachedEntry::get_donation_platforms( + context, None, &state.pool, &state.api_semaphore, diff --git a/packages/app-lib/src/api/worlds.rs b/packages/app-lib/src/api/worlds.rs index 03efec7036..7d394b19da 100644 --- a/packages/app-lib/src/api/worlds.rs +++ b/packages/app-lib/src/api/worlds.rs @@ -923,6 +923,7 @@ mod servers_data { } pub async fn get_instance_protocol_version( + invocation_context: &crate::InvocationContext, instance_id: &str, ) -> Result> { 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(), diff --git a/packages/app-lib/src/install/runner.rs b/packages/app-lib/src/install/runner.rs index 4dee0cdaeb..43d5451c59 100644 --- a/packages/app-lib/src/install/runner.rs +++ b/packages/app-lib/src/install/runner.rs @@ -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, link: InstanceLink, ) -> crate::Result { - 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, ) -> crate::Result { - 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 { - 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 { - 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 { - 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, ) -> crate::Result { - 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 { diagnostics::build_job_support_details(&job, &state).await } -pub async fn retry_job(job_id: Uuid) -> crate::Result { +pub async fn retry_job( + context: &InvocationContext, + job_id: Uuid, +) -> crate::Result { 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 { 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 { ) .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 { +async fn start( + context: &InvocationContext, + request: InstallRequest, +) -> crate::Result { 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::>(); 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, diff --git a/packages/app-lib/src/launcher/download.rs b/packages/app-lib/src/launcher/download.rs index acc00c3f73..abc2a6254b 100644 --- a/packages/app-lib/src/launcher/download.rs +++ b/packages/app-lib/src/launcher/download.rs @@ -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, progress: Option, - context: InstallErrorContext, + error_context: InstallErrorContext, ) -> crate::Result { - 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), diff --git a/packages/app-lib/src/launcher/mod.rs b/packages/app-lib/src/launcher/mod.rs index d178335cfd..3712488695 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -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 { } pub async fn install_minecraft_with_reporter( - context: &InstanceLaunchContext, + invocation_context: &crate::InvocationContext, + launch_context: &InstanceLaunchContext, repairing: bool, reporter: Option, ) -> 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, ) -> 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, - context: &InstanceLaunchContext, + launch_context: &InstanceLaunchContext, mut quick_play_type: QuickPlayType, ) -> 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; 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)), diff --git a/packages/app-lib/src/lib.rs b/packages/app-lib/src/lib.rs index 79b0e997a6..446d1d8772 100644 --- a/packages/app-lib/src/lib.rs +++ b/packages/app-lib/src/lib.rs @@ -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; diff --git a/packages/app-lib/src/operation.rs b/packages/app-lib/src/operation.rs new file mode 100644 index 0000000000..1af1d1760a --- /dev/null +++ b/packages/app-lib/src/operation.rs @@ -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) -> 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::( + 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" + ); + } +} diff --git a/packages/app-lib/src/state/cache.rs b/packages/app-lib/src/state/cache.rs index 99ae6d3457..0809404a5b 100644 --- a/packages/app-lib/src/state/cache.rs +++ b/packages/app-lib/src/state/cache.rs @@ -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 []( + context: &InvocationContext, id: &str, cache_behaviour: Option, pool: &SqlitePool, fetch_semaphore: &FetchSemaphore, ) -> crate::Result> { - Ok(Self::[](&[id], cache_behaviour, pool, fetch_semaphore).await?.into_iter().next()) + Ok(Self::[](context, &[id], cache_behaviour, pool, fetch_semaphore).await?.into_iter().next()) } #[tracing::instrument(skip(pool, fetch_semaphore))] pub async fn []( + context: &InvocationContext, ids: &[&str], cache_behaviour: Option, pool: &SqlitePool, @@ -822,7 +825,7 @@ macro_rules! impl_cache_methods { ) -> crate::Result> { 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 [] ( + context: &InvocationContext, cache_behaviour: Option, pool: &SqlitePool, fetch_semaphore: &FetchSemaphore, ) -> crate::Result> { 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, @@ -897,6 +902,7 @@ impl CachedEntry { fetch_semaphore: &FetchSemaphore, ) -> crate::Result> { 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, @@ -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, fetch_semaphore: &FetchSemaphore, pool: &SqlitePool, ) -> crate::Result> { async fn fetch_many_batched( + 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::>( + 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::>( + context, Method::GET, env!("MODRINTH_API_URL_V3"), "teams?ids=", @@ -1287,6 +1303,7 @@ impl CachedEntry { } CacheValueType::Organization => { let mut orgs = fetch_many_batched::( + context, Method::GET, env!("MODRINTH_API_URL_V3"), "organizations?ids=", @@ -1343,6 +1360,7 @@ impl CachedEntry { } CacheValueType::File => { let mut versions = fetch_json::>( + 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>, >( + 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::>( + 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> { 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, pool: &SqlitePool, fetch_semaphore: &FetchSemaphore, ) -> crate::Result>> { let entry = Self::get( + context, CacheValueType::ProjectVersions, project_id, cache_behaviour, diff --git a/packages/app-lib/src/state/friends.rs b/packages/app-lib/src/state/friends.rs index 9e59febebd..7bb3b3b8a1 100644 --- a/packages/app-lib/src/state/friends.rs +++ b/packages/app-lib/src/state/friends.rs @@ -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> { 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, diff --git a/packages/app-lib/src/state/instances/commands/apply_content_install.rs b/packages/app-lib/src/state/instances/commands/apply_content_install.rs index 0491bcbb7e..f2dc5c8466 100644 --- a/packages/app-lib/src/state/instances/commands/apply_content_install.rs +++ b/packages/app-lib/src/state/instances/commands/apply_content_install.rs @@ -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, } @@ -57,6 +59,7 @@ impl ContentMetadataProvider for CachedEntryContentProvider<'_> { ) -> Result, 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, 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 { 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 { 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 { 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), diff --git a/packages/app-lib/src/state/instances/commands/apply_content_update.rs b/packages/app-lib/src/state/instances/commands/apply_content_update.rs index 09aa358921..168069cff0 100644 --- a/packages/app-lib/src/state/instances/commands/apply_content_update.rs +++ b/packages/app-lib/src/state/instances/commands/apply_content_update.rs @@ -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 { 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 { 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> { @@ -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 { 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::>(); 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::>(); 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> { 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, 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>>, ) -> crate::Result> { 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>, state: &State, ) -> crate::Result> { 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>>, state: &State, ) -> crate::Result>> { if !project_versions_cache.contains_key(project_id) { let versions = CachedEntry::get_project_versions( + context, project_id, Some(CacheBehaviour::MustRevalidate), &state.pool, diff --git a/packages/app-lib/src/state/instances/commands/check_content_updates.rs b/packages/app-lib/src/state/instances/commands/check_content_updates.rs index 771ae806b9..3ec355e966 100644 --- a/packages/app-lib/src/state/instances/commands/check_content_updates.rs +++ b/packages/app-lib/src/state/instances/commands/check_content_updates.rs @@ -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, 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::>(); - 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::>(); 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::>(); 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, state: &State, @@ -168,6 +174,7 @@ async fn installed_update_channels( .map(|candidate| candidate.current_version_id.as_str()) .collect::>(); let versions = CachedEntry::get_version_many( + context, &version_ids, cache_behaviour, &state.pool, diff --git a/packages/app-lib/src/state/instances/commands/create_instance.rs b/packages/app-lib/src/state/instances/commands/create_instance.rs index 7567ec815c..48c019b55e 100644 --- a/packages/app-lib/src/state/instances/commands/create_instance.rs +++ b/packages/app-lib/src/state/instances/commands/create_instance.rs @@ -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 { @@ -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> { @@ -178,6 +183,7 @@ async fn resolve_icon_path( || icon.starts_with("http://") { let fetched = fetch::fetch( + context, icon, None, None, diff --git a/packages/app-lib/src/state/instances/commands/list_content.rs b/packages/app-lib/src/state/instances/commands/list_content.rs index 4272f2990a..83f98670f5 100644 --- a/packages/app-lib/src/state/instances/commands/list_content.rs +++ b/packages/app-lib/src/state/instances/commands/list_content.rs @@ -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, @@ -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> { 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, @@ -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::>(); 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, @@ -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::>(); 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::>(); 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, @@ -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, pool: &SqlitePool, @@ -464,6 +491,7 @@ pub(crate) async fn dependencies_to_content_items( .filter_map(|dependency| dependency.version_id.clone()) .collect::>(); 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, state: &State, filter: ContentFilter<'_>, ) -> crate::Result> { - 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::>(); 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::>(); 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::>(); 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, cache_behaviour: Option, pool: &SqlitePool, @@ -760,6 +794,7 @@ async fn get_installed_update_channels( } let version_id_refs = version_ids.iter().copied().collect::>(); 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, @@ -831,6 +867,7 @@ async fn content_files_to_content_items( }) .collect::>(); 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, version_ids: &HashSet, cache_behaviour: Option, @@ -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> { - 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 { - 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::>(); - 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), diff --git a/packages/app-lib/src/state/instances/commands/sync_content_files.rs b/packages/app-lib/src/state/instances/commands/sync_content_files.rs index 3686558393..833dfc6a4d 100644 --- a/packages/app-lib/src/state/instances/commands/sync_content_files.rs +++ b/packages/app-lib/src/state/instances/commands/sync_content_files.rs @@ -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> { @@ -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> { @@ -33,6 +35,7 @@ pub(crate) async fn sync_instance_content_files( .map(|file| file.hash_cache_key.as_str()) .collect::>(); let hashes = CachedEntry::get_file_hash_many( + context, &cache_keys, None, &state.pool, diff --git a/packages/app-lib/src/state/instances/watcher.rs b/packages/app-lib/src/state/instances/watcher.rs index 08647a5dc3..d3ae6cd4c5 100644 --- a/packages/app-lib/src/state/instances/watcher.rs +++ b/packages/app-lib/src/state/instances/watcher.rs @@ -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 { 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) => { diff --git a/packages/app-lib/src/state/mod.rs b/packages/app-lib/src/state/mod.rs index 18fa7edbb8..f1e873bfe0 100644 --- a/packages/app-lib/src/state/mod.rs +++ b/packages/app-lib/src/state/mod.rs @@ -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, diff --git a/packages/app-lib/src/state/mr_auth.rs b/packages/app-lib/src/state/mr_auth.rs index 09b735ea53..026e4b0183 100644 --- a/packages/app-lib/src/state/mr_auth.rs +++ b/packages/app-lib/src/state/mr_auth.rs @@ -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> { @@ -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::>(); CachedEntry::get_user_many( + context, &user_ids.iter().map(|x| &**x).collect::>(), 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 { let result = fetch_advanced( + context, Method::GET, concat!(env!("MODRINTH_API_URL"), "user"), None, diff --git a/packages/app-lib/src/state/process.rs b/packages/app-lib/src/state/process.rs index 669dac581f..3e97e08662 100644 --- a/packages/app-lib/src/state/process.rs +++ b/packages/app-lib/src/state/process.rs @@ -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, @@ -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 diff --git a/packages/app-lib/src/util/fetch.rs b/packages/app-lib/src/util/fetch.rs index 0662a3d5c4..8812ce6ac2 100644 --- a/packages/app-lib/src/util/fetch.rs +++ b/packages/app-lib/src/util/fetch.rs @@ -2,6 +2,7 @@ use super::io::{self, IOError}; use crate::event::LoadingBarId; use crate::event::emit::emit_loading; +use crate::operation::{InvocationContext, REQUEST_CONTEXT_HEADER}; use crate::{ErrorKind, LabrinthError}; use bytes::Bytes; use chrono::{DateTime, TimeDelta, Utc}; @@ -333,6 +334,7 @@ fn duration_seconds_ceil(duration: Duration) -> u64 { 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))) @@ -353,6 +355,41 @@ pub static REQWEST_CLIENT: LazyLock = 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( @@ -364,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>, @@ -372,6 +410,7 @@ pub async fn fetch( exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, ) -> crate::Result { fetch_advanced( + context, Method::GET, url, sha1, @@ -388,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>, @@ -397,6 +437,7 @@ pub async fn fetch_with_client( client: &reqwest::Client, ) -> crate::Result { fetch_advanced_with_client( + context, Method::GET, url, sha1, @@ -414,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>, @@ -424,6 +466,7 @@ pub async fn fetch_with_client_progress( progress: Option<&mut FetchProgressFn<'_>>, ) -> crate::Result { fetch_advanced_with_client_and_progress( + context, Method::GET, url, sha1, @@ -442,6 +485,7 @@ pub async fn fetch_with_client_progress( #[tracing::instrument(skip(json_body, semaphore))] pub async fn fetch_json( + context: &InvocationContext, method: Method, url: &str, sha1: Option<&str>, @@ -454,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)?; @@ -467,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>, @@ -479,6 +524,7 @@ pub async fn fetch_advanced( exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, ) -> crate::Result { fetch_advanced_with_client( + context, method, url, sha1, @@ -497,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>, @@ -510,6 +557,7 @@ pub async fn fetch_advanced_with_progress( progress: Option<&mut FetchProgressFn<'_>>, ) -> crate::Result { fetch_advanced_with_client_and_progress( + context, method, url, sha1, @@ -530,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>, @@ -543,6 +592,7 @@ pub async fn fetch_advanced_with_client( client: &reqwest::Client, ) -> crate::Result { fetch_advanced_with_client_and_progress( + context, method, url, sha1, @@ -562,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>, @@ -608,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); @@ -755,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>, @@ -770,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, @@ -790,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>, @@ -806,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, @@ -828,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, @@ -835,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? @@ -938,6 +1003,116 @@ pub async fn sha1_async(bytes: Bytes) -> crate::Result { 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, ) -> crate::Result<(u64, String)> {