diff --git a/desktop/src-tauri/process/src/error.rs b/desktop/src-tauri/process/src/error.rs index 8869a463..94fec5f1 100644 --- a/desktop/src-tauri/process/src/error.rs +++ b/desktop/src-tauri/process/src/error.rs @@ -19,6 +19,7 @@ pub enum ProcessError { OpenerError(Arc), InvalidArguments(String), FailedLaunch(String), + NotExecutable(String), NoCompat, } @@ -39,6 +40,9 @@ impl Display for ProcessError { ProcessError::FailedLaunch(game_id) => { &format!("Drop detected that the game {game_id} may have failed to launch properly") } + ProcessError::NotExecutable(command) => { + &format!("The command '{command}' exists but is not marked as executable") + } ProcessError::RequiredDependency(game_id, version_id) => &format!( "Missing a required dependency to launch this game: {} {}", game_id, version_id diff --git a/desktop/src-tauri/process/src/parser.rs b/desktop/src-tauri/process/src/parser.rs index 948fcf64..cbcfdc7b 100644 --- a/desktop/src-tauri/process/src/parser.rs +++ b/desktop/src-tauri/process/src/parser.rs @@ -1,4 +1,6 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; + +use log::info; use crate::error::ProcessError; @@ -38,6 +40,41 @@ impl ParsedCommand { .to_string(); } + pub fn make_command_absolute_if_local(&mut self, base: &Path) { + let candidate = base.join(&self.command); + if candidate.is_file() { + info!( + "resolved local command '{}' to absolute path '{}'", + self.command, + candidate.display() + ); + self.command = candidate.to_string_lossy().to_string(); + } else { + info!( + "command '{}' is not a local file in '{}', leaving as-is for PATH resolution", + self.command, + base.display() + ); + } + } + + pub fn ensure_executable(&self) -> Result<(), ProcessError> { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + if let Ok(metadata) = std::fs::metadata(&self.command) { + let is_executable = + metadata.is_file() && metadata.permissions().mode() & 0o111 != 0; + if !is_executable { + return Err(ProcessError::NotExecutable(self.command.clone())); + } + } + } + + Ok(()) + } + pub fn reconstruct(self) -> String { let mut v = vec![]; v.extend(self.env); diff --git a/desktop/src-tauri/process/src/process_handlers.rs b/desktop/src-tauri/process/src/process_handlers.rs index 977ea661..d3f343f2 100644 --- a/desktop/src-tauri/process/src/process_handlers.rs +++ b/desktop/src-tauri/process/src/process_handlers.rs @@ -312,9 +312,12 @@ impl ProcessHandler for UMUNativeLauncher { let pfx_dir = pfx_dir.join(meta.id.clone()); create_dir_all(&pfx_dir)?; + let game_id_env = shell_words::quote(&format!("GAMEID={game_id}")).into_owned(); + let wineprefix_env = + shell_words::quote(&format!("WINEPREFIX={}", pfx_dir.to_string_lossy())).into_owned(); + Ok(format!( - "GAMEID={game_id} UMU_NO_PROTON=1 WINEPREFIX={} {umu:?} {launch}", - pfx_dir.to_string_lossy(), + "{game_id_env} UMU_NO_PROTON=1 {wineprefix_env} {umu:?} {launch}", umu = UMU_LAUNCHER_EXECUTABLE .as_ref() .expect("Failed to get UMU_LAUNCHER_EXECUTABLE as ref"), @@ -388,12 +391,17 @@ impl ProcessHandler for UMUCompatLauncher { if !proton_valid { return Err(ProcessError::NoCompat); } - let proton_env = format!("PROTONPATH={}", proton_path); + + // Shell-quote the env assignments so values containing spaces (e.g. a + // Proton install named "Proton-GE Latest") aren't split into separate + // tokens and misinterpreted as the command by the launch parser. + let game_id_env = shell_words::quote(&format!("GAMEID={game_id}")).into_owned(); + let proton_env = shell_words::quote(&format!("PROTONPATH={proton_path}")).into_owned(); + let wineprefix_env = + shell_words::quote(&format!("WINEPREFIX={}", pfx_dir.to_string_lossy())).into_owned(); Ok(format!( - "GAMEID={game_id} {} WINEPREFIX={} {umu:?} {launch}", - proton_env, - pfx_dir.to_string_lossy(), + "{game_id_env} {proton_env} {wineprefix_env} {umu:?} {launch}", umu = UMU_LAUNCHER_EXECUTABLE .as_ref() .expect("Failed to get UMU_LAUNCHER_EXECUTABLE as ref"), diff --git a/desktop/src-tauri/process/src/process_manager.rs b/desktop/src-tauri/process/src/process_manager.rs index e7389dec..5822c471 100644 --- a/desktop/src-tauri/process/src/process_manager.rs +++ b/desktop/src-tauri/process/src/process_manager.rs @@ -523,6 +523,13 @@ impl ProcessManager<'_> { install_dir.into(), ); + let mut launch_parameters = launch_parameters; + launch_parameters + .0 + .make_command_absolute_if_local(&launch_parameters.1); + + launch_parameters.0.ensure_executable()?; + info!( "launching (in {}): {:?}", launch_parameters.1.to_string_lossy(),