1
0

feat: split wrapper command on linux (#4427)

* feat: split wrapper command on linux

* feat: use code from #3900

* feat: also use shlex on Windows

* feat: add a version number to global settings

* feat(app): add settings v2, where wrapper command are split
This commit is contained in:
Rayzeq
2025-10-30 22:48:51 +01:00
committed by GitHub
parent 8b39ba491a
commit 6b5f8a41e7
14 changed files with 152 additions and 13 deletions

1
Cargo.lock generated
View File

@@ -9463,6 +9463,7 @@ dependencies = [
"serde_with",
"sha1_smol",
"sha2",
"shlex",
"sqlx",
"sysinfo",
"tauri",

View File

@@ -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 }

View File

@@ -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

View File

@@ -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"
}

View File

@@ -41,7 +41,7 @@
{
"name": "display_claims!: serde_json::Value",
"ordinal": 7,
"type_info": "Text"
"type_info": "Null"
}
],
"parameters": {

View File

@@ -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"
}

View File

@@ -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",

View File

@@ -0,0 +1 @@
ALTER TABLE settings ADD COLUMN version INTEGER NOT NULL DEFAULT 1;

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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(),
);

View File

@@ -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(

View File

@@ -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"
),

View File

@@ -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<String>,
pub pending_update_toast_for_version: Option<String>,
pub auto_download_updates: Option<bool>,
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<Self> {
@@ -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<Sqlite>) -> 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