feat: Metadata and semaphore store
This commit is contained in:
parent
54d16e5d69
commit
1f2d560983
25 changed files with 448 additions and 96 deletions
|
|
@ -6,7 +6,7 @@ reorder_imports = true
|
|||
imports_granularity = "Module"
|
||||
reorder_modules = true
|
||||
remove_nested_parens = true
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
merge_derives = true
|
||||
use_try_shorthand = false
|
||||
use_field_init_shorthand = false
|
||||
|
|
|
|||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -2989,6 +2989,7 @@ name = "onelauncher_entity"
|
|||
version = "0.2.0-alpha.2"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"interpulse",
|
||||
"onelauncher_macro",
|
||||
"sea-orm",
|
||||
"serde",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ tauri = [
|
|||
"specta",
|
||||
"gui"
|
||||
]
|
||||
specta = [ "dep:specta", "onelauncher_entity/specta" ]
|
||||
specta = [ "dep:specta", "interpulse/specta", "onelauncher_entity/specta" ]
|
||||
cli = [ "dep:indicatif" ]
|
||||
gui = []
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ use onelauncher_core::store::Dirs;
|
|||
async fn main() {
|
||||
let dirs = Dirs::get().await.expect("couldn't initialize dirs");
|
||||
|
||||
println!(" Base dir : {:?}", dirs.base_dir());
|
||||
println!("Launcher logs dir : {:?}", dirs.launcher_logs_dir());
|
||||
println!(" Metadata dir : {:?}", dirs.metadata_dir());
|
||||
println!(" DB file : {:?}", dirs.db_file());
|
||||
println!(" Base dir : {}", dirs.base_dir().display());
|
||||
println!("Launcher logs dir : {}", dirs.launcher_logs_dir().display());
|
||||
println!(" Metadata dir : {}", dirs.metadata_dir().display());
|
||||
println!(" DB file : {}", dirs.db_file().display());
|
||||
}
|
||||
20
packages/core/examples/metadata.rs
Normal file
20
packages/core/examples/metadata.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
use onelauncher_core::{api::proxy::ProxyDynamic, error::LauncherResult, initialize_core, store::{CoreOptions, State}};
|
||||
use onelauncher_entity::loader::GameLoader;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> LauncherResult<()> {
|
||||
initialize_core(CoreOptions::default(), ProxyDynamic::default()).await?;
|
||||
|
||||
let state = State::get().await?;
|
||||
let metadata = &mut state.metadata.write().await;
|
||||
|
||||
println!("Initialized: {}", metadata.initialized());
|
||||
println!("Minecraft: {:?}", metadata.get_vanilla(GameLoader::Vanilla).await.is_ok());
|
||||
println!("Forge: {:?}", metadata.get_modded(GameLoader::Forge).await.is_ok());
|
||||
println!("Initialized: {}", metadata.initialized());
|
||||
println!("NeoForge: {:?}", metadata.get_modded(GameLoader::NeoForge).await.is_ok());
|
||||
println!("Fabric: {:?}", metadata.get_modded(GameLoader::Fabric).await.is_ok());
|
||||
println!("Quilt: {:?}", metadata.get_modded(GameLoader::Quilt).await.is_ok());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ pub async fn sync_clusters() -> LauncherResult<Vec<i32>> {
|
|||
// We have a directory that is not in the database
|
||||
if let Err(err) = sync_from_fs_to_db(&path).await {
|
||||
tracing::error!("failed to sync cluster from fs to db: {}", err);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(missing_ids)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,11 @@ pub async fn init_ingress_opt(
|
|||
|
||||
#[tracing::instrument]
|
||||
pub async fn send_ingress(id: impl AsRef<IngressId> + std::fmt::Debug, increment: f64) -> LauncherResult<()> {
|
||||
if !State::initialized() {
|
||||
tracing::debug!("attempted to send ingress when state is not initialized");
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
let state = State::get().await?;
|
||||
let processor = &state.ingress_processor;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ pub mod message;
|
|||
#[cfg(feature = "cli")] mod proxy_cli;
|
||||
mod proxy_empty;
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[cfg(feature = "tauri")] pub use proxy_tauri::ProxyTauri;
|
||||
#[cfg(feature = "cli")] pub use proxy_cli::ProxyCli;
|
||||
pub use proxy_empty::ProxyEmpty;
|
||||
|
|
@ -12,7 +14,7 @@ pub use proxy_empty::ProxyEmpty;
|
|||
#[cfg(not(feature = "cli"))] pub type ProxyDynamic = ProxyEmpty;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait LauncherProxy: Send + Sync {
|
||||
pub trait LauncherProxy: Send + Sync + Debug {
|
||||
async fn send_ingress(&self, ingress: crate::store::ingress::IngressPayload) -> crate::LauncherResult<()>;
|
||||
async fn send_message(&self, message: message::MessagePayload) -> crate::LauncherResult<()>;
|
||||
|
||||
|
|
@ -22,4 +24,3 @@ pub trait LauncherProxy: Send + Sync {
|
|||
#[cfg(feature = "gui")]
|
||||
fn show_main_window(&self) -> crate::LauncherResult<()>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use crate::{constants::CLI_TOTAL_INGRESS, store::ingress::IngressPayload, Launch
|
|||
|
||||
use super::LauncherProxy;
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ProxyCli {
|
||||
ingress_feeds: RwLock<HashMap<Uuid, ProgressBar>>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use crate::{store::ingress::IngressPayload, LauncherResult};
|
|||
|
||||
use super::LauncherProxy;
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ProxyEmpty;
|
||||
|
||||
impl ProxyEmpty {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ pub enum LauncherError {
|
|||
#[error(transparent)]
|
||||
DiscordError(#[from] crate::store::discord::DiscordError),
|
||||
#[error(transparent)]
|
||||
MetadataError(#[from] crate::store::metadata::MetadataError),
|
||||
#[error(transparent)]
|
||||
DaoError(#[from] DaoError),
|
||||
|
||||
#[error(transparent)]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
#![feature(let_chains)]
|
||||
|
||||
use api::proxy::LauncherProxy;
|
||||
use store::{proxy::ProxyState, Core, CoreOptions, Dirs, State};
|
||||
use store::{proxy::ProxyState, semaphore::SemaphoreStore, Core, CoreOptions, Dirs, State};
|
||||
use error::LauncherResult;
|
||||
use logger::start_logger;
|
||||
|
||||
|
|
@ -21,8 +23,11 @@ pub async fn initialize_core(options: CoreOptions, proxy_backend: impl LauncherP
|
|||
Dirs::get().await?;
|
||||
start_logger().await;
|
||||
|
||||
SemaphoreStore::get().await;
|
||||
ProxyState::initialize(proxy_backend).await?;
|
||||
State::get().await?;
|
||||
let _ = State::get().await?;
|
||||
|
||||
tracing::info!("Core initialized successfully");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// This will log to the console, and will not log to a file
|
||||
#[cfg(debug_assertions)]
|
||||
pub async fn start_logger() {
|
||||
use tracing_subscriber::{prelude::*, util::SubscriberInitExt};
|
||||
use tracing_subscriber::{fmt::format::FmtSpan, prelude::*, util::SubscriberInitExt};
|
||||
|
||||
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(format!("{}=info", env!("CARGO_PKG_NAME"))));
|
||||
|
|
@ -15,6 +15,7 @@ pub async fn start_logger() {
|
|||
.with_line_number(true)
|
||||
.with_level(true)
|
||||
.with_thread_names(true)
|
||||
.with_span_events(FmtSpan::FULL)
|
||||
.pretty()
|
||||
)
|
||||
.with(filter)
|
||||
|
|
|
|||
229
packages/core/src/store/metadata.rs
Normal file
229
packages/core/src/store/metadata.rs
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use interpulse::api::minecraft::VersionManifest as VanillaManifest;
|
||||
use interpulse::api::modded::Manifest as ModdedManifest;
|
||||
use onelauncher_entity::loader::GameLoader;
|
||||
use reqwest::Method;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::constants;
|
||||
use crate::error::LauncherResult;
|
||||
use crate::utils::http::fetch_json;
|
||||
use crate::utils::io;
|
||||
|
||||
use super::Dirs;
|
||||
|
||||
#[allow(clippy::unsafe_derive_deserialize)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Metadata {
|
||||
initialized: bool,
|
||||
inner: MetadataInner,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
struct MetadataInner {
|
||||
minecraft: Option<VanillaManifest>,
|
||||
forge: Option<ModdedManifest>,
|
||||
neo: Option<ModdedManifest>, // TODO(metadata): change to neoforge
|
||||
fabric: Option<ModdedManifest>,
|
||||
quilt: Option<ModdedManifest>,
|
||||
// legacyfabric: Option<ModdedManifest>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum MetadataError {
|
||||
#[error("failed to fetch metadata")]
|
||||
FetchError,
|
||||
#[error("loader {0} does not use a modded manifest")]
|
||||
NotModdedManifest(GameLoader),
|
||||
#[error("loader {0} does not use a vanilla manifest")]
|
||||
NotVanillaManifest(GameLoader),
|
||||
#[error("failed to parse metadata")]
|
||||
ParseError(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
#[must_use]
|
||||
pub const fn initialized(&self) -> bool {
|
||||
self.initialized
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn initialize() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
|
||||
pub async fn get_vanilla(&mut self, loader: GameLoader) -> LauncherResult<&VanillaManifest> {
|
||||
if loader.is_mod_loader() {
|
||||
return Err(MetadataError::NotVanillaManifest(loader).into());
|
||||
}
|
||||
|
||||
if !self.initialized() {
|
||||
self.load().await?;
|
||||
}
|
||||
|
||||
match loader {
|
||||
GameLoader::Vanilla => self.inner.minecraft.as_ref(),
|
||||
_ => None,
|
||||
}.ok_or_else(|| MetadataError::FetchError.into())
|
||||
}
|
||||
|
||||
pub async fn get_modded(&mut self, loader: GameLoader) -> LauncherResult<&ModdedManifest> {
|
||||
if !loader.is_mod_loader() {
|
||||
return Err(MetadataError::NotModdedManifest(loader).into());
|
||||
}
|
||||
|
||||
if !self.initialized() {
|
||||
self.load().await?;
|
||||
}
|
||||
|
||||
match loader {
|
||||
GameLoader::Forge => self.inner.forge.as_ref(),
|
||||
GameLoader::NeoForge => self.inner.neo.as_ref(),
|
||||
GameLoader::Fabric => self.inner.fabric.as_ref(),
|
||||
GameLoader::Quilt => self.inner.quilt.as_ref(),
|
||||
_ => None,
|
||||
}.ok_or_else(|| MetadataError::FetchError.into())
|
||||
}
|
||||
|
||||
|
||||
async fn load(&mut self) -> LauncherResult<()> {
|
||||
let path = Dirs::get_caches_dir().await?.join("metadata.json");
|
||||
let mut save_file = false;
|
||||
|
||||
let mut metadata = Self::default();
|
||||
|
||||
if let Ok(metadata_json) = io::read_json::<MetadataInner>(&path).await {
|
||||
metadata.inner = metadata_json;
|
||||
|
||||
if metadata.refetch_errored().await > 0 {
|
||||
save_file = true;
|
||||
}
|
||||
} else {
|
||||
metadata.fetch_all().await;
|
||||
save_file = true;
|
||||
}
|
||||
|
||||
if save_file {
|
||||
io::create_dir_all(&path.parent().expect("couldn't get metadata.json parent")).await?;
|
||||
io::write_json(&path, &metadata.inner).await?;
|
||||
}
|
||||
|
||||
*self = metadata;
|
||||
self.initialized = true;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetches the all missing metadata and returns the amount that were a success
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn refetch_errored(&mut self) -> u8 {
|
||||
let mut changed: u8 = 0;
|
||||
|
||||
macro_rules! check {
|
||||
($var:tt, $is_modded:tt) => {
|
||||
if self.inner.$var.is_none() {
|
||||
if let Ok(loader) = GameLoader::from_str(stringify!($var)) {
|
||||
match check!(_fetch, loader, $is_modded) {
|
||||
Ok(data) => {
|
||||
self.inner.$var = Some(data);
|
||||
changed += 1;
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("failed to fetch manifest for {}: {}", loader, err);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
(_fetch, $loader:expr, false) => {
|
||||
fetch_vanilla_manifest($loader).await
|
||||
};
|
||||
|
||||
(_fetch, $loader:expr, true) => {
|
||||
fetch_modded_manifest($loader).await
|
||||
};
|
||||
}
|
||||
|
||||
check!(minecraft, false);
|
||||
check!(forge, true);
|
||||
check!(neo, true);
|
||||
check!(fabric, true);
|
||||
check!(quilt, true);
|
||||
|
||||
changed
|
||||
}
|
||||
|
||||
/// Force fetches all the manifests and sets them in the current instance
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn fetch_all(&mut self) {
|
||||
macro_rules! fetch_manifest {
|
||||
($loader:expr, false) => {
|
||||
fetch_vanilla_manifest($loader).await
|
||||
};
|
||||
|
||||
($loader:expr, true) => {
|
||||
fetch_modded_manifest($loader).await
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! task {
|
||||
($(($var:tt, $is_modded:tt)),*) => {
|
||||
let ($($var),*) = tokio::join!(
|
||||
$(
|
||||
tokio::spawn(async move {
|
||||
if let Ok(loader) = GameLoader::from_str(stringify!(minecraft)) {
|
||||
fetch_manifest!(loader, $is_modded).map_err(|e| $crate::send_error!("couldn't fetch {loader} manifest: {e}")).ok()
|
||||
} else {
|
||||
tracing::error!("failed to fetch {} manifest", stringify!($var));
|
||||
None
|
||||
}
|
||||
}),
|
||||
)*
|
||||
);
|
||||
|
||||
$(
|
||||
if let Ok(data) = $var {
|
||||
self.inner.$var = data;
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
task! {
|
||||
// (name, is_modded)
|
||||
(minecraft, false),
|
||||
(forge, true),
|
||||
(neo, true),
|
||||
(fabric, true),
|
||||
(quilt, true)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_vanilla_manifest(loader: GameLoader) -> LauncherResult<VanillaManifest> {
|
||||
fetch_manifest(loader).await
|
||||
}
|
||||
|
||||
pub async fn fetch_modded_manifest(loader: GameLoader) -> LauncherResult<ModdedManifest> {
|
||||
fetch_manifest(loader).await
|
||||
}
|
||||
|
||||
pub async fn fetch_manifest<T: DeserializeOwned>(loader: GameLoader) -> LauncherResult<T> {
|
||||
fetch_json::<T>(
|
||||
Method::GET,
|
||||
format!(
|
||||
"{}/{}/v{}/manifest.json",
|
||||
constants::METADATA_API_URL,
|
||||
loader.get_format_name(),
|
||||
loader.get_format_version()
|
||||
)
|
||||
.as_str(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ pub(super) mod db;
|
|||
pub mod discord;
|
||||
pub mod ingress;
|
||||
pub mod proxy;
|
||||
pub mod semaphore;
|
||||
pub mod metadata;
|
||||
mod core_options;
|
||||
mod dirs;
|
||||
mod settings;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ impl Deref for ProxyState {
|
|||
}
|
||||
|
||||
impl ProxyState {
|
||||
#[tracing::instrument(level = "debug")]
|
||||
pub async fn initialize(proxy: impl LauncherProxy + 'static) -> LauncherResult<Arc<Self>> {
|
||||
PROXY_STATE
|
||||
.get_or_try_init(|| async {
|
||||
|
|
|
|||
26
packages/core/src/store/semaphore.rs
Normal file
26
packages/core/src/store/semaphore.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::{OnceCell, Semaphore};
|
||||
|
||||
static STORE_STATE: OnceCell<Arc<SemaphoreStore>> = OnceCell::const_new();
|
||||
|
||||
pub struct SemaphoreStore {
|
||||
fetch: Arc<Semaphore>
|
||||
}
|
||||
|
||||
impl SemaphoreStore {
|
||||
pub async fn get() -> Arc<Self> {
|
||||
STORE_STATE.get_or_init(|| async { Self::initialize() }).await.clone()
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
fn initialize() -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
fetch: Arc::new(Semaphore::new(10)), // Set the limit to 10
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn fetch() -> Arc<Semaphore> {
|
||||
Self::get().await.fetch.clone()
|
||||
}
|
||||
}
|
||||
|
|
@ -3,42 +3,32 @@ use std::sync::Arc;
|
|||
use sea_orm::DatabaseConnection;
|
||||
use tokio::sync::{OnceCell, RwLock};
|
||||
|
||||
use crate::{utils::http::{self, FetchSemaphore}, LauncherResult};
|
||||
use crate::LauncherResult;
|
||||
|
||||
use super::{discord::DiscordRPC, ingress::IngressProcessor, Settings};
|
||||
use super::Settings;
|
||||
use super::discord::DiscordRPC;
|
||||
use super::ingress::IngressProcessor;
|
||||
use super::metadata::Metadata;
|
||||
|
||||
/// The static [`OnceCell<RwLock<State>>`] for storing the global runtime launcher state.
|
||||
static LAUNCHER_STATE: OnceCell<RwLock<State>> = OnceCell::const_new();
|
||||
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new();
|
||||
|
||||
pub struct State {
|
||||
pub ingress_processor: IngressProcessor,
|
||||
pub settings: RwLock<Settings>,
|
||||
pub db: DatabaseConnection,
|
||||
pub metadata: RwLock<Metadata>,
|
||||
pub rpc: Option<DiscordRPC>,
|
||||
|
||||
pub client: Arc<reqwest::Client>,
|
||||
pub(crate) fetch_semaphore: FetchSemaphore,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Get the current global launcher state (or initialize it)
|
||||
pub async fn get() -> LauncherResult<Arc<tokio::sync::RwLockReadGuard<'static, Self>>> {
|
||||
Ok(Arc::new(
|
||||
LAUNCHER_STATE
|
||||
.get_or_try_init(Self::initialize)
|
||||
.await?
|
||||
.read()
|
||||
.await,
|
||||
))
|
||||
}
|
||||
|
||||
/// Get and lock writing to the current global launcher state (or initalize it)
|
||||
pub async fn get_and_write() -> LauncherResult<tokio::sync::RwLockWriteGuard<'static, Self>> {
|
||||
pub async fn get() -> LauncherResult<Arc<Self>> {
|
||||
Ok(LAUNCHER_STATE
|
||||
.get_or_try_init(Self::initialize)
|
||||
.await?
|
||||
.write()
|
||||
.await)
|
||||
.clone()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn initialized() -> bool {
|
||||
|
|
@ -46,30 +36,26 @@ impl State {
|
|||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
async fn initialize() -> LauncherResult<RwLock<Self>> {
|
||||
async fn initialize() -> LauncherResult<Arc<Self>> {
|
||||
let ingress_processor = IngressProcessor::new();
|
||||
let settings = Settings::new().await;
|
||||
let db = super::db::create_pool().await?;
|
||||
let metadata = Metadata::initialize();
|
||||
|
||||
let rpc = match DiscordRPC::initialize() {
|
||||
Ok(rpc) => Some(rpc),
|
||||
Err(err) => {
|
||||
tracing::warn!("{}", err);
|
||||
None
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
let reqwest_client = http::create_client()?;
|
||||
let fetch_semaphore = FetchSemaphore::new(settings.max_concurrent_requests);
|
||||
|
||||
Ok(RwLock::new(Self {
|
||||
Ok(Arc::new(Self {
|
||||
ingress_processor,
|
||||
settings: RwLock::new(settings),
|
||||
db,
|
||||
metadata: RwLock::new(metadata),
|
||||
rpc,
|
||||
|
||||
client: Arc::new(reqwest_client),
|
||||
fetch_semaphore,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use bytes::Bytes;
|
||||
use reqwest::Method;
|
||||
|
|
@ -13,7 +13,8 @@ use tokio_stream::StreamExt;
|
|||
use crate::api::ingress::send_ingress;
|
||||
use crate::error::LauncherResult;
|
||||
use crate::store::ingress::IngressRef;
|
||||
use crate::store::{Core, State};
|
||||
use crate::store::semaphore::SemaphoreStore;
|
||||
use crate::store::Core;
|
||||
|
||||
use super::crypto::{CryptoError, HashAlgorithm};
|
||||
use super::io;
|
||||
|
|
@ -28,8 +29,8 @@ impl FetchSemaphore {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn create_client() -> LauncherResult<reqwest::Client> {
|
||||
Ok(reqwest::ClientBuilder::new()
|
||||
static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
||||
reqwest::ClientBuilder::new()
|
||||
.tcp_keepalive(Some(std::time::Duration::from_secs(15)))
|
||||
.use_rustls_tls()
|
||||
.default_headers({
|
||||
|
|
@ -44,10 +45,12 @@ pub fn create_client() -> LauncherResult<reqwest::Client> {
|
|||
headers.insert(reqwest::header::USER_AGENT, header);
|
||||
headers
|
||||
})
|
||||
.build()?)
|
||||
}
|
||||
.build()
|
||||
.expect("failed to build reqwest client!")
|
||||
});
|
||||
|
||||
#[tracing::instrument(skip(body, headers, ingress_ref))]
|
||||
#[tracing::instrument(level = "debug", skip(body, headers, ingress_ref))]
|
||||
#[onelauncher_macro::pin]
|
||||
pub async fn fetch_advanced(
|
||||
method: Method,
|
||||
url: &str,
|
||||
|
|
@ -56,9 +59,9 @@ pub async fn fetch_advanced(
|
|||
headers: Option<HashMap<&str, &str>>,
|
||||
ingress_ref: Option<&IngressRef<'_>>,
|
||||
) -> LauncherResult<Bytes> {
|
||||
let state = State::get().await?;
|
||||
let _permit = state.fetch_semaphore.0.acquire().await?;
|
||||
let client = &state.client;
|
||||
let semaphore = SemaphoreStore::fetch().await;
|
||||
let _permit = semaphore.acquire().await?;
|
||||
let client = &REQWEST_CLIENT;
|
||||
|
||||
for attempt in 0..Core::get().fetch_attempts {
|
||||
let mut req = client.request(method.clone(), url);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use async_compression::tokio::bufread::GzipDecoder;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use tempfile::TempDir;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
|
|
@ -18,6 +19,8 @@ pub enum IOError {
|
|||
ZipError(#[from] zip::result::ZipError),
|
||||
#[error(transparent)]
|
||||
IOError(#[from] std::io::Error),
|
||||
#[error("json deserialization error: {0}")]
|
||||
DeserializeError(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
impl<P: AsRef<std::path::Path>> From<(P, std::io::Error)> for IOError {
|
||||
|
|
@ -119,7 +122,7 @@ pub async fn read_to_string(path: impl AsRef<std::path::Path>) -> Result<String,
|
|||
})
|
||||
}
|
||||
|
||||
/// Reads the entire contents of a file into a bytes vector.
|
||||
/// Asynchronously reads the entire contents of a file into a bytes vector.
|
||||
pub async fn read(path: impl AsRef<std::path::Path>) -> Result<Vec<u8>, IOError> {
|
||||
let path = path.as_ref();
|
||||
tokio::fs::read(path)
|
||||
|
|
@ -130,6 +133,11 @@ pub async fn read(path: impl AsRef<std::path::Path>) -> Result<Vec<u8>, IOError>
|
|||
})
|
||||
}
|
||||
|
||||
/// Asynchronously read a file as JSON and return the deserialized object
|
||||
pub async fn read_json<T: DeserializeOwned>(path: impl AsRef<std::path::Path>) -> Result<T, IOError> {
|
||||
Ok(serde_json::from_slice(&read(path).await?)?)
|
||||
}
|
||||
|
||||
/// Asynchrously write to a tempfile that is then transferred to an official [`AsRef<Path>`].
|
||||
pub async fn write(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
|
|
@ -144,6 +152,14 @@ pub async fn write(
|
|||
})
|
||||
}
|
||||
|
||||
/// Asynchronously write json to a file, creating it if it does not exist.
|
||||
pub async fn write_json<T: Serialize>(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
data: T
|
||||
) -> Result<(), IOError> {
|
||||
write(path, serde_json::to_vec(&data)?).await
|
||||
}
|
||||
|
||||
/// Renames a file or directory to a new name, replacing the original file if `to` already exists.
|
||||
pub async fn rename(
|
||||
from: impl AsRef<std::path::Path>,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ path = "src/lib.rs"
|
|||
|
||||
[features]
|
||||
default = []
|
||||
specta = [ "dep:specta" ]
|
||||
specta = [ "dep:specta", "interpulse/specta" ]
|
||||
|
||||
[dependencies]
|
||||
onelauncher_macro = { workspace = true }
|
||||
|
|
@ -26,4 +26,5 @@ serde_json = { workspace = true }
|
|||
chrono = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
url = { workspace = true }
|
||||
interpulse = { workspace = true }
|
||||
specta = { workspace = true, optional = true }
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize};
|
|||
#[serde(rename_all = "lowercase")]
|
||||
#[sea_orm(rs_type = "u8", db_type = "Integer")]
|
||||
pub enum GameLoader {
|
||||
#[default]
|
||||
Vanilla = 0,
|
||||
#[default]
|
||||
Unknown = 1,
|
||||
Forge = 2,
|
||||
NeoForge = 3,
|
||||
|
|
@ -25,6 +25,25 @@ impl GameLoader {
|
|||
matches!(self, Self::Forge | Self::NeoForge | Self::Quilt | Self::Fabric | Self::LegacyFabric)
|
||||
}
|
||||
|
||||
pub const fn get_format_version(&self) -> usize {
|
||||
match self {
|
||||
Self::Vanilla | Self::Unknown => interpulse::api::minecraft::CURRENT_FORMAT_VERSION,
|
||||
Self::Forge => interpulse::api::modded::CURRENT_FORGE_FORMAT_VERSION,
|
||||
Self::NeoForge => interpulse::api::modded::CURRENT_NEOFORGE_FORMAT_VERSION,
|
||||
Self::Quilt => interpulse::api::modded::CURRENT_QUILT_FORMAT_VERSION,
|
||||
Self::Fabric => interpulse::api::modded::CURRENT_FABRIC_FORMAT_VERSION,
|
||||
Self::LegacyFabric => interpulse::api::modded::CURRENT_LEGACY_FABRIC_FORMAT_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_format_name(&self) -> String {
|
||||
match self {
|
||||
Self::Vanilla | Self::Unknown => String::from("minecraft"),
|
||||
Self::NeoForge => String::from("neo"), // TODO(metadata): change to neoforge
|
||||
_ => self.to_string().to_lowercase().replace(['_', '.', ' '], "")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl From<String> for GameLoader {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use quote::quote;
|
||||
use syn::{meta::ParseNestedMeta, parse_macro_input, Item, Result};
|
||||
mod specta;
|
||||
mod pin;
|
||||
|
||||
/// This macro is used to derive the `specta::Type` trait for the annotated item.
|
||||
/// It's mainly meant for entity-generation with sea-orm-cli (it doesn't like commas in attributes)
|
||||
|
|
@ -10,43 +10,11 @@ use syn::{meta::ParseNestedMeta, parse_macro_input, Item, Result};
|
|||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn specta(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let mut attrs = SpectaAttributes::default();
|
||||
let parser = syn::meta::parser(|meta| attrs.parse(meta));
|
||||
parse_macro_input!(attr with parser);
|
||||
|
||||
let input = parse_macro_input!(item as Item);
|
||||
|
||||
let specta_event_type = if attrs.with_event {
|
||||
quote! {
|
||||
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let specta_type = quote! {
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
};
|
||||
|
||||
let expanded = quote! {
|
||||
#specta_event_type
|
||||
#specta_type
|
||||
#input
|
||||
};
|
||||
|
||||
expanded.into()
|
||||
specta::specta(attr, item)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct SpectaAttributes {
|
||||
with_event: bool,
|
||||
/// This macro Box::Pin a function
|
||||
#[proc_macro_attribute]
|
||||
pub fn pin(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
pin::pin(attr, item)
|
||||
}
|
||||
|
||||
impl SpectaAttributes {
|
||||
fn parse(&mut self, meta: ParseNestedMeta) -> Result<()> {
|
||||
if meta.path.is_ident("with_event") {
|
||||
self.with_event = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
22
packages/macro/src/pin.rs
Normal file
22
packages/macro/src/pin.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
use quote::quote;
|
||||
use syn::{parse_macro_input, ItemFn};
|
||||
|
||||
pub fn pin(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let input = parse_macro_input!(item as ItemFn);
|
||||
|
||||
let attrs = &input.attrs;
|
||||
let vis = &input.vis;
|
||||
let sig = &input.sig;
|
||||
let body = &input.block;
|
||||
|
||||
let result = quote! {
|
||||
#(#attrs)*
|
||||
#vis #sig {
|
||||
Box::pin(async move {
|
||||
#body
|
||||
}).await
|
||||
}
|
||||
};
|
||||
|
||||
result.into()
|
||||
}
|
||||
44
packages/macro/src/specta.rs
Normal file
44
packages/macro/src/specta.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use quote::quote;
|
||||
use syn::{meta::ParseNestedMeta, parse_macro_input, Item, Result};
|
||||
|
||||
pub fn specta(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let mut attrs = SpectaAttributes::default();
|
||||
let parser = syn::meta::parser(|meta| attrs.parse(meta));
|
||||
parse_macro_input!(attr with parser);
|
||||
|
||||
let input = parse_macro_input!(item as Item);
|
||||
|
||||
let specta_event_type = if attrs.with_event {
|
||||
quote! {
|
||||
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let specta_type = quote! {
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
};
|
||||
|
||||
let expanded = quote! {
|
||||
#specta_event_type
|
||||
#specta_type
|
||||
#input
|
||||
};
|
||||
|
||||
expanded.into()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct SpectaAttributes {
|
||||
with_event: bool,
|
||||
}
|
||||
|
||||
impl SpectaAttributes {
|
||||
fn parse(&mut self, meta: ParseNestedMeta) -> Result<()> {
|
||||
if meta.path.is_ident("with_event") {
|
||||
self.with_event = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue