Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jre detection #54

Merged
merged 12 commits into from
Mar 28, 2023
15 changes: 14 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions theseus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
240 changes: 240 additions & 0 deletions theseus/src/util/jre.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<JavaVersion>, 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<HashSet<JavaVersion>, 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<String, std::io::Error> =
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<Vec<JavaVersion>, 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<Vec<JavaVersion>, 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<HashSet<JavaVersion>, 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<JavaVersion> {
// 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),
}
1 change: 1 addition & 0 deletions theseus/src/util/mod.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down