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/provider.rs | 41 +++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src-tauri/src/core/java/provider.rs (limited to 'src-tauri/src/core/java/provider.rs') 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; +} -- 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/provider.rs') 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 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/provider.rs') 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