diff --git a/Cargo.lock b/Cargo.lock index c002d5f20..ab04768f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2590,7 +2590,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", + "winreg 0.10.1", ] [[package]] @@ -3361,7 +3361,9 @@ dependencies = [ "chrono", "daedalus", "dirs", + "dunce", "futures", + "lazy_static", "log", "once_cell", "pretty_assertions", @@ -3378,6 +3380,7 @@ dependencies = [ "tracing-error", "url", "uuid 1.3.0", + "winreg 0.11.0", "zip", ] @@ -4181,6 +4184,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "winreg" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a1a57ff50e9b408431e8f97d5456f2807f8eb2a2cd79b06068fc87f8ecf189" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "winres" version = "0.1.12" diff --git a/theseus/Cargo.toml b/theseus/Cargo.toml index b87e99c4f..07db049a8 100644 --- a/theseus/Cargo.toml +++ b/theseus/Cargo.toml @@ -28,11 +28,17 @@ thiserror = "1.0" tracing = "0.1" tracing-error = "0.2" + async-tungstenite = { version = "0.17", features = ["tokio-runtime", "tokio-native-tls"] } futures = "0.3" once_cell = "1.9.0" reqwest = { version = "0.11", features = ["json"] } tokio = { version = "1", features = ["full"] } +lazy_static = "1.4.0" + +[target.'cfg(windows)'.dependencies] +winreg = "0.11.0" +dunce = "1.0.3" [dev-dependencies] argh = "0.1.6" diff --git a/theseus/src/util/jre.rs b/theseus/src/util/jre.rs new file mode 100644 index 000000000..d29757e97 --- /dev/null +++ b/theseus/src/util/jre.rs @@ -0,0 +1,240 @@ +use lazy_static::lazy_static; +use regex::Regex; +use std::collections::HashSet; +use std::env; +use std::path::PathBuf; +use std::process::Command; + +#[cfg(target_os = "windows")] +use winreg::{ + enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_32KEY, KEY_WOW64_64KEY}, + RegKey, +}; + +// Uses dunce canonicalization to resolve symlinks without UNC prefixes +#[cfg(target_os = "windows")] +use dunce::canonicalize; +#[cfg(not(target_os = "windows"))] +use std::fs::canonicalize; + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct JavaVersion { + pub path: String, + pub version: String, +} + +// Entrypoint function (Windows) +// Returns a Vec of unique JavaVersions from the PATH, Windows Registry Keys and common Java locations +#[cfg(target_os = "windows")] +#[tracing::instrument] +pub fn get_all_jre() -> Result, JREError> { + // Use HashSet to avoid duplicates + let mut jres = HashSet::new(); + + // Add JRES directly on PATH + jres.extend(get_all_jre_path()?); + + // Hard paths for locations for commonly installed .exes + let java_paths = [r"C:/Program Files/Java", r"C:/Program Files (x86)/Java"]; + for java_path in java_paths { + let Ok(java_subpaths) = std::fs::read_dir(java_path) else {continue }; + for java_subpath in java_subpaths { + let path = java_subpath?.path(); + if let Some(j) = + check_java_at_filepath(PathBuf::from(path).join("bin")) + { + jres.insert(j); + break; + } + } + } + + // Windows Registry Keys + let key_paths = [ + r"SOFTWARE\JavaSoft\Java Runtime Environment", // Oracle + r"SOFTWARE\JavaSoft\Java Development Kit", + r"SOFTWARE\\JavaSoft\\JRE", // Oracle + r"SOFTWARE\\JavaSoft\\JDK", + r"SOFTWARE\\Eclipse Foundation\\JDK", // Eclipse + r"SOFTWARE\\Eclipse Adoptium\\JRE", // Eclipse + r"SOFTWARE\\Eclipse Foundation\\JDK", // Eclipse + r"SOFTWARE\\Microsoft\\JDK", // Microsoft + ]; + for key in key_paths { + if let Ok(jre_key) = RegKey::predef(HKEY_LOCAL_MACHINE) + .open_subkey_with_flags(key, KEY_READ | KEY_WOW64_32KEY) + { + jres.extend(get_all_jre_winregkey(jre_key)?); + } + if let Ok(jre_key) = RegKey::predef(HKEY_LOCAL_MACHINE) + .open_subkey_with_flags(key, KEY_READ | KEY_WOW64_64KEY) + { + jres.extend(get_all_jre_winregkey(jre_key)?); + } + } + + Ok(jres.into_iter().collect()) +} + +#[cfg(target_os = "windows")] +#[tracing::instrument] +pub fn get_all_jre_winregkey( + jre_key: RegKey, +) -> Result, JREError> { + let mut jres = HashSet::new(); + + for subkey in jre_key.enum_keys() { + let subkey = subkey?; + let subkey = jre_key.open_subkey(subkey)?; + + let subkey_value_names = + [r"JavaHome", r"InstallationPath", r"\\hotspot\\MSI"]; + + for subkey_value in subkey_value_names { + let path: Result = + subkey.get_value(subkey_value); + let Ok(path) = path else {continue}; + if let Some(j) = + check_java_at_filepath(PathBuf::from(path).join("bin")) + { + jres.insert(j); + break; + } + } + } + Ok(jres) +} + +// Entrypoint function (Mac) +// Returns a Vec of unique JavaVersions from the PATH, and common Java locations +#[cfg(target_os = "macos")] +#[tracing::instrument] +pub fn get_all_jre() -> Result, JREError> { + // Use HashSet to avoid duplicates + let mut jres = HashSet::new(); + + // Add JREs directly on PATH + jres.extend(get_all_jre_path()?); + + // Hard paths for locations for commonly installed .exes + let java_paths = [ + r"/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java", + r"/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home", + r"/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands", + ]; + for path in java_paths { + if let Some(j) = check_java_at_filepath(PathBuf::from(path).join("bin")) + { + jres.insert(j); + break; + } + } + Ok(jres.into_iter().collect()) +} + +// Entrypoint function (Linux) +// Returns a Vec of unique JavaVersions from the PATH, and common Java locations +#[cfg(target_os = "linux")] +#[tracing::instrument] +pub fn get_all_jre() -> Result, JREError> { + // Use HashSet to avoid duplicates + let mut jres = HashSet::new(); + + // Add JREs directly on PATH + jres.extend(get_all_jre_path()?); + + // Hard paths for locations for commonly installed locations + let java_paths = [ + r"/usr", + r"/usr/java", + r"/usr/lib/jvm", + r"/usr/lib64/jvm", + r"/opt/jdk", + r"/opt/jdks", + ]; + for path in java_paths { + if let Some(j) = + check_java_at_filepath(PathBuf::from(path).join("jre").join("bin")) + { + jres.insert(j); + break; + } + if let Some(j) = check_java_at_filepath(PathBuf::from(path).join("bin")) + { + jres.insert(j); + break; + } + } + Ok(jres.into_iter().collect()) +} + +#[tracing::instrument] +pub fn get_all_jre_path() -> Result, JREError> { + // Iterate over values in PATH variable, where accessible JREs are referenced + let paths = env::var("PATH")?; + let paths = env::split_paths(&paths); + + let mut jres = HashSet::new(); + for path in paths { + if let Some(j) = check_java_at_filepath(path) { + jres.insert(j); + } + } + Ok(jres) +} + +#[cfg(target_os = "windows")] +#[allow(dead_code)] +const JAVA_BIN: &'static str = "java.exe"; + +#[cfg(not(target_os = "windows"))] +#[allow(dead_code)] +const JAVA_BIN: &'static str = "java"; + +// For example filepath 'path', attempt to resolve it and get a Java version at this path +// If no such path exists, or no such valid java at this path exists, returns None +#[tracing::instrument] +pub fn check_java_at_filepath(path: PathBuf) -> Option { + // Attempt to canonicalize the potential java filepath + // If it fails, this path does not exist and None is returned (no Java here) + let Ok(path) = canonicalize(path) else { return None }; + let Some(path_str) = path.to_str() else { return None }; + + // Checks for existence of Java at this filepath + let java = path.join(JAVA_BIN); + if !java.exists() { + return None; + }; + + // Run 'java -version' using found java binary + let output = Command::new(&java).arg("-version").output().ok()?; + let stderr = String::from_utf8_lossy(&output.stderr); + + // Match: version "1.8.0_361" + // Extracting version numbers + lazy_static! { + static ref JAVA_VERSION_CAPTURE: Regex = + Regex::new(r#"version "([\d\._]+)""#).unwrap(); + } + + // Extract version info from it + if let Some(captures) = JAVA_VERSION_CAPTURE.captures(&stderr) { + if let Some(version) = captures.get(1) { + let path = path_str.to_string(); + return Some(JavaVersion { + path, + version: version.as_str().to_string(), + }); + } + } + None +} + +#[derive(thiserror::Error, Debug)] +pub enum JREError { + #[error("Command error : {0}")] + IOError(#[from] std::io::Error), + + #[error("Env error: {0}")] + EnvError(#[from] env::VarError), +} diff --git a/theseus/src/util/mod.rs b/theseus/src/util/mod.rs index a0ac4cddb..6f8731bfe 100644 --- a/theseus/src/util/mod.rs +++ b/theseus/src/util/mod.rs @@ -1,5 +1,6 @@ //! Theseus utility functions pub mod fetch; +pub mod jre; pub mod platform; /// Wrap a builder which uses a mut reference into one which outputs an owned value