OneLauncher-Cracked/packages/oneclient_java/src/service.rs

403 lines
12 KiB
Rust

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
use oneclient_events::{Choice, EventBus, GroupedProgressSession, Prompt};
use oneclient_net::RequestClient;
use crate::checker::{self, JavaCheckInfo};
use crate::data::{JavaPackage, JavaRuntime};
use crate::error::{JavaError, JavaResult};
use crate::resolve::resolve_java_executable;
use crate::store::JavaStore;
use crate::vendors::{self, JavaVendor};
/// Choice ids for the "Java required" prompt. Namespaced so a front-end can
/// tell this prompt apart from any other by looking for one of these ids.
pub const JAVA_CHOICE_DOWNLOAD: &str = "java.download";
pub const JAVA_CHOICE_FOLDER: &str = "java.folder";
/// Names the kind of selection the Java prompt expects back, so the front-end
/// knows to show its vendor list rather than some other picker.
pub const JAVA_VENDOR_HINT: &str = "java-vendor";
pub const INSTALLABLE_MAJORS: &[u32] = &[8, 11, 16, 17, 18, 19, 20, 21, 22, 23];
/// What each option on the "Java required" prompt means. Local to this module,
/// since the event bus only ever sees the choice ids.
enum JavaPromptAnswer {
Download,
PickFolder,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AvailableJava {
pub major: u32,
pub package: JavaPackage,
}
/// Everything the Java flows need: somewhere to remember runtimes, a way to
/// fetch them, and a bus to report progress and ask questions on.
///
/// `JavaManager` was a unit struct whose methods each took a `&DbPool` or a
/// `&LauncherServices`; the state is held here instead, and the store is a port
/// (see [`crate::JavaStore`]) so this crate never depends on the database.
pub struct JavaService {
store: Arc<dyn JavaStore>,
net: RequestClient,
events: EventBus,
}
impl JavaService {
#[must_use]
pub fn new(store: Arc<dyn JavaStore>, net: RequestClient, events: EventBus) -> Self {
Self { store, net, events }
}
#[must_use]
pub fn net(&self) -> &RequestClient {
&self.net
}
#[must_use]
pub fn events(&self) -> &EventBus {
&self.events
}
// --- queries -----------------------------------------------------------
#[tracing::instrument(level = "debug", skip(self))]
pub async fn list_runtimes(&self) -> JavaResult<Vec<JavaRuntime>> {
Ok(self.store.list().await?)
}
/// The runtime a settings profile points at, if it is still registered.
#[tracing::instrument(level = "debug", skip(self))]
pub async fn runtime_for_profile(
&self,
java_path: Option<&str>,
) -> JavaResult<Option<JavaRuntime>> {
let Some(path) = java_path else {
return Ok(None);
};
Ok(self.store.get_by_path(path).await?)
}
#[tracing::instrument(level = "debug", skip(self))]
pub async fn available_versions(&self, vendor: &JavaVendor) -> JavaResult<Vec<AvailableJava>> {
let Some(provider) = provider_for_vendor(vendor) else {
return Ok(Vec::new());
};
let packages = provider.list_packages(None, &self.net).await?;
let mut by_major = BTreeMap::<u32, JavaPackage>::new();
for package in packages {
let Some(&major) = package.java_version.first() else {
continue;
};
by_major.entry(major).or_insert(package);
}
let mut available: Vec<AvailableJava> = by_major
.into_iter()
.map(|(major, package)| AvailableJava { major, package })
.collect();
available.sort_by_key(|entry| std::cmp::Reverse(entry.major));
Ok(available)
}
// --- registration ------------------------------------------------------
/// Re-scans the well-known install locations and records what it finds.
#[tracing::instrument(level = "debug", skip(self))]
pub async fn rescan(&self) -> JavaResult<()> {
self.register_located(&crate::locate::locate_java().await?)
.await
}
#[tracing::instrument(skip(self))]
pub async fn add_custom_runtime(&self, folder: PathBuf) -> JavaResult<JavaRuntime> {
let executable = resolve_java_executable(&folder)?;
self.register_checked(&executable, None).await
}
#[tracing::instrument(skip(self))]
pub async fn remove_runtime(&self, absolute_path: &str) -> JavaResult<()> {
self.store.delete_by_path(absolute_path).await?;
tracing::info!("removed Java runtime");
Ok(())
}
// --- acquisition -------------------------------------------------------
/// Finds a usable Java `major`, downloading or asking the user if needed.
///
/// A JDK is preferred over a JRE of the same major throughout: a recorded
/// runtime that turns out to be a bare JRE is kept only as a fallback, so a
/// system scan gets its chance to turn up a kit first.
#[tracing::instrument(level = "debug", skip(self, progress))]
pub async fn prepare(
&self,
major: u32,
search_system: bool,
auto_install: bool,
progress: Option<&GroupedProgressSession>,
) -> JavaResult<JavaRuntime> {
let recorded = self.store.latest_by_major(major).await?;
if let Some(runtime) = &recorded
&& checker::is_jdk_executable(&runtime.absolute_path)
{
return Ok(runtime.clone());
}
if search_system {
let located = crate::locate::locate_java().await?;
self.register_located(&located).await?;
// Anything located only displaces what is already recorded when it
// is the kit that recording is not.
if let Some((path, info)) = crate::locate::best_for_major(&located, major)
&& (info.is_jdk || recorded.is_none())
{
return self.persist(path, info).await;
}
}
if let Some(runtime) = recorded {
return Ok(runtime);
}
if auto_install {
return self.download_and_register(major, progress).await;
}
self.prompt_and_install(major, progress).await
}
#[tracing::instrument(skip(self, progress))]
pub async fn install_runtime(
&self,
major: u32,
progress: Option<&GroupedProgressSession>,
) -> JavaResult<JavaRuntime> {
self.download_and_register(major, progress).await
}
#[tracing::instrument(skip(self))]
pub async fn install_runtime_from(
&self,
vendor: &JavaVendor,
major: u32,
) -> JavaResult<JavaRuntime> {
let provider = provider_for_vendor(vendor).ok_or(JavaError::PackageNotFound { major })?;
let package = provider
.latest_package_by_major(major, &self.net)
.await?
.ok_or(JavaError::PackageNotFound { major })?;
let executable = provider
.install_package(&package, &self.net, &self.events, None)
.await?;
self.register_checked(&executable, Some(major)).await
}
/// Ask the user how to get a missing runtime, then act on the answer.
///
/// The choice ids stay inside this function: `ask` hands back the
/// [`JavaPromptAnswer`] paired with the chosen option, so the vendor never
/// has to be named in the event layer.
async fn prompt_and_install(
&self,
major: u32,
progress: Option<&GroupedProgressSession>,
) -> JavaResult<JavaRuntime> {
let prompt = Prompt::new(
"Java required",
format!(
"No Java {major} runtime was found. Download it automatically, choose an existing installation folder, or cancel?"
),
)
.option(
Choice::primary(JAVA_CHOICE_DOWNLOAD, "Download").picks_selection(JAVA_VENDOR_HINT),
JavaPromptAnswer::Download,
)
.option(
Choice::new(JAVA_CHOICE_FOLDER, "Choose folder")
.picks_folder(format!("Select a Java {major} installation folder")),
JavaPromptAnswer::PickFolder,
)
.dismiss("Cancel");
let Some(chosen) = self.events.ask(prompt).await? else {
return Err(JavaError::Cancelled);
};
match chosen.value {
// The UI offers a vendor list; picking one installs it directly.
// Without a selection we fall back to trying every vendor in turn.
JavaPromptAnswer::Download => match chosen.selection() {
Some(vendor) => {
let vendor = JavaVendor::from_str(vendor)
.unwrap_or_else(|_| JavaVendor::Other(vendor.to_string()));
self.install_runtime_from(&vendor, major).await
}
None => self.download_and_register(major, progress).await,
},
JavaPromptAnswer::PickFolder => match chosen.folder() {
Some(folder) => {
let executable = resolve_java_executable(folder)?;
self.register_checked(&executable, Some(major)).await
}
None => Err(JavaError::Cancelled),
},
}
}
/// Tries every vendor in turn until one yields a working runtime.
#[tracing::instrument(level = "debug", skip(self, progress))]
async fn download_and_register(
&self,
major: u32,
progress: Option<&GroupedProgressSession>,
) -> JavaResult<JavaRuntime> {
for provider in vendors::runtime_providers() {
let vendor = provider.vendor();
let packages = provider.list_packages(Some(major), &self.net).await;
let package = match &packages {
Ok(packages) => match packages
.iter()
.find(|p| p.java_version.first() == Some(&major))
.or_else(|| packages.first())
{
Some(package) => package,
None => {
tracing::warn!(?vendor, major, "no packages found");
continue;
}
},
Err(err) => {
tracing::warn!(?vendor, major, "failed to query packages: {err}");
continue;
}
};
tracing::info!(?vendor, major, "downloading Java runtime");
match provider
.install_package(package, &self.net, &self.events, progress)
.await
{
Ok(executable) => return self.register_checked(&executable, Some(major)).await,
Err(err) => tracing::warn!(?vendor, major, "install failed: {err}"),
}
}
Err(JavaError::PackageNotFound { major })
}
// --- persistence -------------------------------------------------------
/// Probes `executable`, checks it is the version we expected, and records it.
#[tracing::instrument(level = "debug", skip(self))]
async fn register_checked(
&self,
executable: &Path,
expected_major: Option<u32>,
) -> JavaResult<JavaRuntime> {
let info = checker::check_java_runtime(executable.display().to_string()).await?;
let major = parse_major_version(&info.version)?;
if let Some(expected) = expected_major
&& major != expected
{
return Err(JavaError::VersionMismatch {
expected,
found: major,
});
}
self.persist(executable, &info).await
}
/// Records everything a filesystem scan turned up, skipping anything that
/// fails to probe. A broken JDK on the system should not abort the scan.
#[tracing::instrument(level = "debug", skip_all)]
async fn register_located(&self, located: &[(PathBuf, JavaCheckInfo)]) -> JavaResult<()> {
for (path, info) in located {
if let Err(err) = self.persist(path, info).await {
tracing::warn!(
path = %path.display(),
version = %info.version,
"skipping located Java runtime: {err}"
);
}
}
Ok(())
}
async fn persist(&self, executable: &Path, info: &JavaCheckInfo) -> JavaResult<JavaRuntime> {
let runtime = JavaRuntime {
absolute_path: executable.to_string_lossy().into_owned(),
major: parse_major_version(&info.version)?,
version: info.version.clone(),
vendor: JavaVendor::from_str(&info.vendor)
.unwrap_or_else(|_| JavaVendor::Other(info.vendor.clone())),
os_arch: info.os_arch.clone(),
};
Ok(self.store.upsert(&runtime).await?)
}
}
fn provider_for_vendor(vendor: &JavaVendor) -> Option<Box<dyn vendors::JavaRuntimeProvider>> {
vendors::runtime_providers()
.into_iter()
.find(|provider| &provider.vendor() == vendor)
}
/// Parses the major version out of a `java.version` string.
///
/// Java 8 and earlier report `1.8.0_412`, where the major is the *second*
/// component; 9 and later report `21.0.3`, where it is the first.
pub(crate) fn parse_major_version(version: &str) -> Result<u32, JavaError> {
if let Some(rest) = version.strip_prefix("1.") {
let digit = rest.chars().next().ok_or_else(|| JavaError::ParseVersion {
version: version.to_string(),
})?;
return digit.to_digit(10).ok_or_else(|| JavaError::ParseVersion {
version: version.to_string(),
});
}
let head = version.split('.').next().unwrap_or(version);
head.parse::<u32>().map_err(|_| JavaError::ParseVersion {
version: version.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn legacy_versions_take_the_second_component() {
assert_eq!(parse_major_version("1.8.0_412").unwrap(), 8);
assert_eq!(parse_major_version("1.7.0").unwrap(), 7);
}
#[test]
fn modern_versions_take_the_first() {
assert_eq!(parse_major_version("21.0.3").unwrap(), 21);
assert_eq!(parse_major_version("17").unwrap(), 17);
}
#[test]
fn garbage_is_rejected_rather_than_defaulted() {
assert!(parse_major_version("not-a-version").is_err());
assert!(parse_major_version("1.x").is_err());
}
}