aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src-tauri/src/core/java/detection.rs
blob: 08dcebb948a9bf250006a027a2209374f39787b1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::time::Duration;

#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;

use crate::core::java::strip_unc_prefix;

const WHICH_TIMEOUT: Duration = Duration::from_secs(2);

/// Scans a directory for Java installations, filtering out symlinks
///
/// # Arguments
/// * `base_dir` - Base directory to scan (e.g., mise or SDKMAN java dir)
/// * `should_skip` - Predicate to determine if an entry should be skipped
///
/// # Returns
/// First valid Java installation found, or `None`
fn scan_java_dir<F>(base_dir: &Path, should_skip: F) -> Option<PathBuf>
where
    F: Fn(&std::fs::DirEntry) -> bool,
{
    std::fs::read_dir(base_dir)
        .ok()?
        .flatten()
        .filter(|entry| {
            let path = entry.path();
            // Only consider real directories, not symlinks
            path.is_dir() && !path.is_symlink() && !should_skip(entry)
        })
        .find_map(|entry| {
            let java_path = entry.path().join("bin/java");
            if java_path.exists() && java_path.is_file() {
                Some(java_path)
            } else {
                None
            }
        })
}

/// Finds Java installation from SDKMAN! if available
///
/// Scans the SDKMAN! candidates directory and returns the first valid Java installation found.
/// Skips the 'current' symlink to avoid duplicates.
///
/// Path: `~/.sdkman/candidates/java/`
///
/// # Returns
/// `Some(PathBuf)` pointing to `bin/java` if found, `None` otherwise
pub fn find_sdkman_java() -> Option<PathBuf> {
    let home = std::env::var("HOME").ok()?;
    let sdkman_base = PathBuf::from(&home).join(".sdkman/candidates/java/");

    if !sdkman_base.exists() {
        return None;
    }

    scan_java_dir(&sdkman_base, |entry| entry.file_name() == "current")
}

/// Finds Java installation from mise if available
///
/// Scans the mise Java installation directory and returns the first valid installation found.
/// Skips version alias symlinks (e.g., `21`, `21.0`, `latest`, `lts`) to avoid duplicates.
///
/// Path: `~/.local/share/mise/installs/java/`
///
/// # Returns
/// `Some(PathBuf)` pointing to `bin/java` if found, `None` otherwise
pub fn find_mise_java() -> Option<PathBuf> {
    let home = std::env::var("HOME").ok()?;
    let mise_base = PathBuf::from(&home).join(".local/share/mise/installs/java/");

    if !mise_base.exists() {
        return None;
    }

    scan_java_dir(&mise_base, |_| false) // mise: no additional filtering needed
}

/// 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<String> {
    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<PathBuf> {
    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);
        }

        // Check common mise java candidates
        if let Some(mise_java) = find_mise_java() {
            candidates.push(mise_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);
        }

        // Check common mise java candidates
        if let Some(mise_java) = find_mise_java() {
            candidates.push(mise_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
}