From 156045940952350986f2cdb3dd5016ea487a2543 Mon Sep 17 00:00:00 2001 From: Zacharie Dubrulle Date: Fri, 26 Sep 2025 11:41:15 +0200 Subject: [PATCH 1/3] feat: split wrapper command on linux --- Cargo.lock | 6 ++++-- Cargo.toml | 2 ++ packages/app-lib/Cargo.toml | 4 ++++ packages/app-lib/src/launcher/mod.rs | 18 +++++++++++++++++- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 68f3dcbd66..533569dc20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1342,9 +1342,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -9024,6 +9024,7 @@ dependencies = [ "base64 0.22.1", "bytemuck", "bytes", + "cfg-if", "chardetng", "chrono", "daedalus", @@ -9062,6 +9063,7 @@ dependencies = [ "serde_with", "sha1_smol", "sha2", + "shlex", "sqlx", "sysinfo", "tauri", diff --git a/Cargo.toml b/Cargo.toml index 2c80cd5e31..e665c195fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ bitflags = "2.9.1" bytemuck = "1.23.1" bytes = "1.10.1" censor = "0.3.0" +cfg-if = "1.0.3" chardetng = "0.1.17" chrono = "0.4.41" clap = "4.5.43" @@ -137,6 +138,7 @@ serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde i sha1 = "0.10.6" sha1_smol = { version = "1.0.1", features = ["std"] } sha2 = "0.10.9" +shlex = "1.3.0" spdx = "0.10.9" sqlx = { version = "0.8.6", default-features = false } sysinfo = { version = "0.36.1", default-features = false } diff --git a/packages/app-lib/Cargo.toml b/packages/app-lib/Cargo.toml index c6af3741c5..ceee2691ee 100644 --- a/packages/app-lib/Cargo.toml +++ b/packages/app-lib/Cargo.toml @@ -36,6 +36,7 @@ rgb.workspace = true phf.workspace = true itertools.workspace = true derive_more = { workspace = true, features = ["display"] } +cfg-if.workspace = true chrono = { workspace = true, features = ["serde"] } daedalus.workspace = true @@ -116,6 +117,9 @@ ariadne.workspace = true [target.'cfg(windows)'.dependencies] winreg.workspace = true +[target.'cfg(unix)'.dependencies] +shlex.workspace = true + [build-dependencies] dotenvy.workspace = true dunce.workspace = true diff --git a/packages/app-lib/src/launcher/mod.rs b/packages/app-lib/src/launcher/mod.rs index 1b7a7d7e0e..92c3020950 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -14,6 +14,7 @@ use crate::state::{ use crate::util::io; use crate::util::rpc::RpcServerBuilder; use crate::{State, get_resource_file, process, state as st}; +use cfg_if::cfg_if; use chrono::Utc; use daedalus as d; use daedalus::minecraft::{LoggingSide, RuleAction, VersionInfo}; @@ -567,7 +568,22 @@ 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 command = { + cfg_if! { + if #[cfg(unix)] { + let cmd = shlex::split(hook).ok_or_else(|| { + crate::ErrorKind::LauncherError(format!( + "Invalid wrapper command: {hook}", + )) + })?; + let mut command = Command::new(cmd[0].clone()); + command.args(&cmd[1..]); + command + } else { + Command::new(hook) + } + } + }; command.arg(&java_version.path); command } From 17970f740c5e29e9cb355767913780fe437c8e2c Mon Sep 17 00:00:00 2001 From: Zacharie Dubrulle Date: Sat, 27 Sep 2025 22:19:59 +0200 Subject: [PATCH 2/3] feat: use code from #3900 --- packages/app-lib/src/api/profile/mod.rs | 17 ++++++++++++- packages/app-lib/src/launcher/mod.rs | 20 ++++++++++----- packages/app-lib/src/state/process.rs | 17 ++++++++++++- packages/app-lib/src/state/profiles.rs | 34 ++++++++++++++++++++++++- 4 files changed, 78 insertions(+), 10 deletions(-) diff --git a/packages/app-lib/src/api/profile/mod.rs b/packages/app-lib/src/api/profile/mod.rs index 27869d5124..5364c41d1a 100644 --- a/packages/app-lib/src/api/profile/mod.rs +++ b/packages/app-lib/src/api/profile/mod.rs @@ -24,6 +24,7 @@ use std::collections::{HashMap, HashSet}; use crate::data::Settings; use crate::server_address::ServerAddress; +use cfg_if::cfg_if; use dashmap::DashMap; use std::iter::FromIterator; use std::{ @@ -663,7 +664,21 @@ 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 = { + cfg_if! { + if #[cfg(unix)] { + shlex::split(hook) + .ok_or_else(|| { + crate::ErrorKind::LauncherError(format!( + "Invalid pre-launch command: {hook}", + )) + })? + .into_iter() + } else { + hook.split(' ') + } + } + }; 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 92c3020950..8f481df7b2 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -571,13 +571,19 @@ pub async fn launch_minecraft( let mut command = { cfg_if! { if #[cfg(unix)] { - let cmd = shlex::split(hook).ok_or_else(|| { - crate::ErrorKind::LauncherError(format!( - "Invalid wrapper command: {hook}", - )) - })?; - let mut command = Command::new(cmd[0].clone()); - command.args(&cmd[1..]); + 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 } else { Command::new(hook) diff --git a/packages/app-lib/src/state/process.rs b/packages/app-lib/src/state/process.rs index 4cff0a33e9..f50146707d 100644 --- a/packages/app-lib/src/state/process.rs +++ b/packages/app-lib/src/state/process.rs @@ -3,6 +3,7 @@ use crate::event::{ProcessPayloadType, ProfilePayloadType}; use crate::profile; use crate::util::io::IOError; use crate::util::rpc::RpcServer; +use cfg_if::cfg_if; use chrono::{DateTime, NaiveDateTime, TimeZone, Utc}; use dashmap::DashMap; use quick_xml::Reader; @@ -743,7 +744,21 @@ 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 = { + cfg_if! { + if #[cfg(unix)] { + shlex::split(&hook) + .ok_or_else(|| { + crate::ErrorKind::LauncherError(format!( + "Invalid post-exit command: {hook}", + )) + })? + .into_iter() + } else { + hook.split(' ') + } + } + }; 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 f11eeb489c..58e36f430e 100644 --- a/packages/app-lib/src/state/profiles.rs +++ b/packages/app-lib/src/state/profiles.rs @@ -7,6 +7,7 @@ use crate::state::{ use crate::util; use crate::util::fetch::{FetchSemaphore, IoSemaphore, write_cached_icon}; use crate::util::io::{self}; +use cfg_if::cfg_if; use chrono::{DateTime, TimeDelta, TimeZone, Utc}; use dashmap::DashMap; use regex::Regex; @@ -103,10 +104,11 @@ impl ProfileInstallStage { pub enum LauncherFeatureVersion { None, MigratedServerLastPlayTime, + MigratedLaunchHooksLinux, } impl LauncherFeatureVersion { - pub const MOST_RECENT: Self = Self::MigratedServerLastPlayTime; + pub const MOST_RECENT: Self = Self::MigratedLaunchHooksLinux; pub fn as_str(&self) -> &'static str { match *self { @@ -114,6 +116,7 @@ impl LauncherFeatureVersion { Self::MigratedServerLastPlayTime => { "migrated_server_last_play_time" } + Self::MigratedLaunchHooksLinux => "migrated_launch_hooks_linux", } } @@ -123,6 +126,7 @@ impl LauncherFeatureVersion { "migrated_server_last_play_time" => { Self::MigratedServerLastPlayTime } + "migrated_launch_hooks_linux" => Self::MigratedLaunchHooksLinux, _ => Self::None, } } @@ -781,6 +785,34 @@ impl Profile { self.launcher_feature_version = LauncherFeatureVersion::MigratedServerLastPlayTime; } + LauncherFeatureVersion::MigratedServerLastPlayTime => { + cfg_if! { + if #[cfg(unix)] { + 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::MigratedLaunchHooksLinux; + } + } + } LauncherFeatureVersion::MOST_RECENT => unreachable!( "LauncherFeatureVersion::MOST_RECENT was not updated" ), From 1805d0b69953cf0e9e3cc38b7bbc4434e7b07a42 Mon Sep 17 00:00:00 2001 From: Zacharie Dubrulle Date: Mon, 29 Sep 2025 20:29:33 +0200 Subject: [PATCH 3/3] feat: also use shlex on Windows --- Cargo.lock | 1 - Cargo.toml | 1 - packages/app-lib/Cargo.toml | 5 +-- packages/app-lib/src/api/profile/mod.rs | 24 ++++-------- packages/app-lib/src/launcher/mod.rs | 36 +++++++----------- packages/app-lib/src/state/process.rs | 24 ++++-------- packages/app-lib/src/state/profiles.rs | 49 +++++++++++-------------- 7 files changed, 52 insertions(+), 88 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 533569dc20..84385768b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9024,7 +9024,6 @@ dependencies = [ "base64 0.22.1", "bytemuck", "bytes", - "cfg-if", "chardetng", "chrono", "daedalus", diff --git a/Cargo.toml b/Cargo.toml index e665c195fe..f44bc50123 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,6 @@ bitflags = "2.9.1" bytemuck = "1.23.1" bytes = "1.10.1" censor = "0.3.0" -cfg-if = "1.0.3" chardetng = "0.1.17" chrono = "0.4.41" clap = "4.5.43" diff --git a/packages/app-lib/Cargo.toml b/packages/app-lib/Cargo.toml index ceee2691ee..21f9683898 100644 --- a/packages/app-lib/Cargo.toml +++ b/packages/app-lib/Cargo.toml @@ -36,7 +36,7 @@ rgb.workspace = true phf.workspace = true itertools.workspace = true derive_more = { workspace = true, features = ["display"] } -cfg-if.workspace = true +shlex.workspace = true chrono = { workspace = true, features = ["serde"] } daedalus.workspace = true @@ -117,9 +117,6 @@ ariadne.workspace = true [target.'cfg(windows)'.dependencies] winreg.workspace = true -[target.'cfg(unix)'.dependencies] -shlex.workspace = true - [build-dependencies] dotenvy.workspace = true dunce.workspace = true diff --git a/packages/app-lib/src/api/profile/mod.rs b/packages/app-lib/src/api/profile/mod.rs index 5364c41d1a..141fd6b6fa 100644 --- a/packages/app-lib/src/api/profile/mod.rs +++ b/packages/app-lib/src/api/profile/mod.rs @@ -24,7 +24,6 @@ use std::collections::{HashMap, HashSet}; use crate::data::Settings; use crate::server_address::ServerAddress; -use cfg_if::cfg_if; use dashmap::DashMap; use std::iter::FromIterator; use std::{ @@ -664,21 +663,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 = { - cfg_if! { - if #[cfg(unix)] { - shlex::split(hook) - .ok_or_else(|| { - crate::ErrorKind::LauncherError(format!( - "Invalid pre-launch command: {hook}", - )) - })? - .into_iter() - } else { - 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 8f481df7b2..db29817fb8 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -14,7 +14,6 @@ use crate::state::{ use crate::util::io; use crate::util::rpc::RpcServerBuilder; use crate::{State, get_resource_file, process, state as st}; -use cfg_if::cfg_if; use chrono::Utc; use daedalus as d; use daedalus::minecraft::{LoggingSide, RuleAction, VersionInfo}; @@ -568,28 +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 = { - cfg_if! { - if #[cfg(unix)] { - 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 - } else { - 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/process.rs b/packages/app-lib/src/state/process.rs index f50146707d..0945e27cb1 100644 --- a/packages/app-lib/src/state/process.rs +++ b/packages/app-lib/src/state/process.rs @@ -3,7 +3,6 @@ use crate::event::{ProcessPayloadType, ProfilePayloadType}; use crate::profile; use crate::util::io::IOError; use crate::util::rpc::RpcServer; -use cfg_if::cfg_if; use chrono::{DateTime, NaiveDateTime, TimeZone, Utc}; use dashmap::DashMap; use quick_xml::Reader; @@ -744,21 +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 = { - cfg_if! { - if #[cfg(unix)] { - shlex::split(&hook) - .ok_or_else(|| { - crate::ErrorKind::LauncherError(format!( - "Invalid post-exit command: {hook}", - )) - })? - .into_iter() - } else { - 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 58e36f430e..9654e7a980 100644 --- a/packages/app-lib/src/state/profiles.rs +++ b/packages/app-lib/src/state/profiles.rs @@ -7,7 +7,6 @@ use crate::state::{ use crate::util; use crate::util::fetch::{FetchSemaphore, IoSemaphore, write_cached_icon}; use crate::util::io::{self}; -use cfg_if::cfg_if; use chrono::{DateTime, TimeDelta, TimeZone, Utc}; use dashmap::DashMap; use regex::Regex; @@ -104,11 +103,11 @@ impl ProfileInstallStage { pub enum LauncherFeatureVersion { None, MigratedServerLastPlayTime, - MigratedLaunchHooksLinux, + MigratedLaunchHooks, } impl LauncherFeatureVersion { - pub const MOST_RECENT: Self = Self::MigratedLaunchHooksLinux; + pub const MOST_RECENT: Self = Self::MigratedLaunchHooks; pub fn as_str(&self) -> &'static str { match *self { @@ -116,7 +115,7 @@ impl LauncherFeatureVersion { Self::MigratedServerLastPlayTime => { "migrated_server_last_play_time" } - Self::MigratedLaunchHooksLinux => "migrated_launch_hooks_linux", + Self::MigratedLaunchHooks => "migrated_launch_hooks", } } @@ -126,7 +125,7 @@ impl LauncherFeatureVersion { "migrated_server_last_play_time" => { Self::MigratedServerLastPlayTime } - "migrated_launch_hooks_linux" => Self::MigratedLaunchHooksLinux, + "migrated_launch_hooks" => Self::MigratedLaunchHooks, _ => Self::None, } } @@ -786,32 +785,28 @@ impl Profile { LauncherFeatureVersion::MigratedServerLastPlayTime; } LauncherFeatureVersion::MigratedServerLastPlayTime => { - cfg_if! { - if #[cfg(unix)] { - 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()) - } + let quoter = shlex::Quoter::new().allow_nul(true); - // 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(pre_launch) = self.hooks.pre_launch.as_ref() { + self.hooks.pre_launch = + Some(quoter.join(pre_launch.split(' ')).unwrap()) + } - // 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()) - } + // 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()) + } - self.launcher_feature_version = - LauncherFeatureVersion::MigratedLaunchHooksLinux; - } + // 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"