refactor: Switch to sea-orm

This commit is contained in:
LynithDev 2025-03-30 13:34:26 +02:00
parent 7b82ad6887
commit 33ac7b4397
129 changed files with 2787 additions and 4219 deletions

1
.env.template Normal file
View file

@ -0,0 +1 @@
DATABASE_URL=sqlite://path/to/user_data.db

2899
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,37 +1,32 @@
[workspace]
resolver = "2" # virtual workspace resolver so we don't need a root package
members = [
"apps/testing", # onelauncher_test -- test the core crate in a cli setting
"apps/desktop", # onelauncher_gui -- the rust code for our tauri app
"packages/core", # onelauncher -- the rust core for onelauncher
"packages/core_new",
"packages/macros", # onelauncher_macros -- utility procedural macros for onelauncher
"packages/gamemode", # onelauncher_gamemode -- rust wrapper for feral gamemode
"packages/utils" # onelauncher_utils -- standard utilities for use within all crates
"packages/core",
"packages/entity",
"packages/gamemode",
"packages/macro",
"packages/migration",
]
[workspace.package] # sets default [package] settings for workspace members
[workspace.package]
license = "GPL-3.0-only"
edition = "2021" # latest rust edition
edition = "2024"
repository = "https://github.com/Polyfrost/OneLauncher"
documentation = "https://docs.polyfrost.org/onelauncher"
readme = "README.md"
homepage = "https://polyfrost.org/projects/onelauncher"
authors = [ "Polyfrost", "Polyfrost Contributors" ]
version = "0.1.0-alpha.2" # bump this manually for now
version = "0.2.0-alpha.2"
[workspace.dependencies]
# # ============= WORKSPACE ============= # #
# best practice to define all workspace crates in the root
# even if they are not use, with no features enabled.
onelauncher = { path = "./packages/core" }
onelauncher_macros = { path = "./packages/macros" }
onelauncher_gui = { path = "./apps/desktop" }
onelauncher_test = { path = "./apps/testing" }
# workspace
onelauncher_core = { path = "./packages/core" }
onelauncher_entity = { path = "./packages/entity" }
onelauncher_gamemode = { path = "./packages/gamemode" }
onelauncher_utils = { path = "./packages/utils" }
onelauncher_macro = { path = "./packages/macro" }
onelauncher_migration = { path = "./packages/migration" }
# # ============= TAURI ============= # #
# tauri
tauri = { version = "2.0", features = [
"rustls-tls",
"tracing",
@ -48,7 +43,10 @@ tauri-plugin-updater = { version = "2.0" }
tauri-plugin-dialog = { version = "2.0" }
tauri-plugin-deep-link = { version = "2.0" }
# # ============= CORE ============= # #
# core
interpulse = { version = "=1.2.2", features = [ "specta" ] }
# code gen
specta = { version = "2.0.0-rc.20", features = [
"chrono",
"uuid",
@ -60,19 +58,18 @@ specta = { version = "2.0.0-rc.20", features = [
] }
tauri-specta = { version = "2.0.0-rc.20", features = [ "javascript", "typescript", "derive" ] }
specta-typescript = { version = "=0.0.7" }
interpulse = { version = "=1.2.2", features = [ "specta" ] }
# # ============= SECURITY ============= # #
# security
iota_stronghold = { version = "2.1" }
zeroize = { version = "1.8", features = [ "zeroize_derive" ] }
# # ============= LOGGING ============= # #
# logging
tracing = { version = "0.1" }
tracing-subscriber = { version = "0.3", features = [ "chrono", "env-filter" ] }
tracing-error = { version = "0.2" }
tracing-appender = { version = "0.2" }
# # ============= SERIALIZATION ============= # #
# serialization & text
serde = { version = "1.0", features = [ "derive" ] }
serde_json = { version = "1.0" }
serde_ini = { version = "0.2" }
@ -90,7 +87,7 @@ uuid = { version = "1.10", features = [ "serde", "v4" ] }
toml = { version = "0.8" }
nom = { version = "7.1" }
# # ============= NETWORK ============= # #
# network
reqwest = { version = "0.12", default-features = false, features = [
"json",
"stream",
@ -107,7 +104,7 @@ reqwest = { version = "0.12", default-features = false, features = [
async-tungstenite = { version = "0.28", features = [ "tokio-runtime", "tokio-rustls-webpki-roots" ] }
webbrowser = { version = "1.0" }
# # ============= SYSTEM ============= # #
# io
tokio = { version = "1.40", features = [ "full" ] }
tokio-stream = { version = "0.1.16", features = [ "fs" ] }
notify = { version = "6.1", default-features = false }
@ -119,49 +116,40 @@ dirs = { version = "5.0" }
async_zip = { version = "0.0.17", features = [ "full" ] }
async-compression = { version = "0.4.12", features = [ "flate2", "tokio" ] }
flate2 = { version = "1.0" }
bytes = { version = "1.7" }
# crypto
sha1_smol = { version = "1.0", features = [ "std" ] }
sha2 = { version = "0.10" }
murmur2 = { version = "0.1.0" }
md5 = { version = "0.7.0" }
bytes = { version = "1.7" }
# # ============= UTILITY ============= # #
sqlx = { version = "0.8", features = [
"runtime-tokio",
"sqlite",
# db
sea-orm = { version = "1.1", features = [
"sqlx-sqlite",
"runtime-tokio-rustls",
"macros",
"derive",
"migrate",
"uuid",
"chrono"
"with-chrono",
"with-json",
"with-uuid"
] }
sea-orm-migration = { version = "1.1", features = [ "sqlx-sqlite", "runtime-tokio-rustls" ] }
# utils
thiserror = { version = "1.0" }
anyhow = { version = "1.0" }
async-channel = { version = "2.3" }
mimalloc = { version = "0.1" }
pin-project-lite = { version = "0.2" }
once_cell = { version = "1.19" }
quote = { version = "1.0" }
syn = { version = "2.0", features = [ "full" ] }
sysinfo = { version = "0.31", features = [ "serde" ] }
discord-rich-presence = { version = "0.2" }
winreg = { version = "0.52" }
paste = { version = "1.0" }
dashmap = { version = "6.1", features = [ "serde" ] }
lazy_static = { version = "1.5" }
indicatif = { version = "0.17" }
futures = { version = "0.3" }
async-recursion = { version = "1.1" }
async-trait = { version = "0.1" }
sysinfo = { version = "0.31", features = [ "serde" ] }
sys-info = { version = "0.9" }
whoami = { version = "1.5" }
cocoa = { version = "0.26" }
objc = { version = "0.2" }
cc = { version = "1.1" }
libc = { version = "0.2" }
dotenvy = { git = "https://github.com/allan2/dotenvy/", rev = "db0d6aa8fc8d1b0ddbfe94b5c833372e463eb1c5" }
# https://github.com/tauri-apps/tauri/blob/dev/crates/tauri/Cargo.toml#L102
webkit2gtk = { version = "=2.0.1", features = [ "v2_40" ] }
# macro
syn = { version = "2" }
quote = { version = "1" }
[workspace.lints.clippy]
all = { level = "warn", priority = -1 }
@ -197,10 +185,6 @@ deprecated = "warn"
deprecated_in_future = "forbid"
unused_must_use = "deny"
[patch.crates-io]
# rspc = { git = "https://github.com/oscartbeaumont/rspc", rev = "9f26d6400affdb501837cb023678268ad72feb9b" }
# rspc-tauri2 = { git = "https://github.com/specta-rs/tauri2", rev = "6b9419566e2a826fb2a62538c7647f4524ab0fea" }
[profile.dev]
# add debug symbols/logic and optimize for reduced build time (parallel)
debug = true # add debug info

View file

@ -28,15 +28,17 @@
"test": "pnpm test:vi && pnpm --filter=./* run test",
"format": "pnpm format:es && pnpm format:rs",
"lint": "pnpm lint:es && pnpm lint:rs",
"migrate": "sea-orm-cli migrate -d ./packages/migration",
"entity:gen": "sea-orm-cli generate entity -o packages/entity/src/entities --with-serde both --model-extra-attributes 'onelauncher_macro::specta'",
"frontend": "pnpm --filter @onelauncher/frontend -- ",
"client": "pnpm --filter @onelauncher/client -- ",
"desktop": "pnpm --filter @onelauncher/desktop -- ",
"core_new": "pnpm --filter @onelauncher/core_new -- ",
"core": "pnpm --filter @onelauncher/core -- ",
"scripts": "pnpm --filter @onelauncher/scripts -- ",
"testing": "pnpm --filter @onelauncher/testing --",
"tauri": "pnpm desktop tauri-ext",
"postupdate": "cargo update",
"----------------------": "----------------------",
"test:vi": "pnpm vitest run",
"test:update": "pnpm vitest -u",
"test:ui": "pnpm vitest --ui",

View file

@ -1,5 +1,5 @@
[package]
name = "onelauncher"
name = "onelauncher_core"
description = "The core of OneLauncher"
version = { workspace = true }
license = { workspace = true }
@ -10,67 +10,78 @@ readme = { workspace = true }
homepage = { workspace = true }
authors = { workspace = true }
[lib]
path = "src/lib.rs"
[features]
default = []
# feature flag allowing us to interact with tauri (for loading animations, errors showing on gui, etc)
# also allows us to compile onelauncher core without tauri
tauri = [ "dep:tauri", "dep:tauri-specta", "specta" ]
default = [ "tauri" ]
tauri = [
"dep:tauri",
"dep:tauri-specta",
"specta",
"gui"
]
specta = [ "dep:specta" ]
cli = [ "dep:indicatif" ]
gui = []
[lints]
workspace = true
[dependencies]
# features[default]
onelauncher_macros = { workspace = true }
onelauncher_utils = { workspace = true }
# core
interpulse = { workspace = true }
onelauncher_migration = { workspace = true }
onelauncher_entity = { workspace = true }
onelauncher_macro = { workspace = true }
# logging
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-error = { workspace = true }
tracing-appender = { workspace = true }
notify = { workspace = true }
notify-debouncer-mini = { workspace = true }
iota_stronghold = { workspace = true }
zeroize = { workspace = true }
tokio = { workspace = true }
# serialization
serde = { workspace = true }
serde_json = { workspace = true }
serde_ini = { workspace = true }
serde_repr = { workspace = true }
serde_with = { workspace = true }
paste = { workspace = true }
# utils
sea-orm = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
futures = { workspace = true }
zip = { workspace = true }
uuid = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }
chrono = { workspace = true }
reqwest = { workspace = true }
dirs = { workspace = true }
regex = { workspace = true }
sysinfo = { workspace = true }
whoami = { workspace = true }
rand = { workspace = true }
url = { workspace = true }
async-trait = { workspace = true }
# integration
discord-rich-presence = { workspace = true }
# crypto
sha1_smol = { workspace = true }
sha2 = { workspace = true }
murmur2 = { workspace = true }
md5 = { workspace = true }
# io
zip = { workspace = true }
async_zip = { workspace = true }
discord-rich-presence = { workspace = true }
regex = { workspace = true }
sysinfo = { workspace = true }
whoami = { workspace = true }
lazy_static = { workspace = true }
async-tungstenite = { workspace = true }
async-recursion = { workspace = true }
tokio-stream = { workspace = true }
dirs = { workspace = true }
bytes = { workspace = true }
tempfile = { workspace = true }
rand = { workspace = true }
p256 = { workspace = true }
byteorder = { workspace = true }
base64 = { workspace = true }
flate2 = { workspace = true }
toml = { workspace = true }
url = { workspace = true }
async-compression = { workspace = true }
dunce = { workspace = true }
# features[tauri]
tauri = { workspace = true, optional = true }
@ -82,11 +93,5 @@ specta = { workspace = true, optional = true }
# features[cli]
indicatif = { workspace = true, optional = true }
[build-dependencies]
dotenvy = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
winreg = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
onelauncher_gamemode = { workspace = true }

View file

@ -1,14 +1,17 @@
{
"name": "@onelauncher/core",
"version": "0.1.0-alpha.2",
"version": "0.2.0-alpha.1",
"description": "Next-generation open source Minecraft launcher core",
"license": "GPL-3.0-only",
"repository": {
"type": "git",
"url": "git@github.com:polyfrost/onelauncher",
"directory": "packages/core"
"directory": "packages/core-new"
},
"bugs": "https://github.com/polyfrost/onelauncher/issues",
"sideEffects": false,
"scripts": {}
"scripts": {
"cargo": "cargo",
"binary": "cargo run --bin"
}
}

View file

@ -1,5 +1,56 @@
use crate::store::{IngressProcessType, IngressProcessor};
use crate::{store::{ingress::{IngressId, IngressType}, State}, LauncherResult};
#[tracing::instrument]
pub async fn init_ingress(
ingress_type: IngressType,
message: &str,
total: f64,
) -> LauncherResult<IngressId> {
let state = State::get().await?;
let processor = &state.ingress_processor;
let id = processor.create(ingress_type, message.to_string(), total).await?;
Ok(id)
}
#[tracing::instrument]
pub async fn send_ingress(id: &IngressId, increment: f64) -> LauncherResult<()> {
let state = State::get().await?;
let processor = &state.ingress_processor;
processor.send(id.0, increment).await?;
Ok(())
}
#[cfg(test)]
pub mod tests {
#[cfg(feature = "cli")]
#[tokio::test]
pub async fn create_and_update_ingress() -> LauncherResult<()> {
assert!(cfg!(feature = "cli"));
use std::time::Duration;
use crate::{api::{ingress::{init_ingress, send_ingress}, proxy::proxy_cli::ProxyCli}, initialize_core, store::ingress::IngressType, LauncherResult};
initialize_core(ProxyCli::new()).await?;
let id = init_ingress(IngressType::Download { file_name: "Some-Mod-1.8.9.jar".into() }, "This is a test message", 100.0).await?;
tokio::time::sleep(Duration::from_millis(2350)).await;
send_ingress(&id, 10.0).await?;
tokio::time::sleep(Duration::from_millis(2500)).await;
send_ingress(&id, 20.0).await?;
tokio::time::sleep(Duration::from_millis(2500)).await;
send_ingress(&id, 70.0).await?;
tokio::time::sleep(Duration::from_millis(2500)).await;
drop(id);
Ok(())
}
pub async fn check_ingress_feeds() -> crate::Result<bool> {
IngressProcessor::finished(IngressProcessType::IngressFeed).await
}

View file

@ -1,31 +1,2 @@
//! **`OneLauncher` API**
//!
//! API for interacting with our Rust core.
pub mod cluster;
pub mod handler;
pub mod ingress;
pub mod java;
pub mod metadata;
pub mod minecraft;
pub mod package;
pub mod processor;
pub mod proxy;
pub mod settings;
pub mod data {
pub use crate::store::{
ClusterMeta, Credentials, Directories, InitHooks, JavaOptions, Loader, ManagedDependency,
ManagedPackage, ManagedUser, ManagedVersion, Memory, MinecraftCredentials, PackageData,
PackageType, Resolution, Settings,
};
}
pub mod prelude {
pub use crate::cluster::{self, create, Cluster};
pub use crate::data::*;
pub use crate::proxy::InternetPayload;
pub use crate::store::{ClusterPath, JavaVersions, PackageDependency, PackagePath};
pub use crate::utils::java::JavaVersion;
pub use crate::{java, metadata, minecraft, package, processor, settings, State};
}

View file

@ -1,14 +1,13 @@
use serde::Serialize;
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[onelauncher_macro::specta(with_event)]
#[derive(Serialize, Debug, Clone)]
pub struct MessagePayload {
pub level: MessageLevel,
pub message: String,
}
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[onelauncher_macro::specta]
#[derive(Serialize, Debug, Clone)]
pub enum MessageLevel {
Info,

View file

@ -1,277 +1,20 @@
//! Proxy for Tauri/CLI specific features and communication
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::{OnceCell, RwLock};
use uuid::Uuid;
pub mod message;
use crate::store::{ClusterPath, IngressProcessor};
pub mod send;
pub mod utils;
pub use utils::*;
static PROXY_STATE: OnceCell<Arc<ProxyState>> = OnceCell::const_new();
pub struct ProxyState {
#[cfg(feature = "tauri")]
pub app: tauri::AppHandle<tauri::Wry>,
pub ingress_feeds: RwLock<HashMap<Uuid, Ingress>>,
}
pub mod proxy_tauri;
#[derive(Serialize, Debug, Clone)]
pub struct Ingress {
pub ingress_uuid: Uuid,
pub message: String,
pub ingress_type: IngressType,
pub total: f64,
pub current: f64,
#[serde(skip)]
pub last_sent: f64,
#[cfg(feature = "cli")]
#[serde(skip)]
pub cli: indicatif::ProgressBar,
pub mod proxy_cli;
#[async_trait::async_trait]
pub trait LauncherProxy: Send + Sync {
async fn send_ingress(&self, ingress: crate::store::ingress::IngressPayload) -> crate::LauncherResult<()>;
async fn send_message(&self, message: message::MessagePayload) -> crate::LauncherResult<()>;
#[cfg(feature = "gui")]
fn hide_main_window(&self) -> crate::LauncherResult<()>;
#[cfg(feature = "gui")]
fn show_main_window(&self) -> crate::LauncherResult<()>;
}
#[derive(Serialize, Debug, Clone)]
pub struct IngressId(Uuid);
impl Drop for IngressId {
#[clippy::has_significant_drop]
fn drop(&mut self) {
let ingress_uuid = self.0;
tokio::spawn(async move {
if let Ok(proxy_state) = ProxyState::get().await {
let mut ingress_feeds = proxy_state.ingress_feeds.write().await;
#[cfg(any(feature = "tauri", feature = "cli"))]
if let Some(ingress) = ingress_feeds.remove(&ingress_uuid) {
#[cfg(feature = "cli")]
{
let cli = ingress.cli;
cli.finish();
}
#[cfg(feature = "tauri")]
{
use tauri::Emitter;
let event = ingress.ingress_type.clone();
let fraction = ingress.current / ingress.total;
let _ = proxy_state.app.emit(
"ingress",
IngressPayload {
fraction: None,
message: "Complete".to_string(),
event,
ingress_uuid,
},
);
tracing::trace!("exited at {fraction} for ingress: {:?}", ingress_uuid);
}
}
#[cfg(not(any(feature = "tauri", feature = "cli")))]
ingress_feeds.remove(&ingress_uuid);
}
if crate::State::initalized() {
let _ = IngressProcessor::finish(
crate::store::IngressProcessType::IngressFeed,
ingress_uuid,
)
.await;
}
});
}
}
impl ProxyState {
#[cfg(feature = "tauri")]
pub async fn initialize(handle: &tauri::AppHandle<tauri::Wry>) -> crate::Result<Arc<Self>> {
PROXY_STATE
.get_or_try_init(|| async {
Ok(Arc::new(Self {
app: handle.clone(),
ingress_feeds: RwLock::new(HashMap::new()),
}))
})
.await
.cloned()
}
#[cfg(not(feature = "tauri"))]
pub async fn initialize() -> crate::Result<Arc<Self>> {
PROXY_STATE
.get_or_try_init(|| async {
Ok(Arc::new(Self {
ingress_feeds: RwLock::new(HashMap::new()),
}))
})
.await
.cloned()
}
#[cfg(feature = "tauri")]
pub async fn get_main_window() -> crate::Result<tauri::WebviewWindow> {
use tauri::Manager;
let value = Self::get().await?;
Ok(value.app.get_webview_window("main").unwrap())
}
#[allow(clippy::unused_async)]
#[cfg(feature = "tauri")]
pub async fn get() -> crate::Result<Arc<Self>> {
Ok(PROXY_STATE.get().ok_or(ProxyError::NotInitialized)?.clone())
}
#[cfg(not(feature = "tauri"))]
pub async fn get() -> crate::Result<Arc<Self>> {
Self::initialize().await
}
pub async fn list_ingress_feeds() -> crate::Result<HashMap<Uuid, Ingress>> {
let value = Self::get().await?;
let read = value.ingress_feeds.read().await;
let mut display_list: HashMap<Uuid, Ingress> = HashMap::new();
for (uuid, ingress) in read.iter() {
display_list.insert(*uuid, ingress.clone());
}
Ok(display_list)
}
}
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug, Hash, PartialEq, Eq)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum IngressType {
Initialize,
DownloadJava {
version: u32,
},
DownloadLoader {
cluster_path: PathBuf,
cluster_name: String,
},
SyncCluster {
cluster_path: PathBuf,
cluster_name: String,
},
InferPackages,
CopyCluster {
import: PathBuf,
cluster_name: String,
},
SyncConfig {
new_path: PathBuf,
},
Archival {
cluster_path: PathBuf,
cluster_name: String,
},
DownloadPackage {
cluster_path: PathBuf,
package_name: String,
icon: Option<PathBuf>,
package_id: Option<String>,
package_version: Option<String>,
},
DownloadPack {
cluster_path: PathBuf,
package_name: String,
icon: Option<String>,
package_version: String,
},
}
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
pub struct IngressPayload {
pub event: IngressType,
pub ingress_uuid: Uuid,
pub fraction: Option<f64>,
pub message: String,
}
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
pub struct OfflinePayload {
pub offline: bool,
}
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
pub struct MessagePayload {
pub message: String,
}
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
#[serde(tag = "event")]
pub enum InternetPayload {
InstallPackage { id: String },
InstallPack { id: String },
InstallPath { path: PathBuf },
}
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
pub struct ProcessPayload {
pub uuid: Uuid,
pub pid: u32,
pub event: ProcessPayloadType,
pub message: String,
}
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum ProcessPayloadType {
Started,
Modified,
Finished,
Logging,
}
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
pub struct ClusterPayload {
pub uuid: Uuid,
pub cluster_path: ClusterPath,
pub path: PathBuf,
pub name: String,
pub event: ClusterPayloadType,
}
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
#[serde(rename_all = "snake_case")]
pub enum ClusterPayloadType {
Created,
Inserted,
Synced,
Edited,
Deleted,
}
#[derive(Debug, thiserror::Error)]
pub enum ProxyError {
#[error("event state was not initialized!")]
NotInitialized,
#[error("non-existant ingress of key: {0}")]
NoIngressFound(Uuid),
#[cfg(feature = "tauri")]
#[error("Tauri error: {0}")]
TauriError(#[from] tauri::Error),
}

View file

@ -1,4 +1,4 @@
use onelauncher_new::store::Dirs;
use onelauncher_core::store::Dirs;
#[tokio::main]
async fn main() {

View file

@ -1,11 +1,14 @@
//! **`OneLauncher` Constants**
//!
//! Public constant variables and strings that are used throughout the launcher.
//! Can be shared publically, any secrets should be stored in [`crate::store::credentials`].
// =========== Core Metadata ===========
/// The name of the launcher.
pub const NAME: &str = "OneLauncher";
pub const NAME: &str = match option_env!("LAUNCHER_NAME") {
Some(name) => name,
None => "OneLauncher",
};
/// The version of the launcher (from `../Cargo.toml` env).
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
@ -38,7 +41,7 @@ pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2";
pub const MODRINTH_V3_API_URL: &str = "https://api.modrinth.com/v3";
/// The Curseforge API base url.
pub const CURSEFORGE_API_URL: &str = "https://api.curseforge.com";
/// The Curseforge API key. Reserved for use in OneLauncher projects only. Do not use in other projects without permission from the OneLauncher team.
/// The Curseforge API key. Reserved for use in Polyfrost projects only. Do not use in other projects without permission from the Polyfrost team.
pub const CURSEFORGE_API_KEY: &str = "$2a$10$6utA1UNSmFPrE/Lh7b7ndeeGmiOkjKNY8kpFB0fsmE/d42ZAfFgCe";
/// The Minecraft game ID on Curseforge.
pub const CURSEFORGE_GAME_ID: u32 = 432;
@ -48,7 +51,7 @@ pub const METADATA_API_URL: &str = "https://meta.polyfrost.org";
pub const FEATURED_PACKAGES_URL: &str = "https://polyfrost.org/meta/onelauncher/featured.json";
/// <https://mclo.gs>/ API base url.
pub const MCLOGS_API_URL: &str = "https://api.mclo.gs/1";
/// https://skyclient.co/ metadata base url.
/// <https://skyclient.co/> metadata base url.
pub const SKYCLIENT_BASE_URL: &str = "https://raw.githubusercontent.com/SkyblockClient/SkyblockClient-REPO/refs/heads/main/v1";
// =========== Paths ===========
@ -56,10 +59,6 @@ pub const SKYCLIENT_BASE_URL: &str = "https://raw.githubusercontent.com/Skyblock
///
/// [`Settings`]: crate::store::Settings
pub const SETTINGS_FILE: &str = "settings.json";
/// The public `cluster.json` file used to store the global [`Clusters`] state.
///
/// [`Clusters`]: crate::store::Clusters
pub const CLUSTER_FILE: &str = "cluster.json";
/// The public `authentication.json` file used to store the global [`MinecraftAuth`] state.
///
/// [`MinecraftAuth`]: crate::store::MinecraftAuth
@ -75,15 +74,6 @@ pub const PROCESSOR_FILE: &str = "processor.json";
/// [`Settings`]: crate::store::Settings
pub const CURRENT_SETTINGS_FORMAT_VERSION: u32 = 1;
/// The constant core caches folder.
pub const CACHES_FOLDER: &str = "caches";
/// The constant core crash reports folder.
pub const CRASH_PATH: &str = "crash-reports";
/// The constant core clusters folder.
pub const CLUSTERS_FOLDER: &str = "clusters";
/// The constant core metadata folder.
pub const METADATA_FOLDER: &str = "metadata";
// =========== Hacky Mojang-spec OS constants ===========
#[cfg(target_os = "windows")]
pub const TARGET_OS: &str = "windows";

View file

@ -1,180 +1,22 @@
//! **`OneLauncher` Errors**
//!
//! Public wrappers around common `OneLauncher` errors.
use tracing_error::InstrumentError;
/// A standardized [`thiserror::Error`] type that is used across the launcher
#[derive(thiserror::Error, Debug)]
pub enum ErrorKind {
/// Wrapper around [`tokio::sync::AcquireError`] to handle sempahore errors.
#[error("failed to acquire a tokio semaphore: {0}")]
AcquireError(#[from] tokio::sync::AcquireError),
pub enum LauncherError {
#[error(transparent)]
DirError(#[from] crate::store::DirectoryError),
#[error(transparent)]
IOError(#[from] crate::utils::io::IOError),
#[error(transparent)]
IngressError(#[from] crate::store::ingress::IngressError),
/// Wrapper around [`tokio::sync::oneshot::error::RecvError`] to handle recieve errors.
#[error("failed to handle async reciever: {0}")]
RecvError(#[from] tokio::sync::oneshot::error::RecvError),
/// Wrapper around [`tokio::task::JoinError`] to handle tokio join handle errors.
#[error("failed to handle an async task: {0}")]
JoinError(#[from] tokio::task::JoinError),
/// Wrapper around [`flate2::CompressError`] to handle flate compression errors.
#[error("failed to compress a file with flate: {0}")]
FlateError(#[from] flate2::CompressError),
/// Wrapper around [`flate2::DecompressError`] to handle flate decompression errors.
#[error("failed to decompress a file with flate: {0}")]
DeflateError(#[from] flate2::DecompressError),
/// Wrapper around [`uuid::Error`] to handle UUID parsing errors.
#[error("failed to parse and handle uuids: {0}")]
UUIDError(#[from] uuid::Error),
/// Wrapper around [`interpulse::Error`] to handle interpulse errors.
#[error("failed to parse and handle metadata: {0}")]
MetadataError(#[from] interpulse::Error),
/// Wrapper around [`serde_json::Error`] to handle Serde JSON parsing errors.
#[error("failed to manage serialization of json: {0}")]
#[error(transparent)]
SerdeError(#[from] serde_json::Error),
/// Wrapper around [`serde_ini::de::Error`] to handle Serde INI parsing errors.
#[error("failed to manage deserialization of ini: {0}")]
INIError(#[from] serde_ini::de::Error),
/// Wrapper around [`reqwest::Error`] to handle HTTP errors.
#[error("failed to establish an http connection: {0}")]
HTTPError(#[from] reqwest::Error),
/// Wrapper around [`zip::result::ZipError`] to handle zip errors.
#[error("failed to manage zip files: {0}")]
ZipError(#[from] zip::result::ZipError),
/// Wrapper around [`async_zip::error::ZipError`] to handle async zip errors.
#[error("failed to manage zip files asynchronously: {0}")]
AsyncZipError(#[from] async_zip::error::ZipError),
/// Wrapper around [`anyhow::Error`] to handle generic errors.
#[error(transparent)]
AnyhowError(#[from] anyhow::Error),
#[error(transparent)]
DbError(#[from] sea_orm::DbErr),
/// Wrapper around [`chrono::ParseError`] to handle date parsing errors.
#[error("failed to parse and handle datetime: {0}")]
ChronoError(#[from] chrono::ParseError),
/// Wrapper around [`crate::state::DirectoryError`] to handle directory errors.
#[error("failed to get directory: {0}")]
DirectoryError(#[from] crate::store::DirectoryError),
/// Wrapper around [`crate::api::proxy::ProxyError`] to handle ingress errors.
#[error("failed to handle api communication: {0}")]
ProxyError(#[from] crate::api::proxy::ProxyError),
/// Wrapper around [`crate::store::MinecraftAuthError`] to handle Minecraft auth errors.
#[error("failed to authenticate with microsoft: {0}")]
AuthError(#[from] crate::store::MinecraftAuthError),
/// Wrapper around [`crate::api::cluster::create::CreateClusterError`] to handle Cluster creation errors.
#[error("failed to create clusters: {0}")]
CreateClusterError(#[from] crate::api::cluster::create::CreateClusterError),
/// Wrapper around [`notify:Error`] to handle file watching errors.
#[error("failed to watch file {0}")]
NotifyError(#[from] notify::Error),
/// Wrapper around [`regex::Error`] to handle `RegExp` errors.
#[error("string verification with regex failed: {0}")]
RegexError(#[from] regex::Error),
/// Wrapper around [`reqwest::header::ToStrError`] to handle header conversion errors.
#[error("failed to convert header to string: {0}")]
HeaderToStrError(#[from] reqwest::header::ToStrError),
/// Wrapper around [`async_tungstenite::tungstenite::Error`] to handle WebSocket errors.
#[error("failed to handle websocket connection: {0}")]
WebSocketError(#[from] async_tungstenite::tungstenite::Error),
/// Indicates an error with SHA checksum.
#[error("checksum {0} did not match {1}!")]
HashError(String, String),
/// Wrapper around [`onelauncher_utils::io::IOError`] to handle wrapped `std::io` errors.
#[error("error handling IO operations: {0}")]
IOError(#[from] onelauncher_utils::io::IOError),
/// Wrapper around [`std::io::Error`] to handle non-wrapped `std::io` errors.
#[error("error handling standard IO operations: {0}")]
StdIOError(#[from] std::io::Error),
/// Wrapper around [`crate::utils::java::JavaError`] to handle java errors.
#[error("failed to manage java installations: {0}")]
JavaError(#[from] crate::utils::java::JavaError),
/// Wrapper around [`crate::store::StrongholdError`] to handle stronghold errors.
#[error("failed to manage credentials with IOTA stronghold: {0}")]
StrongholdError(#[from] crate::store::StrongholdError),
/// Wrapper around [`url::ParseError`] to handle URL parsing errors
#[error("failed to parse and handle URLs: {0}")]
URLError(#[from] url::ParseError),
/// Wrapper around [`tauri::Error`] to handle Tauri errors when the feature flag is enabled
#[cfg(feature = "tauri")]
#[error("error handling tauri: {0}")]
#[error(transparent)]
TauriError(#[from] tauri::Error),
}
/// A safe, accessible error structure wrapper around [`ErrorKind`] for use with [`tracing_error`].
#[derive(Debug)]
pub struct Error {
/// The raw, inner [`ErrorKind`], wrapped in an [`Arc`].
pub raw: std::sync::Arc<ErrorKind>,
/// The traced [`ErrorKind`] ([`tracing_error`]) to be parsed and logged.
pub source: tracing_error::TracedError<std::sync::Arc<ErrorKind>>,
}
impl From<Error> for String {
/// Converts an [`Error`] into a [`String`] value.
fn from(value: Error) -> Self {
value.to_string()
}
}
impl std::error::Error for Error {
/// Get the source for the inner traced [`Error`].
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source.source()
}
}
impl std::fmt::Display for Error {
/// Format the [`Error`] and output it into the formatter buffer.
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(fmt, "{}", self.source)
}
}
impl<E: Into<ErrorKind>> From<E> for Error {
/// Converts an [`ErrorKind`] error into a traced [`Error`].
/// Useful for reducing error mapping and standardizing error types.
fn from(source: E) -> Self {
let error = Into::<ErrorKind>::into(source);
let boxed_error = std::sync::Arc::new(error);
Self {
raw: boxed_error.clone(),
source: boxed_error.in_current_span(),
}
}
}
impl ErrorKind {
/// Converts the current [`ErrorKind`] into a traced [`Error`].
pub fn as_error(self) -> Error {
self.into()
}
}
/// Alias for a [`core::result::Result`] with the error type [`Error`].
pub type Result<T> = anyhow::Result<T, Error>;
pub type LauncherResult<T> = Result<T, LauncherError>;

View file

@ -1,20 +1,25 @@
#![allow(clippy::all)]
//! # `OneLauncher`
//!
//! A library used as a core for our launcher and Rust APIs.
use api::proxy::LauncherProxy;
use store::{proxy::ProxyState, Dirs, State};
use error::LauncherResult;
use logger::start_logger;
#[macro_use]
pub mod utils;
pub mod api;
pub mod constants;
pub mod error;
pub mod game;
pub mod logger;
pub mod utils;
pub mod store;
pub mod api;
pub mod error;
pub use api::proxy::{Ingress, IngressType, ProxyState};
pub use api::*;
pub use error::*;
pub use logger::start_logger;
pub use store::{InnerPathLinux, State};
mod logger;
pub use onelauncher_entity as entity;
pub use onelauncher_migration as migration;
pub async fn initialize_core(proxy_backend: impl LauncherProxy + 'static) -> LauncherResult<()> {
Dirs::get().await?;
start_logger().await;
ProxyState::initialize(proxy_backend).await?;
State::get().await?;
Ok(())
}

View file

@ -1,74 +1,72 @@
//! **`OneLauncher` Logger**
//!
//! Public utilities for [`tracing`] logging in `OneLauncher`.
//!
//! - `RUST_LOG=xxx` is a variable which defines tracing configuration (`ERROR > WARN > INFO > DEBUG > TRACE`)
//! - `RUST_LOG="onelauncher=trace"` will show **all** mesages from `OneLauncher` and not other dependencies.
//! - `RUST_LOG="info"` will show info, warn, and error logs.
//!
//! Errors returned to [`tauri`] are displayed as tracing error logs.
//! These errors also include an attached span trace if the error is from a [`tracing_error`] and the level is info, debug, or trace.
//! Tracing errors also provide Sentry metadata to be handled.
//! To set the environment variable on `unix` you can use the following command: `RUST_LOG="xxx" {your command}`
//!
//! The default `RUST_LOG` is `onelauncher=show`, meaning only logs from `OneLauncher` will be displayed at the info, warn, and error levels.
use tracing_appender::non_blocking::WorkerGuard;
// TODO: refactor logging and add more env filters.
/// Initialize the global [`tracing`] logger in development.
// Handling for the live development logging
// This will log to the console, and will not log to a file
#[cfg(debug_assertions)]
#[must_use]
pub fn start_logger() -> Option<WorkerGuard> {
use tracing_subscriber::prelude::*;
pub async fn start_logger() {
use tracing_subscriber::{prelude::*, util::SubscriberInitExt};
let filter = tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
tracing_subscriber::EnvFilter::new("onelauncher=info,onelauncher_gui=info")
});
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(format!("{}=info", env!("CARGO_PKG_NAME"))));
let subscriber = tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer()
.with_ansi(true)
.with_file(true)
.with_line_number(true)
.with_level(true)
.with_thread_names(true)
.pretty()
)
.with(filter)
.with(tracing_error::ErrorLayer::default());
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
None
.with(tracing_error::ErrorLayer::default())
.init();
}
/// Initialize the global [`tracing`] logger in production.
// Handling for the live production logging
// This will log to a file in the logs directory, and will not show any logs in the console
#[cfg(not(debug_assertions))]
pub fn start_logger() -> Option<WorkerGuard> {
use crate::store::Directories;
use tracing_appender::rolling::{RollingFileAppender, Rotation};
pub async fn start_logger() {
use chrono::Local;
use std::fs::OpenOptions;
use tracing_subscriber::fmt::time::ChronoLocal;
use tracing_subscriber::prelude::*;
let logs_dir = if let Some(directory) = Directories::logs_dir() {
directory
} else {
eprintln!("failed to start logger");
use crate::io::Dirs;
// Initialize and get logs directory path
let logs_dir = Dirs::get().await?.launcher_logs_dir();
let log_file_name = format!("launcher_{}.log", Local::now().format("%Y%m%d_%H%M%S"));
let log_file_path = logs_dir.join(log_file_name);
if let Err(err) = std::fs::create_dir_all(&logs_dir) {
eprintln!("Could not create logs directory: {err}");
}
let file = match OpenOptions::new()
.create(true)
.append(true)
.open(&log_file_path)
{
Ok(file) => file,
Err(e) => {
eprintln!("Could not start open log file: {e}");
return None;
}
};
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("onelauncher=info"));
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(format!("{}=info", env!("CARGO_PKG_NAME"))));
let file_appender = RollingFileAppender::new(Rotation::DAILY, logs_dir, "onelauncher.log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let subscriber = tracing_subscriber::registry()
tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.compact()
.with_writer(file)
.with_ansi(false)
.with_timer(ChronoLocal::rfc_3339()),
)
.with(filter)
.with(tracing_error::ErrorLayer::default());
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
Some(guard)
.with(tracing_error::ErrorLayer::default())
.init();
}

View file

@ -0,0 +1,30 @@
use std::{path::PathBuf, time::Duration};
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
use crate::{utils::io::IOError, LauncherResult};
use super::Dirs;
pub async fn create_pool_from_path(path: PathBuf) -> LauncherResult<DatabaseConnection> {
if !path.is_absolute() {
return Err(IOError::InvalidAbsolutePath(path).into());
}
let address = format!("sqlite:/{}?mode=rwc", path.to_string_lossy());
let mut opts = ConnectOptions::new(address);
opts.min_connections(5)
.max_connections(40)
.connect_timeout(Duration::from_secs(5))
.idle_timeout(Duration::from_secs(6 * 60 * 60 /* 6 hours */));
let db = Database::connect(opts).await?;
Ok(db)
}
#[tracing::instrument]
pub async fn create_pool() -> LauncherResult<DatabaseConnection> {
let path = Dirs::get().await?.db_file();
create_pool_from_path(path).await
}

View file

@ -1,191 +1,96 @@
//! Handles core directory management
use std::path::PathBuf;
use tokio::sync::RwLock;
use super::Settings;
use crate::constants;
use tokio::sync::OnceCell;
/// Directory management and utilities.
#[derive(Debug)]
pub struct Directories {
/// Settings: All small caches and the core settings.json
pub settings_dir: PathBuf,
/// Base Configuration: Clusters and all Assets (should be changable as a setting that can go across file systems)
/// By default this should be the same as the `settings_dir`
pub config_dir: RwLock<PathBuf>,
/// The current working directory of the directory manager.
pub cwd: PathBuf,
use crate::{constants, utils, LauncherResult};
/// The static [`OnceCell<RwLock<Dirs>>`] for storing the global directory state.
/// Should be initialized as soon as possible (preferably before logging)
static DIRS_GLOBAL: OnceCell<Dirs> = OnceCell::const_new();
pub struct Dirs {
base_dir: PathBuf,
}
impl Directories {
/// Bootstrap the initial settings directory on first startup.
#[must_use]
pub fn init_settings_dir() -> Option<PathBuf> {
Self::env_path("ONELAUNCHER_CONFIG")
impl Dirs {
pub async fn get() -> LauncherResult<&'static Self> {
DIRS_GLOBAL.get_or_try_init(async || {
Self::initialize()
}).await
}
fn initialize() -> LauncherResult<Self> {
let base_dir = utils::io::env_path("LAUNCHER_DIR")
.or_else(|| Some(dirs::data_dir()?.join(constants::NAME)))
}
.ok_or(DirectoryError::BaseDir)?;
/// Bootstrap the initial settings directory on first startup.
#[inline]
pub fn init_settings_file() -> crate::Result<PathBuf> {
let settings_dir = Self::init_settings_dir().ok_or(DirectoryError::ConfigDir)?;
Ok(settings_dir.join("settings.json"))
}
/// Initialize the core directory manager
#[tracing::instrument]
pub fn initalize(settings: &Settings) -> crate::Result<Self> {
let cwd = std::env::current_dir().map_err(DirectoryError::WorkingDir)?;
let settings_dir = Self::init_settings_dir().ok_or(DirectoryError::ConfigDir)?;
let config_dir = settings
.config_dir
.clone()
.ok_or(DirectoryError::ConfigDir)?;
Ok(Self {
settings_dir,
config_dir: RwLock::new(config_dir),
cwd,
base_dir
})
}
/// Get the base directory for the launcher.
#[must_use]
pub const fn base_dir(&self) -> &PathBuf {
&self.base_dir
}
/// Get the launcher's logs directory.
#[must_use]
pub fn launcher_logs_dir(&self) -> PathBuf {
self.base_dir().join("logs")
}
// --- FILES ---
#[must_use]
pub fn db_file(&self) -> PathBuf {
self.base_dir().join("user_data.db")
}
#[must_use]
pub fn settings_file(&self) -> PathBuf {
self.base_dir().join("settings.json")
}
// --- GAME DIRECTORIES ---
/// Get the `config_dir/metadata` folder within the core config directory.
#[inline]
pub async fn metadata_dir(&self) -> PathBuf {
self.config_dir
.read()
.await
.join(constants::METADATA_FOLDER)
#[must_use]
pub fn metadata_dir(&self) -> PathBuf {
self.base_dir().join("metadata")
}
/// Get the `config_dir/metadata/libraries` folder for Minecraft libraries.
#[inline]
pub async fn libraries_dir(&self) -> PathBuf {
self.metadata_dir().await.join("libraries")
#[must_use]
pub fn libraries_dir(&self) -> PathBuf {
self.metadata_dir().join("libraries")
}
/// Get the `config_dir/metadata/natives` folder for Minecraft natives.
#[must_use]
pub fn natives_dir(&self) -> PathBuf {
self.metadata_dir().join("natives")
}
/// Get the `config_dir/metadata/java` directory.
#[inline]
pub async fn java_dir(&self) -> PathBuf {
self.metadata_dir().await.join("java_versions")
#[must_use]
pub fn java_dir(&self) -> PathBuf {
self.metadata_dir().join("java")
}
/// Get the `config_dir/metadata/caches` directory.
#[inline]
pub async fn caches_dir(&self) -> PathBuf {
self.metadata_dir().await.join("caches")
}
/// Bootstrap the core logs directory and get it as a [`PathBuf`].
#[inline]
#[must_use]
pub fn logs_dir() -> Option<PathBuf> {
Self::init_settings_dir().map(|d| d.join("logs"))
}
/// Get the `settings_dir/settings.json` file as a [`PathBuf`].
#[inline]
pub fn settings_file(&self) -> PathBuf {
self.settings_dir.join(constants::SETTINGS_FILE)
}
/// Get a [`PathBuf`] from a provided environment variable.
#[inline]
fn env_path(name: &str) -> Option<PathBuf> {
std::env::var_os(name).map(PathBuf::from)
}
/// Get the `config_dir/metadata/assets` directory.
#[inline]
pub async fn assets_dir(&self) -> PathBuf {
self.metadata_dir().await.join("assets")
}
/// Get the `config_dir/metadata/assets/indexes` directory.
#[inline]
pub async fn index_dir(&self) -> PathBuf {
self.assets_dir().await.join("indexes")
}
/// Get the `config_dir/metadata/assets/objects` directory.
#[inline]
pub async fn objects_dir(&self) -> PathBuf {
self.assets_dir().await.join("objects")
}
/// Get the `config_dir/metadata/assets/objects/{hash}` directory.
#[inline]
pub async fn object_dir(&self, hash: &str) -> PathBuf {
self.objects_dir().await.join(&hash[..2]).join(hash)
}
/// Get the `config_dir/clusters` directory.
#[inline]
pub async fn clusters_dir(&self) -> PathBuf {
self.config_dir.read().await.join("clusters")
}
/// Get the `config_dir/clusters/{uuid}` directory.
#[inline]
pub async fn cluster_dir(&self, uuid: uuid::Uuid) -> PathBuf {
self.clusters_dir().await.join(uuid.to_string())
}
/// Get the `{cluster_path}/logs` directory.
#[inline]
pub async fn cluster_logs_dir(cluster_path: &super::ClusterPath) -> crate::Result<PathBuf> {
Ok(cluster_path.full_path().await?.join("logs"))
}
/// Get the `{cluster_path}/crash_reports` directory.
#[inline]
pub async fn crash_reports_dir(cluster_path: &super::ClusterPath) -> crate::Result<PathBuf> {
Ok(cluster_path.full_path().await?.join("crash-reports"))
}
/// Get the `config_dir/metadata/resources` directory.
#[inline]
pub async fn legacy_assets_dir(&self) -> PathBuf {
self.metadata_dir().await.join("resources")
}
/// Get the `config_dir/metadata/natives` directory.
#[inline]
pub async fn natives_dir(&self) -> PathBuf {
self.metadata_dir().await.join("natives")
}
/// Get the `config_dir/metadata/natives/{version}` directory.
#[inline]
pub async fn version_natives_dir(&self, version: &str) -> PathBuf {
self.natives_dir().await.join(version)
}
/// Get the Minecraft `config_dir/metadata/versions` directory.
#[inline]
pub async fn versions_dir(&self) -> PathBuf {
self.metadata_dir().await.join("versions")
}
/// Get the Minecraft `config_dir/metadata/versions/{version}` directory.
#[inline]
pub async fn version_dir(&self, version: &str) -> PathBuf {
self.versions_dir().await.join(version)
}
/// Get the `config_dir/icons` directory.
#[inline]
pub async fn icons_dir(&self) -> PathBuf {
self.config_dir.read().await.join("icons")
pub fn caches_dir(&self) -> PathBuf {
self.metadata_dir().join("caches")
}
}
/// Represents a core directory management error.
#[derive(Debug, thiserror::Error)]
pub enum DirectoryError {
/// An error for when a config directory cannot be initialized.
#[error("failed to get the user's config directory")]
ConfigDir,
/// A wrapper over [`std::io::Error`] for cwd fetching errors.
#[error("could not open working directory {0}")]
WorkingDir(std::io::Error),
#[error("Failed to get the base directory for the launcher.")]
BaseDir,
}

View file

@ -93,8 +93,7 @@ pub struct Ingress {
pub last_sent: f64,
}
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[onelauncher_macro::specta(with_event)]
#[derive(Serialize, Debug, Clone)]
pub struct IngressPayload {
pub id: Uuid,
@ -104,7 +103,7 @@ pub struct IngressPayload {
pub total: f64,
}
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[onelauncher_macro::specta]
#[derive(Serialize, Debug, Clone)]
pub enum IngressType {
Download {

View file

@ -0,0 +1,14 @@
use serde::{Deserialize, Serialize};
use crate::LauncherResult;
// pub async fn get_java_versions() -> LauncherResult<Vec<JavaVersion>> {
// // let java_versions = sqlx::query_as!(
// // JavaVersion,
// // r#"
// // SELECT id, major_version, display_name, absolute_path, full_version FROM java_versions
// // "#
// // );
// Ok(vec![])
// }

View file

@ -1,289 +1,11 @@
//! **`OneLauncher` Store**
//!
//! Core state and storage management for the launcher, managing all states.
use std::sync::Arc;
use std::time::Duration;
use notify::RecommendedWatcher;
use notify_debouncer_mini::Debouncer;
use tokio::sync::{OnceCell, RwLock, Semaphore};
use crate::proxy::send::{init_ingress_internal, send_ingress, send_offline};
use crate::utils::http;
use crate::utils::http::{FetchSemaphore, IoSemaphore};
pub(super) mod db;
pub mod ingress;
pub mod proxy;
pub mod java;
mod dirs;
pub use self::dirs::*;
mod minecraft;
pub use self::minecraft::*;
mod credentials;
pub use self::credentials::*;
mod discord;
pub use self::discord::*;
mod metadata;
pub use self::metadata::*;
mod settings;
pub use self::settings::*;
mod state;
mod java_versions;
pub use self::java_versions::*;
mod processor;
pub use self::processor::*;
mod clusters;
pub use self::clusters::*;
mod package;
pub use self::package::*;
mod oneconfig;
pub use self::oneconfig::*;
/// The static [`OnceCell<RwLock<State>>`] for storing the global runtime launcher state.
static LAUNCHER_STATE: OnceCell<RwLock<State>> = OnceCell::const_new();
/// The public `OneLauncher` state structure.
pub struct State {
/// Handles a boolean to check if the launcher is connected to the internet
pub offline: RwLock<bool>,
/// Handles core launcher directory and files
pub directories: Directories,
/// Semaphore used to handle concurrent network requests
pub fetch_semaphore: FetchSemaphore,
/// Maximum number of semaphore permits to limit the amount of network requests at one time
pub fetch_semaphore_max: RwLock<u32>,
/// Semaphore used to handle concurrent IO requests
pub io_semaphore: IoSemaphore,
/// Maximum number of semaphore permits to limit the amount of IO requests at one time
pub io_semaphore_max: RwLock<u32>,
/// Handles launcher metadata via [`interpulse`]
pub metadata: RwLock<Metadata>,
/// Handles launcher configuration and settings
pub settings: RwLock<Settings>,
/// Handles minecraft auth flows and users
pub users: RwLock<MinecraftState>,
/// Handles processes
pub processor: RwLock<Processor>,
/// Handles `OneConfig` integration with [`async-tungstenite`]
pub oneconfig: RwLock<Option<OneConfig>>,
/// Handles secure credential management with [`iota_stronghold`].
pub credentials: RwLock<Option<Credentials>>,
/// Handles clusters
pub(crate) clusters: RwLock<Clusters>,
/// Handles packages
pub(crate) packages: RwLock<Packages>,
/// Handles internal ingress processes
pub(crate) ingress_processor: RwLock<IngressProcessor>,
/// Handles file system watching for cluster manager
pub(crate) watcher: RwLock<Debouncer<RecommendedWatcher>>,
/// Handles Discord rich prescence
pub discord_rpc: DiscordRPC,
}
impl State {
/// Get the current global launcher state (or initialize it)
pub async fn get() -> crate::Result<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() -> crate::Result<tokio::sync::RwLockWriteGuard<'static, Self>> {
Ok(LAUNCHER_STATE
.get_or_try_init(Self::initialize)
.await?
.write()
.await)
}
/// Initializes the core OneLauncher state fully.
#[tracing::instrument]
#[onelauncher_macros::memory]
async fn initialize() -> crate::Result<RwLock<Self>> {
let ingress =
init_ingress_internal(crate::IngressType::Initialize, 100.0, "initializing state")
.await?;
let settings = Settings::initialize(&Directories::init_settings_file()?).await?;
let directories = Directories::initalize(&settings)?;
send_ingress(&ingress, 10.0, None).await?;
let mut watcher = crate::utils::watcher::initialize_watcher()?;
let fetch_semaphore =
FetchSemaphore(RwLock::new(Semaphore::new(settings.max_async_fetches)));
let io_semaphore = IoSemaphore(RwLock::new(Semaphore::new(
settings.max_async_io_operations,
)));
send_ingress(&ingress, 10.0, None).await?;
let is_offline = !http::check_internet_connection(3).await;
// TODO: Make this run in the background, this delays launcher startup by a couple seconds
let metadata_in =
Metadata::initialize(&directories, !is_offline, &io_semaphore, &fetch_semaphore);
let clusters_in = Clusters::initialize(&directories, &mut watcher);
let users_in = MinecraftState::initialize(&directories, &io_semaphore);
let (metadata, clusters, users) = crate::ingress_join! {
Some(&ingress), 70.0, Some("loading core");
metadata_in,
clusters_in,
users_in,
}?;
send_ingress(&ingress, 5.0, Some("loading packages")).await?;
let packages = Packages::initialize(&clusters);
let ingress_processor = IngressProcessor::new();
let discord_rpc = DiscordRPC::initialize(is_offline || settings.disable_discord)?;
if !settings.disable_discord && !is_offline {
let _ = discord_rpc.apply_activity("Idling...", true).await;
}
let processor = Processor::new();
Self::status_loop();
send_ingress(&ingress, 10.0, None).await?;
Ok::<RwLock<Self>, crate::Error>(RwLock::new(Self {
offline: RwLock::new(is_offline),
directories,
fetch_semaphore,
fetch_semaphore_max: RwLock::new(settings.max_async_fetches as u32),
io_semaphore,
io_semaphore_max: RwLock::new(settings.max_async_io_operations as u32),
metadata: RwLock::new(metadata),
settings: RwLock::new(settings),
users: RwLock::new(users),
processor: RwLock::new(processor),
oneconfig: RwLock::new(None),
credentials: RwLock::new(None),
clusters: RwLock::new(clusters),
packages: RwLock::new(packages),
ingress_processor: RwLock::new(ingress_processor),
watcher: RwLock::new(watcher),
discord_rpc,
}))
}
/// Initalizes a "game loop" which checks for updates and if we are online.
pub fn status_loop() {
tokio::task::spawn(async {
loop {
let state = Self::get().await;
if let Ok(state) = state {
let _ = state.check_status().await;
}
tokio::time::sleep(Duration::from_secs(10)).await;
}
});
}
/// Updates all data if we are connected to the internet.
pub fn update() {
tokio::task::spawn(async {
let state = Self::get();
if let Ok(state) = state.await {
if !*state.offline.read().await {
let version_up = Clusters::update_versions();
let meta_up = Metadata::update();
let package_up = Clusters::update_packages();
let _ = tokio::join!(version_up, meta_up, package_up);
}
}
});
}
/// Synchronizes data that can change outside of our control.
#[tracing::instrument]
#[onelauncher_macros::memory]
pub async fn sync() -> crate::Result<()> {
let state = Self::get().await?;
let sync_settings = async {
let state = Arc::clone(&state);
tokio::spawn(async move {
let reader = state.settings.read().await;
reader.sync(&state.directories.settings_file()).await?;
Ok::<_, crate::Error>(())
})
.await?
};
let sync_clusters = async {
let state = Arc::clone(&state);
tokio::spawn(async move {
let clusters = state.clusters.read().await;
clusters.sync().await?;
Ok::<_, crate::Error>(())
})
.await?
};
tokio::try_join!(sync_settings, sync_clusters)?;
Ok(())
}
/// Resets the IO [`Semaphore`] to clear all tasks.
/// # This should ONLY be called when we can ensure
/// # that there are no current tasks running!!
pub async fn reset_io_semaphore(&self) {
let settings = self.settings.read().await;
let mut io_semaphore = self.io_semaphore.0.write().await;
let mut total_permits = self.io_semaphore_max.write().await;
// block ALL semaphore permits - this can be destructive
let _ = io_semaphore.acquire_many(*total_permits).await;
io_semaphore.close();
*total_permits = settings.max_async_io_operations as u32;
*io_semaphore = Semaphore::new(settings.max_async_io_operations);
}
/// Resets the Fetch [`Semaphore`] to clear all tasks.
/// # This should ONLY be called when we can ensure
/// # that there are no current tasks running!!
pub async fn reset_fetch_semaphore(&self) {
let settings = self.settings.read().await;
let mut fetch_semaphore = self.fetch_semaphore.0.write().await;
let mut total_permits = self.fetch_semaphore_max.write().await;
// block ALL semaphore permits - this can be destructive
let _ = fetch_semaphore.acquire_many(*total_permits).await;
fetch_semaphore.close();
*total_permits = settings.max_async_io_operations as u32;
*fetch_semaphore = Semaphore::new(settings.max_async_fetches);
}
/// Checks if we are online used in a loop.
pub async fn check_status(&self) -> crate::Result<()> {
let is_online = http::check_internet_connection(3).await;
let mut offline = self.offline.write().await;
if *offline != is_online {
return Ok(());
}
send_offline(!is_online).await?;
*offline = !is_online;
Ok(())
}
/// Determine if the global state is initalized or not (wrapper)
pub fn initalized() -> bool {
LAUNCHER_STATE.initialized()
}
}
pub use dirs::*;
pub use settings::*;
pub use state::*;

View file

@ -1,209 +1,72 @@
//! Handles user-inputted settings and global values
use super::{Directories, JavaVersions};
use crate::constants::CURRENT_SETTINGS_FORMAT_VERSION;
use onelauncher_utils::io;
use onelauncher_entity::setting_profiles;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
/// A global settings state for the launcher.
#[cfg_attr(feature = "specta", derive(specta::Type))]
use crate::{send_warning, utils::io, LauncherResult};
use super::Dirs;
#[onelauncher_macro::specta]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct Settings {
/// A `OneLauncher` theme managed by the `OneLauncher` GUI.
#[serde(default = "theme_default")]
pub theme: String,
/// A global browser list view for the `OneLauncher` GUI.
#[serde(default)]
pub browser_list_view: BrowserListView,
/// Does not ask for confirmation when closing the `OneLauncher` GUI
#[serde(default)]
pub hide_close_prompt: bool,
/// Disables animations in the `OneLauncher` GUI
#[serde(default)]
pub disable_animations: bool,
/// A global fullscreen Minecraft state.
#[serde(default)]
pub force_fullscreen: bool,
/// Whether to allow launching the same cluster under the same account.
#[serde(default)]
pub global_game_settings: setting_profiles::Model,
pub allow_parallel_running_clusters: bool,
/// Whether to launch Feral Gamemode on Linux systems.
#[serde(default)]
pub enable_gamemode: bool,
/// A global default [`Resolution`] for Minecraft.
pub resolution: Resolution,
/// A global [`JavaVersions`] list and default version.
pub java_versions: JavaVersions,
/// A global [`Memory`] settings store for Java memory settings.
pub memory: Memory,
/// Global and default initialization hooks .
pub init_hooks: InitHooks,
/// Global and default custom Java arguments.
pub custom_java_args: Vec<String>,
/// Global and default custom environment variables.
pub custom_env_args: Vec<(String, String)>,
/// Global and default maximum [`Semaphore`] I/O operations.
pub max_async_io_operations: usize,
/// Global and default maximum [`Semaphore`] HTTP operations.
pub max_async_fetches: usize,
/// The [`CURRENT_FORMAT_VERSION`] of this settings file.
pub version: u32,
/// Whether or not to disable Discord IPC.
#[serde(default)]
pub disable_discord: bool,
/// Whether or not to enable a debug mode in the launcher.
#[serde(default)]
pub debug_mode: bool,
/// Whether or not to disable Plausible and Sentry analytics.
#[serde(default)]
pub disable_analytics: bool,
/// The core global config directory stored as a [`PathBuf`].
#[serde(default = "Directories::init_settings_dir")]
pub config_dir: Option<PathBuf>,
/// Whether or not to minimize the launcher upon a game launch.
#[serde(default)]
pub hide_on_launch: bool,
/// Enable/disable custom window decorations.
#[serde(default)]
pub custom_frame: bool,
/// Completed onboarding.
#[serde(default)]
pub onboarding_completed: bool,
pub discord_enabled: bool,
pub settings_version: u32,
}
fn theme_default() -> String {
"dark".to_string()
impl Default for Settings {
fn default() -> Self {
Self {
global_game_settings: default_global_game_settings(),
allow_parallel_running_clusters: false,
discord_enabled: false,
enable_gamemode: false,
settings_version: 2,
}
}
}
impl Settings {
/// Initializes the global settings state.
#[tracing::instrument]
pub async fn initialize(file: &Path) -> crate::Result<Self> {
let mut recovered_corruption = false;
let settings = if file.exists() {
let read_settings = io::read(&file)
.await
.map_err(|err| anyhow::anyhow!("error reading settings file: {0}", err).into())
.and_then(|it| serde_json::from_slice::<Self>(&it).map_err(crate::Error::from));
if let Err(ref err) = read_settings {
tracing::error!("failed to load settings file: {err}.");
let backup = file.with_extension("json.bak");
tracing::error!(
"corrupted settings will be backed up as {}, and a new one will be created",
backup.display()
);
let _ = io::rename(file, backup).await;
recovered_corruption = true;
}
read_settings.ok()
} else {
None
};
if let Some(settings) = settings {
Ok(settings)
} else {
let settings = Self {
theme: "dark".to_string(),
browser_list_view: BrowserListView::Grid,
hide_close_prompt: true,
disable_animations: false,
force_fullscreen: false,
allow_parallel_running_clusters: false,
#[cfg(target_os = "linux")]
enable_gamemode: true,
#[cfg(not(target_os = "linux"))]
enable_gamemode: false,
resolution: Resolution::default(),
java_versions: JavaVersions::new(),
memory: Memory::default(),
init_hooks: InitHooks::default(),
custom_env_args: Vec::new(),
custom_java_args: Vec::new(),
max_async_fetches: 10,
max_async_io_operations: 10,
version: CURRENT_SETTINGS_FORMAT_VERSION,
disable_analytics: false,
disable_discord: false,
debug_mode: false,
config_dir: Directories::init_settings_dir(),
hide_on_launch: false,
custom_frame: true,
onboarding_completed: false,
};
if recovered_corruption {
settings.sync(file).await?;
}
Ok(settings)
pub async fn new() -> Self {
match Self::read().await {
Ok(settings) => settings,
Err(err) => {
send_warning!("Failed to read settings file: {}", err);
Self::default()
},
}
}
/// Synchronizes the current settings from a file.
#[tracing::instrument(skip(self))]
pub async fn sync(&self, to: &Path) -> crate::Result<()> {
io::write(to, serde_json::to_vec(self)?).await?;
async fn read() -> LauncherResult<Self> {
let path = Dirs::get().await?.settings_file();
let data = io::read(path).await?;
Ok(serde_json::from_slice(&data)?)
}
pub async fn save(&self) -> LauncherResult<()> {
let path = Dirs::get().await?.settings_file();
let data = serde_json::to_string_pretty(self)?;
io::write(path, data).await?;
Ok(())
}
}
/// `OneLauncher` browser list type.
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BrowserListView {
#[default]
Grid,
List,
}
/// Global memory settings across all clusters.
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
pub struct Memory {
/// Maximum amount of Java memory available globally.
pub maximum: u32,
/// Minimum amount of Java memory available globally.
pub minimum: u32,
}
impl Default for Memory {
fn default() -> Self {
Self {
maximum: 2048,
minimum: 256,
fn default_global_game_settings() -> setting_profiles::Model {
setting_profiles::Model {
name: "Global".into(),
force_fullscreen: Some(false),
hook_post: None,
hook_pre: None,
hook_wrapper: None,
java_id: None,
launch_args: None,
launch_env: None,
mem_max: Some(3072),
res_h: None,
res_w: None,
}
}
}
/// Global Minecraft resolution.
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
pub struct Resolution(pub u16, pub u16);
impl Default for Resolution {
fn default() -> Self {
Self(854, 480)
}
}
/// Global initialization hooks for all Minecraft clusters.
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(default)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
pub struct InitHooks {
/// Pre-launch hook.
#[serde(skip_serializing_if = "Option::is_none")]
pub pre: Option<String>,
/// Wrapper hook for the runtime of the game instance.
#[serde(skip_serializing_if = "Option::is_none")]
pub wrapper: Option<String>,
/// Post-launch hook.
#[serde(skip_serializing_if = "Option::is_none")]
pub post: Option<String>,
}

View file

@ -1,6 +1,6 @@
use std::sync::Arc;
use sqlx::SqlitePool;
use sea_orm::DatabaseConnection;
use tokio::sync::{OnceCell, RwLock};
use crate::LauncherResult;
@ -13,7 +13,7 @@ static LAUNCHER_STATE: OnceCell<RwLock<State>> = OnceCell::const_new();
pub struct State {
pub ingress_processor: IngressProcessor,
pub settings: Settings,
pub db: SqlitePool,
pub db: DatabaseConnection,
}
impl State {

View file

@ -4,17 +4,17 @@ use tokio::io::AsyncReadExt;
/// A wrapper around generic and unhelpful [`std::io::Error`] messages.
#[derive(Debug, thiserror::Error)]
pub enum IOError {
/// A wrapped [`std::io::Error`] along with the path involved in the error.
#[error("invalid absolute path '{0}'")]
InvalidAbsolutePath(std::path::PathBuf),
#[error("error acessing path: {source}, path: {path}")]
IOErrorWrapper {
#[source]
source: std::io::Error,
path: String,
},
/// A wrapped [`zip::result::ZipError`].
#[error(transparent)]
ZipError(#[from] zip::result::ZipError),
/// A wrapped [`std::io::Error`].
#[error(transparent)]
IOError(#[from] std::io::Error),
}

View file

@ -1,15 +1 @@
//! **`OneLauncher` Core Utilities**
//! Standard asynchronous utilities and wrappers for use within the launcher core.
//!
//! - [`http`]: Async extensions and wrappers around [`reqwest`] functions.
//! - [`java`]: Async utilities for managing and downloading Java versions.
//! - [`watcher`]: Async utilities for watching files with [`notify`].
pub mod http;
pub mod java;
pub mod pagination;
pub mod watcher;
pub mod crypto;
#[cfg(feature = "tauri")]
pub mod window;
pub mod io;

View file

@ -1,18 +0,0 @@
{
"name": "@onelauncher/core_new",
"version": "0.2.0-alpha.1",
"description": "Next-generation open source Minecraft launcher core",
"license": "GPL-3.0-only",
"repository": {
"type": "git",
"url": "git@github.com:polyfrost/onelauncher",
"directory": "packages/core-new"
},
"bugs": "https://github.com/polyfrost/onelauncher/issues",
"sideEffects": false,
"scripts": {
"cargo": "cargo",
"binary": "cargo run --bin",
"sqlx": "sqlx"
}
}

View file

@ -1,14 +0,0 @@
# Connecting to database on *nix
```bash
export DATABASE_URL=sqlite:/path/to/user_data.db
```
# Connecting to database on Windows
```cmd
set DATABASE_URL=sqlite:C:\\path\\to\\user_data.db
```
# Connecting to database on Windows (PowerShell)
```powershell
$env:DATABASE_URL="sqlite:C:\\path\\to\\user_data.db"
```

View file

@ -1,56 +0,0 @@
use crate::{store::{ingress::{IngressId, IngressType}, State}, LauncherResult};
#[tracing::instrument]
pub async fn init_ingress(
ingress_type: IngressType,
message: &str,
total: f64,
) -> LauncherResult<IngressId> {
let state = State::get().await?;
let processor = &state.ingress_processor;
let id = processor.create(ingress_type, message.to_string(), total).await?;
Ok(id)
}
#[tracing::instrument]
pub async fn send_ingress(id: &IngressId, increment: f64) -> LauncherResult<()> {
let state = State::get().await?;
let processor = &state.ingress_processor;
processor.send(id.0, increment).await?;
Ok(())
}
#[cfg(test)]
pub mod tests {
use std::time::Duration;
use crate::{api::{ingress::{init_ingress, send_ingress}, proxy::proxy_cli::ProxyCli}, initialize_core, store::ingress::IngressType, LauncherResult};
#[tokio::test]
pub async fn create_and_update_ingress() -> LauncherResult<()> {
assert!(cfg!(feature = "cli"));
initialize_core(ProxyCli::new()).await?;
let id = init_ingress(IngressType::Download { file_name: "Some-Mod-1.8.9.jar".into() }, "This is a test message", 100.0).await?;
tokio::time::sleep(Duration::from_millis(2350)).await;
send_ingress(&id, 10.0).await?;
tokio::time::sleep(Duration::from_millis(2500)).await;
send_ingress(&id, 20.0).await?;
tokio::time::sleep(Duration::from_millis(2500)).await;
send_ingress(&id, 70.0).await?;
tokio::time::sleep(Duration::from_millis(2500)).await;
drop(id);
Ok(())
}
}

View file

@ -1,2 +0,0 @@
pub mod ingress;
pub mod proxy;

View file

@ -1,20 +0,0 @@
pub mod message;
#[cfg(feature = "tauri")]
pub mod proxy_tauri;
#[cfg(feature = "cli")]
pub mod proxy_cli;
#[async_trait::async_trait]
pub trait LauncherProxy: Send + Sync {
async fn send_ingress(&self, ingress: crate::store::ingress::IngressPayload) -> crate::LauncherResult<()>;
async fn send_message(&self, message: message::MessagePayload) -> crate::LauncherResult<()>;
#[cfg(feature = "gui")]
fn hide_main_window(&self) -> crate::LauncherResult<()>;
#[cfg(feature = "gui")]
fn show_main_window(&self) -> crate::LauncherResult<()>;
}

View file

@ -1,24 +0,0 @@
#[derive(thiserror::Error, Debug)]
pub enum LauncherError {
#[error(transparent)]
DirError(#[from] crate::store::DirectoryError),
#[error(transparent)]
IOError(#[from] crate::utils::io::IOError),
#[error(transparent)]
IngressError(#[from] crate::store::ingress::IngressError),
#[error(transparent)]
SerdeError(#[from] serde_json::Error),
#[error(transparent)]
DbError(#[from] sqlx::Error),
#[error(transparent)]
DbMigrationError(#[from] sqlx::migrate::MigrateError),
#[error(transparent)]
AnyhowError(#[from] anyhow::Error),
#[cfg(feature = "tauri")]
#[error(transparent)]
TauriError(#[from] tauri::Error),
}
pub type LauncherResult<T> = Result<T, LauncherError>;

View file

@ -1,22 +0,0 @@
pub mod constants;
pub mod utils;
pub mod store;
pub mod api;
mod error;
mod logger;
use api::proxy::LauncherProxy;
pub use error::{LauncherResult, LauncherError};
pub use logger::start_logger;
use store::{proxy::ProxyState, Dirs, State};
pub async fn initialize_core(proxy_backend: impl LauncherProxy + 'static) -> LauncherResult<()> {
Dirs::get().await?;
start_logger().await;
ProxyState::initialize(proxy_backend).await?;
State::get().await?;
Ok(())
}

View file

@ -1,72 +0,0 @@
// Handling for the live development logging
// 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};
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(format!("{}=info", env!("CARGO_PKG_NAME"))));
tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer()
.with_ansi(true)
.with_file(true)
.with_line_number(true)
.with_level(true)
.with_thread_names(true)
.pretty()
)
.with(filter)
.with(tracing_error::ErrorLayer::default())
.init();
}
// Handling for the live production logging
// This will log to a file in the logs directory, and will not show any logs in the console
#[cfg(not(debug_assertions))]
pub async fn start_logger() {
use chrono::Local;
use std::fs::OpenOptions;
use tracing_subscriber::fmt::time::ChronoLocal;
use tracing_subscriber::prelude::*;
use crate::io::Dirs;
// Initialize and get logs directory path
let logs_dir = Dirs::get().await?.launcher_logs_dir();
let log_file_name = format!("launcher_{}.log", Local::now().format("%Y%m%d_%H%M%S"));
let log_file_path = logs_dir.join(log_file_name);
if let Err(err) = std::fs::create_dir_all(&logs_dir) {
eprintln!("Could not create logs directory: {err}");
}
let file = match OpenOptions::new()
.create(true)
.append(true)
.open(&log_file_path)
{
Ok(file) => file,
Err(e) => {
eprintln!("Could not start open log file: {e}");
return None;
}
};
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(format!("{}=info", env!("CARGO_PKG_NAME"))));
tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer()
.compact()
.with_writer(file)
.with_ansi(false)
.with_timer(ChronoLocal::rfc_3339()),
)
.with(filter)
.with(tracing_error::ErrorLayer::default())
.init();
}

View file

@ -1,33 +0,0 @@
use std::path::PathBuf;
use chrono::Duration;
use sqlx::{migrate::MigrateDatabase, sqlite::SqlitePoolOptions, Sqlite, SqlitePool};
use crate::LauncherResult;
use super::Dirs;
pub async fn create_pool_from_path(path: PathBuf) -> LauncherResult<SqlitePool> {
let protocol = String::from("sqlite:");
let connection_str = protocol + path.to_str().expect("invalid path used for sqlite connection");
if !Sqlite::database_exists(&connection_str).await? {
Sqlite::create_database(&connection_str).await?;
}
let pool = SqlitePoolOptions::new()
.max_connections(40)
.idle_timeout(Some(Duration::hours(6).to_std().unwrap()))
.connect(&connection_str).await?;
sqlx::migrate!().run(&pool).await?;
Ok(pool)
}
#[tracing::instrument]
pub async fn create_pool() -> LauncherResult<SqlitePool> {
let path = Dirs::get().await?.db_file();
create_pool_from_path(path).await
}

View file

@ -1,96 +0,0 @@
use std::path::PathBuf;
use tokio::sync::OnceCell;
use crate::{constants, utils, LauncherResult};
/// The static [`OnceCell<RwLock<Dirs>>`] for storing the global directory state.
/// Should be initialized as soon as possible (preferably before logging)
static DIRS_GLOBAL: OnceCell<Dirs> = OnceCell::const_new();
pub struct Dirs {
base_dir: PathBuf,
}
impl Dirs {
pub async fn get() -> LauncherResult<&'static Self> {
DIRS_GLOBAL.get_or_try_init(async || {
Self::initialize()
}).await
}
fn initialize() -> LauncherResult<Self> {
let base_dir = utils::io::env_path("LAUNCHER_DIR")
.or_else(|| Some(dirs::data_dir()?.join(constants::NAME)))
.ok_or(DirectoryError::BaseDir)?;
Ok(Self {
base_dir
})
}
/// Get the base directory for the launcher.
#[must_use]
pub const fn base_dir(&self) -> &PathBuf {
&self.base_dir
}
/// Get the launcher's logs directory.
#[must_use]
pub fn launcher_logs_dir(&self) -> PathBuf {
self.base_dir().join("logs")
}
// --- FILES ---
#[must_use]
pub fn db_file(&self) -> PathBuf {
self.base_dir().join("user_data.db")
}
#[must_use]
pub fn settings_file(&self) -> PathBuf {
self.base_dir().join("settings.json")
}
// --- GAME DIRECTORIES ---
/// Get the `config_dir/metadata` folder within the core config directory.
#[inline]
#[must_use]
pub fn metadata_dir(&self) -> PathBuf {
self.base_dir().join("metadata")
}
/// Get the `config_dir/metadata/libraries` folder for Minecraft libraries.
#[must_use]
pub fn libraries_dir(&self) -> PathBuf {
self.metadata_dir().join("libraries")
}
/// Get the `config_dir/metadata/natives` folder for Minecraft natives.
#[must_use]
pub fn natives_dir(&self) -> PathBuf {
self.metadata_dir().join("natives")
}
/// Get the `config_dir/metadata/java` directory.
#[must_use]
pub fn java_dir(&self) -> PathBuf {
self.metadata_dir().join("java")
}
/// Get the `config_dir/metadata/caches` directory.
#[must_use]
pub fn caches_dir(&self) -> PathBuf {
self.metadata_dir().join("caches")
}
}
#[derive(Debug, thiserror::Error)]
pub enum DirectoryError {
#[error("Failed to get the base directory for the launcher.")]
BaseDir,
}

View file

@ -1,10 +0,0 @@
pub(super) mod db;
pub mod ingress;
pub mod proxy;
mod dirs;
mod settings;
mod state;
pub use dirs::*;
pub use settings::*;
pub use state::*;

View file

@ -1,75 +0,0 @@
use serde::{Deserialize, Serialize};
use sqlx::prelude::FromRow;
use crate::{send_warning, utils::io, LauncherResult};
use super::Dirs;
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct Settings {
pub global_game_settings: SettingsProfile,
pub allow_parallel_running_clusters: bool,
pub enable_gamemode: bool,
pub discord_enabled: bool,
pub settings_version: u32,
}
impl Default for Settings {
fn default() -> Self {
Self {
global_game_settings: SettingsProfile {
name: "Global".into(),
..Default::default()
},
allow_parallel_running_clusters: false,
discord_enabled: false,
enable_gamemode: false,
settings_version: 2,
}
}
}
impl Settings {
#[tracing::instrument]
pub async fn new() -> Self {
match Self::read().await {
Ok(settings) => settings,
Err(err) => {
send_warning!("Failed to read settings file: {}", err);
Self::default()
},
}
}
async fn read() -> LauncherResult<Self> {
let path = Dirs::get().await?.settings_file();
let data = io::read(path).await?;
Ok(serde_json::from_slice(&data)?)
}
pub async fn save(&self) -> LauncherResult<()> {
let path = Dirs::get().await?.settings_file();
let data = serde_json::to_string_pretty(self)?;
io::write(path, data).await?;
Ok(())
}
}
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct SettingsProfile {
pub name: String,
pub java_id: Option<usize>,
pub res_w: Option<usize>,
pub res_h: Option<usize>,
pub force_fullscreen: Option<bool>,
pub mem_max: Option<usize>,
pub launch_args: Option<String>,
pub launch_env: Option<String>,
pub hook_pre: Option<String>,
pub hook_wrapper: Option<String>,
pub hook_post: Option<String>,
}

View file

@ -1 +0,0 @@
pub mod io;

View file

@ -1,5 +1,5 @@
[package]
name = "onelauncher_new"
name = "onelauncher"
description = "The core of OneLauncher"
version = { workspace = true }
license = { workspace = true }
@ -10,75 +10,67 @@ readme = { workspace = true }
homepage = { workspace = true }
authors = { workspace = true }
[lib]
path = "src/lib.rs"
[features]
default = [ "cli" ]
tauri = [
"dep:tauri",
"dep:tauri-specta",
"specta",
"gui"
]
default = []
# feature flag allowing us to interact with tauri (for loading animations, errors showing on gui, etc)
# also allows us to compile onelauncher core without tauri
tauri = [ "dep:tauri", "dep:tauri-specta", "specta" ]
specta = [ "dep:specta" ]
cli = [ "dep:indicatif" ]
gui = []
[lints]
workspace = true
[dependencies]
# features[default]
onelauncher_macros = { workspace = true }
onelauncher_utils = { workspace = true }
interpulse = { workspace = true }
# logging
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-error = { workspace = true }
tracing-appender = { workspace = true }
# serialization
notify = { workspace = true }
notify-debouncer-mini = { workspace = true }
iota_stronghold = { workspace = true }
zeroize = { workspace = true }
tokio = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_ini = { workspace = true }
serde_repr = { workspace = true }
serde_with = { workspace = true }
# utils
sqlx = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
paste = { workspace = true }
futures = { workspace = true }
zip = { workspace = true }
uuid = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }
chrono = { workspace = true }
reqwest = { workspace = true }
regex = { workspace = true }
sysinfo = { workspace = true }
whoami = { workspace = true }
lazy_static = { workspace = true }
rand = { workspace = true }
url = { workspace = true }
async-trait = { workspace = true }
# integration
discord-rich-presence = { workspace = true }
# crypto
dirs = { workspace = true }
sha1_smol = { workspace = true }
sha2 = { workspace = true }
murmur2 = { workspace = true }
md5 = { workspace = true }
# io
zip = { workspace = true }
async_zip = { workspace = true }
dirs = { workspace = true }
discord-rich-presence = { workspace = true }
regex = { workspace = true }
sysinfo = { workspace = true }
whoami = { workspace = true }
lazy_static = { workspace = true }
async-tungstenite = { workspace = true }
async-recursion = { workspace = true }
tokio-stream = { workspace = true }
bytes = { workspace = true }
tempfile = { workspace = true }
rand = { workspace = true }
p256 = { workspace = true }
byteorder = { workspace = true }
base64 = { workspace = true }
async-compression = { workspace = true }
dunce = { workspace = true }
flate2 = { workspace = true }
toml = { workspace = true }
url = { workspace = true }
# features[tauri]
tauri = { workspace = true, optional = true }
@ -90,5 +82,11 @@ specta = { workspace = true, optional = true }
# features[cli]
indicatif = { workspace = true, optional = true }
[build-dependencies]
dotenvy = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
winreg = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
onelauncher_gamemode = { workspace = true }

View file

@ -1,12 +1,12 @@
{
"name": "@onelauncher/macros",
"name": "@onelauncher/core_old",
"version": "0.1.0-alpha.2",
"description": "Next-generation open source Minecraft launcher macros",
"description": "Next-generation open source Minecraft launcher core",
"license": "GPL-3.0-only",
"repository": {
"type": "git",
"url": "git@github.com:polyfrost/onelauncher",
"directory": "packages/macros"
"directory": "packages/core_old"
},
"bugs": "https://github.com/polyfrost/onelauncher/issues",
"sideEffects": false,

View file

@ -0,0 +1,5 @@
use crate::store::{IngressProcessType, IngressProcessor};
pub async fn check_ingress_feeds() -> crate::Result<bool> {
IngressProcessor::finished(IngressProcessType::IngressFeed).await
}

View file

@ -0,0 +1,31 @@
//! **`OneLauncher` API**
//!
//! API for interacting with our Rust core.
pub mod cluster;
pub mod handler;
pub mod ingress;
pub mod java;
pub mod metadata;
pub mod minecraft;
pub mod package;
pub mod processor;
pub mod proxy;
pub mod settings;
pub mod data {
pub use crate::store::{
ClusterMeta, Credentials, Directories, InitHooks, JavaOptions, Loader, ManagedDependency,
ManagedPackage, ManagedUser, ManagedVersion, Memory, MinecraftCredentials, PackageData,
PackageType, Resolution, Settings,
};
}
pub mod prelude {
pub use crate::cluster::{self, create, Cluster};
pub use crate::data::*;
pub use crate::proxy::InternetPayload;
pub use crate::store::{ClusterPath, JavaVersions, PackageDependency, PackagePath};
pub use crate::utils::java::JavaVersion;
pub use crate::{java, metadata, minecraft, package, processor, settings, State};
}

View file

@ -0,0 +1,277 @@
//! Proxy for Tauri/CLI specific features and communication
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::{OnceCell, RwLock};
use uuid::Uuid;
use crate::store::{ClusterPath, IngressProcessor};
pub mod send;
pub mod utils;
pub use utils::*;
static PROXY_STATE: OnceCell<Arc<ProxyState>> = OnceCell::const_new();
pub struct ProxyState {
#[cfg(feature = "tauri")]
pub app: tauri::AppHandle<tauri::Wry>,
pub ingress_feeds: RwLock<HashMap<Uuid, Ingress>>,
}
#[derive(Serialize, Debug, Clone)]
pub struct Ingress {
pub ingress_uuid: Uuid,
pub message: String,
pub ingress_type: IngressType,
pub total: f64,
pub current: f64,
#[serde(skip)]
pub last_sent: f64,
#[cfg(feature = "cli")]
#[serde(skip)]
pub cli: indicatif::ProgressBar,
}
#[derive(Serialize, Debug, Clone)]
pub struct IngressId(Uuid);
impl Drop for IngressId {
#[clippy::has_significant_drop]
fn drop(&mut self) {
let ingress_uuid = self.0;
tokio::spawn(async move {
if let Ok(proxy_state) = ProxyState::get().await {
let mut ingress_feeds = proxy_state.ingress_feeds.write().await;
#[cfg(any(feature = "tauri", feature = "cli"))]
if let Some(ingress) = ingress_feeds.remove(&ingress_uuid) {
#[cfg(feature = "cli")]
{
let cli = ingress.cli;
cli.finish();
}
#[cfg(feature = "tauri")]
{
use tauri::Emitter;
let event = ingress.ingress_type.clone();
let fraction = ingress.current / ingress.total;
let _ = proxy_state.app.emit(
"ingress",
IngressPayload {
fraction: None,
message: "Complete".to_string(),
event,
ingress_uuid,
},
);
tracing::trace!("exited at {fraction} for ingress: {:?}", ingress_uuid);
}
}
#[cfg(not(any(feature = "tauri", feature = "cli")))]
ingress_feeds.remove(&ingress_uuid);
}
if crate::State::initalized() {
let _ = IngressProcessor::finish(
crate::store::IngressProcessType::IngressFeed,
ingress_uuid,
)
.await;
}
});
}
}
impl ProxyState {
#[cfg(feature = "tauri")]
pub async fn initialize(handle: &tauri::AppHandle<tauri::Wry>) -> crate::Result<Arc<Self>> {
PROXY_STATE
.get_or_try_init(|| async {
Ok(Arc::new(Self {
app: handle.clone(),
ingress_feeds: RwLock::new(HashMap::new()),
}))
})
.await
.cloned()
}
#[cfg(not(feature = "tauri"))]
pub async fn initialize() -> crate::Result<Arc<Self>> {
PROXY_STATE
.get_or_try_init(|| async {
Ok(Arc::new(Self {
ingress_feeds: RwLock::new(HashMap::new()),
}))
})
.await
.cloned()
}
#[cfg(feature = "tauri")]
pub async fn get_main_window() -> crate::Result<tauri::WebviewWindow> {
use tauri::Manager;
let value = Self::get().await?;
Ok(value.app.get_webview_window("main").unwrap())
}
#[allow(clippy::unused_async)]
#[cfg(feature = "tauri")]
pub async fn get() -> crate::Result<Arc<Self>> {
Ok(PROXY_STATE.get().ok_or(ProxyError::NotInitialized)?.clone())
}
#[cfg(not(feature = "tauri"))]
pub async fn get() -> crate::Result<Arc<Self>> {
Self::initialize().await
}
pub async fn list_ingress_feeds() -> crate::Result<HashMap<Uuid, Ingress>> {
let value = Self::get().await?;
let read = value.ingress_feeds.read().await;
let mut display_list: HashMap<Uuid, Ingress> = HashMap::new();
for (uuid, ingress) in read.iter() {
display_list.insert(*uuid, ingress.clone());
}
Ok(display_list)
}
}
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug, Hash, PartialEq, Eq)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum IngressType {
Initialize,
DownloadJava {
version: u32,
},
DownloadLoader {
cluster_path: PathBuf,
cluster_name: String,
},
SyncCluster {
cluster_path: PathBuf,
cluster_name: String,
},
InferPackages,
CopyCluster {
import: PathBuf,
cluster_name: String,
},
SyncConfig {
new_path: PathBuf,
},
Archival {
cluster_path: PathBuf,
cluster_name: String,
},
DownloadPackage {
cluster_path: PathBuf,
package_name: String,
icon: Option<PathBuf>,
package_id: Option<String>,
package_version: Option<String>,
},
DownloadPack {
cluster_path: PathBuf,
package_name: String,
icon: Option<String>,
package_version: String,
},
}
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
pub struct IngressPayload {
pub event: IngressType,
pub ingress_uuid: Uuid,
pub fraction: Option<f64>,
pub message: String,
}
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
pub struct OfflinePayload {
pub offline: bool,
}
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
pub struct MessagePayload {
pub message: String,
}
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
#[serde(tag = "event")]
pub enum InternetPayload {
InstallPackage { id: String },
InstallPack { id: String },
InstallPath { path: PathBuf },
}
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
pub struct ProcessPayload {
pub uuid: Uuid,
pub pid: u32,
pub event: ProcessPayloadType,
pub message: String,
}
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum ProcessPayloadType {
Started,
Modified,
Finished,
Logging,
}
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
pub struct ClusterPayload {
pub uuid: Uuid,
pub cluster_path: ClusterPath,
pub path: PathBuf,
pub name: String,
pub event: ClusterPayloadType,
}
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
#[serde(rename_all = "snake_case")]
pub enum ClusterPayloadType {
Created,
Inserted,
Synced,
Edited,
Deleted,
}
#[derive(Debug, thiserror::Error)]
pub enum ProxyError {
#[error("event state was not initialized!")]
NotInitialized,
#[error("non-existant ingress of key: {0}")]
NoIngressFound(Uuid),
#[cfg(feature = "tauri")]
#[error("Tauri error: {0}")]
TauriError(#[from] tauri::Error),
}

View file

@ -1,14 +1,11 @@
//! **`OneLauncher` Constants**
//!
//! Public constant variables and strings that are used throughout the launcher.
//! Can be shared publically, any secrets should be stored in [`crate::store::credentials`].
// =========== Core Metadata ===========
/// The name of the launcher.
pub const NAME: &str = match option_env!("LAUNCHER_NAME") {
Some(name) => name,
None => "OneLauncher",
};
pub const NAME: &str = "OneLauncher";
/// The version of the launcher (from `../Cargo.toml` env).
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
@ -41,7 +38,7 @@ pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2";
pub const MODRINTH_V3_API_URL: &str = "https://api.modrinth.com/v3";
/// The Curseforge API base url.
pub const CURSEFORGE_API_URL: &str = "https://api.curseforge.com";
/// The Curseforge API key. Reserved for use in Polyfrost projects only. Do not use in other projects without permission from the Polyfrost team.
/// The Curseforge API key. Reserved for use in OneLauncher projects only. Do not use in other projects without permission from the OneLauncher team.
pub const CURSEFORGE_API_KEY: &str = "$2a$10$6utA1UNSmFPrE/Lh7b7ndeeGmiOkjKNY8kpFB0fsmE/d42ZAfFgCe";
/// The Minecraft game ID on Curseforge.
pub const CURSEFORGE_GAME_ID: u32 = 432;
@ -51,7 +48,7 @@ pub const METADATA_API_URL: &str = "https://meta.polyfrost.org";
pub const FEATURED_PACKAGES_URL: &str = "https://polyfrost.org/meta/onelauncher/featured.json";
/// <https://mclo.gs>/ API base url.
pub const MCLOGS_API_URL: &str = "https://api.mclo.gs/1";
/// <https://skyclient.co/> metadata base url.
/// https://skyclient.co/ metadata base url.
pub const SKYCLIENT_BASE_URL: &str = "https://raw.githubusercontent.com/SkyblockClient/SkyblockClient-REPO/refs/heads/main/v1";
// =========== Paths ===========
@ -59,6 +56,10 @@ pub const SKYCLIENT_BASE_URL: &str = "https://raw.githubusercontent.com/Skyblock
///
/// [`Settings`]: crate::store::Settings
pub const SETTINGS_FILE: &str = "settings.json";
/// The public `cluster.json` file used to store the global [`Clusters`] state.
///
/// [`Clusters`]: crate::store::Clusters
pub const CLUSTER_FILE: &str = "cluster.json";
/// The public `authentication.json` file used to store the global [`MinecraftAuth`] state.
///
/// [`MinecraftAuth`]: crate::store::MinecraftAuth
@ -74,6 +75,15 @@ pub const PROCESSOR_FILE: &str = "processor.json";
/// [`Settings`]: crate::store::Settings
pub const CURRENT_SETTINGS_FORMAT_VERSION: u32 = 1;
/// The constant core caches folder.
pub const CACHES_FOLDER: &str = "caches";
/// The constant core crash reports folder.
pub const CRASH_PATH: &str = "crash-reports";
/// The constant core clusters folder.
pub const CLUSTERS_FOLDER: &str = "clusters";
/// The constant core metadata folder.
pub const METADATA_FOLDER: &str = "metadata";
// =========== Hacky Mojang-spec OS constants ===========
#[cfg(target_os = "windows")]
pub const TARGET_OS: &str = "windows";

View file

@ -0,0 +1,180 @@
//! **`OneLauncher` Errors**
//!
//! Public wrappers around common `OneLauncher` errors.
use tracing_error::InstrumentError;
/// A standardized [`thiserror::Error`] type that is used across the launcher
#[derive(thiserror::Error, Debug)]
pub enum ErrorKind {
/// Wrapper around [`tokio::sync::AcquireError`] to handle sempahore errors.
#[error("failed to acquire a tokio semaphore: {0}")]
AcquireError(#[from] tokio::sync::AcquireError),
/// Wrapper around [`tokio::sync::oneshot::error::RecvError`] to handle recieve errors.
#[error("failed to handle async reciever: {0}")]
RecvError(#[from] tokio::sync::oneshot::error::RecvError),
/// Wrapper around [`tokio::task::JoinError`] to handle tokio join handle errors.
#[error("failed to handle an async task: {0}")]
JoinError(#[from] tokio::task::JoinError),
/// Wrapper around [`flate2::CompressError`] to handle flate compression errors.
#[error("failed to compress a file with flate: {0}")]
FlateError(#[from] flate2::CompressError),
/// Wrapper around [`flate2::DecompressError`] to handle flate decompression errors.
#[error("failed to decompress a file with flate: {0}")]
DeflateError(#[from] flate2::DecompressError),
/// Wrapper around [`uuid::Error`] to handle UUID parsing errors.
#[error("failed to parse and handle uuids: {0}")]
UUIDError(#[from] uuid::Error),
/// Wrapper around [`interpulse::Error`] to handle interpulse errors.
#[error("failed to parse and handle metadata: {0}")]
MetadataError(#[from] interpulse::Error),
/// Wrapper around [`serde_json::Error`] to handle Serde JSON parsing errors.
#[error("failed to manage serialization of json: {0}")]
SerdeError(#[from] serde_json::Error),
/// Wrapper around [`serde_ini::de::Error`] to handle Serde INI parsing errors.
#[error("failed to manage deserialization of ini: {0}")]
INIError(#[from] serde_ini::de::Error),
/// Wrapper around [`reqwest::Error`] to handle HTTP errors.
#[error("failed to establish an http connection: {0}")]
HTTPError(#[from] reqwest::Error),
/// Wrapper around [`zip::result::ZipError`] to handle zip errors.
#[error("failed to manage zip files: {0}")]
ZipError(#[from] zip::result::ZipError),
/// Wrapper around [`async_zip::error::ZipError`] to handle async zip errors.
#[error("failed to manage zip files asynchronously: {0}")]
AsyncZipError(#[from] async_zip::error::ZipError),
/// Wrapper around [`anyhow::Error`] to handle generic errors.
#[error(transparent)]
AnyhowError(#[from] anyhow::Error),
/// Wrapper around [`chrono::ParseError`] to handle date parsing errors.
#[error("failed to parse and handle datetime: {0}")]
ChronoError(#[from] chrono::ParseError),
/// Wrapper around [`crate::state::DirectoryError`] to handle directory errors.
#[error("failed to get directory: {0}")]
DirectoryError(#[from] crate::store::DirectoryError),
/// Wrapper around [`crate::api::proxy::ProxyError`] to handle ingress errors.
#[error("failed to handle api communication: {0}")]
ProxyError(#[from] crate::api::proxy::ProxyError),
/// Wrapper around [`crate::store::MinecraftAuthError`] to handle Minecraft auth errors.
#[error("failed to authenticate with microsoft: {0}")]
AuthError(#[from] crate::store::MinecraftAuthError),
/// Wrapper around [`crate::api::cluster::create::CreateClusterError`] to handle Cluster creation errors.
#[error("failed to create clusters: {0}")]
CreateClusterError(#[from] crate::api::cluster::create::CreateClusterError),
/// Wrapper around [`notify:Error`] to handle file watching errors.
#[error("failed to watch file {0}")]
NotifyError(#[from] notify::Error),
/// Wrapper around [`regex::Error`] to handle `RegExp` errors.
#[error("string verification with regex failed: {0}")]
RegexError(#[from] regex::Error),
/// Wrapper around [`reqwest::header::ToStrError`] to handle header conversion errors.
#[error("failed to convert header to string: {0}")]
HeaderToStrError(#[from] reqwest::header::ToStrError),
/// Wrapper around [`async_tungstenite::tungstenite::Error`] to handle WebSocket errors.
#[error("failed to handle websocket connection: {0}")]
WebSocketError(#[from] async_tungstenite::tungstenite::Error),
/// Indicates an error with SHA checksum.
#[error("checksum {0} did not match {1}!")]
HashError(String, String),
/// Wrapper around [`onelauncher_utils::io::IOError`] to handle wrapped `std::io` errors.
#[error("error handling IO operations: {0}")]
IOError(#[from] onelauncher_utils::io::IOError),
/// Wrapper around [`std::io::Error`] to handle non-wrapped `std::io` errors.
#[error("error handling standard IO operations: {0}")]
StdIOError(#[from] std::io::Error),
/// Wrapper around [`crate::utils::java::JavaError`] to handle java errors.
#[error("failed to manage java installations: {0}")]
JavaError(#[from] crate::utils::java::JavaError),
/// Wrapper around [`crate::store::StrongholdError`] to handle stronghold errors.
#[error("failed to manage credentials with IOTA stronghold: {0}")]
StrongholdError(#[from] crate::store::StrongholdError),
/// Wrapper around [`url::ParseError`] to handle URL parsing errors
#[error("failed to parse and handle URLs: {0}")]
URLError(#[from] url::ParseError),
/// Wrapper around [`tauri::Error`] to handle Tauri errors when the feature flag is enabled
#[cfg(feature = "tauri")]
#[error("error handling tauri: {0}")]
TauriError(#[from] tauri::Error),
}
/// A safe, accessible error structure wrapper around [`ErrorKind`] for use with [`tracing_error`].
#[derive(Debug)]
pub struct Error {
/// The raw, inner [`ErrorKind`], wrapped in an [`Arc`].
pub raw: std::sync::Arc<ErrorKind>,
/// The traced [`ErrorKind`] ([`tracing_error`]) to be parsed and logged.
pub source: tracing_error::TracedError<std::sync::Arc<ErrorKind>>,
}
impl From<Error> for String {
/// Converts an [`Error`] into a [`String`] value.
fn from(value: Error) -> Self {
value.to_string()
}
}
impl std::error::Error for Error {
/// Get the source for the inner traced [`Error`].
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source.source()
}
}
impl std::fmt::Display for Error {
/// Format the [`Error`] and output it into the formatter buffer.
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(fmt, "{}", self.source)
}
}
impl<E: Into<ErrorKind>> From<E> for Error {
/// Converts an [`ErrorKind`] error into a traced [`Error`].
/// Useful for reducing error mapping and standardizing error types.
fn from(source: E) -> Self {
let error = Into::<ErrorKind>::into(source);
let boxed_error = std::sync::Arc::new(error);
Self {
raw: boxed_error.clone(),
source: boxed_error.in_current_span(),
}
}
}
impl ErrorKind {
/// Converts the current [`ErrorKind`] into a traced [`Error`].
pub fn as_error(self) -> Error {
self.into()
}
}
/// Alias for a [`core::result::Result`] with the error type [`Error`].
pub type Result<T> = anyhow::Result<T, Error>;

View file

@ -0,0 +1,20 @@
#![allow(clippy::all)]
//! # `OneLauncher`
//!
//! A library used as a core for our launcher and Rust APIs.
#[macro_use]
pub mod utils;
pub mod api;
pub mod constants;
pub mod error;
pub mod game;
pub mod logger;
pub mod store;
pub use api::proxy::{Ingress, IngressType, ProxyState};
pub use api::*;
pub use error::*;
pub use logger::start_logger;
pub use store::{InnerPathLinux, State};

View file

@ -0,0 +1,74 @@
//! **`OneLauncher` Logger**
//!
//! Public utilities for [`tracing`] logging in `OneLauncher`.
//!
//! - `RUST_LOG=xxx` is a variable which defines tracing configuration (`ERROR > WARN > INFO > DEBUG > TRACE`)
//! - `RUST_LOG="onelauncher=trace"` will show **all** mesages from `OneLauncher` and not other dependencies.
//! - `RUST_LOG="info"` will show info, warn, and error logs.
//!
//! Errors returned to [`tauri`] are displayed as tracing error logs.
//! These errors also include an attached span trace if the error is from a [`tracing_error`] and the level is info, debug, or trace.
//! Tracing errors also provide Sentry metadata to be handled.
//! To set the environment variable on `unix` you can use the following command: `RUST_LOG="xxx" {your command}`
//!
//! The default `RUST_LOG` is `onelauncher=show`, meaning only logs from `OneLauncher` will be displayed at the info, warn, and error levels.
use tracing_appender::non_blocking::WorkerGuard;
// TODO: refactor logging and add more env filters.
/// Initialize the global [`tracing`] logger in development.
#[cfg(debug_assertions)]
#[must_use]
pub fn start_logger() -> Option<WorkerGuard> {
use tracing_subscriber::prelude::*;
let filter = tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
tracing_subscriber::EnvFilter::new("onelauncher=info,onelauncher_gui=info")
});
let subscriber = tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(filter)
.with(tracing_error::ErrorLayer::default());
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
None
}
/// Initialize the global [`tracing`] logger in production.
#[cfg(not(debug_assertions))]
pub fn start_logger() -> Option<WorkerGuard> {
use crate::store::Directories;
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::fmt::time::ChronoLocal;
use tracing_subscriber::prelude::*;
let logs_dir = if let Some(directory) = Directories::logs_dir() {
directory
} else {
eprintln!("failed to start logger");
return None;
};
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("onelauncher=info"));
let file_appender = RollingFileAppender::new(Rotation::DAILY, logs_dir, "onelauncher.log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let subscriber = tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.with_ansi(false)
.with_timer(ChronoLocal::rfc_3339()),
)
.with(filter)
.with(tracing_error::ErrorLayer::default());
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
Some(guard)
}

View file

@ -0,0 +1,191 @@
//! Handles core directory management
use std::path::PathBuf;
use tokio::sync::RwLock;
use super::Settings;
use crate::constants;
/// Directory management and utilities.
#[derive(Debug)]
pub struct Directories {
/// Settings: All small caches and the core settings.json
pub settings_dir: PathBuf,
/// Base Configuration: Clusters and all Assets (should be changable as a setting that can go across file systems)
/// By default this should be the same as the `settings_dir`
pub config_dir: RwLock<PathBuf>,
/// The current working directory of the directory manager.
pub cwd: PathBuf,
}
impl Directories {
/// Bootstrap the initial settings directory on first startup.
#[must_use]
pub fn init_settings_dir() -> Option<PathBuf> {
Self::env_path("ONELAUNCHER_CONFIG")
.or_else(|| Some(dirs::data_dir()?.join(constants::NAME)))
}
/// Bootstrap the initial settings directory on first startup.
#[inline]
pub fn init_settings_file() -> crate::Result<PathBuf> {
let settings_dir = Self::init_settings_dir().ok_or(DirectoryError::ConfigDir)?;
Ok(settings_dir.join("settings.json"))
}
/// Initialize the core directory manager
#[tracing::instrument]
pub fn initalize(settings: &Settings) -> crate::Result<Self> {
let cwd = std::env::current_dir().map_err(DirectoryError::WorkingDir)?;
let settings_dir = Self::init_settings_dir().ok_or(DirectoryError::ConfigDir)?;
let config_dir = settings
.config_dir
.clone()
.ok_or(DirectoryError::ConfigDir)?;
Ok(Self {
settings_dir,
config_dir: RwLock::new(config_dir),
cwd,
})
}
/// Get the `config_dir/metadata` folder within the core config directory.
#[inline]
pub async fn metadata_dir(&self) -> PathBuf {
self.config_dir
.read()
.await
.join(constants::METADATA_FOLDER)
}
/// Get the `config_dir/metadata/libraries` folder for Minecraft libraries.
#[inline]
pub async fn libraries_dir(&self) -> PathBuf {
self.metadata_dir().await.join("libraries")
}
/// Get the `config_dir/metadata/java` directory.
#[inline]
pub async fn java_dir(&self) -> PathBuf {
self.metadata_dir().await.join("java_versions")
}
/// Get the `config_dir/metadata/caches` directory.
#[inline]
pub async fn caches_dir(&self) -> PathBuf {
self.metadata_dir().await.join("caches")
}
/// Bootstrap the core logs directory and get it as a [`PathBuf`].
#[inline]
#[must_use]
pub fn logs_dir() -> Option<PathBuf> {
Self::init_settings_dir().map(|d| d.join("logs"))
}
/// Get the `settings_dir/settings.json` file as a [`PathBuf`].
#[inline]
pub fn settings_file(&self) -> PathBuf {
self.settings_dir.join(constants::SETTINGS_FILE)
}
/// Get a [`PathBuf`] from a provided environment variable.
#[inline]
fn env_path(name: &str) -> Option<PathBuf> {
std::env::var_os(name).map(PathBuf::from)
}
/// Get the `config_dir/metadata/assets` directory.
#[inline]
pub async fn assets_dir(&self) -> PathBuf {
self.metadata_dir().await.join("assets")
}
/// Get the `config_dir/metadata/assets/indexes` directory.
#[inline]
pub async fn index_dir(&self) -> PathBuf {
self.assets_dir().await.join("indexes")
}
/// Get the `config_dir/metadata/assets/objects` directory.
#[inline]
pub async fn objects_dir(&self) -> PathBuf {
self.assets_dir().await.join("objects")
}
/// Get the `config_dir/metadata/assets/objects/{hash}` directory.
#[inline]
pub async fn object_dir(&self, hash: &str) -> PathBuf {
self.objects_dir().await.join(&hash[..2]).join(hash)
}
/// Get the `config_dir/clusters` directory.
#[inline]
pub async fn clusters_dir(&self) -> PathBuf {
self.config_dir.read().await.join("clusters")
}
/// Get the `config_dir/clusters/{uuid}` directory.
#[inline]
pub async fn cluster_dir(&self, uuid: uuid::Uuid) -> PathBuf {
self.clusters_dir().await.join(uuid.to_string())
}
/// Get the `{cluster_path}/logs` directory.
#[inline]
pub async fn cluster_logs_dir(cluster_path: &super::ClusterPath) -> crate::Result<PathBuf> {
Ok(cluster_path.full_path().await?.join("logs"))
}
/// Get the `{cluster_path}/crash_reports` directory.
#[inline]
pub async fn crash_reports_dir(cluster_path: &super::ClusterPath) -> crate::Result<PathBuf> {
Ok(cluster_path.full_path().await?.join("crash-reports"))
}
/// Get the `config_dir/metadata/resources` directory.
#[inline]
pub async fn legacy_assets_dir(&self) -> PathBuf {
self.metadata_dir().await.join("resources")
}
/// Get the `config_dir/metadata/natives` directory.
#[inline]
pub async fn natives_dir(&self) -> PathBuf {
self.metadata_dir().await.join("natives")
}
/// Get the `config_dir/metadata/natives/{version}` directory.
#[inline]
pub async fn version_natives_dir(&self, version: &str) -> PathBuf {
self.natives_dir().await.join(version)
}
/// Get the Minecraft `config_dir/metadata/versions` directory.
#[inline]
pub async fn versions_dir(&self) -> PathBuf {
self.metadata_dir().await.join("versions")
}
/// Get the Minecraft `config_dir/metadata/versions/{version}` directory.
#[inline]
pub async fn version_dir(&self, version: &str) -> PathBuf {
self.versions_dir().await.join(version)
}
/// Get the `config_dir/icons` directory.
#[inline]
pub async fn icons_dir(&self) -> PathBuf {
self.config_dir.read().await.join("icons")
}
}
/// Represents a core directory management error.
#[derive(Debug, thiserror::Error)]
pub enum DirectoryError {
/// An error for when a config directory cannot be initialized.
#[error("failed to get the user's config directory")]
ConfigDir,
/// A wrapper over [`std::io::Error`] for cwd fetching errors.
#[error("could not open working directory {0}")]
WorkingDir(std::io::Error),
}

View file

@ -0,0 +1,289 @@
//! **`OneLauncher` Store**
//!
//! Core state and storage management for the launcher, managing all states.
use std::sync::Arc;
use std::time::Duration;
use notify::RecommendedWatcher;
use notify_debouncer_mini::Debouncer;
use tokio::sync::{OnceCell, RwLock, Semaphore};
use crate::proxy::send::{init_ingress_internal, send_ingress, send_offline};
use crate::utils::http;
use crate::utils::http::{FetchSemaphore, IoSemaphore};
mod dirs;
pub use self::dirs::*;
mod minecraft;
pub use self::minecraft::*;
mod credentials;
pub use self::credentials::*;
mod discord;
pub use self::discord::*;
mod metadata;
pub use self::metadata::*;
mod settings;
pub use self::settings::*;
mod java_versions;
pub use self::java_versions::*;
mod processor;
pub use self::processor::*;
mod clusters;
pub use self::clusters::*;
mod package;
pub use self::package::*;
mod oneconfig;
pub use self::oneconfig::*;
/// The static [`OnceCell<RwLock<State>>`] for storing the global runtime launcher state.
static LAUNCHER_STATE: OnceCell<RwLock<State>> = OnceCell::const_new();
/// The public `OneLauncher` state structure.
pub struct State {
/// Handles a boolean to check if the launcher is connected to the internet
pub offline: RwLock<bool>,
/// Handles core launcher directory and files
pub directories: Directories,
/// Semaphore used to handle concurrent network requests
pub fetch_semaphore: FetchSemaphore,
/// Maximum number of semaphore permits to limit the amount of network requests at one time
pub fetch_semaphore_max: RwLock<u32>,
/// Semaphore used to handle concurrent IO requests
pub io_semaphore: IoSemaphore,
/// Maximum number of semaphore permits to limit the amount of IO requests at one time
pub io_semaphore_max: RwLock<u32>,
/// Handles launcher metadata via [`interpulse`]
pub metadata: RwLock<Metadata>,
/// Handles launcher configuration and settings
pub settings: RwLock<Settings>,
/// Handles minecraft auth flows and users
pub users: RwLock<MinecraftState>,
/// Handles processes
pub processor: RwLock<Processor>,
/// Handles `OneConfig` integration with [`async-tungstenite`]
pub oneconfig: RwLock<Option<OneConfig>>,
/// Handles secure credential management with [`iota_stronghold`].
pub credentials: RwLock<Option<Credentials>>,
/// Handles clusters
pub(crate) clusters: RwLock<Clusters>,
/// Handles packages
pub(crate) packages: RwLock<Packages>,
/// Handles internal ingress processes
pub(crate) ingress_processor: RwLock<IngressProcessor>,
/// Handles file system watching for cluster manager
pub(crate) watcher: RwLock<Debouncer<RecommendedWatcher>>,
/// Handles Discord rich prescence
pub discord_rpc: DiscordRPC,
}
impl State {
/// Get the current global launcher state (or initialize it)
pub async fn get() -> crate::Result<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() -> crate::Result<tokio::sync::RwLockWriteGuard<'static, Self>> {
Ok(LAUNCHER_STATE
.get_or_try_init(Self::initialize)
.await?
.write()
.await)
}
/// Initializes the core OneLauncher state fully.
#[tracing::instrument]
#[onelauncher_macros::memory]
async fn initialize() -> crate::Result<RwLock<Self>> {
let ingress =
init_ingress_internal(crate::IngressType::Initialize, 100.0, "initializing state")
.await?;
let settings = Settings::initialize(&Directories::init_settings_file()?).await?;
let directories = Directories::initalize(&settings)?;
send_ingress(&ingress, 10.0, None).await?;
let mut watcher = crate::utils::watcher::initialize_watcher()?;
let fetch_semaphore =
FetchSemaphore(RwLock::new(Semaphore::new(settings.max_async_fetches)));
let io_semaphore = IoSemaphore(RwLock::new(Semaphore::new(
settings.max_async_io_operations,
)));
send_ingress(&ingress, 10.0, None).await?;
let is_offline = !http::check_internet_connection(3).await;
// TODO: Make this run in the background, this delays launcher startup by a couple seconds
let metadata_in =
Metadata::initialize(&directories, !is_offline, &io_semaphore, &fetch_semaphore);
let clusters_in = Clusters::initialize(&directories, &mut watcher);
let users_in = MinecraftState::initialize(&directories, &io_semaphore);
let (metadata, clusters, users) = crate::ingress_join! {
Some(&ingress), 70.0, Some("loading core");
metadata_in,
clusters_in,
users_in,
}?;
send_ingress(&ingress, 5.0, Some("loading packages")).await?;
let packages = Packages::initialize(&clusters);
let ingress_processor = IngressProcessor::new();
let discord_rpc = DiscordRPC::initialize(is_offline || settings.disable_discord)?;
if !settings.disable_discord && !is_offline {
let _ = discord_rpc.apply_activity("Idling...", true).await;
}
let processor = Processor::new();
Self::status_loop();
send_ingress(&ingress, 10.0, None).await?;
Ok::<RwLock<Self>, crate::Error>(RwLock::new(Self {
offline: RwLock::new(is_offline),
directories,
fetch_semaphore,
fetch_semaphore_max: RwLock::new(settings.max_async_fetches as u32),
io_semaphore,
io_semaphore_max: RwLock::new(settings.max_async_io_operations as u32),
metadata: RwLock::new(metadata),
settings: RwLock::new(settings),
users: RwLock::new(users),
processor: RwLock::new(processor),
oneconfig: RwLock::new(None),
credentials: RwLock::new(None),
clusters: RwLock::new(clusters),
packages: RwLock::new(packages),
ingress_processor: RwLock::new(ingress_processor),
watcher: RwLock::new(watcher),
discord_rpc,
}))
}
/// Initalizes a "game loop" which checks for updates and if we are online.
pub fn status_loop() {
tokio::task::spawn(async {
loop {
let state = Self::get().await;
if let Ok(state) = state {
let _ = state.check_status().await;
}
tokio::time::sleep(Duration::from_secs(10)).await;
}
});
}
/// Updates all data if we are connected to the internet.
pub fn update() {
tokio::task::spawn(async {
let state = Self::get();
if let Ok(state) = state.await {
if !*state.offline.read().await {
let version_up = Clusters::update_versions();
let meta_up = Metadata::update();
let package_up = Clusters::update_packages();
let _ = tokio::join!(version_up, meta_up, package_up);
}
}
});
}
/// Synchronizes data that can change outside of our control.
#[tracing::instrument]
#[onelauncher_macros::memory]
pub async fn sync() -> crate::Result<()> {
let state = Self::get().await?;
let sync_settings = async {
let state = Arc::clone(&state);
tokio::spawn(async move {
let reader = state.settings.read().await;
reader.sync(&state.directories.settings_file()).await?;
Ok::<_, crate::Error>(())
})
.await?
};
let sync_clusters = async {
let state = Arc::clone(&state);
tokio::spawn(async move {
let clusters = state.clusters.read().await;
clusters.sync().await?;
Ok::<_, crate::Error>(())
})
.await?
};
tokio::try_join!(sync_settings, sync_clusters)?;
Ok(())
}
/// Resets the IO [`Semaphore`] to clear all tasks.
/// # This should ONLY be called when we can ensure
/// # that there are no current tasks running!!
pub async fn reset_io_semaphore(&self) {
let settings = self.settings.read().await;
let mut io_semaphore = self.io_semaphore.0.write().await;
let mut total_permits = self.io_semaphore_max.write().await;
// block ALL semaphore permits - this can be destructive
let _ = io_semaphore.acquire_many(*total_permits).await;
io_semaphore.close();
*total_permits = settings.max_async_io_operations as u32;
*io_semaphore = Semaphore::new(settings.max_async_io_operations);
}
/// Resets the Fetch [`Semaphore`] to clear all tasks.
/// # This should ONLY be called when we can ensure
/// # that there are no current tasks running!!
pub async fn reset_fetch_semaphore(&self) {
let settings = self.settings.read().await;
let mut fetch_semaphore = self.fetch_semaphore.0.write().await;
let mut total_permits = self.fetch_semaphore_max.write().await;
// block ALL semaphore permits - this can be destructive
let _ = fetch_semaphore.acquire_many(*total_permits).await;
fetch_semaphore.close();
*total_permits = settings.max_async_io_operations as u32;
*fetch_semaphore = Semaphore::new(settings.max_async_fetches);
}
/// Checks if we are online used in a loop.
pub async fn check_status(&self) -> crate::Result<()> {
let is_online = http::check_internet_connection(3).await;
let mut offline = self.offline.write().await;
if *offline != is_online {
return Ok(());
}
send_offline(!is_online).await?;
*offline = !is_online;
Ok(())
}
/// Determine if the global state is initalized or not (wrapper)
pub fn initalized() -> bool {
LAUNCHER_STATE.initialized()
}
}

View file

@ -0,0 +1,209 @@
//! Handles user-inputted settings and global values
use super::{Directories, JavaVersions};
use crate::constants::CURRENT_SETTINGS_FORMAT_VERSION;
use onelauncher_utils::io;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
/// A global settings state for the launcher.
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Settings {
/// A `OneLauncher` theme managed by the `OneLauncher` GUI.
#[serde(default = "theme_default")]
pub theme: String,
/// A global browser list view for the `OneLauncher` GUI.
#[serde(default)]
pub browser_list_view: BrowserListView,
/// Does not ask for confirmation when closing the `OneLauncher` GUI
#[serde(default)]
pub hide_close_prompt: bool,
/// Disables animations in the `OneLauncher` GUI
#[serde(default)]
pub disable_animations: bool,
/// A global fullscreen Minecraft state.
#[serde(default)]
pub force_fullscreen: bool,
/// Whether to allow launching the same cluster under the same account.
#[serde(default)]
pub allow_parallel_running_clusters: bool,
/// Whether to launch Feral Gamemode on Linux systems.
#[serde(default)]
pub enable_gamemode: bool,
/// A global default [`Resolution`] for Minecraft.
pub resolution: Resolution,
/// A global [`JavaVersions`] list and default version.
pub java_versions: JavaVersions,
/// A global [`Memory`] settings store for Java memory settings.
pub memory: Memory,
/// Global and default initialization hooks .
pub init_hooks: InitHooks,
/// Global and default custom Java arguments.
pub custom_java_args: Vec<String>,
/// Global and default custom environment variables.
pub custom_env_args: Vec<(String, String)>,
/// Global and default maximum [`Semaphore`] I/O operations.
pub max_async_io_operations: usize,
/// Global and default maximum [`Semaphore`] HTTP operations.
pub max_async_fetches: usize,
/// The [`CURRENT_FORMAT_VERSION`] of this settings file.
pub version: u32,
/// Whether or not to disable Discord IPC.
#[serde(default)]
pub disable_discord: bool,
/// Whether or not to enable a debug mode in the launcher.
#[serde(default)]
pub debug_mode: bool,
/// Whether or not to disable Plausible and Sentry analytics.
#[serde(default)]
pub disable_analytics: bool,
/// The core global config directory stored as a [`PathBuf`].
#[serde(default = "Directories::init_settings_dir")]
pub config_dir: Option<PathBuf>,
/// Whether or not to minimize the launcher upon a game launch.
#[serde(default)]
pub hide_on_launch: bool,
/// Enable/disable custom window decorations.
#[serde(default)]
pub custom_frame: bool,
/// Completed onboarding.
#[serde(default)]
pub onboarding_completed: bool,
}
fn theme_default() -> String {
"dark".to_string()
}
impl Settings {
/// Initializes the global settings state.
#[tracing::instrument]
pub async fn initialize(file: &Path) -> crate::Result<Self> {
let mut recovered_corruption = false;
let settings = if file.exists() {
let read_settings = io::read(&file)
.await
.map_err(|err| anyhow::anyhow!("error reading settings file: {0}", err).into())
.and_then(|it| serde_json::from_slice::<Self>(&it).map_err(crate::Error::from));
if let Err(ref err) = read_settings {
tracing::error!("failed to load settings file: {err}.");
let backup = file.with_extension("json.bak");
tracing::error!(
"corrupted settings will be backed up as {}, and a new one will be created",
backup.display()
);
let _ = io::rename(file, backup).await;
recovered_corruption = true;
}
read_settings.ok()
} else {
None
};
if let Some(settings) = settings {
Ok(settings)
} else {
let settings = Self {
theme: "dark".to_string(),
browser_list_view: BrowserListView::Grid,
hide_close_prompt: true,
disable_animations: false,
force_fullscreen: false,
allow_parallel_running_clusters: false,
#[cfg(target_os = "linux")]
enable_gamemode: true,
#[cfg(not(target_os = "linux"))]
enable_gamemode: false,
resolution: Resolution::default(),
java_versions: JavaVersions::new(),
memory: Memory::default(),
init_hooks: InitHooks::default(),
custom_env_args: Vec::new(),
custom_java_args: Vec::new(),
max_async_fetches: 10,
max_async_io_operations: 10,
version: CURRENT_SETTINGS_FORMAT_VERSION,
disable_analytics: false,
disable_discord: false,
debug_mode: false,
config_dir: Directories::init_settings_dir(),
hide_on_launch: false,
custom_frame: true,
onboarding_completed: false,
};
if recovered_corruption {
settings.sync(file).await?;
}
Ok(settings)
}
}
/// Synchronizes the current settings from a file.
#[tracing::instrument(skip(self))]
pub async fn sync(&self, to: &Path) -> crate::Result<()> {
io::write(to, serde_json::to_vec(self)?).await?;
Ok(())
}
}
/// `OneLauncher` browser list type.
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BrowserListView {
#[default]
Grid,
List,
}
/// Global memory settings across all clusters.
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
pub struct Memory {
/// Maximum amount of Java memory available globally.
pub maximum: u32,
/// Minimum amount of Java memory available globally.
pub minimum: u32,
}
impl Default for Memory {
fn default() -> Self {
Self {
maximum: 2048,
minimum: 256,
}
}
}
/// Global Minecraft resolution.
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
pub struct Resolution(pub u16, pub u16);
impl Default for Resolution {
fn default() -> Self {
Self(854, 480)
}
}
/// Global initialization hooks for all Minecraft clusters.
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(default)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
pub struct InitHooks {
/// Pre-launch hook.
#[serde(skip_serializing_if = "Option::is_none")]
pub pre: Option<String>,
/// Wrapper hook for the runtime of the game instance.
#[serde(skip_serializing_if = "Option::is_none")]
pub wrapper: Option<String>,
/// Post-launch hook.
#[serde(skip_serializing_if = "Option::is_none")]
pub post: Option<String>,
}

Some files were not shown because too many files have changed in this diff Show more