From 76ab8c504c3d094b3c9d3b2034a4c16384220926 Mon Sep 17 00:00:00 2001 From: "Begonia, HE" <163421589+BegoniaHe@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:46:13 +0100 Subject: fix(windows): resolve Java executable path issues on Windows - Add normalize_java_path utility function with Windows-specific handling - Automatically append .exe extension when missing on Windows - Use 'where' command to locate java.exe in PATH if not found - Improve error messages with full path display for debugging - Apply path normalization in both start_game and install_forge commands This fixes the "Failed to launch java: program not found" error on Windows by properly handling Java executable paths, including relative paths, missing extensions, and PATH resolution. Reviewed-by: Claude Sonnet 4.5 --- src-tauri/src/main.rs | 24 ++++++++-- src-tauri/src/utils/mod.rs | 1 + src-tauri/src/utils/path.rs | 105 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 src-tauri/src/utils/path.rs (limited to 'src-tauri') diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 6ea6ece..ec19d0c 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -526,12 +526,28 @@ async fn start_game( emit_log!(window, format!("First 10 args: {:?}", &args[..10])); } + // Get Java path from config or detect + let java_path_str = if !config.java_path.is_empty() && config.java_path != "java" { + config.java_path.clone() + } else { + // Try to find a suitable Java installation + let javas = core::java::detect_all_java_installations(&app_handle); + if let Some(java) = javas.first() { + java.path.clone() + } else { + return Err( + "No Java installation found. Please configure Java in settings.".to_string(), + ); + } + }; + let java_path = utils::path::normalize_java_path(&java_path_str)?; + // Spawn the process emit_log!( window, - format!("Starting Java process: {}", config.java_path) + format!("Starting Java process: {}", java_path.display()) ); - let mut command = Command::new(&config.java_path); + let mut command = Command::new(&java_path); command.args(&args); command.current_dir(&game_dir); // Run in game directory command.stdout(Stdio::piped()); @@ -551,7 +567,7 @@ async fn start_game( // Spawn and handle output let mut child = command .spawn() - .map_err(|e| format!("Failed to launch java: {}", e))?; + .map_err(|e| format!("Failed to launch Java at '{}': {}\nPlease check your Java installation and path configuration in Settings.", java_path.display(), e))?; emit_log!(window, "Java process started successfully".to_string()); @@ -1540,7 +1556,7 @@ async fn install_forge( ); } }; - let java_path = std::path::PathBuf::from(&java_path_str); + let java_path = utils::path::normalize_java_path(&java_path_str)?; emit_log!(window, "Running Forge installer...".to_string()); diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index 00b9087..651d26b 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -1,4 +1,5 @@ pub mod zip; +pub mod path; // File system related utility functions #[allow(dead_code)] diff --git a/src-tauri/src/utils/path.rs b/src-tauri/src/utils/path.rs new file mode 100644 index 0000000..453064e --- /dev/null +++ b/src-tauri/src/utils/path.rs @@ -0,0 +1,105 @@ +/// Path utilities for cross-platform compatibility +use std::path::PathBuf; + +/// Normalize a Java executable path for the current platform. +/// +/// On Windows: +/// - Adds .exe extension if missing +/// - Attempts to locate java.exe in PATH if only "java" is provided +/// - Validates that the path exists +/// +/// On Unix: +/// - Returns the path as-is +/// +/// # Arguments +/// * `java_path` - The Java executable path to normalize +/// +/// # Returns +/// * `Ok(PathBuf)` - Normalized path that exists +/// * `Err(String)` - Error if the path cannot be found or validated +#[cfg(target_os = "windows")] +pub fn normalize_java_path(java_path: &str) -> Result { + let mut path = PathBuf::from(java_path); + + // If path doesn't exist and doesn't end with .exe, try adding .exe + if !path.exists() && path.extension().is_none() { + path.set_extension("exe"); + } + + // If still not found and it's just "java.exe", try to find it in PATH + if !path.exists() && path.file_name() == Some(std::ffi::OsStr::new("java.exe")) { + // Try to locate java.exe in PATH + if let Ok(output) = std::process::Command::new("where").arg("java").output() { + if output.status.success() { + let paths = String::from_utf8_lossy(&output.stdout); + if let Some(first_path) = paths.lines().next() { + path = PathBuf::from(first_path.trim()); + } + } + } + } + + // Verify the path exists + if !path.exists() { + return Err(format!( + "Java executable not found at: {}\nPlease configure a valid Java path in Settings.", + path.display() + )); + } + + Ok(path) +} + +#[cfg(not(target_os = "windows"))] +pub fn normalize_java_path(java_path: &str) -> Result { + let path = PathBuf::from(java_path); + + if !path.exists() && java_path == "java" { + // Try to find java in PATH + if let Ok(output) = std::process::Command::new("which").arg("java").output() { + if output.status.success() { + let path_str = String::from_utf8_lossy(&output.stdout); + if let Some(first_path) = path_str.lines().next() { + return Ok(PathBuf::from(first_path.trim())); + } + } + } + } + + if !path.exists() && java_path != "java" { + return Err(format!( + "Java executable not found at: {}\nPlease configure a valid Java path in Settings.", + path.display() + )); + } + + Ok(path) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[cfg(target_os = "windows")] + fn test_normalize_adds_exe_extension() { + // This test assumes java is not in the current directory + let result = normalize_java_path("nonexistent_java"); + // Should fail since the file doesn't exist + assert!(result.is_err()); + } + + #[test] + fn test_normalize_existing_path() { + // Test with a path that should exist on most systems + #[cfg(target_os = "windows")] + let test_path = "C:\\Windows\\System32\\cmd.exe"; + #[cfg(not(target_os = "windows"))] + let test_path = "/bin/sh"; + + if std::path::Path::new(test_path).exists() { + let result = normalize_java_path(test_path); + assert!(result.is_ok()); + } + } +} -- cgit v1.2.3-70-g09d2 From e38f3b0ac1277f3b918ceb5a819f98d598b1a419 Mon Sep 17 00:00:00 2001 From: "Begonia, HE" <163421589+BegoniaHe@users.noreply.github.com> Date: Fri, 16 Jan 2026 08:29:23 +0100 Subject: fix(path): resolve critical java path validation bug on unix Fix a critical bug in normalize_java_path where Unix implementation would return Ok(non-existent path) when java_path == "java" and the `which` command failed to find Java in PATH. This caused game launch failures with confusing error messages. Key changes: - Add strip_unc_prefix helper for Windows UNC path handling - Fix Unix bug: explicitly return error when PATH search fails - Apply canonicalize + strip_unc_prefix pattern to both platforms - Enhanced error messages distinguishing PATH vs specific path failures - Add comprehensive unit tests covering edge cases - Update documentation to reflect actual behavior Resolves issue where Windows and Unix users could not launch games when Java path resolution failed silently. Reviewed-by: Claude Sonnet 4.5 --- src-tauri/src/utils/path.rs | 160 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 149 insertions(+), 11 deletions(-) (limited to 'src-tauri') diff --git a/src-tauri/src/utils/path.rs b/src-tauri/src/utils/path.rs index 453064e..deaebb5 100644 --- a/src-tauri/src/utils/path.rs +++ b/src-tauri/src/utils/path.rs @@ -1,21 +1,43 @@ /// Path utilities for cross-platform compatibility use std::path::PathBuf; +/// Helper to strip UNC prefix on Windows (\\?\) +/// This is needed because std::fs::canonicalize adds UNC prefix on Windows +#[cfg(target_os = "windows")] +fn strip_unc_prefix(path: PathBuf) -> PathBuf { + let s = path.to_string_lossy().to_string(); + if s.starts_with(r"\\?\") { + return PathBuf::from(&s[4..]); + } + path +} + +#[cfg(not(target_os = "windows"))] +fn strip_unc_prefix(path: PathBuf) -> PathBuf { + path +} + /// Normalize a Java executable path for the current platform. /// +/// This function handles platform-specific requirements and validates that +/// the resulting path points to an executable Java binary. +/// /// On Windows: /// - Adds .exe extension if missing /// - Attempts to locate java.exe in PATH if only "java" is provided +/// - Resolves symlinks and strips UNC prefix /// - Validates that the path exists /// /// On Unix: -/// - Returns the path as-is +/// - Attempts to locate java in PATH using `which` if only "java" is provided +/// - Resolves symlinks to get canonical path +/// - Validates that the path exists /// /// # Arguments -/// * `java_path` - The Java executable path to normalize +/// * `java_path` - The Java executable path to normalize (can be relative, absolute, or "java") /// /// # Returns -/// * `Ok(PathBuf)` - Normalized path that exists +/// * `Ok(PathBuf)` - Canonicalized, validated path to Java executable /// * `Err(String)` - Error if the path cannot be found or validated #[cfg(target_os = "windows")] pub fn normalize_java_path(java_path: &str) -> Result { @@ -37,9 +59,17 @@ pub fn normalize_java_path(java_path: &str) -> Result { } } } + + // If still not found after PATH search, return specific error + if !path.exists() { + return Err( + "Java not found in PATH. Please install Java or configure the full path in Settings." + .to_string(), + ); + } } - // Verify the path exists + // Verify the path exists before canonicalization if !path.exists() { return Err(format!( "Java executable not found at: {}\nPlease configure a valid Java path in Settings.", @@ -47,38 +77,75 @@ pub fn normalize_java_path(java_path: &str) -> Result { )); } - Ok(path) + // Canonicalize and strip UNC prefix for clean path + let canonical = std::fs::canonicalize(&path) + .map_err(|e| format!("Failed to resolve Java path '{}': {}", path.display(), e))?; + + Ok(strip_unc_prefix(canonical)) } #[cfg(not(target_os = "windows"))] pub fn normalize_java_path(java_path: &str) -> Result { - let path = PathBuf::from(java_path); + let mut path = PathBuf::from(java_path); + // If path doesn't exist and it's just "java", try to find java in PATH if !path.exists() && java_path == "java" { - // Try to find java in PATH if let Ok(output) = std::process::Command::new("which").arg("java").output() { if output.status.success() { let path_str = String::from_utf8_lossy(&output.stdout); if let Some(first_path) = path_str.lines().next() { - return Ok(PathBuf::from(first_path.trim())); + path = PathBuf::from(first_path.trim()); } } } + + // If still not found after PATH search, return specific error + if !path.exists() { + return Err( + "Java not found in PATH. Please install Java or configure the full path in Settings." + .to_string(), + ); + } } - if !path.exists() && java_path != "java" { + // Verify the path exists before canonicalization + if !path.exists() { return Err(format!( "Java executable not found at: {}\nPlease configure a valid Java path in Settings.", path.display() )); } - Ok(path) + // Canonicalize to resolve symlinks and get absolute path + let canonical = std::fs::canonicalize(&path) + .map_err(|e| format!("Failed to resolve Java path '{}': {}", path.display(), e))?; + + Ok(strip_unc_prefix(canonical)) } #[cfg(test)] mod tests { use super::*; + use std::fs; + use std::io::Write; + + #[test] + #[cfg(target_os = "windows")] + fn test_normalize_nonexistent_path_windows() { + // Non-existent path should return error + let result = normalize_java_path("C:\\NonExistent\\Path\\java.exe"); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("not found")); + } + + #[test] + #[cfg(not(target_os = "windows"))] + fn test_normalize_nonexistent_path_unix() { + // Non-existent path should return error + let result = normalize_java_path("/nonexistent/path/java"); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("not found")); + } #[test] #[cfg(target_os = "windows")] @@ -90,7 +157,7 @@ mod tests { } #[test] - fn test_normalize_existing_path() { + fn test_normalize_existing_path_returns_canonical() { // Test with a path that should exist on most systems #[cfg(target_os = "windows")] let test_path = "C:\\Windows\\System32\\cmd.exe"; @@ -100,6 +167,77 @@ mod tests { if std::path::Path::new(test_path).exists() { let result = normalize_java_path(test_path); assert!(result.is_ok()); + let normalized = result.unwrap(); + // Should be absolute path after canonicalization + assert!(normalized.is_absolute()); + // Should not contain UNC prefix on Windows + #[cfg(target_os = "windows")] + assert!(!normalized.to_string_lossy().starts_with(r"\\?\")); + } + } + + #[test] + fn test_normalize_java_not_in_path() { + // When "java" is provided but not in PATH, should return error + // This test may pass if java IS in PATH, so we check error message format + let result = normalize_java_path("java"); + if result.is_err() { + let err = result.unwrap_err(); + assert!( + err.contains("not found in PATH") || err.contains("not found at"), + "Expected PATH error, got: {}", + err + ); + } + // If Ok, java was found in PATH - test passes + } + + #[test] + fn test_normalize_with_temp_file() { + // Create a temporary file to test with an actual existing path + let temp_dir = std::env::temp_dir(); + + #[cfg(target_os = "windows")] + let temp_file = temp_dir.join("test_java_normalize.exe"); + #[cfg(not(target_os = "windows"))] + let temp_file = temp_dir.join("test_java_normalize"); + + // Create the file + if let Ok(mut file) = fs::File::create(&temp_file) { + let _ = file.write_all(b"#!/bin/sh\necho test"); + drop(file); + + // Test normalization + let result = normalize_java_path(temp_file.to_str().unwrap()); + + // Clean up + let _ = fs::remove_file(&temp_file); + + // Verify result + assert!(result.is_ok(), "Failed to normalize temp file path"); + let normalized = result.unwrap(); + assert!(normalized.is_absolute()); + } + } + + #[test] + fn test_strip_unc_prefix() { + #[cfg(target_os = "windows")] + { + let unc_path = PathBuf::from(r"\\?\C:\Windows\System32\cmd.exe"); + let stripped = strip_unc_prefix(unc_path); + assert_eq!(stripped.to_string_lossy(), r"C:\Windows\System32\cmd.exe"); + + let normal_path = PathBuf::from(r"C:\Windows\System32\cmd.exe"); + let unchanged = strip_unc_prefix(normal_path.clone()); + assert_eq!(unchanged, normal_path); + } + + #[cfg(not(target_os = "windows"))] + { + let path = PathBuf::from("/usr/bin/java"); + let unchanged = strip_unc_prefix(path.clone()); + assert_eq!(unchanged, path); } } } -- cgit v1.2.3-70-g09d2