Refactor Library

The launcher code was in a position ripe for sphagetti, so this rewrites it in a more robust way.
In addition to cleaner code, this provides the following changes:
- Removal of obsolete Mojang authentication
- The rebasing of some internal state into a Sled database
- Tweaks which make some internal mechanisms more robust (e.g. profiles which fail to load can be removed)
- Additional tooling integration such as direnv
- Distinct public API to avoid messing with too much internal code
- Unified error handling in the form of `theseus::Error` and `theseus::Result`
This commit is contained in:
Danielle
2022-06-27 15:53:25 -07:00
committed by GitHub
parent 179dcdcd04
commit 10610e157f
37 changed files with 2730 additions and 4117 deletions

View File

@@ -1,32 +1,29 @@
use crate::data::profiles::*;
use crate::launcher::auth::provider::Credentials;
use crate::launcher::rules::parse_rules;
use crate::launcher::LauncherError;
use daedalus::get_path_from_artifact;
use daedalus::minecraft::{Argument, ArgumentValue, Library, Os, VersionType};
use daedalus::modded::SidedDataEntry;
use std::collections::HashMap;
//! Minecraft CLI argument logic
// TODO: Rafactor this section
use super::{auth::Credentials, parse_rule};
use crate::{
state::{MemorySettings, WindowSize},
util::platform::classpath_separator,
};
use daedalus::{
get_path_from_artifact,
minecraft::{Argument, ArgumentValue, Library, VersionType},
modded::SidedDataEntry,
};
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::{collections::HashMap, path::Path};
use uuid::Uuid;
fn get_cp_separator() -> &'static str {
match super::download::get_os() {
Os::Osx | Os::Linux | Os::Unknown => ":",
Os::Windows => ";",
}
}
pub fn get_class_paths(
libraries_path: &Path,
libraries: &[Library],
client_path: &Path,
) -> Result<String, LauncherError> {
let mut class_paths = libraries
) -> crate::Result<String> {
let mut cps = libraries
.iter()
.filter_map(|library| {
if let Some(rules) = &library.rules {
if !super::rules::parse_rules(rules.as_slice()) {
if !rules.iter().all(parse_rule) {
return None;
}
}
@@ -39,10 +36,11 @@ pub fn get_class_paths(
})
.collect::<Result<Vec<_>, _>>()?;
class_paths.push(
crate::util::absolute_path(&client_path)
cps.push(
client_path
.canonicalize()
.map_err(|_| {
LauncherError::InvalidInput(format!(
crate::Error::LauncherError(format!(
"Specified class path {} does not exist",
client_path.to_string_lossy()
))
@@ -51,44 +49,35 @@ pub fn get_class_paths(
.to_string(),
);
Ok(class_paths.join(get_cp_separator()))
Ok(cps.join(classpath_separator()))
}
pub fn get_class_paths_jar<T: AsRef<str>>(
libraries_path: &Path,
libraries: &[T],
) -> Result<String, LauncherError> {
let class_paths = libraries
) -> crate::Result<String> {
let cps = libraries
.iter()
.map(|library| get_lib_path(libraries_path, library.as_ref()))
.collect::<Result<Vec<_>, _>>()?;
Ok(class_paths.join(get_cp_separator()))
Ok(cps.join(classpath_separator()))
}
pub fn get_lib_path(libraries_path: &Path, lib: &str) -> Result<String, LauncherError> {
pub fn get_lib_path(libraries_path: &Path, lib: &str) -> crate::Result<String> {
let mut path = libraries_path.to_path_buf();
path.push(get_path_from_artifact(lib.as_ref())?);
let path = crate::util::absolute_path(&path).map_err(|_| {
LauncherError::InvalidInput(format!(
let path = &path.canonicalize().map_err(|_| {
crate::Error::LauncherError(format!(
"Library file at path {} does not exist",
path.to_string_lossy()
))
})?;
/*if !path.exists() {
if let Some(parent) = &path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::File::create(&path)?;
}*/
Ok(path.to_string_lossy().to_string())
}
pub fn get_jvm_arguments(
arguments: Option<&[Argument]>,
natives_path: &Path,
@@ -97,7 +86,7 @@ pub fn get_jvm_arguments(
version_name: &str,
memory: MemorySettings,
custom_args: Vec<String>,
) -> Result<Vec<String>, LauncherError> {
) -> crate::Result<Vec<String>> {
let mut parsed_arguments = Vec::new();
if let Some(args) = arguments {
@@ -113,8 +102,9 @@ pub fn get_jvm_arguments(
} else {
parsed_arguments.push(format!(
"-Djava.library.path={}",
&crate::util::absolute_path(natives_path)
.map_err(|_| LauncherError::InvalidInput(format!(
&natives_path
.canonicalize()
.map_err(|_| crate::Error::LauncherError(format!(
"Specified natives path {} does not exist",
natives_path.to_string_lossy()
)))?
@@ -144,14 +134,15 @@ fn parse_jvm_argument(
libraries_path: &Path,
class_paths: &str,
version_name: &str,
) -> Result<String, LauncherError> {
) -> crate::Result<String> {
argument.retain(|c| !c.is_whitespace());
Ok(argument
.replace(
"${natives_directory}",
&crate::util::absolute_path(natives_path)
&natives_path
.canonicalize()
.map_err(|_| {
LauncherError::InvalidInput(format!(
crate::Error::LauncherError(format!(
"Specified natives path {} does not exist",
natives_path.to_string_lossy()
))
@@ -160,9 +151,10 @@ fn parse_jvm_argument(
)
.replace(
"${library_directory}",
&crate::util::absolute_path(libraries_path)
&libraries_path
.canonicalize()
.map_err(|_| {
LauncherError::InvalidInput(format!(
crate::Error::LauncherError(format!(
"Specified libraries path {} does not exist",
libraries_path.to_string_lossy()
))
@@ -170,7 +162,7 @@ fn parse_jvm_argument(
.to_string_lossy()
.to_string(),
)
.replace("${classpath_separator}", get_cp_separator())
.replace("${classpath_separator}", classpath_separator())
.replace("${launcher_name}", "theseus")
.replace("${launcher_version}", env!("CARGO_PKG_VERSION"))
.replace("${version_name}", version_name)
@@ -188,7 +180,7 @@ pub fn get_minecraft_arguments(
assets_directory: &Path,
version_type: &VersionType,
resolution: WindowSize,
) -> Result<Vec<String>, LauncherError> {
) -> crate::Result<Vec<String>> {
if let Some(arguments) = arguments {
let mut parsed_arguments = Vec::new();
@@ -242,7 +234,7 @@ fn parse_minecraft_argument(
assets_directory: &Path,
version_type: &VersionType,
resolution: WindowSize,
) -> Result<String, LauncherError> {
) -> crate::Result<String> {
Ok(argument
.replace("${auth_access_token}", access_token)
.replace("${auth_session}", access_token)
@@ -254,9 +246,10 @@ fn parse_minecraft_argument(
.replace("${assets_index_name}", asset_index_name)
.replace(
"${game_directory}",
&crate::util::absolute_path(game_directory)
&game_directory
.canonicalize()
.map_err(|_| {
LauncherError::InvalidInput(format!(
crate::Error::LauncherError(format!(
"Specified game directory {} does not exist",
game_directory.to_string_lossy()
))
@@ -266,9 +259,10 @@ fn parse_minecraft_argument(
)
.replace(
"${assets_root}",
&crate::util::absolute_path(assets_directory)
&assets_directory
.canonicalize()
.map_err(|_| {
LauncherError::InvalidInput(format!(
crate::Error::LauncherError(format!(
"Specified assets directory {} does not exist",
assets_directory.to_string_lossy()
))
@@ -278,9 +272,10 @@ fn parse_minecraft_argument(
)
.replace(
"${game_assets}",
&crate::util::absolute_path(assets_directory)
&assets_directory
.canonicalize()
.map_err(|_| {
LauncherError::InvalidInput(format!(
crate::Error::LauncherError(format!(
"Specified assets directory {} does not exist",
assets_directory.to_string_lossy()
))
@@ -297,9 +292,9 @@ fn parse_arguments<F>(
arguments: &[Argument],
parsed_arguments: &mut Vec<String>,
parse_function: F,
) -> Result<(), LauncherError>
) -> crate::Result<()>
where
F: Fn(&str) -> Result<String, LauncherError>,
F: Fn(&str) -> crate::Result<String>,
{
for argument in arguments {
match argument {
@@ -311,7 +306,7 @@ where
}
}
Argument::Ruled { rules, value } => {
if parse_rules(rules.as_slice()) {
if rules.iter().all(parse_rule) {
match value {
ArgumentValue::Single(arg) => {
parsed_arguments.push(parse_function(arg)?);
@@ -334,7 +329,7 @@ pub fn get_processor_arguments<T: AsRef<str>>(
libraries_path: &Path,
arguments: &[T],
data: &HashMap<String, SidedDataEntry>,
) -> Result<Vec<String>, LauncherError> {
) -> crate::Result<Vec<String>> {
let mut new_arguments = Vec::new();
for argument in arguments {
@@ -342,7 +337,10 @@ pub fn get_processor_arguments<T: AsRef<str>>(
if argument.as_ref().starts_with('{') {
if let Some(entry) = data.get(trimmed_arg) {
new_arguments.push(if entry.client.starts_with('[') {
get_lib_path(libraries_path, &entry.client[1..entry.client.len() - 1])?
get_lib_path(
libraries_path,
&entry.client[1..entry.client.len() - 1],
)?
} else {
entry.client.clone()
})
@@ -357,15 +355,23 @@ pub fn get_processor_arguments<T: AsRef<str>>(
Ok(new_arguments)
}
pub async fn get_processor_main_class(path: String) -> Result<Option<String>, LauncherError> {
pub async fn get_processor_main_class(
path: String,
) -> crate::Result<Option<String>> {
Ok(tokio::task::spawn_blocking(move || {
let zipfile = std::fs::File::open(&path)?;
let mut archive = zip::ZipArchive::new(zipfile).map_err(|_| {
LauncherError::ProcessorError(format!("Cannot read processor at {}", path))
crate::Error::LauncherError(format!(
"Cannot read processor at {}",
path
))
})?;
let file = archive.by_name("META-INF/MANIFEST.MF").map_err(|_| {
LauncherError::ProcessorError(format!("Cannot read processor manifest at {}", path))
crate::Error::LauncherError(format!(
"Cannot read processor manifest at {}",
path
))
})?;
let reader = BufReader::new(file);
@@ -381,7 +387,8 @@ pub async fn get_processor_main_class(path: String) -> Result<Option<String>, La
}
}
Ok::<Option<String>, LauncherError>(None)
Ok::<Option<String>, crate::Error>(None)
})
.await??)
.await
.unwrap()?)
}

View File

@@ -1,205 +1,8 @@
pub mod api {
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize)]
pub struct GameProfile {
pub id: Uuid,
pub name: String,
}
#[derive(Debug, Deserialize)]
pub struct UserProperty {
pub name: String,
pub value: String,
}
#[derive(Debug, Deserialize)]
pub struct User {
pub id: String,
pub username: String,
pub properties: Option<Vec<UserProperty>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthenticateResponse {
pub user: Option<User>,
pub client_token: Uuid,
pub access_token: String,
pub available_profiles: Vec<GameProfile>,
pub selected_profile: Option<GameProfile>,
}
pub async fn login(
username: &str,
password: &str,
request_user: bool,
) -> Result<AuthenticateResponse, reqwest::Error> {
let client = reqwest::Client::new();
client
.post("https://authserver.mojang.com/authenticate")
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(
serde_json::json!(
{
"agent": {
"name": "Minecraft",
"version": 1
},
"username": username,
"password": password,
"clientToken": Uuid::new_v4(),
"requestUser": request_user
}
)
.to_string(),
)
.send()
.await?
.json()
.await
}
pub async fn sign_out(username: &str, password: &str) -> Result<(), reqwest::Error> {
let client = reqwest::Client::new();
client
.post("https://authserver.mojang.com/signout")
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(
serde_json::json!(
{
"username": username,
"password": password
}
)
.to_string(),
)
.send()
.await?;
Ok(())
}
pub async fn validate(access_token: &str, client_token: &str) -> Result<(), reqwest::Error> {
let client = reqwest::Client::new();
client
.post("https://authserver.mojang.com/validate")
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(
serde_json::json!(
{
"accessToken": access_token,
"clientToken": client_token
}
)
.to_string(),
)
.send()
.await?;
Ok(())
}
pub async fn invalidate(access_token: &str, client_token: &str) -> Result<(), reqwest::Error> {
let client = reqwest::Client::new();
client
.post("https://authserver.mojang.com/invalidate")
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(
serde_json::json!(
{
"accessToken": access_token,
"clientToken": client_token
}
)
.to_string(),
)
.send()
.await?;
Ok(())
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RefreshResponse {
pub user: Option<User>,
pub client_token: Uuid,
pub access_token: String,
pub selected_profile: Option<GameProfile>,
}
pub async fn refresh(
access_token: &str,
client_token: &str,
selected_profile: &GameProfile,
request_user: bool,
) -> Result<RefreshResponse, reqwest::Error> {
let client = reqwest::Client::new();
client
.post("https://authserver.mojang.com/refresh")
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(
serde_json::json!(
{
"accessToken": access_token,
"clientToken": client_token,
"selectedProfile": {
"id": selected_profile.id,
"name": selected_profile.name,
},
"requestUser": request_user,
}
)
.to_string(),
)
.send()
.await?
.json()
.await
}
}
pub mod provider {
use crate::launcher::auth::api::login;
use crate::launcher::LauncherError;
use uuid::Uuid;
#[derive(Debug)]
/// The credentials of a user
pub struct Credentials {
/// The user UUID the credentials belong to
pub id: Uuid,
/// The username of the user
pub username: String,
/// The access token associated with the credentials
pub access_token: String,
}
impl Credentials {
/// Gets a credentials instance from a user's login
pub async fn from_login(username: &str, password: &str) -> Result<Self, LauncherError> {
let login =
login(username, password, true)
.await
.map_err(|err| LauncherError::FetchError {
inner: err,
item: "authentication credentials".to_string(),
})?;
let profile = login.selected_profile.unwrap();
Ok(Credentials {
id: profile.id,
username: profile.name,
access_token: login.access_token,
})
}
}
//! Authentication flow
// TODO: Implement authentication
#[derive(Debug)]
pub struct Credentials {
pub id: uuid::Uuid,
pub username: String,
pub access_token: String,
}

View File

@@ -1,362 +1,282 @@
//! Downloader for Minecraft data
use crate::{
data::{DataError, Settings},
launcher::LauncherError,
state::State,
util::{fetch::*, platform::OsExt},
};
use daedalus::get_path_from_artifact;
use daedalus::minecraft::{
fetch_assets_index, fetch_version_info, Asset, AssetsIndex, DownloadType,
Library, Os, Version, VersionInfo,
};
use daedalus::modded::{
fetch_partial_version, merge_partial_version, LoaderVersion,
};
use futures::future;
use std::path::Path;
use std::time::Duration;
use tokio::{
fs::File,
io::AsyncWriteExt,
sync::{OnceCell, Semaphore},
use daedalus::{
self as d,
minecraft::{
Asset, AssetsIndex, Library, Os, Version as GameVersion,
VersionInfo as GameVersionInfo,
},
modded::LoaderVersion,
};
use futures::prelude::*;
use std::sync::Arc;
use tokio::{fs, sync::OnceCell};
static DOWNLOADS_SEMAPHORE: OnceCell<Semaphore> = OnceCell::const_new();
pub async fn download_minecraft(
st: &State,
version: &GameVersionInfo,
) -> crate::Result<()> {
log::info!("Downloading Minecraft version {}", version.id);
let assets_index = download_assets_index(st, version).await?;
pub async fn init() -> Result<(), DataError> {
DOWNLOADS_SEMAPHORE
.get_or_try_init(|| async {
let settings = Settings::get().await?;
Ok::<_, DataError>(Semaphore::new(
settings.max_concurrent_downloads,
))
})
.await?;
tokio::try_join! {
download_client(st, version),
download_assets(st, version.assets == "legacy", &assets_index),
download_libraries(st, version.libraries.as_slice(), &version.id)
}?;
log::info!("Done downloading Minecraft!");
Ok(())
}
pub async fn download_version_info(
client_path: &Path,
version: &Version,
loader_version: Option<&LoaderVersion>,
) -> Result<VersionInfo, LauncherError> {
let id = match loader_version {
Some(x) => &x.id,
None => &version.id,
};
st: &State,
version: &GameVersion,
loader: Option<&LoaderVersion>,
) -> crate::Result<GameVersionInfo> {
let version_id = loader.map_or(&version.id, |it| &it.id);
log::debug!("Loading version info for Minecraft {version_id}");
let path = st
.directories
.version_dir(version_id)
.join(format!("{version_id}.json"));
let mut path = client_path.join(id);
path.push(&format!("{id}.json"));
if path.exists() {
let contents = std::fs::read_to_string(path)?;
Ok(serde_json::from_str(&contents)?)
let res = if path.exists() {
fs::read(path)
.err_into::<crate::Error>()
.await
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
} else {
let mut info = fetch_version_info(version).await?;
log::info!("Downloading version info for version {}", &version.id);
let mut info = d::minecraft::fetch_version_info(version).await?;
if let Some(loader_version) = loader_version {
let partial = fetch_partial_version(&loader_version.url).await?;
info = merge_partial_version(partial, info);
info.id = loader_version.id.clone();
if let Some(loader) = loader {
let partial = d::modded::fetch_partial_version(&loader.url).await?;
info = d::modded::merge_partial_version(partial, info);
info.id = loader.id.clone();
}
let info_s = serde_json::to_string(&info)?;
save_file(&path, &bytes::Bytes::from(info_s)).await?;
let permit = st.io_semaphore.acquire().await.unwrap();
write(&path, &serde_json::to_vec(&info)?, &permit).await?;
Ok(info)
}
}?;
log::debug!("Loaded version info for Minecraft {version_id}");
Ok(res)
}
pub async fn download_client(
client_path: &Path,
version_info: &VersionInfo,
) -> Result<(), LauncherError> {
let version = &version_info.id;
st: &State,
version_info: &GameVersionInfo,
) -> crate::Result<()> {
let ref version = version_info.id;
log::debug!("Locating client for version {version}");
let client_download = version_info
.downloads
.get(&DownloadType::Client)
.ok_or_else(|| {
LauncherError::InvalidInput(format!(
"Version {version} does not have any client downloads"
))
})?;
.get(&d::minecraft::DownloadType::Client)
.ok_or(crate::Error::LauncherError(format!(
"No client downloads exist for version {version}"
)))?;
let path = st
.directories
.version_dir(version)
.join(format!("{version}.jar"));
let mut path = client_path.join(version);
path.push(&format!("{version}.jar"));
if !path.exists() {
let permit = st.io_semaphore.acquire().await.unwrap();
let bytes =
fetch(&client_download.url, Some(&client_download.sha1), &permit)
.await?;
write(&path, &bytes, &permit).await?;
log::info!("Fetched client version {version}");
}
save_and_download_file(
&path,
&client_download.url,
Some(&client_download.sha1),
)
.await?;
log::debug!("Client loaded for version {version}!");
Ok(())
}
pub async fn download_assets_index(
assets_path: &Path,
version: &VersionInfo,
) -> Result<AssetsIndex, LauncherError> {
let path =
assets_path.join(format!("indexes/{}.json", &version.asset_index.id));
st: &State,
version: &GameVersionInfo,
) -> crate::Result<AssetsIndex> {
log::debug!("Loading assets index");
let path = st
.directories
.assets_index_dir()
.join(format!("{}.json", &version.asset_index.id));
if path.exists() {
let content = std::fs::read_to_string(path)?;
Ok(serde_json::from_str(&content)?)
let res = if path.exists() {
fs::read(path)
.err_into::<crate::Error>()
.await
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
} else {
let index = fetch_assets_index(version).await?;
save_file(&path, &bytes::Bytes::from(serde_json::to_string(&index)?))
.await?;
let index = d::minecraft::fetch_assets_index(version).await?;
let permit = st.io_semaphore.acquire().await.unwrap();
write(&path, &serde_json::to_vec(&index)?, &permit).await?;
log::info!("Fetched assets index");
Ok(index)
}
}?;
log::debug!("Assets index successfully loaded!");
Ok(res)
}
pub async fn download_assets(
assets_path: &Path,
legacy_path: Option<&Path>,
st: &State,
with_legacy: bool,
index: &AssetsIndex,
) -> Result<(), LauncherError> {
future::join_all(index.objects.iter().map(|(name, asset)| {
download_asset(assets_path, legacy_path, name, asset)
}))
.await
.into_iter()
.collect::<Result<Vec<()>, LauncherError>>()?;
) -> crate::Result<()> {
log::debug!("Loading assets");
Ok(())
}
stream::iter(index.objects.iter())
.map(Ok::<(&String, &Asset), crate::Error>)
.try_for_each_concurrent(None, |(name, asset)| async move {
let ref hash = asset.hash;
let resource_path = st.directories.object_dir(hash);
let url = format!(
"https://resources.download.minecraft.net/{sub_hash}/{hash}",
sub_hash = &hash[..2]
);
async fn download_asset(
assets_path: &Path,
legacy_path: Option<&Path>,
name: &str,
asset: &Asset,
) -> Result<(), LauncherError> {
let hash = &asset.hash;
let sub_hash = &hash[..2];
let fetch_cell = OnceCell::<bytes::Bytes>::new();
tokio::try_join! {
async {
if !resource_path.exists() {
let permit = st.io_semaphore.acquire().await.unwrap();
let resource = fetch_cell
.get_or_try_init(|| fetch(&url, Some(hash), &permit))
.await?;
write(&resource_path, &resource, &permit).await?;
log::info!("Fetched asset with hash {hash}");
}
Ok::<_, crate::Error>(())
},
async {
if with_legacy {
let permit = st.io_semaphore.acquire().await.unwrap();
let resource = fetch_cell
.get_or_try_init(|| fetch(&url, Some(hash), &permit))
.await?;
let resource_path = st.directories.legacy_assets_dir().join(
name.replace('/', &String::from(std::path::MAIN_SEPARATOR))
);
write(&resource_path, &resource, &permit).await?;
log::info!("Fetched legacy asset with hash {hash}");
}
Ok::<_, crate::Error>(())
},
}?;
let mut resource_path = assets_path.join("objects");
resource_path.push(sub_hash);
resource_path.push(hash);
let url =
format!("https://resources.download.minecraft.net/{sub_hash}/{hash}");
let resource =
save_and_download_file(&resource_path, &url, Some(hash)).await?;
if let Some(legacy_path) = legacy_path {
let resource_path = legacy_path
.join(name.replace('/', &std::path::MAIN_SEPARATOR.to_string()));
save_file(resource_path.as_path(), &resource).await?;
}
log::debug!("Loaded asset with hash {hash}");
Ok(())
}).await?;
log::debug!("Done loading assets!");
Ok(())
}
pub async fn download_libraries(
libraries_path: &Path,
natives_path: &Path,
st: &State,
libraries: &[Library],
) -> Result<(), LauncherError> {
future::join_all(libraries.iter().map(|library| {
download_library(libraries_path, natives_path, library)
}))
.await
.into_iter()
.collect::<Result<Vec<()>, LauncherError>>()?;
version: &str,
) -> crate::Result<()> {
log::debug!("Loading libraries");
let (libraries_dir, natives_dir) = (
Arc::new(st.directories.libraries_dir()),
Arc::new(st.directories.version_natives_dir(version)),
);
Ok(())
}
tokio::try_join! {
fs::create_dir_all(st.directories.libraries_dir()),
fs::create_dir_all(st.directories.version_natives_dir(version))
}?;
async fn download_library(
libraries_path: &Path,
natives_path: &Path,
library: &Library,
) -> Result<(), LauncherError> {
if let Some(rules) = &library.rules {
if !super::rules::parse_rules(rules) {
return Ok(());
}
}
stream::iter(libraries.iter())
.map(Ok::<&Library, crate::Error>)
.try_for_each_concurrent(None, |library| async move {
if let Some(rules) = &library.rules {
if !rules.iter().all(super::parse_rule) {
return Ok(());
}
}
tokio::try_join! {
async {
let artifact_path = d::get_path_from_artifact(&library.name)?;
let path = st.directories.libraries_dir().join(&artifact_path);
future::try_join(
download_library_jar(libraries_path, library),
download_native(natives_path, library),
)
.await?;
match library.downloads {
_ if path.exists() => Ok(()),
Some(d::minecraft::LibraryDownloads {
artifact: Some(ref artifact),
..
}) => {
let permit = st.io_semaphore.acquire().await.unwrap();
let bytes = fetch(&artifact.url, Some(&artifact.sha1), &permit)
.await?;
write(&path, &bytes, &permit).await?;
log::info!("Fetched library {}", &library.name);
Ok::<_, crate::Error>(())
}
None => {
let url = [
library
.url
.as_deref()
.unwrap_or("https://libraries.minecraft.net"),
&artifact_path
].concat();
Ok(())
}
let permit = st.io_semaphore.acquire().await.unwrap();
let bytes = fetch(&url, None, &permit).await?;
write(&path, &bytes, &permit).await?;
log::info!("Fetched library {}", &library.name);
Ok::<_, crate::Error>(())
}
_ => Ok(())
}
},
async {
// HACK: pseudo try block using or else
if let Some((os_key, classifiers)) = None.or_else(|| Some((
library
.natives
.as_ref()?
.get(&Os::native())?,
library
.downloads
.as_ref()?
.classifiers
.as_ref()?
))) {
let parsed_key = os_key.replace(
"${arch}",
crate::util::platform::ARCH_WIDTH,
);
async fn download_library_jar(
libraries_path: &Path,
library: &Library,
) -> Result<(), LauncherError> {
let artifact_path = get_path_from_artifact(&library.name)?;
let path = libraries_path.join(&artifact_path);
if let Some(downloads) = &library.downloads {
if let Some(library) = &downloads.artifact {
save_and_download_file(&path, &library.url, Some(&library.sha1))
.await?;
}
} else {
let url = format!(
"{}{artifact_path}",
library
.url
.as_deref()
.unwrap_or("https://libraries.minecraft.net/"),
);
save_and_download_file(&path, &url, None).await?;
}
Ok(())
}
async fn download_native(
natives_path: &Path,
library: &Library,
) -> Result<(), LauncherError> {
use daedalus::minecraft::LibraryDownload;
use std::collections::HashMap;
// Try blocks in stable Rust when?
let optional_cascade =
|| -> Option<(&String, &HashMap<String, LibraryDownload>)> {
let os_key = library.natives.as_ref()?.get(&get_os())?;
let classifiers =
library.downloads.as_ref()?.classifiers.as_ref()?;
Some((os_key, classifiers))
};
if let Some((os_key, classifiers)) = optional_cascade() {
#[cfg(target_pointer_width = "64")]
let parsed_key = os_key.replace("${arch}", "64");
#[cfg(target_pointer_width = "32")]
let parsed_key = os_key.replace("${arch}", "32");
if let Some(native) = classifiers.get(&parsed_key) {
let file = download_file(&native.url, Some(&native.sha1)).await?;
let reader = std::io::Cursor::new(&file);
let mut archive = zip::ZipArchive::new(reader).unwrap();
archive.extract(natives_path).unwrap();
}
}
Ok(())
}
async fn save_and_download_file(
path: &Path,
url: &str,
sha1: Option<&str>,
) -> Result<bytes::Bytes, LauncherError> {
match std::fs::read(path) {
Ok(bytes) => Ok(bytes::Bytes::from(bytes)),
Err(_) => {
let file = download_file(url, sha1).await?;
save_file(path, &file).await?;
Ok(file)
}
}
}
async fn save_file(path: &Path, bytes: &bytes::Bytes) -> std::io::Result<()> {
let _save_permit = DOWNLOADS_SEMAPHORE
.get()
.expect("File operation semaphore not initialized!")
.acquire()
.await
.unwrap();
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let mut file = File::create(path).await?;
file.write_all(bytes).await?;
Ok(())
}
pub fn get_os() -> Os {
match std::env::consts::OS {
"windows" => Os::Windows,
"macos" => Os::Osx,
"linux" => Os::Linux,
_ => Os::Unknown,
}
}
pub async fn download_file(
url: &str,
sha1: Option<&str>,
) -> Result<bytes::Bytes, LauncherError> {
let _download_permit = DOWNLOADS_SEMAPHORE
.get()
.expect("File operation semaphore not initialized!")
.acquire()
.await
.unwrap();
let client = reqwest::Client::builder()
.tcp_keepalive(Some(Duration::from_secs(10)))
.build()
.map_err(|err| LauncherError::FetchError {
inner: err,
item: url.to_string(),
})?;
for attempt in 1..=4 {
let result = client.get(url).send().await;
match result {
Ok(x) => {
let bytes = x.bytes().await;
if let Ok(bytes) = bytes {
if let Some(sha1) = sha1 {
if &get_hash(bytes.clone()).await? != sha1 {
if attempt <= 3 {
continue;
} else {
return Err(LauncherError::ChecksumFailure {
hash: sha1.to_string(),
url: url.to_string(),
tries: attempt,
});
}
if let Some(native) = classifiers.get(&parsed_key) {
let permit = st.io_semaphore.acquire().await.unwrap();
let data = fetch(&native.url, Some(&native.sha1), &permit).await?;
let reader = std::io::Cursor::new(&data);
let mut archive = zip::ZipArchive::new(reader).unwrap();
archive.extract(&st.directories.version_natives_dir(version)).unwrap();
log::info!("Fetched native {}", &library.name);
}
}
return Ok(bytes);
} else if attempt <= 3 {
continue;
} else if let Err(err) = bytes {
return Err(LauncherError::FetchError {
inner: err,
item: url.to_string(),
});
Ok(())
}
}
Err(_) if attempt <= 3 => continue,
Err(err) => {
return Err(LauncherError::FetchError {
inner: err,
item: url.to_string(),
})
}
}?;
log::debug!("Loaded library {}", library.name);
Ok(())
}
}
unreachable!()
}
).await?;
/// Computes a checksum of the input bytes
async fn get_hash(bytes: bytes::Bytes) -> Result<String, LauncherError> {
let hash =
tokio::task::spawn_blocking(|| sha1::Sha1::from(bytes).hexdigest())
.await?;
Ok(hash)
log::debug!("Done loading libraries!");
Ok(())
}

View File

@@ -1,205 +1,116 @@
use daedalus::minecraft::{ArgumentType, VersionInfo};
use daedalus::modded::LoaderVersion;
use serde::{Deserialize, Serialize};
//! Logic for launching Minecraft
use crate::state as st;
use daedalus as d;
use std::{path::Path, process::Stdio};
use thiserror::Error;
use tokio::process::{Child, Command};
pub use crate::launcher::auth::provider::Credentials;
mod args;
pub mod auth;
mod auth;
pub use auth::Credentials;
mod download;
mod rules;
pub(crate) use download::init as init_download_semaphore;
pub fn parse_rule(rule: &d::minecraft::Rule) -> bool {
use d::minecraft::{Rule, RuleAction};
#[derive(Error, Debug)]
pub enum LauncherError {
#[error("Failed to validate file checksum at url {url} with hash {hash} after {tries} tries")]
ChecksumFailure {
hash: String,
url: String,
tries: u32,
},
let res = match rule {
Rule {
os: Some(ref os), ..
} => crate::util::platform::os_rule(os),
Rule {
features: Some(ref features),
..
} => features.has_demo_resolution.unwrap_or(false),
_ => true,
};
#[error("Failed to run processor: {0}")]
ProcessorError(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Error while managing asynchronous tasks")]
TaskError(#[from] tokio::task::JoinError),
#[error("Error while reading/writing to the disk: {0}")]
IoError(#[from] std::io::Error),
#[error("Error while spawning child process {process}")]
ProcessError {
inner: std::io::Error,
process: String,
},
#[error("Error while deserializing JSON")]
SerdeError(#[from] serde_json::Error),
#[error("Unable to fetch {item}")]
FetchError { inner: reqwest::Error, item: String },
#[error("{0}")]
ParseError(String),
#[error("Error while fetching metadata: {0}")]
DaedalusError(#[from] daedalus::Error),
#[error("Error while reading metadata: {0}")]
MetaError(#[from] crate::data::DataError),
#[error("Java error: {0}")]
JavaError(String),
#[error("Command exited with non-zero exit code: {0}")]
ExitError(i32),
}
// TODO: this probably should be in crate::data
#[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ModLoader {
Vanilla,
Forge,
Fabric,
}
impl Default for ModLoader {
fn default() -> Self {
ModLoader::Vanilla
match rule.action {
RuleAction::Allow => res,
RuleAction::Disallow => !res,
}
}
impl std::fmt::Display for ModLoader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let repr = match self {
&Self::Vanilla => "Vanilla",
&Self::Forge => "Forge",
&Self::Fabric => "Fabric",
};
f.write_str(repr)
macro_rules! processor_rules {
($dest:expr; $($name:literal : client => $client:expr, server => $server:expr;)+) => {
$(std::collections::HashMap::insert(
$dest,
String::from($name),
daedalus::modded::SidedDataEntry {
client: String::from($client),
server: String::from($server),
},
);)+
}
}
pub async fn launch_minecraft(
game_version: &str,
loader_version: &Option<LoaderVersion>,
root_dir: &Path,
java: &Path,
java_args: &Vec<String>,
loader_version: &Option<d::modded::LoaderVersion>,
instance_path: &Path,
java_install: &Path,
java_args: &[String],
wrapper: &Option<String>,
memory: &crate::data::profiles::MemorySettings,
resolution: &crate::data::profiles::WindowSize,
credentials: &Credentials,
) -> Result<Child, LauncherError> {
let (metadata, settings) = futures::try_join! {
crate::data::Metadata::get(),
crate::data::Settings::get(),
}?;
let root_dir = root_dir.canonicalize()?;
let metadata_dir = &settings.metadata_dir;
memory: &st::MemorySettings,
resolution: &st::WindowSize,
credentials: &auth::Credentials,
) -> crate::Result<Child> {
let state = st::State::get().await?;
let instance_path = instance_path.canonicalize()?;
let (
versions_path,
libraries_path,
assets_path,
legacy_assets_path,
natives_path,
) = (
metadata_dir.join("versions"),
metadata_dir.join("libraries"),
metadata_dir.join("assets"),
metadata_dir.join("resources"),
metadata_dir.join("natives"),
);
let version = metadata
let version = state
.metadata
.minecraft
.versions
.iter()
.find(|it| it.id == game_version)
.ok_or_else(|| {
LauncherError::InvalidInput(format!(
"Invalid game version: {game_version}",
))
})?;
.ok_or(crate::Error::LauncherError(format!(
"Invalid game version: {game_version}"
)))?;
let version_jar = loader_version
.as_ref()
.map_or(version.id.clone(), |it| it.id.clone());
let mut version = download::download_version_info(
&versions_path,
version,
let mut version_info = download::download_version_info(
&state,
&version,
loader_version.as_ref(),
)
.await?;
let client_path = versions_path
.join(&version.id)
.join(format!("{}.jar", &version_jar));
let version_natives_path = natives_path.join(&version.id);
let client_path = state
.directories
.version_dir(&version.id)
.join(format!("{version_jar}.jar"));
download_minecraft(
&version,
&versions_path,
&assets_path,
&legacy_assets_path,
&libraries_path,
&version_natives_path,
)
.await?;
download::download_minecraft(&state, &version_info).await?;
st::State::sync().await?;
if let Some(processors) = &version.processors {
if let Some(ref mut data) = version.data {
data.insert(
"SIDE".to_string(),
daedalus::modded::SidedDataEntry {
client: "client".to_string(),
server: "".to_string(),
},
);
data.insert(
"MINECRAFT_JAR".to_string(),
daedalus::modded::SidedDataEntry {
client: client_path.to_string_lossy().to_string(),
server: "".to_string(),
},
);
data.insert(
"MINECRAFT_VERSION".to_string(),
daedalus::modded::SidedDataEntry {
client: game_version.to_string(),
server: "".to_string(),
},
);
data.insert(
"ROOT".to_string(),
daedalus::modded::SidedDataEntry {
client: root_dir.to_string_lossy().to_string(),
server: "".to_string(),
},
);
data.insert(
"LIBRARY_DIR".to_string(),
daedalus::modded::SidedDataEntry {
client: libraries_path.to_string_lossy().to_string(),
server: "".to_string(),
},
);
if let Some(processors) = &version_info.processors {
if let Some(ref mut data) = version_info.data {
processor_rules! {
data;
"SIDE":
client => "client",
server => "";
"MINECRAFT_JAR" :
client => client_path.to_string_lossy(),
server => "";
"MINECRAFT_VERSION":
client => game_version,
server => "";
"ROOT":
client => instance_path.to_string_lossy(),
server => "";
"LIBRARY_DIR":
client => state.directories.libraries_dir().to_string_lossy(),
server => "";
}
for processor in processors {
if let Some(sides) = &processor.sides {
if !sides.contains(&"client".to_string()) {
if !sides.contains(&String::from("client")) {
continue;
}
}
@@ -209,120 +120,93 @@ pub async fn launch_minecraft(
let child = Command::new("java")
.arg("-cp")
.arg(args::get_class_paths_jar(&libraries_path, &cp)?)
.arg(args::get_class_paths_jar(
&state.directories.libraries_dir(),
&cp,
)?)
.arg(
args::get_processor_main_class(args::get_lib_path(
&libraries_path,
&state.directories.libraries_dir(),
&processor.jar,
)?)
.await?
.ok_or_else(|| {
LauncherError::ProcessorError(format!(
crate::Error::LauncherError(format!(
"Could not find processor main class for {}",
processor.jar
))
})?,
)
.args(args::get_processor_arguments(
&libraries_path,
&state.directories.libraries_dir(),
&processor.args,
data,
)?)
.output()
.await
.map_err(|err| LauncherError::ProcessError {
inner: err,
process: "java".to_string(),
.map_err(|err| {
crate::Error::LauncherError(format!(
"Error running processor: {err}",
))
})?;
if !child.status.success() {
return Err(LauncherError::ProcessorError(
String::from_utf8_lossy(&child.stderr).to_string(),
));
return Err(crate::Error::LauncherError(format!(
"Processor error: {}",
String::from_utf8_lossy(&child.stderr)
)));
}
}
}
}
let arguments = version.arguments.clone().unwrap_or_default();
let args = version_info.arguments.clone().unwrap_or_default();
let mut command = match wrapper {
Some(hook) => {
let mut cmd = Command::new(hook);
cmd.arg(java);
cmd.arg(java_install);
cmd
}
None => Command::new(java.to_string_lossy().to_string()),
None => Command::new(String::from(java_install.to_string_lossy())),
};
command
.args(args::get_jvm_arguments(
arguments.get(&ArgumentType::Jvm).map(|x| x.as_slice()),
&version_natives_path,
&libraries_path,
args.get(&d::minecraft::ArgumentType::Jvm)
.map(|x| x.as_slice()),
&state.directories.version_natives_dir(&version.id),
&state.directories.libraries_dir(),
&args::get_class_paths(
&libraries_path,
version.libraries.as_slice(),
&state.directories.libraries_dir(),
version_info.libraries.as_slice(),
&client_path,
)?,
&version_jar,
*memory,
java_args.clone(),
Vec::from(java_args),
)?)
.arg(version.main_class.clone())
.arg(version_info.main_class.clone())
.args(args::get_minecraft_arguments(
arguments.get(&ArgumentType::Game).map(|x| x.as_slice()),
version.minecraft_arguments.as_deref(),
args.get(&d::minecraft::ArgumentType::Game)
.map(|x| x.as_slice()),
version_info.minecraft_arguments.as_deref(),
credentials,
&version.id,
&version.asset_index.id,
&root_dir,
&assets_path,
&version_info.asset_index.id,
&instance_path,
&state.directories.assets_dir(),
&version.type_,
*resolution,
)?)
.current_dir(root_dir.clone())
.current_dir(instance_path.clone())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
command.spawn().map_err(|err| LauncherError::ProcessError {
inner: err,
process: format!("minecraft-{} @ {}", &version.id, root_dir.display()),
command.spawn().map_err(|err| {
crate::Error::LauncherError(format!(
"Error running Minecraft (minecraft-{} @ {}): {err}",
&version.id,
instance_path.display()
))
})
}
pub async fn download_minecraft(
version: &VersionInfo,
versions_dir: &Path,
assets_dir: &Path,
legacy_assets_dir: &Path,
libraries_dir: &Path,
natives_dir: &Path,
) -> Result<(), LauncherError> {
let assets_index =
download::download_assets_index(assets_dir, version).await?;
let (a, b, c) = futures::future::join3(
download::download_client(versions_dir, version),
download::download_assets(
assets_dir,
if version.assets == "legacy" {
Some(legacy_assets_dir)
} else {
None
},
&assets_index,
),
download::download_libraries(
libraries_dir,
natives_dir,
version.libraries.as_slice(),
),
)
.await;
a?;
b?;
c?;
Ok(())
}

View File

@@ -1,55 +0,0 @@
use crate::launcher::download::get_os;
use daedalus::minecraft::{OsRule, Rule, RuleAction};
use regex::Regex;
pub fn parse_rules(rules: &[Rule]) -> bool {
rules.iter().all(|x| parse_rule(x))
}
pub fn parse_rule(rule: &Rule) -> bool {
let result = if let Some(os) = &rule.os {
parse_os_rule(os)
} else if let Some(features) = &rule.features {
features.has_demo_resolution.unwrap_or(false)
} else {
true
};
match rule.action {
RuleAction::Allow => result,
RuleAction::Disallow => !result,
}
}
pub fn parse_os_rule(rule: &OsRule) -> bool {
if let Some(arch) = &rule.arch {
match arch.as_str() {
"x86" => {
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
return false;
}
"arm" => {
#[cfg(not(target_arch = "arm"))]
return false;
}
_ => {}
}
}
if let Some(name) = &rule.name {
if &get_os() != name {
return false;
}
}
if let Some(version) = &rule.version {
let regex = Regex::new(version.as_str());
if let Ok(regex) = regex {
if !regex.is_match(&sys_info::os_release().unwrap_or_default()) {
return false;
}
}
}
true
}