Metadata state management

This commit is contained in:
Jai A
2021-12-15 22:24:08 -07:00
parent 104afe1bb1
commit e9851a8e23
11 changed files with 198 additions and 53 deletions

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
</set>
</option>
</component>
</project>

1
.idea/theseus.iml generated
View File

@@ -46,6 +46,7 @@
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/crossbeam-queue-96bec372a2ebc7a5/out" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/target/debug/build/crossbeam-queue-96bec372a2ebc7a5/out" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/crossbeam-utils-bab62be590a5955d/out" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/target/debug/build/crossbeam-utils-bab62be590a5955d/out" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/memoffset-235ac8b3550fb50a/out" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/target/debug/build/memoffset-235ac8b3550fb50a/out" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/theseus/examples" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />

6
Cargo.lock generated
View File

@@ -710,9 +710,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.8.0" version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
[[package]] [[package]]
name = "opaque-debug" name = "opaque-debug"
@@ -1170,6 +1170,8 @@ dependencies = [
"futures", "futures",
"json5", "json5",
"lazy_static", "lazy_static",
"log",
"once_cell",
"path-clean", "path-clean",
"regex", "regex",
"reqwest", "reqwest",

1
launcher/meta.json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -9,6 +9,7 @@ edition = "2018"
[dependencies] [dependencies]
thiserror = "1.0" thiserror = "1.0"
async-trait = "0.1.51" async-trait = "0.1.51"
once_cell = "1.9.0"
daedalus = "0.1.6" daedalus = "0.1.6"
@@ -33,6 +34,8 @@ futures = "0.3"
sys-info = "0.9.0" sys-info = "0.9.0"
log = "0.4.14"
[dev-dependencies] [dev-dependencies]
argh = "0.1.6" argh = "0.1.6"

View File

@@ -20,50 +20,39 @@ pub enum LauncherError {
url: String, url: String,
tries: u32, tries: u32,
}, },
#[error("Failed to run processor: {0}")] #[error("Failed to run processor: {0}")]
ProcessorError(String), ProcessorError(String),
#[error("Invalid input: {0}")] #[error("Invalid input: {0}")]
InvalidInput(String), InvalidInput(String),
#[error("Error while managing asynchronous tasks")] #[error("Error while managing asynchronous tasks")]
TaskError(#[from] tokio::task::JoinError), TaskError(#[from] tokio::task::JoinError),
#[error("Error while reading/writing to the disk: {0}")] #[error("Error while reading/writing to the disk: {0}")]
IoError(#[from] std::io::Error), IoError(#[from] std::io::Error),
#[error("Error while spawning child process {process}")] #[error("Error while spawning child process {process}")]
ProcessError { ProcessError {
inner: std::io::Error, inner: std::io::Error,
process: String, process: String,
}, },
#[error("Error while deserializing JSON")] #[error("Error while deserializing JSON")]
SerdeError(#[from] serde_json::Error), SerdeError(#[from] serde_json::Error),
#[error("Unable to fetch {item}")] #[error("Unable to fetch {item}")]
FetchError { inner: reqwest::Error, item: String }, FetchError { inner: reqwest::Error, item: String },
#[error("{0}")] #[error("{0}")]
ParseError(String), ParseError(String),
#[error("Error while fetching metadata: {0}")] #[error("Error while fetching metadata: {0}")]
DaedalusError(#[from] daedalus::Error), DaedalusError(#[from] daedalus::Error),
}
const META_URL: &str = "https://staging-cdn.modrinth.com/gamedata"; #[error("Error while reading metadata: {0}")]
MetaError(#[from] crate::meta::MetaError),
pub async fn fetch_metadata() -> Result<
(
daedalus::minecraft::VersionManifest,
daedalus::modded::Manifest,
daedalus::modded::Manifest,
),
LauncherError,
> {
let (game, forge, fabric) = futures::future::join3(
daedalus::minecraft::fetch_version_manifest(Some(&*format!(
"{}/minecraft/v0/manifest.json",
META_URL
))),
daedalus::modded::fetch_manifest(&*format!("{}/forge/v0/manifest.json", META_URL)),
daedalus::modded::fetch_manifest(&*format!("{}/fabric/v0/manifest.json", META_URL)),
)
.await;
Ok((game?, forge?, fabric?))
} }
#[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize)] #[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize)]
@@ -86,7 +75,7 @@ pub async fn launch_minecraft(
root_dir: &Path, root_dir: &Path,
credentials: &Credentials, credentials: &Credentials,
) -> Result<(), LauncherError> { ) -> Result<(), LauncherError> {
let (game, forge, fabric) = fetch_metadata().await?; let metadata = crate::meta::Metadata::get().await?;
let versions_path = crate::util::absolute_path(root_dir.join("versions"))?; let versions_path = crate::util::absolute_path(root_dir.join("versions"))?;
let libraries_path = crate::util::absolute_path(root_dir.join("libraries"))?; let libraries_path = crate::util::absolute_path(root_dir.join("libraries"))?;
@@ -95,7 +84,9 @@ pub async fn launch_minecraft(
let mut version = download::download_version_info( let mut version = download::download_version_info(
&versions_path, &versions_path,
game.versions metadata
.minecraft
.versions
.iter() .iter()
.find(|x| x.id == version_name) .find(|x| x.id == version_name)
.ok_or_else(|| { .ok_or_else(|| {
@@ -105,7 +96,8 @@ pub async fn launch_minecraft(
ModLoader::Vanilla => None, ModLoader::Vanilla => None,
ModLoader::Forge | ModLoader::Fabric => { ModLoader::Forge | ModLoader::Fabric => {
let loaders = if mod_loader.unwrap_or_default() == ModLoader::Forge { let loaders = if mod_loader.unwrap_or_default() == ModLoader::Forge {
&forge &metadata
.forge
.game_versions .game_versions
.iter() .iter()
.find(|x| x.id == version_name) .find(|x| x.id == version_name)
@@ -117,7 +109,8 @@ pub async fn launch_minecraft(
})? })?
.loaders .loaders
} else { } else {
&fabric &metadata
.fabric
.game_versions .game_versions
.iter() .iter()
.find(|x| x.id == version_name) .find(|x| x.id == version_name)

View File

@@ -5,6 +5,33 @@
#![warn(missing_docs, unused_import_braces, missing_debug_implementations)] #![warn(missing_docs, unused_import_braces, missing_debug_implementations)]
use std::path::Path;
use std::time::Duration;
lazy_static::lazy_static! {
pub static ref LAUNCHER_WORK_DIR: &'static Path = Path::new("./launcher");
}
pub mod launcher; pub mod launcher;
mod meta;
pub mod modpack; pub mod modpack;
mod util; mod util;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Launcher error: {0}")]
LauncherError(#[from] launcher::LauncherError),
#[error("Modpack error: {0}")]
ModpackError(#[from] modpack::ModpackError),
#[error("Meta error: {0}")]
DaedalusError(#[from] meta::MetaError),
}
pub async fn init() -> Result<(), Error> {
std::fs::create_dir_all(*LAUNCHER_WORK_DIR).expect("Unable to create launcher root directory!");
crate::meta::Metadata::init().await?;
Ok(())
}

116
theseus/src/meta.rs Normal file
View File

@@ -0,0 +1,116 @@
use serde::{Deserialize, Serialize};
use std::io;
use tokio::sync::RwLockReadGuard;
const META_FILE: &str = "meta.json";
const META_URL: &str = "https://staging-cdn.modrinth.com/gamedata";
#[derive(thiserror::Error, Debug)]
pub enum MetaError {
#[error("I/O error while reading metadata: {0}")]
IOError(#[from] io::Error),
#[error("Daedalus error: {0}")]
DaedalusError(#[from] daedalus::Error),
#[error("Attempted to access metadata without initializing it!")]
InitializedError,
#[error("Error while serializing/deserializing JSON")]
SerdeError(#[from] serde_json::Error),
}
use once_cell::sync;
use tokio::sync::RwLock;
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct Metadata {
pub minecraft: daedalus::minecraft::VersionManifest,
pub forge: daedalus::modded::Manifest,
pub fabric: daedalus::modded::Manifest,
}
static METADATA: sync::OnceCell<RwLock<Metadata>> = sync::OnceCell::new();
impl Metadata {
pub async fn init() -> Result<(), MetaError> {
let meta_path = crate::LAUNCHER_WORK_DIR.join(META_FILE);
if meta_path.exists() {
let meta_data = std::fs::read_to_string(meta_path)
.map(|x| serde_json::from_str::<Metadata>(&*x).ok())
.ok()
.flatten();
if let Some(metadata) = meta_data {
METADATA.get_or_init(|| RwLock::new(metadata));
}
}
let future = async {
for attempt in 0..=3 {
let res = async {
let new = Self::fetch().await?;
std::fs::write(
crate::LAUNCHER_WORK_DIR.join(META_FILE),
&*serde_json::to_string(&new)?,
)?;
if let Some(metadata) = METADATA.get() {
*metadata.write().await = new;
} else {
METADATA.get_or_init(|| RwLock::new(new));
}
Ok::<(), MetaError>(())
}
.await;
match res {
Ok(_) => {
break;
}
Err(_) if attempt <= 3 => continue,
Err(err) => {
log::warn!("Unable to fetch launcher metadata: {}", err)
}
}
}
};
if METADATA.get().is_some() {
tokio::task::spawn(future);
} else {
future.await;
}
Ok(())
}
pub async fn fetch() -> Result<Self, MetaError> {
let (game, forge, fabric) = futures::future::join3(
daedalus::minecraft::fetch_version_manifest(Some(&*format!(
"{}/minecraft/v0/manifest.json",
META_URL
))),
daedalus::modded::fetch_manifest(&*format!("{}/forge/v0/manifest.json", META_URL)),
daedalus::modded::fetch_manifest(&*format!("{}/fabric/v0/manifest.json", META_URL)),
)
.await;
Ok(Self {
minecraft: game?,
forge: forge?,
fabric: fabric?,
})
}
pub async fn get<'a>() -> Result<RwLockReadGuard<'a, Self>, MetaError> {
Ok(METADATA
.get()
.ok_or_else(|| MetaError::InitializedError)?
.read()
.await)
}
}

View File

@@ -184,7 +184,10 @@ pub async fn compile_modpack(dir: &Path) -> ModpackResult<()> {
) )
.await?; .await?;
let result_zip = fs::File::create(dir.join(COMPILED_ZIP)).await?.into_std().await; let result_zip = fs::File::create(dir.join(COMPILED_ZIP))
.await?
.into_std()
.await;
let mut zip = zip::ZipWriter::new(&result_zip); let mut zip = zip::ZipWriter::new(&result_zip);
zip.create_from_directory(&result_dir)?; zip.create_from_directory(&result_dir)?;

View File

@@ -1,13 +1,9 @@
use std::{ use std::{collections::HashSet, convert::TryFrom, path::PathBuf};
collections::HashSet,
convert::TryFrom,
path::{Path, PathBuf},
};
use crate::launcher::ModLoader; use crate::launcher::ModLoader;
use super::{ use super::{
manifest::{ManifestEnv, ManifestEnvs, ManifestHashes}, manifest::{ManifestEnvs, ManifestHashes},
pack::{ModpackEnv, ModpackFile, ModpackFileHashes, ModpackGame}, pack::{ModpackEnv, ModpackFile, ModpackFileHashes, ModpackGame},
ModpackError, ModpackResult, ModpackError, ModpackResult,
}; };

View File

@@ -4,13 +4,14 @@ use serde::{Deserialize, Serialize};
use std::{ use std::{
collections::HashSet, collections::HashSet,
hash::Hash, hash::Hash,
path::{Path, PathBuf}, iter::FromIterator, iter::FromIterator,
path::{Path, PathBuf},
}; };
use tokio::fs; use tokio::fs;
use super::{ use super::{
modrinth_api::{self, ModrinthV1}, modrinth_api::{self, ModrinthV1},
ModpackResult, ModpackError, ModpackError, ModpackResult,
}; };
use crate::launcher::ModLoader; use crate::launcher::ModLoader;
@@ -104,7 +105,13 @@ impl Modpack {
Ok(()) Ok(())
} }
pub async fn add_file(&mut self, source: reqwest::Url, dest: &Path, hashes: Option<ModpackFileHashes>, env: Option<ModpackEnv>) -> ModpackResult<()> { pub async fn add_file(
&mut self,
source: reqwest::Url,
dest: &Path,
hashes: Option<ModpackFileHashes>,
env: Option<ModpackEnv>,
) -> ModpackResult<()> {
let whitelisted_domains = std::env::var(MODRINTH_MODPACK_DOMAIN_WHITELIST_VAR) let whitelisted_domains = std::env::var(MODRINTH_MODPACK_DOMAIN_WHITELIST_VAR)
.map(|it| serde_json::from_str::<Vec<String>>(&it).ok().unwrap()) .map(|it| serde_json::from_str::<Vec<String>>(&it).ok().unwrap())
.unwrap_or( .unwrap_or(
@@ -115,15 +122,21 @@ impl Modpack {
.collect::<Vec<String>>(), .collect::<Vec<String>>(),
); );
if (whitelisted_domains.iter().find(|it| it == &source.host_str().unwrap()).is_none()) { if whitelisted_domains
return Err(ModpackError::SourceWhitelistError(String::from(source.host_str().unwrap()))); .iter()
} .find(|it| it == &source.host_str().unwrap())
.is_none()
{
return Err(ModpackError::SourceWhitelistError(String::from(
source.host_str().unwrap(),
)));
}
let file = ModpackFile { let file = ModpackFile {
path: PathBuf::from(dest), path: PathBuf::from(dest),
hashes, hashes,
env: env.unwrap_or(ModpackEnv::Both), env: env.unwrap_or(ModpackEnv::Both),
downloads: HashSet::from_iter([String::from(source)].into_iter().cloned()) downloads: HashSet::from_iter([String::from(source)].into_iter().cloned()),
}; };
self.files.insert(file); self.files.insert(file);