//! Logic for launching Minecraft use crate::data::ModLoader; use crate::event::emit::{emit_instance, emit_loading, init_loading}; use crate::event::{InstancePayloadType, LoadingBarType}; use crate::install::{ InstallJavaStep, InstallPhaseDetails, InstallPhaseId, InstallProgress, InstallProgressReporter, }; use crate::instance::QuickPlayType; use crate::launcher::download::download_log_config; use crate::launcher::io::IOError; use crate::launcher::quick_play_version::{ QuickPlayServerVersion, QuickPlayVersion, }; use crate::server_address::{ServerAddress, parse_server_address}; use crate::state::server_join_log::JoinLogEntry; use crate::state::{ Credentials, InstanceInstallStage, InstanceLaunchContext, InstanceLink, JavaVersion, MemorySettings, ProcessMetadata, WindowSize, }; use crate::util::io; use crate::util::rpc::RpcServerBuilder; use crate::{State, get_resource_file, process}; use chrono::Utc; use daedalus as d; use daedalus::minecraft::{LoggingSide, RuleAction, VersionInfo}; use daedalus::modded::{LoaderVersion, Manifest}; use regex::Regex; use serde::Deserialize; use std::fmt::Write; use std::path::PathBuf; use tokio::process::Command; mod args; pub mod download; pub mod quick_play_version; // All nones -> disallowed // 1+ true -> allowed // 1+ false -> disallowed #[tracing::instrument] pub fn parse_rules( rules: &[d::minecraft::Rule], java_version: &str, quick_play_type: &QuickPlayType, minecraft_updated: bool, ) -> bool { let mut x = rules .iter() .map(|x| { parse_rule(x, java_version, quick_play_type, minecraft_updated) }) .collect::>>(); if rules .iter() .all(|x| matches!(x.action, RuleAction::Disallow)) { x.push(Some(true)) } !(x.iter().any(|x| x == &Some(false)) || x.iter().all(|x| x.is_none())) } // if anything is disallowed, it should NOT be included // if anything is not disallowed, it shouldn't factor in final result // if anything is not allowed, it should NOT be included // if anything is allowed, it should be included #[tracing::instrument] pub fn parse_rule( rule: &d::minecraft::Rule, java_version: &str, quick_play_type: &QuickPlayType, minecraft_updated: bool, ) -> Option { use d::minecraft::{Rule, RuleAction}; let res = match rule { Rule { os: Some(os), .. } => { crate::util::platform::os_rule(os, java_version, minecraft_updated) } Rule { features: Some(features), .. } => { !features.is_demo_user.unwrap_or(true) || features.has_custom_resolution.unwrap_or(false) || !features.has_quick_plays_support.unwrap_or(true) || (features.is_quick_play_singleplayer.unwrap_or(false) && matches!( quick_play_type, QuickPlayType::Singleplayer(_) )) || (features.is_quick_play_multiplayer.unwrap_or(false) && matches!(quick_play_type, QuickPlayType::Server(..))) || !features.is_quick_play_realms.unwrap_or(true) } _ => return Some(true), }; match rule.action { RuleAction::Allow => { if res { Some(true) } else { Some(false) } } RuleAction::Disallow => { if res { Some(false) } else { None } } } } macro_rules! processor_rules { ($dest:expr; $($name:literal : client => $client:expr, server => $server:expr;)+) => { $(std::collections::HashMap::insert( $dest, String::from($name), daedalus::modded::SidedDataEntry { client: String::from($client), server: String::from($server), }, );)+ } } pub async fn get_java_version_from_launch_context( context: &InstanceLaunchContext, version_info: &VersionInfo, ) -> crate::Result> { if let Some(java) = context.launch_overrides.java_path.as_ref() { let java = crate::api::jre::check_jre(std::path::PathBuf::from(java)).await; if let Ok(java) = java { return Ok(Some(java)); } } let key = version_info .java_version .as_ref() .map_or(8, |it| it.major_version); let state = State::get().await?; let java_version = JavaVersion::get(key, &state.pool).await?; Ok(java_version) } pub async fn get_loader_version_from_profile( game_version: &str, loader: ModLoader, loader_version: Option<&str>, ) -> crate::Result> { if loader == ModLoader::Vanilla { return Ok(None); } let version = loader_version.unwrap_or("latest"); let filter = |it: &LoaderVersion| match version { "latest" => true, "stable" => it.stable, id => it.id == *id, }; let versions = crate::api::metadata::get_loader_versions(loader.as_meta_str()).await?; if let Some(loaders) = loader_versions_for_game_version(&versions, game_version) { let loader_version = loaders .iter() .find(|x| filter(x)) .or(if version == "stable" { loaders.first() } else { None }); Ok(loader_version.cloned()) } else { Ok(None) } } fn loader_versions_for_game_version<'a>( manifest: &'a Manifest, game_version: &str, ) -> Option<&'a [LoaderVersion]> { let version = manifest.game_versions.iter().find(|x| { x.id.replace(daedalus::modded::DUMMY_REPLACE_STRING, game_version) == game_version })?; if let Some(version_group) = &version.version_group { manifest .version_groups .iter() .find(|group| group.id == *version_group) .map(|group| group.loaders.as_slice()) } else { Some(version.loaders.as_slice()) } } /// Resolves the Minecraft version manifest and finds the index for the given /// 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( game_version: &str, state: &State, ) -> crate::Result<(d::minecraft::VersionManifest, usize)> { let minecraft = crate::api::metadata::get_minecraft_versions().await?; if let Some(idx) = minecraft .versions .iter() .position(|it| it.id == game_version) { return Ok((minecraft, idx)); } // 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( Some(crate::state::CacheBehaviour::MustRevalidate), &state.pool, &state.api_semaphore, ) .await? .ok_or_else(|| { crate::ErrorKind::NoValueFor("minecraft versions".to_string()) })?; let idx = refreshed .versions .iter() .position(|it| it.id == game_version) .ok_or(crate::ErrorKind::LauncherError(format!( "Invalid game version: {game_version}" )))?; Ok((refreshed, idx)) } async fn get_instance_full_path(instance_path: &str) -> crate::Result { let state = State::get().await?; let instances_dir = state.directories.instances_dir(); let full_path = io::canonicalize(instances_dir.join(instance_path))?; Ok(full_path) } pub async fn install_minecraft_with_reporter( context: &InstanceLaunchContext, repairing: bool, reporter: Option, ) -> crate::Result<()> { let instance = &context.instance; let content_set = &context.applied_content_set; let phase_details = InstallPhaseDetails::Minecraft { game_version: content_set.game_version.clone(), loader: content_set.loader, }; let loading_bar = if reporter.is_none() { Some( init_loading( LoadingBarType::MinecraftDownload { // If we are downloading minecraft for a profile, provide its name and uuid instance_name: instance.name.clone(), instance_id: instance.id.clone(), }, 100.0, "Downloading Minecraft", ) .await?, ) } else { None }; let state = State::get().await?; crate::state::instances::commands::set_instance_install_stage( &instance.id, InstanceInstallStage::MinecraftInstalling, &state.pool, ) .await?; emit_instance(&instance.id, InstancePayloadType::Edited).await?; let instance_path = get_instance_full_path(&instance.path).await?; if let Some(reporter) = &reporter { reporter .update( InstallPhaseId::ResolvingMinecraft, None, phase_details.clone(), ) .await?; } let (minecraft, version_index) = resolve_minecraft_manifest(&content_set.game_version, &state).await?; let version = &minecraft.versions[version_index]; let minecraft_updated = version_index <= minecraft .versions .iter() .position(|x| x.id == "22w16a") .unwrap_or(0); if content_set.loader != ModLoader::Vanilla && let Some(reporter) = &reporter { reporter .update( InstallPhaseId::ResolvingLoader, None, phase_details.clone(), ) .await?; } let mut loader_version = get_loader_version_from_profile( &content_set.game_version, content_set.loader, content_set.loader_version.as_deref(), ) .await?; // 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( &content_set.game_version, content_set.loader, Some("stable"), ) .await?; crate::state::instances::commands::set_applied_content_set_loader_version( &instance.id, loader_version.as_ref().map(|x| x.id.as_str()), &state.pool, ) .await?; } let version_jar = loader_version.as_ref().map_or(version.id.clone(), |it| { format!("{}-{}", version.id.clone(), it.id.clone()) }); // Download version info (5) let mut version_info = download::download_version_info( &state, version, loader_version.as_ref(), Some(repairing), loading_bar.as_ref(), ) .await?; let key = version_info .java_version .as_ref() .map_or(8, |it| it.major_version); if let Some(reporter) = &reporter { reporter .update( InstallPhaseId::PreparingJava, Some(InstallProgress { current: 0, total: 4, secondary: None, }), InstallPhaseDetails::Java { major_version: key, step: InstallJavaStep::Resolving, }, ) .await?; } let (java_version, set_java) = if let Some(java_version) = get_java_version_from_launch_context(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( key, reporter.clone(), ) .await? } else { crate::api::jre::auto_install_java_with_loading(key, true).await? }; (path, true) }; // Test jre version if let Some(reporter) = &reporter { reporter .update( InstallPhaseId::PreparingJava, Some(InstallProgress { current: 4, total: 4, secondary: None, }), InstallPhaseDetails::Java { major_version: key, step: InstallJavaStep::Validating, }, ) .await?; } let java_version = crate::api::jre::check_jre(java_version.clone()).await?; if set_java { java_version.upsert(&state.pool).await?; } // Download minecraft (5-90) if let Some(reporter) = &reporter { reporter .update( InstallPhaseId::DownloadingMinecraft, None, phase_details.clone(), ) .await?; } download::download_minecraft( &state, &version_info, loading_bar.as_ref(), &java_version.architecture, repairing, minecraft_updated, reporter.clone(), phase_details.clone(), ) .await?; let client_path = state .directories .version_dir(&version_jar) .join(format!("{version_jar}.jar")); if let Some(processors) = &version_info.processors { let libraries_dir = state.directories.libraries_dir(); if let Some(ref mut data) = version_info.data { processor_rules! { data; "SIDE": client => "client", server => ""; "MINECRAFT_JAR" : client => client_path.to_string_lossy(), server => ""; "MINECRAFT_VERSION": client => content_set.game_version.clone(), server => ""; "ROOT": client => instance_path.to_string_lossy(), server => ""; "LIBRARY_DIR": client => libraries_dir.to_string_lossy(), server => ""; } if let Some(loading_bar) = &loading_bar { emit_loading( loading_bar, 0.0, Some("Running forge processors"), )?; } let total_length = processors.len(); if let Some(reporter) = &reporter { reporter .update( InstallPhaseId::RunningLoaderProcessors, Some(InstallProgress { current: 0, total: total_length as u64, secondary: None, }), phase_details.clone(), ) .await?; } // Forge processors (90-100) for (index, processor) in processors.iter().enumerate() { if let Some(sides) = &processor.sides && !sides.contains(&String::from("client")) { if let Some(reporter) = &reporter { reporter .update( InstallPhaseId::RunningLoaderProcessors, Some(InstallProgress { current: (index + 1) as u64, total: total_length as u64, secondary: None, }), phase_details.clone(), ) .await?; } continue; } let cp = { let mut cp = processor.classpath.clone(); cp.push(processor.jar.clone()); cp }; let child = Command::new(&java_version.path) .arg("-cp") .arg(args::get_class_paths_jar( &libraries_dir, &cp, &java_version.architecture, )?) .arg( args::get_processor_main_class(args::get_lib_path( &libraries_dir, &processor.jar, false, )?) .await? .ok_or_else(|| { crate::ErrorKind::LauncherError(format!( "Could not find processor main class for {}", processor.jar )) })?, ) .args(args::get_processor_arguments( &libraries_dir, &processor.args, data, )?) .output() .await .map_err(|e| IOError::with_path(e, &java_version.path)) .map_err(|err| { crate::ErrorKind::LauncherError(format!( "Error running processor: {err}", )) })?; if !child.status.success() { return Err(crate::ErrorKind::LauncherError(format!( "Processor error: {}", String::from_utf8_lossy(&child.stderr) )) .as_error()); } if let Some(loading_bar) = &loading_bar { emit_loading( loading_bar, 30.0 / total_length as f64, Some(&format!( "Running forge processor {index}/{total_length}" )), )?; } if let Some(reporter) = &reporter { reporter .update( InstallPhaseId::RunningLoaderProcessors, Some(InstallProgress { current: (index + 1) as u64, total: total_length as u64, secondary: None, }), phase_details.clone(), ) .await?; } } } } let protocol_version = read_protocol_version_from_jar(client_path).await?; crate::state::instances::commands::set_instance_install_stage( &instance.id, InstanceInstallStage::Installed, &state.pool, ) .await?; emit_instance(&instance.id, InstancePayloadType::Edited).await?; crate::state::instances::commands::set_applied_content_set_protocol_version( &instance.id, protocol_version, &state.pool, ) .await?; if let Some(loading_bar) = &loading_bar { emit_loading(loading_bar, 1.0, Some("Finished installing"))?; } Ok(()) } pub async fn install_minecraft_for_instance_id_with_reporter( instance_id: &str, repairing: bool, reporter: Option, ) -> crate::Result<()> { let state = State::get().await?; let context = crate::state::instances::commands::get_instance_launch_context( instance_id, &state.pool, ) .await? .ok_or_else(|| { crate::ErrorKind::OtherError(format!( "Tried to install a nonexistent or unloaded instance {instance_id}!" )) })?; install_minecraft_with_reporter(&context, repairing, reporter).await } pub async fn read_protocol_version_from_jar( path: PathBuf, ) -> crate::Result> { let zip = async_zip::tokio::read::fs::ZipFileReader::new(path).await?; let Some(entry_index) = zip .file() .entries() .iter() .position(|x| matches!(x.filename().as_str(), Ok("version.json"))) else { return Ok(None); }; #[derive(Deserialize, Debug)] struct VersionData { protocol_version: Option, } let mut data = vec![]; zip.reader_with_entry(entry_index) .await? .read_to_end_checked(&mut data) .await?; let data: VersionData = serde_json::from_slice(&data)?; Ok(data.protocol_version) } fn link_project_and_version( link: &InstanceLink, ) -> (Option<&String>, Option<&String>) { match link { InstanceLink::ModrinthModpack { project_id, version_id, } => (Some(project_id), Some(version_id)), InstanceLink::ServerProject { project_id } => (Some(project_id), None), InstanceLink::ServerProjectModpack { server_project_id, content_version_id, .. } => (Some(server_project_id), Some(content_version_id)), InstanceLink::ImportedModpack { project_id, version_id, .. } => (project_id.as_ref(), version_id.as_ref()), InstanceLink::Unmanaged | InstanceLink::ModrinthHosting { .. } | InstanceLink::SharedInstance { .. } => (None, None), } } #[tracing::instrument(skip_all)] #[allow(clippy::too_many_arguments)] pub async fn launch_minecraft( java_args: &[String], env_args: &[(String, String)], mc_set_options: &[(String, String)], wrapper: &Option, memory: &MemorySettings, resolution: &WindowSize, credentials: &Credentials, post_exit_hook: Option, context: &InstanceLaunchContext, mut quick_play_type: QuickPlayType, ) -> crate::Result { let instance = &context.instance; let content_set = &context.applied_content_set; if instance.install_stage == InstanceInstallStage::PackInstalling || instance.install_stage == InstanceInstallStage::MinecraftInstalling { return Err(crate::ErrorKind::LauncherError( "Instance is still installing".to_string(), ) .into()); } if instance.install_stage != InstanceInstallStage::Installed { return Err(crate::ErrorKind::LauncherError( "Instance is not installed; start an install job first".to_string(), ) .into()); } let state = State::get().await?; let instance_path = get_instance_full_path(&instance.path).await?; let (minecraft, version_index) = resolve_minecraft_manifest(&content_set.game_version, &state).await?; let version = &minecraft.versions[version_index]; let minecraft_updated = version_index <= minecraft .versions .iter() .position(|x| x.id == "22w16a") .unwrap_or(0); let loader_version = get_loader_version_from_profile( &content_set.game_version, content_set.loader, content_set.loader_version.as_deref(), ) .await?; if content_set.loader != ModLoader::Vanilla && loader_version.is_none() { return Err(crate::ErrorKind::LauncherError(format!( "No loader version selected for {}", content_set.loader.as_str() )) .into()); } let version_jar = loader_version.as_ref().map_or(version.id.clone(), |it| { format!("{}-{}", version.id.clone(), it.id.clone()) }); let mut version_info = download::download_version_info( &state, version, loader_version.as_ref(), None, None, ) .await?; if version_info.logging.is_none() { let requires_logging_info = version_index <= minecraft .versions .iter() .position(|x| x.id == "13w39a") .unwrap_or(0); if requires_logging_info { version_info = download::download_version_info( &state, version, loader_version.as_ref(), Some(true), None, ) .await?; } } let _ = download_log_config(&state, &version_info, None, false, None).await?; let java_version = get_java_version_from_launch_context(context, &version_info) .await? .ok_or_else(|| { crate::ErrorKind::LauncherError( "Missing correct java installation".to_string(), ) })?; // Test jre version let java_version = crate::api::jre::check_jre(java_version.path.clone().into()).await?; let client_path = state .directories .version_dir(&version_jar) .join(format!("{version_jar}.jar")); let args = version_info.arguments.clone().unwrap_or_default(); let mut command = match wrapper { Some(hook) => { let mut cmd = shlex::split(hook) .ok_or_else(|| { crate::ErrorKind::LauncherError(format!( "Invalid wrapper command: {hook}", )) })? .into_iter(); let mut command = Command::new(cmd.next().ok_or( crate::ErrorKind::LauncherError( "Empty wrapper command".to_owned(), ), )?); command.args(cmd); command.arg(&java_version.path); command } None => Command::new(&java_version.path), }; let env_args = Vec::from(env_args); // Check if instance has a running process, and reject running the command if it does // Done late so a quick double call doesn't launch two instances let existing_processes = process::get_by_instance_id(&instance.id).await?; if let Some(process) = existing_processes.first() { return Err(crate::ErrorKind::LauncherError(format!( "Instance {} is already running as process {}", instance.id, process.uuid )) .as_error()); } let natives_dir = state.directories.version_natives_dir(&version_jar); if !natives_dir.exists() { io::create_dir_all(&natives_dir).await?; } let quick_play_version = QuickPlayVersion::find_version(version_index, &minecraft.versions); tracing::debug!( "Found QuickPlayVersion for {}: {quick_play_version:?}", content_set.game_version ); if let QuickPlayType::Server(address) = &mut quick_play_type && quick_play_version.server >= QuickPlayServerVersion::BuiltinLegacy { // Record last-played for the original server address immediately so // recent-worlds can match without DNS/SRV resolution. let original = match address { ServerAddress::Unresolved(address) => parse_server_address(address) .ok() .map(|(h, p)| (h.to_owned(), p)), ServerAddress::Resolved { original_host, original_port, .. } => Some((original_host.clone(), *original_port)), }; if let Some((host, port)) = original && let Err(e) = (JoinLogEntry { instance_id: instance.id.clone(), host, port, join_time: Utc::now(), }) .upsert(&state.pool) .await { tracing::warn!("Failed to write server join log entry: {e}"); } address.resolve().await?; } let (main_class_keep_alive, main_class_path) = get_resource_file!(env "JAVA_JARS_DIR" / "theseus.jar")?; let rpc_server = RpcServerBuilder::new().launch().await?; command.args( args::get_jvm_arguments( args.get(&d::minecraft::ArgumentType::Jvm) .map(|x| x.as_slice()), &natives_dir, &state.directories.libraries_dir(), &state.directories.log_configs_dir(), &args::get_class_paths( &state.directories.libraries_dir(), version_info.libraries.as_slice(), &[&main_class_path, &client_path], &java_version.architecture, minecraft_updated, )?, &main_class_path, &version_jar, *memory, Vec::from(java_args), &java_version.architecture, &quick_play_type, quick_play_version, version_info .logging .as_ref() .and_then(|x| x.get(&LoggingSide::Client)), rpc_server.address(), )? .into_iter(), ); // The java launcher requires access to java.lang.reflect in order to force access in to // whatever module the main class is in if java_version.parsed_version >= 9 { command.arg("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED"); } // The java launcher code requires internal JDK code in Java 25+ in order to support JEP 512 if java_version.parsed_version >= 25 { command.arg("--add-opens=jdk.internal/jdk.internal.misc=ALL-UNNAMED"); } command .arg("com.modrinth.theseus.MinecraftLaunch") .arg(version_info.main_class.clone()) .args( args::get_minecraft_arguments( args.get(&d::minecraft::ArgumentType::Game) .map(|x| x.as_slice()), version_info.minecraft_arguments.as_deref(), credentials, &version.id, &version_info.asset_index.id, &instance_path, &state.directories.assets_dir(), &version.type_, *resolution, &java_version.architecture, &quick_play_type, quick_play_version, ) .await? .into_iter(), ) .current_dir(instance_path.clone()); // CARGO-set DYLD_LIBRARY_PATH breaks Minecraft on macOS during testing on playground #[cfg(target_os = "macos")] if std::env::var("CARGO").is_ok() { command.env_remove("DYLD_FALLBACK_LIBRARY_PATH"); } // Java options should be set in instance options (the existence of _JAVA_OPTIONS overwrites them) command.env_remove("_JAVA_OPTIONS"); command.envs(env_args); // Overwrites the minecraft options.txt file with the settings from the profile // Uses 'a:b' syntax which is not quite yaml if !mc_set_options.is_empty() { let options_path = instance_path.join("options.txt"); let (mut options_string, input_encoding) = if options_path.exists() { io::read_any_encoding_to_string(&options_path).await? } else { (String::new(), encoding_rs::UTF_8) }; // UTF-16 encodings may be successfully detected and read, but we cannot encode // them back, and it's technically possible that the game client strongly expects // such encoding if input_encoding != input_encoding.output_encoding() { return Err(crate::ErrorKind::LauncherError(format!( "The instance options.txt file uses an unsupported encoding: {}. \ Please either turn off instance options that need to modify this file, \ or convert the file to an encoding that both the game and this app support, \ such as UTF-8.", input_encoding.name() )) .into()); } for (key, value) in mc_set_options { let re = Regex::new(&format!(r"(?m)^{}:.*$", regex::escape(key)))?; // check if the regex exists in the file if !re.is_match(&options_string) { // The key was not found in the file, so append it write!(&mut options_string, "\n{key}:{value}").unwrap(); } else { let replaced_string = re .replace_all(&options_string, &format!("{key}:{value}")) .to_string(); options_string = replaced_string; } } io::write(&options_path, input_encoding.encode(&options_string).0) .await?; } crate::state::instances::commands::set_instance_last_played( &instance.id, Utc::now(), &state.pool, ) .await?; // If in tauri, and the 'minimize on launch' setting is enabled, minimize the window #[cfg(feature = "tauri")] { use crate::EventState; let window = EventState::get_main_window().await?; if let Some(window) = window { let settings = crate::state::Settings::get(&state.pool).await?; if settings.hide_on_process_start { window.minimize()?; } } } let _ = state .discord_rpc .set_activity(&format!("Playing {}", instance.name), true) .await; let _ = state .friends_socket .update_status(Some(instance.name.clone())) .await; // Create Minecraft child by inserting it into the state // This also spawns the process and prepares the subsequent processes state .process_manager .insert_new_process( &instance.id, &instance.path, &instance.name, command, post_exit_hook, state.directories.instance_logs_dir(&instance.path), version_info.logging.is_some(), main_class_keep_alive, rpc_server, async |process: &ProcessMetadata, rpc_server| { let process_start_time = process.start_time.to_rfc3339(); 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); let system_properties = [ ("modrinth.process.startTime", Some(&process_start_time)), ("modrinth.profile.created", Some(&instance_created_time)), ("modrinth.profile.icon", instance.icon_path.as_ref()), ("modrinth.profile.link.project", link_project_id), ("modrinth.profile.link.version", link_version_id), ( "modrinth.profile.modified", Some(&instance_modified_time), ), ("modrinth.profile.name", Some(&instance.name)), ]; for (key, value) in system_properties { let Some(value) = value else { continue; }; rpc_server .call_method_2::<()>("set_system_property", key, value) .await?; } rpc_server.call_method::<()>("launch").await?; Ok(()) }, ) .await }