use std::io::Read; use std::path::PathBuf; use std::process::{Command, Stdio}; use std::time::Duration; #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; 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"); if sdkman_path.exists() { Some(sdkman_path) } else { None } } /// 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 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() { 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) => { // Command still running, sleep briefly before checking again std::thread::sleep(Duration::from_millis(50)); } Err(_) => { let _ = child.kill(); let _ = child.wait(); return None; } } } } /// 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(); // 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() { 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); } } } } // 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 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); if java_path.exists() { candidates.push(java_path); } } candidates }