diff --git a/Cargo.lock b/Cargo.lock index 41bea0586..069844001 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9463,6 +9463,7 @@ dependencies = [ "serde_with", "sha1_smol", "sha2", + "shlex", "sqlx", "sysinfo", "tauri", diff --git a/Cargo.toml b/Cargo.toml index eb2310501..d7efdc3e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -158,6 +158,7 @@ serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde sha1 = "0.10.6" sha1_smol = { version = "1.0.1", features = ["std"] } sha2 = "0.10.9" +shlex = "1.3.0" spdx = "0.12.0" sqlx = { version = "0.8.6", default-features = false } sysinfo = { version = "0.37.2", default-features = false } diff --git a/apps/app-frontend/src/helpers/settings.ts b/apps/app-frontend/src/helpers/settings.ts index b7bbcd920..ae935224c 100644 --- a/apps/app-frontend/src/helpers/settings.ts +++ b/apps/app-frontend/src/helpers/settings.ts @@ -67,6 +67,8 @@ export type AppSettings = { skipped_update: string | null pending_update_toast_for_version: string | null auto_download_updates: boolean | null + + version: number } // Get full settings object diff --git a/packages/app-lib/.sqlx/query-7dc83d7ffa3d583fc5ffaf13811a8dab4d0b9ded6200f827b9de7ac32e5318d5.json b/packages/app-lib/.sqlx/query-07ea3a644644de61c4ed7c30ee711d29fd49f10534230b1b03097275a30cb50f.json similarity index 93% rename from packages/app-lib/.sqlx/query-7dc83d7ffa3d583fc5ffaf13811a8dab4d0b9ded6200f827b9de7ac32e5318d5.json rename to packages/app-lib/.sqlx/query-07ea3a644644de61c4ed7c30ee711d29fd49f10534230b1b03097275a30cb50f.json index 43518eb57..4ebdb79b0 100644 --- a/packages/app-lib/.sqlx/query-7dc83d7ffa3d583fc5ffaf13811a8dab4d0b9ded6200f827b9de7ac32e5318d5.json +++ b/packages/app-lib/.sqlx/query-07ea3a644644de61c4ed7c30ee711d29fd49f10534230b1b03097275a30cb50f.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT\n max_concurrent_writes, max_concurrent_downloads,\n theme, default_page, collapsed_navigation, hide_nametag_skins_page, advanced_rendering, native_decorations,\n discord_rpc, developer_mode, telemetry, personalized_ads,\n onboarded,\n json(extra_launch_args) extra_launch_args, json(custom_env_vars) custom_env_vars,\n mc_memory_max, mc_force_fullscreen, mc_game_resolution_x, mc_game_resolution_y, hide_on_process_start,\n hook_pre_launch, hook_wrapper, hook_post_exit,\n custom_dir, prev_custom_dir, migrated, json(feature_flags) feature_flags, toggle_sidebar,\n skipped_update, pending_update_toast_for_version, auto_download_updates\n FROM settings\n ", + "query": "\n SELECT\n max_concurrent_writes, max_concurrent_downloads,\n theme, default_page, collapsed_navigation, hide_nametag_skins_page, advanced_rendering, native_decorations,\n discord_rpc, developer_mode, telemetry, personalized_ads,\n onboarded,\n json(extra_launch_args) extra_launch_args, json(custom_env_vars) custom_env_vars,\n mc_memory_max, mc_force_fullscreen, mc_game_resolution_x, mc_game_resolution_y, hide_on_process_start,\n hook_pre_launch, hook_wrapper, hook_post_exit,\n custom_dir, prev_custom_dir, migrated, json(feature_flags) feature_flags, toggle_sidebar,\n skipped_update, pending_update_toast_for_version, auto_download_updates,\n version\n FROM settings\n ", "describe": { "columns": [ { @@ -157,6 +157,11 @@ "name": "auto_download_updates", "ordinal": 30, "type_info": "Integer" + }, + { + "name": "version", + "ordinal": 31, + "type_info": "Integer" } ], "parameters": { @@ -193,8 +198,9 @@ false, true, true, - true + true, + false ] }, - "hash": "7dc83d7ffa3d583fc5ffaf13811a8dab4d0b9ded6200f827b9de7ac32e5318d5" + "hash": "07ea3a644644de61c4ed7c30ee711d29fd49f10534230b1b03097275a30cb50f" } diff --git a/packages/app-lib/.sqlx/query-6e3fa492c085ebb8e7280dd4d55cdcf73da199ea6ac05ee3ee798ece80d877cf.json b/packages/app-lib/.sqlx/query-6e3fa492c085ebb8e7280dd4d55cdcf73da199ea6ac05ee3ee798ece80d877cf.json index 22e39e75b..9742cb7b4 100644 --- a/packages/app-lib/.sqlx/query-6e3fa492c085ebb8e7280dd4d55cdcf73da199ea6ac05ee3ee798ece80d877cf.json +++ b/packages/app-lib/.sqlx/query-6e3fa492c085ebb8e7280dd4d55cdcf73da199ea6ac05ee3ee798ece80d877cf.json @@ -41,7 +41,7 @@ { "name": "display_claims!: serde_json::Value", "ordinal": 7, - "type_info": "Text" + "type_info": "Null" } ], "parameters": { diff --git a/packages/app-lib/.sqlx/query-eb95fac3043d0ffd10caef69cc469474cc5c0d36cc0698c4cc0852da81fed158.json b/packages/app-lib/.sqlx/query-a40e60da6dd1312d4a1ed52fa8fd2394e7ad21de1cb44cf8b93c4b1459cdc716.json similarity index 90% rename from packages/app-lib/.sqlx/query-eb95fac3043d0ffd10caef69cc469474cc5c0d36cc0698c4cc0852da81fed158.json rename to packages/app-lib/.sqlx/query-a40e60da6dd1312d4a1ed52fa8fd2394e7ad21de1cb44cf8b93c4b1459cdc716.json index 90d12211d..e80d1cb60 100644 --- a/packages/app-lib/.sqlx/query-eb95fac3043d0ffd10caef69cc469474cc5c0d36cc0698c4cc0852da81fed158.json +++ b/packages/app-lib/.sqlx/query-a40e60da6dd1312d4a1ed52fa8fd2394e7ad21de1cb44cf8b93c4b1459cdc716.json @@ -1,12 +1,12 @@ { "db_name": "SQLite", - "query": "\n UPDATE settings\n SET\n max_concurrent_writes = $1,\n max_concurrent_downloads = $2,\n\n theme = $3,\n default_page = $4,\n collapsed_navigation = $5,\n advanced_rendering = $6,\n native_decorations = $7,\n\n discord_rpc = $8,\n developer_mode = $9,\n telemetry = $10,\n personalized_ads = $11,\n\n onboarded = $12,\n\n extra_launch_args = jsonb($13),\n custom_env_vars = jsonb($14),\n mc_memory_max = $15,\n mc_force_fullscreen = $16,\n mc_game_resolution_x = $17,\n mc_game_resolution_y = $18,\n hide_on_process_start = $19,\n\n hook_pre_launch = $20,\n hook_wrapper = $21,\n hook_post_exit = $22,\n\n custom_dir = $23,\n prev_custom_dir = $24,\n migrated = $25,\n\n toggle_sidebar = $26,\n feature_flags = $27,\n hide_nametag_skins_page = $28,\n\n skipped_update = $29,\n pending_update_toast_for_version = $30,\n auto_download_updates = $31\n ", + "query": "\n UPDATE settings\n SET\n max_concurrent_writes = $1,\n max_concurrent_downloads = $2,\n\n theme = $3,\n default_page = $4,\n collapsed_navigation = $5,\n advanced_rendering = $6,\n native_decorations = $7,\n\n discord_rpc = $8,\n developer_mode = $9,\n telemetry = $10,\n personalized_ads = $11,\n\n onboarded = $12,\n\n extra_launch_args = jsonb($13),\n custom_env_vars = jsonb($14),\n mc_memory_max = $15,\n mc_force_fullscreen = $16,\n mc_game_resolution_x = $17,\n mc_game_resolution_y = $18,\n hide_on_process_start = $19,\n\n hook_pre_launch = $20,\n hook_wrapper = $21,\n hook_post_exit = $22,\n\n custom_dir = $23,\n prev_custom_dir = $24,\n migrated = $25,\n\n toggle_sidebar = $26,\n feature_flags = $27,\n hide_nametag_skins_page = $28,\n\n skipped_update = $29,\n pending_update_toast_for_version = $30,\n auto_download_updates = $31,\n\n version = $32\n ", "describe": { "columns": [], "parameters": { - "Right": 31 + "Right": 32 }, "nullable": [] }, - "hash": "eb95fac3043d0ffd10caef69cc469474cc5c0d36cc0698c4cc0852da81fed158" + "hash": "a40e60da6dd1312d4a1ed52fa8fd2394e7ad21de1cb44cf8b93c4b1459cdc716" } diff --git a/packages/app-lib/Cargo.toml b/packages/app-lib/Cargo.toml index 709ec4154..d4343b91a 100644 --- a/packages/app-lib/Cargo.toml +++ b/packages/app-lib/Cargo.toml @@ -73,6 +73,7 @@ serde_json = { workspace = true } serde_with = { workspace = true } sha1_smol = { workspace = true } sha2 = { workspace = true } +shlex = { workspace = true } sqlx = { workspace = true, features = [ "json", "macros", diff --git a/packages/app-lib/migrations/20251002173041_settings-version.sql b/packages/app-lib/migrations/20251002173041_settings-version.sql new file mode 100644 index 000000000..285b2af6c --- /dev/null +++ b/packages/app-lib/migrations/20251002173041_settings-version.sql @@ -0,0 +1 @@ +ALTER TABLE settings ADD COLUMN version INTEGER NOT NULL DEFAULT 1; \ No newline at end of file diff --git a/packages/app-lib/src/api/profile/mod.rs b/packages/app-lib/src/api/profile/mod.rs index 209b78b63..935c837a2 100644 --- a/packages/app-lib/src/api/profile/mod.rs +++ b/packages/app-lib/src/api/profile/mod.rs @@ -666,7 +666,14 @@ async fn run_credentials( .filter(|hook_command| !hook_command.is_empty()); if let Some(hook) = pre_launch_hooks { // TODO: hook parameters - let mut cmd = hook.split(' '); + let mut cmd = shlex::split(hook) + .ok_or_else(|| { + crate::ErrorKind::LauncherError(format!( + "Invalid pre-launch command: {hook}", + )) + })? + .into_iter(); + if let Some(command) = cmd.next() { let full_path = get_full_path(&profile.path).await?; let result = Command::new(command) diff --git a/packages/app-lib/src/launcher/mod.rs b/packages/app-lib/src/launcher/mod.rs index 1b7a7d7e0..db29817fb 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -567,7 +567,19 @@ pub async fn launch_minecraft( let args = version_info.arguments.clone().unwrap_or_default(); let mut command = match wrapper { Some(hook) => { - let mut command = Command::new(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 } diff --git a/packages/app-lib/src/state/mod.rs b/packages/app-lib/src/state/mod.rs index ab7a5e3e9..7666218c0 100644 --- a/packages/app-lib/src/state/mod.rs +++ b/packages/app-lib/src/state/mod.rs @@ -89,6 +89,7 @@ impl State { let res = tokio::try_join!( state.discord_rpc.clear_to_default(true), Profile::refresh_all(), + Settings::migrate(&state.pool), ModrinthCredentials::refresh_all(), ); diff --git a/packages/app-lib/src/state/process.rs b/packages/app-lib/src/state/process.rs index ee527fb27..bfcb70cf1 100644 --- a/packages/app-lib/src/state/process.rs +++ b/packages/app-lib/src/state/process.rs @@ -743,7 +743,14 @@ impl Process { // We do not wait on the post exist command to finish running! We let it spawn + run on its own. // This behaviour may be changed in the future if let Some(hook) = post_exit_command { - let mut cmd = hook.split(' '); + let mut cmd = shlex::split(&hook) + .ok_or_else(|| { + crate::ErrorKind::LauncherError(format!( + "Invalid post-exit command: {hook}", + )) + })? + .into_iter(); + if let Some(command) = cmd.next() { let mut command = Command::new(command); command.args(cmd).current_dir( diff --git a/packages/app-lib/src/state/profiles.rs b/packages/app-lib/src/state/profiles.rs index 27fa0c58b..84d4e7726 100644 --- a/packages/app-lib/src/state/profiles.rs +++ b/packages/app-lib/src/state/profiles.rs @@ -103,10 +103,11 @@ impl ProfileInstallStage { pub enum LauncherFeatureVersion { None, MigratedServerLastPlayTime, + MigratedLaunchHooks, } impl LauncherFeatureVersion { - pub const MOST_RECENT: Self = Self::MigratedServerLastPlayTime; + pub const MOST_RECENT: Self = Self::MigratedLaunchHooks; pub fn as_str(&self) -> &'static str { match *self { @@ -114,6 +115,7 @@ impl LauncherFeatureVersion { Self::MigratedServerLastPlayTime => { "migrated_server_last_play_time" } + Self::MigratedLaunchHooks => "migrated_launch_hooks", } } @@ -123,6 +125,7 @@ impl LauncherFeatureVersion { "migrated_server_last_play_time" => { Self::MigratedServerLastPlayTime } + "migrated_launch_hooks" => Self::MigratedLaunchHooks, _ => Self::None, } } @@ -785,6 +788,30 @@ impl Profile { self.launcher_feature_version = LauncherFeatureVersion::MigratedServerLastPlayTime; } + LauncherFeatureVersion::MigratedServerLastPlayTime => { + let quoter = shlex::Quoter::new().allow_nul(true); + + // Previously split by spaces + if let Some(pre_launch) = self.hooks.pre_launch.as_ref() { + self.hooks.pre_launch = + Some(quoter.join(pre_launch.split(' ')).unwrap()) + } + + // Previously treated as complete path to command + if let Some(wrapper) = self.hooks.wrapper.as_ref() { + self.hooks.wrapper = + Some(quoter.quote(wrapper).unwrap().to_string()) + } + + // Previously split by spaces + if let Some(post_exit) = self.hooks.post_exit.as_ref() { + self.hooks.post_exit = + Some(quoter.join(post_exit.split(' ')).unwrap()) + } + + self.launcher_feature_version = + LauncherFeatureVersion::MigratedLaunchHooks; + } LauncherFeatureVersion::MOST_RECENT => unreachable!( "LauncherFeatureVersion::MOST_RECENT was not updated" ), diff --git a/packages/app-lib/src/state/settings.rs b/packages/app-lib/src/state/settings.rs index 785c7b453..9eef6c39c 100644 --- a/packages/app-lib/src/state/settings.rs +++ b/packages/app-lib/src/state/settings.rs @@ -1,6 +1,7 @@ //! Theseus settings file use serde::{Deserialize, Serialize}; +use sqlx::{Pool, Sqlite}; use std::collections::HashMap; // Types @@ -42,6 +43,8 @@ pub struct Settings { pub skipped_update: Option, pub pending_update_toast_for_version: Option, pub auto_download_updates: Option, + + pub version: usize, } #[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, Hash, PartialEq)] @@ -54,6 +57,8 @@ pub enum FeatureFlag { } impl Settings { + const CURRENT_VERSION: usize = 2; + pub async fn get( exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, ) -> crate::Result { @@ -68,7 +73,8 @@ impl Settings { mc_memory_max, mc_force_fullscreen, mc_game_resolution_x, mc_game_resolution_y, hide_on_process_start, hook_pre_launch, hook_wrapper, hook_post_exit, custom_dir, prev_custom_dir, migrated, json(feature_flags) feature_flags, toggle_sidebar, - skipped_update, pending_update_toast_for_version, auto_download_updates + skipped_update, pending_update_toast_for_version, auto_download_updates, + version FROM settings " ) @@ -126,6 +132,7 @@ impl Settings { pending_update_toast_for_version: res .pending_update_toast_for_version, auto_download_updates: res.auto_download_updates.map(|x| x == 1), + version: res.version as usize, }) } @@ -140,6 +147,7 @@ impl Settings { let extra_launch_args = serde_json::to_string(&self.extra_launch_args)?; let custom_env_vars = serde_json::to_string(&self.custom_env_vars)?; let feature_flags = serde_json::to_string(&self.feature_flags)?; + let version = self.version as i64; sqlx::query!( " @@ -183,7 +191,9 @@ impl Settings { skipped_update = $29, pending_update_toast_for_version = $30, - auto_download_updates = $31 + auto_download_updates = $31, + + version = $32 ", max_concurrent_writes, max_concurrent_downloads, @@ -216,12 +226,75 @@ impl Settings { self.skipped_update, self.pending_update_toast_for_version, self.auto_download_updates, + version, ) .execute(exec) .await?; Ok(()) } + + pub async fn migrate(exec: &Pool) -> crate::Result<()> { + let mut settings = Self::get(exec).await?; + + if settings.version < Settings::CURRENT_VERSION { + tracing::info!( + "Migrating settings version {} to {:?}", + settings.version, + Settings::CURRENT_VERSION + ); + } + while settings.version < Settings::CURRENT_VERSION { + if let Err(err) = settings.perform_migration() { + tracing::error!( + "Failed to migrate settings from version {}: {}", + settings.version, + err + ); + return Err(err); + } + } + + settings.update(exec).await?; + + Ok(()) + } + + pub fn perform_migration(&mut self) -> crate::Result<()> { + match self.version { + 1 => { + let quoter = shlex::Quoter::new().allow_nul(true); + + // Previously split by spaces + if let Some(pre_launch) = self.hooks.pre_launch.as_ref() { + self.hooks.pre_launch = + Some(quoter.join(pre_launch.split(' ')).unwrap()) + } + + // Previously treated as complete path to command + if let Some(wrapper) = self.hooks.wrapper.as_ref() { + self.hooks.wrapper = + Some(quoter.quote(wrapper).unwrap().to_string()) + } + + // Previously split by spaces + if let Some(post_exit) = self.hooks.post_exit.as_ref() { + self.hooks.post_exit = + Some(quoter.join(post_exit.split(' ')).unwrap()) + } + + self.version = 2; + } + version => { + return Err(crate::ErrorKind::OtherError(format!( + "Invalid settings version: {version}" + )) + .into()); + } + } + + Ok(()) + } } /// Theseus theme