Various final backend fixes (#117)

* Various final backend fixes

* Add FS watching

* run lint

* Autodetect installed jars
This commit is contained in:
Geometrically
2023-05-16 15:30:04 -07:00
committed by GitHub
parent 5cb54b44be
commit 3fa0e99de2
26 changed files with 941 additions and 529 deletions

View File

@@ -1,12 +1,16 @@
//! Authentication flow interface
use reqwest::Method;
use serde::Deserialize;
use std::path::PathBuf;
use crate::event::emit::{emit_loading, init_loading};
use crate::util::fetch::{fetch_advanced, fetch_json};
use crate::{
launcher::download,
prelude::Profile,
state::JavaGlobals,
util::jre::{self, extract_java_majorminor_version, JavaVersion},
State,
LoadingBarType, State,
};
pub const JAVA_8_KEY: &str = "JAVA_8";
@@ -133,6 +137,87 @@ pub async fn find_java17_jres() -> crate::Result<Vec<JavaVersion>> {
.collect())
}
pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
let state = State::get().await?;
let loading_bar = init_loading(
LoadingBarType::JavaDownload {
version: java_version,
},
100.0,
"Downloading java version",
)
.await?;
#[derive(Deserialize)]
struct Package {
pub download_url: String,
pub name: PathBuf,
}
emit_loading(&loading_bar, 0.0, Some("Fetching java version")).await?;
let packages = fetch_json::<Vec<Package>>(
Method::GET,
&format!(
"https://api.azul.com/metadata/v1/zulu/packages?arch={}&java_version={}&os={}&archive_type=zip&javafx_bundled=false&java_package_type=jre&page_size=1",
std::env::consts::ARCH, java_version, std::env::consts::OS
),
None,
None,
&state.fetch_semaphore,
).await?;
emit_loading(&loading_bar, 10.0, Some("Downloading java version")).await?;
if let Some(download) = packages.first() {
let file = fetch_advanced(
Method::GET,
&download.download_url,
None,
None,
None,
Some((&loading_bar, 80.0)),
&state.fetch_semaphore,
)
.await?;
let path = state.directories.java_versions_dir();
if path.exists() {
tokio::fs::remove_dir_all(&path).await?;
}
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(file))
.map_err(|_| {
crate::Error::from(crate::ErrorKind::InputError(
"Failed to read java zip".to_string(),
))
})?;
emit_loading(&loading_bar, 0.0, Some("Extracting java")).await?;
archive.extract(&path).map_err(|_| {
crate::Error::from(crate::ErrorKind::InputError(
"Failed to extract java zip".to_string(),
))
})?;
emit_loading(&loading_bar, 100.0, Some("Done extracting java")).await?;
Ok(path
.join(
download
.name
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
)
.join(format!("zulu-{}.jre/Contents/Home/bin/java", java_version)))
} else {
Err(crate::ErrorKind::LauncherError(format!(
"No Java Version found for Java version {}, OS {}, and Architecture {}",
java_version, std::env::consts::OS, std::env::consts::ARCH,
)).into())
}
}
// Get all JREs that exist on the system
pub async fn get_all_jre() -> crate::Result<Vec<JavaVersion>> {
Ok(jre::get_all_jre().await?)
@@ -148,3 +233,14 @@ pub async fn validate_globals() -> crate::Result<bool> {
pub async fn check_jre(path: PathBuf) -> crate::Result<Option<JavaVersion>> {
Ok(jre::check_java_at_filepath(&path).await)
}
// Gets maximum memory in KiB.
pub async fn get_max_memory() -> crate::Result<u64> {
Ok(sys_info::mem_info()
.map_err(|_| {
crate::Error::from(crate::ErrorKind::LauncherError(
"Unable to get computer memory".to_string(),
))
})?
.total)
}

View File

@@ -6,8 +6,7 @@ use crate::event::emit::{
};
use crate::event::{LoadingBarId, LoadingBarType};
use crate::state::{
LinkedData, ModrinthProject, ModrinthVersion, Profile, ProfileInstallStage,
SideType,
LinkedData, ModrinthProject, ModrinthVersion, ProfileInstallStage, SideType,
};
use crate::util::fetch::{
fetch, fetch_advanced, fetch_json, fetch_mirrors, write, write_cached_icon,
@@ -478,7 +477,6 @@ async fn install_pack(
if let Some(profile_val) =
crate::api::profile::get(&profile, None).await?
{
Profile::sync_projects_task(profile.clone());
crate::launcher::install_minecraft(
&profile_val,
Some(loading_bar),

View File

@@ -5,7 +5,6 @@ use crate::state::ProjectMetadata;
use crate::{
auth::{self, refresh},
event::{emit::emit_profile, ProfilePayloadType},
launcher::download,
state::MinecraftChild,
};
pub use crate::{
@@ -109,40 +108,10 @@ pub async fn list(
.collect())
}
/// Query + sync profile's projects with the UI from the FS
#[tracing::instrument]
pub async fn sync(path: &Path) -> crate::Result<()> {
Box::pin({
async move {
let state = State::get().await?;
let result = {
let mut profiles: tokio::sync::RwLockWriteGuard<
crate::state::Profiles,
> = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(path) {
profile.sync_projects().await?;
Ok(())
} else {
Err(crate::ErrorKind::UnmanagedProfileError(
path.display().to_string(),
)
.as_error())
}
};
State::sync().await?;
result
}
})
.await
}
/// Installs/Repairs a profile
#[tracing::instrument]
pub async fn install(path: &Path) -> crate::Result<()> {
let profile = get(path, None).await?;
if let Some(profile) = profile {
if let Some(profile) = get(path, None).await? {
crate::launcher::install_minecraft(&profile, None).await?;
} else {
return Err(crate::ErrorKind::UnmanagedProfileError(
@@ -155,11 +124,8 @@ pub async fn install(path: &Path) -> crate::Result<()> {
}
pub async fn update_all(profile_path: &Path) -> crate::Result<()> {
let state = State::get().await?;
Box::pin(async move {
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(profile_path) {
if let Some(profile) = get(profile_path, None).await? {
let loading_bar = init_loading(
LoadingBarType::ProfileUpdate {
profile_path: profile.path.clone(),
@@ -187,8 +153,6 @@ pub async fn update_all(profile_path: &Path) -> crate::Result<()> {
)
.await?;
profile.sync_projects().await?;
Ok(())
} else {
Err(crate::ErrorKind::UnmanagedProfileError(
@@ -204,10 +168,7 @@ pub async fn update_project(
profile_path: &Path,
project_path: &Path,
) -> crate::Result<PathBuf> {
let state = State::get().await?;
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(profile_path) {
if let Some(profile) = get(profile_path, None).await? {
if let Some(project) = profile.projects.get(project_path) {
if let ProjectMetadata::Modrinth {
update_version: Some(update_version),
@@ -222,15 +183,20 @@ pub async fn update_project(
profile.remove_project(project_path, Some(true)).await?;
}
let value = profile.projects.remove(project_path);
if let Some(mut project) = value {
if let ProjectMetadata::Modrinth {
ref mut version, ..
} = project.metadata
{
*version = Box::new(new_version);
let state = State::get().await?;
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(project_path) {
let value = profile.projects.remove(project_path);
if let Some(mut project) = value {
if let ProjectMetadata::Modrinth {
ref mut version,
..
} = project.metadata
{
*version = Box::new(new_version);
}
profile.projects.insert(path.clone(), project);
}
profile.projects.insert(path.clone(), project);
}
return Ok(path);
@@ -249,57 +215,15 @@ pub async fn update_project(
}
}
/// Replaces a project given a new version ID
pub async fn replace_project(
profile_path: &Path,
project: &Path,
version_id: String,
) -> crate::Result<PathBuf> {
let state = State::get().await?;
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(profile_path) {
let (path, new_version) =
profile.add_project_version(version_id).await?;
if path != project {
profile.remove_project(project, Some(true)).await?;
}
let value = profile.projects.remove(project);
if let Some(mut project) = value {
if let ProjectMetadata::Modrinth {
ref mut version, ..
} = project.metadata
{
*version = Box::new(new_version);
}
profile.projects.insert(path.clone(), project);
}
Ok(path)
} else {
Err(crate::ErrorKind::UnmanagedProfileError(
profile_path.display().to_string(),
)
.as_error())
}
}
/// Add a project from a version
#[tracing::instrument]
pub async fn add_project_from_version(
profile_path: &Path,
version_id: String,
) -> crate::Result<PathBuf> {
let state = State::get().await?;
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(profile_path) {
if let Some(profile) = get(profile_path, None).await? {
let (path, _) = profile.add_project_version(version_id).await?;
Profile::sync_projects_task(profile.path.clone());
Ok(path)
} else {
Err(crate::ErrorKind::UnmanagedProfileError(
@@ -316,10 +240,7 @@ pub async fn add_project_from_path(
path: &Path,
project_type: Option<String>,
) -> crate::Result<PathBuf> {
let state = State::get().await?;
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(profile_path) {
if let Some(profile) = get(profile_path, None).await? {
let file = fs::read(path).await?;
let file_name = path
.file_name()
@@ -335,8 +256,6 @@ pub async fn add_project_from_path(
)
.await?;
Profile::sync_projects_task(profile.path.clone());
Ok(path)
} else {
Err(crate::ErrorKind::UnmanagedProfileError(
@@ -352,10 +271,7 @@ pub async fn toggle_disable_project(
profile: &Path,
project: &Path,
) -> crate::Result<()> {
let state = State::get().await?;
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(profile) {
if let Some(profile) = get(profile, None).await? {
profile.toggle_disable_project(project).await?;
Ok(())
@@ -373,10 +289,7 @@ pub async fn remove_project(
profile: &Path,
project: &Path,
) -> crate::Result<()> {
let state = State::get().await?;
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(profile) {
if let Some(profile) = get(profile, None).await? {
profile.remove_project(project, None).await?;
Ok(())
@@ -423,7 +336,6 @@ pub async fn run_credentials(
Box::pin(async move {
let state = State::get().await?;
let settings = state.settings.read().await;
let metadata = state.metadata.read().await;
let profile = get(path, None).await?.ok_or_else(|| {
crate::ErrorKind::OtherError(format!(
"Tried to run a nonexistent or unloaded profile at path {}!",
@@ -431,25 +343,6 @@ pub async fn run_credentials(
))
})?;
let version = metadata
.minecraft
.versions
.iter()
.find(|it| it.id == profile.metadata.game_version)
.ok_or_else(|| {
crate::ErrorKind::LauncherError(format!(
"Invalid or unknown Minecraft version: {}",
profile.metadata.game_version
))
})?;
let version_info = download::download_version_info(
&state,
version,
profile.metadata.loader_version.as_ref(),
None,
None,
)
.await?;
let pre_launch_hooks =
&profile.hooks.as_ref().unwrap_or(&settings.hooks).pre_launch;
if let Some(hook) = pre_launch_hooks {
@@ -473,49 +366,6 @@ pub async fn run_credentials(
}
}
let java_version = match profile.java {
// Load profile-specific Java implementation choice
// (This defaults to Daedalus-decided key on init, but can be changed by the user)
Some(JavaSettings {
jre_key: Some(ref jre_key),
..
}) => settings.java_globals.get(jre_key),
// Fall back to Daedalus-decided key if no profile-specific key is set
_ => {
match version_info
.java_version
.as_ref()
.map(|it| it.major_version)
.unwrap_or(0)
{
0..=16 => settings
.java_globals
.get(&crate::jre::JAVA_8_KEY.to_string()),
17 => settings
.java_globals
.get(&crate::jre::JAVA_17_KEY.to_string()),
_ => settings
.java_globals
.get(&crate::jre::JAVA_18PLUS_KEY.to_string()),
}
}
};
let java_version = java_version.as_ref().ok_or_else(|| {
crate::ErrorKind::LauncherError(format!(
"No Java stored for version {}",
version_info.java_version.map_or(8, |it| it.major_version),
))
})?;
// Get the path to the Java executable from the chosen Java implementation key
let java_install: &Path = &PathBuf::from(&java_version.path);
if !java_install.exists() {
return Err(crate::ErrorKind::LauncherError(format!(
"Could not find Java install: {}",
java_install.display()
))
.as_error());
}
let java_args = profile
.java
.as_ref()
@@ -550,7 +400,6 @@ pub async fn run_credentials(
};
let mc_process = crate::launcher::launch_minecraft(
java_install,
java_args,
env_args,
wrapper,

View File

@@ -1,9 +1,7 @@
//! Theseus profile management interface
use crate::event::emit::emit_warning;
use crate::state::LinkedData;
use crate::{
event::{emit::emit_profile, ProfilePayloadType},
jre,
prelude::ModLoader,
};
pub use crate::{
@@ -84,7 +82,12 @@ pub async fn profile_create(
&canonicalize(&path)?.display()
);
let loader = if modloader != ModLoader::Vanilla {
get_loader_version_from_loader(game_version.clone(), modloader, loader_version).await?
get_loader_version_from_loader(
game_version.clone(),
modloader,
loader_version,
)
.await?
} else {
None
};
@@ -112,19 +115,6 @@ pub async fn profile_create(
profile.metadata.linked_data = linked_data;
// Attempts to find optimal JRE for the profile from the JavaGlobals
// Finds optimal key, and see if key has been set in JavaGlobals
let settings = state.settings.read().await;
let optimal_version_key = jre::get_optimal_jre_key(&profile).await?;
if settings.java_globals.get(&optimal_version_key).is_some() {
profile.java = Some(JavaSettings {
jre_key: Some(optimal_version_key),
extra_arguments: None,
});
} else {
emit_warning(&format!("Could not detect optimal JRE: {optimal_version_key}, falling back to system default.")).await?;
}
emit_profile(
uuid,
path.clone(),
@@ -144,7 +134,8 @@ pub async fn profile_create(
State::sync().await?;
Ok(path)
}).await
})
.await
}
pub(crate) async fn get_loader_version_from_loader(

View File

@@ -84,6 +84,9 @@ pub enum ErrorKind {
#[error("Error: {0}")]
OtherError(String),
#[error("File watching error: {0}")]
NotifyError(#[from] notify::Error),
}
#[derive(Debug)]

View File

@@ -149,6 +149,9 @@ impl Drop for LoadingBar {
#[serde(rename_all = "snake_case")]
pub enum LoadingBarType {
StateInit,
JavaDownload {
version: u32,
},
PackFileDownload {
profile_path: PathBuf,
pack_name: String,

View File

@@ -22,12 +22,13 @@ pub fn get_class_paths(
libraries_path: &Path,
libraries: &[Library],
client_path: &Path,
java_arch: &str,
) -> crate::Result<String> {
let mut cps = libraries
.iter()
.filter_map(|library| {
if let Some(rules) = &library.rules {
if !rules.iter().all(parse_rule) {
if !rules.iter().any(|x| parse_rule(x, java_arch)) {
return None;
}
}
@@ -53,19 +54,20 @@ pub fn get_class_paths(
.to_string(),
);
Ok(cps.join(classpath_separator()))
Ok(cps.join(classpath_separator(java_arch)))
}
pub fn get_class_paths_jar<T: AsRef<str>>(
libraries_path: &Path,
libraries: &[T],
java_arch: &str,
) -> crate::Result<String> {
let cps = libraries
.iter()
.map(|library| get_lib_path(libraries_path, library.as_ref(), false))
.collect::<Result<Vec<_>, _>>()?;
Ok(cps.join(classpath_separator()))
Ok(cps.join(classpath_separator(java_arch)))
}
pub fn get_lib_path(
@@ -91,6 +93,8 @@ pub fn get_lib_path(
Ok(path.to_string_lossy().to_string())
}
#[allow(clippy::too_many_arguments)]
pub fn get_jvm_arguments(
arguments: Option<&[Argument]>,
natives_path: &Path,
@@ -99,19 +103,26 @@ pub fn get_jvm_arguments(
version_name: &str,
memory: MemorySettings,
custom_args: Vec<String>,
java_arch: &str,
) -> crate::Result<Vec<String>> {
let mut parsed_arguments = Vec::new();
if let Some(args) = arguments {
parse_arguments(args, &mut parsed_arguments, |arg| {
parse_jvm_argument(
arg.to_string(),
natives_path,
libraries_path,
class_paths,
version_name,
)
})?;
parse_arguments(
args,
&mut parsed_arguments,
|arg| {
parse_jvm_argument(
arg.to_string(),
natives_path,
libraries_path,
class_paths,
version_name,
java_arch,
)
},
java_arch,
)?;
} else {
parsed_arguments.push(format!(
"-Djava.library.path={}",
@@ -147,6 +158,7 @@ fn parse_jvm_argument(
libraries_path: &Path,
class_paths: &str,
version_name: &str,
java_arch: &str,
) -> crate::Result<String> {
argument.retain(|c| !c.is_whitespace());
Ok(argument
@@ -174,7 +186,7 @@ fn parse_jvm_argument(
})?
.to_string_lossy(),
)
.replace("${classpath_separator}", classpath_separator())
.replace("${classpath_separator}", classpath_separator(java_arch))
.replace("${launcher_name}", "theseus")
.replace("${launcher_version}", env!("CARGO_PKG_VERSION"))
.replace("${version_name}", version_name)
@@ -192,13 +204,37 @@ pub fn get_minecraft_arguments(
assets_directory: &Path,
version_type: &VersionType,
resolution: WindowSize,
java_arch: &str,
) -> crate::Result<Vec<String>> {
if let Some(arguments) = arguments {
let mut parsed_arguments = Vec::new();
parse_arguments(arguments, &mut parsed_arguments, |arg| {
parse_minecraft_argument(
arg,
parse_arguments(
arguments,
&mut parsed_arguments,
|arg| {
parse_minecraft_argument(
arg,
&credentials.access_token,
&credentials.username,
&credentials.id,
version,
asset_index_name,
game_directory,
assets_directory,
version_type,
resolution,
)
},
java_arch,
)?;
Ok(parsed_arguments)
} else if let Some(legacy_arguments) = legacy_arguments {
let mut parsed_arguments = Vec::new();
for x in legacy_arguments.split(' ') {
parsed_arguments.push(parse_minecraft_argument(
&x.replace(' ', TEMPORARY_REPLACE_CHAR),
&credentials.access_token,
&credentials.username,
&credentials.id,
@@ -208,26 +244,9 @@ pub fn get_minecraft_arguments(
assets_directory,
version_type,
resolution,
)
})?;
)?);
}
Ok(parsed_arguments)
} else if let Some(legacy_arguments) = legacy_arguments {
Ok(parse_minecraft_argument(
&legacy_arguments.replace(' ', TEMPORARY_REPLACE_CHAR),
&credentials.access_token,
&credentials.username,
&credentials.id,
version,
asset_index_name,
game_directory,
assets_directory,
version_type,
resolution,
)?
.split(' ')
.map(|x| x.to_string())
.collect())
} else {
Ok(Vec::new())
}
@@ -300,6 +319,7 @@ fn parse_arguments<F>(
arguments: &[Argument],
parsed_arguments: &mut Vec<String>,
parse_function: F,
java_arch: &str,
) -> crate::Result<()>
where
F: Fn(&str) -> crate::Result<String>,
@@ -314,7 +334,7 @@ where
}
}
Argument::Ruled { rules, value } => {
if rules.iter().all(parse_rule) {
if rules.iter().any(|x| parse_rule(x, java_arch)) {
match value {
ArgumentValue::Single(arg) => {
parsed_arguments.push(parse_function(

View File

@@ -24,6 +24,7 @@ pub async fn download_minecraft(
st: &State,
version: &GameVersionInfo,
loading_bar: &LoadingBarId,
java_arch: &str,
) -> crate::Result<()> {
tracing::info!("Downloading Minecraft version {}", version.id);
// 5
@@ -45,7 +46,7 @@ pub async fn download_minecraft(
// Total loading sums to 90/60
download_client(st, version, Some(loading_bar)), // 10
download_assets(st, version.assets == "legacy", &assets_index, Some(loading_bar), amount), // 40
download_libraries(st, version.libraries.as_slice(), &version.id, Some(loading_bar), amount) // 40
download_libraries(st, version.libraries.as_slice(), &version.id, Some(loading_bar), amount, java_arch) // 40
}?;
tracing::info!("Done downloading Minecraft!");
@@ -247,6 +248,7 @@ pub async fn download_libraries(
version: &str,
loading_bar: Option<&LoadingBarId>,
loading_amount: f64,
java_arch: &str,
) -> crate::Result<()> {
Box::pin(async move {
tracing::debug!("Loading libraries");
@@ -260,10 +262,12 @@ pub async fn download_libraries(
stream::iter(libraries.iter())
.map(Ok::<&Library, crate::Error>), None, loading_bar,loading_amount,num_files, None,|library| async move {
if let Some(rules) = &library.rules {
if !rules.iter().all(super::parse_rule) {
if !rules.iter().any(|x| super::parse_rule(x, java_arch)) {
tracing::trace!("Skipped library {}", &library.name);
return Ok(());
}
}
tokio::try_join! {
async {
let artifact_path = d::get_path_from_artifact(&library.name)?;
@@ -278,24 +282,23 @@ pub async fn download_libraries(
let bytes = fetch(&artifact.url, Some(&artifact.sha1), &st.fetch_semaphore)
.await?;
write(&path, &bytes, &st.io_semaphore).await?;
tracing::trace!("Fetched library {}", &library.name);
tracing::trace!("Fetched library {} to path {:?}", &library.name, &path);
Ok::<_, crate::Error>(())
}
None => {
_ => {
let url = [
library
.url
.as_deref()
.unwrap_or("https://libraries.minecraft.net"),
.unwrap_or("https://libraries.minecraft.net/"),
&artifact_path
].concat();
let bytes = fetch(&url, None, &st.fetch_semaphore).await?;
write(&path, &bytes, &st.io_semaphore).await?;
tracing::trace!("Fetched library {}", &library.name);
tracing::trace!("Fetched library {} to path {:?}", &library.name, &path);
Ok::<_, crate::Error>(())
}
_ => Ok(())
}
},
async {
@@ -304,7 +307,7 @@ pub async fn download_libraries(
library
.natives
.as_ref()?
.get(&Os::native_arch())?,
.get(&Os::native_arch(java_arch))?,
library
.downloads
.as_ref()?

View File

@@ -1,6 +1,8 @@
//! Logic for launching Minecraft
use crate::event::emit::{emit_loading, init_or_edit_loading};
use crate::event::{LoadingBarId, LoadingBarType};
use crate::jre::{JAVA_17_KEY, JAVA_18PLUS_KEY, JAVA_8_KEY};
use crate::prelude::JavaVersion;
use crate::state::ProfileInstallStage;
use crate::{
process,
@@ -8,10 +10,11 @@ use crate::{
State,
};
use daedalus as d;
use daedalus::minecraft::VersionInfo;
use dunce::canonicalize;
use st::Profile;
use std::fs;
use std::{path::Path, process::Stdio, sync::Arc};
use std::{process::Stdio, sync::Arc};
use tokio::process::Command;
use uuid::Uuid;
@@ -21,18 +24,18 @@ pub mod auth;
pub mod download;
#[tracing::instrument]
pub fn parse_rule(rule: &d::minecraft::Rule) -> bool {
pub fn parse_rule(rule: &d::minecraft::Rule, java_version: &str) -> bool {
use d::minecraft::{Rule, RuleAction};
let res = match rule {
Rule {
os: Some(ref os), ..
} => crate::util::platform::os_rule(os),
} => crate::util::platform::os_rule(os, java_version),
Rule {
features: Some(ref features),
..
} => features.has_demo_resolution.unwrap_or(false),
_ => true,
_ => false,
};
match rule.action {
@@ -54,6 +57,40 @@ macro_rules! processor_rules {
}
}
async fn get_java_version_from_profile(
profile: &Profile,
version_info: &VersionInfo,
) -> crate::Result<JavaVersion> {
if let Some(java) = profile.java.clone().and_then(|x| x.override_version) {
Ok(java)
} else {
let optimal_keys = match version_info
.java_version
.as_ref()
.map(|it| it.major_version)
.unwrap_or(8)
{
0..=15 => vec![JAVA_8_KEY, JAVA_17_KEY, JAVA_18PLUS_KEY],
16..=17 => vec![JAVA_17_KEY, JAVA_18PLUS_KEY],
_ => vec![JAVA_18PLUS_KEY],
};
let state = State::get().await?;
let settings = state.settings.read().await;
for key in optimal_keys {
if let Some(java) = settings.java_globals.get(&key.to_string()) {
return Ok(java.clone());
}
}
Err(crate::ErrorKind::LauncherError(
"No available java installation".to_string(),
)
.into())
}
}
#[tracing::instrument(skip(profile))]
pub async fn install_minecraft(
profile: &Profile,
@@ -112,8 +149,10 @@ pub async fn install_minecraft(
)
.await?;
let java_version = get_java_version_from_profile(profile, &version_info).await?;
// Download minecraft (5-90)
download::download_minecraft(&state, &version_info, &loading_bar).await?;
download::download_minecraft(&state, &version_info, &loading_bar, &java_version.architecture).await?;
if let Some(processors) = &version_info.processors {
let client_path = state
@@ -157,11 +196,12 @@ pub async fn install_minecraft(
cp.push(processor.jar.clone())
});
let child = Command::new("java")
let child = Command::new(&java_version.path)
.arg("-cp")
.arg(args::get_class_paths_jar(
&state.directories.libraries_dir(),
&cp,
&java_version.architecture
)?)
.arg(
args::get_processor_main_class(args::get_lib_path(
@@ -231,7 +271,6 @@ pub async fn install_minecraft(
#[allow(clippy::too_many_arguments)]
pub async fn launch_minecraft(
java_install: &Path,
java_args: &[String],
env_args: &[(String, String)],
wrapper: &Option<String>,
@@ -286,6 +325,8 @@ pub async fn launch_minecraft(
)
.await?;
let java_version = get_java_version_from_profile(profile, &version_info).await?;
let client_path = state
.directories
.version_dir(&version_jar)
@@ -294,9 +335,9 @@ pub async fn launch_minecraft(
let args = version_info.arguments.clone().unwrap_or_default();
let mut command = match wrapper {
Some(hook) => {
wrap_ref_builder!(it = Command::new(hook) => {it.arg(java_install)})
wrap_ref_builder!(it = Command::new(hook) => {it.arg(&java_version.path)})
}
None => Command::new(String::from(java_install.to_string_lossy())),
None => Command::new(&java_version.path),
};
let env_args = Vec::from(env_args);
@@ -324,10 +365,12 @@ pub async fn launch_minecraft(
&state.directories.libraries_dir(),
version_info.libraries.as_slice(),
&client_path,
&java_version.architecture
)?,
&version_jar,
*memory,
Vec::from(java_args),
&java_version.architecture
)?
.into_iter()
.collect::<Vec<_>>(),
@@ -345,6 +388,7 @@ pub async fn launch_minecraft(
&state.directories.assets_dir(),
&version.type_,
*resolution,
&java_version.architecture
)?
.into_iter()
.collect::<Vec<_>>(),
@@ -362,7 +406,7 @@ pub async fn launch_minecraft(
// Get Modrinth logs directories
let datetime_string =
chrono::Local::now().format("%Y%m%y_%H%M%S").to_string();
chrono::Local::now().format("%Y%m%d_%H%M%S").to_string();
let logs_dir = {
let st = State::get().await?;
st.directories

View File

@@ -44,6 +44,12 @@ impl DirectoryInfo {
self.config_dir.join("meta")
}
/// Get the Minecraft java versions metadata directory
#[inline]
pub fn java_versions_dir(&self) -> PathBuf {
self.metadata_dir().join("java_versions")
}
/// Get the Minecraft versions metadata directory
#[inline]
pub fn versions_dir(&self) -> PathBuf {

View File

@@ -1,5 +1,6 @@
//! Theseus state management system
use crate::event::emit::emit_loading;
use std::path::PathBuf;
use crate::event::emit::init_loading;
use crate::event::LoadingBarType;
@@ -7,9 +8,14 @@ use crate::loading_join;
use crate::state::users::Users;
use crate::util::fetch::{FetchSemaphore, IoSemaphore};
use notify::RecommendedWatcher;
use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{OnceCell, RwLock, Semaphore};
use futures::{channel::mpsc::channel, SinkExt, StreamExt};
// Submodules
mod dirs;
pub use self::dirs::*;
@@ -69,6 +75,9 @@ pub struct State {
pub(crate) users: RwLock<Users>,
/// Launcher tags
pub(crate) tags: RwLock<Tags>,
/// File watcher debouncer
pub(crate) file_watcher: RwLock<Debouncer<RecommendedWatcher>>,
}
impl State {
@@ -84,6 +93,8 @@ impl State {
)
.await?;
let mut file_watcher = init_watcher().await?;
let directories = DirectoryInfo::init().await?;
emit_loading(&loading_bar, 10.0, None).await?;
@@ -100,7 +111,8 @@ impl State {
let metadata_fut =
Metadata::init(&directories, &io_semaphore);
let profiles_fut = Profiles::init(&directories);
let profiles_fut =
Profiles::init(&directories, &mut file_watcher);
let tags_fut = Tags::init(
&directories,
&io_semaphore,
@@ -137,6 +149,7 @@ impl State {
children: RwLock::new(children),
auth_flow: RwLock::new(auth_flow),
tags: RwLock::new(tags),
file_watcher: RwLock::new(file_watcher),
}))
}
})
@@ -220,3 +233,51 @@ impl State {
*io_semaphore = Semaphore::new(settings.max_concurrent_downloads);
}
}
async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
let (mut tx, mut rx) = channel(1);
let file_watcher = new_debouncer(
Duration::from_secs(2),
None,
move |res: DebounceEventResult| {
futures::executor::block_on(async {
tx.send(res).await.unwrap();
})
},
)?;
tokio::task::spawn(async move {
while let Some(res) = rx.next().await {
match res {
Ok(events) => {
let mut visited_paths = Vec::new();
events.iter().for_each(|e| {
let mut new_path = PathBuf::new();
let mut found = false;
for component in e.path.components() {
new_path.push(component);
if found {
break;
}
if component.as_os_str() == "profiles" {
found = true;
}
}
if !visited_paths.contains(&new_path) {
Profile::sync_projects_task(new_path.clone());
visited_paths.push(new_path);
}
});
}
Err(errors) => errors.iter().for_each(|err| {
tracing::warn!("Unable to watch file: {err}")
}),
}
}
});
Ok(file_watcher)
}

View File

@@ -3,6 +3,7 @@ use crate::config::MODRINTH_API_URL;
use crate::data::DirectoryInfo;
use crate::event::emit::emit_profile;
use crate::event::ProfilePayloadType;
use crate::prelude::JavaVersion;
use crate::state::projects::Project;
use crate::state::{ModrinthVersion, ProjectMetadata, ProjectType};
use crate::util::fetch::{
@@ -13,6 +14,8 @@ use daedalus::get_hash;
use daedalus::modded::LoaderVersion;
use dunce::canonicalize;
use futures::prelude::*;
use notify::{RecommendedWatcher, RecursiveMode};
use notify_debouncer_mini::Debouncer;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use std::io::Cursor;
@@ -124,7 +127,7 @@ impl ModLoader {
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct JavaSettings {
#[serde(skip_serializing_if = "Option::is_none")]
pub jre_key: Option<String>,
pub override_version: Option<JavaVersion>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extra_arguments: Option<Vec<String>>,
}
@@ -173,36 +176,10 @@ impl Profile {
semaphore: &IoSemaphore,
icon: bytes::Bytes,
file_name: &str,
) -> crate::Result<&'a mut Self> {
) -> crate::Result<()> {
let file =
write_cached_icon(file_name, cache_dir, icon, semaphore).await?;
self.metadata.icon = Some(file);
Ok(self)
}
pub async fn sync_projects(&mut self) -> crate::Result<()> {
let state = State::get().await?;
let paths = self.get_profile_project_paths()?;
let projects = crate::state::infer_data_from_files(
self.clone(),
paths,
state.directories.caches_dir(),
&state.io_semaphore,
&state.fetch_semaphore,
)
.await?;
self.projects = projects;
emit_profile(
self.uuid,
self.path.clone(),
&self.metadata.name,
ProfilePayloadType::Synced,
)
.await?;
Ok(())
}
@@ -228,6 +205,10 @@ impl Profile {
if let Some(profile) = new_profiles.0.get_mut(&path) {
profile.projects = projects;
}
} else {
tracing::warn!(
"Unable to fetch single profile projects: path {path:?} invalid",
);
}
Ok::<(), crate::Error>(())
@@ -268,8 +249,44 @@ impl Profile {
Ok(files)
}
pub async fn watch_fs(
profile_path: &Path,
watcher: &mut Debouncer<RecommendedWatcher>,
) -> crate::Result<()> {
async fn watch_path(
profile_path: &Path,
watcher: &mut Debouncer<RecommendedWatcher>,
path: &str,
) -> crate::Result<()> {
let path = profile_path.join(path);
fs::create_dir_all(&path).await?;
watcher
.watcher()
.watch(&profile_path.join(path), RecursiveMode::Recursive)?;
Ok(())
}
watch_path(profile_path, watcher, ProjectType::Mod.get_folder())
.await?;
watch_path(profile_path, watcher, ProjectType::ShaderPack.get_folder())
.await?;
watch_path(
profile_path,
watcher,
ProjectType::ResourcePack.get_folder(),
)
.await?;
watch_path(profile_path, watcher, ProjectType::DataPack.get_folder())
.await?;
Ok(())
}
pub async fn add_project_version(
&mut self,
&self,
version_id: String,
) -> crate::Result<(PathBuf, ModrinthVersion)> {
let state = State::get().await?;
@@ -314,7 +331,7 @@ impl Profile {
}
pub async fn add_project_bytes(
&mut self,
&self,
file_name: &str,
bytes: bytes::Bytes,
project_type: Option<ProjectType>,
@@ -354,16 +371,22 @@ impl Profile {
write(&path, &bytes, &state.io_semaphore).await?;
let hash = get_hash(bytes).await?;
{
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(&self.path) {
profile.projects.insert(
path.clone(),
Project {
sha512: hash,
disabled: false,
metadata: ProjectMetadata::Unknown,
file_name: file_name.to_string(),
},
);
}
}
self.projects.insert(
path.clone(),
Project {
sha512: hash,
disabled: false,
metadata: ProjectMetadata::Unknown,
file_name: file_name.to_string(),
},
);
emit_profile(
self.uuid,
self.path.clone(),
@@ -376,10 +399,19 @@ impl Profile {
}
pub async fn toggle_disable_project(
&mut self,
&self,
path: &Path,
) -> crate::Result<()> {
if let Some(mut project) = self.projects.remove(path) {
let state = State::get().await?;
if let Some(mut project) = {
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(&self.path) {
profile.projects.remove(path)
} else {
None
}
} {
let path = path.to_path_buf();
let mut new_path = path.clone();
@@ -395,7 +427,10 @@ impl Profile {
fs::rename(path, &new_path).await?;
self.projects.insert(new_path, project);
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(&self.path) {
profile.projects.insert(new_path, project);
}
} else {
return Err(crate::ErrorKind::InputError(format!(
"Project path does not exist: {:?}",
@@ -408,14 +443,19 @@ impl Profile {
}
pub async fn remove_project(
&mut self,
&self,
path: &Path,
dont_remove_arr: Option<bool>,
) -> crate::Result<()> {
let state = State::get().await?;
if self.projects.contains_key(path) {
fs::remove_file(path).await?;
if !dont_remove_arr.unwrap_or(false) {
self.projects.remove(path);
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(&self.path) {
profile.projects.remove(path);
}
}
} else {
return Err(crate::ErrorKind::InputError(format!(
@@ -430,8 +470,11 @@ impl Profile {
}
impl Profiles {
#[tracing::instrument]
pub async fn init(dirs: &DirectoryInfo) -> crate::Result<Self> {
#[tracing::instrument(skip(file_watcher))]
pub async fn init(
dirs: &DirectoryInfo,
file_watcher: &mut Debouncer<RecommendedWatcher>,
) -> crate::Result<Self> {
let mut profiles = HashMap::new();
fs::create_dir_all(dirs.profiles_dir()).await?;
let mut entries = fs::read_dir(dirs.profiles_dir()).await?;
@@ -449,6 +492,7 @@ impl Profiles {
};
if let Some(profile) = prof {
let path = canonicalize(path)?;
Profile::watch_fs(&path, file_watcher).await?;
profiles.insert(path, profile);
}
}
@@ -517,6 +561,11 @@ impl Profiles {
ProfilePayloadType::Added,
)
.await?;
let state = State::get().await?;
let mut file_watcher = state.file_watcher.write().await;
Profile::watch_fs(&profile.path, &mut file_watcher).await?;
self.0.insert(
canonicalize(&profile.path)?
.to_str()
@@ -529,15 +578,6 @@ impl Profiles {
Ok(self)
}
#[tracing::instrument(skip(self))]
pub async fn insert_from<'a>(
&'a mut self,
path: &'a Path,
) -> crate::Result<&Self> {
self.insert(Self::read_profile_from_dir(&canonicalize(path)?).await?)
.await
}
#[tracing::instrument(skip(self))]
pub async fn remove(
&mut self,

View File

@@ -1,14 +1,15 @@
use dunce::canonicalize;
use futures::prelude::*;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::env;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use std::{collections::HashSet, path::Path};
use tempfile::NamedTempFile;
use tokio::task::JoinError;
use crate::State;
#[cfg(target_os = "windows")]
use winreg::{
enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_32KEY, KEY_WOW64_64KEY},
@@ -19,6 +20,7 @@ use winreg::{
pub struct JavaVersion {
pub path: String,
pub version: String,
pub architecture: String,
}
// Entrypoint function (Windows)
@@ -30,6 +32,7 @@ pub async fn get_all_jre() -> Result<Vec<JavaVersion>, JREError> {
// Add JRES directly on PATH
jre_paths.extend(get_all_jre_path().await?);
jre_paths.extend(get_all_autoinstalled_jre_path().await?);
// Hard paths for locations for commonly installed .exes
let java_paths = [r"C:/Program Files/Java", r"C:/Program Files (x86)/Java"];
@@ -110,6 +113,7 @@ pub async fn get_all_jre() -> Result<Vec<JavaVersion>, JREError> {
// Add JREs directly on PATH
jre_paths.extend(get_all_jre_path().await?);
jre_paths.extend(get_all_autoinstalled_jre_path().await?);
// Hard paths for locations for commonly installed .exes
let java_paths = [
@@ -128,6 +132,7 @@ pub async fn get_all_jre() -> Result<Vec<JavaVersion>, JREError> {
jre_paths.insert(entry);
}
}
// Get JRE versions from potential paths concurrently
let j = check_java_at_filepaths(jre_paths)
.await?
@@ -146,6 +151,7 @@ pub async fn get_all_jre() -> Result<Vec<JavaVersion>, JREError> {
// Add JREs directly on PATH
jre_paths.extend(get_all_jre_path().await?);
jre_paths.extend(get_all_autoinstalled_jre_path().await?);
// Hard paths for locations for commonly installed locations
let java_paths = [
@@ -177,6 +183,30 @@ pub async fn get_all_jre() -> Result<Vec<JavaVersion>, JREError> {
Ok(j)
}
// Gets all JREs from the PATH env variable
#[tracing::instrument]
async fn get_all_autoinstalled_jre_path() -> Result<HashSet<PathBuf>, JREError>
{
let state = State::get().await.map_err(|_| JREError::StateError)?;
let mut jre_paths = HashSet::new();
let base_path = state.directories.java_versions_dir();
if base_path.is_dir() {
for entry in std::fs::read_dir(base_path)? {
let entry = entry?;
let file_path = entry.path().join("bin");
let contents = std::fs::read_to_string(file_path)?;
let entry = entry.path().join(contents);
println!("{:?}", entry);
jre_paths.insert(entry);
}
}
Ok(jre_paths)
}
// Gets all JREs from the PATH env variable
#[tracing::instrument]
async fn get_all_jre_path() -> Result<HashSet<PathBuf>, JREError> {
@@ -231,26 +261,49 @@ pub async fn check_java_at_filepath(path: &Path) -> Option<JavaVersion> {
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);
let mut file = NamedTempFile::new().ok()?;
file.write_all(include_bytes!("../../library/JavaInfo.class"))
.ok()?;
// Match: version "1.8.0_361"
// Extracting version numbers
lazy_static! {
static ref JAVA_VERSION_CAPTURE: Regex =
Regex::new(r#"version "([\d\._]+)""#)
.expect("Error creating java version capture regex");
let original_path = file.path().to_path_buf();
let mut new_path = original_path.clone();
new_path.set_file_name("JavaInfo");
new_path.set_extension("class");
tokio::fs::rename(&original_path, &new_path).await.ok()?;
// Run java checker on java binary
let output = Command::new(&java)
.arg("-cp")
.arg(file.path().parent().unwrap())
.arg("JavaInfo")
.output()
.ok()?;
let stdout = String::from_utf8_lossy(&output.stdout);
let mut java_version = None;
let mut java_arch = None;
for line in stdout.lines() {
let mut parts = line.split('=');
let key = parts.next().unwrap_or_default();
let value = parts.next().unwrap_or_default();
if key == "os.arch" {
java_arch = Some(value);
} else if key == "java.version" {
java_version = Some(value);
}
}
// Extract version info from it
if let Some(captures) = JAVA_VERSION_CAPTURE.captures(&stderr) {
if let Some(version) = captures.get(1) {
let Some(path) = java.to_str() else { return None };
let path = path.to_string();
if let Some(arch) = java_arch {
if let Some(version) = java_version {
let path = java.to_string_lossy().to_string();
return Some(JavaVersion {
path,
version: version.as_str().to_string(),
version: version.to_string(),
architecture: arch.to_string(),
});
}
}
@@ -311,6 +364,9 @@ pub enum JREError {
#[error("No stored tag for Minecraft Version {0}")]
NoMinecraftVersionFound(String),
#[error("Error getting launcher sttae")]
StateError,
}
#[cfg(test)]

View File

@@ -8,27 +8,27 @@ pub trait OsExt {
fn native() -> Self;
/// Gets the OS + Arch of the current system
fn native_arch() -> Self;
fn native_arch(java_arch: &str) -> Self;
}
impl OsExt for Os {
fn native_arch() -> Self {
fn native_arch(java_arch: &str) -> Self {
if std::env::consts::OS == "windows" {
if std::env::consts::ARCH == "aarch64" {
if java_arch == "aarch64" {
Os::WindowsArm64
} else {
Os::Windows
}
} else if std::env::consts::OS == "linux" {
if std::env::consts::ARCH == "aarch64" {
if java_arch == "aarch64" {
Os::LinuxArm64
} else if std::env::consts::ARCH == "arm" {
} else if java_arch == "arm" {
Os::LinuxArm32
} else {
Os::Linux
}
} else if std::env::consts::OS == "macos" {
if std::env::consts::ARCH == "aarch64" {
if java_arch == "aarch64" {
Os::OsxArm64
} else {
Os::Osx
@@ -39,30 +39,6 @@ impl OsExt for Os {
}
fn native() -> Self {
if std::env::consts::OS == "windows" {
if std::env::consts::ARCH == "aarch64" {
Os::WindowsArm64
} else {
Os::Windows
}
} else if std::env::consts::OS == "linux" {
if std::env::consts::ARCH == "aarch64" {
Os::LinuxArm64
} else if std::env::consts::ARCH == "arm" {
Os::LinuxArm32
} else {
Os::Linux
}
} else if std::env::consts::OS == "macos" {
if std::env::consts::ARCH == "aarch64" {
Os::OsxArm64
} else {
Os::Osx
}
} else {
Os::Unknown
};
match std::env::consts::OS {
"windows" => Self::Windows,
"macos" => Self::Osx,
@@ -80,7 +56,7 @@ pub const ARCH_WIDTH: &str = "64";
pub const ARCH_WIDTH: &str = "32";
// Platform rule handling
pub fn os_rule(rule: &OsRule) -> bool {
pub fn os_rule(rule: &OsRule, java_arch: &str) -> bool {
let mut rule_match = true;
if let Some(ref arch) = rule.arch {
@@ -88,7 +64,8 @@ pub fn os_rule(rule: &OsRule) -> bool {
}
if let Some(name) = &rule.name {
rule_match &= &Os::native() == name || &Os::native_arch() == name;
rule_match &=
&Os::native() == name || &Os::native_arch(java_arch) == name;
}
if let Some(version) = &rule.version {
@@ -101,8 +78,8 @@ pub fn os_rule(rule: &OsRule) -> bool {
rule_match
}
pub fn classpath_separator() -> &'static str {
match Os::native_arch() {
pub fn classpath_separator(java_arch: &str) -> &'static str {
match Os::native_arch(java_arch) {
Os::Osx
| Os::OsxArm64
| Os::Linux