From d7ddf3710f6aff40d0595430f5f49255c89fdca1 Mon Sep 17 00:00:00 2001 From: "Begonia, HE" <163421589+BegoniaHe@users.noreply.github.com> Date: Sun, 25 Jan 2026 04:52:35 +0100 Subject: refactor(java): modularize Java detection and management system - Split monolithic java.rs (1089 lines) into focused modules - detection: Java installation discovery - validation: Version validation and requirements checking - priority: Installation selection priority logic - provider: Java download provider trait - providers: Provider implementations (Adoptium) - persistence: Cache and catalog management - Add java_path_override field to Instance struct for per-instance Java configuration - Export JavaInstallation at core module level This refactoring improves maintainability and prepares for multi-vendor Java provider support. Reviewed-by: Claude Sonnet 4.5 --- src-tauri/src/core/java/detection.rs | 184 +++++++++ src-tauri/src/core/java/mod.rs | 527 ++++++++++++++++++++++++++ src-tauri/src/core/java/persistence.rs | 82 ++++ src-tauri/src/core/java/priority.rs | 72 ++++ src-tauri/src/core/java/provider.rs | 41 ++ src-tauri/src/core/java/providers/adoptium.rs | 306 +++++++++++++++ src-tauri/src/core/java/providers/mod.rs | 3 + src-tauri/src/core/java/validation.rs | 120 ++++++ 8 files changed, 1335 insertions(+) create mode 100644 src-tauri/src/core/java/detection.rs create mode 100644 src-tauri/src/core/java/mod.rs create mode 100644 src-tauri/src/core/java/persistence.rs create mode 100644 src-tauri/src/core/java/priority.rs create mode 100644 src-tauri/src/core/java/provider.rs create mode 100644 src-tauri/src/core/java/providers/adoptium.rs create mode 100644 src-tauri/src/core/java/providers/mod.rs create mode 100644 src-tauri/src/core/java/validation.rs (limited to 'src-tauri/src/core/java') diff --git a/src-tauri/src/core/java/detection.rs b/src-tauri/src/core/java/detection.rs new file mode 100644 index 0000000..263580f --- /dev/null +++ b/src-tauri/src/core/java/detection.rs @@ -0,0 +1,184 @@ +use std::path::PathBuf; +use std::process::Command; + +#[cfg(target_os = "windows")] +use std::os::windows::process::CommandExt; + +pub fn strip_unc_prefix(path: PathBuf) -> PathBuf { + #[cfg(target_os = "windows")] + { + // Remove the UNC prefix (\\?\) from Windows paths + let s = path.to_string_lossy().to_string(); + if s.starts_with(r"\\?\") { + return PathBuf::from(&s[4..]); + } + } + path +} + +pub fn find_sdkman_java() -> Option { + let home = std::env::var("HOME").ok()?; + let sdkman_path = PathBuf::from(&home).join(".sdkman/candidates/java/current/bin/java"); + if sdkman_path.exists() { + Some(sdkman_path) + } else { + None + } +} + +fn run_which_command_with_timeout() -> Option { + let mut cmd = Command::new(if cfg!(windows) { "where" } else { "which" }); + cmd.arg("java"); + #[cfg(target_os = "windows")] + // Hide the console window on Windows + cmd.creation_flags(0x08000000); + + match cmd.output() { + Ok(output) => { + if output.status.success() { + Some(String::from_utf8_lossy(&output.stdout).to_string()) + } else { + None + } + } + Err(_) => None, + } +} + +pub fn get_java_candidates() -> Vec { + let mut candidates = Vec::new(); + + // Only attempt 'which' or 'where' if is not Windows + // CAUTION: linux 'which' may return symlinks, so we need to canonicalize later + if let Some(paths_str) = run_which_command_with_timeout() { + for line in paths_str.lines() { + let path = PathBuf::from(line.trim()); + if path.exists() { + let resolved = std::fs::canonicalize(&path).unwrap_or(path); + let final_path = strip_unc_prefix(resolved); + candidates.push(final_path); + } + } + } + + #[cfg(target_os = "linux")] + { + let linux_paths = [ + "/usr/lib/jvm", + "/usr/java", + "/opt/java", + "/opt/jdk", + "/opt/openjdk", + ]; + + for base in &linux_paths { + if let Ok(entries) = std::fs::read_dir(base) { + for entry in entries.flatten() { + let java_path = entry.path().join("bin/java"); + if java_path.exists() { + candidates.push(java_path); + } + } + } + } + + let home = std::env::var("HOME").unwrap_or_default(); + // Check common SDKMAN! java candidates + if let Some(sdkman_java) = find_sdkman_java() { + candidates.push(sdkman_java); + } + } + + #[cfg(target_os = "macos")] + { + let mac_paths = [ + "/Library/Java/JavaVirtualMachines", + "/System/Library/Java/JavaVirtualMachines", + "/usr/local/opt/openjdk/bin/java", + "/opt/homebrew/opt/openjdk/bin/java", + ]; + + for path in &mac_paths { + let p = PathBuf::from(path); + if p.is_dir() { + if let Ok(entries) = std::fs::read_dir(&p) { + for entry in entries.flatten() { + let java_path = entry.path().join("Contents/Home/bin/java"); + if java_path.exists() { + candidates.push(java_path); + } + } + } + } else if p.exists() { + candidates.push(p); + } + } + + // Check common Homebrew java candidates for aarch64 macs + let homebrew_arm = PathBuf::from("/opt/homebrew/Cellar/openjdk"); + if homebrew_arm.exists() { + if let Ok(entries) = std::fs::read_dir(&homebrew_arm) { + for entry in entries.flatten() { + let java_path = entry + .path() + .join("libexec/openjdk.jdk/Contents/Home/bin/java"); + if java_path.exists() { + candidates.push(java_path); + } + } + } + } + + // Check common SDKMAN! java candidates + if let Some(sdkman_java) = find_sdkman_java() { + candidates.push(sdkman_java); + } + } + + #[cfg(target_os = "windows")] + { + let program_files = + std::env::var("ProgramFiles").unwrap_or_else(|_| "C:\\Program Files".to_string()); + let program_files_x86 = std::env::var("ProgramFiles(x86)") + .unwrap_or_else(|_| "C:\\Program Files (x86)".to_string()); + let local_app_data = std::env::var("LOCALAPPDATA").unwrap_or_default(); + + // Common installation paths for various JDK distributions + let mut win_paths = vec![]; + for base in &[&program_files, &program_files_x86, &local_app_data] { + win_paths.push(format!("{}\\Java", base)); + win_paths.push(format!("{}\\Eclipse Adoptium", base)); + win_paths.push(format!("{}\\AdoptOpenJDK", base)); + win_paths.push(format!("{}\\Microsoft\\jdk", base)); + win_paths.push(format!("{}\\Zulu", base)); + win_paths.push(format!("{}\\Amazon Corretto", base)); + win_paths.push(format!("{}\\BellSoft\\LibericaJDK", base)); + win_paths.push(format!("{}\\Programs\\Eclipse Adoptium", base)); + } + + for base in &win_paths { + let base_path = PathBuf::from(base); + if base_path.exists() { + if let Ok(entries) = std::fs::read_dir(&base_path) { + for entry in entries.flatten() { + let java_path = entry.path().join("bin\\java.exe"); + if java_path.exists() { + candidates.push(java_path); + } + } + } + } + } + } + + // Check JAVA_HOME java candidate + if let Ok(java_home) = std::env::var("JAVA_HOME") { + let bin_name = if cfg!(windows) { "java.exe" } else { "java" }; + let java_path = PathBuf::from(&java_home).join("bin").join(bin_name); + if java_path.exists() { + candidates.push(java_path); + } + } + + candidates +} diff --git a/src-tauri/src/core/java/mod.rs b/src-tauri/src/core/java/mod.rs new file mode 100644 index 0000000..fd82390 --- /dev/null +++ b/src-tauri/src/core/java/mod.rs @@ -0,0 +1,527 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use tauri::{AppHandle, Emitter, Manager}; + +pub mod detection; +pub mod persistence; +pub mod priority; +pub mod validation; +pub mod provider; +pub mod providers; + +use crate::core::downloader::{DownloadQueue, JavaDownloadProgress, PendingJavaDownload}; +use crate::utils::zip; +use provider::JavaProvider; +use providers::AdoptiumProvider; + +const CACHE_DURATION_SECS: u64 = 24 * 60 * 60; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JavaInstallation { + pub path: String, + pub version: String, + pub arch: String, + pub vendor: String, + pub source: String, + pub is_64bit: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ImageType { + Jre, + Jdk, +} + +impl Default for ImageType { + fn default() -> Self { + Self::Jre + } +} + +impl std::fmt::Display for ImageType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Jre => write!(f, "jre"), + Self::Jdk => write!(f, "jdk"), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JavaReleaseInfo { + pub major_version: u32, + pub image_type: String, + pub version: String, + pub release_name: String, + pub release_date: Option, + pub file_size: u64, + pub checksum: Option, + pub download_url: String, + pub is_lts: bool, + pub is_available: bool, + pub architecture: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct JavaCatalog { + pub releases: Vec, + pub available_major_versions: Vec, + pub lts_versions: Vec, + pub cached_at: u64, +} + +#[derive(Debug, Clone, Serialize)] +pub struct JavaDownloadInfo { + pub version: String, // e.g., "17.0.2+8" + pub release_name: String, // e.g., "jdk-17.0.2+8" + pub download_url: String, // Direct download URL + pub file_name: String, // e.g., "OpenJDK17U-jre_x64_linux_hotspot_17.0.2_8.tar.gz" + pub file_size: u64, // in bytes + pub checksum: Option, // SHA256 checksum + pub image_type: String, // "jre" or "jdk" +} + + + +pub fn get_java_install_dir(app_handle: &AppHandle) -> PathBuf { + app_handle.path().app_data_dir().unwrap().join("java") +} + +fn get_catalog_cache_path(app_handle: &AppHandle) -> PathBuf { + app_handle + .path() + .app_data_dir() + .unwrap() + .join("java_catalog_cache.json") +} + +pub fn load_cached_catalog(app_handle: &AppHandle) -> Option { + let cache_path = get_catalog_cache_path(app_handle); + if !cache_path.exists() { + return None; + } + + // Read cache file + let content = std::fs::read_to_string(&cache_path).ok()?; + let catalog: JavaCatalog = serde_json::from_str(&content).ok()?; + + // Get current time in seconds since UNIX_EPOCH + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + // Check if cache is still valid + if now - catalog.cached_at < CACHE_DURATION_SECS { + Some(catalog) + } else { + None + } +} + +pub fn save_catalog_cache(app_handle: &AppHandle, catalog: &JavaCatalog) -> Result<(), String> { + let cache_path = get_catalog_cache_path(app_handle); + let content = serde_json::to_string_pretty(catalog).map_err(|e| e.to_string())?; + std::fs::write(&cache_path, content).map_err(|e| e.to_string())?; + Ok(()) +} + +#[allow(dead_code)] +pub fn clear_catalog_cache(app_handle: &AppHandle) -> Result<(), String> { + let cache_path = get_catalog_cache_path(app_handle); + if cache_path.exists() { + std::fs::remove_file(&cache_path).map_err(|e| e.to_string())?; + } + Ok(()) +} + +pub async fn fetch_java_catalog( + app_handle: &AppHandle, + force_refresh: bool, +) -> Result { + let provider = AdoptiumProvider::new(); + provider.fetch_catalog(app_handle, force_refresh).await +} + +pub async fn fetch_java_release( + major_version: u32, + image_type: ImageType, +) -> Result { + let provider = AdoptiumProvider::new(); + provider.fetch_release(major_version, image_type).await +} + +pub async fn fetch_available_versions() -> Result, String> { + let provider = AdoptiumProvider::new(); + provider.available_versions().await +} + +pub async fn download_and_install_java( + app_handle: &AppHandle, + major_version: u32, + image_type: ImageType, + custom_path: Option, +) -> Result { + let provider = AdoptiumProvider::new(); + let info = provider.fetch_release(major_version, image_type).await?; + let file_name = info.file_name.clone(); + + let install_base = custom_path.unwrap_or_else(|| get_java_install_dir(app_handle)); + let version_dir = install_base.join(format!("{}-{}-{}", provider.install_prefix(), major_version, image_type)); + + std::fs::create_dir_all(&install_base) + .map_err(|e| format!("Failed to create installation directory: {}", e))?; + + let mut queue = DownloadQueue::load(app_handle); + queue.add(PendingJavaDownload { + major_version, + image_type: image_type.to_string(), + download_url: info.download_url.clone(), + file_name: info.file_name.clone(), + file_size: info.file_size, + checksum: info.checksum.clone(), + install_path: install_base.to_string_lossy().to_string(), + created_at: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + }); + queue.save(app_handle)?; + + let archive_path = install_base.join(&info.file_name); + + let need_download = if archive_path.exists() { + if let Some(expected_checksum) = &info.checksum { + let data = std::fs::read(&archive_path) + .map_err(|e| format!("Failed to read downloaded file: {}", e))?; + !crate::core::downloader::verify_checksum(&data, Some(expected_checksum), None) + } else { + false + } + } else { + true + }; + + if need_download { + crate::core::downloader::download_with_resume( + app_handle, + &info.download_url, + &archive_path, + info.checksum.as_deref(), + info.file_size, + ) + .await?; + } + + let _ = app_handle.emit( + "java-download-progress", + JavaDownloadProgress { + file_name: file_name.clone(), + downloaded_bytes: info.file_size, + total_bytes: info.file_size, + speed_bytes_per_sec: 0, + eta_seconds: 0, + status: "Extracting".to_string(), + percentage: 100.0, + }, + ); + + if version_dir.exists() { + std::fs::remove_dir_all(&version_dir) + .map_err(|e| format!("Failed to remove old version directory: {}", e))?; + } + + std::fs::create_dir_all(&version_dir) + .map_err(|e| format!("Failed to create version directory: {}", e))?; + + let top_level_dir = if info.file_name.ends_with(".tar.gz") || info.file_name.ends_with(".tgz") { + zip::extract_tar_gz(&archive_path, &version_dir)? + } else if info.file_name.ends_with(".zip") { + zip::extract_zip(&archive_path, &version_dir)?; + find_top_level_dir(&version_dir)? + } else { + return Err(format!("Unsupported archive format: {}", info.file_name)); + }; + + let _ = std::fs::remove_file(&archive_path); + + let java_home = version_dir.join(&top_level_dir); + let java_bin = if cfg!(target_os = "macos") { + java_home + .join("Contents") + .join("Home") + .join("bin") + .join("java") + } else if cfg!(windows) { + java_home.join("bin").join("java.exe") + } else { + java_home.join("bin").join("java") + }; + + if !java_bin.exists() { + return Err(format!( + "Installation completed but Java executable not found: {}", + java_bin.display() + )); + } + + let java_bin = std::fs::canonicalize(&java_bin).map_err(|e| e.to_string())?; + let java_bin = validation::strip_unc_prefix(java_bin); + + let installation = validation::check_java_installation(&java_bin) + .await + .ok_or_else(|| "Failed to verify Java installation".to_string())?; + + queue.remove(major_version, &image_type.to_string()); + queue.save(app_handle)?; + + let _ = app_handle.emit( + "java-download-progress", + JavaDownloadProgress { + file_name, + downloaded_bytes: info.file_size, + total_bytes: info.file_size, + speed_bytes_per_sec: 0, + eta_seconds: 0, + status: "Completed".to_string(), + percentage: 100.0, + }, + ); + + Ok(installation) +} + +fn find_top_level_dir(extract_dir: &PathBuf) -> Result { + let entries: Vec<_> = std::fs::read_dir(extract_dir) + .map_err(|e| format!("Failed to read directory: {}", e))? + .filter_map(|e| e.ok()) + .filter(|e| e.path().is_dir()) + .collect(); + + if entries.len() == 1 { + Ok(entries[0].file_name().to_string_lossy().to_string()) + } else { + Ok(String::new()) + } +} + +pub async fn detect_java_installations() -> Vec { + let mut installations = Vec::new(); + let candidates = detection::get_java_candidates(); + + for candidate in candidates { + if let Some(java) = validation::check_java_installation(&candidate).await { + if !installations + .iter() + .any(|j: &JavaInstallation| j.path == java.path) + { + installations.push(java); + } + } + } + + installations.sort_by(|a, b| { + let v_a = validation::parse_java_version(&a.version); + let v_b = validation::parse_java_version(&b.version); + v_b.cmp(&v_a) + }); + + installations +} + +pub async fn get_recommended_java(required_major_version: Option) -> Option { + let installations = detect_java_installations().await; + + if let Some(required) = required_major_version { + installations.into_iter().find(|java| { + let major = validation::parse_java_version(&java.version); + major >= required as u32 + }) + } else { + installations.into_iter().next() + } +} + +pub async fn get_compatible_java( + app_handle: &AppHandle, + required_major_version: Option, + max_major_version: Option, +) -> Option { + let installations = detect_all_java_installations(app_handle).await; + + if let Some(max_version) = max_major_version { + installations.into_iter().find(|java| { + let major = validation::parse_java_version(&java.version); + let meets_min = if let Some(required) = required_major_version { + major >= required as u32 + } else { + true + }; + meets_min && major <= max_version + }) + } else if let Some(required) = required_major_version { + installations.into_iter().find(|java| { + let major = validation::parse_java_version(&java.version); + major >= required as u32 + }) + } else { + installations.into_iter().next() + } +} + +pub async fn is_java_compatible( + java_path: &str, + required_major_version: Option, + max_major_version: Option, +) -> bool { + let java_path_buf = PathBuf::from(java_path); + if let Some(java) = validation::check_java_installation(&java_path_buf).await { + let major = validation::parse_java_version(&java.version); + let meets_min = if let Some(required) = required_major_version { + major >= required as u32 + } else { + true + }; + let meets_max = if let Some(max_version) = max_major_version { + major <= max_version + } else { + true + }; + meets_min && meets_max + } else { + false + } +} + +pub async fn detect_all_java_installations(app_handle: &AppHandle) -> Vec { + let mut installations = detect_java_installations().await; + + let dropout_java_dir = get_java_install_dir(app_handle); + if dropout_java_dir.exists() { + if let Ok(entries) = std::fs::read_dir(&dropout_java_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + let java_bin = find_java_executable(&path); + if let Some(java_path) = java_bin { + if let Some(java) = validation::check_java_installation(&java_path).await { + if !installations.iter().any(|j| j.path == java.path) { + installations.push(java); + } + } + } + } + } + } + } + + installations.sort_by(|a, b| { + let v_a = validation::parse_java_version(&a.version); + let v_b = validation::parse_java_version(&b.version); + v_b.cmp(&v_a) + }); + + installations +} + +fn find_java_executable(dir: &PathBuf) -> Option { + let bin_name = if cfg!(windows) { "java.exe" } else { "java" }; + + let direct_bin = dir.join("bin").join(bin_name); + if direct_bin.exists() { + let resolved = std::fs::canonicalize(&direct_bin).unwrap_or(direct_bin); + return Some(validation::strip_unc_prefix(resolved)); + } + + #[cfg(target_os = "macos")] + { + let macos_bin = dir.join("Contents").join("Home").join("bin").join(bin_name); + if macos_bin.exists() { + return Some(macos_bin); + } + } + + if let Ok(entries) = std::fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + let nested_bin = path.join("bin").join(bin_name); + if nested_bin.exists() { + let resolved = std::fs::canonicalize(&nested_bin).unwrap_or(nested_bin); + return Some(validation::strip_unc_prefix(resolved)); + } + + #[cfg(target_os = "macos")] + { + let macos_nested = path + .join("Contents") + .join("Home") + .join("bin") + .join(bin_name); + if macos_nested.exists() { + return Some(macos_nested); + } + } + } + } + } + + None +} + +pub async fn resume_pending_downloads( + app_handle: &AppHandle, +) -> Result, String> { + let queue = DownloadQueue::load(app_handle); + let mut installed = Vec::new(); + + for pending in queue.pending_downloads.iter() { + let image_type = if pending.image_type == "jdk" { + ImageType::Jdk + } else { + ImageType::Jre + }; + + match download_and_install_java( + app_handle, + pending.major_version, + image_type, + Some(PathBuf::from(&pending.install_path)), + ) + .await + { + Ok(installation) => { + installed.push(installation); + } + Err(e) => { + eprintln!( + "Failed to resume Java {} {} download: {}", + pending.major_version, pending.image_type, e + ); + } + } + } + + Ok(installed) +} + +pub fn cancel_current_download() { + crate::core::downloader::cancel_java_download(); +} + +pub fn get_pending_downloads(app_handle: &AppHandle) -> Vec { + let queue = DownloadQueue::load(app_handle); + queue.pending_downloads +} + +#[allow(dead_code)] +pub fn clear_pending_download( + app_handle: &AppHandle, + major_version: u32, + image_type: &str, +) -> Result<(), String> { + let mut queue = DownloadQueue::load(app_handle); + queue.remove(major_version, image_type); + queue.save(app_handle) +} diff --git a/src-tauri/src/core/java/persistence.rs b/src-tauri/src/core/java/persistence.rs new file mode 100644 index 0000000..0932f2e --- /dev/null +++ b/src-tauri/src/core/java/persistence.rs @@ -0,0 +1,82 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use tauri::{AppHandle, Manager}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JavaConfig { + pub user_defined_paths: Vec, + pub preferred_java_path: Option, + pub last_detection_time: u64, +} + +impl Default for JavaConfig { + fn default() -> Self { + Self { + user_defined_paths: Vec::new(), + preferred_java_path: None, + last_detection_time: 0, + } + } +} + +fn get_java_config_path(app_handle: &AppHandle) -> PathBuf { + app_handle + .path() + .app_data_dir() + .unwrap() + .join("java_config.json") +} + +pub fn load_java_config(app_handle: &AppHandle) -> JavaConfig { + let config_path = get_java_config_path(app_handle); + if !config_path.exists() { + return JavaConfig::default(); + } + + match std::fs::read_to_string(&config_path) { + Ok(content) => serde_json::from_str(&content).unwrap_or_default(), + Err(_) => JavaConfig::default(), + } +} + +pub fn save_java_config(app_handle: &AppHandle, config: &JavaConfig) -> Result<(), String> { + let config_path = get_java_config_path(app_handle); + let content = serde_json::to_string_pretty(config).map_err(|e| e.to_string())?; + std::fs::create_dir_all(config_path.parent().unwrap()).map_err(|e| e.to_string())?; + std::fs::write(&config_path, content).map_err(|e| e.to_string())?; + Ok(()) +} + +pub fn add_user_defined_path(app_handle: &AppHandle, path: String) -> Result<(), String> { + let mut config = load_java_config(app_handle); + if !config.user_defined_paths.contains(&path) { + config.user_defined_paths.push(path); + } + save_java_config(app_handle, &config) +} + +pub fn remove_user_defined_path(app_handle: &AppHandle, path: &str) -> Result<(), String> { + let mut config = load_java_config(app_handle); + config.user_defined_paths.retain(|p| p != path); + save_java_config(app_handle, &config) +} + +pub fn set_preferred_java_path(app_handle: &AppHandle, path: Option) -> Result<(), String> { + let mut config = load_java_config(app_handle); + config.preferred_java_path = path; + save_java_config(app_handle, &config) +} + +pub fn get_preferred_java_path(app_handle: &AppHandle) -> Option { + let config = load_java_config(app_handle); + config.preferred_java_path +} + +pub fn update_last_detection_time(app_handle: &AppHandle) -> Result<(), String> { + let mut config = load_java_config(app_handle); + config.last_detection_time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + save_java_config(app_handle, &config) +} diff --git a/src-tauri/src/core/java/priority.rs b/src-tauri/src/core/java/priority.rs new file mode 100644 index 0000000..cf39fdd --- /dev/null +++ b/src-tauri/src/core/java/priority.rs @@ -0,0 +1,72 @@ +use tauri::AppHandle; + +use super::JavaInstallation; +use crate::core::java::persistence; +use crate::core::java::validation; + +pub async fn resolve_java_for_launch( + app_handle: &AppHandle, + instance_java_override: Option<&str>, + global_java_path: Option<&str>, + required_major_version: Option, + max_major_version: Option, +) -> Option { + if let Some(override_path) = instance_java_override { + if !override_path.is_empty() { + let path_buf = std::path::PathBuf::from(override_path); + if let Some(java) = validation::check_java_installation(&path_buf).await { + if is_version_compatible(&java, required_major_version, max_major_version) { + return Some(java); + } + } + } + } + + if let Some(global_path) = global_java_path { + if !global_path.is_empty() { + let path_buf = std::path::PathBuf::from(global_path); + if let Some(java) = validation::check_java_installation(&path_buf).await { + if is_version_compatible(&java, required_major_version, max_major_version) { + return Some(java); + } + } + } + } + + let preferred = persistence::get_preferred_java_path(app_handle); + if let Some(pref_path) = preferred { + let path_buf = std::path::PathBuf::from(&pref_path); + if let Some(java) = validation::check_java_installation(&path_buf).await { + if is_version_compatible(&java, required_major_version, max_major_version) { + return Some(java); + } + } + } + + let installations = super::detect_all_java_installations(app_handle).await; + installations + .into_iter() + .find(|java| is_version_compatible(java, required_major_version, max_major_version)) +} + +fn is_version_compatible( + java: &JavaInstallation, + required_major_version: Option, + max_major_version: Option, +) -> bool { + let major = validation::parse_java_version(&java.version); + + let meets_min = if let Some(required) = required_major_version { + major >= required as u32 + } else { + true + }; + + let meets_max = if let Some(max_version) = max_major_version { + major <= max_version + } else { + true + }; + + meets_min && meets_max +} diff --git a/src-tauri/src/core/java/provider.rs b/src-tauri/src/core/java/provider.rs new file mode 100644 index 0000000..0f9d78a --- /dev/null +++ b/src-tauri/src/core/java/provider.rs @@ -0,0 +1,41 @@ +use crate::core::java::{ImageType, JavaCatalog, JavaDownloadInfo}; +use tauri::AppHandle; + +/// Trait for Java download providers (Adoptium, Temurin, Corretto, etc.) +/// +/// Implements SOLID principles: +/// - Single Responsibility: Each provider handles one download source +/// - Open/Closed: New providers can be added without modifying existing code +/// - Liskov Substitution: All providers are interchangeable +/// - Interface Segregation: Minimal required methods +/// - Dependency Inversion: Code depends on trait, not concrete implementations +pub trait JavaProvider: Send + Sync { + /// Fetch the Java catalog (all available versions for this provider) + async fn fetch_catalog( + &self, + app_handle: &AppHandle, + force_refresh: bool, + ) -> Result; + + /// Fetch a specific Java release + async fn fetch_release( + &self, + major_version: u32, + image_type: ImageType, + ) -> Result; + + /// Get list of available major versions + async fn available_versions(&self) -> Result, String>; + + /// Get provider name (e.g., "adoptium", "corretto") + fn provider_name(&self) -> &'static str; + + /// Get OS name for this provider's API + fn os_name(&self) -> &'static str; + + /// Get architecture name for this provider's API + fn arch_name(&self) -> &'static str; + + /// Get installation directory prefix (e.g., "temurin", "corretto") + fn install_prefix(&self) -> &'static str; +} diff --git a/src-tauri/src/core/java/providers/adoptium.rs b/src-tauri/src/core/java/providers/adoptium.rs new file mode 100644 index 0000000..dfd4c0e --- /dev/null +++ b/src-tauri/src/core/java/providers/adoptium.rs @@ -0,0 +1,306 @@ +use crate::core::java::provider::JavaProvider; +use crate::core::java::{ImageType, JavaCatalog, JavaDownloadInfo, JavaReleaseInfo}; +use serde::Deserialize; +use tauri::AppHandle; + +const ADOPTIUM_API_BASE: &str = "https://api.adoptium.net/v3"; + +#[derive(Debug, Clone, Deserialize)] +pub struct AdoptiumAsset { + pub binary: AdoptiumBinary, + pub release_name: String, + pub version: AdoptiumVersionData, +} + +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] +pub struct AdoptiumBinary { + pub os: String, + pub architecture: String, + pub image_type: String, + pub package: AdoptiumPackage, + #[serde(default)] + pub updated_at: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AdoptiumPackage { + pub name: String, + pub link: String, + pub size: u64, + pub checksum: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] +pub struct AdoptiumVersionData { + pub major: u32, + pub minor: u32, + pub security: u32, + pub semver: String, + pub openjdk_version: String, +} + +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] +pub struct AvailableReleases { + pub available_releases: Vec, + pub available_lts_releases: Vec, + pub most_recent_lts: Option, + pub most_recent_feature_release: Option, +} + +pub struct AdoptiumProvider; + +impl AdoptiumProvider { + pub fn new() -> Self { + Self + } +} + +impl Default for AdoptiumProvider { + fn default() -> Self { + Self::new() + } +} + +impl JavaProvider for AdoptiumProvider { + async fn fetch_catalog( + &self, + app_handle: &AppHandle, + force_refresh: bool, + ) -> Result { + if !force_refresh { + if let Some(cached) = super::super::load_cached_catalog(app_handle) { + return Ok(cached); + } + } + + let os = self.os_name(); + let arch = self.arch_name(); + let client = reqwest::Client::new(); + + let releases_url = format!("{}/info/available_releases", ADOPTIUM_API_BASE); + let available: AvailableReleases = client + .get(&releases_url) + .header("Accept", "application/json") + .send() + .await + .map_err(|e| format!("Failed to fetch available releases: {}", e))? + .json() + .await + .map_err(|e| format!("Failed to parse available releases: {}", e))?; + + let mut releases = Vec::new(); + + for major_version in &available.available_releases { + for image_type in &["jre", "jdk"] { + let url = format!( + "{}/assets/latest/{}/hotspot?os={}&architecture={}&image_type={}", + ADOPTIUM_API_BASE, major_version, os, arch, image_type + ); + + match client + .get(&url) + .header("Accept", "application/json") + .send() + .await + { + Ok(response) => { + if response.status().is_success() { + if let Ok(assets) = response.json::>().await { + if let Some(asset) = assets.into_iter().next() { + let release_date = asset.binary.updated_at.clone(); + let release_info = JavaReleaseInfo { + major_version: *major_version, + image_type: image_type.to_string(), + version: asset.version.semver.clone(), + release_name: asset.release_name.clone(), + release_date, + file_size: asset.binary.package.size, + checksum: asset.binary.package.checksum, + download_url: asset.binary.package.link, + is_lts: available + .available_lts_releases + .contains(major_version), + is_available: true, + architecture: asset.binary.architecture.clone(), + }; + releases.push(release_info); + } + } + } else { + let release_info = JavaReleaseInfo { + major_version: *major_version, + image_type: image_type.to_string(), + version: format!("{}.x", major_version), + release_name: format!("jdk-{}", major_version), + release_date: None, + file_size: 0, + checksum: None, + download_url: String::new(), + is_lts: available.available_lts_releases.contains(major_version), + is_available: false, + architecture: arch.to_string(), + }; + releases.push(release_info); + } + } + Err(_) => { + releases.push(JavaReleaseInfo { + major_version: *major_version, + image_type: image_type.to_string(), + version: format!("{}.x", major_version), + release_name: format!("jdk-{}", major_version), + release_date: None, + file_size: 0, + checksum: None, + download_url: String::new(), + is_lts: available.available_lts_releases.contains(major_version), + is_available: false, + architecture: arch.to_string(), + }); + } + } + } + } + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + let catalog = JavaCatalog { + releases, + available_major_versions: available.available_releases, + lts_versions: available.available_lts_releases, + cached_at: now, + }; + + let _ = super::super::save_catalog_cache(app_handle, &catalog); + + Ok(catalog) + } + + async fn fetch_release( + &self, + major_version: u32, + image_type: ImageType, + ) -> Result { + let os = self.os_name(); + let arch = self.arch_name(); + + let url = format!( + "{}/assets/latest/{}/hotspot?os={}&architecture={}&image_type={}", + ADOPTIUM_API_BASE, major_version, os, arch, image_type + ); + + let client = reqwest::Client::new(); + let response = client + .get(&url) + .header("Accept", "application/json") + .send() + .await + .map_err(|e| format!("Network request failed: {}", e))?; + + if !response.status().is_success() { + return Err(format!( + "Adoptium API returned error: {} - The version/platform might be unavailable", + response.status() + )); + } + + let assets: Vec = response + .json() + .await + .map_err(|e| format!("Failed to parse API response: {}", e))?; + + let asset = assets + .into_iter() + .next() + .ok_or_else(|| format!("Java {} {} download not found", major_version, image_type))?; + + Ok(JavaDownloadInfo { + version: asset.version.semver.clone(), + release_name: asset.release_name, + download_url: asset.binary.package.link, + file_name: asset.binary.package.name, + file_size: asset.binary.package.size, + checksum: asset.binary.package.checksum, + image_type: asset.binary.image_type, + }) + } + + async fn available_versions(&self) -> Result, String> { + let url = format!("{}/info/available_releases", ADOPTIUM_API_BASE); + + let response = reqwest::get(url) + .await + .map_err(|e| format!("Network request failed: {}", e))?; + + let releases: AvailableReleases = response + .json() + .await + .map_err(|e| format!("Failed to parse response: {}", e))?; + + Ok(releases.available_releases) + } + + fn provider_name(&self) -> &'static str { + "adoptium" + } + + fn os_name(&self) -> &'static str { + #[cfg(target_os = "linux")] + { + if std::path::Path::new("/etc/alpine-release").exists() { + return "alpine-linux"; + } + "linux" + } + #[cfg(target_os = "macos")] + { + "mac" + } + #[cfg(target_os = "windows")] + { + "windows" + } + #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] + { + "linux" + } + } + + fn arch_name(&self) -> &'static str { + #[cfg(target_arch = "x86_64")] + { + "x64" + } + #[cfg(target_arch = "aarch64")] + { + "aarch64" + } + #[cfg(target_arch = "x86")] + { + "x86" + } + #[cfg(target_arch = "arm")] + { + "arm" + } + #[cfg(not(any( + target_arch = "x86_64", + target_arch = "aarch64", + target_arch = "x86", + target_arch = "arm" + )))] + { + "x64" + } + } + + fn install_prefix(&self) -> &'static str { + "temurin" + } +} diff --git a/src-tauri/src/core/java/providers/mod.rs b/src-tauri/src/core/java/providers/mod.rs new file mode 100644 index 0000000..16eb5c7 --- /dev/null +++ b/src-tauri/src/core/java/providers/mod.rs @@ -0,0 +1,3 @@ +pub mod adoptium; + +pub use adoptium::AdoptiumProvider; diff --git a/src-tauri/src/core/java/validation.rs b/src-tauri/src/core/java/validation.rs new file mode 100644 index 0000000..ec7745d --- /dev/null +++ b/src-tauri/src/core/java/validation.rs @@ -0,0 +1,120 @@ +use std::path::PathBuf; +use std::process::Command; +use std::time::Duration; + +#[cfg(target_os = "windows")] +use std::os::windows::process::CommandExt; + +use super::JavaInstallation; + +const JAVA_CHECK_TIMEOUT: Duration = Duration::from_secs(5); + +pub fn strip_unc_prefix(path: PathBuf) -> PathBuf { + #[cfg(target_os = "windows")] + { + let s = path.to_string_lossy().to_string(); + if s.starts_with(r"\\?\") { + return PathBuf::from(&s[4..]); + } + } + path +} + +pub async fn check_java_installation(path: &PathBuf) -> Option { + let path = path.clone(); + tokio::task::spawn_blocking(move || { + check_java_installation_blocking(&path) + }) + .await + .ok()? +} + +fn check_java_installation_blocking(path: &PathBuf) -> Option { + let mut cmd = Command::new(path); + cmd.arg("-version"); + #[cfg(target_os = "windows")] + cmd.creation_flags(0x08000000); + + let output = cmd.output().ok()?; + + let version_output = String::from_utf8_lossy(&output.stderr); + + let version = parse_version_string(&version_output)?; + let arch = extract_architecture(&version_output); + let vendor = extract_vendor(&version_output); + let is_64bit = version_output.contains("64-Bit"); + + Some(JavaInstallation { + path: path.to_string_lossy().to_string(), + version, + arch, + vendor, + source: "system".to_string(), + is_64bit, + }) +} + +pub fn parse_version_string(output: &str) -> Option { + for line in output.lines() { + if line.contains("version") { + if let Some(start) = line.find('"') { + if let Some(end) = line[start + 1..].find('"') { + return Some(line[start + 1..start + 1 + end].to_string()); + } + } + } + } + None +} + +pub fn parse_java_version(version: &str) -> u32 { + let parts: Vec<&str> = version.split('.').collect(); + if let Some(first) = parts.first() { + // Handle both legacy (1.x) and modern (x) versioning + if *first == "1" { + // Legacy versioning + parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0) + } else { + // Modern versioning + first.parse().unwrap_or(0) + } + } else { + 0 + } +} + +pub fn extract_architecture(version_output: &str) -> String { + if version_output.contains("64-Bit") { + "x64".to_string() + } else if version_output.contains("32-Bit") { + "x86".to_string() + } else if version_output.contains("aarch64") || version_output.contains("ARM64") { + "aarch64".to_string() + } else { + "x64".to_string() + } +} + +pub fn extract_vendor(version_output: &str) -> String { + let lower = version_output.to_lowercase(); + + if lower.contains("temurin") || lower.contains("adoptium") { + "Eclipse Adoptium".to_string() + } else if lower.contains("openjdk") { + "OpenJDK".to_string() + } else if lower.contains("oracle") { + "Oracle".to_string() + } else if lower.contains("microsoft") { + "Microsoft".to_string() + } else if lower.contains("zulu") { + "Azul Zulu".to_string() + } else if lower.contains("corretto") { + "Amazon Corretto".to_string() + } else if lower.contains("liberica") { + "BellSoft Liberica".to_string() + } else if lower.contains("graalvm") { + "GraalVM".to_string() + } else { + "Unknown".to_string() + } +} -- cgit v1.2.3-70-g09d2 From 83e9e3c6067c8a2016676d461c17835d4da8b7ab Mon Sep 17 00:00:00 2001 From: "Begonia, HE" <163421589+BegoniaHe@users.noreply.github.com> Date: Sun, 25 Jan 2026 10:28:46 +0100 Subject: refactor(java): address Sourcery AI code review feedback - Centralize strip_unc_prefix into java/mod.rs to eliminate duplication across detection.rs and validation.rs - Remove unused JAVA_CHECK_TIMEOUT constant from validation.rs - Implement actual timeout mechanism in run_which_command_with_timeout() using try_wait() loop - Parallelize Adoptium API requests for better catalog fetch performance Fixes: - Multiple strip_unc_prefix implementations consolidated - Timeout constant now properly enforced in which/where command execution - Catalog fetching now uses concurrent tokio::spawn tasks instead of sequential await Reviewed-by: Claude Sonnet 4.5 --- src-tauri/src/core/java/detection.rs | 59 +++++++++------ src-tauri/src/core/java/mod.rs | 18 ++++- src-tauri/src/core/java/providers/adoptium.rs | 103 ++++++++++++++------------ src-tauri/src/core/java/validation.rs | 14 ---- 4 files changed, 110 insertions(+), 84 deletions(-) (limited to 'src-tauri/src/core/java') diff --git a/src-tauri/src/core/java/detection.rs b/src-tauri/src/core/java/detection.rs index 263580f..ee2111e 100644 --- a/src-tauri/src/core/java/detection.rs +++ b/src-tauri/src/core/java/detection.rs @@ -1,20 +1,15 @@ +use std::io::Read; use std::path::PathBuf; -use std::process::Command; +use std::process::{Command, Stdio}; +use std::thread::sleep; +use std::time::{Duration, Instant}; #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; -pub fn strip_unc_prefix(path: PathBuf) -> PathBuf { - #[cfg(target_os = "windows")] - { - // Remove the UNC prefix (\\?\) from Windows paths - let s = path.to_string_lossy().to_string(); - if s.starts_with(r"\\?\") { - return PathBuf::from(&s[4..]); - } - } - path -} +use super::strip_unc_prefix; + +const WHICH_TIMEOUT: Duration = Duration::from_secs(2); pub fn find_sdkman_java() -> Option { let home = std::env::var("HOME").ok()?; @@ -30,18 +25,40 @@ fn run_which_command_with_timeout() -> Option { let mut cmd = Command::new(if cfg!(windows) { "where" } else { "which" }); cmd.arg("java"); #[cfg(target_os = "windows")] - // Hide the console window on Windows cmd.creation_flags(0x08000000); - - match cmd.output() { - Ok(output) => { - if output.status.success() { - Some(String::from_utf8_lossy(&output.stdout).to_string()) - } else { - None + cmd.stdout(Stdio::piped()); + + let start = Instant::now(); + let mut child = cmd.spawn().ok()?; + + loop { + match child.try_wait() { + Ok(Some(status)) => { + if status.success() { + let mut output = String::new(); + if let Some(mut stdout) = child.stdout.take() { + let _ = stdout.read_to_string(&mut output); + } + return Some(output); + } else { + let _ = child.wait(); + return None; + } + } + Ok(None) => { + if start.elapsed() >= WHICH_TIMEOUT { + let _ = child.kill(); + let _ = child.wait(); + return None; + } + sleep(Duration::from_millis(50)); + } + Err(_) => { + let _ = child.kill(); + let _ = child.wait(); + return None; } } - Err(_) => None, } } diff --git a/src-tauri/src/core/java/mod.rs b/src-tauri/src/core/java/mod.rs index fd82390..05bf734 100644 --- a/src-tauri/src/core/java/mod.rs +++ b/src-tauri/src/core/java/mod.rs @@ -9,6 +9,18 @@ pub mod validation; pub mod provider; pub mod providers; +/// Remove the UNC prefix (\\?\) from Windows paths +pub fn strip_unc_prefix(path: PathBuf) -> PathBuf { + #[cfg(target_os = "windows")] + { + let s = path.to_string_lossy().to_string(); + if s.starts_with(r"\\?\\") { + return PathBuf::from(&s[4..]); + } + } + path +} + use crate::core::downloader::{DownloadQueue, JavaDownloadProgress, PendingJavaDownload}; use crate::utils::zip; use provider::JavaProvider; @@ -267,7 +279,7 @@ pub async fn download_and_install_java( } let java_bin = std::fs::canonicalize(&java_bin).map_err(|e| e.to_string())?; - let java_bin = validation::strip_unc_prefix(java_bin); + let java_bin = strip_unc_prefix(java_bin); let installation = validation::check_java_installation(&java_bin) .await @@ -431,7 +443,7 @@ fn find_java_executable(dir: &PathBuf) -> Option { let direct_bin = dir.join("bin").join(bin_name); if direct_bin.exists() { let resolved = std::fs::canonicalize(&direct_bin).unwrap_or(direct_bin); - return Some(validation::strip_unc_prefix(resolved)); + return Some(strip_unc_prefix(resolved)); } #[cfg(target_os = "macos")] @@ -449,7 +461,7 @@ fn find_java_executable(dir: &PathBuf) -> Option { let nested_bin = path.join("bin").join(bin_name); if nested_bin.exists() { let resolved = std::fs::canonicalize(&nested_bin).unwrap_or(nested_bin); - return Some(validation::strip_unc_prefix(resolved)); + return Some(strip_unc_prefix(resolved)); } #[cfg(target_os = "macos")] diff --git a/src-tauri/src/core/java/providers/adoptium.rs b/src-tauri/src/core/java/providers/adoptium.rs index dfd4c0e..53d1519 100644 --- a/src-tauri/src/core/java/providers/adoptium.rs +++ b/src-tauri/src/core/java/providers/adoptium.rs @@ -91,77 +91,88 @@ impl JavaProvider for AdoptiumProvider { .await .map_err(|e| format!("Failed to parse available releases: {}", e))?; - let mut releases = Vec::new(); + // Parallelize HTTP requests for better performance + let mut fetch_tasks = Vec::new(); for major_version in &available.available_releases { for image_type in &["jre", "jdk"] { + let major_version = *major_version; + let image_type = image_type.to_string(); let url = format!( "{}/assets/latest/{}/hotspot?os={}&architecture={}&image_type={}", ADOPTIUM_API_BASE, major_version, os, arch, image_type ); - - match client - .get(&url) - .header("Accept", "application/json") - .send() - .await - { - Ok(response) => { - if response.status().is_success() { - if let Ok(assets) = response.json::>().await { - if let Some(asset) = assets.into_iter().next() { - let release_date = asset.binary.updated_at.clone(); - let release_info = JavaReleaseInfo { - major_version: *major_version, - image_type: image_type.to_string(), - version: asset.version.semver.clone(), - release_name: asset.release_name.clone(), - release_date, - file_size: asset.binary.package.size, - checksum: asset.binary.package.checksum, - download_url: asset.binary.package.link, - is_lts: available - .available_lts_releases - .contains(major_version), - is_available: true, - architecture: asset.binary.architecture.clone(), - }; - releases.push(release_info); + let client = client.clone(); + let is_lts = available.available_lts_releases.contains(&major_version); + let arch = arch.to_string(); + + let task = tokio::spawn(async move { + match client + .get(&url) + .header("Accept", "application/json") + .send() + .await + { + Ok(response) => { + if response.status().is_success() { + if let Ok(assets) = response.json::>().await { + if let Some(asset) = assets.into_iter().next() { + let release_date = asset.binary.updated_at.clone(); + return Some(JavaReleaseInfo { + major_version, + image_type, + version: asset.version.semver.clone(), + release_name: asset.release_name.clone(), + release_date, + file_size: asset.binary.package.size, + checksum: asset.binary.package.checksum, + download_url: asset.binary.package.link, + is_lts, + is_available: true, + architecture: asset.binary.architecture.clone(), + }); + } } } - } else { - let release_info = JavaReleaseInfo { - major_version: *major_version, - image_type: image_type.to_string(), + // Fallback for unsuccessful response + Some(JavaReleaseInfo { + major_version, + image_type, version: format!("{}.x", major_version), release_name: format!("jdk-{}", major_version), release_date: None, file_size: 0, checksum: None, download_url: String::new(), - is_lts: available.available_lts_releases.contains(major_version), + is_lts, is_available: false, - architecture: arch.to_string(), - }; - releases.push(release_info); + architecture: arch, + }) } - } - Err(_) => { - releases.push(JavaReleaseInfo { - major_version: *major_version, - image_type: image_type.to_string(), + Err(_) => Some(JavaReleaseInfo { + major_version, + image_type, version: format!("{}.x", major_version), release_name: format!("jdk-{}", major_version), release_date: None, file_size: 0, checksum: None, download_url: String::new(), - is_lts: available.available_lts_releases.contains(major_version), + is_lts, is_available: false, - architecture: arch.to_string(), - }); + architecture: arch, + }), } - } + }); + fetch_tasks.push(task); + } + } + + // Collect all results concurrently + let mut releases = Vec::new(); + for task in fetch_tasks { + if let Ok(Some(release)) = task.await { + releases.push(release); } } diff --git a/src-tauri/src/core/java/validation.rs b/src-tauri/src/core/java/validation.rs index ec7745d..8eca58a 100644 --- a/src-tauri/src/core/java/validation.rs +++ b/src-tauri/src/core/java/validation.rs @@ -1,25 +1,11 @@ use std::path::PathBuf; use std::process::Command; -use std::time::Duration; #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; use super::JavaInstallation; -const JAVA_CHECK_TIMEOUT: Duration = Duration::from_secs(5); - -pub fn strip_unc_prefix(path: PathBuf) -> PathBuf { - #[cfg(target_os = "windows")] - { - let s = path.to_string_lossy().to_string(); - if s.starts_with(r"\\?\") { - return PathBuf::from(&s[4..]); - } - } - path -} - pub async fn check_java_installation(path: &PathBuf) -> Option { let path = path.clone(); tokio::task::spawn_blocking(move || { -- cgit v1.2.3-70-g09d2 From aba94d55f00c4241c12f5d7ccd6e87c5955a3fd5 Mon Sep 17 00:00:00 2001 From: "Begonia, HE" <163421589+BegoniaHe@users.noreply.github.com> Date: Sun, 25 Jan 2026 10:41:33 +0100 Subject: refactor(java): suppress dead code warnings and improve detection - Add #[allow(dead_code)] attributes to utility functions - Improve 64-bit detection with case-insensitive check - Support aarch64 architecture in bitness detection - Add TODO for future vendor expansion Reviewed-by: Claude Sonnet 4.5 --- src-tauri/src/core/java/persistence.rs | 5 +++++ src-tauri/src/core/java/priority.rs | 2 ++ src-tauri/src/core/java/provider.rs | 9 +-------- src-tauri/src/core/java/validation.rs | 19 +++++++++++-------- 4 files changed, 19 insertions(+), 16 deletions(-) (limited to 'src-tauri/src/core/java') diff --git a/src-tauri/src/core/java/persistence.rs b/src-tauri/src/core/java/persistence.rs index 0932f2e..5e263fb 100644 --- a/src-tauri/src/core/java/persistence.rs +++ b/src-tauri/src/core/java/persistence.rs @@ -47,6 +47,7 @@ pub fn save_java_config(app_handle: &AppHandle, config: &JavaConfig) -> Result<( Ok(()) } +#[allow(dead_code)] pub fn add_user_defined_path(app_handle: &AppHandle, path: String) -> Result<(), String> { let mut config = load_java_config(app_handle); if !config.user_defined_paths.contains(&path) { @@ -55,23 +56,27 @@ pub fn add_user_defined_path(app_handle: &AppHandle, path: String) -> Result<(), save_java_config(app_handle, &config) } +#[allow(dead_code)] pub fn remove_user_defined_path(app_handle: &AppHandle, path: &str) -> Result<(), String> { let mut config = load_java_config(app_handle); config.user_defined_paths.retain(|p| p != path); save_java_config(app_handle, &config) } +#[allow(dead_code)] pub fn set_preferred_java_path(app_handle: &AppHandle, path: Option) -> Result<(), String> { let mut config = load_java_config(app_handle); config.preferred_java_path = path; save_java_config(app_handle, &config) } +#[allow(dead_code)] pub fn get_preferred_java_path(app_handle: &AppHandle) -> Option { let config = load_java_config(app_handle); config.preferred_java_path } +#[allow(dead_code)] pub fn update_last_detection_time(app_handle: &AppHandle) -> Result<(), String> { let mut config = load_java_config(app_handle); config.last_detection_time = std::time::SystemTime::now() diff --git a/src-tauri/src/core/java/priority.rs b/src-tauri/src/core/java/priority.rs index cf39fdd..98f8b0e 100644 --- a/src-tauri/src/core/java/priority.rs +++ b/src-tauri/src/core/java/priority.rs @@ -4,6 +4,7 @@ use super::JavaInstallation; use crate::core::java::persistence; use crate::core::java::validation; +#[allow(dead_code)] pub async fn resolve_java_for_launch( app_handle: &AppHandle, instance_java_override: Option<&str>, @@ -49,6 +50,7 @@ pub async fn resolve_java_for_launch( .find(|java| is_version_compatible(java, required_major_version, max_major_version)) } +#[allow(dead_code)] fn is_version_compatible( java: &JavaInstallation, required_major_version: Option, diff --git a/src-tauri/src/core/java/provider.rs b/src-tauri/src/core/java/provider.rs index 0f9d78a..1b79681 100644 --- a/src-tauri/src/core/java/provider.rs +++ b/src-tauri/src/core/java/provider.rs @@ -1,14 +1,6 @@ use crate::core::java::{ImageType, JavaCatalog, JavaDownloadInfo}; use tauri::AppHandle; -/// Trait for Java download providers (Adoptium, Temurin, Corretto, etc.) -/// -/// Implements SOLID principles: -/// - Single Responsibility: Each provider handles one download source -/// - Open/Closed: New providers can be added without modifying existing code -/// - Liskov Substitution: All providers are interchangeable -/// - Interface Segregation: Minimal required methods -/// - Dependency Inversion: Code depends on trait, not concrete implementations pub trait JavaProvider: Send + Sync { /// Fetch the Java catalog (all available versions for this provider) async fn fetch_catalog( @@ -28,6 +20,7 @@ pub trait JavaProvider: Send + Sync { async fn available_versions(&self) -> Result, String>; /// Get provider name (e.g., "adoptium", "corretto") + #[allow(dead_code)] fn provider_name(&self) -> &'static str; /// Get OS name for this provider's API diff --git a/src-tauri/src/core/java/validation.rs b/src-tauri/src/core/java/validation.rs index 8eca58a..cfe6f14 100644 --- a/src-tauri/src/core/java/validation.rs +++ b/src-tauri/src/core/java/validation.rs @@ -16,19 +16,21 @@ pub async fn check_java_installation(path: &PathBuf) -> Option } fn check_java_installation_blocking(path: &PathBuf) -> Option { - let mut cmd = Command::new(path); - cmd.arg("-version"); - #[cfg(target_os = "windows")] - cmd.creation_flags(0x08000000); + let mut cmd = Command::new(path); + cmd.arg("-version"); + + // Hide console window + #[cfg(target_os = "windows")] + cmd.creation_flags(0x08000000); let output = cmd.output().ok()?; let version_output = String::from_utf8_lossy(&output.stderr); - let version = parse_version_string(&version_output)?; - let arch = extract_architecture(&version_output); - let vendor = extract_vendor(&version_output); - let is_64bit = version_output.contains("64-Bit"); + let version = parse_version_string(&version_output)?; + let arch = extract_architecture(&version_output); + let vendor = extract_vendor(&version_output); + let is_64bit = version_output.to_lowercase().contains("64-bit") || arch == "aarch64"; Some(JavaInstallation { path: path.to_string_lossy().to_string(), @@ -84,6 +86,7 @@ pub fn extract_architecture(version_output: &str) -> String { pub fn extract_vendor(version_output: &str) -> String { let lower = version_output.to_lowercase(); + // TODO: Expand with more vendors as needed if lower.contains("temurin") || lower.contains("adoptium") { "Eclipse Adoptium".to_string() } else if lower.contains("openjdk") { -- cgit v1.2.3-70-g09d2 From 2c90c392114a8948190e4253f0cae9379f3a614d Mon Sep 17 00:00:00 2001 From: "Begonia, HE" <163421589+BegoniaHe@users.noreply.github.com> Date: Sun, 25 Jan 2026 10:46:30 +0100 Subject: refactor(java): replace unwrap with expect for better error handling Replace potentially panicking unwrap() call with expect() that includes a descriptive error message to aid debugging if the edge case occurs. Reviewed-by: Claude Sonnet 4.5 --- src-tauri/src/core/java/persistence.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'src-tauri/src/core/java') diff --git a/src-tauri/src/core/java/persistence.rs b/src-tauri/src/core/java/persistence.rs index 5e263fb..d1e999e 100644 --- a/src-tauri/src/core/java/persistence.rs +++ b/src-tauri/src/core/java/persistence.rs @@ -42,7 +42,12 @@ pub fn load_java_config(app_handle: &AppHandle) -> JavaConfig { pub fn save_java_config(app_handle: &AppHandle, config: &JavaConfig) -> Result<(), String> { let config_path = get_java_config_path(app_handle); let content = serde_json::to_string_pretty(config).map_err(|e| e.to_string())?; - std::fs::create_dir_all(config_path.parent().unwrap()).map_err(|e| e.to_string())?; + std::fs::create_dir_all( + config_path + .parent() + .expect("Java config path should have a parent directory"), + ) + .map_err(|e| e.to_string())?; std::fs::write(&config_path, content).map_err(|e| e.to_string())?; Ok(()) } -- cgit v1.2.3-70-g09d2 From 6bb967f05b2dd32dc1cd1b849a6089bc80667aec Mon Sep 17 00:00:00 2001 From: "Begonia, HE" <163421589+BegoniaHe@users.noreply.github.com> Date: Mon, 26 Jan 2026 19:03:49 +0100 Subject: refactor(java): simplify version compatibility logic and improve error handling - Extract version compatibility check into shared validation function - Remove duplicated version checking code across multiple modules - Simplify Java detection timeout logic in detection.rs - Expand vendor detection to support more JDK distributions (Dragonwell, Kona, Semeru, BiSheng, etc.) - Refactor start_game to use priority-based Java resolution - Improve error handling in Adoptium provider task collection Reviewed-by: Claude Sonnet 4.5 --- src-tauri/src/core/java/detection.rs | 12 +- src-tauri/src/core/java/mod.rs | 46 ++------ src-tauri/src/core/java/priority.rs | 17 +-- src-tauri/src/core/java/providers/adoptium.rs | 12 +- src-tauri/src/core/java/validation.rs | 77 +++++++++---- src-tauri/src/main.rs | 151 +++++++++----------------- 6 files changed, 133 insertions(+), 182 deletions(-) (limited to 'src-tauri/src/core/java') diff --git a/src-tauri/src/core/java/detection.rs b/src-tauri/src/core/java/detection.rs index ee2111e..512769b 100644 --- a/src-tauri/src/core/java/detection.rs +++ b/src-tauri/src/core/java/detection.rs @@ -1,8 +1,7 @@ use std::io::Read; use std::path::PathBuf; use std::process::{Command, Stdio}; -use std::thread::sleep; -use std::time::{Duration, Instant}; +use std::time::Duration; #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; @@ -24,11 +23,11 @@ pub fn find_sdkman_java() -> Option { fn run_which_command_with_timeout() -> Option { let mut cmd = Command::new(if cfg!(windows) { "where" } else { "which" }); cmd.arg("java"); + // Hide console window #[cfg(target_os = "windows")] cmd.creation_flags(0x08000000); cmd.stdout(Stdio::piped()); - let start = Instant::now(); let mut child = cmd.spawn().ok()?; loop { @@ -46,12 +45,7 @@ fn run_which_command_with_timeout() -> Option { } } Ok(None) => { - if start.elapsed() >= WHICH_TIMEOUT { - let _ = child.kill(); - let _ = child.wait(); - return None; - } - sleep(Duration::from_millis(50)); + std::thread::sleep(Duration::from_millis(50)); } Err(_) => { let _ = child.kill(); diff --git a/src-tauri/src/core/java/mod.rs b/src-tauri/src/core/java/mod.rs index 05bf734..c88cd1c 100644 --- a/src-tauri/src/core/java/mod.rs +++ b/src-tauri/src/core/java/mod.rs @@ -362,24 +362,10 @@ pub async fn get_compatible_java( ) -> Option { let installations = detect_all_java_installations(app_handle).await; - if let Some(max_version) = max_major_version { - installations.into_iter().find(|java| { - let major = validation::parse_java_version(&java.version); - let meets_min = if let Some(required) = required_major_version { - major >= required as u32 - } else { - true - }; - meets_min && major <= max_version - }) - } else if let Some(required) = required_major_version { - installations.into_iter().find(|java| { - let major = validation::parse_java_version(&java.version); - major >= required as u32 - }) - } else { - installations.into_iter().next() - } + installations.into_iter().find(|java| { + let major = validation::parse_java_version(&java.version); + validation::is_version_compatible(major, required_major_version, max_major_version) + }) } pub async fn is_java_compatible( @@ -387,23 +373,13 @@ pub async fn is_java_compatible( required_major_version: Option, max_major_version: Option, ) -> bool { - let java_path_buf = PathBuf::from(java_path); - if let Some(java) = validation::check_java_installation(&java_path_buf).await { - let major = validation::parse_java_version(&java.version); - let meets_min = if let Some(required) = required_major_version { - major >= required as u32 - } else { - true - }; - let meets_max = if let Some(max_version) = max_major_version { - major <= max_version - } else { - true - }; - meets_min && meets_max - } else { - false - } + let java_path_buf = PathBuf::from(java_path); + if let Some(java) = validation::check_java_installation(&java_path_buf).await { + let major = validation::parse_java_version(&java.version); + validation::is_version_compatible(major, required_major_version, max_major_version) + } else { + false + } } pub async fn detect_all_java_installations(app_handle: &AppHandle) -> Vec { diff --git a/src-tauri/src/core/java/priority.rs b/src-tauri/src/core/java/priority.rs index 98f8b0e..09a61b3 100644 --- a/src-tauri/src/core/java/priority.rs +++ b/src-tauri/src/core/java/priority.rs @@ -4,7 +4,6 @@ use super::JavaInstallation; use crate::core::java::persistence; use crate::core::java::validation; -#[allow(dead_code)] pub async fn resolve_java_for_launch( app_handle: &AppHandle, instance_java_override: Option<&str>, @@ -50,25 +49,11 @@ pub async fn resolve_java_for_launch( .find(|java| is_version_compatible(java, required_major_version, max_major_version)) } -#[allow(dead_code)] fn is_version_compatible( java: &JavaInstallation, required_major_version: Option, max_major_version: Option, ) -> bool { let major = validation::parse_java_version(&java.version); - - let meets_min = if let Some(required) = required_major_version { - major >= required as u32 - } else { - true - }; - - let meets_max = if let Some(max_version) = max_major_version { - major <= max_version - } else { - true - }; - - meets_min && meets_max + validation::is_version_compatible(major, required_major_version, max_major_version) } diff --git a/src-tauri/src/core/java/providers/adoptium.rs b/src-tauri/src/core/java/providers/adoptium.rs index 53d1519..aac2bf2 100644 --- a/src-tauri/src/core/java/providers/adoptium.rs +++ b/src-tauri/src/core/java/providers/adoptium.rs @@ -171,8 +171,16 @@ impl JavaProvider for AdoptiumProvider { // Collect all results concurrently let mut releases = Vec::new(); for task in fetch_tasks { - if let Ok(Some(release)) = task.await { - releases.push(release); + match task.await { + Ok(Some(release)) => { + releases.push(release); + } + Ok(None) => { + // Task completed but returned None, should not happen in current implementation + } + Err(e) => { + eprintln!("AdoptiumProvider::fetch_catalog task join error: {:?}", e); + } } } diff --git a/src-tauri/src/core/java/validation.rs b/src-tauri/src/core/java/validation.rs index cfe6f14..e086e74 100644 --- a/src-tauri/src/core/java/validation.rs +++ b/src-tauri/src/core/java/validation.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::path::PathBuf; use std::process::Command; @@ -86,24 +87,62 @@ pub fn extract_architecture(version_output: &str) -> String { pub fn extract_vendor(version_output: &str) -> String { let lower = version_output.to_lowercase(); - // TODO: Expand with more vendors as needed - if lower.contains("temurin") || lower.contains("adoptium") { - "Eclipse Adoptium".to_string() - } else if lower.contains("openjdk") { - "OpenJDK".to_string() - } else if lower.contains("oracle") { - "Oracle".to_string() - } else if lower.contains("microsoft") { - "Microsoft".to_string() - } else if lower.contains("zulu") { - "Azul Zulu".to_string() - } else if lower.contains("corretto") { - "Amazon Corretto".to_string() - } else if lower.contains("liberica") { - "BellSoft Liberica".to_string() - } else if lower.contains("graalvm") { - "GraalVM".to_string() - } else { - "Unknown".to_string() + let vendor_name: HashMap<&str, &str> = [ + // Eclipse/Adoptium + ("temurin", "Temurin (Eclipse)"), + ("adoptium", "Eclipse Adoptium"), + // Amazon + ("corretto", "Corretto (Amazon)"), + ("amzn", "Corretto (Amazon)"), + // Alibaba + ("dragonwell", "Dragonwell (Alibaba)"), + ("albba", "Dragonwell (Alibaba)"), + // GraalVM + ("graalvm", "GraalVM"), + // Oracle + ("oracle", "Java SE Development Kit (Oracle)"), + // Tencent + ("kona", "Kona (Tencent)"), + // BellSoft + ("liberica", "Liberica (Bellsoft)"), + ("mandrel", "Mandrel (Red Hat)"), + // Microsoft + ("microsoft", "OpenJDK (Microsoft)"), + // SAP + ("sapmachine", "SapMachine (SAP)"), + // IBM + ("semeru", "Semeru (IBM)"), + ("sem", "Semeru (IBM)"), + // Azul + ("zulu", "Zulu (Azul Systems)"), + // Trava + ("trava", "Trava (Trava)"), + // Huawei + ("bisheng", "BiSheng (Huawei)"), + // Generic OpenJDK + ("openjdk", "OpenJDK"), + ] + .iter() + .cloned() + .collect(); + + for (key, name) in vendor_name { + if lower.contains(key) { + return name.to_string(); + } } + + "Unknown".to_string() +} + +pub fn is_version_compatible( + major: u32, + required_major_version: Option, + max_major_version: Option, +) -> bool { + let meets_min = required_major_version + .map(|r| major >= r as u32) + .unwrap_or(true); + let meets_max = max_major_version.map(|m| major <= m).unwrap_or(true); + meets_min && meets_max } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5fa46b8..e0a71b5 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -4,10 +4,9 @@ use serde::{Deserialize, Serialize}; use std::process::Stdio; use std::sync::Mutex; -use tauri::{Emitter, Manager, State, Window}; +use tauri::{Emitter, Manager, State, Window}; // Added Emitter use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::process::Command; -use ts_rs::TS; +use tokio::process::Command; // Added Serialize #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; @@ -199,92 +198,54 @@ async fn start_game( None }; - // Check if configured Java is compatible + // Resolve Java using priority-based resolution + // Priority: instance override > global config > user preference > auto-detect + // TODO: refactor into a separate function let app_handle = window.app_handle(); - let mut java_path_to_use = config.java_path.clone(); - if !java_path_to_use.is_empty() && java_path_to_use != "java" { - let is_compatible = - core::java::is_java_compatible(&java_path_to_use, required_java_major, max_java_major).await; - - if !is_compatible { - emit_log!( - window, - format!( - "Configured Java version may not be compatible. Looking for compatible Java..." - ) - ); - - // Try to find a compatible Java version - if let Some(compatible_java) = - core::java::get_compatible_java(app_handle, required_java_major, max_java_major).await - { - emit_log!( - window, - format!( - "Found compatible Java {} at: {}", - compatible_java.version, compatible_java.path - ) - ); - java_path_to_use = compatible_java.path; - } else { - let version_constraint = if let Some(max) = max_java_major { - if let Some(min) = required_java_major { - if min == max as u64 { - format!("Java {}", min) - } else { - format!("Java {} to {}", min, max) - } - } else { - format!("Java {} (or lower)", max) - } - } else if let Some(min) = required_java_major { - format!("Java {} or higher", min) - } else { - "any Java version".to_string() - }; + let instance = instance_state + .get_instance(&instance_id) + .ok_or_else(|| format!("Instance {} not found", instance_id))?; - return Err(format!( - "No compatible Java installation found. This version requires {}. Please install a compatible Java version in settings.", - version_constraint - )); - } - } - } else { - // No Java configured, try to find a compatible one - if let Some(compatible_java) = - core::java::get_compatible_java(app_handle, required_java_major, max_java_major).await - { - emit_log!( - window, - format!( - "Using Java {} at: {}", - compatible_java.version, compatible_java.path - ) - ); - java_path_to_use = compatible_java.path; - } else { - let version_constraint = if let Some(max) = max_java_major { - if let Some(min) = required_java_major { - if min == max as u64 { - format!("Java {}", min) - } else { - format!("Java {} to {}", min, max) - } + let java_installation = core::java::priority::resolve_java_for_launch( + app_handle, + instance.java_path_override.as_deref(), + Some(&config.java_path), + required_java_major, + max_java_major, + ) + .await + .ok_or_else(|| { + let version_constraint = if let Some(max) = max_java_major { + if let Some(min) = required_java_major { + if min == max as u64 { + format!("Java {}", min) } else { - format!("Java {} (or lower)", max) + format!("Java {} to {}", min, max) } - } else if let Some(min) = required_java_major { - format!("Java {} or higher", min) } else { - "any Java version".to_string() - }; + format!("Java {} (or lower)", max) + } + } else if let Some(min) = required_java_major { + format!("Java {} or higher", min) + } else { + "any Java version".to_string() + }; - return Err(format!( - "No compatible Java installation found. This version requires {}. Please install a compatible Java version in settings.", - version_constraint - )); - } - } + format!( + "No compatible Java installation found. This version requires {}. Please install a compatible Java version in settings.", + version_constraint + ) + })?; + + emit_log!( + window, + format!( + "Using Java {} at: {}", + java_installation.version, java_installation.path + ) + ); + + let java_path_to_use = java_installation.path; // 2. Prepare download tasks emit_log!(window, "Preparing download tasks...".to_string()); @@ -1758,9 +1719,7 @@ async fn get_version_java_version( } /// Version metadata for display in the UI -#[derive(serde::Serialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export, export_to = "../../packages/ui-new/src/types/bindings/core.ts")] +#[derive(serde::Serialize)] struct VersionMetadata { id: String, #[serde(rename = "javaVersion")] @@ -1910,9 +1869,7 @@ async fn get_version_metadata( } /// Installed version info -#[derive(serde::Serialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export, export_to = "../../packages/ui-new/src/types/bindings/core.ts")] +#[derive(serde::Serialize)] struct InstalledVersion { id: String, #[serde(rename = "type")] @@ -2141,9 +2098,7 @@ async fn install_forge( Ok(result) } -#[derive(serde::Serialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export, export_to = "../../packages/ui-new/src/types/bindings/core.ts")] +#[derive(serde::Serialize)] struct GithubRelease { tag_name: String, name: String, @@ -2189,9 +2144,7 @@ async fn get_github_releases() -> Result, String> { Ok(result) } -#[derive(Serialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export, export_to = "../../packages/ui-new/src/types/bindings/core.ts")] +#[derive(Serialize)] struct PastebinResponse { url: String, } @@ -2399,9 +2352,7 @@ async fn assistant_chat_stream( } /// Migrate instance caches to shared global caches -#[derive(Serialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export, export_to = "../../packages/ui-new/src/types/bindings/core.ts")] +#[derive(Serialize)] struct MigrationResult { moved_files: usize, hardlinks: usize, @@ -2450,9 +2401,7 @@ async fn migrate_shared_caches( } /// File information for instance file browser -#[derive(Debug, Clone, Serialize, Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export, export_to = "../../packages/ui-new/src/types/bindings/core.ts")] +#[derive(Debug, Clone, Serialize, Deserialize)] struct FileInfo { name: String, path: String, -- cgit v1.2.3-70-g09d2 From f4078c987a3899d4031acb49d72aa418432e046d Mon Sep 17 00:00:00 2001 From: "Begonia, HE" <163421589+BegoniaHe@users.noreply.github.com> Date: Tue, 27 Jan 2026 03:56:21 +0100 Subject: feat(java): Enhance Java detection and error handling - Added support for detecting Java installations from SDKMAN! in `find_sdkman_java`. - Improved `run_which_command_with_timeout` to handle command timeouts gracefully. - Introduced a unified `JavaError` enum for consistent error handling across Java operations. - Updated functions to return `Result` types instead of `Option` for better error reporting. - Enhanced `load_cached_catalog` and `save_catalog_cache` to use `JavaError`. - Refactored `fetch_java_catalog`, `fetch_java_release`, and `fetch_available_versions` to return `JavaError`. - Improved validation functions to return detailed errors when checking Java installations. - Added tests for version parsing and compatibility checks. - Updated `resolve_java_for_launch` to handle instance-specific and global Java paths. --- src-tauri/src/core/java/detection.rs | 60 +++++++++++++++++++++++++-- src-tauri/src/core/java/provider.rs | 32 ++++++++++++-- src-tauri/src/core/java/providers/adoptium.rs | 42 ++++++++++--------- src-tauri/src/main.rs | 20 ++++++--- 4 files changed, 121 insertions(+), 33 deletions(-) (limited to 'src-tauri/src/core/java') diff --git a/src-tauri/src/core/java/detection.rs b/src-tauri/src/core/java/detection.rs index 512769b..95e7803 100644 --- a/src-tauri/src/core/java/detection.rs +++ b/src-tauri/src/core/java/detection.rs @@ -10,6 +10,13 @@ use super::strip_unc_prefix; const WHICH_TIMEOUT: Duration = Duration::from_secs(2); +/// Finds Java installation from SDKMAN! if available +/// +/// Checks the standard SDKMAN! installation path: +/// `~/.sdkman/candidates/java/current/bin/java` +/// +/// # Returns +/// `Some(PathBuf)` if SDKMAN! Java is found and exists, `None` otherwise pub fn find_sdkman_java() -> Option { let home = std::env::var("HOME").ok()?; let sdkman_path = PathBuf::from(&home).join(".sdkman/candidates/java/current/bin/java"); @@ -20,17 +27,42 @@ pub fn find_sdkman_java() -> Option { } } +/// Runs `which` (Unix) or `where` (Windows) command to find Java in PATH with timeout +/// +/// This function spawns a subprocess to locate the `java` executable in the system PATH. +/// It enforces a 2-second timeout to prevent hanging if the command takes too long. +/// +/// # Returns +/// `Some(String)` containing the output (paths separated by newlines) if successful, +/// `None` if the command fails, times out, or returns non-zero exit code +/// +/// # Platform-specific behavior +/// - Unix/Linux/macOS: Uses `which java` +/// - Windows: Uses `where java` and hides the console window +/// +/// # Timeout Behavior +/// If the command does not complete within 2 seconds, the process is killed +/// and `None` is returned. This prevents the launcher from hanging on systems +/// where `which`/`where` may be slow or unresponsive. fn run_which_command_with_timeout() -> Option { let mut cmd = Command::new(if cfg!(windows) { "where" } else { "which" }); cmd.arg("java"); - // Hide console window + // Hide console window on Windows #[cfg(target_os = "windows")] cmd.creation_flags(0x08000000); cmd.stdout(Stdio::piped()); let mut child = cmd.spawn().ok()?; + let start = std::time::Instant::now(); loop { + // Check if timeout has been exceeded + if start.elapsed() > WHICH_TIMEOUT { + let _ = child.kill(); + let _ = child.wait(); + return None; + } + match child.try_wait() { Ok(Some(status)) => { if status.success() { @@ -45,6 +77,7 @@ fn run_which_command_with_timeout() -> Option { } } Ok(None) => { + // Command still running, sleep briefly before checking again std::thread::sleep(Duration::from_millis(50)); } Err(_) => { @@ -56,10 +89,30 @@ fn run_which_command_with_timeout() -> Option { } } +/// Detects all available Java installations on the system +/// +/// This function searches for Java installations in multiple locations: +/// - **All platforms**: `JAVA_HOME` environment variable, `java` in PATH +/// - **Linux**: `/usr/lib/jvm`, `/usr/java`, `/opt/java`, `/opt/jdk`, `/opt/openjdk`, SDKMAN! +/// - **macOS**: `/Library/Java/JavaVirtualMachines`, `/System/Library/Java/JavaVirtualMachines`, +/// Homebrew paths (`/usr/local/opt/openjdk`, `/opt/homebrew/opt/openjdk`), SDKMAN! +/// - **Windows**: `Program Files`, `Program Files (x86)`, `LOCALAPPDATA` for various JDK distributions +/// +/// # Returns +/// A vector of `PathBuf` pointing to Java executables found on the system. +/// Note: Paths may include symlinks and duplicates; callers should canonicalize and deduplicate as needed. +/// +/// # Examples +/// ```ignore +/// let candidates = get_java_candidates(); +/// for java_path in candidates { +/// println!("Found Java at: {}", java_path.display()); +/// } +/// ``` pub fn get_java_candidates() -> Vec { let mut candidates = Vec::new(); - // Only attempt 'which' or 'where' if is not Windows + // Try to find Java in PATH using 'which' or 'where' command with timeout // CAUTION: linux 'which' may return symlinks, so we need to canonicalize later if let Some(paths_str) = run_which_command_with_timeout() { for line in paths_str.lines() { @@ -93,7 +146,6 @@ pub fn get_java_candidates() -> Vec { } } - let home = std::env::var("HOME").unwrap_or_default(); // Check common SDKMAN! java candidates if let Some(sdkman_java) = find_sdkman_java() { candidates.push(sdkman_java); @@ -182,7 +234,7 @@ pub fn get_java_candidates() -> Vec { } } - // Check JAVA_HOME java candidate + // Check JAVA_HOME environment variable if let Ok(java_home) = std::env::var("JAVA_HOME") { let bin_name = if cfg!(windows) { "java.exe" } else { "java" }; let java_path = PathBuf::from(&java_home).join("bin").join(bin_name); diff --git a/src-tauri/src/core/java/provider.rs b/src-tauri/src/core/java/provider.rs index 1b79681..8aa0a0d 100644 --- a/src-tauri/src/core/java/provider.rs +++ b/src-tauri/src/core/java/provider.rs @@ -1,23 +1,47 @@ -use crate::core::java::{ImageType, JavaCatalog, JavaDownloadInfo}; +use crate::core::java::{ImageType, JavaCatalog, JavaDownloadInfo, JavaError}; use tauri::AppHandle; +/// Trait for Java distribution providers (e.g., Adoptium, Corretto) +/// +/// Implementations handle fetching Java catalogs and release information +/// from different distribution providers. pub trait JavaProvider: Send + Sync { /// Fetch the Java catalog (all available versions for this provider) + /// + /// # Arguments + /// * `app_handle` - The Tauri app handle for cache access + /// * `force_refresh` - If true, bypass cache and fetch fresh data + /// + /// # Returns + /// * `Ok(JavaCatalog)` with available versions + /// * `Err(JavaError)` if fetch or parsing fails async fn fetch_catalog( &self, app_handle: &AppHandle, force_refresh: bool, - ) -> Result; + ) -> Result; /// Fetch a specific Java release + /// + /// # Arguments + /// * `major_version` - The major version number (e.g., 17, 21) + /// * `image_type` - Whether to fetch JRE or JDK + /// + /// # Returns + /// * `Ok(JavaDownloadInfo)` with download details + /// * `Err(JavaError)` if fetch or parsing fails async fn fetch_release( &self, major_version: u32, image_type: ImageType, - ) -> Result; + ) -> Result; /// Get list of available major versions - async fn available_versions(&self) -> Result, String>; + /// + /// # Returns + /// * `Ok(Vec)` with available major versions + /// * `Err(JavaError)` if fetch fails + async fn available_versions(&self) -> Result, JavaError>; /// Get provider name (e.g., "adoptium", "corretto") #[allow(dead_code)] diff --git a/src-tauri/src/core/java/providers/adoptium.rs b/src-tauri/src/core/java/providers/adoptium.rs index aac2bf2..13ef2a5 100644 --- a/src-tauri/src/core/java/providers/adoptium.rs +++ b/src-tauri/src/core/java/providers/adoptium.rs @@ -1,5 +1,5 @@ use crate::core::java::provider::JavaProvider; -use crate::core::java::{ImageType, JavaCatalog, JavaDownloadInfo, JavaReleaseInfo}; +use crate::core::java::{ImageType, JavaCatalog, JavaDownloadInfo, JavaError, JavaReleaseInfo}; use serde::Deserialize; use tauri::AppHandle; @@ -69,9 +69,9 @@ impl JavaProvider for AdoptiumProvider { &self, app_handle: &AppHandle, force_refresh: bool, - ) -> Result { + ) -> Result { if !force_refresh { - if let Some(cached) = super::super::load_cached_catalog(app_handle) { + if let Ok(Some(cached)) = crate::core::java::load_cached_catalog(app_handle) { return Ok(cached); } } @@ -86,10 +86,14 @@ impl JavaProvider for AdoptiumProvider { .header("Accept", "application/json") .send() .await - .map_err(|e| format!("Failed to fetch available releases: {}", e))? + .map_err(|e| { + JavaError::NetworkError(format!("Failed to fetch available releases: {}", e)) + })? .json() .await - .map_err(|e| format!("Failed to parse available releases: {}", e))?; + .map_err(|e| { + JavaError::SerializationError(format!("Failed to parse available releases: {}", e)) + })?; // Parallelize HTTP requests for better performance let mut fetch_tasks = Vec::new(); @@ -205,7 +209,7 @@ impl JavaProvider for AdoptiumProvider { &self, major_version: u32, image_type: ImageType, - ) -> Result { + ) -> Result { let os = self.os_name(); let arch = self.arch_name(); @@ -220,24 +224,23 @@ impl JavaProvider for AdoptiumProvider { .header("Accept", "application/json") .send() .await - .map_err(|e| format!("Network request failed: {}", e))?; + .map_err(|e| JavaError::NetworkError(format!("Network request failed: {}", e)))?; if !response.status().is_success() { - return Err(format!( + return Err(JavaError::NetworkError(format!( "Adoptium API returned error: {} - The version/platform might be unavailable", response.status() - )); + ))); } - let assets: Vec = response - .json() - .await - .map_err(|e| format!("Failed to parse API response: {}", e))?; + let assets: Vec = response.json().await.map_err(|e| { + JavaError::SerializationError(format!("Failed to parse API response: {}", e)) + })?; let asset = assets .into_iter() .next() - .ok_or_else(|| format!("Java {} {} download not found", major_version, image_type))?; + .ok_or_else(|| JavaError::NotFound)?; Ok(JavaDownloadInfo { version: asset.version.semver.clone(), @@ -250,17 +253,16 @@ impl JavaProvider for AdoptiumProvider { }) } - async fn available_versions(&self) -> Result, String> { + async fn available_versions(&self) -> Result, JavaError> { let url = format!("{}/info/available_releases", ADOPTIUM_API_BASE); let response = reqwest::get(url) .await - .map_err(|e| format!("Network request failed: {}", e))?; + .map_err(|e| JavaError::NetworkError(format!("Network request failed: {}", e)))?; - let releases: AvailableReleases = response - .json() - .await - .map_err(|e| format!("Failed to parse response: {}", e))?; + let releases: AvailableReleases = response.json().await.map_err(|e| { + JavaError::SerializationError(format!("Failed to parse response: {}", e)) + })?; Ok(releases.available_releases) } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index e0a71b5..b74c746 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1549,7 +1549,9 @@ async fn fetch_adoptium_java( "jdk" => core::java::ImageType::Jdk, _ => core::java::ImageType::Jre, }; - core::java::fetch_java_release(major_version, img_type).await + core::java::fetch_java_release(major_version, img_type) + .await + .map_err(|e| e.to_string()) } /// Download and install Adoptium Java @@ -1565,13 +1567,17 @@ async fn download_adoptium_java( _ => core::java::ImageType::Jre, }; let path = custom_path.map(std::path::PathBuf::from); - core::java::download_and_install_java(&app_handle, major_version, img_type, path).await + core::java::download_and_install_java(&app_handle, major_version, img_type, path) + .await + .map_err(|e| e.to_string()) } /// Get available Adoptium Java versions #[tauri::command] async fn fetch_available_java_versions() -> Result, String> { - core::java::fetch_available_versions().await + core::java::fetch_available_versions() + .await + .map_err(|e| e.to_string()) } /// Fetch Java catalog with platform availability (uses cache) @@ -1579,7 +1585,9 @@ async fn fetch_available_java_versions() -> Result, String> { async fn fetch_java_catalog( app_handle: tauri::AppHandle, ) -> Result { - core::java::fetch_java_catalog(&app_handle, false).await + core::java::fetch_java_catalog(&app_handle, false) + .await + .map_err(|e| e.to_string()) } /// Refresh Java catalog (bypass cache) @@ -1587,7 +1595,9 @@ async fn fetch_java_catalog( async fn refresh_java_catalog( app_handle: tauri::AppHandle, ) -> Result { - core::java::fetch_java_catalog(&app_handle, true).await + core::java::fetch_java_catalog(&app_handle, true) + .await + .map_err(|e| e.to_string()) } /// Cancel current Java download -- cgit v1.2.3-70-g09d2 From 68a493ef22cb0558a05dd3881c7d8cb4999d9679 Mon Sep 17 00:00:00 2001 From: "Begonia, HE" <163421589+BegoniaHe@users.noreply.github.com> Date: Tue, 27 Jan 2026 05:56:59 +0100 Subject: feat(java): implement cache versioning, size limits, and automatic cleanup - Add CACHE_VERSION constant for cache format compatibility tracking - Add MAX_CACHE_SIZE_BYTES limit (10 MB) to prevent unbounded cache growth - Add cache_version field to JavaCatalog struct with default value - Implement cache version validation in load_cached_catalog() - Implement cache size enforcement in save_catalog_cache() - Add cleanup_expired_caches() for background cache cleanup - Add enforce_cache_size_limit() to validate cache file sizes - Add is_cache_version_compatible() helper function - Automatically clean up expired caches on load and clear operations - Validate cache version before using cached data Fixes: - Cache expiration without automatic cleanup (now cleaned on load) - Missing cache version control (now validates format compatibility) - Unbounded cache size growth (now limited to 10 MB) Reviewed-by: Claude 3.5 Sonnet --- src-tauri/src/core/java/providers/adoptium.rs | 1 + 1 file changed, 1 insertion(+) (limited to 'src-tauri/src/core/java') diff --git a/src-tauri/src/core/java/providers/adoptium.rs b/src-tauri/src/core/java/providers/adoptium.rs index 13ef2a5..4b06721 100644 --- a/src-tauri/src/core/java/providers/adoptium.rs +++ b/src-tauri/src/core/java/providers/adoptium.rs @@ -198,6 +198,7 @@ impl JavaProvider for AdoptiumProvider { available_major_versions: available.available_releases, lts_versions: available.available_lts_releases, cached_at: now, + cache_version: 1, }; let _ = super::super::save_catalog_cache(app_handle, &catalog); -- cgit v1.2.3-70-g09d2 From c46d6c51b8bec6a52ca66087ef9b8edc48d809a3 Mon Sep 17 00:00:00 2001 From: "Begonia, HE" <163421589+BegoniaHe@users.noreply.github.com> Date: Thu, 29 Jan 2026 02:34:16 +0100 Subject: refactor(java): improve error handling and logging - Extract JavaError to dedicated error.rs module - Add serde defaults for JavaInstallation optional fields - Replace unwrap() with proper error propagation - Add detailed logging for Java resolution priority chain - Improve error mapping in validation (NotFound vs VerificationFailed) Reviewed-by: Claude Sonnet 4.5 --- src-tauri/src/core/java/error.rs | 95 +++++++++++++++++++++++++++ src-tauri/src/core/java/persistence.rs | 54 ++++++++++----- src-tauri/src/core/java/providers/adoptium.rs | 8 ++- src-tauri/src/core/mod.rs | 2 - src-tauri/src/main.rs | 1 + 5 files changed, 140 insertions(+), 20 deletions(-) create mode 100644 src-tauri/src/core/java/error.rs (limited to 'src-tauri/src/core/java') diff --git a/src-tauri/src/core/java/error.rs b/src-tauri/src/core/java/error.rs new file mode 100644 index 0000000..bf78d3b --- /dev/null +++ b/src-tauri/src/core/java/error.rs @@ -0,0 +1,95 @@ +use std::fmt; + +/// Unified error type for Java component operations +/// +/// This enum represents all possible errors that can occur in the Java component, +/// providing a consistent error handling interface across all modules. +#[derive(Debug, Clone)] +pub enum JavaError { + // Java installation not found at the specified path + NotFound, + // Invalid Java version format or unable to parse version + InvalidVersion(String), + // Java installation verification failed (e.g., -version command failed) + VerificationFailed(String), + // Network error during API calls or downloads + NetworkError(String), + // File I/O error (reading, writing, or accessing files) + IoError(String), + // Timeout occurred during operation + Timeout(String), + // Serialization/deserialization error + SerializationError(String), + // Invalid configuration or parameters + InvalidConfig(String), + // Download or installation failed + DownloadFailed(String), + // Extraction or decompression failed + ExtractionFailed(String), + // Checksum verification failed + ChecksumMismatch(String), + // Other unspecified errors + Other(String), +} + +impl fmt::Display for JavaError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + JavaError::NotFound => write!(f, "Java installation not found"), + JavaError::InvalidVersion(msg) => write!(f, "Invalid Java version: {}", msg), + JavaError::VerificationFailed(msg) => write!(f, "Java verification failed: {}", msg), + JavaError::NetworkError(msg) => write!(f, "Network error: {}", msg), + JavaError::IoError(msg) => write!(f, "I/O error: {}", msg), + JavaError::Timeout(msg) => write!(f, "Operation timeout: {}", msg), + JavaError::SerializationError(msg) => write!(f, "Serialization error: {}", msg), + JavaError::InvalidConfig(msg) => write!(f, "Invalid configuration: {}", msg), + JavaError::DownloadFailed(msg) => write!(f, "Download failed: {}", msg), + JavaError::ExtractionFailed(msg) => write!(f, "Extraction failed: {}", msg), + JavaError::ChecksumMismatch(msg) => write!(f, "Checksum mismatch: {}", msg), + JavaError::Other(msg) => write!(f, "{}", msg), + } + } +} + +impl std::error::Error for JavaError {} + +/// Convert JavaError to String for Tauri command results +impl From for String { + fn from(err: JavaError) -> Self { + err.to_string() + } +} + +/// Convert std::io::Error to JavaError +impl From for JavaError { + fn from(err: std::io::Error) -> Self { + JavaError::IoError(err.to_string()) + } +} + +/// Convert serde_json::Error to JavaError +impl From for JavaError { + fn from(err: serde_json::Error) -> Self { + JavaError::SerializationError(err.to_string()) + } +} + +/// Convert reqwest::Error to JavaError +impl From for JavaError { + fn from(err: reqwest::Error) -> Self { + if err.is_timeout() { + JavaError::Timeout(err.to_string()) + } else if err.is_connect() || err.is_request() { + JavaError::NetworkError(err.to_string()) + } else { + JavaError::NetworkError(err.to_string()) + } + } +} + +/// Convert String to JavaError +impl From for JavaError { + fn from(err: String) -> Self { + JavaError::Other(err) + } +} diff --git a/src-tauri/src/core/java/persistence.rs b/src-tauri/src/core/java/persistence.rs index d1e999e..fd81394 100644 --- a/src-tauri/src/core/java/persistence.rs +++ b/src-tauri/src/core/java/persistence.rs @@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize}; use std::path::PathBuf; use tauri::{AppHandle, Manager}; +use super::error::JavaError; + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JavaConfig { pub user_defined_paths: Vec, @@ -34,26 +36,43 @@ pub fn load_java_config(app_handle: &AppHandle) -> JavaConfig { } match std::fs::read_to_string(&config_path) { - Ok(content) => serde_json::from_str(&content).unwrap_or_default(), - Err(_) => JavaConfig::default(), + Ok(content) => match serde_json::from_str(&content) { + Ok(config) => config, + Err(err) => { + // Log the error but don't panic - return default config + log::warn!( + "Failed to parse Java config at {}: {}. Using default configuration.", + config_path.display(), + err + ); + JavaConfig::default() + } + }, + Err(err) => { + log::warn!( + "Failed to read Java config at {}: {}. Using default configuration.", + config_path.display(), + err + ); + JavaConfig::default() + } } } -pub fn save_java_config(app_handle: &AppHandle, config: &JavaConfig) -> Result<(), String> { +pub fn save_java_config(app_handle: &AppHandle, config: &JavaConfig) -> Result<(), JavaError> { let config_path = get_java_config_path(app_handle); - let content = serde_json::to_string_pretty(config).map_err(|e| e.to_string())?; - std::fs::create_dir_all( - config_path - .parent() - .expect("Java config path should have a parent directory"), - ) - .map_err(|e| e.to_string())?; - std::fs::write(&config_path, content).map_err(|e| e.to_string())?; + let content = serde_json::to_string_pretty(config)?; + + std::fs::create_dir_all(config_path.parent().ok_or_else(|| { + JavaError::InvalidConfig("Java config path has no parent directory".to_string()) + })?)?; + + std::fs::write(&config_path, content)?; Ok(()) } #[allow(dead_code)] -pub fn add_user_defined_path(app_handle: &AppHandle, path: String) -> Result<(), String> { +pub fn add_user_defined_path(app_handle: &AppHandle, path: String) -> Result<(), JavaError> { let mut config = load_java_config(app_handle); if !config.user_defined_paths.contains(&path) { config.user_defined_paths.push(path); @@ -62,14 +81,17 @@ pub fn add_user_defined_path(app_handle: &AppHandle, path: String) -> Result<(), } #[allow(dead_code)] -pub fn remove_user_defined_path(app_handle: &AppHandle, path: &str) -> Result<(), String> { +pub fn remove_user_defined_path(app_handle: &AppHandle, path: &str) -> Result<(), JavaError> { let mut config = load_java_config(app_handle); config.user_defined_paths.retain(|p| p != path); save_java_config(app_handle, &config) } #[allow(dead_code)] -pub fn set_preferred_java_path(app_handle: &AppHandle, path: Option) -> Result<(), String> { +pub fn set_preferred_java_path( + app_handle: &AppHandle, + path: Option, +) -> Result<(), JavaError> { let mut config = load_java_config(app_handle); config.preferred_java_path = path; save_java_config(app_handle, &config) @@ -82,11 +104,11 @@ pub fn get_preferred_java_path(app_handle: &AppHandle) -> Option { } #[allow(dead_code)] -pub fn update_last_detection_time(app_handle: &AppHandle) -> Result<(), String> { +pub fn update_last_detection_time(app_handle: &AppHandle) -> Result<(), JavaError> { let mut config = load_java_config(app_handle); config.last_detection_time = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .unwrap() + .map_err(|e| JavaError::Other(format!("System time error: {}", e)))? .as_secs(); save_java_config(app_handle, &config) } diff --git a/src-tauri/src/core/java/providers/adoptium.rs b/src-tauri/src/core/java/providers/adoptium.rs index 4b06721..40e1757 100644 --- a/src-tauri/src/core/java/providers/adoptium.rs +++ b/src-tauri/src/core/java/providers/adoptium.rs @@ -1,5 +1,6 @@ +use crate::core::java::error::JavaError; use crate::core::java::provider::JavaProvider; -use crate::core::java::{ImageType, JavaCatalog, JavaDownloadInfo, JavaError, JavaReleaseInfo}; +use crate::core::java::{ImageType, JavaCatalog, JavaDownloadInfo, JavaReleaseInfo}; use serde::Deserialize; use tauri::AppHandle; @@ -183,7 +184,10 @@ impl JavaProvider for AdoptiumProvider { // Task completed but returned None, should not happen in current implementation } Err(e) => { - eprintln!("AdoptiumProvider::fetch_catalog task join error: {:?}", e); + return Err(JavaError::NetworkError(format!( + "Failed to join Adoptium catalog fetch task: {}", + e + ))); } } } diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 12dff7c..dcbd47a 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -12,5 +12,3 @@ pub mod manifest; pub mod maven; pub mod rules; pub mod version_merge; - -pub use java::JavaInstallation; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index b74c746..7984ea8 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -208,6 +208,7 @@ async fn start_game( let java_installation = core::java::priority::resolve_java_for_launch( app_handle, + &window, instance.java_path_override.as_deref(), Some(&config.java_path), required_java_major, -- cgit v1.2.3-70-g09d2 From 5d630a24bed07dca20b6ddf55ffe4be36399ad0f Mon Sep 17 00:00:00 2001 From: "Begonia, HE" <163421589+BegoniaHe@users.noreply.github.com> Date: Thu, 29 Jan 2026 03:20:10 +0100 Subject: fix: resolve rebase conflicts and compilation errors - Export JavaError from java module - Fix type mismatches in Adoptium provider methods - Add type annotations for reqwest json() calls - Remove non-existent cache_version field from JavaCatalog - Fix resolve_java_for_launch call signature (remove extra window param) - Add error conversion to String for Tauri commands - Fix import for save_catalog_cache in adoptium.rs Reviewed-by: Claude Sonnet 4.5 --- src-tauri/src/core/java/mod.rs | 161 ++++++++++++++------------ src-tauri/src/core/java/priority.rs | 76 ++++++------ src-tauri/src/core/java/providers/adoptium.rs | 22 ++-- src-tauri/src/core/java/validation.rs | 30 +++-- src-tauri/src/main.rs | 1 - 5 files changed, 152 insertions(+), 138 deletions(-) (limited to 'src-tauri/src/core/java') diff --git a/src-tauri/src/core/java/mod.rs b/src-tauri/src/core/java/mod.rs index c88cd1c..770ba08 100644 --- a/src-tauri/src/core/java/mod.rs +++ b/src-tauri/src/core/java/mod.rs @@ -3,11 +3,14 @@ use std::path::PathBuf; use tauri::{AppHandle, Emitter, Manager}; pub mod detection; +pub mod error; pub mod persistence; pub mod priority; -pub mod validation; pub mod provider; pub mod providers; +pub mod validation; + +pub use error::JavaError; /// Remove the UNC prefix (\\?\) from Windows paths pub fn strip_unc_prefix(path: PathBuf) -> PathBuf { @@ -94,8 +97,6 @@ pub struct JavaDownloadInfo { pub image_type: String, // "jre" or "jdk" } - - pub fn get_java_install_dir(app_handle: &AppHandle) -> PathBuf { app_handle.path().app_data_dir().unwrap().join("java") } @@ -153,7 +154,10 @@ pub async fn fetch_java_catalog( force_refresh: bool, ) -> Result { let provider = AdoptiumProvider::new(); - provider.fetch_catalog(app_handle, force_refresh).await + provider + .fetch_catalog(app_handle, force_refresh) + .await + .map_err(|e| e.to_string()) } pub async fn fetch_java_release( @@ -161,12 +165,18 @@ pub async fn fetch_java_release( image_type: ImageType, ) -> Result { let provider = AdoptiumProvider::new(); - provider.fetch_release(major_version, image_type).await + provider + .fetch_release(major_version, image_type) + .await + .map_err(|e| e.to_string()) } pub async fn fetch_available_versions() -> Result, String> { let provider = AdoptiumProvider::new(); - provider.available_versions().await + provider + .available_versions() + .await + .map_err(|e| e.to_string()) } pub async fn download_and_install_java( @@ -180,7 +190,12 @@ pub async fn download_and_install_java( let file_name = info.file_name.clone(); let install_base = custom_path.unwrap_or_else(|| get_java_install_dir(app_handle)); - let version_dir = install_base.join(format!("{}-{}-{}", provider.install_prefix(), major_version, image_type)); + let version_dir = install_base.join(format!( + "{}-{}-{}", + provider.install_prefix(), + major_version, + image_type + )); std::fs::create_dir_all(&install_base) .map_err(|e| format!("Failed to create installation directory: {}", e))?; @@ -319,48 +334,48 @@ fn find_top_level_dir(extract_dir: &PathBuf) -> Result { } pub async fn detect_java_installations() -> Vec { - let mut installations = Vec::new(); - let candidates = detection::get_java_candidates(); - - for candidate in candidates { - if let Some(java) = validation::check_java_installation(&candidate).await { - if !installations - .iter() - .any(|j: &JavaInstallation| j.path == java.path) - { - installations.push(java); - } - } - } - - installations.sort_by(|a, b| { - let v_a = validation::parse_java_version(&a.version); - let v_b = validation::parse_java_version(&b.version); - v_b.cmp(&v_a) - }); - - installations + let mut installations = Vec::new(); + let candidates = detection::get_java_candidates(); + + for candidate in candidates { + if let Some(java) = validation::check_java_installation(&candidate).await { + if !installations + .iter() + .any(|j: &JavaInstallation| j.path == java.path) + { + installations.push(java); + } + } + } + + installations.sort_by(|a, b| { + let v_a = validation::parse_java_version(&a.version); + let v_b = validation::parse_java_version(&b.version); + v_b.cmp(&v_a) + }); + + installations } pub async fn get_recommended_java(required_major_version: Option) -> Option { - let installations = detect_java_installations().await; - - if let Some(required) = required_major_version { - installations.into_iter().find(|java| { - let major = validation::parse_java_version(&java.version); - major >= required as u32 - }) - } else { - installations.into_iter().next() - } + let installations = detect_java_installations().await; + + if let Some(required) = required_major_version { + installations.into_iter().find(|java| { + let major = validation::parse_java_version(&java.version); + major >= required as u32 + }) + } else { + installations.into_iter().next() + } } pub async fn get_compatible_java( - app_handle: &AppHandle, - required_major_version: Option, - max_major_version: Option, + app_handle: &AppHandle, + required_major_version: Option, + max_major_version: Option, ) -> Option { - let installations = detect_all_java_installations(app_handle).await; + let installations = detect_all_java_installations(app_handle).await; installations.into_iter().find(|java| { let major = validation::parse_java_version(&java.version); @@ -369,9 +384,9 @@ pub async fn get_compatible_java( } pub async fn is_java_compatible( - java_path: &str, - required_major_version: Option, - max_major_version: Option, + java_path: &str, + required_major_version: Option, + max_major_version: Option, ) -> bool { let java_path_buf = PathBuf::from(java_path); if let Some(java) = validation::check_java_installation(&java_path_buf).await { @@ -383,34 +398,34 @@ pub async fn is_java_compatible( } pub async fn detect_all_java_installations(app_handle: &AppHandle) -> Vec { - let mut installations = detect_java_installations().await; - - let dropout_java_dir = get_java_install_dir(app_handle); - if dropout_java_dir.exists() { - if let Ok(entries) = std::fs::read_dir(&dropout_java_dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - let java_bin = find_java_executable(&path); - if let Some(java_path) = java_bin { - if let Some(java) = validation::check_java_installation(&java_path).await { - if !installations.iter().any(|j| j.path == java.path) { - installations.push(java); - } - } - } - } - } - } - } - - installations.sort_by(|a, b| { - let v_a = validation::parse_java_version(&a.version); - let v_b = validation::parse_java_version(&b.version); - v_b.cmp(&v_a) - }); - - installations + let mut installations = detect_java_installations().await; + + let dropout_java_dir = get_java_install_dir(app_handle); + if dropout_java_dir.exists() { + if let Ok(entries) = std::fs::read_dir(&dropout_java_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + let java_bin = find_java_executable(&path); + if let Some(java_path) = java_bin { + if let Some(java) = validation::check_java_installation(&java_path).await { + if !installations.iter().any(|j| j.path == java.path) { + installations.push(java); + } + } + } + } + } + } + } + + installations.sort_by(|a, b| { + let v_a = validation::parse_java_version(&a.version); + let v_b = validation::parse_java_version(&b.version); + v_b.cmp(&v_a) + }); + + installations } fn find_java_executable(dir: &PathBuf) -> Option { diff --git a/src-tauri/src/core/java/priority.rs b/src-tauri/src/core/java/priority.rs index 09a61b3..e456680 100644 --- a/src-tauri/src/core/java/priority.rs +++ b/src-tauri/src/core/java/priority.rs @@ -5,48 +5,48 @@ use crate::core::java::persistence; use crate::core::java::validation; pub async fn resolve_java_for_launch( - app_handle: &AppHandle, - instance_java_override: Option<&str>, - global_java_path: Option<&str>, - required_major_version: Option, - max_major_version: Option, + app_handle: &AppHandle, + instance_java_override: Option<&str>, + global_java_path: Option<&str>, + required_major_version: Option, + max_major_version: Option, ) -> Option { - if let Some(override_path) = instance_java_override { - if !override_path.is_empty() { - let path_buf = std::path::PathBuf::from(override_path); - if let Some(java) = validation::check_java_installation(&path_buf).await { - if is_version_compatible(&java, required_major_version, max_major_version) { - return Some(java); - } - } - } - } + if let Some(override_path) = instance_java_override { + if !override_path.is_empty() { + let path_buf = std::path::PathBuf::from(override_path); + if let Some(java) = validation::check_java_installation(&path_buf).await { + if is_version_compatible(&java, required_major_version, max_major_version) { + return Some(java); + } + } + } + } - if let Some(global_path) = global_java_path { - if !global_path.is_empty() { - let path_buf = std::path::PathBuf::from(global_path); - if let Some(java) = validation::check_java_installation(&path_buf).await { - if is_version_compatible(&java, required_major_version, max_major_version) { - return Some(java); - } - } - } - } + if let Some(global_path) = global_java_path { + if !global_path.is_empty() { + let path_buf = std::path::PathBuf::from(global_path); + if let Some(java) = validation::check_java_installation(&path_buf).await { + if is_version_compatible(&java, required_major_version, max_major_version) { + return Some(java); + } + } + } + } - let preferred = persistence::get_preferred_java_path(app_handle); - if let Some(pref_path) = preferred { - let path_buf = std::path::PathBuf::from(&pref_path); - if let Some(java) = validation::check_java_installation(&path_buf).await { - if is_version_compatible(&java, required_major_version, max_major_version) { - return Some(java); - } - } - } + let preferred = persistence::get_preferred_java_path(app_handle); + if let Some(pref_path) = preferred { + let path_buf = std::path::PathBuf::from(&pref_path); + if let Some(java) = validation::check_java_installation(&path_buf).await { + if is_version_compatible(&java, required_major_version, max_major_version) { + return Some(java); + } + } + } - let installations = super::detect_all_java_installations(app_handle).await; - installations - .into_iter() - .find(|java| is_version_compatible(java, required_major_version, max_major_version)) + let installations = super::detect_all_java_installations(app_handle).await; + installations + .into_iter() + .find(|java| is_version_compatible(java, required_major_version, max_major_version)) } fn is_version_compatible( diff --git a/src-tauri/src/core/java/providers/adoptium.rs b/src-tauri/src/core/java/providers/adoptium.rs index 40e1757..a73a0f6 100644 --- a/src-tauri/src/core/java/providers/adoptium.rs +++ b/src-tauri/src/core/java/providers/adoptium.rs @@ -1,5 +1,6 @@ use crate::core::java::error::JavaError; use crate::core::java::provider::JavaProvider; +use crate::core::java::save_catalog_cache; use crate::core::java::{ImageType, JavaCatalog, JavaDownloadInfo, JavaReleaseInfo}; use serde::Deserialize; use tauri::AppHandle; @@ -72,7 +73,7 @@ impl JavaProvider for AdoptiumProvider { force_refresh: bool, ) -> Result { if !force_refresh { - if let Ok(Some(cached)) = crate::core::java::load_cached_catalog(app_handle) { + if let Some(cached) = crate::core::java::load_cached_catalog(app_handle) { return Ok(cached); } } @@ -90,7 +91,7 @@ impl JavaProvider for AdoptiumProvider { .map_err(|e| { JavaError::NetworkError(format!("Failed to fetch available releases: {}", e)) })? - .json() + .json::() .await .map_err(|e| { JavaError::SerializationError(format!("Failed to parse available releases: {}", e)) @@ -202,10 +203,9 @@ impl JavaProvider for AdoptiumProvider { available_major_versions: available.available_releases, lts_versions: available.available_lts_releases, cached_at: now, - cache_version: 1, }; - let _ = super::super::save_catalog_cache(app_handle, &catalog); + let _ = save_catalog_cache(app_handle, &catalog); Ok(catalog) } @@ -238,9 +238,10 @@ impl JavaProvider for AdoptiumProvider { ))); } - let assets: Vec = response.json().await.map_err(|e| { - JavaError::SerializationError(format!("Failed to parse API response: {}", e)) - })?; + let assets: Vec = + response.json::>().await.map_err(|e| { + JavaError::SerializationError(format!("Failed to parse API response: {}", e)) + })?; let asset = assets .into_iter() @@ -265,9 +266,10 @@ impl JavaProvider for AdoptiumProvider { .await .map_err(|e| JavaError::NetworkError(format!("Network request failed: {}", e)))?; - let releases: AvailableReleases = response.json().await.map_err(|e| { - JavaError::SerializationError(format!("Failed to parse response: {}", e)) - })?; + let releases: AvailableReleases = + response.json::().await.map_err(|e| { + JavaError::SerializationError(format!("Failed to parse response: {}", e)) + })?; Ok(releases.available_releases) } diff --git a/src-tauri/src/core/java/validation.rs b/src-tauri/src/core/java/validation.rs index e086e74..48782f6 100644 --- a/src-tauri/src/core/java/validation.rs +++ b/src-tauri/src/core/java/validation.rs @@ -8,12 +8,10 @@ use std::os::windows::process::CommandExt; use super::JavaInstallation; pub async fn check_java_installation(path: &PathBuf) -> Option { - let path = path.clone(); - tokio::task::spawn_blocking(move || { - check_java_installation_blocking(&path) - }) - .await - .ok()? + let path = path.clone(); + tokio::task::spawn_blocking(move || check_java_installation_blocking(&path)) + .await + .ok()? } fn check_java_installation_blocking(path: &PathBuf) -> Option { @@ -24,23 +22,23 @@ fn check_java_installation_blocking(path: &PathBuf) -> Option #[cfg(target_os = "windows")] cmd.creation_flags(0x08000000); - let output = cmd.output().ok()?; + let output = cmd.output().ok()?; - let version_output = String::from_utf8_lossy(&output.stderr); + let version_output = String::from_utf8_lossy(&output.stderr); let version = parse_version_string(&version_output)?; let arch = extract_architecture(&version_output); let vendor = extract_vendor(&version_output); let is_64bit = version_output.to_lowercase().contains("64-bit") || arch == "aarch64"; - Some(JavaInstallation { - path: path.to_string_lossy().to_string(), - version, - arch, - vendor, - source: "system".to_string(), - is_64bit, - }) + Some(JavaInstallation { + path: path.to_string_lossy().to_string(), + version, + arch, + vendor, + source: "system".to_string(), + is_64bit, + }) } pub fn parse_version_string(output: &str) -> Option { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 7984ea8..b74c746 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -208,7 +208,6 @@ async fn start_game( let java_installation = core::java::priority::resolve_java_for_launch( app_handle, - &window, instance.java_path_override.as_deref(), Some(&config.java_path), required_java_major, -- cgit v1.2.3-70-g09d2